
上图是,无参构造函数
class Date
{
public:
Date()//无参的构造函数
{
_year = 2022;
_month = 7;
_day = 24;
}
Date(int year, int month, int day)//带参构造函数
{
_year = year;
_month = month;
_day = day;
}
void Print()//因为要多次调用打印函数,所以将它定义在类中,当作inline函数使用;
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;//注意:如果通过无参构造函数创建对象时,对象后面不用跟括号,否则就成了函数声明
d1.Print();
Date d2(2022, 7, 25);//带参
d2.Print(); //打印结果为2022/7/25
return 0;
}
构造函数的重载推荐写成全缺省的样子,这样会相对方便很多:
class Date
{
public:
Date(int year=2022, int month=7, int day=24)
{
_year = year;
_month = month;
_day = day;
}
void Print()//因为要多次调用打印函数,所以将它定义在类中,当作inline函数使用;
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
Date d2(2022, 7, 25);
d2.Print();
return 0;
}
上述代码打印结果:
//2022/7/24
//2022/7/25
可以看出,全缺省函数,不管是否手动定义,都不会报错,代码可读性和安全性提高。而且,如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成。
class Date
{
public:
//无参的构造函数
Date()
{
_year = 1;
_month = 1;
_day = 1;
}
//全缺省构造函数
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
}
在实际调用的过程中,上述代码不会报错,但是会存在歧义
实例化的d1到底是调用哪一个构造函数?并且此段代码编译阶段不会报错,但是结果会出现错误。
由此可见:它在语法上可以同时存在,但是使用存在调用的歧义,所以不能同时存在,所以一般情况下,直接写个全缺省构造函数。因为是否传参数,不管是否手动定义变量,都不会报错。传参数数量也是自己决定。
class Date
{
public:
void Print()//因为要多次调用打印函数,所以将它定义在类中,当作inline函数使用;
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
如果类中没有显式定义构造函数,则C++编译器会自动生成一个无参的默认构造函数,一旦用户显式定义编译器将不再生成,但是这里又会有一个问题,会生成随机值,上述代码打印结果为:
也就说在这里编译器生成的默认构造函数并没有什么用??
答:并不是,这跟构造函数的机制有关,C++把类型分成内置类型(基本类型)和自定义类型。内置类型就是语言提供的数据类型,如:int ,char,double...,自定义类型就是我们使用class,struct,union...等自己定义的类型。看看下面的程序,就会发现编译器生成默认的构造函数会对自定类型成员_t调用的它的默认成员函数。
class Time
{
public:
Time()
{
cout << "Time()" << endl;
_hour = 0;
_minute = 0;
_second = 0;
}
private:
int _hour;
int _minute;
int _second;
};
//注意:C++11中针对内置类型成员不初始化的缺陷,又打了补丁,即:内置类型成员变量在类中声明时可以给默认值。
class Date
{
public:
void Print()//因为要多次调用打印函数,所以将它定义在类中,当作inline函数使用;
{
cout << _year << "/" << _month << "/" << _day << endl;
}
private:
Time _t;// 自定义类型
int _year ;
int _month ;
int _day;
};
int main()
{
Date d1;
d1.Print();
return 0;
}
//打印结果会输出Time(),表明确实调用了自定义的构造函数
通过运行结果以及调试,默认构造函数对自定义类型才会处理。
2.析构函数 析构函数:与构造函数功能相反,析构函数不是完成对对象本身的销毁,局部对象销毁工作是由编译器完成的。而对象在销毁时会自动调用析构函数,完成对象中资源的清理工作 。 析构函数 是特殊的成员函数,其 特征 如下: 1. 析构函数名是在类名前加上字符 ~ 。 2. 无参数无返回值类型。 3. 一个类只能有一个析构函数。若未显式定义,系统会自动生成默认的析构函数。注意:析构函数不能重载。 4. 对象生命周期结束时, C++ 编译系统自动调用析构函数。5.编译器生成的默认析构函数,会对自定类型成员调用它的析构函数。
6.如果类中没有申请资源时,析构函数可以不写,直接使用编译器生成的默认析构函数,比如Date类;有资源申请时,一定要写,否则会造成资源泄漏,比如Stack类。
//栈类
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 7)//栈的初始化,此处可写任意值,尽量>=4
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("申请失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(DataType data)//压栈
{
_array[_size] = data;
_size++;
}
void Pop()//出栈
{
_size--;
}
void StackTop()//打印栈顶元素
{
cout << _array[_size];
}
~Stack()//栈的析构函数
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
cout << "~Stack():" << this << endl;
}
private:
DataType* _array;
int _size=16;
int _capacity=25;
};
int main()
{
Stack s;
s.Push(1);
s.Push(2);
s.Pop();
s.StackTop();
return 0;
}
上述代码中,要我们手动构建析构函数,来进行“垃圾处理”。
class Date
{
public:
Date(int year = 1, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
~Date()//这个析构函数,可写可不写,并不会出现资源的申请与浪费
{
cout << "~Date()" << endl;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
return 0;
}
上述代码中,其实并不需要用户自己构建析构函数,系统会自行调用,因为并没有出现空间的申请与浪费,若自己没有定义析构函数,系统会自动生成默认析构函数,和构造函数相同,内置类型不处理,自定义类型会去调用它的析构函数,如下:
//队列类
class MyQueue
{
public:
MyQueue(size_t capacity = 7)
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("申请失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(DataType x)//入队列
{
_array[_size] = x;
_size++;
}
void Pop()//出队列
{
_size--;
}
private:
DataType* _array;
int _size = 16;
int _capacity = 25;
Stack _S1;
Stack _s2;
};
对于MyQueue而言,其实我们不需要写它的默认构造函数,编译器对于自定义类型成员_S1和_S2会去调用它的类中的构造函数,出了作用域,编译器会针对自定义类型的成员去默认调用它的析构函数,因为有两个自定义成员(_S1和_S2),自然析构函数也调了两次,所以会输出两次Stack()…
3.拷贝构造 拷贝构造函数 : 只有单个形参 ,该形参是对本 类类型对象的引用 ( 一般常用 const 修饰 ) ,在用 已存在的类类型对象 创建新对象时由编译器自动调用int main()
{
Date d1(2022, 7, 29);
Date d2(d1);
return 0;
}
也就是说,用已经实例化过的一个类,去定义另外一个类,拷贝构造函数也是特殊的成员函数,其特征如下:
1.拷贝构造函数是构造函数的一个重载形式。
2.拷贝构造函数的参数只有一个且必须使用引用传参,使用传值方式会引发无穷递归调用。
3.若未显示定义,系统生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝我们叫做浅拷贝,或者值拷贝。
如下即为拷贝构造函数:
class Date
{
public:
Date(int year = 1900, int month = 1, int day = 1)
{
_year = year;
_month = month;
_day = day;
}
Date(const Date& d)//拷贝构造函数
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1;
Date d2(d1);
return 0;
}
针对上述特征,为什么拷贝构造采用传值传参会出现无限递归呢?
因为在复制对象时要分为两个步骤,
第一步:开辟一个临时空间;
第二步:由于临时空间是需要构造的,重新调用拷贝构造函数(无穷递归形成…)
即上述代码中,将d1拷贝d2,也就是把实参传给形参,即把实参的值拷贝给形参,而形参又会调用d1的拷贝函数来初始化自己,重复此过程,即形成无限递归;为了避免出现无限递归调用拷贝构造,所以要加上引用,引用后,d就是d1的别名,不存在传值传参。不会造成无限递归。
//栈类
typedef int DataType;
class Stack
{
public:
Stack(size_t capacity = 7)//栈的初始化,此处可写任意值,尽量>=4
{
_array = (DataType*)malloc(sizeof(DataType) * capacity);
if (_array == nullptr)
{
perror("申请失败");
return;
}
_size = 0;
_capacity = capacity;
}
void Push(DataType data)//压栈
{
_array[_size] = data;
_size++;
}
void Pop()//出栈
{
_size--;
}
void StackTop()//打印栈顶元素
{
cout << _array[_size];
}
//此处用默认的拷贝构造
~Stack()//栈的析构函数
{
if (_array)
{
free(_array);
_array = NULL;
_capacity = 0;
_size = 0;
}
cout << "~Stack():" << this << endl;
}
private:
DataType* _array;
int _size=16;
int _capacity=25;
};
int main()
{
Stack s1(16);
Stack s2(s1);
return 0;
}
上述代码会报错,原因是因为此时的s1(指针)和s2(指针)指向同一块空间,而因为类在栈中构造,秉持栈的先进后出原则,s1先构造,s2再构造,所以s2会先析构,s1再析构。不过s1指向的空间已经先被s2析构过了,而它俩指向同一块空间,该空间经过第一次s2的析构函数就已经释放了空间资源,同一块空间连两次释放空间资源,势必会出现问题。
4.运算符重载 首先,例如int、char、double等 内置类型是可以直接进行比较的,但是自定义类型是不能直接通过上述的运算符进行比较的,为了能够让自定义类型使用各种运算符,于是C++ 为了增强代码的可读性引入了运算符重载 。 运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表。其返回值类型、参数列表与普通的函数类似,函数名字为:关键字operator 后面接需要重载的运算符符号。函数名字为:关键字operator后面接需要重载的运算符符号。
函数参数:运算符操作数
函数返回值:运算符运算后结果
函数写法:返回值类型 operator操作符(参数列表)
运算符重载函数的参数由运算符决定,比如运算符"=="应有两个参数,其对应的运算符重载函数就有两个参数,单操作数运算符(++或--)就有一个参数
class Date
{
public:
//全缺省构造函数
Date(int year = 2022, int month = 8, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 8, 3);
Date d2(2021, 8, 3);
if (d1 == d2)
{
cout << "==" << endl;
}
return 0;
}
在上述代码中,运算符重载函数也被写入了日期类当中,一般情况下,都要把重载函数写入类当中,因为通常都会频繁调用该函数,所以写入类中,当作内联函数对待;还有,在上述代码中,原本参数数量是由其重载的运算符决定的,"==''相等原本需要两个运算符,但是上述代码中只有一个,但也成功运行,这是什么原理呢?原因是在于成员函数存在隐含的this指针,所以如果写两个参数,其实就是有三个参数。
编译器会处理成 bool operator(Date* const this, const Date& d)然后这两者之间进行是否相等的比较。并且,写好赋值运算符后,可以直接将自定义值当作内置类型进行运算符比较,如此时可以直接写if(d1==d2)。所以,内置函数具有如下的特点:
1 不能通过连接其他符号来创建新的操作符:比如operator@;
2 重载操作符必须有一个类类型(对自定义类型成员才可运算符重载)或者枚举类型的操作数;
3 用于内置类型的操作符,其含义不能改变,例如:内置的整型+,不能改变其含义;
4 作为类成员的重载函数时,其形参看起来比操作数数目要少1个,因为成员函数的操作符有一个默认的形参this,限定为第一个形参;
5 (1) .* (2):: (3)sizeof (4)?: (5). 注意以上5个运算符不能重载。这个经常在笔试选择题中出现。
class Date
{
public:
//全缺省构造函数
Date(int year = 2022, int month = 8, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//比较两个日期是否相等的运算符重载
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator>(const Date& d)//比较日期大小的运算符重载
{
if (_year > d._year || _month > d._month || _day > d._day)
return true;
else
return false;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 8, 3);
Date d2(2021, 8, 3);
if (d1 == d2)
{
cout << "==" << endl;
}
Date d3(2022, 8, 7);
Date d4(2022, 8, 3);
if (d3 > d4)
{
cout << ">" << endl;
}
return 0;
}
上述是比较两个日期是否相等和比较日期大小的运算符重载的运算符重载
4.1赋值运算符重载
赋值运算符重载,和拷贝构造类似,但又有些不同。拷贝构造,是拿同类型的对象去初始化另一个对象,赋值运算符重载是直接进行赋值
class Date
{
public:
//全缺省构造函数
Date(int year = 2022, int month = 8, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//比较两个日期是否相等的运算符重载
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator>(const Date& d)//比较日期大小的运算符重载
{
if (_year > d._year || _month > d._month || _day > d._day)
return true;
else
return false;
}
void operator=(const Date& d)//赋值运算符重载,但是存在问题
{
_year = d._year;
_month = d._month;
_day = d._day;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 8, 3);
Date d2(2021, 8, 3);
if (d1 == d2)
{
cout << "==" << endl;
}
Date d3(2022, 8, 7);
Date d4(2022, 8, 3);
if (d3 > d4)
{
cout << ">" << endl;
}
return 0;
}
在写赋值运算符重载时,必须要注意到以下几点:第一、要有一个返回值也就是上述代码中的return *this,不然会导致无法连等,上述代码就是无法连等。因为,把d1赋值给d2后,没有一个返回值来赋给d3,在C语言中,赋值完全可以连等,所以作为C语言的扩展的C++,势必要可以做到连等赋值,第二、参数尽量传引用传参,这样避免传值参数所带来的拷贝构造;第三、有可能会存在这样的情况:d1=d1,像这样自己给自己赋值的情况没必要调用赋值运算符重载,所以最好用if条件进行判断。即修改后的代码如下图所示。
class Date
{
public:
//全缺省构造函数
Date(int year = 2022, int month = 8, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void Print()
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//比较两个日期是否相等的运算符重载
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator>(const Date& d)//比较日期大小的运算符重载
{
if (_year > d._year || _month > d._month || _day > d._day)
return true;
else
return false;
}
Date operator=(const Date& d)//修改后的赋值运算符重载
{
if (this!= &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
private:
int _year;
int _month;
int _day;
};
int main()
{
Date d1(2021, 8, 3);
Date d2(2021, 8, 7);
if (d1 == d2)
{
cout << "==" << endl;
}
if (d2 > d1)
{
cout << ">" << endl;
}
Date d3;
d1=d3 = d2;
d3.Print();
d1.Print();
return 0;
}
赋值重载和拷贝构造一样,不写也会对内置类型完成值拷贝, 但是自定义类型不会。所以,构造和析构函数都是针对自定义类型才会处理而内置类型不会处理,而拷贝构造和赋值运算重载针对内置类型的成员会完成值拷贝(浅拷贝),不会对自定义类型浅拷贝,可以有深拷贝,这个后续会提及。
5.const函数在实际中,往往有如下情况:
因为该函数中,直接引用了实例化的d.Print(),但是d1.Print(),却不会调用失败,原因还是因为权限问题,Date类中的 Print()函数隐藏的this指针为Date* const this,
Print()函数里的const修饰this本身,this不能修改,但是this可以初始化,&d的类型为const Date*
而const Date* 指向的内容不能被修改,可是当它传给Date*时就出错了,因为Date*是可以修改的,这里传过去会导致权限放大。权限可以缩小,但是不能放大。换句话说,就是非const对象可以调用const成员函数,反之,则不行,上述代码就是const调用给了非const,所以d调用Print()函数报错。
所以,上述代码的补救措施是:
void Print() const// 编译器默认处理成:void Print(const Date* const this)
{
cout << _year << "-" << _month << "-" << _day << endl;
}
因为this指针是隐含的,不能显示的将const写出来。因此,C++为了解决此问题,允许在函数后面加上const以达到刚才的效果,const Date*传给const Date*权限不变,不会出错。建议成员函数中不修改成员变量的成员函数,都加上const,此时普通对象和const对象都可以调用。
5.2 取地址及const取地址操作符重载class Date
{
public:
//全缺省构造函数
Date(int year = 2022, int month = 8, int day = 3)
{
_year = year;
_month = month;
_day = day;
}
void Print()const
{
cout << _year << "-" << _month << "-" << _day << endl;
}
//比较两个日期是否相等的运算符重载
bool operator==(const Date& d)//编译器会处理成 bool operator(Date* const this, const Date& d)
{
return _year == d._year &&
_month == d._month &&
_day == d._day;
}
bool operator>(const Date& d)//比较日期大小的运算符重载
{
if (_year > d._year || _month > d._month || _day > d._day)
return true;
else
return false;
}
Date operator=(const Date& d)
{
if (this!= &d)
{
_year = d._year;
_month = d._month;
_day = d._day;
}
return *this;
}
//取地址&重载
Date* operator&()
{
return this;
}
//const取地址&重载
const Date* operator&()const
{
return this;
}
private:
int _year;
int _month;
int _day;
};
void Func(const Date& d)
{
d.Print();
}
int main()
{
Date d1(2021, 8, 3);
Date d2(2021, 8, 7);
if (d1 == d2)
{
cout << "==" << endl;
}
if (d2 > d1)
{
cout << ">" << endl;
}
Date d3;
d1=d3 = d2;
d3.Print();
d1.Print();
return 0;
}
自己不写&重载,编译器也会默认生成,而且这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可。