C++ Primer第十三章⑥
C++ Primer
拷贝控制
对象移动
新标准引入了移动对象的概念,回忆一下,我们在赋值操作时,经常进行这样的操作,对象拷贝后就立即销毁,在这些情况下,移动而非拷贝对象会大幅度提升性能。
还有一个引入移动对象的原因:
有些类型是不能被拷贝的,例如IO类和unique_ptr,在旧标准中我们无法在容器中保存它们,因为它们无法被拷贝,就不存在赋值之类的操作,但引入了移动操作后,我们就可以用容器保存它们。
标准库容器、string和shared_ptr类既支持移动也支持拷贝;IO类和unique_ptr类可以移动但不能拷贝
右值引用
为了支持移动操作,新标准引入了一种很难搞的新概念-右值引用:必须绑定到右值的引用(还记得左值表示身份右值表示值吗)。
我们通过&&来获得右值引用,右值引用只能绑定到一个即将销毁的对象,所以啊,我们才能自由地将一个右值引用的资源移动到另一个对象中。
接下来你要记住哪些表达式返回右值,哪些返回左值,这样才好正确绑定:
| 类型 | 表达式 |
|---|---|
| 左值 | 返回左值引用的函数、赋值、下标、解引用、前置递增递减运算符 |
| 右值 | 返回非引用类型的函数、算术、关系、位运算符、后置递增递减运算符 |
- 左值引用就可以绑定到类型为左值的表达式
- 右值引用以及const左值引用可以绑定到类型为右值的表达式
看着有点烦吧,其实也还好,就记住右值是临时的,是即将销毁的,左值长期存在,来几个例子看看:
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本身是变量,是左值
- 左值有持久状态
- 右值要么是字面常量(const),要么是求值过程中创建的临时对象
因为右值引用只能绑定到临时对象:
- 所引用的对象将要被销毁
- 该对象没有用户 因为这样,使用右值引用的代码可以自由地接管所引用的对象的资源
标准库move函数
强行右值,move算是一个移动构造函数:
int a = 12;
int &&b = std::move(a) //move函数告诉编译器,我们要把这个左值当成右值来处理
调用move就意味着:除了对a赋值或销毁外,我们将不再使用它,例如我们不能把它的值赋给别人。
移动构造函数和移动赋值运算符
这两个成员类似对应的拷贝操作,但是它们从给定对象窃取资源而不是拷贝资源
移动构造函数和拷贝构造函数的唯一区别就是它的引用是右值引用
除了完成资源移动,移动构造函数还必须确保移后源对象处于这样一个状态-销毁它是无害的;特别是,一旦资源完成移动,源对象必须不再指向被移动的资源-这些资源的所有权已经归属新对象
我们来为老朋友StrVec定义移动构造函数(注意看,它没有分配新内存哦):
StrVec::StrVec(StrVec &&s) noexcept : elements(s.elements),
first_free(s.first_free), cap(s.cap) //noexcept表示不抛出异常(具体不解释了,先跳过)
{
//上面的列表初始化就移动好了,注意参数是右值引用
//接下来的话保证s进入这样的状态-对其进行析构函数是安全的
s.elements = s.first_free = s.cap = nullptr;
}
千万别忘了函数体里面的那句话,不然销毁移动后源对象就会释放掉我们刚刚移动的内存了。
其实就是s把资源给了新对象,自己都变成空指针,深藏功与名了。
移动赋值运算符
和赋值运算符类似,也要正确处理自赋值情况:
StrVec &StrVec::operator=(StrVec &&rhs) noexcept
{
if(this != &rhs) //检测,不是自赋值再进行下面步骤,是自赋值直接返回
{
free(); //释放已有元素(是左侧对象的,就是this的,因为它要接管rhs的,原来的内存就不用了)
//从rhs窃取资源
elements = rhs.elements;
first_free = rhs.first_free;
cap = rhs.cap;
//将rhs置于可析构状态
rhs.elements = rhs.first_free = rhs.cap = nullptr;
}
return *this;
}
移后源对象要保持有效的,可析构的状态,但最好不要去动它(除了析构它之外),让它安静地功成身退
合成的移动操作
编译器这个好朋友,在某些条件下,还是会给我们合成移动操作的-移动构造函数和移动赋值运算符。
这个某些条件略苛刻:
只有当一个类没有定义任何自己版本的拷贝控制成员,且类的每个非static数据成员都可以移动时(话说有不能移动的吗,有吧,比如自定义的类),编译器才会为它合成移动构造函数或移动赋值运算符
举个栗子:
struct X
{
int i; //内置类型可移动
string s; //string定义了自己的移动操作
}
X x;
X x2 = std::move(x); //调用了合成的移动构造函数
接下来要记住一些东西:
- 移动操作不会隐式定义为删除的函数(你是要用它的)
- 如果我们用=default来要求编译器显式合成移动操作,但是呢,有些成员不能被移动,那编译器怎么办,只好把移动操作都定义为删除的,不让你用了。
下面还有六条,关于何时移动操作是删除的,的准则(这句话很拗口,但下面的准则很有逻辑性):
- 移动构造函数被定义为删除的条件是:
- 有类成员定义了自己的拷贝构造函数但是没定义移动构造函数
- 有类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数
- 有类的移动操作被定义为删除的或是private的,那移动操作就是删除的
- 类似拷贝构造函数,如果类的析构函数被定义为删除的或是private的,那类的移动构造函数被定义为删除的
- 类似拷贝赋值运算符,如果类成员有const或者引用,则类的移动赋值运算符被定义为删除的。
- 一个类定义了自己的移动操作,那合成的移动操作就会被定义为删除的
- 定义了移动操作的类必须也定义拷贝操作,不然,这些成员被合成为删除的
查看3道真题和解析
OPPO公司福利 1133人发布