区分左值和右值
在学习c++11的右值引用前,大家肯定会有点陌生什么是右值?什么是左值?现在我先来带大家熟悉一下概念。
左值
可以被取地址,也可被修改(const修饰的除外)
可以出现在等号左边,也可以出现在右边
//a,b,c均为左值
int a = 1;//a出现在等号左边
const int b = a;//a出现在等号右边
int* c = new int;
右值
举例:字母常量、表达式的返回值、函数的返回值(不能是左值引用返回)等等。
不可以被取地址,不可被修改
只能出现在等号右边,不能出现在等号左边(因为不可被修改)
int x = 2, y = 1;
//下面常见的右值
10;
"xxxxxx";
x + y;
fmax(x, y);
如何区分?
误区:许多小伙伴喜欢看这个值在等号的哪边来区分这个值是左值还是右值,其实是不正确的,正确的区分方法应该是判断这值是否能被取地址!
左值引用:
左值引用也就是对左值取别名,其实我们之前学习的引用就是左值引用,用符号&来声明,比如:
//a,b,c均为左值
int a = 1;//a出现在等号左边
int b = a;//a出现在等号右边
int* c = new int;
//d,e,f均为左值引用
int& d = a;
int& e = b;
int& f = *c;
这里其实和大家之前学的引用一样,就不过多赘述。
右值引用
右值引用也就是对右值取别名,用符号&&来声明,比如:
int x = 2, y = 1;
//下面是常见的右值
10;
"xxxxxx";
x + y;
fmax(x, y);
//下面是常见的右值引用
int&& a = 10;
string&& b = "xxxxx";
int&& c = x + y;
int&& d = fmax(x, y);
特别注意:
右值引用本身是左值! 右值引用本身是左值! 右值引用本身是左值!
也就是说上面代码中的 a,b,c,d均是左值!!!
原因很简单,如果右值引用本身还是右值,那么右值引用将毫无意义,无法修改,进行后续操作。
左值引用及右值引用的意义
正所谓知其然,知其所以然。
想要彻底掌握这两种引用的用法,我们就需先了解这两种引用的出现意义和历史渊源。
现在我带大家从c语言讲起:
C语言的弊端:
大家在学习c++初期时,想必都了解过,c++其实是为了解决c语言的大部分弊端,而衍生出来的新语言,那么引用的出现,究竟是为了解决哪些弊端呢?请看如下代码:
#include<iostream>
using namespace std;
void my_swap(int x,int y)
{
int t = x;
x = y;
y = t;
}
int main()
{
int x = 1;
int y = 2;
cout << "x=" << x << " " << "y=" << y << endl;
my_swap(x, y);
cout << "x=" << x << " " << "y=" << y << endl;
return 0;
}
运行结果:
我实现了一个简单的交换函数,为什么最后的值没有交换?
原因很简单,函数里的x,y是形参,形参只是实参的一份临时拷贝,形参的修改不会影响到实参,那么c语言是如何解决这个问题的呢?
没错,使用指针!
#include<iostream>
using namespace std;
void my_swap(int* x,int* y)
{
int t = *x;
*x = *y;
*y = t;
}
int main()
{
int x = 1;
int y = 2;
cout << "x=" << x << " " << "y=" << y << endl;
my_swap(&x, &y);
cout << "x=" << x << " " << "y=" << y << endl;
return 0;
}
左值引用的诞生
但是在c++看来指针繁琐且难以理解,故c++创造了左值引用来解决这类问题:
#include<iostream>
using namespace std;
void my_swap(int& x,int& y)
{
int t = x;
x = y;
y = t;
}
int main()
{
int x = 1;
int y = 2;
cout << "x=" << x << " " << "y=" << y << endl;
my_swap(x, y);
cout << "x=" << x << " " << "y=" << y << endl;
return 0;
}
左值引用还可作返回值:
using namespace std;
class my_string
{
public:
my_string(string str = "xxxxx")
{
_str = str;
}
my_string& operator+=(char s)
{
_str.push_back(s);
return *this;
}
private:
string _str;
};
int main()
{
my_string s1("test ");
my_string s2 = s1 += 'a';
return 0;
}
如果不用左值引用返回,直接传值返回,那么返回时还要进行一次拷贝,这样大大降低了效率,左值引用的出现,减少了拷贝,大大提高了效率。
注意:
左值引用作返回值时,返回的自定义类型必须出了作用域(函数体)仍然存在,才可使用,不然就会出现野引用。
所以如果我们在函数体里面创建了一个自定义类型,是不能左值引用返回的,因为这个自定义类型是在函数里面创建的,出了函数体就不存在了。比如(错误示范):
my_string& operator+(char s)
{
string str1(_str);
str1.push_back(s);
my_string str2(str1);
return str2;
}
上面str2出了函数体就释放了,所以不能用左值引用返回。
到这里不难总结出,左值引用虽然在某些情况,减少了拷贝,大大提高了代码的效率,但是不全面,还是有些场景下会出现不可避免的拷贝问题。
右值引用的诞生
c++11更新后为了弥补左值引用的不足,创造出了右值引用,完全彻底避免了不必要的拷贝,没错就是右值引用返回。
继续看到上面的代码,引入一个新名词:将亡值 也就是str2,顾名思义,将亡值也就是即将消亡的值,正如str2出了函数体后直接释放,那么右值引用又是如何解决这个问题的呢?
#include<iostream>
#include<string>
using namespace std;
class my_string
{
public:
my_string(string str = "xxxxx")
{
_str = str;
}
void swap(my_string& str)
{
std::swap(_str, str._str);
}
my_string(my_string&& s)//移动构造
{
swap(s);
}
my_string&& operator+(char s)
{
string str1(_str);
str1.push_back(s);
my_string str2(str1);
return move(str2);
}
private:
string _str;
};
右值引用返回+移动构造,通过上述代码不难看出,移动构造其实就是将即将释放的将亡值str2的资源直接通过swap函数转移出来,大大减少了拷贝。
移动赋值同理。
移动构造及移动赋值特点
移动构造和移动赋值也是类的默认成员函数,一般其它的默认成员函数,都是自己不写,编译器自动生成,但这两个默认构造函数略有不同:
如果你没有自己实现移动构造函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中的任
意一个。那么编译器会自动生成一个默认移动构造。默认生成的移动构造函数,对于内置类
型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动构造,
如果实现了就调用移动构造,没有实现就调用拷贝构造。
如果你没有自己实现移动赋值重载函数,且没有实现析构函数 、拷贝构造、拷贝赋值重载中
的任意一个,那么编译器会自动生成一个默认移动赋值。默认生成的移动构造函数,对于内
置类型成员会执行逐成员按字节拷贝,自定义类型成员,则需要看这个成员是否实现移动赋
值,如果实现了就调用移动赋值,没有实现就调用拷贝赋值。(默认移动赋值跟上面移动构造
完全类似)
如果你提供了移动构造或者移动赋值,编译器不会自动提供拷贝构造和拷贝赋值。
关于红字部分原因很简单,一般涉及到深拷贝时,就都要实习那析构,拷贝构造,拷贝赋值,移动构造,移动赋值,所以这些函数差不多是绑定在一块的。
完美转发
模板中的&&万能引用:
注意&&如果出现在模板中,那么它代表的不一定是右值引用,而是万能引用,既可以接受左值,又可以接收右值。
std::forward 完美转发在传参的过程中保留对象原生类型属性
void Fun(int& x) { cout << "左值引用" << endl; }
void Fun(const int& x) { cout << "const 左值引用" << endl; }
void Fun(int&& x) { cout << "右值引用" << endl; }
void Fun(const int&& x) { cout << "const 右值引用" << endl; }
// std::forward<T>(t)在传参的过程中保持了t的原生类型属性。
template<typename T>
void PerfectForward(T&& t)
{
Fun(std::forward<T>(t));
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(std::move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(std::move(b)); // const 右值
return 0;
}
运行结果:
注意,如果不加forward,那么如果传的是右值那么t本身会变成左值,因为右值引用本身是左值。
不加forward运行结果: