右值
左值
左值是指放在赋值号左边可以被赋值的值(左值必须要在内存中有实体)
右值
右值是指放在赋值号右边赋给其他变量的值(右值可以在内存也可以在CPU寄存器)
右值不可以直接修改
右值可以赋值给左值
右值不可以直接取地址
C++11将右值分为纯右值和将亡值。
纯右值: 非引用返回的临时变量;运算表达式产生的结果;字面常量.
将亡值:其实就是中间变量的过渡,过度后就消亡,分为两种
1. 函数的临时返回值int a = f(3);f(3) 的返回值就是右值,副本拷贝给a,然后消亡
2. 表达式像(x+y),其中(x+y)就是右值
这里有个问题,右值不可以修改,那如果我们想要修改右值怎么办?解决方法就是右值引用
右值引用
左值引用
类型名 &引用名 = 左值表达式;
右值引用(就是给右值取个名字,因为右值是匿名的,所以我们要找到它必须用引用的方式,也就是给右值起一个名称)
类型名 && 引用名 = 右值表达式;
int a1 = 10;
//int&& a2 = a1; //右值引用无法绑定到左值上
//注释:getObj返回一个Object对象
//Object& t = getObj(); //右值引用无法初始化左值引用
下面给出示例1
#include<iostream>
using namespace std;
class AA
{
public:
int m_a = 9;
};
AA getTemp()
{
return AA();
}
int main()
{
int&& a = 3; //3是右值
a++;
int b = 8; //b是左值
int&& c = b + 5; //b+5是右值
c++;
AA&& aa = getTemp(); //返回值是右值
//getTemp的返回值的生命周期在getTemp函数结束时就该结束,
//但是通过右值能给右值“续命”
//左值引用只能绑定左值,右值引用只能绑定右值
//常量左值引用可以绑定非常量左值、常量左值、右值,
//而且常量左值引用还可以像右值的生命期延长,缺点:只能读不能改
aa.m_a++;
cout << "a=" << a << " " << &a << endl;
cout << "c=" << c << " " << &c << endl;
cout << "aa.m_a=" << aa.m_a << " " << &aa.m_a << endl;
return 0;
}
getTemp的返回值的生命周期在getTemp函数结束时就该结束,但是通过右值能给右值“续命”,此时他的右值生命周期和aa的声明周期一样。
左值引用只能绑定左值,右值引用只能绑定右值,常量左值引用可以绑定非常量左值、常量左值、右值,而且常量左值引用还可以像右值的生命期延长,缺点:只能读不能改
示例2
#include<iostream>
using namespace std;
class Object
{
public:
Object():m_num(new int(100))
{
cout << "Object()" << endl;
}
~Object()
{
delete m_num;
cout << "~Object()" << endl;
}
int* m_num;
};
void test1()
{
int a = 50; //a->左值,50->右值
//左值右值引用都必须立即初始化,通过右值引用的声明,该右值获得重生。
//该右值的生命周期与该右值引用变量的生命周期一样
int a1 = 10;
//int&& a2 = a1; //右值引用无法绑定到左值上
//Object& t = getObj(); //右值引用无法初始化左值引用
Object&& t1 = getObj();
const Object& t2 = getObj();
//常量左值引用是一个万能引用类型,它可以接受左值、右值、常量左值和常量右值。
}
右值引用的应用(移动构造函数)
在C++中进行对象赋值时,有时候会发生对象的深拷贝,如果对象的堆内存太大,拷贝的代价是很大的,所以程序性能会降低,如果想要避免深拷贝,可以使用右值引用
看一个示例
#include<iostream>
using namespace std;
class Object
{
public:
Object():m_num(new int(100))
{
cout << "Object()" << endl;
}
Object(const Object& a) :m_num(new int(*a.m_num))
{
cout << "Object(&)" << endl;
}
//移动拷贝构造函数
/*Object(Object&& a) :m_num(a.m_num)
{
a.m_num = nullptr;
cout << "Object(&&)" << endl;
}*/
~Object()
{
delete m_num;
cout << "~Object()" << endl;
}
int* m_num;
};
Object getObj()
{
Object tmp;
return tmp;
}
void test2()
{
Object obj2 = getObj();
}
void main()
{
test2();
}
我们发现调用了深拷贝构造函数,我们把代码注释的部分打开运行结果如下
我们发现调用了移动构造函数(参数为右值引用类型),这有什么意义呢???我们发现移动构造函数只是对对象进行了浅拷贝,性能优化。因为赋值号右边是一个函数的返回值,在函数的栈帧中创建的临时对象在函数结束时就会释放,这时移动构造函数用了右值引用,将函数栈帧中创建的临时对象中的堆内存的(资源)所有权交给了tmp,这块内存得到了“续命”。
&&的扩展
根据前面说的,是否&&就代表一个右值引用。答案是否定的。
具体上体现在模板和自动类型推导。
当模板参数是T&&,或者自动类型推到指定为auto &&时,这时&&称为未定的引用类型。
注意:const T&&表示右值引用
通过右值推导 T&& 或者 auto&& 得到的是一个右值引用类型,其余都是左值引用类型。
函数模板中的&&
练习
#include<iostream>
using namespace std;
template<typename T>
void f(T&& param) {}
void f1(const T&& param) {}
void main()
{
f(10); //实参->右值,T&&->右值引用
int a=10;
f(a); //a->左值,T&&->左值引用
//f1(a); //错误,a->左值,constT&&本身时右值引用,左值不能初始化右值引用
f1(10); //实参->右值
}
自动类型推导的&&
练习
void test5()
{
int&& a1 = 5;
auto&& bb = a1; //a1->右值引用,bb->左值引用
auto&& bb1 = 5; //5->右值,bb1->右值引用
int a2 = 5;
int& a3 = a2;
auto&& cc = a3; //a3->左值引用,auto&&->左值引用
auto&& cc1 = a2; //a2->左值,auto&&->左值引用
const int& s1 = 100;
const int&& s2 = 100;
auto&& dd = s1; //s1->常量左值引用,dd->常量左值引用
auto&& ee = s2; //s2->常量右值引用,ee->常量左值引用
const auto&& x = 5;
}
最后做一个题目,写出运行结果
#include<iostream>
using namespace std;
void Print(int& value)
{
cout << "l_value:" << value << endl;
}
void Print(int&& value)
{
cout << "r_value:" << value << endl;
}
void forward(int&& x)
{
Print(x);
}
void test()
{
int i = 666;
Print(i);
Print(6);
int&& l_val = 6;
Print(l_val);
forward(369);
}
void main()
{
test();
}
解析
Print(i); i->左值,调用重载的Print(int& value);
Print(6); 6->右值,调用重载的Print(int&& value);
Print(l_val); l_val->左值,调用重载的Print(int& value);
forward(369); 函数接受的是右值,在函数中有调用Print,把参数当成左值处理;