当定义一个类时,我们显示地或隐示地指定在此类型的对象拷贝,移动,赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数,拷贝赋值运算符,移动构造函数,移动赋值运算符和折构函数。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作。
每个类都会控制该类型对象拷贝,移动,赋值以及销毁时发生什么。特殊的成员函数——拷贝构造函数,移动构造函数,拷贝赋值运算符,移动赋值运算符和析构函数定义了这些操作。移动构造函数和移动赋值运算符接受一个(通常是非const)右值引用;而拷贝版本则接受一个(通常是const)普通左值引用。
如果一个类未声明这些操作,编译器会自动未其生成。如果这些操作未定义成删除的,它们会逐成员初始化,移动,赋值或销毁对象:合成的操作依次处理每个非static 数据成员,根据成员类型确定如何移动,拷贝,赋值或销毁它。
分配了内存或其他资源的类几乎总是需要定义拷贝控制成员来管理分配的资源,如果一个类需要析构函数,则它几乎肯定也需要定义移动和拷贝构造函数及移动和拷贝赋值运算符。
拷贝构造函数
如果构造函数的第一个参数是自身类类型的引用,且所有其他参数(如果有的话)都有默认值,则此构造函数是拷贝构造函数。拷贝构造函数在以下几种情况下会被使用:
1. 拷贝初始化(用=定义变量)
2. 将一个对象作为实参传递给非引用类型的形参。
3. 一个返回类型为非引用类型的函数返回一个对象。
4. 用花括号列表初始化一个数组中的元素或一个聚合类中的成员。
5. 初始化标准库容器或调用其insert / push操作时,容器会对其元素进行拷贝初始化。
例:
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
}
拷贝赋值运算符
拷贝赋值运算符本身是一个重载的赋值运算符,定义为类的成员函数,左侧运算对象绑定到隐含的this参数,而右侧运算对象是所属类类型的,作为函数的参数,函数返回指向其左侧运算对象的引用。
1. 当对类对象进行赋值时,会使用拷贝赋值运算符。
2. 通常情况下,合成的拷贝赋值运算符会将右侧对象的非 static 成员逐个赋予左侧对象的对应成员,这些赋值操作符是由成员类型的拷贝赋值运算符来完成的。
3. 若一个类未定义自己的拷贝赋值运算符,编译器就会为其合成拷贝赋值运算符,完成赋值操作,但对于某些类,还会起到禁止该类型对象赋值的效果。
例:
class Foo{
public:
Foo(); //默认构造函数
Foo(const Foo&); //拷贝构造函数
Foo& operator=(const Foo&); //拷贝赋值运算符
}
析构函数
1. 析构函数完成与构造函数相反的工作:释放对象使用的资源,销毁非静态数据成员。从语法上看,它是类的一个成员函数,名字是波浪号接类名,没有返回值,也不接受参数。
2. 当一个类没有定义折构函数时,编译器会为他合成析构函数。
3. 合成的析构函数体为空,但这并不意味着它什么也不做,当空函数体执行完后,非静态数据成员会逐个被销毁。也就是说,成员是在析构函数体之后隐含的析构阶段中进行销毁的。
移动构造函数
类似拷贝构造函数,移动构造函数的第一个参数是该类类型的一个引用。不同于拷贝构造函数的是,这个引用参数在移动构造函数中是一个右值引用。与拷贝构造函数一样,任何额外的参数都必须有默认实参。
与拷贝构造函数不同,移动构造函数不分配任何新内存;它接管给定的类中的内存。在接管内存之后,它将给定对象中的指针都置为nullptr。这样就完成了从给定对象的移动操作,此对象将继续存在。最终,移后源对象会被销毁,意味着将在其上运行折构函数。
例:
StrVec :: StrVec(Strvec &&s) noexcept :elements(s.elements),first_free(s.first_free),cap(s.cap)
{
s.elements = s.first_free = s.cap=nullptr;
}
移动赋值运算符
移动赋值运算符执行与折构函数和移动构造函数相同的工作。与移动构造函数一样,如果我们的移动赋值运算符不抛出任何异常,我们就应该将它标记为noexcept。类似拷贝赋值运算符,移动赋值运算符必须正确处理自赋值。
例:
StrVec &StrVec :: operator=(StrVec &&rhs) noexcept
{
if (this != &rhs) {
free();
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
右值引用与左值应用的区别?
所谓右值引用就是必须绑定到右值的引用,通过&&获得。右值引用只能绑定到一个将要销毁的对象上,因此可以自由地移动其资源。
左值引用,也就是“常规引用”,不能绑定到要转换的表达式,字面常量或返回右值的表达式。而右值引用恰恰相反,可以绑定到这类表达式,但是不能绑定到一个左值上。
返回左值的表达式包括返回左值引用的函数及赋值,下标,解引用和前置递增/递减运算符,返回右值的包括返回非引用类型的函数及算术,关系,位和后置递增/递减运算符。可以看到,左值的特点是持久的状态,而右值则是短暂的。