怎样提高网站浏览量,机械加工网名怎么起,在线听音乐网站建设,中国工商注册营业执照的官网文章目录C11为什么引入右值#xff1f;区分左值引用、右值引用move移动语义移动构造函数移动赋值运算符合成的移动操作小结引用限定符规定this是左值or右值引用限定符与重载C11为什么引入右值#xff1f;
C11引入了一个扩展内存的方法——移动而非拷贝#xff0c;移动较之拷…
文章目录C11为什么引入右值区分左值引用、右值引用move移动语义移动构造函数移动赋值运算符合成的移动操作小结引用限定符规定this是左值or右值引用限定符与重载C11为什么引入右值
C11引入了一个扩展内存的方法——移动而非拷贝移动较之拷贝有两个优点
效率更高 在此之前当数据结构申请的内存用尽时一般是申请一块更大的内存然后将旧内存中存储的元素拷贝到新内存中。但很多情况下为了方便拷贝操作而建立的临时对象在拷贝完成后就被销毁了因此不如直接将旧内存中的元素移动到新内存中即省空间临时对象也是要占内存的还省时间不用建立临时对象了。IO、unique_ptr 这样的类都包含不可被共享的资源如指针或IO缓冲因此这些类不支持拷贝仅支持移动。
PSSTL 和 shared_ptr 既支持移动也支持拷贝。
而为了支持移动操作就诞生了一种新的引用类型——右值引用rvalue reference。
为了与左值引用进行划分使用 时则代表是左值引用而使用 则代表右值引用。
右值引用有一个重要的特性——只能绑定到一个将要销毁的对象。 区分左值引用、右值引用 左值 生成左值 返回引用的函数、赋值、取下标、解引用、前置递增/递减运算符。
我们可以将一个 左值引用 绑定到这类表达式的结果上。 右值 生成右值 返回非引用类型的函数、算术、关系、位、后置递增/递减运算符。
我们可以将一个 const的左值引用 或者一个 右值引用 绑定到这类表达式上。
举一些例子
int i 42;
int r i; // 正确左值引用绑定变量
int rr i; // 错误不能将右值引用绑定到左值上
int r2 i * 42; // 错误i*42是右值不能将左值引用绑定到右值上
const int r3 i * 42; // 正确可以将const左值引用绑定到右值上
int rr2 i * 42; // 正确右值引用可以绑定到算术结果上详细来讲
普通类型的变量因为有名字可以取地址都认为是左值。const修饰的常量不可修改只读类型的理论应该按照右值对待但因为其可以取地址C11认为其是左值。(const类型常量初始化时编译器不给其开辟空间当对该常量取地址时编译器才为其开辟空间。)如果表达式的运行结果是一个临时变量或者对象认为是右值。如果表达式运行结果或单个变量是一个引用认为是左值。
总的来讲即为左值持久、右值短暂左值有持久的状态而右值要么是字面常量、要么是在表达式求值过程中创建的临时对象。
由于右值引用只能绑定到临时对象我们得知
所引用的对象将要被销毁该对象没有其他用户
这两个特性意味着可以自由地接管右值引用绑定的资源而不必担心发生错误。
有趣的是右值引用本身是一个变量因此它是一个左值也就是说不能将右值引用绑定到一个右值引用类型的变量上
int rr1 42; // 正确字面常量是右值
int rr2 rr1; // 错误表达式rr1是左值move
按照语法来说右值引用应该只能引用右值但我们可以通过move函数显式地将一个左值转换为对应的右值引用类型
#includeutility //move的头文件
int rr1 42; // 右值引用
int rr2 std::move(rr1); // rr1是左值绑定到右值引rr2上调用move就意味着可以销毁一个移后源对象rr1也可以赋予它新值但不能使用一个移后源对象rr1的值。
与大多数标准库名字的使用不同对 move 我们不提供 using声明。换言之我们直接调用 std::move 而不是 move。因为 STL 还有另一个 move那个的作用就是将一个范围中的元素搬移到另一个位置。 移动语义
移动构造函数
类似拷贝构造函数移动构造函数的第一个参数是该类类型的引用任何额外的参数都必须有默认实参。不同于拷贝构造函数的是这个引用参数在移动构造函数中是一个右值引用。
除了完成资源移动移动构造函数还必须确保移后源对象是可销毁的。 一旦资源完成移动源对象必须不再指向被移动的资源——这些资源的所有权已经归属新创建的对象。
作为一个例子我们为 IntVec类 定义移动构造函数实现从一个 IntVec 到另一个 IntVec 的元素移动而非拷贝
class IntVec // IntVec是对标准库vector类的模仿仅存储int元素
{int *begin; // 指向已分配的内存中的首元素int *end; // 指向最后一个实际元素之后的位置int *cap; // 指向分配的内存末尾之后的位置
public:IntVec(IntVec a) noexcept // noexcept通知标准库不抛出任何异常: begin(a.begin), end(a.end), cap(a.cap) // 成员初始化器接管a中的资源{a.begin a.end a.cap nullptr;// 令a进入可销毁状态确保对其运行析构函数是安全的。}
}; 工作流程 移动构造函数不分配任何新内存而是接管给定的 IntVec 中的内存。接管之后将给定对象中的指针都置为 nullptr 。函数体执行完毕自动调用析构函数销毁移后源对象。
在第三点中如果我们没有进行第二点此时移后源对象仍指向被接管的内存此时调用析构函数会释放掉刚刚移动的内存因此三步一步都不能少。 关于 noexcept 由于移动操作不分配任何资源因此不会抛出异常我们可以通知标准库这样他就不会因为需要等待处理异常而浪费资源。noexcept 是通知标准库的方式之一出现在参数列表和初始化列表开始的冒号之间。 为什么移动操作不会抛出异常 首先明确一定是允许移动操作抛出异常的但是这么做反而有坏处。
以 vector 的 push_back 操作来讲当执行尾插操作但是内存空间已经满了需要重新分配内存空间此时
如果重新分配过程使用了移动构造函数且在移动了部分元素后抛出了一个异常就会产生问题——旧空间中的移动源元素已经被改变了而新空间中移动源元素尚未构造好。在此情况下vector 将丢失自身的部分元素。如果 vector 使用了拷贝构造函数当在新内存中构造元素时旧内存中的元素保持不变。如果此时发生了异常vector 可以释放新分配的但还未成功构造的内存并返回。vector 原有的元素仍然存在。
因此对于移动操作来讲不抛出异常反而能保证数据的完整性。 移动赋值运算符
和移动构造函数一样——不抛出异常但仍要注意处理所有赋值运算符逃不过的劫难——自赋值问题。
IntVec IntVec::operator(IntVec rhs) noexcept{if(this ! rhs){ // 处理非自赋值free(); // 释放已有资源begin rhs.begin; // 从 rhs 接管资源end rhs.end;cap rhs.cap;// 将 rhs 置于可析构状态rhs.begin rhs.end rhs.cap nullptr;}return *this;
}这种写法其实是最常用也最简单的自赋值处理方法像之前讲的 用临时量存右侧运算对象、swap实现自赋值 。巧妙则巧妙但是写起来一定要很小心远不如直接 if-else 来的方便。 合成的移动操作
如果我们不声明自己的拷贝构造函数或拷贝赋值运算符编译器总会为我们合成这些操作。但与拷贝操作不同如果一个类定义了自己的拷贝构造函数、拷贝赋值运算符或者析构函数编译器就不会为它合成移动构造函数和移动赋值运算符了。如果一个类没有移动操作通过正常的函数匹配类会使用对应的拷贝操作来代替移动操作。
只有当一个类没有定义任何自己版本的拷贝控制成员且类的每个 非static数据成员 都可以移动时编译器才会为它合成移动操作。
编译器可以移动内置类型的成员。如果一个成员是类类型且该类有对应的移动操作编译器也能移动这个成员 小结
在移动操作之后移后源对象必须保持有效的、可析构的状态。 移后源对象仍然保持有效 我们可以对它执行诸如 empty 或 size 这些操作。但是我们不知道将会得到什么结果。我们可能猜测一个移后源对象是空的但结果并不一定如我们猜测的那样。换言之我们可以重新用它但是我们不知道用之前它是什么状态。 同时存在拷贝控制操作和移动操作时的匹配规则 拷贝构造函数接受一个 const 类型名 的左值引用类型移动构造函数接受一个 类型名 右值引用类型。
因此左值只能匹配拷贝构造函数但是右值却都可以匹配只是调用拷贝操作时需要进行一次到 const 的转换而移动操作是精确匹配因此右值会使用移动操作。 swap实现一个赋值运算符既是拷贝操作也是移动操作 移动赋值运算符接受一个 类型名 右值引用类型拷贝赋值运算符接受一个 const 类型名 的左值引用类型。
因此我们可以在已经定义好移动构造函数的基础上借助 swap函数 实现一个形参为 类型名 的赋值运算符
class IntVec
{
public:IntVec(IntVec a) noexcept: begin(a.begin), end(a.end), cap(a.cap){a.begin a.end a.cap nullptr;}IntVec operator(IntVec a){swap(*this, a);return *this;}
}; 具体思想我们在上一篇博客的swap实现自赋值中讲过一次这里简单再提一下。
首先 swap函数 是类自己重载的而不是标准库中的 swap函数目的是避免浪费内存。一定要确保类已经定义好了移动构造函数否则像我们之前说过的那样在有拷贝操作的情况下类不会合成移动操作则该赋值运算符只实现了拷贝操作而没有实现移动操作。该赋值运算符最终实现的操作由传入的实参类型决定左值拷贝、右值移动。
举个例子
// 假定 v1、v2 都是 IntVec 对象
v1 v2; // v2是左值拷贝构造函数来拷贝v2
v1 std::move(v2); // 移动构造函数移动v2匹配详情就不多说了在上文的匹配规则中讲的很详细了这里主要想体现的是不管使用的是拷贝构造函数还是移动构造函数赋值运算符都可以将他们的结果作为实参来执行。换言之配合上 swap函数 的 赋值运算符 同时支持 移动操作 和 拷贝操作 。 为什么拷贝操作的形参通常是 const X 而不是 X移动操作的形参通常是 X 而不是 const X 当我们希望使用 将亡值 时通常传递一个右值引用。为了在移动后释放源对象持有的资源实参不能是 const 的。从一个对象进行拷贝的操作不应该改变该对象。因此通常不需要定义一个接受一个 普通的X 参数的版本。 引用限定符
规定this是左值or右值
有时会看到这样的代码
string s1 hello, s2 world;
s1 s2 !;此处我们对两个 string 的连接结果——一个右值进行了赋值。
在旧标准中我们没有办法阻止这种使用方式。为了维持向后兼容性新标准库类仍然允许向右值赋值。但是我们有时需要阻止这种用法。在此情况下我们希望强制左侧运算对象即this指向的对象是一个左值。
我们指出 this 的左值/右值属性的方式与定义 const 成员函数相同即在参数列表后放置一个引用限定符reference qualifier
class IntVec
{
public:IntVec operator(IntVec a) // 只能向可修改的左值赋值{swap(*this, a);return *this;}
}; 引用限定符可以是 或 分别指出 this 可以指向一个左值或右值。类似 const 限定符引用限定符只能用于非static成员函数且必须同时出现在函数的声明和定义中。
一个函数可以同时用 const 和 引用限定。在此情况下引用限定符必须跟随在const限定符之后
class IntVec
{
public:IntVec operator(IntVec a) const
}; 引用限定符与重载
就像一个成员函数可以根据是否有 const 来区分其重载版本一样引用限定符也可以区分重载版本。
举个例子
编译器会根据调用 sorted 的对象的左值/右值属性来确定使用哪个 sorted 版本
当我们定义 const成员函数 时可以定义两个版本唯一的差别是一个版本有 const限定 而另一个没有。引用限定的函数则不一样。如果我们定义两个或两个以上具有相同名字和相同参数列表的成员函数就必须对所有函数都加上引用限定符或者所有都不加。