文章目录1. 值类别描述表达式怎样表示对象1.1 左值 lvalue有身份可以取地址1.2 纯右值 prvalue临时值没有持久身份1.3 将亡值 xvalue即将消亡的对象可以被移动2. 引用绑定与重载选择2.1 move 提供转换不直接完成移动3. 临时对象寿命的延长有适用条件4. 用 Buffer 看清资源所有权4.1 深拷贝保留两份独立资源4.2 移动构造转交现有资源5. 默认移动不等于默认转交资源所有权5.1 default、delete 与零法则 / 五法则6. 返回值优化与移动解决不同层次的问题7. 小结1. 值类别描述表达式怎样表示对象int、std::string是类型而左值、纯右值、将亡值是表达式的值的类别。intx1;constintcx2;int*px;std::string texthello;表达式值类别此处的意义x、cx、*p、text[0]左值 lvalue表示有身份的对象是否能修改还要看类型及 const42、整数的x 1、临时对象std::string(hi)纯右值 prvalue计算数值或产生初始化所需的值std::move(text)将亡值 xvalue仍表示原对象但允许匹配可接管资源的接口纯右值和将亡值统称右值左值和将亡值统称泛左值。1.1 左值 lvalue有身份可以取地址表达式代表一个实实在在、有独立内存位置的对象。能不能修改则要具体看对象有没有 const 修饰int x 1;表达式x左值可以修改const int cx 2;表达式cx左值但带 const不能修改*p、text[0]都是左值可以取地址一句话概括能拿到稳定内存地址的表达式是左值1.2 纯右值 prvalue临时值没有持久身份用来做计算、初始化用完就销毁没有长期内存地址。字面量42计算结果x 1临时对象std::string(hi)一句话概括临时数字、临时对象一次性的值纯右值1.3 将亡值 xvalue即将消亡的对象可以被移动对象内存还在但是我们告诉编译器这个对象的资源可以被接管、偷走移动语义。例子std::move(text)std::move 不会移动数据它仅仅是强制类型转换把左值转成将亡值。对象本体还在原地只是标记成 “资源可以被拿走”。2. 引用绑定与重载选择之前了解过的T称为左值引用。C11 新增的T称为右值引用两者都能为对象建立别名。对类型匹配的普通整数可以先记住这组关系引用普通左值const 左值非 const 右值int可以不可以不可以const int可以可以可以int不可以不可以可以intx10;constintcx20;intr1x;//普通左值绑定int// int r2 cx; //不能把int绑const左值// int r3 30; //不能把int绑右值constintcr1x;constintcr2cx;constintcr330;// int rr1 x; //右值引用不能绑普通左值intrr230;//绑定右值intrr3std::move(x);//move转成将亡值属于右值才可以绑定左值引用T绑定左值别名可以修改非 const 版本右值引用T绑定右值prvalue/xvalue作用是接管临时对象资源移动构造 / 移动赋值。const 左值引用能绑定所有情况常见用法是函数参数用它来接收临时对象。绑定右值时临时对象生命周期会延长到引用销毁。#includeiostream// 重载三个函数用于判断具体接收类型voidSelect(int){std::coutintstd::endl;}voidSelect(constint){std::coutconst intstd::endl;}voidSelect(int){std::coutintstd::endl;}intmain(){intx1;constintcx2;// named是【右值引用变量】但变量本身作为表达式是左值intnamed3;Select(x);// x普通非const左值优先匹配 intSelect(cx);// cxconst左值只能匹配 const intSelect(4);// 4纯右值优先匹配 intSelect(named);// named非const左值 → 匹配 intSelect(std::move(named));// std::move转成将亡值(右值)匹配 intSelect(std::move(cx));// cx是constmove后得到 const int只能绑定const intintaliasstd::move(x);// alias是右值引用绑定x转成的将亡值alias9;// alias是x的别名修改alias等价修改原变量xstd::coutx: xstd::endl;return0;}运行示例named的类型是int右值引用它绑定的对象是临时的3但变量名named作为表达式是非 const 左值。2.1 move 提供转换不直接完成移动对于普通对象xstd::move(x)可以理解为将它转换成可匹配右值引用的表达式。它在头文件utility中声明并不调用某个对象的移动构造函数也不修改对象内容。很多编译器GCC、MSVC里iostream内部间接 include 了utility所以只写#include iostream也能找到std::move代码可以编译通过。3. 临时对象寿命的延长有适用条件直接用局部引用绑定相应的临时对象可以按照规则延长其寿命conststd::stringreadstd::string(hello);std::stringeditstd::string(world);edit!;两个临时字符串都可以活到对应局部引用的寿命作用域结束。区别是不能通过read去修改字符串而edit可以修改。但是不能概括为“只要最后还有引用临时对象就一直活着”。延长寿命不是一条可以不断传递的链// 错误示例片段不执行后续悬空访问。conststd::stringIdentity(conststd::stringvalue){returnvalue;}conststd::stringbadIdentity(std::string(hello));// 上面的临时对象只活到该完整表达式结束bad 此后悬空。std::stringBadReturn(){std::string localhello;returnstd::move(local);// 返回的引用指向即将销毁的局部对象。}函数引用参数绑定的临时对象通常只保持到包含调用的完整表达式结束返回它的引用不会再次延长寿命。把局部变量转换成右值也不会改变局部变量离开作用域时被销毁的事实。因此创建一个独立结果时应该正常按值返回。移动语义和拷贝省略可以帮助实现高效的按值返回所以就没有必要为了“省一次复制”返回悬空引用。4. 用 Buffer 看清资源所有权下面的Buffer管理着一段int数组只保留必要的构造、赋值、析构和访问接口。类的基本约定是非空对象独占一段new[]得到的数组空对象保存nullptr和长度 0所有数组最终配对delete[]。拷贝得到独立数组移动则转移现有数组。#includealgorithm#includeiostream#includeutilityclassBuffer{public:// 构造函数分配size个int数组默认初始化为0explicitBuffer(std::size_t size0):data_(size?newint[size]():nullptr),size_(size){}// 拷贝构造深拷贝新建内存并复制内容Buffer(constBufferother):Buffer(other.size_){if(size_!0)std::copy(other.data_,other.data_size_,data_);copies;// 拷贝计数1}// 移动构造接管other的资源不拷贝内存other置空Buffer(Bufferother):data_(other.data_),size_(other.size_){other.data_nullptr;other.size_0;moves;// 移动计数1}// 拷贝赋值Bufferoperator(constBufferother){if(this!other){Buffertemporary(other);// 先构造临时对象触发拷贝构造swap(temporary);// 交换临时对象和当前对象资源}return*this;}// 移动赋值Bufferoperator(Bufferother)noexcept{if(this!other){delete[]data_;// 释放自己旧内存data_other.data_;size_other.size_;other.data_nullptr;// 源对象置空防止析构重复释放other.size_0;}return*this;}// 析构函数释放堆数组~Buffer(){delete[]data_;}// 交换两个Buffer的成员voidswap(Bufferother){std::swap(data_,other.data_);std::swap(size_,other.size_);}std::size_tsize()const{returnsize_;}int*data(){returndata_;}constint*data()const{returndata_;}// 下标重载读写intoperator[](std::size_t index){returndata_[index];}constintoperator[](std::size_t index)const{returndata_[index];}// 静态统计拷贝构造次数、移动构造次数staticintcopies;staticintmoves;private:int*data_;std::size_t size_;};// 静态成员初始化intBuffer::copies0;intBuffer::moves0;intmain(){Buffera(3);// 普通构造开辟3个int数组a[0]7;// a数组第0位赋值7Buffer ba;// 调用拷贝构造b是a的深拷贝copies 1Buffer cstd::move(a);// 移动构造c接管a的内存a变成空moves 1// b是深拷贝独立内存c接管a原来的内存std::coutb/c: b[0]/c[0]std::endl;std::couta size: a.size()std::endl;// a被移动后size 0b[0]9;// 修改b不会影响c两块独立内存std::coutindependent c: c[0]std::endl;Bufferd(1);// 普通构造大小为1dstd::move(c);// 移动赋值d释放旧内存接管c资源c置空std::coutassigned/source size: d[0]/c.size()std::endl;Bufferselfd;dstd::move(self);// 自移动赋值this other直接跳过逻辑d不变std::coutself move: d[0]std::endl;constBufferfixed(2);// const对象Buffer estd::move(fixed);// fixed是conststd::move(fixed)得到const右值不能匹配移动构造走拷贝构造 copies 1std::coutconst source/copy size: fixed.size()/e.size()std::endl;// 输出拷贝、移动构造的总次数std::coutcopy/move constructors: Buffer::copies/Buffer::movesstd::endl;return0;}运行示例4.1 深拷贝保留两份独立资源Buffer b a;使用拷贝构造深拷贝。此时a与b中的元素值相同但数组地址不同。b[0] 9;不影响另一个数组的元素。对于这个Buffer复制长度为 N 的数组需要 O(N) 时间。源对象为空时也必须正确处理因此代码在长度为零时跳过指针区间拷贝避免了围绕空指针做不必要的操作。4.2 移动构造转交现有资源Buffer c std::move(a);匹配Buffer(Buffer)。构造函数读取源指针和长度把它们保存进c再把a设为约定的空状态。移动构造的目标对象刚出生没有旧数组需要处理。这个实现只处理固定数量的成员所以是 O(1)。移动赋值则不同目标d已经拥有自己的数组必须先释放再接管c的资源。若管理的是 N 个析构昂贵的对象释放目标旧资源本身可能需要 O(N)。this ! other处理自移动。此个 buffer 类明确选择了自移动时不改变内容。5. 默认移动不等于默认转交资源所有权C11 可以隐式声明移动构造和移动赋值但它们的出现有条件。通常要求该类没有用户自己声明的拷贝构造、拷贝赋值、移动构造、移动赋值和析构函数。所以如果自己声明了其中某些特殊成员就会抑制隐式移动的生成。#includeiostream#includeutilitystructMember{Member()default;Member(constMember){std::coutmember copystd::endl;}Member(Member){std::coutmember movestd::endl;}};structNatural{Member member;};structWithDestructor{Member member;~WithDestructor()default;};structRestored{Member member;Restored()default;~Restored()default;Restored(constRestored)default;Restored(Restored)default;};intmain(){Natural a;Naturala2(std::move(a));WithDestructor b;WithDestructorb2(std::move(b));Restored c;Restoredc2(std::move(c));}运行示例Natural没有这些用户声明的特殊成员隐式移动构造会尝试移动其成员。WithDestructor声明了析构函数隐式移动被抑制传入右值仍然可以调用接收const的隐式复制构造。Restored显式请求默认移动构造成员再次被移动。对于原始指针成员默认移动通常只是复制指针值不会自动把源指针置空。如果两个对象都在析构时释放这个指针仍可能会造成重复释放。5.1 default、delete 与零法则 / 五法则写法或习惯实际作用Type() default;请求默认构造实现成员是否有合理初值仍需检查Type(const Type) delete;禁止这个拷贝构造重载被使用Type operator(const Type) delete;禁止拷贝赋值显式声明移动构造或移动赋值隐式复制成员会受到影响可能被定义为删除函数类仅组合string、vector等资源管理成员通常不需要自己写资源管理特殊成员即常说的零法则零法则类只用标准库 RAII 容器不需要手写 5 个特殊成员。管理裸资源时要一起审视析构函数、拷贝构造、拷贝赋值、移动构造、移动赋值这通常被称为五法则。只要手动写了其中一个通常就要全部处理。 delete还可以用于普通函数或重载例如void Set(int); void Set(double) delete;以阻止不希望发生的调用。删除函数仍参与重载选择选中它之后程序不合法。6. 返回值优化与移动解决不同层次的问题下面两种写法看起来接近实际却分别是初始化和赋值// 片段。BufferMakeBuffer(){Bufferlocal(100);returnlocal;}Buffer firstMakeBuffer();// 创建新对象。Buffer second;secondMakeBuffer();// 给已有对象赋值。NRVO命名返回值优化编译器优化函数内局部对象直接在外部接收对象的内存上构造省去拷贝 / 移动。返回局部对象时编译器可能进行命名返回值优化 NRVO将局部对象直接构造在函数返回结果对象的位置。C11 中再用这个返回结果初始化调用方的新对象还可以继续省略一次复制或移动因此通常直接写return local;。如若写成return std::move(local);的话往往会阻碍 NRVO。7. 小结右值引用T只能绑定到非 const 右值用来识别临时对象、即将销毁的对象。std::move本质只是类型转换把对象转为右值引用本身不移动、不修改内存。移动语义转移堆资源所有权不拷贝数据移动后源对象进入合法但未指定的有效状态。移动构造函数接收T参数接管源对象资源源对象置空开销远小于深拷贝。移动赋值运算符释放自身旧资源接管传入对象资源需要做自赋值判断。