4.拷贝构造函数
4.1概念与特征
在定义内置类型的时候,我们有时候会使用类似int a = b这样的语句,这就是一种拷贝,对于自定义类型,我们可以直接拷贝,这种拷贝我们把它叫做浅拷贝,还有一种拷贝叫做深拷贝,深拷贝就是创建一个新的对象和数组,将原对象的各项属性的“值”(数组的所有元素)拷贝过来,是“值”而不是“引用”。
我们在实例化一个新的对象的时候,经常可能会遇到这种我们想拷贝已有对象的数据,这时候会用到深拷贝,所以引入了拷贝构造函数的概念。
拷贝构造函数:只有单个形参,该形参是对本类类型对象的引用(一般常用const修饰),在用已存在的类类型对象创建新对象时由编译器自动调用。
拷贝构造的特征:
1、拷贝构造函数是构造函数的一个重载形式。
2、拷贝构造函数的参数只有一个且必须是类类型对象的引用,使用传值方式编译器直接报错,因为会引发无穷递归调用。
3、若未显式定义,编译器会生成默认的拷贝构造函数。 默认的拷贝构造函数对象按内存存储按字节序完成拷贝,这种拷贝叫做浅拷贝,或者值拷贝。
4、编译器自动生成的默认拷贝构造函数中,内置类型是按照字节方式直接拷贝的,而自定义类型是调用其拷贝构造函数完成拷贝的
总结:类中如果没有涉及资源申请时,拷贝构造函数是否写都可以;一旦涉及到资源申请时,则拷贝构造函数是一定要写的,否则就是浅拷贝。
5、拷贝构造函数典型调用场景:
- 使用已存在对象创建新对象
- 函数参数类型为类类型对象
- 函数返回值类型为类类型对象
4.2特征分析
以date类为例
class date { public: void print() { cout << _year << '/' << _month << '/' << _day << endl; } date(int year = 1970, 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; };
拷贝构造函数是构造函数的一个重载形式,拷贝构造的函数参数与构造函数不同**(特性1),由于我们对被拷贝的对象不需要改变它的值,为了安全方面考虑,在参数列表中加上const。拷贝构造的参数类型是类引用,否则就会引发无穷递归调用。(特性2)**
5.运算符重载
5.1运算符重载的概念
在C++中,我们会定义很多个类并且实例化对象,然而我们自定义的这些类对于编译器是陌生的,所以一些操作符对于自定义的类型无法识别,为了增强代码的可读性,C++引入了运算符重载的概念。
运算符重载是具有特殊函数名的函数,也具有其返回值类型,函数名字以及参数列表,其返回值类型与参数列表与普通的函数类似。
函数名为:关键字operator后面接需要重载的运算符符号,例如需要重载+=,那么函数名就是"operator+=",
函数原型:**返回值类型 operator操作符(参数列表) **
注意
- 不能通过连接其他符号来创建新的操作符:比如operator@
- 重载操作符必须有一个类类型参数
- 用于内置类型的运算符,其含义不能改变,例如:内置的整型+,不能改变其含义
- 作为类成员函数重载时,其形参看起来比操作数数目少1,因为成员函数的第一个参数为隐藏的this
.*
::
sizeof
?:
.
注意以上5个运算符不能重载。
以重载日期类的日期+天数为例:
Date类
class Date { public: Date(int year = 1970, int month = 1, int day = 1) { _year = year; _month = month; _day = day; } //获取每个月的天数 int GetMonthDay(int year, int month) { static int day[13] = { 0,31,28,31,30,31,30,31,31,30,31,30,31 }; if ((month == 2) && ((year % 4 == 0 && year % 100 != 0) || (year % 400 == 0))) //闰年 return 29; return day[month]; } //打印 void Print() { cout << _year << "/" << _month << "/" << _day << endl; } private: int _year; int _month; int _day; };
重载:
void operator+=(Date& d, int day) { d._day += day; while (d._day > GetMonthDay(d._year, d._month)) { d._day -= GetMonthDay(d._year, d._month); d._month++; if (d._month > 12) { d._month -= 12; d._year++; } } }
但是,在我们上手去写的时候就会发现,我们在类外面写运算符重载的话,不能直接访问类里面的private成员变量,所以我们推荐把运算符重载写在类里面,由于类里面的成员函数的第一个参数都是隐藏的this指针,所以写在类里面的运算符重载的参数通常会比运算符少一个操作数,所以在类里面的日期+天数重载代码为:
//运算符重载+= void operator+=(int day) //只传递右操作数,通过this操作左操作数 { this->_day += day; //这里的this->编译器会自动添加 while (_day > GetMonthDay(_year, _month)) { _day -= GetMonthDay(_year, _month); _month++; if (_month > 12) { _month -= 12; _year++; } } }
5.2赋值运算符重载
赋值重载既是默认成员函数,又是运算符重载。
5.2.1特性
- 赋值重载的格式规范;
- 赋值运算符只能重载成类的成员函数不能重载成全局函数;
- 若未显式定义,编译器会生成默认的赋值重载函数;
- 默认的赋值重载函数对内置类型以字节为单位直接进行拷贝 – 浅拷贝,对自定义类型调用其自身的赋值重载函数;
5.2.2特性分析
1.函数格式:
赋值重载函数的格式一般有如下要求:
使用引用做参数,并以 const 修饰
我们知道,使用传值传参时函数形参是实参的一份临时拷贝,所以传值传参会调用拷贝构造函数;而使用引用做参数时,形参是实参的别名,从而减少了调用拷贝构造在时间和空间上的消耗;另外,赋值重载只会改变被赋值对象,而不会改变赋值对象,所以我们使用 const 来防止函数内部的误操作;
void operator=(const Date& d);
使用引用做返回值且返回值为*this
我们可以对内置类型进行连续赋值,,比如 int i,j; i = j = 0;
那么对于自定义类型来说,我们也可以使用运算符重载来让其支持连续赋值,则重载函数就必须具有返回值;同时,由于我们是在函数外部调用重载函数,所以重载函数调用结束后该对象仍然存在,那么我们就可以使用引用作为函数的返回值,从而减少一次返回值的拷贝,提高程序效率;
另外,我们一般使用左操作数作为函数的返回值,也就是 this 指针指向的对象;
Date& operator=(const Date& d);
检测是否自己给自己赋值
用户在调用成员函数时有可能发生下面这种情况:Date d1; Date& d2 = d1; d1 = d2;
这种情况对于只需要浅拷贝的对象来说并没有什么大碍,但对于有资源申请,需要进行深拷贝的对象来说就会发生不可控的事情,具体案例我们在后文中讲解;
在 《Effective C++》中对赋值重载函数自我赋值的解释是这样的:
2.重载为成员函数:
赋值运算符只能重载成类的成员函数不能重载成全局函数,这是因为赋值重载函数作为六个默认成员函数之一,如果我们不显示实现,编译器会默认生成;此时用户如果再在类外自己实现一个全局的赋值运算符重载,就会和编译器在类中生成的默认赋值运算符重载冲突,从而造成链接错误。
3.深浅拷贝:
赋值重载函数的特性和拷贝构造函数非常类似 – 如果我们没有显式定义赋值重载,则编译器会自动生成一个赋值重载,且自动生成的函数对内置类型以字节为单位直接进行拷贝,对自定义类型会去调用其自身的赋值重载函数;所以对于没有资源申请的类来说,我们不用自己去写赋值重载函数,直接使用默认生成的即可,因为这种类只需要进行浅拷贝 (值拷贝),比如 Date 类;而对于有资源申请的类来说,我们必须自己手动实现赋值重载函数,来完成深拷贝工作;比如 Stack 类;
**注:**拷贝构造函数完成的是初始化工作,在创建对象时自动调用;赋值重载完成的是已存在的对象之间的拷贝,需要手动调用;
**总结:**自动生成的赋值重载函数对成员变量的处理规则和析构函数一样 – 对内置类型以字节方式按值拷贝,对自定义类型调用其自身的赋值重载函数;我们可以理解为:需要写析构函数的类就需要写赋值重载函数,不需要写析构函数的类就不需要写赋值重载函数;
6.const成员&&取地址及const取地址操作符重载
6.1const成员
我们看下面一个例子:
class date { public: void print() { cout << _year << '/' << _month << '/' << _day << endl; } date(int year = 1970, 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(2022,12,1); const date d2(d1); d1.print(); d2.print(); return 0; }
这段代码运行会报以下错误,原因是我们在调用d2.print()
的时候默认的this指针参数的了类型是date* const this
,this指向的值是可修改的,但是实际上d2是const修饰的对象,所以会造成权限的放大。
要解决这类问题的话,我们就需要把this的类型改为const date* const this
,但是this指针是隐藏的,所以C++提供了一种新的方式,就是在函数参数的括号后面加const表示修饰*this的const。
void print() const
6.2取地址及const取地址操作符重载
这两个默认成员函数一般不用重新定义 ,编译器默认会生成。如果一定要自己定义的话,那么代码是这样的:
class date { public: date* operator&() { return this; } const date* operator&() const { return this; } private: int _year; int _month; int _day; };
这两个运算符一般不需要重载,使用编译器生成的默认取地址的重载即可,只有特殊情况,才需要重载,比如想让别人获取到指定的内容
7、总结
C++的类里面存在六个默认成员函数 – 构造、析构、拷贝构造、赋值重载、取地址重载、const 取地址重载,其中前面四个函数非常重要,也非常复杂,需要我们根据具体情况判断是否需要显式定义,而最后两个函数通常不需要显示定义,使用编译器默认生成的即可;
1、构造函数
- 构造函数完成对象的初始化工作,由编译器在实例化对象时自动调用;
- 默认构造函数是指不需要传递参数的构造函数,一共有三种 – 编译器自动生成的、显式定义且无参数的、显式定义且全缺省的;
- 如果用户显式定义了构造函数,那么编译器会根据构造函数的内容进行初始化,如果用户没有显式定义,那么编译器会调用默生成的构造函数;
- 默认生成的构造函数对内置类型不处理,对自定义类型会去调用自定义类型的默认构造;
- 为了弥补构造函数对内置类型不处理的缺陷,C++11打了一个补丁 – 允许在成员变量声明的地方给缺省值;如果构造函数没有对该变量进行初始化,则该变量会被初始化为缺省值;
- 构造函数还存在一个初始化列表,初始化列表的存在有着非常大的意义;
2、析构函数
- 析构函数完成对象中资源的清理工作,由编译器在销毁对象时自动调用;
- 如果用户显式定义了析构函数,编译器会根据析构函数的内容进行析构;如果用户没有显示定义,编译器会调用默认生成的析构函数;
- 默认生成的析构函数对内置类型不处理,对自定义类型会去调用自定义类型的析构函数;
- 如果类中有资源的申请,比如动态开辟空间、打开文件,那么需要我们显式定义析构函数;
3、拷贝构造
- 拷贝构造函数是用一个已存在的对象去初始化另一个正在实例化的对象,由编译器在实例化对象时自动调用;
- 拷贝构造的参数必须为引用类型,否则编译器报错 – 值传递会引发拷贝构造函数的无穷递归;
- 如果用户显式定义了拷贝构造函数,编译器会根据拷贝构造函数的内容进行拷贝;如果用户没有显示定义,编译器会调用默认生成的拷贝构造函数;
- 默认生成的拷贝构造函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的拷贝构造函数;
- 当类里面有空间的动态开辟时,直接进行值拷贝会让两个指针指向同一块动态内存,从而使得对象销毁时对同一块空间析构两次;所以这种情况下我们需要自己显式定义拷贝构造函数完成深拷贝;
4、运算符重载
- 运算符重载是C++为了增强代码的可读性而引入的语法,它只能对自定义类型使用,其函数名为 operator 关键字加相关运算符;
- 由于运算符重载函数通常都要访问类的成员变量,所以我们一般将其定义为类的成员函数;同时,因为类的成员函数的一个参数为隐藏的 this 指针,所以其看起来会少一个参数;
- 同一运算符的重载函数之间也可以构成函数重载,比如 operator++ 与 operator++(int);
5、赋值重载
- 赋值重载函数是将一个已存在对象中的数据赋值给另一个已存在的对象,注意不是初始化,需要自己显示调用;它属于运算符重载的一种;
- 如果用户显式定义了赋值重载函数,编译器会根据赋值重载函数的内容进行赋值;如果用户没有显示定义,编译器会调用默认生成的赋值重载函数;
- 默认生成的赋值重载函数对于内置类型完成值拷贝 (浅拷贝),对于自定义类型会去调用自定义类型的赋值重载函数;
- 赋值重载函数和拷贝构造函数一样,也存在着深浅拷贝的问题,且其与拷贝构造函数不同的地方在于它还很有可能造成内存泄漏;所以当类中有空间的动态开辟时我们需要自己显式定义赋值重载函数来释放原空间以及完成深拷贝;
- 为了提高函数效率与保护对象,通常使用引用作参数,并加以 const 修饰;同时为了满足连续赋值,通常使用引用作返回值,且一般返回左操作数,即 *this;
- 赋值重载函数必须定义为类的成员函数,否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突;
6、const 成员函数
- 由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题,所以 const 类型的对象不能调用成员函数,因为成员函数的 this 指针默认是非 const 的,二者之间传参存在权限扩大的问题;
- 同时我们为了提高函数效率以及保护对象,一般都会将成员函数的第二个参数使用 const 修饰,这就导致了该对象在成员函数内也不能调用其他成员函数;
- 为了解决这个问题,C++设计出了 const 成员函数 – 在函数最后面添加 const 修饰,该 const 只修饰 this 指针,不修饰函数的其他参数;
- 所以如果我们在设计类时,只要成员函数不改变第一个对象,我们建议最后都使用 const 修饰;
7、取地址重载与 const 取地址重载
- 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址,需要自己显式调用;它们属于运算符重载,同时它们二者之间还构成函数重载;
- 大多数情况下我们都不会去显示实现这两个函数,使用编译器默认生成的即可;只有极少数情况需要我们自己定义,比如防止用户获取到一个对象的地址;
- 员函数,否则编译器默认生成的赋值重载会与类外自定义的赋值重载冲突;
6、const 成员函数
- 由于指针和引用传递参数时存在权限的扩大、缩小与平移的问题,所以 const 类型的对象不能调用成员函数,因为成员函数的 this 指针默认是非 const 的,二者之间传参存在权限扩大的问题;
- 同时我们为了提高函数效率以及保护对象,一般都会将成员函数的第二个参数使用 const 修饰,这就导致了该对象在成员函数内也不能调用其他成员函数;
- 为了解决这个问题,C++设计出了 const 成员函数 – 在函数最后面添加 const 修饰,该 const 只修饰 this 指针,不修饰函数的其他参数;
- 所以如果我们在设计类时,只要成员函数不改变第一个对象,我们建议最后都使用 const 修饰;
7、取地址重载与 const 取地址重载
- 取地址重载与 const 取地址重载是获取一个对象/一个只读对象的地址,需要自己显式调用;它们属于运算符重载,同时它们二者之间还构成函数重载;
- 大多数情况下我们都不会去显示实现这两个函数,使用编译器默认生成的即可;只有极少数情况需要我们自己定义,比如防止用户获取到一个对象的地址;