参考链接:
一文入魂:妈妈再也不担心我不懂C++移动语义了 - 知乎 (zhihu.com)
拷贝控制
当定义一个类时,我们显式地或隐式地指定在此类型的对象拷贝、移动、赋值和销毁时做什么。一个类通过定义五种特殊的成员函数来控制这些操作,包括:拷贝构造函数( copy constructor)、 拷贝赋值运算符(copy-assignment operator)、 移动构造函数( moveconstructor)、移动赋值运算符(move assignment operator)和析构函数( destructor)。拷贝和移动构造函数定义了当用同类型的另一个对象初始化本对象时做什么。拷贝和移动赋值运算符定义了将一个对象赋予同类型的另一个对象时做什么。析构函数定义了当此类型对象销毁时做什么。我们称这些操作为拷贝控制操作( copy control)。
-
拷贝构造
如果一个类没有定义所有这些拷贝控制成员,编译器会自动为它定义缺失的操作。因此,很多类会忽略这些拷贝控制操作。但是,对一些类来说,依赖这些操作的默认定义会导致灾难。通常,实现拷贝控制操作最困难的地方是首先认识到什么时候需要定义这些操作。
拷贝构造函数的第一个参数必须是一个引用类型。思考为什么??
虽然我们可以定义一个接受非const引用的拷贝构造函数,但此参数几乎总是一个const的引用。拷贝构造函数在几种情况下都会被隐式地使用。因此,拷贝构造函数通常不应该是explicit的。
-
拷贝赋值
与拷贝构造函数一一样,如果类未定义自己的拷贝赋值运算符,编译器会为它合成一个。
为了与内置类型的赋值保持一致,赋值运算符通常返回一个指向其左侧运算对象的引用。另外值得注意的是,标准库通常要求保存在容器中的类型要具有赋值运算符,且其返回值是左侧运算对象的引用。
-
析构函数
对象释放时候会调用析构函数。
对于临时对象,当创建它的完整表达式结束时被销毁。
-
三/五法则
三法则:如果这个类需要一个析构函数,我们几乎可以肯定它也需要一个拷贝构造函数和一个拷贝赋值运算符。需要拷贝操作的类也需要赋值操作,反之亦然
class A { public: A(int size){a = new int[a];} ~A(){delete a;} private: int*data; }//只写构造行吗??
-
=default
我们可以通过将拷贝控制成员定义为=default来显式地要求编译器生成合成的版本.当我们在类内用=default修饰成员的声明时,合成的函数将隐式地声明为内联的(就像任何其他类内声明的成员函数一样)。如果我们不希望合成的成员是内联函数,应该只对成员的类外定义使用=default,就像对拷贝赋值运算符所做的那样。
-
阻止拷贝
例如,iostream 类阻止了拷贝,以避免多个对象写入或读取相同的IO缓冲。为了阻止拷贝,看起来可能应该不定义拷贝控制成员。但是,这种策略是无效的:如果我们的类未定义这些操作,编译器为它生成合成的版本。
=delete
struct NoCopy { NoCopy() = default; //使用合成的默认构造函数 NoCopy (const NoCopy&) = delete; // 阻止拷贝 NoCopy &operator= (const NoCopy&) = delete; //阻止赋值 ~NoCopy() = default; //使用合成的析构函数 //其他成员 };
-
左值和右值
C+ +的表达式要不然是右值(rvalue, 读作“are-value"),要不然就是左值(lvalue,读作“ell-value")。 这两个名词是从C语言继承过来的,原本是为了帮助记忆:左值可以位于赋值语句的左侧,右值则不能。
在C++语言中,二者的区别就没那么简单了。一个左值表达式的求值结果是一个对象或者一个函数,然而以常量对象为代表的某些左值实际上不能作为赋值语句的左侧运算对象。此外,虽然某些表达式的求值结果是对象,但它们是右值而非左值。可以做一个简单的归纳:当一个对象被用作右值的时候,用的是对象的值(内容);当对象被用作左值的时候,用的是对象的身份(在内存中的位置)。
-
移动语义
我们知道, 移动构造函数通常是将资源从给定对象“移动”而不是拷贝到正在创建的对象。而且我们.知道标准库保证“移后源”(moved-from)string仍然保持一个有效的、可析构的状态。
我们使用的第二个机制是一个名为move的标准库函数,它定义在utility头文件中。
新标准的一个最主要的特性是可以移动而非拷贝对象的能力。很多情况下都会发生对象拷贝。在其中某些情况下,对象拷贝后就立即被销毁了。在这些情况下,移动而非拷贝对象会大幅度提升性能。
在旧C++标准中,没有直接的方法移动对象。因此,即使不必拷贝对象的情况下,我们也不得不拷贝。如果对象较大,或者是对象本身要求分配内存空间(如string), 进行不必要的拷贝代价非常高。类似的,在旧版本的标准库中,容器中所保存的类必须是可拷贝的。但在新标准中,我们可以用容器保存不可拷贝的类型,只要它们能被移动即可。
-
右值引用
为了支持移动操作,新标准引入了一种新的引用类型----右值引用(rvalue reference)。所谓右值引用就是必须绑定到右值的引用。我们通过&&而不是&来获得右值引用。如我们将要看到的,右值引用有一个重要的性质------只能绑定到一个将要销毁的对象。因此,我们可以自由地将一个右值引用的资源“移动”到另一个对象中。
一般而言,一个左值表达式表示的是一个对象的身份,而一个右值表达式表示的是对象的值。
类似任何引用,一个右值引用也不过是某个对象的另一个名字而已。如我们所知,对于常规引用(为了与右值引用区分开来,我们可以称之为左值引用(lvalue reference)),我们不能将其绑定到要求转换的表达式、字面常量或是返回右值的表达式。右值引用有着完全相反的绑定特性:我们可以将一个右值引用绑定到这类表达式上,但不能将一个右值引用直接绑定到一个左值上:
int i=42; int &r=i; //正确:r引用i int &&rr = i; //错误:不能将一个右值引用绑定到一个左值上 int&r2=i*42; //错误: i*42是一个右值 const int &r3 = i*42; // 正确:我们可以将一个const的引用绑定到一个右值上 int&&rr2=i*42; //正确:将rr2绑定到乘法结果上
返回左值引用的函数,连同赋值、下标、解引用和前置递增/递减运算符,都是返回左值的表达式的例子。我们可以将一个左值引用绑定到这类表达式的结果上。
返回非引用类型的函数,连同算术、关系、位以及后置递增/递减运算符,都生成右值。我们不能将一个左值引用绑定到这类表达式上,但我们可以将一个const的左值引用或者一个右值引用绑定到这类表达式上。
-
左值持久;右值短暂
考察左值和右值表达式的列表,两者相互区别之处就很明显了:左值有持久的状态,而右值要么是字面常量,要么是在表达式求值过程中创建的临时对象。 由于右值引用只能绑定到临时对象,我们得知
-
所引用的对象将要被销毁
-
该对象没有其他用户 这两个特性意味着:使用右值引用的代码可以自由地接管所引用的对象的资源。
-
变量是左值
int &&rrl = 42; //正确:字面常量是右值 int &&rr2=rr1; //错误:表达式rr1是左值!
-
move 函数
int&&rr3 =std::move(rr1) ; // ok /* move 调用告诉编译器:我们有一个左值,但我们希望像一个右值一 样处理它。 我们必须认识到,调用move就意味着承诺:除了对rr1赋值或销毁它外,我们将不再使用它。在 调用move之后,我们不能对移后源对象的值做任何假设。 */
-
移动构造和移动赋值
这两个成员类似对应的拷贝操作,但它们从给定对象“窃取”资源而不是拷贝资源。
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态----销毁它是无害的。特别是,一旦资源完成移动,源对象必须不再指向被移动的资源一这 些资源的所有权已经归属新创建的对象。
StrVec::StrVec (StrVec &&s) noexcept //移动操作不应抛出任何异常 //成员初始化器接管s中的资源 :elements(s.elements),first_free(s.first_free), cap(s.cap) { //令s进入这样的状态一 对其运行析构函数是安全的 s.elements = s.first_free = s.cap = nullptr; }
StrVec &StrVec::operator=(StrVec &&rhs) noexcept { //直接检测自赋值 if (this != &rhs) { this->free();//假设一个释放函数 //释放已有元素 elements = rhs.elements; //从rhs接管资源 first_free = rhs. first_free; cap = rhs.cap; //将rhs置于可析构状态 rhs.elements = rhs. first_free = rhs.cap = nullptr; } return *this; }
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时,编译器才会为它合成移动构造函数或移动赋值运算符。编译器可以移动内置类型的成员。如果一个成员是类类型,且该类有对应的移动操作,编译器也能移动这个成员:
移动操作和合成的拷贝控制成员间还有最后一个相互作用关系:一个类是否定义了自己的移动操作对拷贝操作如何对合成有影响。如果类定义了一个移动构造函数和/或一个移动赋值运算符,则该类的合成拷贝构造函数和拷贝赋值运算符会被定义为删除的。
-
移动右值,拷贝左值
如果一个类既有移动构造函数,也有拷贝构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。赋值操作的情况类似。例如,在我们的StrVec类中,拷贝构造函数接受一个const StrVec的引用。因此,它可以用于任何可以转换为StrVec的类型。而移动构造函数接受一个StrVec&&,因此只能用于实参是(非static)右值的情形:
StrVec v1,v2; v1 = v2; //v2是左值;使用拷贝赋值 StrVec getVec(istream & ); // getVec 返回一个右值 v2 = getVec(cin) ; // getVec(cin) 是一个右值;使用移动赋值
如果没有移动构造函数,右值也被拷贝
-
三/五法则
五法则(c++11):所有五个拷贝控制成员应该看作一个整体:一般来说,如果一个类定义了任何一个拷贝操作,它就应该定义所有五个操作。如前所述,某些类必须定义拷贝构造函数、拷贝赋值运算符和析构函数才能正确工作。这些类通常拥有一个资源,而拷贝成员必须拷贝此资源。一般来说,拷贝一个资源会导致一些额外开销。在这种拷贝并非必要的情况下,定义了移动构造函数和移动赋值运算符的类就可以避免此问题。
C++三五法则
当定义一个类时、我们显式或隐式地定义了此类型的对象在拷见、赋值和销毁时做什么?
一个类通过定义三种特殊成员成员函数来控制这些操作:拷贝构造函数、拷贝赋值函数、析构函数。 什么是三法则? C++三法则:如果需要析构函数,则一定需要拷贝构造函数和拷贝赋值操作符。 如何理解这句话,通常,若一个类需要析构函数,则代表其合成的析构函数不足以释放类所拥有的资源,其中最典型的就是指针成员。 所以,我们需要自己写析构函数来释放给指针所分配的内存来防止内存泄露。 那么为什么说“一定需要拷贝构造函数和赋值操作符”呢? 原因还是这样:类中出现了指针类型的成员。有指针类型的成员,我们必须防止浅拷贝问题,所以,一定需要拷贝构造函数和赋值操作符, 这两个函数是防止浅拷贝问题所必须的。
class rule_of_three
{
char*cstring;
void init(const char* s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n];
std::memcpy(cstring, s, n);//填充
}
public:
rule_of_three(const char s = "") { init(s); }
~rule_of_three()
{
delete[] cstring;
}
rule_of_three(const rule_of_three& other) //复制构造函数
{
init(other.cstring);
}
rule_of_three& operator=(const rule_of_three& other) // 复制赋值
{
if(this != &other)
{
delete[] cstring; // 解分配
init(other.cstring);
}
return *this;
}
}
什么是五法则 在较新的C++11标准中,为了支持移动语义,又增加了移动构造函数和移动赋值运算符,这样共有五个特殊的成员函数,所以又称为“C++五法则”; 也就是说,“三法则” 是针对较旧的C++89标准说的,“五法则” 是针对较新的C++11标准说的;为了统一称呼, 后来人们干把它叫做“C++三/五法则”;
class rule_of_five
{
char* cstring; //以裸指针为动态分配内存的句柄
public:
rule_of_five(const char* s = ""):cstring(nullptr)
{
if(s)
{
std::size_t n = std::strlen(s) + 1;
cstring = new char[n];
std:memcpy(cstring, s, n); //填充
}
}
~rule_of_five()
{
delete[] cstring;
}
rule_of_five(const rule_of_five& other) // 复制构造函数
:rule_of_five(other.cstring)
{}
rule_of_five(rule_ of_ five&& other) noexcept //移动构造函数
:cstring(std:exchange(other.cstring, nullptr))
{}
rule_of_five& operator=(const rule_of_five& other) //复制赋值
return *this = rule_of_five(other);
}
rule_of_five& operator=(rule_of_five&& other) noexcept //移动赋值
std:swap(cstring, other.cstring);
return *this;
}