【c++11】看完立马就懂--右值引用!!!

2024-04-21 22:36
文章标签 c++ 引用 右值 立马

本文主要是介绍【c++11】看完立马就懂--右值引用!!!,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

右值引用

  • 一、什么是右值?什么是左值?
  • 二、右值引用
  • 三、右值引用的好处
  • 四、万能引用
  • 五、完美转发

一、什么是右值?什么是左值?

首先,当我们看到右值的时候,我们很自然的就会产生疑问?
什么的右边呢?
等号的右边吗?
那么如果是按赋值=符号的右边来定义的话,那么,左值是不是就是=符号的左边的值呢?
但是,看到下面的这段代码,我们又感觉,上面的说法,貌似不太对!

#include<iostream>int main()
{int a = 10;int b = a;return 0;
}

仔细推敲,我们发现,10是个字面常量,和我们的a,在栈上创建的变量,貌似不是一个东西?这样来看,好像和我们上面的定义挺符合的啊?两个不同的东西,刚好用左值和右值这么个名称来进行区分。好像挺对的哦。
但是,b不也是在栈上开辟的变量吗?b在赋值符号=的左边,但是a也是栈上的变量,a在赋值符号=的右边呀!那这样子来看,左值和右值不就没什么区别了吗?????
————————————————————————————————
所以,上面的这种结论,肯定是不正确的!那么究竟什么是左值?什么是右值???
左值和右值,它肯定会存在区别!不然为什么要出现这样子的命名。通过作者的不懈努力的查阅资料!!!
左值,我们可以理解我们平常开辟在栈上的变量,例如上面的a,b这些变量,都可以叫做左值。
右值,通常我们认为,右值是那么字面常量,如10;表达式的返回值(a + b返回一个临时变量);函数的返回值(这个返回值不是引用);我们将这些统称为右值。
其中,我们最佳的区分方式就是,看看这个变量能不能取地址。
在这里插入图片描述
注意,字符串,我们将它认为是左值,它可以取地址,地址是首元素的地址。
在这里插入图片描述
在这里插入图片描述
————————————————————————————————

二、右值引用

引用我们都很熟悉,也就是

#include <iostream>int main()
{int a = 0;int &ra = a;return 0;
}

这种引用我们叫做左值引用,右值引用是长啥样子的?

#include <iostream>
int main()
{int&& ra = 10;return 0;
}

这样子的引用方式我们叫做右值引用。
那么我们就会产生以下的疑问?

  • 左值引用能不能引用右值?
  • 右值引用能不能引用左值?

左值能不能引用右值?
在这里插入图片描述
撕~~~~,好像不可以诶?仔细想了想,是不是和引用的权限升级有关呢?
因为10是个字面常量呀,它又不能修改,我用一个左值引用它,不就发生权限放大吗?那么我们给它加上一个const,来限制它的权限,是不是就能够左值引用右值了呢?
在这里插入图片描述
可以看到,编译器这次没有报错了,说明这样子的方式是可以的,也就说明了,const的左值引用可以引用右值,所以,这就解释了,为什么STL容器里面的拷贝构造,构造函数,为什么要对参数加上const的原因了,这样既能够将我们的左值传参过来,也可以将右值传参传过来。

右值引用能不能引用左值?
我们来尝试一下就知道了
在这里插入图片描述
编译器报错了,说明是不可以的?肯定不可以吗?想想原因,貌似想不出来什么原因了??????
是不是真的不可以?
通过我查阅C++11,发现了,c++11里面给我们提供了一个方法。也就是将左值move一下。
在这里插入图片描述
好像可以了,但是为什么move一下,a就可以被右值引用了呢?这样子写可以吗?
在这里插入图片描述
好像又不行了,所以我推断,move应该是根据左值a,来产生一个临时的右值作为返回值,然后右值引用就可以引用了。

三、右值引用的好处

右值引用说了半天,那么右值引用的出现到底有什么用啊?它难道就是为了能够替换const的左值引用,给右值也能够名正言顺的加上一个右值引用的名称吗?仅仅只是为了给我们的右值也有了地位,恩宠一下右值是吧?????
那肯定是不可能的啊,怎么可能会耗费那么大的力气搞出来一个右值引用,就为了恩宠一下右值???
对于它的作用我也很好奇,所以我也去看别人的博客和上网查资料,我看到一些博客上面介绍:右值引用可以延长生命周期???
我看到的时候,我满脸问号
在这里插入图片描述
什么玩意???延长生命周期???
延长啥的生命周期啊???
为什么要延长生命周期???
延长生命周期有啥用啊???
我表示很疑问和好奇,在这种好奇和疑问的驱动下,我就去看了《C++ Primer》这本书,我仿佛领悟到了一些!!!
那篇博客的作者可能想表达的是,延长资源的生命周期,右值引用可以说是一个非常非常强大牛逼的东西。
场景一:

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;string func()
{string s("aaaaaaaaaa");return s;
}int main()
{string s1 = func();return 0;
}

按照右值引用没有出现之前,我们的代码逻辑应该是这样子的
在这里插入图片描述
然后这里由于出现的s要拷贝构造一次,s1也要拷贝构造,编译器就会优化,直接跳过中间的那个临时变量,直接s1拷贝构造s(编译器优化就是我们的编译器其实是很智能的,如果发生那种创建一个变量,需要连续的构造几个中间变量的时候,编译器就会直接将中间的变量优化掉)。

这是右值引用没有出现的时候的分析,如果这个函数里面的string非常的大,那么在构造的时候,string肯定是深拷贝,那么这样子的代价真的太大了,如果返回的是vector< vector < int >>这样的类型,那么拷贝的代价真的太恐怖了,所以这里在右值引用没有出现之前,我们是通过输出型参数的方式来获取结果,也就是这样形式的代码风格

#include <iostream>void func(string& s1)
{.......
}

那么右值引用的出现,就可以解决这样的问题,如何解决?

string func()
{string s("aaaaaaaaaa");return s;
}int main()
{string s1 = func();return 0;
}

我们看到,func里面的s,它会随着函数调用的结束销毁栈帧,那么在里面的s,它的生命周期也就在那一个函数体里,函数销毁,那么s自然而然也要销毁,但是我们要返回s的内容,那么我们为什么不把s的资源夺过来,也就是将s夺舍!!!把它的资源占为己有,然后我的s1,它都已经要被赋值,把原先的内容给丢掉了,构造新的内容出来了,那么我为什么不把s的资源给拿过来,将我s1不要的资源丢给你s,你s都要销毁了,你就顺路把我那些垃圾,不要的资源也带走吧!!!这样的思路,就是右值引用的意义。
它将我们的将亡值,也就是即将销毁的s的资源的生命周期延续了下去,其实本质就是一种交换资源的方式。
那么这种实现也很简单,只需要创建一些指针变量,来交换我们的变量所指向在堆上创建的资源,获取到对方指向的内存块,这样就完成拷贝,这种拷贝我们称为移动拷贝
那么根据上面的思路,我们可以这样子玩

#define _CRT_SECURE_NO_WARNINGS 1
#include <iostream>
#include <string>
using namespace std;class A
{
public:A(int size = 0):_a(new int[size]),_size(size){}A(const A& it){cout << "A(const A& it) -- 左值引用" << endl;_size = it._size;_a = new int[_size];for (int i = 0; i < _size; i++){_a[i] = it._a[i];}}A(A&& it){cout << "A(A&& it) -- 右值引用" << endl;int* tmp = it._a;it._a = _a;_a = tmp;}private:int* _a = nullptr;int _size = 0;
};A func()
{A a(10);return a;
}int main()
{A a1 = func();return 0;
}

当我们运行到箭头位置的时候
在这里插入图片描述
我们看到a的地址的后四位是0510,然后接着运行
在这里插入图片描述
我们可以观察到,a的地址转移到了a1上面,而我们实现的方式,仅仅只是创建了一个临时的指针对象,交换双方指向的内容,而深拷贝要开辟空间,要把对方的值拷贝给我开辟的空间上,要调佣循环,这样的代价相比,移动构造的代价小的非常非常的多。

我们运行的结果
在这里插入图片描述

这样子,我们只需要很小的代价,就将资源获取到了,创造出右值引用的人实在是太牛了。所以右值引用我只能说,真香!
————————————————————————————————

四、万能引用

我们来看这段代码

template<class T>
void func(T&& x)
{cout << x << endl;
}int main()
{func(10);        //右值int a = 9;func(a);         //左值const int b = 8;func(b);         //const 左值func(move(a));   //move 左值 -> 右值func(move(b));   //move const 左值 -> const 右值return 0;
}

按理来说,我们的func函数里面看着是个右值,所以理所应当的应该要传一个右值过去,但是我们运行发现,
在这里插入图片描述
咦???怎么全部都可以编译通过并且运行呢???
这里需要科普一个知识,
首先,这是一个模版,它是通过具体的函数调用的值来实例化的,然后这里涉及到了一个叫做引用折叠的知识,如果传参传过去的是个左值,那么这里的T&&就会发生引用折叠,变成T&,而右值传过去依然是T&&。
那么这样的语法,它被称为万能引用,就是字面意思,模版的加入给予了它根据参数来实例化出不同的具体函数,这种方式让我们c++变得更加的灵活强大。

五、完美转发

我们也是一样,思考一下这段代码的结果


void test(int& x)
{cout << "void test(int& x)        -- 左值引用" << endl;
}void test(const int& x)
{cout << "void test1(const int& x) -- const 左值引用" << endl;
}void test(int&& x)
{cout << "void test(int&& x)       -- 右值引用" << endl;
}
void test(const int&& x)
{cout << "void test(const int&& x) -- const 右值引用" << endl;
}template<class T>
void func(T&& x)
{test(x);
}int main()
{func(10);        //右值int a = 9;func(a);         //左值const int b = 8;func(b);         //const 左值func(move(a));   //move 左值 -> 右值func(move(b));   //move const 左值 -> const 右值return 0;
}

我们来分析一下,根据上面学的万能引用,这里的输出结果应该是
右值
左值
const 左值
右值
const 右值
我们来查看编译器运行的结果
在这里插入图片描述
诶???这里就要产生疑问了,怎么全是左值了???很奇怪是不是??
我们之前说过,判断一个变量,是左值还是右值,关键在于能不能取地址,那么我们就怀疑,右值引用这个变量本身,能不能取地址呢?

int&& x = 10;cout << &x << endl;

在这里插入图片描述
我在vs2019下测试发现,右值引用本身竟然是一个左值,我们再来看它的汇编语言
在这里插入图片描述
我们发现,它好像是将10放在了栈上的某个位置,然后将这个位置用寄存器保存起来了,然后x仿佛被当成了一个指针, 然后将10存放在栈的位置存到x里面。
所以在我的编译器上,右值引用被当成是一个左值了,所以这就解释了为什么上面的代码执行结果全是左值!
那么我们如何解决这个问题?
c++11提供了forward来实现完美转发,即在传参过程中保证了参数的属性不会发生改变,也就是右值引用去当参数传递时,调用的是右值引用的函数,


void test(int& x)
{cout << "void test(int& x)        -- 左值引用" << endl;
}void test(const int& x)
{cout << "void test1(const int& x) -- const 左值引用" << endl;
}void test(int&& x)
{cout << "void test(int&& x)       -- 右值引用" << endl;
}
void test(const int&& x)
{cout << "void test(const int&& x) -- const 右值引用" << endl;
}template<class T>
void func(T&& x)
{test(forward<T>(x));
}int main()
{func(10);        //右值int a = 9;func(a);         //左值const int b = 8;func(b);         //const 左值func(move(a));   //move 左值 -> 右值func(move(b));   //move const 左值 -> const 右值return 0;
}

运行结果
在这里插入图片描述
这样就没有发生问题了!

这篇关于【c++11】看完立马就懂--右值引用!!!的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/924199

相关文章

【C++ Primer Plus习题】13.4

大家好,这里是国中之林! ❥前些天发现了一个巨牛的人工智能学习网站,通俗易懂,风趣幽默,忍不住分享一下给大家。点击跳转到网站。有兴趣的可以点点进去看看← 问题: 解答: main.cpp #include <iostream>#include "port.h"int main() {Port p1;Port p2("Abc", "Bcc", 30);std::cout <<

C++包装器

包装器 在 C++ 中,“包装器”通常指的是一种设计模式或编程技巧,用于封装其他代码或对象,使其更易于使用、管理或扩展。包装器的概念在编程中非常普遍,可以用于函数、类、库等多个方面。下面是几个常见的 “包装器” 类型: 1. 函数包装器 函数包装器用于封装一个或多个函数,使其接口更统一或更便于调用。例如,std::function 是一个通用的函数包装器,它可以存储任意可调用对象(函数、函数

C++11第三弹:lambda表达式 | 新的类功能 | 模板的可变参数

🌈个人主页: 南桥几晴秋 🌈C++专栏: 南桥谈C++ 🌈C语言专栏: C语言学习系列 🌈Linux学习专栏: 南桥谈Linux 🌈数据结构学习专栏: 数据结构杂谈 🌈数据库学习专栏: 南桥谈MySQL 🌈Qt学习专栏: 南桥谈Qt 🌈菜鸡代码练习: 练习随想记录 🌈git学习: 南桥谈Git 🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈🌈�

【C++】_list常用方法解析及模拟实现

相信自己的力量,只要对自己始终保持信心,尽自己最大努力去完成任何事,就算事情最终结果是失败了,努力了也不留遗憾。💓💓💓 目录   ✨说在前面 🍋知识点一:什么是list? •🌰1.list的定义 •🌰2.list的基本特性 •🌰3.常用接口介绍 🍋知识点二:list常用接口 •🌰1.默认成员函数 🔥构造函数(⭐) 🔥析构函数 •🌰2.list对象

06 C++Lambda表达式

lambda表达式的定义 没有显式模版形参的lambda表达式 [捕获] 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 有显式模版形参的lambda表达式 [捕获] <模版形参> 模版约束 前属性 (形参列表) 说明符 异常 后属性 尾随类型 约束 {函数体} 含义 捕获:包含零个或者多个捕获符的逗号分隔列表 模板形参:用于泛型lambda提供个模板形参的名

6.1.数据结构-c/c++堆详解下篇(堆排序,TopK问题)

上篇:6.1.数据结构-c/c++模拟实现堆上篇(向下,上调整算法,建堆,增删数据)-CSDN博客 本章重点 1.使用堆来完成堆排序 2.使用堆解决TopK问题 目录 一.堆排序 1.1 思路 1.2 代码 1.3 简单测试 二.TopK问题 2.1 思路(求最小): 2.2 C语言代码(手写堆) 2.3 C++代码(使用优先级队列 priority_queue)

【C++高阶】C++类型转换全攻略:深入理解并高效应用

📝个人主页🌹:Eternity._ ⏩收录专栏⏪:C++ “ 登神长阶 ” 🤡往期回顾🤡:C++ 智能指针 🌹🌹期待您的关注 🌹🌹 ❀C++的类型转换 📒1. C语言中的类型转换📚2. C++强制类型转换⛰️static_cast🌞reinterpret_cast⭐const_cast🍁dynamic_cast 📜3. C++强制类型转换的原因📝

C++——stack、queue的实现及deque的介绍

目录 1.stack与queue的实现 1.1stack的实现  1.2 queue的实现 2.重温vector、list、stack、queue的介绍 2.1 STL标准库中stack和queue的底层结构  3.deque的简单介绍 3.1为什么选择deque作为stack和queue的底层默认容器  3.2 STL中对stack与queue的模拟实现 ①stack模拟实现

c++的初始化列表与const成员

初始化列表与const成员 const成员 使用const修饰的类、结构、联合的成员变量,在类对象创建完成前一定要初始化。 不能在构造函数中初始化const成员,因为执行构造函数时,类对象已经创建完成,只有类对象创建完成才能调用成员函数,构造函数虽然特殊但也是成员函数。 在定义const成员时进行初始化,该语法只有在C11语法标准下才支持。 初始化列表 在构造函数小括号后面,主要用于给

2024/9/8 c++ smart

1.通过自己编写的class来实现unique_ptr指针的功能 #include <iostream> using namespace std; template<class T> class unique_ptr { public:         //无参构造函数         unique_ptr();         //有参构造函数         unique_ptr(