拷贝控制操作包括,拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符,析构函数。拷贝和移动构造函数定义了用同类型的另一个对象初始化本对象的过程,拷贝和移动赋值运算符定义了将一个对象赋值给同类型的另一个对象做了什么,析构函数定义了当对象销毁时做了什么。
1 拷贝,赋值,销毁
1.1 拷贝构造函数
拷贝构造函数的第一个参数必须是一个引用类型,因为要进行实参的拷贝,几乎总是一个const引用。如果我们没有定义拷贝构造函数,编译器会为我们定义一个合成拷贝构造函数。合成拷贝构造函数会把非static成员参数逐个拷贝到新的对象。
拷贝初始化
string dots(10, ' ') //直接初始化
string s(dots) //直接初始化
string s1 = dots //拷贝初始化
string s2 = "1234" //拷贝初始化
string s3 = string(10, ' ') //拷贝初始化
拷贝初始化通常由拷贝构造函数完成,如果存在移动构造函数,则拷贝初始化有时由移动构造函数完成。拷贝初始化不仅在=定义变量时发生。下列情况也会发生。
- 函数内的局部对象做为返回值返回(不是引用)的时候会发生拷贝(拷贝为临时对象返回)
- 函数形参为传值的时候,会发生拷贝构造
1.2 拷贝赋值函数
重载赋值运算符
重载运算符的本质是函数,名字由operater运算符加上要定义的运算符的符号。赋值运算符就是由operater=表示,存在参数和返回值。重载运算符的参数是要运算的对象,返回值指向其左侧运算运算对象的引用。
合成拷贝赋值函数,如果一个类没有定义拷贝赋值函数,那么编译器会定义一个合成拷贝赋值函数,它会将右侧对象的每个非static成员参数拷贝到左侧对象。并返回一个指向其左侧运算对象的引用。
1.3 析构函数
析构函数执行与构造函数相反的操作。析构函数中,首先执行函数体,然后销毁成员。成员按初始化顺序的逆序销毁。无论何时一个对象被销毁,就会自动调用其析构函数:
- 变量在离开其作用域时被销毁
- 当一个对象被销毁时,其成员被销毁
- 容器(无论是标准容器还是数组)被销毁时,其元素被销毁
- 对于动态分配的对象,当对指向它的指针应用delete运算符时被销毁
- 对于临时对象,当创建它的完整表达式结束时被销毁
class Foo{
public:
Foo();
Foo(const Foo&);
Foo& operator=(const Foo&);
~Foo();
}
1.4 三五法则
三五法则规定了什么时候需要拷贝构造函数,拷贝赋值函数,析构函数
需要析构函数的类也需要拷贝构造函数和拷贝赋值函数
通常,若一个类需要析构函数,则代表其合成的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员(析构时需要手动去释放指针指向的内存)。
若存在自定义的析构函数,但使用合成的拷贝构造函数,那么拷贝过去的也只是指针,此时两个对象的指针变量同时指向同一块内存,指向同一块内存的后果很有可能是在两个对象中的析构函数中先后被释放两次。所以需要额外的拷贝控制函数去控制相应资源的拷贝。
这类例子的共同点就是:一个对象拥有额外的资源(指针指向的内存),但另一个对象使用合成的拷贝构造函数也同时拥有这块资源。当一方对象被销毁后,析构函数释放了资源,这时另一个对象便失去了这块资源。
需要拷贝操作的类也需要赋值操作,反之亦然
需要拷贝操作代表这个类在拷贝时需要进行一些额外的操作。 赋值操作 <<< = >>> 先析构+拷贝,所以拷贝需要的赋值也需要。反之亦然。
析构函数不能是删除的
如果类的析构函数是删除的,那么成员便无法销毁。所以在程序中不能定义这个类的对象。可以动态分配该对象并获得其指针,但无法销毁这个动态分配的对象(delete 失效)。
如果一个类有删除的或不可访问的析构函数,那么其默认和拷贝构造函数会被定义为删除的
如果没有这条规则,可能会创造出无法被删除的对象。 理论上来说,当析构函数不能被访问时,任何静态定义的对象都不能通过编译器的编译,所以这种情况只会出现在与动态分配有关的拷贝/默认构造函数身上。
如果一个类有const或引用成员,则不能使用合成的拷贝赋值操作
const或引用成员只能在初始化时被赋值一次,而合成的拷贝赋值操作会对所有成员都进行赋值。显然,它不能赋值const和引用成员,所以合成的拷贝构造函数不能被使用,即会被定义为删除的。当不可能拷贝、赋值、或销毁类的所有成员时,类的合成拷贝控制函数就被定义成删除的了。
网友评论