拷贝构造函数、拷贝赋值运算符
移动构造函数、移动赋值运算符
析构函数
当定义一个类时,我们显式和隐式地指定在此类型对象的拷贝、移动、赋值和销毁时做些什么。而一个类通过定义上述五种特殊的成员函数来控制这些操作。
拷贝和移动构造函数定义了当同类型的另一个对象初始化本对象时做什么。
拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。
析构函数定义了当此类型对象销毁时做什么。
1、拷贝构造函数
class Foo {
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
};拷贝构造函数的第一个参数是引用类型,而且此参数几乎总是一个const的引用。
合成拷贝构造函数(synthesized copy constructor)
如果我们没有定义拷贝构造函数,编译器则会为我们定义一个合成拷贝构造函数。
一般情况下,合成拷贝构造函数将其参数的成员逐个拷贝到正在创建的对象中。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。
每个成员的类型决定其如何拷贝:
类类型的成员---------调用其拷贝构造函数来拷贝
内置类型-------------直接拷贝
拷贝初始化
拷贝初始化和直接初始化的差异
string dots(10, ' '); //直接初始化
string s(dots); //直接初始化
string s2 = dots; //拷贝初始化
string book = "9-999-99999-9"; //拷贝初始化
string nines = string(100, '9'); //拷贝初始化当使用直接初始化时,我们实际上是要求编译器使用普通的函数匹配来选择与我们提供参数最匹配的构造函数。
而使用构造初始化时,我们要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换。
拷贝初始化通常使用拷贝构造函数完成,有时也使用移动构造函数完成。
拷贝初始化除了在我们用 = 定义变量时会发生,在下列情况也会发生
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
例如:在函数调用过程中i,具有非引用类型的参数要进行拷贝初始化,类似的,当一个函数返回具有非引用的返回类型时也会。
2、拷贝赋值运算符
与类控制其对象如何初始化一样,类也可以控制其对象如何赋值。
Sales_data trans, accum;
trans = accum; //使用Sales_data的拷贝赋值运算符合成拷贝赋值运算符如果一个类未定义自己的拷贝赋值运算符,编译器会自动为其生成一个合成拷贝赋值运算符。它会将右侧运算对象的每一个非static成员赋予左侧运算对象。
合成拷贝赋值运算符返回一个指向其左侧运算对象的引用。
//等价于合成拷贝赋值运算符
Sales_data&
Sales_data::operator=(const Sales_data &rhs) {
bookNO = rhs.bookNO; //调用string::operator=
units_sold = rhs.units_sold; //使用内置的int赋值
revenue = rhs.revenue;
return *this; //返回一个此对象的引用
}3、析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他的工作:析构函数释放对象使用的资源,并销毁对象的非static数据成员。
如同构造函数有一个初始化部分和一个函数体,析构函数也有一个函数体和一个析构部分。在一个构造函数中,成员的初始化是在函数体之前完成的,且按照它们在类中出现的顺序初始化。在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化的顺序逆序销毁。
无论何时一个对象被销毁,就会自动调用其析构函数:
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准库容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
合成析构函数
当一个类未定义自己的析构函数时,编译器会自动为它分配一个合成的析构函数
class Sales_data {
public:
//成员会被自动销毁,除此之外不需要做其他事情
~Sales_data() { }
//其他成员的定义,如前
};认识到析构函数体自身并不直接销毁成员是非常重要的。成员是在析构函数体之后隐含的析构阶段中被销毁的。在整个对象销毁的过程中,析构函数体是作为成员销毁步骤之外的另一部分进行的。
4、三五法则
- 需要析构函数的类也需要拷贝和赋值操作
- 需要拷贝操作的类也需要赋值操作,反之亦然
5、使用 = default
我们通过将拷贝控制成员定义为 =default 来显式地要求编译器生成合成的版本。
class Sales_data {
public:
//拷贝控制成员,使用 =default
Sales_data() = default;
Sales_data(const Sales_data &) = default;
Sales_data&operator=(const Sales_data &);
~Sales_data() = default;
//其他成员定义,如前
};
Sales_data& Sales_data::operator=(const Sales_data &) = default;6、阻止拷贝
对于某些类来说,拷贝操作没有合理的意义,在此情况下,定义类时必须采用某种机制阻止拷贝或赋值。例如,iostream类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。阻止拷贝,除了不定义拷贝控制成员外,还要考虑编译器合成的版本。
定义删除的函数
在新标准下,我们通过将拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。
struct NoCopy {
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator=(const NoCopy&) = delete; //阻止赋值
~NoCopy() = default;
//其他成员
};析构函数不能是删除的成员
值得注意的是,我们不能删除析构函数。如果析构函数被删除,就无法销毁此类型的对象了。对于一个删除了析构函数的类型,编译器不允许定义该类型的变量或创建该类型的对象。而且,如果一个类有一个成员的类型删除了析构函数,我们也不能定义该类的变量或临时对象。
合成的拷贝控制成员可能是删除的
对下列情况的类,编译器将合成的成员定义为删除的函数:
- 如果类的某个成员的析构函数是删除的或不可访问的(例如,是private的),则类的合成析构函数定义为删除的。
- 如果类的某个成员的拷贝构造函数是删除的或不可访问的,则类的合成构造函数定义为删除的。如果类的某个成员的析构函数是删除的或不可访问的,则类的合成构造函数定义为删除的。
- 如果类的某个成员的拷贝赋值运算符是删除的或不可访问的,或类有一个const的或引用成员,则类的合成拷贝赋值运算符定义为删除的。
- 如果类的某个成员的析构函数是删除的或不可访问的,或类有一个引用成员,它没有类内初始化器,或类有一个const成员,它没有类内初始化器且类型未显式定义默认构造函数,则类的默认构造函数定义为删除的。
本质上,这些规则的含义是,如果一个类有数据成员不能默认构造、拷贝、复制或销毁,则对应的成员函数将被定义为删除的。
private拷贝控制
在新标准发布以前,类是通过将其拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝的。
764

被折叠的 条评论
为什么被折叠?



