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

【C++11 ——— 右值引用和移动语义】

C++11 ——— 右值引用和移动语义

  • 右值引用和移动语义
    • 左值引用和右值引用
    • 左值引用与右值引用比较
    • 右值引用使用场景和意义
      • 左值引用的使用场景:
      • 左值引用的短板:
      • 左值引用中编译器的优化
      • 右值引用和移动语义
      • 右值引用引用左值
      • 右值引用的其他使用场景
  • 完美转发
    • 万能引用
  • 完美转发
    • 模板中的&& 万能引用
    • 完美转发的使用场景
    • 引用折叠

右值引用和移动语义

左值引用和右值引用

传统的C++语法中就有引用的语法,而C++11中新增了的右值引用语法特性,所以从现在开始我们之前学习的引用就叫做左值引用。无论左值引用还是右值引用,都是给对象取别名。

什么是左值?什么是左值引用?

左值(Lvalue)
 左值是指可以取地址的表达式,通常是具名变量或对象。左值在赋值表达式中出现在等号的左边,表示一个持久的对象。左值可以通过取地址符(&) 获取其内存地址。

特征

  • 可以取地址。
  • 具有持久的生命周期,直到其作用域结束。
  • 例如:变量名、数组元素、解引用的指针等。

左值引用就是给左值的引用,给左值取别名。

左值引用(Lvalue Reference)
 左值引用是对左值的引用,使用符号 & 声明。左值引用可以绑定到左值,允许通过引用来访问和修改原始对象。

特征

  • 只能绑定到左值。
  • 可以通过左值引用修改原始对象的值。
  • 例如:int a = 10; int& ref = a;,ref是a的左值引用。

示例:

int main()
{// 以下的p、b、c、*p都是左值int* p = new int(0);   // p是指向动态分配的int对象的指针,是左值int b = 1;            // b是int变量,是左值const int c = 2;      // c是const int变量,是左值// 以下几个是对上面左值的左值引用int*& rp = p;         // rp是对p的左值引用,是左值int& rb = b;          // rb是对b的左值引用,是左值 const int& rc = c;    // rc是对c的const左值引用,是左值int& pvalue = *p;     // pvalue是对*p的左值引用,是左值// *p是对动态分配的int对象的解引用,是左值// new int(0)是动态分配int对象的右值表达式// 1和2是int字面值,是右值return 0;
}

右值(Rvalue)
 右值是指不能取地址的表达式,通常是临时对象,字面常量,表达式返回值,函数返回值。右值在赋值表达式中出现在等号的右边,表示不持久的值。C++11将右值细分为纯右值(prvalue)和将亡值(xvalue)。

右值的分类:

  • 纯右值(prvalue):
    表示临时对象不与任何对象关联的值
    例如:字面量(如1、true)、函数返回的非引用值(如int func() { return 42; })、表达式结果(如a + b)。
  • 将亡值(xvalue):
    表示即将被移动的对象,通常是一个临时对象的引用
    例如:通过std::move转换的对象、返回右值引用的函数。
#include <cmath> // 引入cmath库以使用fmin函数int main()
{double x = 1.1, y = 2.2;// 以下几个都是常见的右值10;                  // 10是一个字面值,属于右值x + y;              // x + y是一个表达式,其结果是一个右值fmin(x, y);         // fmin(x, y)的返回值是一个右值,表示x和y的最小值// 以下几个都是对右值的右值引用int&& rr1 = 10;     // rr1是一个右值引用,绑定到右值10double&& rr2 = x + y; // rr2是一个右值引用,绑定到表达式x + y的结果double&& rr3 = fmin(x, y); // rr3是一个右值引用,绑定到fmin(x, y)的结果// 这里编译会报错:error C2106: “=”: 左操作数必须为左值10 = 1;             // 10是右值,不能作为赋值的左操作数x + y = 1;         // x + y是右值,不能作为赋值的左操作数fmin(x, y) = 1;    // fmin(x, y)是右值,不能作为赋值的左操作数return 0;
}

需要注意的是,右值不能直接取地址。然而,当右值被绑定到一个右值引用时,它会被存储在特定的内存位置,这样就可以通过该引用获取这个位置的地址。

例如,虽然无法直接获取字面量10的地址,但在将其绑定到右值引用rr1后,可以获取rr1的地址,并且可以修改rr1的值。
如果不希望rr1的值被修改,可以使用const int&& rr1来引用,这样rr1将成为一个常量右值引用。

int main()
{double x = 1.1, y = 2.2; // 定义两个double类型的变量x和yint&& rr1 = 10;          // rr1是一个右值引用,绑定到右值10const double&& rr2 = x + y; // rr2是一个常量右值引用,绑定到表达式x + y的结果rr1 = 20;                // 将rr1的值修改为20,合法,因为rr1是非常量的右值引用rr2 = 5.5;               // 这里会报错:不能修改常量右值引用// 解释:rr2是一个const double&&,这意味着它绑定的值不能被修改。尝试给rr2赋值会导致编译错误。return 0; // 程序正常结束
}

左值引用与右值引用比较

左值引用总结:

  1. 左值引用只能引用左值,不能引用右值。
  2. 但是const左值引用既可引用左值,也可引用右值。
#include <iostream>
using namespace std;int main()
{// 左值引用只能引用左值,不能引用右值。int a = 10; // 定义一个整型变量a,并初始化为10// 创建一个左值引用ra1,引用变量aint& ra1 = a; // ra1是a的别名,可以通过ra1访问和修改a的值cout << "ra1: " << ra1 << endl; // 输出ra1的值,即10// int& ra2 = 10; // 编译失败,因为10是右值,左值引用不能绑定到右值// const左值引用既可引用左值,也可引用右值。const int& ra3 = 10; // ra3是一个常量左值引用,引用右值10const int& ra4 = a;  // ra4是一个常量左值引用,引用左值a// 输出ra3和ra4的值cout << "ra3: " << ra3 << endl; // 输出ra3的值,即10cout << "ra4: " << ra4 << endl; // 输出ra4的值,即10return 0; // 程序正常结束
}

右值引用总结:

  1. 右值引用只能右值,不能引用左值。
  2. 但是右值引用可以move以后的左值。
int main()
{// 右值引用只能绑定到右值,不能绑定到左值。int&& r1 = 10; // r1是一个右值引用,绑定到字面量10(右值)// 这行代码是合法的,因为10是右值。// 以下代码尝试将左值绑定到右值引用int a = 10;    // a是一个左值,初始化为10// int&& r2 = a; // 这行代码会导致编译错误// error C2440: “初始化”: 无法从“int”转换为“int &&”// message : 无法将左值绑定到右值引用// 右值引用可以引用move以后的左值int&& r3 = std::move(a); // std::move将左值a转换为右值// r3现在是一个右值引用,绑定到a的右值版本return 0; // 程序正常结束
}

右值引用使用场景和意义

前面我们可以看到左值引用既可以引用左值和又可以引用右值,那为什么C++11还要提出右值引用呢?是不是化蛇添足呢?下面我们来看看左值引用的短板,右值引用是如何补齐这个短板的!

右值引用肯定是要在某些场景比左值引用更加高效,所以才会引入右值引用的概念。
下面自己手动实现一个深拷贝的类,string类,观察其中的调用逻辑 :

namespace qq
{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[_size + 1];strcpy(_str, str);cout << "string(const char* str = "") -- 构造函数" << endl;}// 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){cout << "string(const string& s) -- 深拷贝" << endl;_str = new char[s._capacity + 1];strcpy(_str, s._str);_size = s._size;_capacity = s._capacity;}// 赋值重载//左值string& operator=(const string& s){cout << "string& operator=(string s) -- 深拷贝" << endl;char* tmp = new char[s._capacity + 1];strcpy(tmp, s._str);delete[] _str;_str = tmp;_size = s._size;_capacity = s._capacity;return *this;}~string(){delete[] _str;_str = nullptr;}char& operator[](size_t pos){assert(pos < _size);return _str[pos];}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){push_back(ch);return *this;}const char* c_str()const{return _str;}private:char* _str;size_t _size;size_t _capacity;};string to_string(int value){bool flag = true;if (value < 0){flag = false;value = 0 - value;}qq::string str;while (value > 0){int x = value % 10;value /= 10;str += ('0' + x);}if (flag == false){str += '-';}std::reverse(str.begin(), str.end());return str;}
}

左值引用的使用场景:

做参数和做返回值都可以提高效率:

  • 左值引用做参数,防止传参的时候进行拷贝操作。
  • 左值引用做返回值时,防止返回临时对象的时候进行拷贝构造。

void func1(qq::string s)
{}
void func2(const qq::string& s)
{}
int main()
{qq::string s1("hello world");cout << "------" << endl;// func1和func2的调用我们可以看到左值引用做参数减少了拷贝,提高效率的使用场景和价值func1(s1);cout << "------" << endl;func2(s1);// string operator+=(char ch) 传值返回存在深拷贝// string& operator+=(char ch) 传左值引用没有拷贝提高了效率s1 += '!';return 0;
}

在这里插入图片描述

  • 在上面的代码中,首先fun1fun2 分别接受string类型的参数,前者通过值传递,后者通过常量左值引用来传递。
  • 首先在构造对象s1的时候首先调用构造函数进行构造。
  • 其次在调用func(s1)的时候,会发生深拷贝,因为传入的是s1的一个副本,这就会发生深拷贝,因为传递的是一个副本。
  • 而在调用func(2)的时候,由于使用了常量左值引用,避免了拷贝,提高了效率。

左值引用的短板:

但是当函数的返回对象是一个局部变量的时候,当出了函数的作用域,该变量就被销毁了,因为其声明周期只限于函数作用域。所以此时就不能使用左值返回了,不得不只能继续使用传值返回,但是传值返回至少都有一次拷贝构造,这就造成了效率的低下。

在这里插入图片描述

左值引用中编译器的优化

在这里插入图片描述

  • 对于上面的str,其自身本是一个左值,传值返回,所以在销毁前会先创建一个临时对象,再用这个临时对象来赋值给s1
  • 所以理论上,在声明str的时候会调用一次构造函数
  • 其次在返回和赋值的时候会调用两次拷贝构造

在这里插入图片描述
但是实际上就只有一次拷贝构造,因为编译器会把连续的拷贝构造合为一个拷贝构造!
在这里插入图片描述

右值引用和移动语义

qq::string中增加移动构造,移动构造本质是将参数右值的资源窃取过来,占为已有,那么就不用做深拷贝了,所以它叫做移动构造,就是窃取别人的资源来构造自己。

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

我们添加了移动构造之后,继续执行上面的代码可以发现,此时直接调用了移动构造。
在这里插入图片描述

  • 这里to_string的返回值是一个临时对象,然而此时这个临时对象不才会去调用const 修饰的左值引用,而是直接调用了右值引用,此时直接窃取这个将亡值。在这里插入图片描述
    在这里插入图片描述

并且这里编译器也对其做了相对于的优化,直接一步移动构造到位!
在这里插入图片描述

不仅仅有移动构造,还有移动赋值:

在qq::string类中增加移动赋值函数,再去调用qq::to_string(123),不过这次是将
qq::to_string(123)返回的右值对象赋值给ret1对象,这时调用的是移动构造。

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

在这里插入图片描述

  • 在这里,首先构造s1调用一次构造函数
  • 其次在to_string函数的内部再次调用构造函数来创建str
  • 在str返回的时候会调用移动构造创建出一个临时对象,最后再通过移动赋值向s1赋值。
  • 这里的移动构造和移动赋值只是完成了资源的交换,并没有拷贝,所以整体的效率得到了提高。

在这里插入图片描述

右值引用引用左值

根据 C++ 的语法规则,右值引用只能引用右值。那么,右值引用是否绝对不能引用左值呢?


实际上,在某些场景下,我们可能确实需要使用右值引用来引用左值,以实现移动语义。当我们需要将一个左值转化为右值引用时,可以使用 std::move 函数。 在 C++11 中,std::move() 函数位于 utility头文件中。
这个函数的名称可能会引起误解,因为它并不实际“搬移”任何东西。它的唯一功能是将一个左值强制转换为右值引用,从而实现移动语义。

template<class _Ty>
inline typename remove_reference<_Ty>::type&& move(_Ty&& _Arg) _NOEXCEPT
{
// forward _Arg as movable
return ((typename remove_reference<_Ty>::type&&)_Arg);
}

下面的代码实现了move对左值的强转:

int main()
{qq::string s1("hello world");// 这里s1是左值,调用的是拷贝构造qq::string s2(s1);// 这里我们把s1 move处理以后, 会被当成右值,调用移动构造// 但是这里要注意,一般是不要这样用的,因为我们会发现s1的// 资源被转移给了s3,s1被置空了。qq::string s3(std::move(s1));return 0;
}

在这里插入图片描述

右值引用的其他使用场景

C++11标准出来之后,STL中的容器除了增加移动构造和移动赋值之外,STL容器插入接口函数也增加了右值引用版本。

以list容器的push_back接口为例:
在这里插入图片描述
在这里插入图片描述
当使用std的list时,因为其已经实现了右值引用,所以当我们传入需要深拷贝的自定义string时,其会调用string的构造函数,左值的就调用左值引用版本的,右值就调用右值引用版本的。

  • 第一个lt.push_back(qq::string("11111"))中,传入的是一个右值,故其直接调用移动构造即可完成插入。
  • 第二个qq::string s1("12345") lt.push_back(s1); 其中s1是一个左值,所以调用左值引用版本的push_back,故这是一次深拷贝
  • 第三个lt.push_back(move(s1));对于s1进行了move操作,强制其调用右值引用版本,即移动构造

完美转发

万能引用

模板中的&&不代表右值引用,而是万能引用,其既能接收左值又能接收右值。比如

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

但是引用类型的唯一作用就是限制了接收的类型,后续使用中都退化成了左值,具体见下面的示例:

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; }template<typename T>
void PerfectForward(T&& 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;
}

我们一共实现了四种Fun函数,分别是左值引用,右值引用,const 左值引用 和 const 右值引用。
然后通过传入不同的左值右值以分别调用其函数,比如,传入右值10,就应该调用右值引用函数,但是实际上的结果是:
在这里插入图片描述
这也就应证了之前说的:右值引用属性本身是个左值、,也就是说右值引用后续使用中都退化成了左值

所以这里就需要用到完美转发了!

完美转发

模板中的&& 万能引用

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

template<class T>
void PerfectForward(T&& t)
{Func(std::forward<T>(t));
}

在使用完完美转发后,当PerfectForward函数传入的是右值时,就不会退化为左值,而是匹配到右值引用的Func函数中,传入左值时,类似。
在这里插入图片描述

完美转发的使用场景

下面实现了一个简化版本的list:


template<class T>
struct ListNode
{ListNode* _next = nullptr;ListNode* _prev = nullptr;T _data;
};
template<class T>
class List
{typedef ListNode<T> Node;
public:List(){_head = new Node;_head->_next = _head;_head->_prev = _head;}void PushBack(T&& x){//Insert(_head, x);Insert(_head, std::forward<T>(x));}void PushFront(T&& x){//Insert(_head->_next, x);Insert(_head->_next, std::forward<T>(x));}void Insert(Node* pos, T&& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<T>(x); // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}void Insert(Node* pos, const T& x){Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = x; // 关键位置// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}
private:Node* _head;
};int main()
{List<qq::string> lt;lt.PushBack("1111");lt.PushFront("2222");return 0;
}

在这里插入图片描述

代码执行过程

main 函数中,调用 lt.PushBack("1111");lt.PushFront("2222"); 时,字符串字面量 "1111""2222" 被传递给 PushBackPushFront

第一次调用 PushBack("1111")

  1. 构造 std::string 对象

    • 字符串字面量 "1111" 被隐式转换为 std::string 对象,调用构造函数 string(const char* str)
    • 输出string(const char* str = ) -- 构造函数
  2. 调用 Insert 方法

    • Insert 中,std::forward<T>(x)x 作为右值传递给 newnode->_data
    • 由于 newnode->_datastd::string 类型,右值绑定到 newnode->_data 时,会调用移动构造函数(如果存在)。
    • 输出string(const char* str = ) -- 构造函数(用于创建 newnode_data
  3. 移动赋值

    • Insert 方法中,newnode->_data 的赋值可能会触发移动赋值操作(如果 std::string 的实现中有这样的逻辑)。
    • 输出string& operator=(string&& s) -- 移动赋值

第二次调用 PushFront("2222")

  1. 构造 std::string 对象

    • 字符串字面量 "2222" 被隐式转换为 std::string 对象,调用构造函数 string(const char* str)
    • 输出string(const char* str = ) -- 构造函数
  2. 调用 Insert 方法

    • 同样,std::forward<T>(x)x 作为右值传递给 newnode->_data
    • 由于 newnode->_datastd::string 类型,右值绑定到 newnode->_data 时,会调用移动构造函数(如果存在)。
    • 输出string(const char* str = ) -- 构造函数(用于创建 newnode_data
  3. 移动赋值

    • 移动赋值操作可能再次被触发。
    • 输出string& operator=(string&& s) -- 移动赋值

引用折叠

这里需要介绍一下引用折叠:
引用折叠是 C++ 中的一种规则,涉及到如何处理引用的组合。具体来说,当我们在模板中使用引用时,可能会出现引用的引用。`C++ 规定了引用的折叠规则:

左值引用 + 左值引用: 结果为左值引用(T& & 折叠为 T&)。
左值引用 + 右值引用: 结果为左值引用(T& && 折叠为 T&)。
右值引用 + 左值引用: 结果为左值引用(T&& & 折叠为 T&)。
右值引用 + 右值引用: 结果为右值引用(T&& && 折叠为 T&&)。

完美转发的机制是通过结合万能引用、引用折叠和 std::forward 来实现的。std::forward 的工作原理依赖于引用折叠的规则,以确保在转发参数时能够正确处理左值和右值。


http://www.mrgr.cn/news/22928.html

相关文章:

  • 久久派搭建风电系统网站(基于mariadb数据库)
  • PVE动态核显直通
  • 一个数组向左移动i位(学会分析问题,找出规律,不要小看任何一个小程序;小程序都是实现大的功能的基础--体现问题分解的思想)
  • Python数据分析高频面试题及答案
  • 使用gdb跟踪调试linux内核
  • 仕考网:公务员国考考什么?
  • 三维点云骨架提取(以树木为例 python 代码)
  • 月考结束老师如何发布成绩查询?
  • 时间戳和日期相互转换+检验日期合法性功能C语言
  • 推荐4款高效的录屏工具,教你如何快速录屏。
  • 风趣图解LLMs RAG的15种设计模式-第三课
  • gpt4最新保姆级教程
  • 一些面试和找工作的技巧-新资要的低并不会给你加分薪资要的高不会成为公司拒绝你的核心理由
  • OZON商品免收仓储费,OZON隐藏被取消订单的评论
  • 驾驭复杂市场,商品计划软件:企业制胜的智囊团
  • web基础之信息泄露
  • 茶则电子秤方案设计
  • 多个pdf怎么合并成一个pdf?推荐5种方法轻松合并pdf文件
  • React基础
  • Python中常用的几种数据类型及其特点