目录
一.移动语义的前置知识
1.移动语义的简单理解
a.基本概念
移动语义可以通过右值引用将资源(如动态内存、文件句柄等)从一个濒临死亡的对象"移动"到另一个对象,而不是进行资源的复制,它本质是进行"资源转移"。
例如:
2.作用
C++ 引入移动语义旨在减少不必要的对象复制 ,从而提高程序的性能。移动语义主要通过右值引用 和移动构造函数以及移动赋值运算符重载来实现。
移动语义能够实现资源"管理权的转移",以减少不必要的资源拷贝,那么,它的底层原理是什么??--- 想要挖掘它的工作原理,咱们得先认识一下右值引用~~
2.右值引用
a.左值和右值的基本概念
什么是左值??--- 左值在内存中有一个确定的地址,我们可以获取它的地址 ,并对它进行赋值操作 ,左值可以出现在赋值符号的左边,如:指针、变量(包括const修饰的)等。
什么是右值??--- 右值通常只能出现在赋值语句的右边 ,它们代表的是临时对象 或即将被销毁的对象 。右值不能取地址,也不能修改,一般是:常量、匿名对象、表达式返回值、函数返回值(不能是引用返回)。
b.左值引用和右值引用
左值引用,本质就是给左值取别名;
左值引用使用"&"符号声明,它与引用对象使用同一块地址:
左值引用的使用场景以及缺陷
使用场景:①做函数参数; ②做函数返回值。
价值:能够避免复制大的对象,提高函数参数传递和返回的效率。
缺陷:函数内的局部对象无法做引用返回的参数。这种情况就得靠咱们的移动语义了!!
右值引用,本质就是给右值取别名!
右值引用使用"&&"符号声明,它可以引用临时生成的值 ,这些值在表达式结束后通常会被销毁。但绑定到右值引用后,这些值的生存期会延长至与绑定到它的右值引用的生存期相同。
内置类型的右值,就是纯右值,而自定义类型的右值,都可以理解成将亡值!
右值引用存在的意义:①用于支持移动语义;②用于支持完美转发。
两者的区别和联系
左值引用只能引用左值,即具有确定地址和可修改性的对象。但如果用 const修饰左值引用,就可以给右值取别名。
右值引用只能引用右值,即临时生成的值或字面常量等。但通过 std::move() 函数可以将左值转换为右值,从而允许其参与移动语义。
二.移动语义
1.移动构造函数
a.基本功能
移动构造函数是一个特殊的构造函数,它接受一个右值引用作为参数 。右值引用使用 && 语法,表示该引用对象是临时对象或即将被销毁的对象。移动构造函数通常用于从源对象"窃取"资源(如动态分配的内存、文件句柄等),而不是复制这些资源,从而避免不必要的开销。
例如:
b.应用场景
①返回局部对象:当函数返回局部对象(自定义类型且需要深拷贝的对象)时,如果返回类型与局部对象类型相同且支持移动语义,编译器将使用移动构造函数而不是拷贝构造函数。
②插入到容器中:当对象被插入到支持移动语义的容器中(如std::vector)时,如果该对象是"将亡值",则使用移动构造使容器新元素"夺取"插入对象的资源;如果容器需要扩容,它将使用移动构造函数来移动现有元素,而不是复制它们。
③使用std::move:std::move 是一个标准库函数,它将其参数转换为右值引用,从而允许在需要时强制使用移动构造函数或移动赋值运算符。但请注意,std::move 本身并不移动任何东西;它只是改变了对象的值类别(从左值变为右值),从而允许编译器选择移动操作。
如果我们在类中定义了移动构造函数,那么我们通常也应该定义移动赋值运算符,以及确保析构函数能够安全地处理被移动的对象(即,被移动的对象在移动后应处于有效但未定义的状态)。
2.移动赋值重载
a.基本功能
移动赋值重载是类中一个特殊的成员函数,其作用是允许一个对象从另一个右值对象(蒋亡值)中"夺取"资源,而不是复制这些资源。这样做的目的是减少资源管理的开销,特别是在处理动态内存分配、文件句柄、网络连接等资源时。
b.注意事项
异常安全性:在移动赋值重载中,推荐使用 noexcept 说明符,因为许多标准库容器和算法在移动对象时假定它们不会抛出异常。
资源管理 :确保在移动资源后,源对象处于有效但不可预知的状态。这通常意味着将指针成员设置为 nullptr 或适当的无效值。
自赋值检查:始终检查自赋值情况,以避免资源泄漏或未定义行为。
与拷贝操作的交互 :如果类同时提供了拷贝构造函数和拷贝赋值操作符,以及移动构造函数和移动赋值操作符,需注意,要避免在移动操作中意外地调用拷贝操作。
浅拷贝的类,没有需要转移的资源,移动语义不需要实现,其传值返回拷贝的代价也不是很大!移动构造和移动赋值重载,是专门为了解决自定义类型深拷贝问题的!
编译器自动生成移动构造和移动赋值的条件 :类未显式声明任何拷贝构造函数 、赋值重载、析构函数 和移动构造函数(显然的,如果已声明则不会再次生成)。
3.move
a.基本功能
template <class T> typename remove_reference<T>::type&& move (T&& arg) noexcept;
当我们有一个左值对象,且我们想利用移动语义时,我们可以使用 std::move()函数。这样就能将一个对象的资源"转移"到另一个新对象,以减少拷贝所带来的性能消耗。
使用 std::move 后,原对象的状态是未定义的。这意味着我们不能在移动后再使用它,除非我们重新给它赋予了一个有效的状态。
std::move 本身并不改变性能;它只是一个类型转换。真正的性能提升来自于移动构造函数或移动赋值操作符的实现,这些操作通常比复制操作更高效。
如果我们的移动构造函数或移动赋值操作符可能抛出异常,那么使用 std::move 需要格外小心。如果移动操作失败(抛出异常),原对象可能已经被破坏,而新对象可能还未完全构造。
b.底层原理
std::move 并不真正移动资源,而是将对象转换为右值引用,从而允许编译器选择移动构造函数或移动赋值操作符:
模版+右值引用=万能引用,如:template<typename T>void func( T&& t) { }
若所传参数是左值,编译器就会将t识别成左值引用(引用折叠),即T&类型;若所传参数是右值,编译器就会将t识别成右值引用,即T&&类型!
编译器自动生成的默认构造和赋值,对内置类型的成员变量"浅拷贝",对自定义类型的成员变量会去找它的"移动构造"或"移动赋值",若没写,则调用其拷贝构造或赋值重载!
注意:基于右值引用的移动构造和移动赋值,其作用对象是"将亡值"且需要"深拷贝"!!
三.完美转发
1.右值引用的对象具有左值属性
我们知道,右值不能取地址,也不能修改。但是,引用对象本身有一个持久的存储位置(即它指向某个对象),这使得它表现得像左值一样。即,右值引用的对象是左值,具有左值属性,可以取地址,也可以被修改!
例如:string(string&& str) { this->swap(str); } void swap(string& s) { ... }
其中,str是右值引用的对象,但 swap(string& s) 参数类型是左值引用,这就表明 str 必须是左值,具有左值属性!
再如:
int&& rr = 10; 本质是,OS在内存中开了一块空间,存放右值,而右值引用便指向这块空间,所以右值引用的对象具有左值属性!
那么,若是想要使右值引用的对象保持右值属性,该如何做?? --- 完美转发
2.forward
std::forward 是一个标准库函数,它用于在模板编程中实现完美转发。完美转发允许我们在不改变参数类型或值类别的情况下,保持参数的原始值类别。
例如:forward<T>( t ); T是t的类型,若t是右值引用的对象,那么完美转发便可使t保持右值属性!
完美转发的使用场景:
list<string> lt; ------> lt.push_back("hello world"); ------>
void push_back (T&& val) { node* newnode = new node(val); } ------>
node(const string& val) : _val(val) { }
其中,val是右值引用的对象,具有左值属性,所以new node(val)调用node类的构造时,会自动调用左值引用的拷贝构造,而不会去调用 node(const string&& val) : _val(val) { },也就无法进行移动拷贝,所以这里需要使val保持右值属性,即使用forward<T&&>(val)
decltype 关键字 --- 将变量的类型,声明为指定变量或表达式的类型,例如:
decltype( pf ) pf2;
它会先推导出 pf 的类型,并用该类型再次定义一个对象,decltype( pf )还可以作为函数参数!
C++11 新出的容器和接口
新容器:array、forward_list、unordered_map、unordered_set
新接口:cbegin、crbegin、cend、crend,鸡肋,没啥用
干货知识:
1.所有的容器都支持了**{ }列表初始化的构造函数**
2.所有容器均新增了emplace系列的接口(性能提升),右值引用+模版的可变参数
3.所有的容器都增加了移动拷贝和移动赋值(在特殊场景下大幅度优化了深拷贝的性能)。