类的拷贝控制操作
一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数、拷贝赋值运算符、移动构造函数、移动赋值运算符和析构函数。如果一个类没有显示地定义这些函数,编译器会自动帮你定义。
拷贝、赋值与销毁
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都有默认值,则此构造函数是拷贝构造函数。
例子:
class Foo
{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
}
详细说明下列几点:
- 拷贝构造函数的第一个参数必须是一个引用类型。
- 虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引用。
- 拷贝构造函数通常不应该是explicit的。
如果没有为一个类定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数。
例子:
class Sales_data
{
public:
Sales_data(const Salse_data&); //与合成的拷贝构造函数等价的拷贝构造函数的声明
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
//与Sales_data合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), //使用string 的拷贝构造函数
units_sold(orig.units_sold), //拷贝orig.units_sold
revenue(orig.revenue) //拷贝orig.revenue
{ } //空函数体
详细说明下列几点:
- 每个成员函数决定了它如何拷贝:对类类型的成员,会使用拷贝构造函数来拷贝。
- 内置类型的成员直接拷贝。
- 数组会逐元素地拷贝一个数组类型的成员。
- 编译器可以略过拷贝构造函数
string null_book = "1234"; //拷贝初始化 string null_book("1234");//编译器略过了拷贝构造函数
1. 拷贝赋值运算符
例子:
class Sales_data
{
public:
//与合成的拷贝构造函数等价的拷贝构造函数的声明
Sales_data(const Salse_data&);
//拷贝赋值运算符
Sales_data& operator=(const Sales_data&);
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
//与Sales_data合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), //使用string 的拷贝构造函数
units_sold(orig.units_sold), //拷贝orig.units_sold
revenue(orig.revenue) //拷贝orig.revenue
{ } //空函数体
2. 析构函数
析构函数执行与构造函数相反的操作:构造函数初始化对象的非static数据成员,还可以做一些其他工作;析构函数释放对象使用的资源,并销毁对象的非static数据成员。
析构函数是类的一个成员函数,名字由波浪号接类名构成。它没有返回值,也不接受参数。
例子:
class Sales_data
{
public:
//与合成的拷贝构造函数等价的拷贝构造函数的声明
Sales_data(const Salse_data&);
//拷贝赋值运算符
Sales_data& operator=(const Sales_data&);
//析构函数
~Sales_data();
private:
std::string bookNo;
int units_sold = 0;
double revenue = 0.0;
};
//与Sales_data合成的拷贝构造函数等价
Sales_data::Sales_data(const Sales_data &orig):
bookNo(orig.bookNo), //使用string 的拷贝构造函数
units_sold(orig.units_sold), //拷贝orig.units_sold
revenue(orig.revenue) //拷贝orig.revenue
{ } //空函数体
详细说明下列几点:
- 由于析构函数不接受参数,因此它不能够被重载。对一个给定的类,只会有唯一一个析构函数。
- 在一个析构函数中,首先执行函数体,然后销毁成员。 成员按初始化顺序的逆序销毁。
- 成员销毁时发生什么完全依赖于成员的类型。
什么时候会调用析构函数:
- 变量在离开其作用域时被销毁。
- 当一个对象被销毁时,其成员被销毁。
- 容器被销毁时候,其元素被销毁。
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁。
- 对于临时对象,当创建它的完整表达式结束时被销毁。
例子:
{//新作用域
//p和p2指向动态分配的对象
Sales_data *p = new Sales_data; //p是一个内置指针
auto p2 = make_shared<Sales_data>(); //p2是一个shared_ptr
Sales_data item(*p); //拷贝构造函数將*p拷贝到item中
vector<Sales_data> vec; //局部对象
vec.push_back(*p2); //拷贝p2指向的对象
delete p; //对p指向的对象执行析构函数
} //退出作用域,对item、p2和vec调用析构函数
//销毁p2会递减其引用计数;如果引用计数为0,对象被释放
//销毁vec会销毁它的元素
3. =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;
详细说明下列几点:
- 当我们在类内用=default修饰成员的声明时,合成的函数將隐式地声明为内联的。
- 如果不希望合成的成员是内联函数,应该只对成员的类外定义使用=default。
4. =delete
我们可以通过將拷贝构造函数和拷贝赋值运算符定义为删除的函数来阻止拷贝。删除的函数作用是:我们虽然声明了它们,但不能以任何方式使用它们。
例子:
struct NoCopy
{
NoCopy() = default; //使用合成的默认构造函数
NoCopy(const NoCopy&) = delete; //阻止拷贝
NoCopy &operator = (const NoCopy&) = delete; //阻止赋值
~NoCopy() = default; //使用合成的析构函数
}
详细说明下列几点:
- 析构函数是不能删除的,如果可以被删除,就无法销毁此类型的对象了。
struct NoDtor { NoDtor() = default; //使用合成默认构造函数 ~NoDtor() = delete; //我们不能销毁NoDtor类型的对象 }; NoDtor nd; //错误: NoDtor的析构函数是删除的。 NoDtor *p = new NoDtor(); //正确: 但我们不能delete p delete p; //错误: NoDtor的析构函数是删除的。
也可以通过將拷贝构造函数和拷贝赋值运算符声明为private的来阻止拷贝。