前文中我们讲解了C++11中的部分知识点,下面我们来介绍一下C++11中的一个比较重要的知识点右值引用。
右值引用和移动语义
左值引用和右值引用
左值引用
左值就是一个数据的表达式(如变量名和解引用指针),我们可以获取它的地址+可以对其进行赋值,左值可以出现在赋值符号的左边也可以出现在赋值符号的右边,定义时const修饰符后的左值,不能给其进行赋值,但是可以取地址。左值引用就是给左值的引用,给左值取别名。
int main()
{
// 以下的p、b、c、*p都是左值
int* p = new int(0);
int b = 1;
const int c = 2;
// 以下几个是对上面左值的左值引用
int*& rp = p;
int& rb = b;
const int& rc = c;
int& pvalue = *p;
return 0;
}
右值引用
右值也是一个数据的表达式,如:字面常量、表达式返回值,函数返回值(这个返回值不是左值引用的返回)等等,右值可以出现在赋值符号的右边,但是不能出现在赋值符号的左边,且右值不能取地址。右值引用就是对右值的引用,给右值取别名。右值引用的符号就是&&。
int main()
{
double x = 1.1, y = 2.2;
// 以下几个都是常见的右值
10;
x + y;
fmin(x, y);
// 以下几个都是对右值的右值引用
int&& rr1 = 10;
double&& rr2 = x + y;
double&& rr3 = fmin(x, y);
// 这里编译会报错:error C2106: “=”: 左操作数必须为左值
10 = 1;
x + y = 1;
fmin(x, y) = 1;
return 0;
}
有一点需要注意的就是右值是不能取地址的,但是给右值取别名后,会导致右值被存储到特定位置,且可以取到该位置的地址,也就是说例如:不能取字面量10的地址,但是rr1引用后,可以对rr1取地址,也可以修改rr1。如果不想rr1被修改,可以用const int&& rr1 去引用。
int main()
{
double x = 1.1, y = 2.2;
int&& rr1 = 10;
const double&& rr2 = x + y;
rr1 = 20;
rr2 = 5.5; // 报错
return 0;
}
左值引用和右值引用比较
int main()
{
int a = 0;
int b = 1;
int* p = &a;
a + b;
// 左值引用给左值取别名
int& ref1 = a;
// 左值引用给右值取别名
//int& ref2 = (a + b); err
const int& ref = (a + b);
// 右值引用给右值取别名
int&& ref3 = (a + b);
// 右值引用给左值取别名
//int&& ref4 = a; err
int&& ref4 = move(a);
return 0;
}
右值引用总结:
1. 右值引用只能右值,不能引用左值。
2. 但是右值引用可以move以后的左值。
右值引用的使用场景和意义
首先我们来看一个简单的小例子:
void func(const int& a)
{
cout << "void func(const int& a)" << endl;
}
void func(int& a)
{
cout << "void func(int& a)" << endl;
}
void func(int&& a)
{
cout << "void func(int&& a)" << endl;
}
int main()
{
int a = 0;
int b = 1;
func(a);
func(a + b); // 如果只有左值引用,且临时变量具有常性,那么由于权限的放大是无法进行传递的;如果要进行传递就需要在func的形参中添加const。但是这样的无法对传入的参数是左值还是右值进行区分。有了右值引用就可以将左值与右值进行区分。
}
在上述的例子中就可以看出使用右值引用就可以对左值与右值进行区分。
第二个例子:
我们将右值可以进行区分将其分为纯右值与将亡值,例如,函数的返回值就可以视为将亡值。在我们自己编写的函数中编写了一个to_string的函数将整形字符变为string类型的数据。这个函数的返回值就是将亡值。
从上图中的内容符合我们之前学习过的知识,函数返回值进行传值返回的时候先要拷贝一份临时对象,然后临时对象再次给valStr进行一次拷贝构造,但是在某些编译器中就可以进行优化,被优化为一次的拷贝构造。
最后得到的就是这样的显示:
那么在这里我们就会思考一个问题,是用函数的返回值进行拷贝构造传参还是有一些资源的浪费,毕竟函数的返回值属于出了函数的作用域就会销毁,我们是否可以将这返回值的数据直接移动到需要的位置没这样就可以减少拷贝构造的使用。于是在C++11中就产生了移动构造。
// 移动构造
string(string&& s)
:_str(nullptr)
{
cout << "string(string&& s) -- 移动拷贝" << endl;
swap(s);
}
在string类中新编写一个新的构造函数参数为右值引用。这样在我们的设想中就会变成先拷贝构造临时对象,在使用临时对象进行移动构造。这样还是不太足够,毕竟还是又一个拷贝构造产生,编译器就会将先拷贝构造再移动构造的过程优化为直接进行移动构造的过程。由于函数return的值是一个左值,所以处理的时候编译器会想办法将这个返回值识别成右值,进行资源的转换,然后再对返回值进行销毁。
使用库中自带的string类型就可以看出s1的资源转移到了s3上。
同样在C++11中STL所有的容器都增加了右值引用的版本,并且所有容器的插入数据接口函数都增加了右值引用的版本。这样,例如在尾插一个右值的时候就可以直接进行资源的移动而不是进行深拷贝。
总结
左值引用减少拷贝,提高效率,右值引用也是减少拷贝,提高效率,但是他们的角度不同,左值引用时直接减少拷贝,右值引用是间接减少拷贝,识别出是左值还是右值,如果是右值就不再进行深拷贝,直接移动拷贝提高效率。
完美转发
下面来看这样的一个问题:
我们可以将&&称之为万能引用,因为它不仅可以引用左值,还可以引用右值,我们有时会需要将一个函数传入的左值,再次向另一个函数传递就如下面的例子所示:
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; }
// 万能引用(引用折叠):既可以引用左值,也可以引用右值
template<typename T>
void PerfectForward(T&& t)
{
Fun(t);
}
int main()
{
PerfectForward(10); // 右值
int a;
PerfectForward(a); // 左值
PerfectForward(move(a)); // 右值
const int b = 8;
PerfectForward(b); // const 左值
PerfectForward(move(b)); // const 右值
return 0;
}
可以发现,不论我们传入的参数是什么样的形式最终都在Fun函数中显示的是左值。 原因是右值引用引用后属性会变为左值,这样才能够实现资源的转移。右值在被引用之后就会被存储到特定的位置,且可以取到该位置的地址,那么就可以对这个引用后的值进行修改或者资源转移等的操作。如果这个数在右值引用之后还是一个右值,那么后续对该值的一些列操作都可能会失败。那么为了能够进行正确的转发,就需要使用std::forward<T>(t),这个函数的作用就是完美转发在传参的过程中保留对象原生类型属性。
下面我们可以来看一个例子:
有一个自己编写的链表,使用自己编写的string类型作为变量类型对于下面的代码来说都执行的是深拷贝,这非常的好理解。
在代码中添加上&&的版本运行之后却发现并没有执行移动构造,这就是因为作为右值的变量push_back到链表中后它的属性就变为了左值,因此需要添加完美转发来保证变量的属性为右值。然而在push_back函数中添加完之后,结果并没有发生改变,因为这个函数还调用了insert函数,insert函数还调用了构造函数,形成了函数传递的过程。当确保了整个过程都是完美转发时候,就能够保证移动构造的执行。
void push_back(T&& x)
{
insert(end(), forward<T>(x));
}
void insert(iterator pos, T&& x)
{
node* cur = pos._node;
node* prev = cur->_prev;
node* new_node = new node(forward<T>(x));
prev->_next = new_node;
new_node->_prev = prev;
new_node->_next = cur;
cur->_prev = new_node;
}
list_node(T&& x = T())
:_next(nullptr)
, _prev(nullptr)
, _data(forward<T>(x))
{}
移动赋值
除了移动构造在C++11中还有着移动赋值的出现,在没有右值引用之前,使用赋值重载的结果如下图,在to_string函数返回时调用了移动构造,如我们之前所述将函数返回值这个将亡值的资源进行转移,然后调用赋值重载在赋值重载中使用了现代写法进行深拷贝。
那么在有了右值引用之后就可以进行修改,将将亡值的资源直接移动拷贝到需要的地方。
// s = 将亡值
string& operator=(string&& s)
{
cout << "string& operator=(string&& s) -- 移动拷贝" << endl;
swap(s);
return *this;
}