目录
1、左值引用的缺陷
左值引用作为函数参数传递,减少了参数拷贝;但是作为函数返回值,并不适用于所有场景,比如要返回一个临时对象。
直接返回临时对象的话,此时编译器就会调用拷贝构造,构造一个新的临时对象保存到main函数的栈上,然后销毁原本的临时对象。如果对象中有一个大小为10000的数组,深拷贝对程序运行效率影响较大。
class Point
{
public:
Point(int x, int y): _x(x), _y(y) {}
Point(const Point& p):_x(p._x), _y(p._y)
{
cout << "Point(Point& p)被调用了 —— 深拷贝" << endl;
}
Point& operator=(const Point& p)
{
_x = p._x;
_y = p._y;
}
Point getPos() {
Point p(_x, _y); // 创建临时对象
return p; // 返回临时对象
}
private:
int _x;
int _y;
};
2、移动构造:解决临时对象的深拷贝
移动构造是右值引用的应用之一,其本质就是将要返回的临时对象的资源移动到其他地方。下面通过示意图和案例来了解。
(1) 移动构造的本质
移动构造也是构造函数的一种,区别在于,传递的参数必须是右值,即表达式、函数返回值等。其本质是将右值保存的资源,转移到其他临时对象。以上述 getPos 函数为例。
getPos函数返回一个临时对象A,临时对象A出了作用域就被销毁,肯定不能直接返回,所以会先调用移动构造(函数返回值是右值)。
移动构造会将临时对象A的资源转移到新构造的对象B中,然后再销毁临时对象A,这样的话,就减少了深拷贝,提升程序的运行效率。
(2) 移动构造具体实现
所谓转移资源,其实就是将临时对象A和临时对象B的资源交换:
- 临时对象A:传递的右值形参 p
- 临时对象B:构造的对象 *this
// 移动构造
Point(Point&& p)
{
// cout << "Point(Point&& p) 被调用了 —— 资源转移" << endl;
this->swap(p);
}
// 资源交换
void swap(Point& p)
{
std::swap(_x, p._x);
std::swap(_y, p._y);
}
注意:移动构造的形参 p 是左值,因为临时对象A(右值)作为实参被传递,被赋给了形参 p(左值)。
(3) 移动构造测试
我们在先前的代码中加入移动构造,然后使用同样的方法测试
class Point
{
public:
Point(int x, int y): _x(x), _y(y) {}
Point(const Point& p):_x(p._x), _y(p._y)
{
cout << "Point(Point& p)被调用了 —— 深拷贝" << endl;
}
Point& operator=(const Point& p)
{
_x = p._x;
_y = p._y;
}
// 移动构造
Point(Point&& p)
{
cout << "Point(Point&& p) 被调用了 —— 资源转移" << endl;
this->swap(p);
}
// 交换资源
void swap(Point& p)
{
std::swap(_x, p._x);
std::swap(_y, p._y);
}
Point getPos() {
Point p(_x, _y);
return p;
}
private:
int _x;
int _y;
};
3、拓展:移动赋值
(1) 起因
当我们把一个对象赋给另一个对象的时候,其实就是把一个对象里的内容完全拷贝到另一个对象,这个时候也会发生深拷贝。
Point p(10, 20);
// Point p1 = p; // 会被编译器优化成调用拷贝构造
Point p1(20, 30);
p1 = p; // 赋值完毕以后打算舍弃 p
假如我们的目的是,将 p 赋给 p1 以后舍弃对象p,若对象p中包含一个大小为10000的数组,这个时候的深拷贝就比较影响效率了。
(2) 移动赋值
移动赋值借用了移动构造的思路,既然不需要对象p,那就索性将一个对象p的资源转移到对象p1
那么我们就需要新增一个 operator=() 的重载了
Point& operator=(Point&& p)
{
// cout << "Point& operator=(Point&& p) 被调用了 —— 移动赋值,资源转移" << endl;
this->swap(p); // swap 函数的定义详见上面的移动构造
return *this;
}
(3) 移动赋值测试
测试用的代码需要稍微改动一下,移动赋值的形参必须是一个右值,这里我们需要用move函数将对象p 转换成右值。其内部也是以特殊的方式直接返回传入的对象,这样的话返回值就变成了一个临时对象,也就是右值。
int main() {
Point p(10, 20);
Point p1(20, 30);
p1 = move(p);
return 0;
}
4、右值引用的缺陷(“完美转发”解决)
尽管右值引用能够解决 “ 函数传递临时对象发生深拷贝 ” 的问题,但是依然也有缺陷:将右值传递给函数形参以后,会退化为左值。
(1) 缺陷
请看下面的案例:
- Test 函数被调用,传递给Test函数的实参是一个右值
- Test 函数调用 Func 函数,将参数传递给 Func 函数
void Func(int&& x)
{
cout << "右值引用" << endl;
}
void Func(int& x)
{
cout << "左值引用" << endl;
}
void Test(int&& x) {
Func(x);
}
int main()
{
Test(10);
return 0;
}
按照正常结论来将,Test接收的是右值,然后把右值传递给Func,此时应该调用的是接收右值的Func函数
(2) 原因分析
10 作为实参(右值)传递给Test函数,但是右值作为实参被赋给了 形参 x,形参 x 是左值。实参在被传递的时候就失去了原本的右值属性,所以调用的是 Func(int& x)
(2) 解决
我们的目的是希望在传递参数的时候以最原始的属性传递,也就是右值属性,我们只需要在调用Func的时候使用“完美转发”。
完美转发格式:std::forward<数据类型>(变量名)
void Test(int&& x) {
Func(std::forward<int>(x)); // std::forward<类型>(变量名)
}
int main()
{
Test(10);
return 0;
}
注意:完美转发同样适用于模板