当前位置: 首页 > news >正文

C++11--右值引用与移动语义

目录

基本概念

左值与右值

左值引用与右值引用

右值引用的使用场景和意义

左值引用的使用场景

右值引用和移动语义

移动构造和拷贝构造的区别

编译器的优化

移动赋值和赋值运算符重载的区别

右值引用的其他应用场景

完美转发

万能引用

完美转发保持值属性

完美转发的使用场景


基本概念

左值与右值

什么是左值?

左值是一个表示数据的表达式,如:变量名或解引用的指针
它的两个特点
·我们可以获取它的地址也可以对它赋值(const修饰除外)

·左值既可以出现在表达式的左边也可以出现在表达式的右边

//可以取地址对象,就是左值
int main()
{int a = 10;int& r1 = a;int* p = &a;int& r2 = *p;const int b = 10;const int& r3 = b;return 0;
}

什么是右值?

右值也是一个表示数据的表达式,如:字面常量、表达式返回值,函数返回值(这个不能是左值引
用返回)等等 
它的两个特点

·右值不能被取地址也不能被修改

·右值只能出现在表达式的右边不能出现在表达式左边

//不能取地址对象,就是右值
int main()
{double x = 1.1, y = 2.2;//常见的右值10;x + y;fmin(x, y);//cout << &fmin(x, y) << endl;return 0;
}

左值引用与右值引用

传统的C++语法中就有引用的语法,而在C++11中更新了右值引用的语法。为了进行区分,我们将C++11之前的引用叫做左值引用,将C++11之后更新的引用叫做右值引用,不论是左值引用还是右值引用,它们的本质都是 “取别名”。

左值引用

左值引用就是对于左值的引用,即对左值取别名,通过&来声明

下面是一段代码示例

// 以下的p b c *p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pv= *p;

右值引用

右值引用就是对右值的引用,即对右值取别名,通过&&来声明

下面是一段代码示例

//不能取地址对象,就是右值
int main()
{double x = 1.1, y = 2.2;//常见的右值10;x + y;fmin(x, y);//cout << &fmin(x, y) << endl;//右值引用int&& rr1 = 10;double&& rr2 = x + y;double&& rr3 = fmin(x, y);return 0;
}
需要注意的是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可
以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地
址,也可以修改rr1.
如果不想rr1被修改,可以用const int&& rr1 去引用.
左值引用可以引用右值吗?
  • 左值引用不能引用右值,左值是可以被修改的而右值是不可以被修改的,这里涉及到一个权限放大的问题
  • 如果想要用左值引用来引用右值,需要用到const关键字来修饰左值引用,因为经过const修饰后左值引用就没有修改的权限了

因此const左值引用可以引用左值也可以引用右值

template<class T>
void func(const T& val)
{cout << val << endl;
}int main()
{string s("hello");func(s);                    //s为变量 左值func("world");              // "world"是常量 右值return 0;
}

右值引用可以引用左值吗?

  • 右值引用只能引用右值不能引用左值
  • 如果想要用右值引用来引用左值,需要用到move函数

move函数是C++11标准提供的一个函数,被move后的左值能够被右值引用引用

int main()
{//左值int* p = new int(0);int b = 1;const int c = 2;//左值引用能否引用右值 -- 不能直接引用,但是const左值引用可以引用右值//void push_back(const T& x)const int& r1 = 10;const double& r2 = x + y;const double& r3 = fmin(x, y);//右值引用能否引用左值 -- 不能直接引用,但是右值引用可以引用move以后左值int*&& rr1 = move(p);int&& rr2 = move(*p);int&& rr3 = move(b);const int&& rr4 = move(c);return 0;
}

右值引用的使用场景和意义

虽然使用const修饰的左值引用能够同时引用左值和右值,但是左值引用终究是存在一些缺陷,而C++11提出的右值引用正是用来解决这些缺陷的

我们写出一个简单string类来

namespace qwe
{class string{public:typedef char* iterator;iterator begin(){return _str;}iterator end(){return _str + _size;}string(const char* str = ""):_size(strlen(str)), _capacity(_size){_str = new char[_capacity + 1];strcpy(_str, str);}//s1.swap(s2)void swap(string& s){::swap(_str, s._str);::swap(_size, s._size);::swap(_capacity, s._capacity);}//拷贝构造string(const string& s):_str(nullptr), _size(0), _capacity(0){cout << "string(const string& s) -- 深拷贝" << endl;string tmp(s._str);swap(tmp);}//赋值重载string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;string tmp(s);swap(tmp);return *this;}~string(){delete[] _str;_str = nullptr;}void reserve(size_t n){if (n > _capacity){char* tmp = new char[n + 1];strcpy(tmp, _str);delete[] _str;_str = tmp;_capacity = n;}}void push_back(char ch){if (_size >= _capacity){size_t newcapacity = _capacity == 0 ? 4 : _capacity * 2;reserve(newcapacity);}_str[_size] = ch;++_size;_str[_size] = '\0';}//string operator+=(char ch)string& operator+=(char ch){push_back(ch);return *this;}string operator+(char ch){string tmp(*this);push_back(ch);return tmp;}const char* c_str() const{return _str;}private:char* _str;size_t _size;size_t _capacity;   };
}

左值引用的使用场景

在说明左值引用的缺陷之前我们先来看它的使用场景

  • 做参数--防止传参时进行拷贝构造
  • 做返回值--防止返回时对返回对象进行拷贝构造
void func1(shy::string s)
{}void func2(const shy::string& s)
{}int main()
{shy::string s("hello world");func1(s);                       func2(s);                        // 左值引用传参s += 'X';                        // 左值引用返回return 0; 
}

func1它传递的参数是形式参数,他是实际参数的一份临时拷贝

func2它传递的参数是s的别名,是左值引用

最后是+=,它返回的也是一份左值引用

string的拷贝是深拷贝,深拷贝的代价是很高的,所以说这里的左值引用效果很明显

左值引用的缺陷
左值引用虽然能避免不必要的拷贝操作 但是缺不能完全避免

左值引用做参数,能够完全避免传参时的拷贝操作
左值引用做返回值,不能完全避免函数对象返回时的拷贝操作
如果函数返回对象是一个局部变量,那么该变量出了局部作用域就会被销毁

这种情况下不能使用左值引用作为返回值,只能传值返回,这就是左值引用的短板

比如说我们实现一个to_string函数 将字符串转化为int类型 此时它的返回值就必须要是值拷贝 如果使用左值引用返回就会返回一个销毁的局部变量

代码表示如下

namespace qwe 
{string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}string str;while (value > 0){int x = value % 10;value /= 10;str += (x + '0');}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}

我们在调用to_string函数返回的时候会调用拷贝构造函数

C++11提出右值引用就是为了解决左值引用的这个缺陷,但是它的解决方法并不是单纯的将右值引用作为返回值

右值引用和移动语义

右值引用和移动语义解决上述问题的方式就是增加移动构造和移动赋值

移动构造

移动构造是一个构造函数 它的参数是右值引用类型

移动构造的本质就是将传入右值的资源转移过来 

代码表示如下

// 移动构造 
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{cout << "string(string&& s)" << endl;swap(s);
}

移动构造和拷贝构造的区别:

在没有增加移动构造之前 由于拷贝构造使用的是const左值引用来接受参数 因此无论是左值还是右值 都会调用拷贝构造函数
增加移动构造之后 由于移动构造采用的是右值引用来接受参数 因此如果拷贝构造对象时传入的是右值 那么就会调用移动构造
 拷贝构造进行的是深拷贝 而移动构造只需要调用swap函数进行资源转移即可 因此移动构造的代价比拷贝构造的代价小很多
给string类增加移动构造之后 对于返回局部string类对象的函数 返回string类对象的时候会调用移动构造进行资源的转移 不会像原来一样进行深拷贝了

演示效果如下

对于to_string当中返回局部的string对象是一个左值 一个临时变量 由于它出了局部作用域就会被销毁 被消耗的值我们将它叫做 “将亡值” 匿名对象也可以被称为 “将亡值”,因此对待这种 “将亡值” 编译器会将它识别为右值 这样就可以匹配搭配参数为右值的移动构造函数

编译器的优化

当一个函数在返回局部对象时,会先用局部对象拷贝出一个临时对象,然后再用这个临时拷贝的对象来拷贝定义的对象

 对于深拷贝的类会进行两次深拷贝 但是大部分编译器为了提高效率都对这种情况进行了优化 优化成了一次深拷贝

效果图如下

如果不进行优化 这里应该会调用拷贝构造和移动构造
如果进行了优化 这里就只会进行一次移动构造了
但是我们如果不是用函数的返回值来构造出一个对象 而是用一个之前已经定义过的对象来接受函数的返回值 这里就无法进行优化了

示例图如下

        对于返回局部对象的函数 就算只是调用函数而不接收该函数的返回值 也会存在一次拷贝构造或移动构造 因为函数的返回值不管接不接收都必须要有 而当函数结束后该函数内的局部对象都会被销毁 所以就算不接收函数的返回值也会调用一次拷贝构造或移动构造生成临时对象

移动赋值

移动赋值是对于赋值运算符重载的一个重载函数 该函数的参数是右值引用类型

在当前的string类中增加一个移动赋值函数 就是调用swap函数将传入右值的资源窃取过来

代码表示如下

// 移动赋值
string& operator= (string && s)
{cout << "string& operatpr=(string&& s)" << endl;swap(s);return *this;
}

移动赋值和赋值运算符重载的区别

在没有增加移动赋值之前 赋值运算符重载是使用const左值引用来接受参数 无论传入的是左值还是右值 都会调用它
增加移动赋值之后 由于移动赋值采用的是右值引用来接受参数 因此如果移动赋值传入的是右值 那么就会调用移动赋值
原本赋值时是调用拷贝构造进行了深拷贝 而移动赋值只需要调用swap函数进行资源转移即可 因此移动赋值的代价比赋值运算符重载小的很多

STL中的容器

以string为例

移动构造

移动赋值

move函数

move函数它并不能移动过任何值 它的功能是将一个左值强制转化为右值引用 然后实现移动语义

move函数的定义如下

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{//forward _Arg as movablereturn ((typename remove_reference<_Ty>::type&&)_Arg);
}
  • move函数模板中_Arg参数的类型不是右值引用而是万能引用 万能引用和右值引用的形式一样 但是右值引用是需要确定的类型
  • 一个左值被move之后它的资源有可能被转移给别的数据了 所以说慎用被move后的左值

右值引用的其他应用场景

插入函数

 如果list中插入的对象类型是string

list<qwe::string> ls;
qwe::string s("1111");ls.push_back(s);                   // 拷贝构造ls.push_back("2222");              // 移动构造
ls.push_back(qwe::string("3333")); // 移动构造
ls.push_back(std::move(s));        // 移动构造
效果如下

完美转发

万能引用

模板中的&&不代表右值引用 而是万能引用 这样它既能接收左值又能接收右值 

template<class T>
void PerfectForward(T&& t)
{}

右值引用和万能引用的区别就是 右值引用需要确定类型 而万能引用会根据传入的类型进行推导 如果传入的实参是一个左值 那么这里的形参t就是左值引用 如果传入的实参是一个右值 那么这里的形参t就是右值引用

下面重载了四个func函数 这四个func函数的参数分别左值引用 const左值引用 右值引用和const右值引用

我们在主函数中使用完美引用模板函数来调用func函数

代码表示如下
 

void Fun(int &x){ cout << "左值引用" << endl; }
void Fun(const int &x){ cout << "const 左值引用" << endl; }void Fun(int &&x){ cout << "右值引用" << endl; }
void Fun(const int &&x){ cout << "const 右值引用" << endl; }void PerfectForward(int&& t)
{Fun(t);
}void PerfectForward(const int& t)
{Fun(t);
}int main()
{PerfectForward(10);           // 右值int a;PerfectForward(a);            // 左值PerfectForward(std::move(a)); // 右值const int b = 8;PerfectForward(b);		      // const 左值PerfectForward(std::move(b)); // const 右值return 0;
}

不管传入何种类型 最后调用的都是左值引用而不是右值引用

因为只要右值经过一次引用之后右值引用就会被储存到特定位置 这个右值就可以被取地址和改 所以在经过一次参数传递之后右值就会退化为左值 如果我们想要让他保持右值的属性 这个时候就要用到完美转发

完美转发保持值属性

要想在参数传递过程中保持其原有的属性 需要在传参时调用forward函数

代码表示如下

//模板中的&& 表示万能引用,既能接收左值又能接收右值
//会退化成左值   --  完美转发  
template<typename T>
void PerFectForeard(T&& t)
{Fun(std::forward<T>(t));
}

完美转发的使用场景

下面提供一个简单的list类 分别提供了左值引用和右值引用的接口函数

	template<class T>struct ListNode{T _data;ListNode* _next = nullptr;ListNode* _prev = nullptr;};template<class T>class list{typedef ListNode<T> node;public://构造函数list(){_head = new node;_head->_next = _head;_head->_prev = _head;}//左值引用版本的push_backvoid push_back(const T& x){insert(_head, x);}//右值引用版本的push_backvoid push_back(T&& x){insert(_head, std::forward<T>(x)); //完美转发}//左值引用版本的insertvoid insert(node* pos, const T& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = x;prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}//右值引用版本的insertvoid insert(node* pos, T&& x){node* prev = pos->_prev;node* newnode = new node;newnode->_data = std::forward<T>(x); //完美转发prev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:node* _head; //指向链表头结点的指针};

定义一个list对象 储存我们之前实现的list类 我们分别传入左值和右值调用不同版本的push_back函数

	qwe::list<qwe::string> lt;qwe::string s("1111");lt.push_back(s);           //调用左值引用版本的push_backlt.push_back("2222");      //调用右值引用版本的push_back

我们在实现push_back的时候复用了insert的代码 对于左值引用的insert函数来说 它会先new一个节点 然后将对应的左值赋值给这个节点 调用赋值运算符重载 又因为赋值运算符重载本质上复用了拷贝构造 
对于右值版本的push_back函数 它复用了insert的代码 对于右值引用的insert函数来说 它会先new一个节点 然后将对应的右值赋值给这个节点 调用移动构造来进行转移资源
这其中调用函数传参的时候多处用到了 完美转发 这是因为如果不使用完美转发就会让右值退化为左值 最终导致多一次深拷贝 从而降低效率
 
如果我们想要保持右值的属性 每次传参的时候就必须要使用完美转发

与STL中的list的区别

如果将刚才测试代码中的list换成STL当中的list

调用左值版本的push_back插入节点时 在构造结点时会调用string的拷贝构造函数
调用右值版本的push_back插入节点时 在构造结点时会调用string的移动构造函数
而我们实现的list代码却使用的是赋值运算符重载和移动赋值

这是因为我们是使用的new操作符来申请空间 new操作符申请空间之后会自动调用构造函数进行初始化

而初始化之后就只能使用赋值运算符重载了

而STL库中使用空间配置器获取内存 因此在申请到内存后不会调用构造函数对其进行初始化 是后续用左值或右值对其进行拷贝构造 所以会产生这样子的结果

如果我们想要达到STL中的效果 我们只需要使用malloc开辟空间 然后使用定位new进行初始化就可以了

http://www.lryc.cn/news/1236.html

相关文章:

  • Python SQLAlchemy入门教程
  • 你是真的“C”——操作符详解【下篇】+整形提升+算术转换
  • 文本匹配SimCSE模型代码详解以及训练自己的中文数据集
  • Biotin-PEG-FITC 生物素聚乙二醇荧光素;FITC-PEG-Biotin 科研用生物试剂
  • FISCO BCOS 搭建区块链,在SpringBoot中调用合约
  • 面试官:int和Integer有什么区别?
  • MFC常用技巧
  • C++ —— 多态
  • java agent设计开发概要
  • node.js笔记-模块化(commonJS规范),包与npm(Node Package Manager)
  • Linux 磁盘坏块修复处理(错误:read error: Input/output error)
  • API 面试四连杀:接口如何设计?安全如何保证?签名如何实现?防重如何实现?
  • 操作系统题目收录(六)
  • 2023年十款开源测试开发工具推荐!
  • MySQL慢查询分析和性能优化
  • C++学习笔记(四)
  • 【4】深度学习之Pytorch——如何使用张量处理时间序列数据集(共享自行车数据集)
  • mulesoft MCIA 破釜沉舟备考 2023.02.10.01
  • 干货 | PCB拼板,那几条很讲究的规则!
  • 笔试题-2023-思远半导体-数字IC设计【纯净题目版】
  • canvas根据坐标点位画图形-canvas拖拽编辑单个图形形状
  • JavaEE 初阶 — 确认应答机制
  • 0207 事件
  • SpringBoot整合Swagger
  • 20230210英语学习
  • 【图像处理OpenCV(C++版)】——4.5 全局直方图均衡化
  • 2022年API安全研究报告
  • 【内网安全-横向移动】基于SMB协议-PsExec
  • whistle 一个神奇的前端调试工具(抓包\代理工具)
  • node.js下载和vite项目创建以及可能遇到的错误