详解C++中对构造函数和赋值运算符的复制和移动操作
复制构造函数和复制赋值运算符
从C++11中开始,该语言支持两种类型的分配:复制赋值和移动赋值。在本文中,“赋值”意味着复制赋值,除非有其他显式声明。赋值操作和初始化操作都会导致对象被复制。
赋值:在将一个对象的值赋给另一个对象时,第一个对象将复制到第二个对象中。因此,
Pointa,b; ... a=b;
导致b的值被复制到a中。
初始化:在以下情况下将进行初始化:声明新对象、参数通过值传递给函数或值通过值从函数返回。
您可以为类类型的对象定义“复制”的语义。例如,考虑此代码:
TextFilea,b; a.Open("FILE1.DAT"); b.Open("FILE2.DAT"); b=a;
前面的代码可能表示“将FILE1.DAT的内容复制到FILE2.DAT”,也可能表示“忽略FILE2.DAT并使b成为FILE1.DAT的另一个句柄”。您必须将适当的复制语义附加到每个类,如下所示。
通过将赋值运算符operator=与对类类型的引用一起用作返回类型和const引用所传递的参数(例如,ClassName&operator=(constClassName&x);)。
通过通过复制构造函数。有关复制构造函数的详细信息,请参阅声明构造函数的规则。
如果不声明复制构造函数,编译器将为你生成member-wise复制构造函数。 如果不声明复制赋值运算符,编译器将为你生成member-wise复制赋值运算符。声明复制构造函数不会取消编译器生成的复制赋值运算符,反之亦然。如果实现上述其中一项,建议您还实现另一项以使代码的含义变得明确。
逐个成员赋值和初始化中更详细地介绍了逐个成员赋值。
复制构造函数采用了class-name&类型的参数,其中class-name是为其定义构造函数的类的名称。例如:
//spec1_copying_class_objects.cpp classWindow { public: Window(constWindow&);//Declarecopyconstructor. //... }; intmain() { }
说明:
尽可能创建该类型的复制构造函数的参数constclass-name&。这可防止复制构造函数意外更改从中复制它的对象。它还支持从const对象进行复制。
编译器生成的构造函数
编译器生成的复制构造函数(如用户定义的复制构造函数)具有单个参数类型“对class-name的引用”。当所有基类和成员类都具有声明为采用constclass-name&类型的单个参数的复制构造函数时,将引发异常。在这种情况下,编译器生成的复制构造函数的参数也是const。
当复制构造函数的参数类型不是const时,通过复制const对象进行初始化将产生错误。反之则不然:如果参数是const,您可以通过复制不是const的对象进行初始化。
编译器生成的赋值运算符遵循关于const的相同模式。除非所有基类和成员类中的赋值运算符都采用constclass-name&类型的参数,否则它们将采用class-name&类型的单个参数。在这种情况下,类的生成的赋值运算符采用const参数。
说明:
当虚拟基类由复制构造函数(编译器生成或用户定义的)初始化时,将只初始化这些基类一次:在构造它们时。
含义类似于复制构造函数的含义。当参数类型不是const时,从const对象赋值将产生错误。反之则不然:如果将const值赋给不是const的值,则赋值能成功。
移动构造函数和移动赋值运算符
下面的示例基于用于管理内存缓冲区的C++类MemoryBlock。
//MemoryBlock.h #pragmaonce #include<iostream> #include<algorithm> classMemoryBlock { public: //Simpleconstructorthatinitializestheresource. explicitMemoryBlock(size_tlength) :_length(length) ,_data(newint[length]) { std::cout<<"InMemoryBlock(size_t).length=" <<_length<<"."<<std::endl; } //Destructor. ~MemoryBlock() { std::cout<<"In~MemoryBlock().length=" <<_length<<"."; if(_data!=nullptr) { std::cout<<"Deletingresource."; //Deletetheresource. delete[]_data; } std::cout<<std::endl; } //Copyconstructor. MemoryBlock(constMemoryBlock&other) :_length(other._length) ,_data(newint[other._length]) { std::cout<<"InMemoryBlock(constMemoryBlock&).length=" <<other._length<<".Copyingresource."<<std::endl; std::copy(other._data,other._data+_length,_data); } //Copyassignmentoperator. MemoryBlock&operator=(constMemoryBlock&other) { std::cout<<"Inoperator=(constMemoryBlock&).length=" <<other._length<<".Copyingresource."<<std::endl; if(this!=&other) { //Freetheexistingresource. delete[]_data; _length=other._length; _data=newint[_length]; std::copy(other._data,other._data+_length,_data); } return*this; } //Retrievesthelengthofthedataresource. size_tLength()const { return_length; } private: size_t_length;//Thelengthoftheresource. int*_data;//Theresource. };
以下过程介绍如何为示例C++类编写移动构造函数和移动赋值运算符。
为C++创建移动构造函数
定义一个空的构造函数方法,该方法采用一个对类类型的右值引用作为参数,如以下示例所示:
MemoryBlock(MemoryBlock&&other) :_data(nullptr) ,_length(0) { }
在移动构造函数中,将源对象中的类数据成员添加到要构造的对象:
_data=other._data; _length=other._length;
将源对象的数据成员分配给默认值。这样可以防止析构函数多次释放资源(如内存):
other._data=nullptr; other._length=0;
为C++类创建移动赋值运算符
定义一个空的赋值运算符,该运算符采用一个对类类型的右值引用作为参数并返回一个对类类型的引用,如以下示例所示:
MemoryBlock&operator=(MemoryBlock&&other) { }
在移动赋值运算符中,如果尝试将对象赋给自身,则添加不执行运算的条件语句。
if(this!=&other) { }
在条件语句中,从要将其赋值的对象中释放所有资源(如内存)。
以下示例从要将其赋值的对象中释放_data成员:
//Freetheexistingresource. delete[]_data;
执行第一个过程中的步骤2和步骤3以将数据成员从源对象转移到要构造的对象:
//Copythedatapointeranditslengthfromthe //sourceobject. _data=other._data; _length=other._length; //Releasethedatapointerfromthesourceobjectsothat //thedestructordoesnotfreethememorymultipletimes. other._data=nullptr; other._length=0;
返回对当前对象的引用,如以下示例所示:
return*this;
以下示例显示了MemoryBlock类的完整移动构造函数和移动赋值运算符:
//Moveconstructor. MemoryBlock(MemoryBlock&&other) :_data(nullptr) ,_length(0) { std::cout<<"InMemoryBlock(MemoryBlock&&).length=" <<other._length<<".Movingresource."<<std::endl; //Copythedatapointeranditslengthfromthe //sourceobject. _data=other._data; _length=other._length; //Releasethedatapointerfromthesourceobjectsothat //thedestructordoesnotfreethememorymultipletimes. other._data=nullptr; other._length=0; } //Moveassignmentoperator. MemoryBlock&operator=(MemoryBlock&&other) { std::cout<<"Inoperator=(MemoryBlock&&).length=" <<other._length<<"."<<std::endl; if(this!=&other) { //Freetheexistingresource. delete[]_data; //Copythedatapointeranditslengthfromthe //sourceobject. _data=other._data; _length=other._length; //Releasethedatapointerfromthesourceobjectsothat //thedestructordoesnotfreethememorymultipletimes. other._data=nullptr; other._length=0; } return*this; }
以下示例演示移动语义如何能提高应用程序的性能。此示例将两个元素添加到一个矢量对象,然后在两个现有元素之间插入一个新元素。在VisualC++2010中,vector类使用移动语义,通过移动矢量元素(而非复制它们)来高效地执行插入操作。
//rvalue-references-move-semantics.cpp //compilewith:/EHsc #include"MemoryBlock.h" #include<vector> usingnamespacestd; intmain() { //Createavectorobjectandaddafewelementstoit. vector<MemoryBlock>v; v.push_back(MemoryBlock(25)); v.push_back(MemoryBlock(75)); //Insertanewelementintothesecondpositionofthevector. v.insert(v.begin()+1,MemoryBlock(50)); }
该示例产生下面的输出:
InMemoryBlock(size_t).length=25. InMemoryBlock(MemoryBlock&&).length=25.Movingresource. In~MemoryBlock().length=0. InMemoryBlock(size_t).length=75. InMemoryBlock(MemoryBlock&&).length=25.Movingresource. In~MemoryBlock().length=0. InMemoryBlock(MemoryBlock&&).length=75.Movingresource. In~MemoryBlock().length=0. InMemoryBlock(size_t).length=50. InMemoryBlock(MemoryBlock&&).length=50.Movingresource. InMemoryBlock(MemoryBlock&&).length=50.Movingresource. Inoperator=(MemoryBlock&&).length=75. Inoperator=(MemoryBlock&&).length=50. In~MemoryBlock().length=0. In~MemoryBlock().length=0. In~MemoryBlock().length=25.Deletingresource. In~MemoryBlock().length=50.Deletingresource. In~MemoryBlock().length=75.Deletingresource.
使用移动语义的此示例版本比不使用移动语义的版本更高效,因为前者执行的复制、内存分配和内存释放操作更少。
可靠编程
若要防止资源泄漏,请始终释放移动赋值运算符中的资源(如内存、文件句柄和套接字)。
若要防止不可恢复的资源损坏,请正确处理移动赋值运算符中的自我赋值。
如果为您的类同时提供了移动构造函数和移动赋值运算符,则可以编写移动构造函数来调用移动赋值运算符,从而消除冗余代码。以下示例显示了调用移动赋值运算符的移动构造函数的修改后的版本:
//Moveconstructor. MemoryBlock(MemoryBlock&&other) :_data(nullptr) ,_length(0) { *this=std::move(other); }