来点 C++:左值、右值与 Constructor
date
Oct 27, 2021
slug
lvalue_rvalue_and_constructor_cpp
status
Published
tags
C++
summary
谜语人左右值
type
Post
state
over
General Sort
啥是左值右值右边必须是 rvalue?取址符知道函数返回值中的 l 与 r返回值为引用类型左值引用左值变右值const reference右值引用左值变右值在 constructor 中Reference(是参考不是引用)
啥是左值右值
左值就是在内存里有确定地址的东西(lvalue is something that points to a specific memory location),正常来讲,也就是变量。
而右值则是没有确定地址的、临时的量(literal constant),就像各种字面量等等。二者有什么基本的规则呢?看几个例子:
int d = 10; // ok, it is definitely correct!
//int 12 = d; // WRONG!! compiler tell "expression must be a modifiable lvalue"
所以可以看到,赋值运算符的左侧必须是一个 lvalue,如果不是的话编译器将会提醒你“expression must be a modifiable lvalue”,这个地方必须是一个 lvalue
右边必须是 rvalue?
赋值运算符的右侧必须是一个 rvalue 吗?显然不是的:
int d = 10; // lvalue = rvalue
int f = d; // lvalue = lvalue
int* g = &f // lvalue = lvalue
取址符知道
当你尝试:
int* g = &45; //error: lvalue required as unary ‘&’ operand
聪明的编译器就会 declare:
error: lvalue required as unary ‘&’ operand
所以说取址符知道如何分辨 lvalue 与 rvalue:它能够取到地址的就是 lvalue,反之则为 rvalue
函数返回值中的 l 与 r
这里有一个函数:
int GetSix()
{
return 6;
}
我们来用它做一些实验:
int d = GetSix(); // It is definitely correct
//GetSix() = 4; // WRONG!!! Compiler say: "lvalue required as left operand of assignment"
这个函数的返回值是一个 rvalue,它是临时的,没有办法置于赋值运算符的左侧。
返回值为引用类型
当我们把函数改造成这样:
int& GetSix()
{
int static _temp = 6;
return _temp;
}
再去做如上的实验,都是没有错误、完全可行的。为什么?
当我们说引用类型的时候,在引用什么?引用的就是一个已经存在的、确定的内存地址。所以:
- 首先,先说说第一句。这句
int d = GetSix()
还是将返回的引用类型的值(当然是 lvalue赋付给了变量d
- 第二句。首先明确一点,这个函数是将指向函数
GetSix()
中的static
类型变量的一个引用作为返回值给了出去。这个引用是一个 lvalue,故这个语句也是成立的
效果就是函数中的变量
_temp
的值变为了 4左值引用
左值引用讲述了一个什么样的事情呢?其实很简单,就是只有 lvalue 才能被 reference。
一个 rvalue 连内存地址都没有,去哪里引用呢?😂
int hello = 5;
int& helloRef = hello; // It is definitely correct
// int& hiRef = 5; // WRONG!!!
编译器将在第二行报错:
cannot bind
non-const lvalue
reference of type ‘int&’ to an rvalue of type ‘int’
这个例子也是一样的:
void ReceiveRef(int& _ref) { //pass }
/* invoke: */
int d = 3;
ReceiveRef(d); //Correct, 'd' is a lvalue
//ReceiveRef(5); //WRONG!!!!!!
左值变右值
就像隐式转换一样,这是一件很自然的事情,举个例子:
int d = 4, f = 5;
int g = d + f; // lvalue = lvalue + lvalue
加法运算符的两侧都需要是 rvalue,但是在这里却是两个 lvalue,就是在运算过程中很自然的变化成了 rvalue。
那 rvalue 能(自然地)变为 lvalue 吗?很遗憾,不可以。但是允许将 rvalue 变为一个 const lvalue,这也是左值引用中例子报错中显示不能将 rvalue 赋给
non-const lvalue
的原因const reference
尝试:
const int& d = 4; //Corrcet
//int& d = 4; //WRONG!!!
这里其实是编译器创造了内存空间然后再做为 lvalue 赋给了
d
,(the compiler creates an hidden variable for you)也就是相当于:int _hidden = 4;
const int& d = _hidden;
当然,为什么设为
const
之后就可以了呢。const
类型的引用是只读的,也就是这个内存空间虽然有了,但是不可更改,这是一件符合自然直觉的事情。右值引用
在 C++11 中引入了右值引用,右值引用只能引用 rvalue 不能引用 lvalue:
int&& rRef = 5; // Correct!!
int d = 4;
//int&& rRef = d; //WRONG!!!!
左值变右值
使用
std::move()
可以将一个 lvalue 强制转化为一个 rvalue 返回。但是这里有几个谜语人一样的点需要注意:std::move(d)
本身是一个 rvalue:std::move(d)
的返回类型是int&&
,也就是一个 rvalue。当它没有赋给任何int&&
类型的变量的时候,它本身就是一个 rvalue- 如果使用
int&& rRef = std::move(d)
那么这个右值引用变量rRef
实则是一个 lvalue。因为它是一个有内存地址的变量。
在这篇文章中有一个直观的例子:
// 形参是个右值引用
void change(int&& right_value) {
right_value = 8;
}
int main() {
int a = 5; // a是个左值
int &ref_a_left = a; // ref_a_left是个左值引用
int &&ref_a_right = std::move(a); // ref_a_right是个右值引用
change(a); // 编译不过,a是左值,change参数要求右值
change(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值
change(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值
change(std::move(a)); // 编译通过
change(std::move(ref_a_right)); // 编译通过
change(std::move(ref_a_left)); // 编译通过
change(5); // 当然可以直接接右值,编译通过
cout << &a << ' ';
cout << &ref_a_left << ' ';
cout << &ref_a_right;
// 打印这三个左值的地址,都是一样的
}
在 constructor 中
其实分清楚 lvalue 与 rvalue 就是因为这个问题一直没有搞懂。用下面的代码做实验:
注意编译时一定要使用参数
-fno-elide-constructors
与 -O0
以禁用对于构造函数的优化#include <iostream>
using namespace std;
class A {
public:
int x;
A(int x) : x(x)
{
cout << "Constructor" << endl;
}
A(A& a) : x(a.x)
{
cout << "Copy Constructor" << endl;
}
A& operator=(A& a)
{
x = a.x;
cout << "Copy Assignment operator" << endl;
return *this;
}
A(A&& a) : x(a.x)
{
cout << "Move Constructor" << endl;
}
A& operator=(A&& a)
{
x = a.x;
cout << "Move Assignment operator" << endl;
return *this;
}
};
A GetA()
{
return A(1);
}
A&& MoveA(A& a)
{
return std::move(a);
}
int main()
{
cout << "-------------------------1-------------------------" << endl;
A a(1);
cout << "-------------------------2-------------------------" << endl;
A b = a;
cout << "-------------------------3-------------------------" << endl;
A c(a);
cout << "-------------------------4-------------------------" << endl;
b = a;
cout << "-------------------------5-------------------------" << endl;
A d = A(1);
cout << "-------------------------6-------------------------" << endl;
A e = std::move(a);
cout << "-------------------------7-------------------------" << endl;
A f = GetA();
cout << "-------------------------8-------------------------" << endl;
A&& g = MoveA(f);
cout << "-------------------------9-------------------------" << endl;
d = A(1);
}
结果如下:
-------------------------1-------------------------
Constructor
-------------------------2-------------------------
Copy Constructor
-------------------------3-------------------------
Copy Constructor
-------------------------4-------------------------
Copy Assignment operator
-------------------------5-------------------------
Constructor
Move Constructor
-------------------------6-------------------------
Move Constructor
-------------------------7-------------------------
Constructor
Move Constructor
Move Constructor
-------------------------8-------------------------
-------------------------9-------------------------
Constructor
Move Assignment operator
逐个情况分析吧:
- 第一个。就是普通的初始化,constructor 没什么问题
- 第二个。是首先创建了新实例
b
,然后用a
去初始化它。有新实例产生,那就需要构造,所以是使用 copy constructor
- 第三个。与第二个一样的,有新实例产生,所以使用 copy constructor
- 第四个。与前面不一样了,这里的
b
不是新的实例,所以不需要构造,所以直接使用 copy assign operator
- 第五个:
- 有新实例被创造,需要构造;
- 而且使用 rvalue 来实例化了一个临时对象
A(1)
,可以认为也是一个 rvalue。赋完值是没有地方去的,所以直接 move。 - 综合就是:
A(1)
使用了 constructor;后面的过程使用了 move constructor
- 第六个:
- 有实例被创造,需要构造;
- 使用
std::move()
将a
转化为一个 rvalue,随后赋给e
- 综合使用 move constructor
用 rvalue 初始化一个新实例,使用 move constructor
- 第七个:与第五个如出一辙,但是多了一个 move constructor,是从哪里来的呢?
- 第一个是当函数 return 的时候
A(1)
作为 rvalue 进行了一次 move constructor 来返回 - 第二个就是函数的返回值作为 rvalue 进行了一次初始化新实例
f
的 move constructor
- 第八个:
- 首先没有新实例被创造出来,所以不需要构造;
- 其次,将 a 变为 rvalue 之后一直是作为引用的形式进行传参,也不牵涉 move, copy 等
- 所以什么都没有
- 第九个:
- 有新的临时实例
A(1)
被创造,所以有 constructor - 使用临时实例这个 rvalue 来更新 d,所以是 move constructor(参考我们在五、六中得出的结论)
Reference(是参考不是引用)
🤣 很同意: