C++ 移动语义

目录

一.移动语义的前置知识

1.移动语义的简单理解

a.基本概念

2.作用

2.右值引用

a.左值和右值的基本概念

b.左值引用和右值引用

二.移动语义

1.移动构造函数

a.基本功能

b.应用场景

2.移动赋值重载

a.基本功能

b.注意事项

3.move

a.基本功能

b.底层原理

三.完美转发

1.右值引用的对象具有左值属性

2.forward


一.移动语义的前置知识

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.所有的容器都增加了移动拷贝和移动赋值(在特殊场景下大幅度优化了深拷贝的性能)。

相关推荐
LaoZhangGong12319 分钟前
使用常数指针作为函数参数
c++·经验分享
边疆.20 分钟前
C++类和对象 (中)
c语言·开发语言·c++·算法
随便取个六字2 小时前
C++学习:类和对象(二)
c++·学习
jimmy.hua3 小时前
C++刷怪笼(9)继承
开发语言·c++
OMGmyhair3 小时前
【 C++ 】C++11的初步学习
开发语言·c++·学习
秋说4 小时前
【数据结构 | PTA】懂蛇语
数据结构·c++
凯子坚持 c5 小时前
String的长度有限,而我对你的思念却无限延伸
c++
何曾参静谧5 小时前
「C/C++」C++20 之 #include<ranges> 范围
c语言·c++·c++20
挨代码6 小时前
C++ —— 常见的初始化
c++
酒鬼猿6 小时前
C++初阶(七)--类和对象(4)
开发语言·c++