C++ Primer 读书笔记梳理系列 (四)

本文深入探讨C++11引入的移动语义,解释为何及如何在对象赋值和容器保存中利用移动而非拷贝提升性能。解析右值引用概念,区分左值与右值,展示移动构造函数与赋值运算符的实现细节,以及标准库move函数的使用。讨论合成移动操作的条件,强调移动操作的合理运用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

C++ Primer 读书笔记梳理系列 (四)

这里主要接上一篇文章的拷贝控制,不过这里是C++11的新特性,移动语义

第13章 拷贝控制

对象移动

引入移动对象的原因
  1. 赋值操作时,对象拷贝后就立即销毁,在这些情况下移动对象而非拷贝对象会大幅度提升性能
  2. 有些类型不允许被拷贝(如IO类和unique_ptr),在就标准中我们无法再容器中保存他们,因为它们无法被拷贝,就不存在赋值之类的操作,但引入移动操作后,我们就可以用容器保存
  3. 标准容器库、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赋值或销毁外,我们将不再使用它,例如我们不能把它的值赋给别人

移动构造函数和赋值运算符
  1. 这两个成员类似的拷贝操作,但是它们从给定对象窃取资源而不是拷贝资源
  2. 移动构造与拷贝构造的唯一区别是他的引用是右值引用
  3. 除了完成资源移动,移动构造函数还必须确保移后源对象 处于这样一个状态-销毁它是无害的;特别是,一旦资源完成移动,源对象 必须不再指向被移动的资源-这些资源的所有权已经归属新对象

例如下面

  1. 移动构造函数

    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把资源给了新对象,自己都变成空指针,就释放掉刚刚移动的内存了
    }
    
  2. 移动赋值运算符

    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;
    }
    
  3. 合成的移动操作

    编译器在某些条件下,还会给我们合成移动操作

    条件

    1. 一个类没有定义任何版本的拷贝控制成员(自定义的类如果定义了自己拷贝构造函数、拷贝赋值运算符、或者析构函数,编译器不会为期合成移动操作)
    2. 类的每一个非静态数据成员都可以移动时;(类中类成员变量如果该类有对应的移动操作)

    例子

    struct X
    {
        int i; //内置类型可移动
        string s; //string定义了自己的移动操作
    }
    X x;
    X x2 = std::move(x); //调用了合成的移动构造函数
    
  4. 注意

    1. 移动操作不会隐式定义为删除的函数
    2. 如果我们用=default来要求编译器显式合成移动操作,但是呢,有些成员不能被移动,那编译器怎么办,只好把移动操作都定义为删除的,不让你用了。
  5. 移动操作是删除的条件

    1. 移动构造函数被定义为删除的条件是:
      • 类成员定义了自己拷贝构造函数但是没有定义移动构造函数
      • 类成员未定义自己的拷贝构造函数且编译器不能为其合成移动构造函数
    2. 有类的移动操作被定义为删除的或者private的,那移动操作就是删除
    3. 类似拷贝构造寒素,如果类的析构函数被定义为删除的或者private的,那类的移动构造函数被定义为删除的
    4. 类似拷贝赋值运算符,如果类成员有const或者引用,则类的移动赋值运算符被定义为删除的
    5. 一个类定义了自己的移动操作, 那合成的移动操作就是被定义为删除的
    6. 定义了移动操作的类也必须定义拷贝操作,不然,这些成员被合成为删除的
何时使用移动操作

谁更匹配用哪个

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
注意 : 不要随意的使用移动操作,因为你不知道以后原对象是什么状态,而且你不能再去对他做什么
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值