笔记会持续更新,有错误的地方欢迎指正,谢谢!
对象移动
移动对象的优势:
- 我们在赋值操作时,对象拷贝后就立即销毁,但移动而非拷贝对象会大幅度提升性能。
- 有些类型是不能被拷贝的,如IO类和unique_ptr,在旧标准中我们无法在容器中保存它们,因为它们无法被拷贝,就不存在赋值之类的操作。但引入了移动操作后,我们就可以用容器保存它们。
右值引用
通过&&来获得右值引用,右值引用只能绑定到一个即将销毁的对象上,所以,我们才能自由地将一个右值引用的资源移动到另一个对象中。
记住左值长期存在的;右值是临时的,是即将销毁的。也就是左值持久,右值短暂。
右值要么是字面常量,要么是求值过程中创建的临时对象。
来看几个例子:
int i = 42;
int &r = i; //对
int &&rr = i; //错,右值不是字面常量,也不是临时对象
int &r2 = i * 24; //错,不能把临时对象赋给普通引用
const int &r3 = i * 13; //对,可以把临时对象赋给引用常量
int &&rr2 = i * 2; //对,右值是临时对象
int &&rr3 = 42; //对,右值是字面值常量
int &&rr4 = rr3; //错,绑定右值引用的变量rr3仍是左值,即rr3是正常的变量。
右值引用只能绑定到临时对象的原因:
- 所引用的对象将要被销毁
- 该对象没有用户
所以,使用右值引用的代码可以自由地接管所引用的对象的资源。
标准库move函数
强行右值,move算是一个移动构造函数:
int a = 12;
int &&b = std::move(a) //move函数告诉编译器,要把这个左值a当右值处理。
调用move就意味着:除了对a赋值或销毁外,我们将不再使用它,例如我们不能把它的值赋给别人。
移动构造函数
类似对应的拷贝操作,但它从给定对象窃取资源而不是拷贝资源。
移动构造函数和拷贝构造函数的唯一区别就是它的引用是右值引用。
一旦资源完成移动,原对象必须不再指向被移动的资源,所有权已归属新对象。
用老朋友StrVec定义移动构造函数(它没有分配新内存):
StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements),
first_free(s.first_free), cap(s.cap) //noexcept表示不抛出异常
//noexcept:声明和定义不抛出异常的移动构造函数和移动赋值函数都要显式
//指定noexcept,否则系统会使用拷贝操作。
{
//上面的列表初始化就移动好了,注意第一个参数是非const右值引用
//接下来的话保证s进入这样的状态-对其进行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
其实就是s把资源给了新对象,自己都变成空指针,深藏功与名了。
移动赋值运算符
类似对应的拷贝操作,但它从给定对象窃取资源而不是拷贝资源。
和赋值运算符类似,也要正确处理自赋值情况:
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) //检测,不是自赋值再进行下面步骤,是自赋值直接返回
{
free(); //因为它要接管rhs,原来的内存就不用了。
//从rhs窃取资源
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
移后原对象要保持有效的,可析构的状态,但最好不要去动它,除了析构它之外,让它安静地功成身退。
合成的移动操作
没有任何自定义的拷贝控制成员,且每个非stastic数据成员都可以移动时,编译器才会为他们合成移动构造函数或移动赋值运算符。
移动和拷贝
编译器使用普通的函数匹配规则确定使用哪个拷贝控制成员。通常,拷贝左值,移动右值。如果没有移动构造函数,右值也被拷贝。
移动迭代器
通过标准库的make_move_iterator函数来将普通迭代器转换为移动迭代器。