背景
开始学习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(lvalue);
左/右值引用本身是什么?
被声明出来的左、右值引用都是左值。 因为被声明出的左右值引用是有地址的,也位于等号左边。
void test(int&& right_value) { right_value = https://www.it610.com/article/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 = https://www.it610.com/article/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 = https://www.it610.com/article/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
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(ref_r));
// ok,std::forward的T是int类型,属于条件b,因此会把ref_r转为右值
}
int main() {
int a = 5;
A(std::move(a));
}
参考资料 【C++右值引用与移动语义】https://zhuanlan.zhihu.com/p/...
推荐阅读
- 个人日记|K8s中Pod生命周期和重启策略
- 学习分享|【C语言函数基础】
- C++|C++浇水装置问题
- 数据结构|C++技巧(用class类实现链表)
- C++|从零开始学C++之基本知识
- 步履拾级杂记|VS2019的各种使用问题及解决方法
- leetcode题解|leetcode#106. 从中序与后序遍历序列构造二叉树
- 动态规划|暴力递归经典问题
- 麦克算法|4指针与队列
- 遇见蓝桥遇见你|小唐开始刷蓝桥(一)2020年第十一届C/C++ B组第二场蓝桥杯省赛真题