移动语义

左值与右值

概念

左值(lvalue)是指那些有明确内存位置(有标识符)的表达式,可以出现在赋值语句的左侧。

特点:

例如:

右值(rvalue)是指那些临时的、无法获取地址的表达式,只能出现在赋值语句的右侧。

特点:

例如:

引用

左值引用 (lvalue reference)使用&符号声明,只能绑定到左值:

例外:常量左值引用可以绑定到右值:

右值引用 (rvalue reference)(C++11引入)使用&&符号声明,只能绑定到右值:

右值引用的主要用途是实现移动语义,避免不必要的拷贝:

完美转发 (perfect forwarding)

使用std::forward保持参数的值类别(左值/右值):

值类别的细分

C++11进一步细分了表达式的值类别:

泛左值 (glvalue)

右值 (rvalue)

判断左值和右值的方法

  1. 能否取地址:如果可以对表达式使用&运算符,则它是左值;否则是右值

  2. 能否赋值:如果表达式可以出现在赋值语句左侧,则它是左值;否则是右值

  3. 有无名称:有名称的通常是左值,无名称的通常是右值

常见的左值和右值

左值示例

右值示例

移动构造函数

概念

移动构造函数是C++11引入的一种特殊构造函数,用于从一个临时对象(右值)高效地"窃取"资源,而不是进行昂贵的深拷贝操作。它的主要目的是优化性能,特别是对于包含动态分配资源(如堆内存、文件句柄等)的类。

移动构造函数的声明语法如下:

关键特点:

实现示例

简单字符串类的移动构造函数

移动构造函数的工作原理

  1. 资源转移而非复制:移动构造函数不创建新资源,而是转移现有资源的所有权

  2. 指针窃取:通常通过浅拷贝指针来实现资源转移

  3. 源对象重置:将源对象的指针设为nullptr,防止资源被多次释放

  4. 零开销抽象:理想情况下,移动操作只涉及几个指针赋值,几乎没有性能开销

何时调用移动构造函数

移动构造函数在以下情况会被调用:

  1. 使用std::move显式转换

  2. 返回值优化失败时的函数返回

  3. 右值参数传递

与拷贝构造函数的比较

特性拷贝构造函数移动构造函数
参数类型const T&T&&
源对象保持不变被修改(资源被窃取)
资源处理深拷贝(分配新资源)转移所有权(无新分配)
性能较慢(需要内存分配和复制)快(仅指针操作)
异常安全可能抛出异常通常标记为noexcept

当返回的对象其本身生命周期即将结束,就不再调用拷贝构造函数,而是调用移动构造函数。

你问得非常好!这确实是理解移动语义的关键点。让我详细解释不同情况下移动构造的行为。

移动构造的资源转移分类

有动态资源的情况

"夺取指针":

没有指针的情况

关键:如果对象没有动态资源,移动构造通常退化为拷贝构造!

编译器优化的情况

对于没有动态资源的简单类型,编译器通常会:

有意义的移动构造

移动构造真正有意义的情况:

标准库容器的移动

总结

移动构造的资源转移取决于对象的内部结构:

对象类型移动行为性能提升
有动态资源(指针、容器等)真正的资源转移显著
只有基本类型成员退化为拷贝无或微小
包含可移动的成员递归调用成员的移动取决于成员

关键点:

  1. 如果没有指针等动态资源,移动构造通常等同于拷贝构造

  2. 移动的意义在于避免昂贵的深拷贝操作

  3. 对于简单的值类型,移动和拷贝的成本相同

  4. 真正的性能提升来自于避免动态内存分配和大量数据复制

如果原对象没有指针或其他动态资源,移动构造基本上没有资源可以转移,性能提升也就微乎其微。

应用示例

1. 智能指针的移动构造

2. 容器类的移动构造

移动赋值函数

概念

移动赋值函数(Move Assignment Operator)是C++11引入的一种特殊赋值运算符,用于实现资源的高效转移而非复制。它的主要目的是避免不必要的深拷贝操作,通过"窃取"右值对象的资源来提高性能。

移动赋值函数的典型声明如下:

关键特点:

移动赋值函数的基本实现步骤:

  1. 释放当前对象拥有的资源

  2. 从参数对象"窃取"资源(转移所有权)

  3. 将参数对象置于有效但未指定的状态(通常是空/null状态)

  4. 返回当前对象的引用

示例

字符串类示例

智能指针示例

与复制赋值的区别

特性移动赋值复制赋值
参数类型右值引用 (T&&)常量左值引用 (const T&)
资源处理转移所有权创建资源副本
源对象状态被修改(通常置空)保持不变
性能高效(避免深拷贝)可能较慢(需要深拷贝)
适用场景临时对象、即将销毁的对象需要保留源对象的情况

移动赋值函数在以下情况下会被调用:

  1. 显式使用std::move将左值转换为右值引用:

  2. 赋值临时对象(右值):

  3. 在标准库容器中,当元素需要移动而非复制时

std::move 函数

基本概念

std::move 是C++11引入的一个标准库函数,定义在 <utility> 头文件中。它的主要作用是将一个左值(lvalue)转换为右值引用(rvalue reference),从而使移动语义得以实现。

函数原型:

在C++14及以后的版本中,实现更简洁:

std::move 本质上是一个类型转换函数,它并不会真正"移动"任何东西。它的核心功能是:

  1. 接受一个通用引用参数 T&&

  2. 使用 std::remove_reference 移除引用

  3. 将参数转换为右值引用类型

简单来说,std::move 告诉编译器:"我不再需要这个对象的值,你可以自由地移动它的资源。"

使用场景

1. 实现移动构造函数和移动赋值运算符

2. 转移对象所有权

3. 在容器操作中提高效率

4. 强制使用移动语义

Note

  1. 移动后的对象状态:被移动后的对象仍然有效,但处于未指定(但有效)的状态。通常,标准库实现会将被移动的对象置于一个"空"状态。

  2. 不要对常量对象使用 std::move:常量对象不能被移动,因为移动操作需要修改源对象。

  3. 返回值优化 (RVO):在函数返回局部对象时,通常不需要使用 std::move,因为编译器会应用返回值优化。

  4. 不要在 return 语句中对函数参数使用 std::move

  5. std::forward 的区别

    • std::move 无条件将参数转换为右值引用

    • std::forward 有条件地转换,只在输入是右值时才转换为右值引用

实际应用示例

交换两个对象

在容器中重用内存

实现移动语义的工厂函数

右值引用本身是左值还是右值,取决于是否有名字,有名字就是左值,没名字就是右值。

资源管理

RAII技术

RAII (Resource Acquisition Is Initialization,资源获取即初始化)是C++中一种重要的编程技术和设计模式。

RAII的基本思想是:将资源的生命周期与对象的生命周期绑定。当对象创建时获取资源,当对象销毁时自动释放资源。

RAII技术,具备以下基本特征:

我们可以实现以下的一个类模板,模拟RAII的思想

如下,raii不是一个指针,而是一个对象,但是它的使用已经和指针完全一致了。这个对象可以托管堆上的Point对象,而且不用考虑delete。

RAII技术的本质:利用栈对象的生命周期管理资源,因为栈对象在离开作用域时候,会执行析构函数。

智能指针

智能指针是C++中用于自动管理动态内存的重要工具,它们通过RAII技术确保内存的正确分配和释放,有效防止内存泄漏和悬空指针问题。

std::unique_ptr

unique_ptr表示对资源的独占所有权,不能被复制,只能被移动。

特点1:不允许复制或者赋值

具备对象语义。

特点2:独享所有权的智能指针

特点3:作为容器元素

要利用移动语义的特点,可以直接传递右值属性的unique_ptr作为容器的元素。如果传入左值形态的unique_ptr,会进行复制操作,而unique_ptr是不能复制的。

构建右值的方式有

1、std::move的方式

2、可以直接使用unique_ptr的构造函数,创建匿名对象(临时对象),构建右值。

std::shared_ptr

shared_ptr允许多个指针共享同一个对象,使用引用计数管理对象生命周期。

特征1:共享所有权的智能指针

可以使用引用计数记录对象的个数。

特征2:可以进行复制或者赋值

表明具备值语义。

特征3:也可以作为容器的元素

作为容器元素的时候,即可以传递左值,也可以传递右值。(区别于unique_ptr只能传右值)

特征4:也具备移动语义

表明也有移动构造函数与移动赋值函数。

shared_ptr的循环引用

shared_ptr的循环引用是智能指针使用中的一个重要问题,它会导致内存泄漏,因为相互引用的对象永远不会被销毁。

循环引用的产生

问题分析

  1. Parent对象持有shared_ptr<Child>,引用计数为1

  2. Child对象持有shared_ptr<Parent>,引用计数为1

  3. 当局部变量parentchild离开作用域时:

    • parent的引用计数从2变为1(Child仍持有引用)

    • child的引用计数从2变为1(Parent仍持有引用)

  4. 两个对象都不会被销毁,造成内存泄漏

检测循环引用的工具

解决方案:使用weak_ptr

std::weak_ptr

weak_ptr提供对shared_ptr管理对象的弱引用,不影响引用计数,主要用于解决循环引用问题。

weak_ptr知道所托管的对象是否还存活,如果存活,必须要提升为shared_ptr才能对资源进行访问,不能直接访问。

判断关联的空间是否还在

1.可以直接使用use_count函数

如果use_count的返回值大于0,表明关联的空间还在

2.将weak_ptr提升为shared_ptr

这种赋值操作可以让wp也能够托管这片空间,但是它作为一个weak_ptr仍不能够去管理,甚至连访问都不允许(weak_ptr不支持直接解引用)

想要真正地去进行管理需要使用lock函数将weak_ptr提升为shared_ptr

如果托管的资源没有被销毁,就可以成功提升为shared_ptr,否则就会返回一个空的shared_ptr(空指针)

查看lock函数的说明

3.可以使用expired函数

该函数返回true等价于use_count() == 0.

删除器

删除器是C++智能指针的一个重要组成部分,用于定义当智能指针销毁时如何释放所管理的资源。

删除器(Deleter)是一个可调用对象(函数、函数对象、lambda表达式等),用于指定智能指针在销毁时如何释放资源。不同的智能指针对删除器的支持程度不同:

unique_ptr的删除器

std::unique_ptr的默认删除器是std::default_delete,对单个对象使用delete,对数组使用delete[]

自定义删除器

1. 函数指针作为删除器

2. 函数对象作为删除器

3. Lambda表达式作为删除器

shared_ptr的删除器

std::shared_ptr的删除器更加灵活,可以在运行时指定,且不影响类型。

管理非内存资源

删除器的高级用法

1. 空删除器(不删除)

2. 条件删除器

3. 统计删除器

make_unique和make_shared与删除器

make_unique的限制

make_shared的限制

Caution

智能指针被误用,原因都是将一个原生裸指针交给了不同的智能指针进行托管,而造成尝试对一个对象销毁两次