详解 C++ 左值、右值、左值引用以及右值引用
文章目录
- 一、左值和右值
- 1.左值
- 2.右值
- 3.总结
- 二、左值引用和右值引用
- 1.左值引用
- 2.右值引用
- 3.对比与总结
- 三、左值引用的使用场景及实际意义
- 1.使用场景
- 2.实际意义
- 3.短板
- 四、右值引用
- 1.移动语义(Move semantics)
- (1)移动构造
- ① 概念
- ② 移动构造有无的比较
- (2)移动赋值
- ① 概念
- ② 移动赋值有无的比较
- 2.右值引用的使用场景
- 3.完美转发(Perfect forwarding)
- (1)引入原因
- (2)概念
- (3)使用场景
- 4.重大意义
一、左值和右值
1.左值
左值是一个表示数据的表达式,比如:变量名、解引用的指针变量。一般地,我们可以获取它的地址和对它赋值,但被 const 修饰后的左值,不能给它赋值,但是仍然可以取它的地址。
总体而言,可以取地址的对象就是左值。
// 以下的a、p、*p、b都是左值
int a = 3;
int* p = &a;
*p;
const int b = 2;
2.右值
右值也是一个表示数据的表达式,比如:字面常量、表达式返回值,传值返回函数的返回值(是传值返回,而非传引用返回),右值不能出现在赋值符号的左边且不能取地址。
总体而言,不可以取地址的对象就是右值。
double x = 1.3, y = 3.8;
// 以下几个都是常见的右值
10; // 字面常量
x + y; // 表达式返回值
fmin(x, y); // 传值返回函数的返回值
以下写法均不能通过编译:
10 = 4;
、x + y = 4;
、fmin(x, y) = 4;
,VS2015 编译报错:error C2106: “=”: 左操作数必须为左值。原因:右值不能出现在赋值符号的左边。&10;
、&(x + y);
、&fmin(x, y);
,VS2015 编译报错:error C2102: “&” 要求左值。原因:右值不能取地址。
3.总结
区分左值和右值,终究还是要看能否取地址。
二、左值引用和右值引用
传统的 C++ 语法中就存在引用语法,而 C++11标准中新增了右值引用的语法特性,因此为了区分两者,将C++11标准出现之前的引用称为左值引用。
无论左值引用还是右值引用,都是给对象取别名。
1.左值引用
左值引用就是对左值的引用,给左值取别名。
// 以下几个是对上面左值的左值引用
int& ra = a;
int*& rp = p;
int& r = *p;
const int& rb = b;
2.右值引用
右值引用就是对右值的引用,给右值取别名。
右值引用的表示是在具体的变量类型名称后加两个 &,比如:
int&& rr = 4;
。
// 以下几个是对上面右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
注意:
右值引用
引用右值
,会使右值被存储到特定的位置。
也就是说,右值引用变量其实是左值,可以对它取地址和赋值(const右值引用变量可以取地址但不可以赋值,因为 const 在起作用)。
当然,取地址是指取变量空间的地址(右值是不能取地址的)。比如:
double&& rr2 = x + y;
&rr2;
rr2 = 9.4;
右值引用 rr2 引用右值 x + y 后,该表达式的返回值被存储到特定的位置,不能取表达式返回值 x + y 的地址,但是可以取 rr2 的地址,也可以修改 rr2 。const double&& rr4 = x + y;
&rr4;
可以对 rr4 取地址,但不能修改 rr4,即写成rr4 = 5.3;
会编译报错。
现在我们知道左值引用可以引用左值,右值引用可以引用右值。
那么左值引用是否可以引用右值?右值引用是否可以引用左值呢?
下面的对比与总结给出了答案。
3.对比与总结
左值引用总结:
- 左值引用只能引用左值,不能直接引用右值。
- 但是
const左值引用
既可以引用左值,也可以引用右值。
// 1.左值引用只能引用左值
int t = 8;
int& rt1 = t;//int& rt2 = 8; // 编译报错,因为10是右值,不能直接引用右值// 2.但是const左值引用既可以引用左值
const int& rt3 = t;const int& rt4 = 8; // 也可以引用右值
const double& r1 = x + y;
const double& r2 = fmin(x, y);
问:为什么
const左值引用
也可以引用右值?
答:在 C++11标准产生之前,是没有右值引用这个概念的,当时如果想要一个类型既能接收左值也能接收右值的话,需要用const左值引用
,比如标准容器的 push_back 接口:void push_back (const T& val)
。
也就是说,如果const左值引用
不能引用右值的话,有些接口就不好支持了。
下面就是 C++98标准中相关接口const左值引用
引用右值的例子:
vector<int> v;
v.push_back(1);
v.push_back(2);
v.push_back(3);
右值引用总结:
- 右值引用只能引用右值,不能直接引用左值。
- 但是右值引用可以引用被
move
的左值。
move
,本文指std::move
(C++11),作用是将一个左值强制转化为右值,以实现移动语义。
左值被 move 后变为右值,于是右值引用可以引用。
// 1.右值引用只能引用右值
int&& rr1 = 10;
double&& rr2 = x + y;
const double&& rr3 = x + y;int t = 10;
//int&& rrt = t; // 编译报错,不能直接引用左值// 2.但是右值引用可以引用被move的左值
int&& rrt = std::move(t);
int*&& rr4 = std::move(p);
int&& rr5 = std::move(*p);
const int&& rr6 = std::move(b);
三、左值引用的使用场景及实际意义
1.使用场景
// 1.左值引用做参数
void func1(string s)
{...}void func2(const string& s)
{...}int main()
{string s1("Hello World!");func1(s1); // 由于是传值传参且做的是深拷贝,代价较大func2(s1); // 左值引用做参数减少了拷贝,提高了效率return 0;
}
// 2.左值引用做返回值(仅限于对象出了函数作用域以后还存在的情况)
string s2("hello");
// string operator+=(char ch) 传值返回存在拷贝且是深拷贝
// string& operator+=(char ch) 左值引用做返回值没有拷贝,提高了效率
s2 += '!';
2.实际意义
传值传参和传值返回都会产生拷贝,有的甚至是深拷贝,代价很大。而左值引用的实际意义在于做参数和做返回值都可以减少拷贝,从而提高效率。
3.短板
左值引用虽然较完美地解决了大部分问题,但对于有些问题仍然不能很好地解决。
当对象出了函数作用域以后仍然存在时,可以使用左值引用返回,这是没问题的。
string& operator+=(char ch)
{push_back(ch);return *this;
}
但当对象(对象是函数内的局部对象)出了函数作用域以后不存在时,就不可以使用左值引用返回了。
string operator+(const string& s, char ch)
{string ret(s);ret.push_back(ch);return ret;
}// 拿现在这个函数来举例:ret是函数内的局部对象,出了函数作用域后会被析构,即被销毁了
// 若此时再返回它的别名(左值引用),也就是再拿这个对象来用,就会出问题
于是,对于第二种情形,左值引用也无能为力,只能传值返回。
四、右值引用
于是,为了解决上述传值返回的拷贝问题,C++11标准就增加了右值引用和移动语义。
1.移动语义(Move semantics)
将一个对象中的资源移动到另一个对象(资源控制权的转移)。
(1)移动构造
① 概念
转移参数右值的资源来构造自己。
// 这是一个模拟string类的实现的移动构造
string(string&& s):_str(nullptr), _size(0), _capacity(0)
{swap(s);
}
拷贝构造函数和移动构造函数都是构造函数的重载函数,所不同的是:
- 拷贝构造函数的参数是 const左值引用,接收左值或右值;
- 移动构造函数的参数是右值引用,接收右值或被 move 的左值。
注:当传来的参数是右值时,虽然拷贝构造函数可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数。
总的来说,如果这两个函数都有在类内定义的话,在构造对象时:
- 若是左值做参数,那么就会调用拷贝构造函数,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝构造就会做一次深拷贝)。
- 若是右值做参数,那么就会调用移动构造,而调用移动构造就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动构造就会少做一次深拷贝)。
比如执行下面这几行代码:
string s("Hello World11111111111111111");
string s1 = s; // s是左值,所以调用拷贝构造函数
string s2 = move(s); // s被move后变为右值,所以调用移动构造函数,s的资源会被转移用来构造s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了
执行
string s1 = s;
前:
执行string s1 = s;
后(也是执行string s2 = move(s);
前):
执行string s2 = move(s);
后:
② 移动构造有无的比较
比如执行语句cout << MyLib::to_string(1234) << endl;
只有拷贝构造没有移动构造:
在 to_string 函数栈帧销毁前,用局部对象 str 拷贝构造出临时对象返回到函数调用处。
既有拷贝构造也有移动构造:
在 to_string 函数栈帧销毁前,用局部对象 str (反正 str 要销毁,将 str 视为右值,直接转移 str 的资源 )移动构造出临时对象返回到函数调用处。
比如执行语句MyLib::string ret = MyLib::to_string(1234);
只有拷贝构造没有移动构造:
在 to_string 函数栈帧销毁前,先用局部对象 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,再用临时对象拷贝构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,这样的话临时对象的创建和销毁就显得多余了,不如省略掉这一步,直接用 str 拷贝构造出 ret 。
既有拷贝构造也有移动构造:
在 to_string 函数栈帧销毁前,由于局部对象 str 是左值(可以对它取地址),所以用 str 拷贝构造出临时对象返回到函数调用处,to_string 函数栈帧销毁后,由于临时对象是右值,所以用临时对象移动构造出 ret 。
但现在的编译器一般都会进行优化:因为临时对象有 ret 来接收,先拷贝构造出临时对象再用它移动构造出 ret ,临时对象好像没必要产生一样,不如省略掉。既然 str 是 to_string 函数栈帧的局部对象,最后还是要销毁,不如将 str 视为右值,直接转移 str 的资源用来构造 ret ,也就是直接用 str 移动构造出 ret 。
再比如执行下面的代码:
调用该函数后,需要传值返回这种占用很多资源的自定义类型,
在 C++98 中,没有移动构造,拷贝构造做深拷贝,花费的代价很大;
在 C++11 中,直接移动构造,转移 m 的资源给 ret ,提高了效率。
(2)移动赋值
① 概念
转移参数右值的资源来赋给自己。
// 这是一个模拟string类的实现的移动赋值
string& operator=(string&& s)
{swap(s);return *this;
}
拷贝赋值函数和移动赋值函数都是赋值运算符重载函数的重载函数,所不同的是:
- 拷贝赋值函数的参数是 const左值引用,接收左值或右值;
- 移动赋值函数的参数是右值引用,接收右值或被 move 的左值。
注:当传来的参数是右值时,虽然拷贝赋值函数可以接收,但是编译器会认为移动赋值函数更加匹配,就会调用移动赋值函数。
总的来说,如果这两个函数都有在类内定义的话,在进行对象的赋值时:
- 若是左值做参数,那么就会调用拷贝赋值,做一次拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次拷贝赋值就会做一次深拷贝)。
- 若是右值做参数,那么就会调用移动赋值,而调用移动赋值就会减少拷贝(如果是像 string 这样的在堆空间上存在资源的类,那么每调用一次移动赋值就会少做一次深拷贝)。
比如下面这几行代码:
string s("11111111111111111");
string s1("22222222222222222");
s1 = s; // s是左值,所以调用拷贝赋值函数string s2("333333333333333333");
s2 = std::move(s); // s被move后变为右值,所以调用移动赋值函数,s的资源会被转移用来赋给s2
// 要注意的是,move一般是不这样用的,因为s的资源被转走了
② 移动赋值有无的比较
比如执行下面的语句:
MyLib::string ret("111111111111111111111111");
ret = MyLib::to_string(12345);
没有移动赋值(有移动构造和拷贝赋值):
用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,再用临时对象拷贝赋值给 ret 。
有移动赋值:
用 str(编译器视 str 为右值)移动构造出临时对象作为返回值,由于临时对象是右值,再用临时对象移动赋值给 ret 。
2.右值引用的使用场景
除了上面的使用场景之外,C++11标准的STL 容器的相关接口函数也增加了右值引用版本。
比如:
3.完美转发(Perfect forwarding)
(1)引入原因
在此之前我们需要知道什么是万能引用:
确定类型的 && 表示右值引用(比如:int&& ,string&&),
但函数模板中的 && 不表示右值引用,而是万能引用,模板类型必须通过推断才能确定,其接收左值后会被推导为左值引用,接收右值后会被推导为右值引用。
注意区分右值引用和万能引用:下面的函数的 T&& 并不是万能引用,因为 T 的类型在模板实例化时已经确定。
template<typename T>
class A
{void func(T&& t); // 模板实例化时T的类型已经确定,调用函数时T是一个确定类型,所以这里是右值引用
};
让我们通过下面的程序来认识万能引用:
template<typename T>
void f(T&& t) // 万能引用
{//...
}int main()
{int a = 5; // 左值f(a); // 传参后万能引用被推导为左值引用const string s("hello"); // const左值f(s); // 传参后万能引用被推导为const左值引用f(to_string(1234)); // to_string函数会返回一个string临时对象,是右值,传参后万能引用被推导为右值引用const double d = 1.1;f(std::move(d)); // const左值被move后变成const右值,传参后万能引用被推导为const右值引用return 0;
}
在调试下开监视窗口可看到传参后参数 t 的类型:
于是我们会用万能引用去做一些有意义的事,比如下面的代码:
void Func(int& x) { cout << "左值引用" << endl; }void Func(const int& x) { cout << "const左值引用" << endl; }void Func(int&& x) { cout << "右值引用" << endl; }void Func(const int&& x) { cout << "const右值引用" << endl; }template<typename T>
void f(T&& t) // 万能引用
{Func(t); // 根据参数t的类型去匹配合适的重载函数
}int main()
{int a = 4; // 左值f(a);const int b = 8; // const左值f(b);f(10); // 10是右值const int c = 13;f(std::move(c)); // const左值被move后变成const右值return 0;
}
运行程序后,我们本以为打印的结果是:
左值引用
const左值引用
右值引用
const右值引用
但实际的结果却是:
后两行的运行结果跟我们预想的不一样。
那么这是怎么一回事呢?
其实在本文的前面已经讲过了,右值引用变量其实是左值,所以就有了上面的运行结果。
具体解释:
f(10);
10是右值,传参后万能引用被推导为右值引用,但该右值引用变量其实是左值,因此实际调用的函数是void Func(int& x)
。f(std::move(c));
const左值被move后变成const右值,传参后万能引用被推导为const右值引用,但该const右值引用变量其实是const左值,因此实际调用的函数是void Func(const int& x)
。
也就是说,右值引用失去了右值的属性。
但我们希望的是,在传递过程中能够保持住它的原有的左值或右值属性,于是 C++11标准提出完美转发。
(2)概念
完美转发是指在函数模板中,完全依照模板的参数类型,将参数传递给当前函数模板中的另外一个函数。
因此,为了实现完美转发,除了使用万能引用之外,我们还要用到std::forward
(C++11),它在传参的过程中保留对象的原生类型属性。
这样右值引用在传递过程中就能够保持右值的属性。
void Func(int& x) { cout << "左值引用" << endl; }void Func(const int& x) { cout << "const左值引用" << endl; }void Func(int&& x) { cout << "右值引用" << endl; }void Func(const int&& x) { cout << "const右值引用" << endl; }template<typename T>
void PerfectForward(T&& t) // 万能引用
{Func(std::forward<T>(t)); // 根据参数t的类型去匹配合适的重载函数
}int main()
{int a = 4; // 左值PerfectForward(a);const int b = 8; // const左值PerfectForward(b);PerfectForward(10); // 10是右值const int c = 13;PerfectForward(std::move(c)); // const左值被move后变成const右值return 0;
}
运行结果如下:
实现完美转发需要用到万能引用和 std::forward 。
(3)使用场景
除了上面的使用场景之外,C++11标准的 STL 容器的相关接口函数也实现了完美转发,这样就能够真正实现右值引用的价值。
比如 STL 库中的容器 list :
上面四个接口函数都调用 _Insert 函数,_Insert 函数模板实现了完美转发。
再比如自己模拟实现的 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(const T& x) // 左值引用{Insert(_head, x);}void PushFront(const T& x) // 左值引用{Insert(_head->_next, x);}void PushBack(T&& x) // 右值引用{Insert(_head, std::forward<T>(x)); // 关键位置:保留对象的原生类型属性}void PushFront(T&& x) // 右值引用{Insert(_head->_next, std::forward<T>(x)); // 关键位置:保留对象的原生类型属性}template<class TPL> // 该函数模板实现了完美转发void Insert(Node* pos, TPL&& x) // 万能引用{Node* prev = pos->_prev;Node* newnode = new Node;newnode->_data = std::forward<TPL>(x); // 关键位置:保留对象的原生类型属性// prev newnode posprev->_next = newnode;newnode->_prev = prev;newnode->_next = pos;pos->_prev = newnode;}private:Node* _head;
};
只要是右值引用,由当前函数再传递给其它函数调用,要保持右值属性,必须实现完美转发。
4.重大意义
右值引用(及其支持的移动语义和完美转发)是 C++11 中加入的最重要的新特性之一,它使得 C++ 程序的运行更加高效。
详解 C++ 左值、右值、左值引用以及右值引用相关推荐
- R语言生存分析详解:KM曲线、COX比例风险模型、HR值解读、模型比较、残差分析、是否比例风险验证:基于survival包lung数据集
R语言生存分析详解:KM曲线.COX比例风险模型.HR值解读.模型比较.残差分析.是否比例风险验证:基于survival包lung数据集 目录
- smali语言详解之一般/构造方法(函数)的声明与返回值关键字
smali语言详解之一般/构造方法(函数)的声明与返回值关键字 一. smali语言的方法声明格式 .method与.end method成对出现,类似于java中的花括号 1.1.非静态的一般方法 ...
- 详解SQL的四种连接-左外连接、右外连接、内连接、全连接
1.内联接(典型的联接运算,使用像 = 或 <> 之类的比较运算符).包括相等联接和自然联接. 内联接使用比较运算符根据每个表共有的列的值匹配两个表中的行.例如,检索 stud ...
- 详解自监督发展趋势! 何恺明连获三年CVPR最高引用的秘诀是?
点击文末公众号卡片,不错过计算机会议投稿信息 0 引言 许多加了我好友的读者知道尼谟之前的研究方向是"自监督学习",而最近我无意中发现,CVPR最近三年引用量最高的论文竟然都是来自 ...
- C++左值、右值、左值引用、右值引用的详解
目录 一.概述 1.左值和右值 2.引用 3.左值引用和右值引用 3.1 左值引用 3.2 右值引用 二.左值引用与右值引用详解 1.左值引用详解 2.右值引用详解 一.概述 1.左值和右值 左值是可 ...
- windows批处理(cmd/bat脚本)编程详解
cmd文件和bat文件的区别:二者本质上没有区别,都是简单的文本编码方式,都可以用记事本创建.编辑和查看.两者所用的命令行代码也是共用的,只是cmd文件中允许使用的命令要比bat文件多.cmd文件只有 ...
- Python基础语法day02字符串详解和列表
今天是python基础语法入门第二天,大概总共会有四天左右.四天后就是对于python的numpy库的详细文章.那话不多说,我们开始. 目录 字符串详解 字符串独有功能 检测头尾字符 判断输入结果是否 ...
- 常见字典用法集锦及代码详解
目录 前言 字典的简介 1. 字典对象 1.1 Add 方法 1.2 Exists 方法 1.3 Keys 方法 1.4 Items 方法 1.5 Remove 方法 1.6 RemoveAll 方法 ...
- Java集合详解之Map
一.首先看看集合框架体系图 从图中可以看到,Map接口扩展了Iterator接口,关于Iterator接口详解请移步:Iterator接口详解 二.Map是什么? Map<k,v>使用键值 ...
- Java类的加载过程详解 面试高频!!!值得收藏!!!
受多种情况的影响,又开始看JVM 方面的知识. 1.Java 实在过于内卷,没法不往深了学. 2.面试题问的多,被迫学习. 3.纯粹的好奇. 很喜欢一句话: 八小时内谋生活,八小时外谋发展. 望别日与 ...
最新文章
- CUDA Samples: 获取设备属性信息
- 滴滴自动驾驶获得江苏省苏州公安局颁发的路测牌照
- 英伟达再出GAN神作!多层次特征的风格迁移人脸生成器
- vagrant安装及使用方法
- HTML中的meta(转载)
- 【笔记】mybatis的sqlSession和Mapper详解
- 如何使用Visual Studio创建SQL Server数据库项目
- servlet,listener,filter,interceptor的关系
- 通讯录标准化输入fread c语言,C语言实现通讯录系统
- MCGS组态屏CRC16(Modbus)校验计算脚本
- 【华为机试真题 JAVA】最大股票收益-100
- python基础教程虎课-想配一台PS修图的电脑,请大家推荐配置? | ps修图基础
- GDI+ 中图片的绘制
- kvm介绍、kvm存储池、kvm快照和克隆、kvm虚拟机基本管理和网络管理
- 磁盘阵列(RAID)级别的简单介绍
- 手把手带你搞定OPENSTACK
- 仿新浪微博2014之登陆界面四(总体功能)
- VGGNet网络详解与模型搭建
- 【编译原理】中间代码优化(二) 局部优化
- excel组合汇总_Excel汇总20160222
热门文章
- win7中IIS配置添加网站,局域网访问不到··
- 毕业设计-基于Springboot实现图书管理系统
- 小白用c++画出手枪,写代码时实属不易,请点赞加关注吧!
- 快手发现页视频采集问题
- A Unified Objective for Novel Class Discovery
- 一只青蛙一次可以跳上1级台阶,也可以跳上2级,也可以跳上3级 请问跳100级有多少种方法?
- YYCache 设计分析
- 【面向对象项目】基于【Java】实战的飞机大战(一)
- Day 21-30 :Web前端概述
- Python Flask,Flask-WTF,表单验证,CSRF验证