09 左右值引用及移动语义
左、右值引用
左值
1.定义
左值是一个在内存中具有持久存储位置的表达式,它可以出现在赋值表达式(=)的左侧或右侧(通常在左边)
- 左值通常指的是局部变量或者具有持久存储的实体包括:
- 命名空间作用域的变量:例如全局变量或静态成员变量,它们在程序的整个运行期间都存在。
- 局部静态变量:在函数内部定义的静态局部变量,它们在第一次使用时初始化,并在程序的其余部分保持其值。
- 局部非静态变量:在函数内部定义的局部变量(自动变量),它们在进入函数时创建,并在函数退出时销毁。
- 类成员变量:对象的成员变量,它们与对象一起被创建和销毁。
- 数组:具有确定大小和存储位置的数组。
具有持久存储位置的变量可以被多次访问和修改,它们可以作为函数参数的左值引用传递,以避免复制并允许修改原始变量
语法:
1 | int a = 10; |
2.左值引用
左值引用是C++的一个引用类型,必须绑定到一个对象上,相当于给这个对象起别名,从而可以通过左值引用来操作被绑定的变量
注意事项:
- 绑定:左值引用必须绑定到一个有效的对象上,不能绑定到临时对象、匿名对象、右值上
- 作用:左值引用允许对原始对象进行操作,包括修改和访问
- 生命周期:左值引用的生命周期与它绑定的对象的生命周期相关联。如果引用的对象超出作用域而失效,那么左值引用本身也会变得无效
函数如果要返回引用类型的变量的时候需要格外注意,不能返回函数体内的一个局部变量的引用!
3.与指针的区别
左值引用和指针很像,但是仍有以下的区别:
- 左值引用必须在定义时立即初始化,即绑定一个持久存在的对象;而指针可以不在定义的时候初始化
- 左值不能绑定到临时变量和匿名对象上,但是指针可以,甚至是执行
nullptr
3.1问题代码
1 | int& test(){ |
这个代码就是个典型的问题代码,因为它返回了局部变量的引用,这样的话,由于函数结束a的内存就被释放了,所以返回的那个引用,就不知道指向哪里了。修改方法:
1 | int& test(){ |
由于上述代码的返回值是
int&
,所以调用函数的时候,=左边的变量应该是引用类型,比如:int &c = test();
3.2左值引用会调构造函数么
创建左值引用不会再次调用构造函数
比如:
1 | class Person; |
创建空指针其实也不会调用构造函数,只有创建对象是才会
1 | Person* p1 = new Person(); // 调用构造函数 |
4.左值引用的作用
1.函数参数
使用左值引用作为函数参数,可以避免不必要的拷贝(不会调用拷贝构造函数),并允许函数修改原始对象
1 | void increment(int& number) { |
如果形参类型除了左值引用还有const,比如fun(const int& a)
,作用如下:
- 避免不必要的拷贝
- 表面意图,提高可读性
- 保持不变性
2.返回左值引用以实现链式操作
1 | class Wrapper { |
右值
1.定义
右值指的是那些在内存中不具有持久存储位置,通常在表达式中出现作为临时对象的值,在表达式求值后就会被销毁
- 右值通常包括以下这些:
1.字面量
1 | int x = 10; // '10' 是一个整数字面量,它是一个右值 |
在这里,10
是一个整数字面量,它没有存储在内存的特定位置,仅在表达式中存在。
2.表达式结果
1 | int a = 5; |
a + b
这个表达式的结果是一个临时值,它存储在某个临时的内存位置,不能被多次使用。
3.函数返回值或函数的调用结果
1 | int fun(){ |
在函数返回时,会把左值变成右值来在表达式中赋值,且离开函数作用域时,左值因为是局部变量,本身也没了
4.对象的临时拷贝
1 | std::string createString() { |
调用 createString()
时返回的 std::string
对象是一个临时对象,它在赋值给 mainString
之前是一个右值。注意,这种复制会调用==拷贝==构造函数
5.使用 std::move
1 | std::string getString() { |
在这里,std::move(s)
将左值 s
转换为右值引用,这样 getString()
函数的返回值就是一个右值的引用,main
中的 s2
通过==移动==构造函数而非拷贝构造函数来接收这个字符串
2.右值引用
右值引用也是一个引用类型,相当于给一个右值起个别名,从而可以通过右值引用来操作被绑定的变量
注意事项:
- 绑定:右值引用必须绑定到右值上,比如临时变量,表达式的值,函数调用结果或是被
std::move()
转成右值的左值 - 生命周期:右值引用的生命周期与它绑定的对象的生命周期相关联。一旦被绑定的右值失效,右值引用也会立即失效
- 不允许多个右值引用绑定同一个右值:右值引用通常绑定到临时对象,因此不允许两个右值引用指向同一个对象,因为这可能导致资源的多次释放
3.右值引用的作用
1.移动语义(Move Senmantic)
移动语义是一个编程中的概念,它们可以实现在程序运行时,移动一个即将被销毁的对象(右值)的所有权,以避免复制操作,提高程序性能(通常用于对象的初始化)==由于移动了所有权,故移动后不能再操作原对象==
通常只有分配了资源(动态内存、锁,文件描述符…)的类/结构体实例,才需要用到移动语义,普通变量如
int
不能用移动语义
要实现移动语义,必须通过以下2个方式:
- 移动构造函数
- 移动赋值运算
这2种方式的输入形参都是右值引用
1 |
|
从上面的例子可以看出,只要我们实现了一个类的移动构造函数 或者 移动赋值运算符,那么我们在初始化这个类的新对象时,就可以用到移动语义了
std::move()
是C++11中引入的一个标准库的函数,用于将一个左值强制转为右值引用,以触发移动语义
std::move()
可以将任何左值转成右值引用,但是是否能触发移动语义,得看std::move()
的这个类,是否定义了移动构造函数、赋值符之类的
1 |
|
从上面的2个实例可以看出,移动语义有2种常见的使用范式:
1.函数返回就返回普通变量,然后在需要使用移动语义时,手动使用std::move()
将这个值转成右值引用,进而调动移动构造函数,触发移动语义
1 | cv::Mat a(100,100,CV_8UC1); // 左值 |
2.函数return一个右值引用,这样的话因为返回值是cv::Mat
所以会触发一个移动构造函数,如果不这样的话,应该是一个拷贝构造函数,我觉得差不多,这样反而难以理解,所以最好别用这个方式了
1 | cv::Mat getMat() |
对函数形参做移动语义的时候要格外小心,特别是传入的是左值引用时
你传入的这个形参别的地方可能也要用,你这里用移动语义的话,就相当于抢占了这个资源,可能会引发很多问题