13.1.1 拷贝构造函数
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
合成拷贝构造函数
与默认构造函数不同的是,即使我们定义了其他构造函数,系统也会为我们生成合成拷贝构造函数(除非定义了自己的拷贝构造函数)。合成的拷贝构造函数会将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。对类类型的成员,会使用其拷贝构造函数来拷贝;内置类型的成员则直接拷贝。数组会逐元素拷贝。
拷贝初始化
string dots(10m '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-999-99999-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化
直接初始化:要求编译器使用普通的函数匹配规则来选择与我们提供的参数最匹配的构造函数。
拷贝初始化:要求编译器将右侧运算对象拷贝到正在创建的对象中,通常使用拷贝构造函数。可能需要进行一步隐式类型转换。
拷贝初始化不仅在我们用=定义变量时发生,在下列情况下也会发生:
- 将一个对象作为实参传递给一个非引用类型的形参。
- 从一个返回类型为非引用类型的函数返回一个对象。
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
参数和返回值
拷贝构造函数被用来初始化非引用类类型参数,这一特性解释了为什么拷贝构造函数自己的参数必须是引用类型。如果其参数不是引用类型,会循环反复调用拷贝构造函数。
拷贝初始化的限制
如果我们希望使用一个explicit
构造函数,就必须显式地使用。
vector<int> v1(10); // 正确,直接初始化
vector<int> v2 = 10; // 错误,vector接收大小参数的构造函数是explicit的
void f(vector<int>); // f的参数进行拷贝初始化
f(10); // 错误
f(vector<int>(10)) // 正确
13.1.2 拷贝赋值运算符
重载赋值运算符
为了与内置类型的赋值保持一致,重载赋值运算符通常返回一个指向左侧运算对象的引用。
另外值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
合成拷贝赋值运算符
如果我们不重载赋值运算符,编译器会为我们生成一个合成拷贝赋值运算符,类似默认构造函数,系统默认提供的赋值操作,但是系统默认提供的赋值操作是浅拷贝。
13.1.3 析构函数
析构函数完成什么工作
在构造函数中,成员的初始化是在函数体执行之前完成的,且按照它们在类中出现的顺序进行初始化。在析构函数中,首先执行函数体,然后销毁成员,成员按初始化顺序的逆序销毁。销毁类类型的成员需要执行成员自己的析构函数。
所以隐式销毁一个内置指针类型的成员不会delete它所指向的对象。
什么时候会调用析构函数
无论何时一个对象被销毁,就会自动调用其析构函数。
当指向一个对象的引用或指针离开作用域时,析构函数不会执行。
合成析构函数
当一个类未定义自己的析构函数时,编译器会为它定义一个合成析构函数。
13.1.4 三/五法则
需要析构函数的类也需要拷贝和赋值操作
class HasPtr
{
public:
HasPtr(const std::string &s = std::string()):
ps(new std::string (s)), i(0) { }
~HasPtr() { delete ps; }
private:
std::string *ps;
int i;
};
如果HasPtr
类使用合成的拷贝构造函数和拷贝赋值函数,这些函数简单拷贝指针成员,这意味着多个HasPtr
对象可能指向相同的内存,可能会造成double free
问题。
HasPtr f(HasPtr hp)
{
HasPtr ret = hp;
// ...
return ret;
} // f返回时,hp和ret都被销毁,会调用两次析构函数,delete两次
需要拷贝操作的类也需要赋值操作,反之亦然
考虑一个例子,一个类为每个对象分配一个独有的、唯一的序号,这个类需要拷贝构造函数来为每个新创建的对象生成一个新的、独一无二的序号,除此之外,拷贝构造函数还要从给定对象拷贝所有其它数据成员。这个类还需要自定义拷贝赋值运算符来避免将序号也赋予目的对象。
不一定需要析构函数。
13.1.15 使用=default
可以使用=default
来显式地要求编译器生成合成的版本。(只能对具有合成版本的成员函数使用=default
。)
13.1.6 阻止拷贝
定义删除的函数
我们可以通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
析构函数不能是删除的成员
对于一个删除了析构函数的类型,编译器将不允许定义该类型的变量或创建该类的临时对象。
如果一个类有某个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。
我们可以动态分配这种类型的对象,但不能释放这些对象。
struct NoDtor{
NoDtor() = default;
~NoDtor() = delete;
};
NoDtor nd; // 错误,不允许定义该类型的变量
NtDtor *p = new NoDtor(); // 正确,可以动态分配这种类型的对象
delete p; // delete会执行NoDtor的析构函数,在释放内存。 析构函数是删除的,执行不了。
合成的拷贝构造控制成员可能是删除的
如果一个类有数据成员不能默认构造、拷贝、赋值或销毁,则对应的成员函数将被定义为删除的。
具体几种情况:<< C++ Primer >> P450。
本质上,当不可能拷贝、赋值、销毁类的成员时,类的合成拷贝控制成员就被定义为删除的。
private拷贝控制
在新标准之前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private
的来阻止拷贝,并只声明不定义函数体阻止友元和成员函数进行拷贝。
现在已不推荐这种做法,推荐使用=delete
。