左值和右值,右值引用、重载 std-move,引用折叠
# 左值和右值概念
在C++11中:
- 可以取地址的,有名字的,非临时的就是左值;
- 不能取地址的,没有名字的,临时的就是右值;
- 左值一定在内存中,右值有可能在内存或者寄存器中。
为什么++i
可以作为左值,而i++
不可以?
因为++i
返回的是i,而i++
返回的是一个临时变量。
https://www.cnblogs.com/nanqiang/p/9979059.html
# 基础知识和内存细节
参考:https://blog.csdn.net/qianyayun19921028/article/details/80875002 (opens new window)
引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
举个例子,int a = b+c, a 就是左值,其有变量名为a,通过&a可以获取该变量的地址; 表达式b+c、函数int func()的返回值是右值,在其被赋值给某一变量前,我们不能通过变量名找到它,&(b+c)这样的操作则不会通过编译。 左值一定在内存中,右值有可能在内存中也有可能在寄存器中
int a=5;
int b=a;//此时a在内存中
int a=5;
int b=a+1;//此时a+1在寄存器中
int *p=&a;//此时&a在寄存器中
2
3
4
5
6
7
引用:就是取别名 ,引用不可以重定义
void main()
{
int num1(5);
int num2(10);
int *pnum(&num1);//将num1的地址传递给pnum
int * &rnum = pnum;//rnum是pnum的别名
rnum = &num2;//rnumhe pnum指向同一片内存 改变了rnum就相当于改变了pnum
cout << *pnum << endl;
system("pause");
}
void main()
{
int num1(5);
int num2(10);
int * &rnum = &num1;//这是不允许的 无法从“int *”转换为“int *&”
system("pause");
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
从以上两个例子可以看出int *pnum(&num1); int * &rnum = pnum;通过一个指针在进行取别名是可以的,因为此时指针在内存中,而直接int * &rnum = &num1;取别名是不行的,&num1在寄存器中。在内存中的值是可以直接取别名的也就是引用。但是在寄存器中的值在不可以直接被引用的。其实这就是所谓的左值引用和右值引用。
在C++11中可以取地址的、有名字的就是左值,反之,不能取地址的、没有名字的就是右值(将亡值或纯右值)。
在内存中的变量才是可以取地址的,而在寄存器中的变量是不可以取地址的。对于一个不能取地址的表达式或者值是无法直接引用的。所以int * &rnum = &num1;编译不通过。
讲了以上那么多,左值引用就是对一个左值进行引用的类型。右值引用就是对一个右值进行引用的类型。右值引用和左值引用都是属于引用类型。无论是声明一个左值引用还是右值引用,都必须立即进行初始化。而其原因可以理解为是引用类型本身自己并不拥有所绑定对象的内存,只是该对象的一个别名。左值引用是具名变量值的别名,而右值引用则是不具名(匿名)变量的别名。
# 常量左值
左值引用通常也不能绑定到右值,但引用是个“万能”的引用类型。它可以接受非常量左值、常量左值、右值对其进行初始化。不过常量左值所引用的右值在它的“余生”中只能是只读的。相对地,非常量左值只能接受非常量左值对其进行初始化。
int &a = 2; # 左值引用绑定到右值,编译失败
int b = 2; # 非常量左值
const int &c = b; # 常量左值引用绑定到非常量左值,编译通过
const int d = 2; # 常量左值
const int &e = c; # 常量左值引用绑定到常量左值,编译通过
const int &b =2; # 常量左值引用绑定到右值,编程通过
2
3
4
5
6
7
右值值引用通常不能绑定到任何的左值,要想绑定一个左值到右值引用,通常需要std::move()将左值强制转换为右值,例如:
int a;
int &&r1 = c; # 编译失败
int &&r2 = std::move(a); # 编译通过
2
3
右值引用的方法就是int * &&rnum = &num1; 。
下面来说一下为什么要右值引用,右值引用在你需要使用寄存器中的值的时候可以进行右值引用。寄存器的刷新速度很快,没有右值引用的话就需要将寄存器中的值拷贝到内存中,在进行使用,这是很浪费时间的。
int getdata(int &&num)
{
cout << num;
num += 10;
return num;
}
void main()
{
int a = 5;
cout << getdata(a + 1) << endl;
}
2
3
4
5
6
7
8
9
10
11
12
如上int getdata(int &&num)就是对右值进行引用。getdata(a + 1) 中a+1是右值在寄存器中,我们是不可以直接对他进行操作的,如果要操作得将其拷贝到内存中,如果是一个非常大的数据这种拷贝就会很占用内存,如果直接用右值引用就可以直接对其进行操作。从而节约内存。
将右值转化为左值 直接新建变量然后赋值就可以了
int b=a+1 //将a+1这个右值转变为左值了
move(a) //将a这个左值转变为了右值
2
# 引用折叠
- std::move(),将左值转换为右值
- std::forward,完美转发,保持原来的左值/右值属性
- (参考https://blog.csdn.net/xiangbaohui/article/details/103673177)
引用折叠:
- 1.所有右值引用折叠到右值引用上仍然是一个右值引用。(A&& && 变成 A&&)
- 2.所有的其他引用类型之间的折叠都将变成左值引用。 (A& & 变成 A&; A& && 变成 A&; A&& & 变成 A&)
# std::move 原理
参考: https://blog.csdn.net/p942005405/article/details/84644069/ (opens new window)
std::move源码:
template<typename T>
typename remove_reference<T>::type&& move(T&& t){
return static_cast<typename remove_reference<T>::type&&>(t);
}
2
3
4
string s("hello");
std::move(s) => std::move(string& &&) => 折叠后 std::move(string& )
此时:T的类型为string&
typename remove_reference<T>::type为string
整个std::move被实例化如下
string&& move(string& t) //t为左值,移动后不能在使用t
{
//通过static_cast将string&强制转换为string&&
return static_cast<string&&>(t);
}
2
3
4
5
6
7
8
9
10
注:std::move() 只是改变了对象的类型,实际上并没有move任何东西。 看下面的代码:
#include <string>
#include <iostream>
using namespace std;
int main(void) {
string a = "haha";
string b(a);
cout<<"--1 a="<< a <<" b="<< b <<endl; //输出 a=haha b=haha
string c(move(a));
cout<<"--2 a="<< a <<" c="<< c <<endl; //输出 a= c=haha
string d = "mmm";
cout << "--3-- d=" << d <<endl; //输出 mmm
move(d);
cout << "--4-- d=" << d <<endl; //输出 mmm
string e(d);
cout << "--5-- d=" << d <<endl; //输出 mmm
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
上面看似a被move之后自身变成了空字符串,实际上是因为string c的构造函数中,当传入类型为右值,它会把传入的字符串设置成空。通过查看string的源码也可以验证这一点。
对于string d,虽然对它执行了一次move,但是实际上只是改变了类型,并没有做任何事情。并且后续再用d构造e后,d依然没变成空,说明move在后续代码中并没有将d的类型改变为右值,只是move(d)
的返回值为右值。
# 右值引用参数重载
参考https://zhuanlan.zhihu.com/p/97128024 (opens new window)
重载右值和非右值,在运行时可以区分开。当拷贝构造函数或者赋值运算符传入的对象为右值时,直接将右值中指针指向的内存区域拿来用,不需要进行内存拷贝和右值内存释放,提高效率。
class Stack {
public:
Stack(int size=1000):msize(size),mtop(0) {
std::cout <<this<< ": Stack(int) construct" << std::endl;
mpstack = new int[size];
}
~Stack() {
std::cout << this <<": ~Stack()" << std::endl;
delete[]mpstack;
mpstack = nullptr;
}
// 拷贝构造
Stack(const Stack &src):msize(src.msize),mtop(src.mtop) {
cout <<this<< ": Stack(const Stack &src)" << endl;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
}
//带右值引用参数的拷贝构造函数
Stack(Stack &&src):msize(src.msize),mtop(src.mtop) {
cout << this << ": Stack(Stack&&)" << endl;
mpstack = src.mpstack;
src.mpstack = nullptr;
}
// 带右值引用参数的赋值运算符重载函数
Stack& operator=(Stack &&src)
{
cout << this << ": operator=(Stack&&)" << endl;
if (this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
/*此处没有重新开辟内存拷贝数据,把src的资源直接给当前对象,再把src置空*/
mpstack = src.mpstack;
src.mpstack = nullptr;
return *this;
}
// 赋值重载
Stack& operator=(const Stack &src) {
cout << this << " operator=" << endl;
if (this == &src)
return *this;
delete[]mpstack;
msize = src.msize;
mtop = src.mtop;
mpstack = new int[src.msize];
for (int i = 0; i < mtop; ++i) {
mpstack[i] = src.mpstack[i];
}
return *this;
}
int getSize() {
return msize;
}
private:
int *mpstack;
int mtop;
int msize;
};
Stack CreateSameSizeStack(Stack &stack)
{
cout << "create tmp" << endl;
Stack tmp(stack.getSize());
cout << "create tmp end" << endl;
return tmp; //temp对象作为返回值返回时,经历一次copy到main内存中,
//(现在编译器会自动优化,直接生成到main内存中,不存在copy,除非禁用优化)
}
int main() {
Stack aa(1000);
aa = CreateSameSizeStack(aa);//函数返回值作为一个右值临时变量存在,即等号的右边部分。
//此时调用“带右值引用参数的赋值运算符重载函数”,直接把aa中的数据拿来用,不拷贝。
cout << "next bb" << endl;
Stack bb(aa);
return 0;
}
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
# 返回值优化
gcc默认编译是带有返回值优化的,可以通过-fno-elide-constructors
禁用。
#include <stdio.h>
class A {
public:
A(int val) {
puts("A(int)");
}
A(A&& a) {
puts("A(A&&)");
}
A(A& a) {
puts("A(A&)");
}
~A() {
puts("A destruct");
}
int m;
};
A create_A(int val) {
A a(val); //输出: A(int)
printf("--1-- &m = %p\n\n", &a.m);
return a; //输出: A(A&&)、A destruct,说明a被拷贝到main的栈里,然后a在这析构了。
}
int main(void) {
A&& temp = create_A(5); //这里加上对temp右值引用是为了防止create_A返回的临时值当场析构。
printf("\n&temp.m=%p\n", &temp.m); //这里用了一招瞒天过海,temp是右值不能取地址,但是其成员可以去地址。
return 0;
}
//运行结果如下:
A(int)
--1-- &m = 0x7ffcbf0588a4
A(A&&) //create_A函数中的return a; 调用了一次拷贝和一次析构。
A destruct
&temp.m=0x7ffcbf0588dc //两个m的地址不同,同样可以证明函数返回时经历了一次拷贝。
A destruct
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
如果不带-fno-elide-constructors,也就是默认启用返回值优化,则输出结果为:
A(int)
create_A()::&m = 0x7ffc67ca1e2c
&temp.m=0x7ffc67ca1e2c
A destruct
2
3
4
此时返回值没有拷贝,在执行的时候实际上main函数中首先创建了一片内存,然后把地址传给create_A(),create_A()执行的时候直接在这个地址创建返回值。
另外注意,如果启用返回值优化,而在create_A()函数返回时使用return std::move(a);
,则返回值优化反而会不起作用,说明编译器没有对这种情况做返回值优化。所以注意不要弄巧成拙。