拷贝控制
参考文献
类的特殊函数
- 初始化——构造函数
- 拷贝——拷贝构造函数
- 移动——移动构造函数
- 赋值——拷贝赋值运算符、移动赋值运算符
- 销毁——析构函数
默认构造(无参) T()
拷贝构造 T(const T& )
移动构造 T(T&&)
拷贝赋值 T& operator=(T& )
移动赋值 T& operator=(T&& )
析构 ~T()
1 构造函数
与类同名的,没有返回值的函数,用来创建、拷贝、移动、销毁该类的对象。
1.1 合成构造函数
编译器自动生成的一系列构造函数。包括以下几种
- 合成默认构造函数
- 当用户定义了任意类型的构造函数,编译器不再自动生成合成默认构造函数
- 合成拷贝构造函数
- 即是用户定义了其他类型的构造函数,编译器还会自动生成合成拷贝构造函数。
- 编译器自动生成的拷贝构造函数。从给定的对象中依次将每个非static成员拷贝到正在创建的对象当中。
- 合成析构函数
- 系统自动生成的析构函数。
1.2 默认构造函数和普通构造函数
- 默认构造函数是无参构造函数
- 普通构造函数是一系列有参数的构造函数。
1.3 拷贝构造函数和拷贝赋值运算符
唯一参数是当前类类型,或者当前类型的const引用。
示例
class Foo{
Foo();
Foo(const Foo&)//拷贝构造函数
}
赋值初始化(拷贝构造函数)
赋值初始化的时候会自动调用拷贝构造函数。
string nies = string("efji");
- 当我门使用 赋值= 运算符时,发生赋值初始化,执行拷贝构造函数。
- 将一个对象作为实参传递给一个非引用类型的形参
- 从一个返回类型为费引用类型的函数返回一个对象
- 用花括号列表初始化一个数组中的元素或一个聚合类中的成员
赋值运算符(拷贝赋值运算符)
普通赋值的时候,会调用重载的赋值运算符。
- 编译器会自动生成合成拷贝赋值运算符
- 需要重载赋值运算符。
1.4 移动构造函数和移动赋值运算符
- 在面向对象中,有的类是可以拷贝的,例如车、房等他们的属性是可以复制的,可以调用拷贝构造函数,有点类的对象则是独一无二的,或者类的资源是独一无二的,比如 IO 、 std::unique_ptr等,他们不可以复制,但是可以把资源交出所有权给新的对象,称为可以移动的。
- C++11最重要的一个改进之一就是引入了move语义,这样在一些对象的构造时可以获取到已有的资源(如内存)而不需要通过拷贝,申请新的内存,这样移动而非拷贝将会大幅度提升性能。例如有些右值即将消亡析构,这个时候我们用移动构造函数可以接管他们的资源。
#include <iostream>
#include <cstring>
using namespace std;
class A{
public:
//默认构造函数
A():i(new int[500]){
cout<<"class A construct!"<<endl;
}
//拷贝构造函数
A(const A &a):i(new int[500]){
memcpy(i, a.i,500*sizeof(int));
cout<<"class A copy!"<<endl;
}
//拷贝赋值运算符
A &operator =(A &rhs) noexcept{
// check self assignment
if(this != &rhs){
delete []i;
i = rhs.i;
}
cout<< "class A copy and assignment"<<std::endl;
return *this;
}
//移动构造函数
A(A &&a)noexcept:i(a.i)
{
a.i = nullptr;
cout<< "class A move"<<endl;
}
//移动赋值运算符
A &operator =(A &&rhs) noexcept{
// check self assignment
if(this != &rhs){
delete []i;
i = rhs.i;
rhs.i = nullptr;
}
cout<< "class A move and assignment"<<std::endl;
return *this;
}
//析构函数
~A(){
delete []i;
cout<<"class A destruct!"<<endl;
}
private:
int *i;
};
A get_A_value(){
return A();
}
void pass_A_by_value(A a){
}
int main(){
A a = get_A_value();
return 0;
}
- 在移动构造函数中,我们做了什么呢,我们只是获取了被移动对象的资源(这里是内存)的所有权,同时把被移动对象的成员指针置为空(以避免移动过来的内存被析构),这个过程中没有新内存的申请和分配,在大量对象的系统中,移动构造相对与拷贝构造可以显著提高性能!这里noexcept告诉编译器这里不会抛出异常,从而让编译器省一些操作(这个也是保证了STL容器在重新分配内存的时候(知道是noexpect)而使用移动构造而不是拷贝构造函数),通常移动构造都不会抛出异常的。
注意事项:
- 偷梁换柱直接“浅拷贝”右值引用的对象的成员;
- 需要把原先右值引用的指针成员置为 nullptr,以避免右值在析构的时候把我们浅拷贝的资源给释放了;
- 移动构造函数需要先检查一下是否是自赋值,然后才能先delet自己的成员内存再浅拷贝右值的成员,始终记住第2条。
1.5 委托构造函数
- 使用已有的构造函数初始化。
2 析构函数
定义析构函数
- 析构函数与构造函数对应,当对象结束其生命周期,如对象所在的函数已调用完毕时,系统会自动执行析构函数。
- 类的成员函数,由拨浪号接类名构成,没有返回值,不接受参数。不能被重载,一个类只有一个析构函数。
class Foo{
public:
~Foo();
}
原理
- 在一个析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序逆序销毁。
- 智能指针成员在西沟阶段会自动销毁。
何时调用
- 变量离开作用域被销毁
- 一个对象被销毁
- 容器被销毁
- 动态对象,使用delete
对象析构顺序
- 派生类本身的析构函数;
- 对象成员析构函数;
- 基类析构函数。
3 虚函数与构造函数和析构函数
构造函数不必是虚函数
- 对象通过虚函数指针访问虚函数。在执行构造函数之前,虚函数指针没有创建,所以即使声明为虚函数,也不会有多态,所以不必要是虚函数。
析构函数必须是虚函数
- 删除动态运行时的具体对象。
- 普通对象如果不被继承,析构函数可以不使用虚函数。避免生成虚函数表和虚函数指针,浪费内存空间。
4 三五法则
在较新的 C++11 标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”;也就是说,“三法则”是针对较旧的 C++89 标准说的,“五法则”是针对较新的 C++11 标准说的;为了统一称呼,后来人们干把它叫做“C++ 三/五法则”;
- 需要析构函数的类也需要拷贝构造函数和拷贝赋值函数。
- 需要拷贝操作的类也需要赋值操作,反之亦然。
- 析构函数不能是删除的
- 如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的。
- 如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作。
5 概念区分:声明declare、定义define、初始化initialize、赋值assign
声明declare
- 声明一个符号。如果有extern,则表示变量在外部顶底,链接其他问件事,匹配外部定义的变量。
定义define
- 分配内存、指定变量名。
初始化initialize
- 对象创建时获得的初始值。初始化的含义是,在创建变量的时候赋予其一个初始值。
赋值assign
- 赋值的含义是,将当前的值擦除,而以一个新的值来代替。
- 不能是简单的覆盖,将当前值擦除,需要调用当前对象的析构函数,对指针变量进行析构,如果只是简单的覆盖,肯定不行。