背景
开始学习C++11和准备秋招面试时,对右值引用和移动语义进行的深入学习,恰巧在面试中又被问到,深入记录一下。
左值/右值
左值:可以取地址、位于等号左边 -> 有地址的变量
右值:没法取地址、位于等号右边 -> 没有地址的字面值、临时值
两个例子:
int a = 5;
- a->可以通过 & 取地址,位于等号左边,是左值。
- 5位于等号右边,5没法通过 & 取地址,所以5是个右值。
struct A {
A(int a = 0) {
a_ = a;
}
int a_;
};
A a = A();
- a ------------>可以通过&取地址,位于等号左边,是左值
A()-> 临时值,没法通过&取地址,位于等号右边,是右值
左值引用/右值引用
引用的本质是别名。
通过引用修改变量的值,传参时传引用可以避免拷贝。左值引用
左值引用:能指向左值,不能指向右值的引用
引用时变量的别名,右值没有地址无法被修改
const左值引用可以指向右值(不会修改指向值,可以指向右值)int a = 5; int &ref_left_a = a; //左值引用指向左值,编译通过 int &ref_left_a = 5; //左值引用指向右值,编译失败 const int &ref_left_a = 5; //编译通过
右值引用
右值引用:可以指向右值,不能指向左值
int a = 5; int &&ref_right = 5; //编译通过 int &&ref_a_right = a; //编译不通过 ref_right = 6; //右值引用:可以修改右值
左/右值引用本质的讨论
右值指向左值的方法
使用
std::move
int a = 5; int &ref_a_left = a; // 左值引用指向左值 int &&ref_a_right = std::move(a); // 通过std::move将左值转化为右值,可以被右值引用指向 cout << a; // 打印结果:5
std::move
唯一的功能:把左值强制转换为右值,让右值引用可以指向左值
等同实现:static_cast<T&&>(lvalue);左/右值引用本身是什么?
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
void test(int&& right_value) { right_value = 8; } int main() { int a = 5; // a是个左值 int &ref_a_left = a; // ref_a_left是个左值引用 int &&ref_a_right = std::move(a); // ref_a_right是个右值引用 test(a); // 编译不过,a是左值,change参数要求右值 test(ref_a_left); // 编译不过,左值引用ref_a_left本身也是个左值 test(ref_a_right); // 编译不过,右值引用ref_a_right本身也是个左值 test(std::move(a)); // 编译通过 test(std::move(ref_a_right)); // 编译通过 test(std::move(ref_a_left)); // 编译通过 test(5); // 当然可以直接接右值,编译通过 cout << &a << ' '; cout << &ref_a_left << ' '; cout << &ref_a_right; // 打印这三个左值的地址,都是一样的 }
右值引用既可以是左值也可以是右值,如果有名称则为左值,否则是右值。
作为函数返回值的 && 是右值,直接声明出来的 && 是左值。
相同点:
传参使用左右值引用都可以避免拷贝。
不同点:
右值引用可以直接指向右值,也可以通过std::move指向左值;而左值引用只能指向左值(const左值引用也能指向右值)
作为函数形参时,右值引用更灵活。虽然const左值引用也可以做到左右值都接受,但它无法修改,有一定局限性。
void f(const int& n) {
n += 1; // 编译失败,const左值引用不能修改指向变量
}
void f2(int && n) {
n += 1; // ok
}
int main() {
f(5);
f2(5);
}
右值引用和std::move的应用
右值引用和std::move被广泛用于在STL和自定义类中实现移动语义,避免拷贝,从而提升程序性能。
实现移动语义
以数组类举例:
class Array {
public:
Array(int size) : size_(size) { data = new int[size_]; }
// 深拷贝构造
Array(const Array& temp_array) {
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
// 深拷贝赋值
Array& operator=(const Array& temp_array) {
delete[] data_;
size_ = temp_array.size_;
data_ = new int[size_];
for (int i = 0; i < size_; i ++) {
data_[i] = temp_array.data_[i];
}
}
~Array() { delete[] data_; }
private:
int *data_;
int size_;
};
提供一个移动构造函数,把被拷贝者的数据移动过来,这样就可以避免深拷贝了
class Array {
public:
Array(int size) : size_(size) { data = new int[size_]; }
// 深拷贝构造
Array(const Array& temp_array) { ... }
// 深拷贝赋值
Array& operator=(const Array& temp_array) { ... }
// 移动构造函数,可以浅拷贝
Array(const Array& temp_array, bool move) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr; //实际上编译不通过
}
~Array() {delete [] data_; }
private:
int *data_;
int size_;
};
存在的两个问题:
1、表示移动语义还需要一个额外的参数(或者其他方式)
2、无法实现!temp_array是个const左值引用,无法被修改
右值引用出现解决问题:
class Array {
public:
......
// 优雅
Array(Array&& temp_array) {
data_ = temp_array.data_;
size_ = temp_array.size_;
// 为防止temp_array析构时delete data,提前置空其data_
temp_array.data_ = nullptr;
}
private:
int *data_;
int size_;
};
其他:
1、vector::push_back使用std::move提高性能
2、部分是move-only,例如unique_ptr,只有移动构造函数
完成转发 std::forward
std::forward
并不会做转发,同样也是做类型转换。
move只能转出来右值,forward都可以。
std::forward<T>(u)有两个参数:T与 u。
1、当T为左值引用类型时,u将被转换为T类型的左值;
2、否则u将被转换为T类型右值。
void B(int&& ref_r) { ref_r = 1; }
// A、B的入参是右值引用
// 有名字的右值引用是左值,因此ref_r是左值
void A(int&& ref_r) {
B(ref_r); // 错误,B的入参是右值引用,需要接右值,ref_r是左值,编译失败
B(std::move(ref_r)); // ok,std::move把左值转为右值,编译通过
B(std::forward<int>(ref_r)); // ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
int main() {
int a = 5;
A(std::move(a));
}
**粗体** _斜体_ [链接](http://example.com) `代码` - 列表 > 引用
。你还可以使用@
来通知其他用户。