C++中的左值和右值,以及右值引用
在C++中有左值和右值:左值和右值其实是C语言中的概念,但是C标准中并没有给出严格的区分方法。普遍的认为是, 放在=左边的,或者能够取地址的,我们称为左值。只能放在=右边的,或者不能取地址的,称为右值. 但有时候这个判断标准也不一定准确。
C++11中,左值和右值的区分标准:
1. 普通类型的变量,因为有名字,可以取地址,都认为是左值
2. const修饰的常量, 不可修改,只读类型的,理论上应该按右值对待,但因为它可以获取地址(如果只是const类型常量的定义,编译器不给其开辟空间. 但对该const类型的常量取地址时,编译器才为其开辟空间),C++11 认为其是右值
3. 如果表达式的运行结果是一个临时变量或者对象,则认为是右值
4. 如果表达式的运行结果是一个引用,或者单个变量是一个引用,则认为是左值
我们知道C++中有引用的概念,现在我们有了左值和右值得概念,就来讲讲一个新的概念 - 右值引用. 那么,以前的引用我们这里称为普通引用,还有const修饰普通引用,我们称为const引用
普通引用(左值引用) => 普通引用只能引用左值,不能引用右值
const引用 (const左值引用) (const修饰的普通引用) => const引用既可以引用左值,也可以引用右值
C++11中的右值引用 => 只能引用右值,一般情况下不能直接引用左值
举例如下
int main() {
//普通引用只能引用左值,不能引用右值
int x = 10;
int& p1 = x; //普通引用p1,把左值x赋给它,是可以的。 这里,p1成了左值x的别名
int& p2 = 10; //编译失败,把右值10赋给普通引用p2,这是不允许的,编译不过去
//const引用 可以引用左值,也可以引用右值
const int& c1 = 10; //把右值赋给const引用
const int& c2 = x; //把左值赋给const引用
//右值引用,只能引用右值,一般情况不能直接引用左值
int&& r1 = 10;
int a = 5;
int&& r2 = a; //编译失败,右值不能引用左值 }
在C++11标准之前,没有右值引用这个概念。那时只有左值引用,那为什么还搞出了一个const引用呢 => 因为有些标准的容器接口,参数既要能接收左值,又要能接收右值, 比如 标准容器的push_back接口: void push_back(const T& val)
vector<int> myVector;
myVector.push_back(1);
myVector.push_back(2);
myVector.push_back(3);
上面我们提到,右值引用只能引用右值,不能直接引用左值。 但是在C++11中有个 ”移动语义” 的概念, std::move() 可以把一个左值强制转化为右值, 这样,一个左值通过std::move()变为右值,于是就可以被右值引用来进行引用了
int x = 10;
int&& rrx = x; //编译报错,右值引用rrx不能引用左值x
//但是右值引用可以引用被move的左值
int&& rrx = std::move(x); // 正确
1. 左值引用的使用场景以及意义
左值引用的使用场景 -----》 主要目的是为了避免对象拷贝
1.1 左值引用作为参数
void testFun1(string str) { .......... }
void testFun2(const string& str)
{
.......
}
int main()
{
string str1("Hello World!");
testFun1(str1); //testFun1函数是传值作为参数来传递,做的是深拷贝,代价很大
testFun2(str1); //testFun2参数传递的是const引用,这个减少了拷贝,提高了效率
//函数中采用值来进行参数传递时,系统会在内存中开辟空间来存储形参变量,并将实参变量的值拷贝给形参变量。如果函数传递的是类的对象,系统还会调用类中的拷贝构造函数来构造形参对象
//使用引用来作为参数传递时,由于此时形参只是传递函数的实参变量或者对象的别名而非副本,所以系统不会在内存中开辟新的空间
}
1.2 左值引用作为返回值 => 仅限于对象出了函数作用域以后还存在的情况
string s1("world");
// string operator += (char ch) 传值返回,会进行拷贝并且是深拷贝
// string& operator += (char ch) 这里用左值引用作为返回值,没有拷贝,提高了效率
s1 += '&';
1.1 左值引用的短板 => 左值引用不能引用局部变量
当我们把左值引用作为函数返回值时,意味着函数返回时(函数结束调用,出了函数的作用域),这个返回的左值引用所引用的那个对象(变量)依然还要存在,所以,它肯定不能是函数内部的局部对象(局部变量). 因为函数内部的局部变量,当函数调用结束时,应该就不存在了,那你怎么还能用一个左值引用
去引用它呢. 所以它应该是全局对象(全局变量). 举例如下:
string operator+(const string& str, char cha) { string reValue(str); reValue.push_back(cha); return reValue; }
//上面这个函数,reValue是函数内部新建立的一个局部变量,它只在这个函数内部存在. 一旦出了函数作用域,就会被析构,也就是被销毁了. 这里我们这个函数的返回值就不能是左值引用,因为如果是引用,那么引用的就是这个reValue局部变量,但是函数返回时,这个变量reValue
// 已经不存在,所以你还用它的引用,肯定会出问题。 所以只能是返回值类型
//另外,也不能返回函数内部通过new分配的内存的引用,因为这种情况下,被返回的函数的引用只是作为一个临时变量出现,而没有将其赋值给一个实际的变量,那么很有可能会造成这个引用所指向的空间(new分配的)无法释放的情况(由于没有具体的变量名,所以也无法用delete手动
// 释放该内存), 从而会造成内存泄漏
2. 右值引用的使用场景以及意义
1.1 移动语义 (Move semantics)
将一个对象中的资源移动到另一个对象 (资源控制权的转移)
1.1.1 移动构造 -- 转移参数右值的资源来构造自己
我们在C++中的构造函数中,大部分都是拷贝构造. 什么意思呢,就是通过构造函数去构造类的一个对象时,会把需要的资源拷贝一遍。 那么除了拷贝构造函数外,其实C++11中还有一种 “”移动构造函数”. 顾名思义,移动构造函数肯定不会拷贝资源,而是把资源直接移动过来,直接构造新的对象.
拷贝构造函数和移动构造函数都是构造函数的重载函数,不同的是
拷贝构造函数 => 参数是const引用,它可以接收左值或者右值
移动构造函数 => 参数是右值引用,接收的是右值,或者move的左值 (左值可以通过移动语义 std::move成右值)
**特别注意** -- 如果一个类既有拷贝构造函数,也有移动构造函数,那么我现在对这个类进行实例化时,传入的是右值,按照上面的说法,右值既可以是拷贝构造函数的参数,也可以是移动构造函数的参数,那么到底调用的是哪个构造函数 => 当传来的参数是右值时,虽然拷贝构造函数也可以接收,但是编译器会认为移动构造函数更加匹配,就会调用移动构造函数
所以总结如下: 如果一个类中既定义了拷贝构造函数,又定义了移动构造函数,那么,在对这个类进行实例化构造对象时:
a. 如果是左值做参数,那么就会调用拷贝构造函数,做一次拷贝 (比如这个类的构造函数参数是string类型,string是存放在堆空间上的资源,每做一次拷贝构造函数就会对这个资源做一次深拷贝)
b. 如果是右值做参数,那么就会调用移动构造,调用移动构造就不需要拷贝,避免了资源的深拷贝
我们来看一个简单而直观的例子:
string oriStr("My Work");
string leftStr = oriStr; //oriStr是左值,所以这里调用了拷贝构造函数
string rightStr = move(oriStr); // oriStr被move后成为了右值,所以调用了移动构造函数,oriStr的资源会被移动走,转移用来构造rightStr
我们可以模拟具体的指向情况和步骤如下:
1.1.2 移动赋值 -- 转移参数右值的资源来赋给自己
//模拟实现string类的移动赋值
string& operator=(string&& str) { swap(str); return *this; }
拷贝赋值运算符(copy assignment operator)和移动赋值运算符(move assignment operator)都是用于对象之间的赋值操作,它们之间的不同是
拷贝赋值的参数是const引用,接收左值或者右值
移动赋值的参数是右值引用,接收右值或者被move的左值(左值可以通过移动语义move成右值)
和上面的构造函数一样,如果同时存在拷贝赋值和移动赋值,当传来的参数是右值时,虽然拷贝赋值函数也可以接收,但是编译器会认为移动赋值函数更加匹配,就会调用移动赋值函数. 所以总结如下:
若是左值做参数,那么会调用拷贝赋值,做一次拷贝
若是右值做参数,那么就会调用移动赋值,调用移动赋值会减少拷贝
我们来看一个简单的例子
string oriStr("aaaaa"); string copyStr("bbbbb"); copyStr = oriStr; //oriStr是左值,所以调用拷贝赋值函数 string moveStr("ccccc"); moveStr = std::move(oriStr); //oriStr通过移动语义std::move后变成了右值,所以调用移动构造函数,oriStr的资源会被转移用来赋给moveStr => 这里就是举个例子,std::move一般是不会这样用的,因为这样,oriStr的资源就被转移走了