C++ Primer 读书笔记梳理系列 (四)
这里主要接上一篇文章的拷贝控制,不过这里是C++11的新特性,移动语义
第13章 拷贝控制
对象移动
引入移动对象的原因
- 赋值操作时,对象拷贝后就立即销毁,在这些情况下移动对象而非拷贝对象会大幅度提升性能
- 有些类型不允许被拷贝(如IO类和unique_ptr),在就标准中我们无法再容器中保存他们,因为它们无法被拷贝,就不存在赋值之类的操作,但引入移动操作后,我们就可以用容器保存
- 标准容器库、string和shared_ptr类既支持移动也支持拷贝;IO类和unique_ptr可以移动但是不能拷贝
右值引用
右值引用是新标准为了支持移动操作而提出的一个概念。用&&来获得右值引用,右值引用只能绑定到一个即将销毁的对象,所以我们才能自由地将一个右值引用的资源移动到另一个对象的
哪些表达式返回右值哪些返回左值:
类型 | 表达式 |
---|---|
左值 | 返回左值引用的函数、赋值、下标、解引用、前置递增递减运算符 |
右值 | 返回非引用类型的函数、算术、关系、位运算符、后置递增递减运算符 |
- 左值引用就是可以绑定到类型为左值的表达式
- 右值引用以及const左值引用可以绑定到类型为右值得表达式
右值是临时的,是即将销毁的,左值是长期存在的
int i = 42;
int &r = i; //正确, r引用i
int &&rr = i; //错, 不能将一个右值引用绑定到一个左值上
int &r2 = i * 24; //错,是一个右值
const int &r3 = i * 13; //对 可以将一个const左值绑定到右值引用上面
int &&rr2 = i * 2; //对 将仍然rr2绑定到乘法结果上
int &&rr3 = 42; //正确,字面常量
int &&rr4 = rr3; //错误,rr3本身是变量,是左值
- 左值有持久状态,变量是左值,即使这个变量是右值引用也不行
- 右值要么是字面常量,要么是求职过程中创建的临时对象
- 所有引用的对象将要被销毁
- 该对象没有其他用户
标准库move函数
强制右值,move算是一个移动构造函数
int a = 12;
int &&b = std::move(a) //move函数告诉编译器,我们要把这个左值当成右值来处理
调用move就意味着:除了对a赋值或销毁外,我们将不再使用它,例如我们不能把它的值赋给别人
移动构造函数和赋值运算符
- 这两个成员类似的拷贝操作,但是它们从给定对象窃取资源而不是拷贝资源
- 移动构造与拷贝构造的唯一区别是他的引用是右值引用
- 除了完成资源移动,移动构造函数还必须确保移后源对象 处于这样一个状态-销毁它是无害的;特别是,一旦资源完成移动,源对象 必须不再指向被移动的资源-这些资源的所有权已经归属新对象
例如下面
-
移动构造函数
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; }
-
合成的移动操作
编译器在某些条件下,还会给我们合成移动操作
条件 :
- 一个类没有定义任何版本的拷贝控制成员(自定义的类如果定义了自己拷贝构造函数、拷贝赋值运算符、或者析构函数,编译器不会为期合成移动操作)
- 类的每一个非静态数据成员都可以移动时;(类中类成员变量如果该类有对应的移动操作)
例子
struct X { int i; //内置类型可移动 string s; //string定义了自己的移动操作 } X x; X x2 = std::move(x); //调用了合成的移动构造函数
-
注意
- 移动操作不会隐式定义为删除的函数
- 如果我们用=default来要求编译器显式合成移动操作,但是呢,有些成员不能被移动,那编译器怎么办,只好把移动操作都定义为删除的,不让你用了。
-
移动操作是删除的条件
- 移动构造函数被定义为删除的条件是:
- 类成员定义了自己拷贝构造函数但是没有定义移动构造函数
- 类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数
- 有类的移动操作被定义为删除的或者private的,那移动操作就是删除
- 类似拷贝构造寒素,如果类的析构函数被定义为删除的或者private的,那类的移动构造函数被定义为删除的
- 类似拷贝赋值运算符,如果类成员有const或者引用,则类的移动赋值运算符被定义为删除的
- 一个类定义了自己的移动操作, 那合成的移动操作就是被定义为删除的
- 定义了移动操作的类也必须定义拷贝操作,不然,这些成员被合成为删除的
- 移动构造函数被定义为删除的条件是:
何时使用移动操作
谁更匹配用哪个
StrVec v1, v2;
v1 = v2; //拷贝赋值
StrVec getVec(istream &); //函数声明,返回非引用,即返回右值
v2 = getVec(cin); //移动赋值
如果一个类有拷贝构造函数而没有移动构造函数,那其对象的移动是通过拷贝来完成的,这一点对于拷贝赋值运算符和移动赋值运算符也适用。
class Foo
{
public:
Foo() = default; //强行合成默认构造函数
Foo(const Foo&); //自定义拷贝构造函数(函数声明)
//所以说,Foo没有移动构造函数
};
Foo x;
Foo y(x); //直接初始化,调用拷贝构造函数,因为x是左值
Foo z(std::move(x)); //还是调用拷贝构造函数,虽然我们把x当右值用,但人家没有移动操作啊
移动赋值运算符和拷贝赋值运算符可以是同一个函数
class HasPtr
{
public:
HasPtr(HasPtr &&p) noexcept : ps(p.ps), i(p.i) {p.ps = 0;} //移动构造函数
//下面这个函数既是移动赋值运算符又是拷贝赋值运算符,为什么待会说
HasPtr& operator=(HasPtr rhs)
{
swap(*this, rhs);
return *this;
}
};
注意上面的函数是值传递,值传递需要使用拷贝构造函数或者移动构造函数,这取决于实参的类型,左值拷贝右值移动
HasPtr hp, hp2; //调用默认构造函数初始化
hp = hp2; //hp2作为变量是个左值,调用拷贝构造函数来赋值
hp = std::move(hp2); //强行右值,调用移动构造函数来移动hp2