当定义一个类时,可以显式或隐式地指定在此类型的对象的进行拷贝、移动、赋值、销毁时做什么,可以通过定义五种特殊的成员函数来控制这些操作:
- 拷贝构造函数(copy constructor)
- 拷贝赋值运算符(copy-assignment operator)
- 移动构造函数(move constructor)
- 移动赋值运算符(move-assignment operator)
- 析构函数(destructor)
当没有定义这些拷贝控制成员函数时,编译器会自动为它定义缺失的操作。
class Sales_data
{
public:
Sales_data(const Sales_data&);
private:
std::string bookNo;
double revenue;
}
假设有这么一个class类型
构造函数
在一个构造函数中,成员的初始化是在构造函数函数体执行之前完成的,初始化顺序为它们在类中出现的顺序
默认构造函数
”默认“的意思不是默认的构造函数,而是会对所有成员进行赋(默认)值操作
如果没有提供,编译器会提供一个无参的默认构造函数
拷贝构造函数
class Foo
{
public:
Foo(); // 默认构造函数
Foo(const Foo&); // 拷贝构造函数
}
拷贝初始化一般使用拷贝构造函数。
如果一个类有一个移动构造函数,则拷贝初始化有时会使用移动构造函数而不是拷贝构造函数
// 自定义的拷贝构造函数,与合成构造函数等价
Sales_data::sales_data(const Sales_data &origin):
bookNo(origin.bookNo), // 使用string的拷贝构造函数
revenue(origin.revenue) // 直接拷贝基本类型
{ }
直接初始化和拷贝初始化的区别
string dots(10, '.'); // 直接初始化
string s(dots); // 直接初始化
string s2 = dots; // 拷贝初始化
string null_book = "9-9-9"; // 拷贝初始化
string nines = string(100, '9'); // 拷贝初始化
使用直接初始化**(direct initialization)**实际上就是要求编译器使用普通的函数匹配来选择与我们提供的参数最匹配的构造函数
使用**拷贝初始化(copy initialization)**时,则是要求编译器将右侧运算对象拷贝到正在创建的对象中,如果需要的话还要进行类型转换
拷贝初始化何时发生
- 使用=操作符定义变量
- 将一个对象作为实参传递给函数的(非引用类型的)形参
- 从一个返回类型为非引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
某些类型还会对他们所分配的对象使用拷贝初始化。比如初始化标准库容器或是调用其insert、push等成员函数时,容器会对其元素进行拷贝初始化,而使用emplace成员函数创建的元素都进行直接初始化
如何拷贝成员
每个成员的类型决定了如何拷贝:
- 类类型的成员:使用其拷贝构造函数进行拷贝
- 内置类型:直接拷贝
- 数组:合成拷贝构造函数会逐元素地拷贝一个数组类型的成员。而如果有自己定义的拷贝构造函数则取决于该函数的行为 如果数组元素是类类型,则又会使用元素的拷贝构造函数进行拷贝
拷贝构造函数的参数为什么必须是引用类型
在函数调用过程中,非引用类型的参数会被拷贝初始化,进而调用拷贝构造函数。而
合成拷贝构造函数(synthesized copy constructor)
如果没有为一个类定义拷贝构造函数,编译器会为我们定义一个。和默认构造函数不同的是,即使定义了其他构造函数,编译器仍会为我们合成一个拷贝构造函数
一般情况下,合成拷贝构造函数会将其参数(被拷贝对象)的成员逐个拷贝到正在创建的对象。编译器从给定对象中依次将每个非static成员拷贝到正在创建的对象中。指针类型的成员只是简单地拷贝地址(浅拷贝)
对某些类来说,合成拷贝构造函数用来阻止我们拷贝该类类型的对象
拷贝赋值操作符
重载运算符
重载运算符本质上是函数,其名字由operator
关键字后接表示要定义的运算符的符号组成。因此赋值运算符就是一个名为operator=
的函数,类似于任何其他函数,运算符函数也有一个返回类型和参数列表
重载运算符的参数表示运算符的运算对象。
某些运算符(包括赋值运算符)必须定义为成员函数。如果一个运算符是成员函数,其左侧运算对象会绑定到隐式的this参数。
重载赋值运算符
对于一个二元运算符(比如赋值运算符),其右侧运算符作为显式参数传递
为了和内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用
合成拷贝赋值运算符
合成拷贝运算符会将右侧运算对象的每个非static成员赋予左侧运算对象的对应成员,这是通过对应成员类型的拷贝赋值运算符来完成的。最后会返回一个指向其左侧运算对象的引用
// 自定义的拷贝构造函数,与合成构造函数等价
Sales_data&
Sales_data::operator=(const Sales_data &rhs)
{
bookNo = rhs.bookNo; // 调用string::operator=
revenue = rhs.revenue; // 使用内置的double赋值
return *this; // 这里对this解引用得到this指针所指的对象自己,返回一个指向该对象的引用
}
如何拷贝成员
每个成员的类型决定了如何拷贝:
- 非数组:使用对应成员类型的拷贝赋值运算符来完成
- 数组:逐元素地对成员进行赋值
其他应用
(类似于拷贝构造函数)对于某些类,合成拷贝赋值运算符用于禁止该类型对象的赋值。
拷贝赋值与拷贝构造的区别
注意以下区别,虽然都是使用了=操作符
string s1 = "test";
string s2 = null;
s2 = s1; // 这是拷贝赋值(定义后的赋值),调用拷贝赋值操作符函数
string s3 = s1; // 这是拷贝构造(发生在定义时),调用拷贝构造函数
析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可能做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员
在对象不需要使用对象后,析构函数的函数体可执行类设计者希望执行的任何收尾工作,通常在析构函数中释放对象在生存期内分配的所有资源
class Foo
{
public:
~Foo(); // 析构函数
}
析构函数不接受参数,因此不能被重载(没有函数重载)。一个类只会有唯一的一个析构函数
销毁何时发生
在一个析构函数中,先执行函数体,成员的销毁发生在函数体之后,销毁顺序为初始化顺序的逆序
无论何时,一个(非内置类型)对象被销毁,就会自动调用其析构函数:
- 变量在离开其作用域时
- 对象被销毁时,成员也被销毁
- 容器(包括标准库容器和数组)被销毁时,其所有元素都会被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁(参见12.1.2节 直接管理内存)
- 对于临时对象,当创建它的完整表达式结束时被销毁
由于析构函数自动运行,我们的程序可以按需要分配资源,而(通常)无须担心何时释放这些资源
注意:当指向一个对象的引用或指针离开作用域时,对象的析构函数不会执行
如何销毁成员
在一个析构函数中,不存在类似构造函数中初始化列表的东西来控制成员如何销毁,析构部分是隐式的。成员销毁时发生什么完全依赖于成员的类型。
- 销毁class类型的成员需要执行成员自己的析构函数
- 销毁内置类型成员什么也不会做,因为内置类型没有析构函数 注意:隐式销毁一个内置指针类型的成员不会delete它指向的对象。 与普通指针不同,智能指针是class类型,具有析构函数,因此智能指针的成员在析构阶段会被自动销毁。