笔记会持续更新,有错误的地方欢迎指正,谢谢!
拷贝控制
一个类可通过定义五种特殊的成员函数来控制拷贝、赋值、移动、销毁操作。
特殊的成员函数 | 控制类的行为 |
---|---|
拷贝构造函数和移动构造函数 | 用同类型的另一个对象初始化本对象时做什么(class a(b)) |
拷贝赋值运算符和移动赋值运算符 | 将一个对象赋予同类型的另一个对象时做什么(class a = b) |
析构函数 | 此类型对象销毁时做什么 |
如果我们定义的类中没定义这些特殊的成员函数时,编译器会为我们生成它们,但是编译器生成的那些函数的行为可能不是我们想要的。这点和构造函数一样。所以,我们要认识到什么时候需要自己去定义这些操作,这往往也是实现拷贝控制最难的地方。
拷贝构造函数
拷贝构造函数的三个特性:
- 构造函数
- 第一个参数是自身类类型的引用
- 任何额外参数都有默认值
class Foo
{
public:
Foo(); // 构造函数
Foo(const Foo&); //拷贝构造函数
//参数最好是const,因为参数总是一个const的引用
//函数声明不要加explicit,因为拷贝构造函数有时会被隐式地使用。
};
拷贝构造函数的参数必是引用类型
(了解就好,可跳过)拷贝构造函数被用来初始化非引用类参数,这一特性解释了为什么拷贝构造函数的参数必须是引用类型。如果,拷贝构造函数的参数是值类型,永远得不到实参。
explicit为拷贝构造函数造成的影响
我们以vector为例,vector接受单一大小参数的构造函数是explicit的:
//变量赋值
vector<int> v1(10); //正确:直接初始化,10个0
vector<int> v2 = 10; //错误:无法隐式转换
//函数参数
void f(vector<int>); //正确
f(10); //错误
f(vector<int>(10)); //正确
所以,在拷贝构造函数中别用explicit,允许隐式转换,爱怎么用就怎么用。
合成拷贝构造函数
当一个类未定义自己的拷贝构造函数时,编译器会自动为它定义一个合成拷贝构造函数。
一般来说,合成的拷贝拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中,编译器从给定的对象中依次将每个非static成员拷贝到正在创建的对象中。
每个成员的类型决定了它如何拷贝:
- 类类型成员:调用拷贝构造函数来拷贝
- 内置类型成员:直接拷贝
- 数组:逐个元素来拷贝一个数组
用Sales_data作为例子:
class Sales_data
{
public:
Sales_data(const Sales_data&);
private:
string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
//与Sales_data的合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig) :
bookNo(orig.bookNo),//使用string的拷贝构造函数,拷贝orig.bookNo
units_sold(orig.units_sold), //拷贝orig.units_sold
revenue(orig.revenue) //拷贝orig.revenue
{ } //空函数体
拷贝初始化和直接初始化的区别
string dots(10, 's'); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
直接初始化:普通的函数匹配
拷贝初始化:拷贝构造函数或移动构造函数
拷贝初始化发生在用=定义变量、实参传递给非引用形参、返回类型为非引用、花括号列表初始化数组中的元素或聚合类中的元素、初始化标准库容器或调用其insert或push成员。
编译器可以绕过拷贝构造函数,直接创建对象
string a = "aa"; //这是调用拷贝构造函数
string b("aa"); //这样就略过了拷贝构造函数,直接初始化了
其实就是构造函数替代了拷贝构造函数。
拷贝赋值运算符
int a = 0; //拷贝构造函数
int b;
b = a; //拷贝赋值运算符
如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
重载运算符
重载运算符就是函数,只不过函数名是operator关键字后面接表示要定义的运算符的符号组成。
所以,赋值运算符就是operator=,operator=就是函数名。该函数(赋值运算符)也有参数和返回值,参数为一个与其所在类相同类型的参数,返回值为一个指向其左侧运算对象的引用。
class Foo
{
public:
Foo& operator=(const Foo&); //赋值运算符
};
合成拷贝赋值运算符
当一个类未定义自己的拷贝赋值运算符时,编译器会自动为它定义一个合成拷贝赋值运算符。
Sales_data& Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo;
units_sold = rhs.units_sold;
revenue = rhs.revenue;
return *this; //返回此对象的引用
}
总结:无论是拷贝构造函数还是拷贝赋值运算符,它们大多数是拷贝的作用,但有些情况也会用来禁止该对象的拷贝或赋值,后面会有介绍。其实,反正操作都是自己定义的,爱怎么来就怎么来呗。
析构函数
析构函数执行与构造函数相反的操作:
- 构造函数初始化对象的非static数据成员
- 析构函数释放对象使用的资源,并销毁对象的非static成员
class Foo
{
public:
~Foo(); ///析构函数,它不接受参数,没有返回值,所以不能被重载。
//一个类只有一个析构函数
};
析构函数完成什么工作
- 构造函数:成员的初始化是在函数体执行之前完成的,按出现的顺序初始化。
- 析构函数:首先执行函数体,然后销毁成员,按初始化顺序的逆序隐式销毁。
隐式销毁:成员销毁时,发生什么完全依赖于成员的类型。
所以,销毁类类型的成员(如智能指针)需要执行成员自己的析构函数,而内置类型的成员(如普通指针)没有析构函数,因此销毁内置类型成员什么都不用做。
隐式销毁一个内置指针类型的成员不会delete它所指向的对象,但智能指针会。
哪些操作不调用析构函数?
引用和值指针离开作用域不析构绑定对象。
总之,只要记得析构自己new出来的指针就好了,其他的析构都不用自己操心,系统自动调用。你要是不用new的话,你什么也不用管。
合成析构函数
当一个类未定义自己的析构函数时,编译器会自动为它定义一个合成析构函数。和合成拷贝构造函数对应,只是不存在类似构造函数中初始化列表的东西来控制成员的销毁。
class Sales_data
{
public:
~Sales_data() {} //成员会被自动销毁,所以不需要做任何事。
}
总结:
析构函数其实名不符实: 析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。说人话就是,销毁这事人家自己会去做,你要是想另外加点什么操作,就在析构函数体里面加~
三/五法则
C++并不要求我们定义所有的五个特殊的成员函数,可以只定义其中一两个。但是,有三/五法则的约束:
- 如果必须定义析构函数,那么也必须定义拷贝构造函数和拷贝赋值运算符
- 拷贝构造函数与赋值运算符共同存在,而并不意味着一定要有析构函数
使用=default
使用=default:显式要求编译器生成合成拷贝构造函数等。反正C++允许你做任何事情,毛病!
阻止拷贝
C++毛病不少,本来蛮好的拷贝构造函数、拷贝赋值运算符,现在又来个阻止拷贝。。
不过,有点道理:我不想让我的类对象被拷贝,于是我不定义拷贝构造函数,但是编译器又会自动生成,我能怎么办,只好定义一个拷贝构造函数来阻止拷贝。
阻止拷贝:定义删除的拷贝控制函数。