第十三章:拷贝控制
这一章我们学习C++的拷贝控制,所谓拷贝控制就是类如何控制该类型对象拷贝,赋值,移动或销毁时做什么。主要通过五种特殊的成员函数来控制这些操作:拷贝构造函数,拷贝赋值函数,移动构造函数,移动赋值函数,析构函数。所谓构造就是用一个同类型的对象来初始化本对象,所谓赋值就是将一个对象赋予同类型的另一个对象。如果自己不定义编译器会默认定义这些操作。
一.拷贝、赋值和销毁
如果一个构造函数的第一个参数是自身类类型的引用且其他参数都有默认值则这个构造函数为拷贝构造函数,拷贝构造函数的第一个参数必须是一个引用类型,且不应该是explicit的。
当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数。当我们使用拷贝初始化时我们要求编译器将右侧运算对象拷贝到正在创建的对象中。
拷贝初始化一般通过拷贝构造函数完成,但有时也通过移动构造函数完成。
拷贝构造函数的第一个参数必须是引用类型,否则会陷入无限循环。
编译器有可能跳过拷贝/移动构造函数直接初始化。
构造和赋值是不一样的,构造是初始化,而赋值就是赋值。
重载运算符本质上是函数,函数名由operator关键字加上要定义的运算符的符号组成。如果一个运算符是一个成员函数,其左侧运算对象就绑定到隐式的this参数,其右侧运算对象作为显示参数传递。赋值运算符通常应该返回一个指向其左侧对象的引用。
析构函数没有返回值,也不接受参数,所以不能重载。
在一个构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在一个析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。
析构函数体自身并不直接销毁成员,成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个销毁过程中,析构函数提是作为成员销毁步骤之外的另一部分进行的。
如果一个类需要自定义析构函数,那么几乎可以肯定它也需要自定义拷贝构造函数和拷贝赋值函数(例如浅拷贝)。
拷贝构造函数和拷贝赋值函数一般同时需要或不被需要自定义。
练习
13.1
如果构造函数的第一个参数是自身类类型的引用,且所有其他参数(如果有的话)都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数在以下几种情况下会被使用:
拷贝初始化(用 = 定义变量)。
将一个对象作为实参传递给非引用类型的形参。
一个返回类型为非引用类型的函数返回一个对象。
用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
初始化标准库容器或调用其 insert/push 操作时,容器会对其元素进行拷贝初始化。
13.2
拷贝构造函数的第一个参数必须是引用类型。
13.3
13.4
除了那个new,其他的都用了拷贝构造函数。
13.5
HasPtr(const HasPtr& arg) {
i = arg.i;
ps = new string(*arg.ps);
}
13.6
当对类对象进行赋值时,会使用拷贝赋值运算符。
通常情况下,合成的拷贝赋值运算符会将右侧对象的非 static 成员逐个赋予左侧对象的对应成员,这些赋值操作是由类型的拷贝赋值运算符完成的。
若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。
13.7
13.8
HasPtr& operator=(const HasPtr& rhs) {
i = rhs.i;
delete ps;
ps = new string(*rhs.ps);
return *this;
}
13.9
析构函数是释放资源的函数。
合成析构函数什么也不做
当我们没有定义析构函数的时候编译器会生成合成析构函数
13.10
13.11
~HasPtr() {
delete ps;
}
13.12
有三个临时对象,accum,item1,item2,会调用三次析构函数。
13.13
X& operator=(const X& rhs) {
cout << "X& operator(const X&)" << endl;
return *this;
}
~X() {
cout << "~X()" << endl;
}
13.14
输出相同的内容
13.15
会,因为初始化b和c的时候使用的是拷贝构造函数,但同时要注意在调用f函数的时候又会调用三次拷贝构造函数。
13.16
会改变,这样调用f的时候就不会拷贝构造了。
13.17