[笔记]C++拷贝构造和移动构造
一、拷贝构造
如果一个构造函数的第一个参数是自身类类型的引用,且任何额外参数都没有默认值,则此构造函数是拷贝构造函数。(《C++Primer,第五版》)
class Foo { public : Foo(); Foo(const Foo&); //拷贝构造函数 Foo& operator=(const Foo&); //拷贝赋值运算符 };
类的成员中有指针时,使用深拷贝。
#include <iostream> using namespace std; class Foo { public: Foo() { pInt = new int; *pInt = 1; } Foo(const Foo&)= default; //拷贝构造函数 Foo& operator=(const Foo&) = default; //拷贝赋值运算符 int* pInt; }; //浅拷贝,foo1和foo2中的pInt指向同一块内存地址 Foo foo1; Foo foo2(foo1); //深拷贝 class Cop { public: Cop() { pInt = new int; *pInt = 2; } Cop(const Cop& cop) { pInt = new int; *pInt = *(cop.pInt); } int* pInt; }; //深拷贝,cop1和cop2中pInt指向不同内存 Cop cop1; Cop cop2(cop1);
二、移动构造
在某些情况下(函数返回对象引用),对象拷贝后立即就被消耗了。拷贝构造就回造成性能上的浪费,而且深拷贝也会造成浪费。移动构造可以避免这种情况的发生。
为了支持移动构造,C++11引入了右值引用。
右值引用:必须绑定到右值的引用,通过&&来获得右值引用,类似左值引用(常规引用),右值应用也是一块内存的别名。
右值:字面常量、在表达式求值过程中临时创建的对象,这些使用过后就被销毁的资源。不同于左值的持久状态。
因此,右值引用只能绑定到将要被销毁的对象上,左值引用只能绑定到持久的对象上。
//例子来源于 《C++primer 第五版》 int i = 42; int& r = i; //正确,i是左值 int&& rr = i; //错误 int& r2 = i * 42; //错误,i*42是右值,用完即销毁 const int& r3 = i * 42; //正确,隐式转换为左值后,r3引用 int&& rr2 = i * 42; //正确,绑定右值
int&& rr1 = 42; //正确,字面常量是右值
int&& rr2 = rr1; //错误,rr1左值
变量都是左值,因此无法将右值引用绑定到一个右值引用类型的变量上。
如果要将右值引用绑定到左值上,可以通过move函数来获得左值的右值引用类型。对一个左值调用move函数后,除了对该左值赋值和销毁外,不再使用它。
int i = 42; int&& rr = move(i); //i和rr引用同一块内存 i = 32; cout << i << endl; cout << rr << endl;
移动构造函数依靠右值引用特性来将来改变内存的管理者,而不同于拷贝构造对内存进行拷贝。
移动构造:第一个参数是该类类型的一个右值引用,且任何额外的参数都必须有默认实参。使用移动构造函数必须确保销毁移后源对象是无害的(不会重复释放同一块内存)。
Cop(Cop&& cop) noexcept : pInt(cop.pInt) { cout << "this is &&" << endl; cop.pInt = NULL; } Cop& operator=(Cop&& cop) noexcept {
if ( this == &cop ) return *this; cout << "this is && =" << endl; delete pInt; pInt = cop.pInt; cop.pInt = NULL; } Cop retCop() { Cop cop3; *cop3.pInt = 3; return cop3; } //调用移动赋值运算符,cop3中pInt接管retCop返回对象中的pInt指向内存 Cop cop3 = retCop();
noexcept承诺函数不抛出异常,标准库对这个函数不做额外处理。