对象交换
-
概念
swap 是 STL 的一部分,交换操作即,将两对象的值彼此赋予对方。交互两个对象,需要进行一次拷贝和两次赋值操作。
namespace std { template<typename T> void swap(T& a,T& b) { T temp(a);//构造一个临时副本; a = b;//两次赋值 b = a; } }
-
自定义 swap
对于包含动态内存的类来说,拷贝会产生不必要的内存,本质上我们只需要交换指针即可。可以对类定义一个自己版本的 swap 来重载 swap 的默认行为。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 friend void swap(Example& t,Example& s);//自定义swap,定义为友元,为访问私有成员 private: int a; string *b; }; inline void swap(Example& t,Example& s) { using std::swap;//如果包含自定义的 swap 函数, 则优先使用自定义的 swap 函数; 否则, 系统使用标准库的 swap 函数 swap(t.b,s.b);//交换指针,非底层 string swap(t.a,s.a); }
-
copy and swap
定义了 swap 的类通常用 swap 来定义赋值运算符,即将左侧运算对象与右侧运算对象的一个副本进行交换,核心是右侧运算对象以传值方式传递给赋值运算符。使用了拷贝和交换的赋值运算符是异常安全的,且能正确处理自赋值。
Example& Example::operator= (const Example emp) //值传递参数 { swap(*this,emp);//交换左侧对象和局部变量 emp,emp中指针指向了左侧对象的旧内存 return *this;// emp 被销毁,释放了旧内存 }
对象移动
-
概念
①.左值和右值
左值表达式表示的是一个对象的身份,左值具有持久的状态,变量是左值;右值表达式表示的是对象的值,右值要么是字面常量,要么是过程中的临时对象。
②.右值引用
右值引用就是必须绑定到右值的引用,用过 && 来获得右值引用。右值引用只能绑定到一个将要销毁的对象:所引用的对象将要被销毁、该对象没有其他用户。
int a = 2; int &&r = a*10;// r绑定到计算结果
右值引用可以给临时变量续命,不再被销毁
int getVal(); int i = getVal();// 产生两个变量,左值 i,表达式结束后依然存在;getVal()返回的临时值是右值,表达式结束后被销毁 int&& j = getVal();// j 是右值的引用,getVal()返回的临时值不再被销毁,生命周期通过右值引用得以延续
③.移动
移动即数据资源的转移,从一个对象转移到另个对象,更改了资源的所有权。
④.move 函数
move 函数定义在头文件 utility 中,是用来将一个右值引用绑定到一个左值的标准函数。move
调用 move 隐含的承诺我们将不再使用移动后的源对象,除了销毁它或者赋给它一个新值。
int a = 2; int &&r = std::move(a);// r 是绑定到 a 右值的引用
-
移动构造函数
移动构造函数的第一个参数是该类型的一个右值引用,且为必须非 const,因为必须要确保移动资源后源对象是可以被析构的。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 //移动拷贝构造函数 Example( Example&& emp) noexcept //承诺不抛出任何异常 :b( emp.b),a(emp.a) { b = nullptr;//承诺可被析构 }; private: int a; string *b; };
-
移动赋值运算符
移动赋值运算符执行与析构函数和移动构造函数相同的工作,也必须确保正确处理自我赋值。
class Example { public: Example(string s ):b(new string(s)) {} ;//构造函数 ~Example() ;//析构函数 Example( Example & emp) :b( new string(*emp.b)) {};//拷贝构造函数 Example& operator= (const Example& emp) ;//拷贝赋值运算符 //移动赋值运算符 Example& operator= ( Example&& emp) noexcept //承诺不抛出任何异常 { if( *this!= emp) { delete b;//释放旧资源 b = emp.b; a = emp.a; emp.b = nullptr;//承诺可被析构 } return *this; }; private: int a; string *b; };
-
合成的移动操作
①.如果类自定义了拷贝操作或者析构函数,则不会生成默认移动操作。
②.只有类没有自定义拷贝操作且成员都可移动时,才会生成默认移动操作。
③.如果类定义了移动操作,那默认的拷贝操作将会定义成删除的
-
移动操作应用
①.移动右值,拷贝左值
若一个类既有拷贝构造函数,又有移动构造函数,编译器使用普通的函数匹配规则来确定使用哪个构造函数。
Example GetObj();//返回一个 Example 类型对象 Example exp1,exp2; exp1 = exp2;//exp2 是左值,使用拷贝赋值 exp1 = GetObj();//GetObj() 返回一个右值,使用移动赋值
②.若没有移动构造函数,右值也可以被拷贝
T&& 可用转换成 const T&,当没有移动构造函数时,可用匹配为拷贝构造函数。
③.拷贝并交互赋值运算符
拷贝并交互运算符的参数非引用类型,此参数需要进行拷贝初始化,依赖于实参类型:左值使用拷贝构造、右值使用移动构造。因此,单一运算符实现了拷贝赋值和移动赋值两种功能。
//拷贝并交互赋值运算符 Example& Example::operator= (const Example emp) //值传递参数 { swap(*this,emp);//交换左侧对象和局部变量 emp,emp中指针指向了左侧对象的旧内存 return *this;// emp 被销毁,释放了旧内存 } //移动构造函数 Example:: Example( Example&& emp) noexcept //承诺不抛出任何异常 :b( emp.b),a(emp.a) { b = nullptr;//承诺可被析构 }
-
引用限定符
类似于 const 限定符的使用,可以使用 & 或者 && 来限定成员函数可以作用于的对象类型,必须在函数声明和定义中都加限定符。
class A { public: A& operator=(const A&) &; };