好好讲讲移动构造 移动赋值

移动拷贝(移动构造)与移动赋值


一、先搞懂:移动语义到底是什么?

C++11 引入的移动语义(Move Semantics),核心是 **「资源所有权转移」**,而不是传统拷贝的「资源复制」。它解决两个完全不同的核心问题:

  1. 性能优化:避免临时对象、大型对象的无谓深拷贝,减少内存分配和数据复制的开销。
  2. 独占资源设计 :实现「只可移动、不可拷贝」的类型,比如文件句柄、线程、std::unique_ptr 这类天生不能被复制的资源,只能通过转移所有权来传递。

而移动拷贝(移动构造)和移动赋值,就是实现移动语义的两个核心接口。


二、先铺垫:移动语义的基础 ------ 右值引用

移动语义的语法基础是右值引用 &&,先搞懂三个概念:

类型 定义 例子
左值 有名字、能取地址的对象 int a = 10; 中的 a
右值 临时对象、即将被销毁的对象,不能取地址 Person("temp", 20)(临时对象)、函数返回的局部对象
右值引用 && 专门绑定到右值的引用,用来触发移动语义 Person&& r = Person("temp", 20);

补充两个关键误区:

  • std::move 不是 "移动对象",只是把左值强制转换成右值引用,本身不修改对象,真正的资源转移是在移动构造 / 移动赋值里实现的。
  • std::move 后的对象,处于「有效但未指定」的状态,只能被赋值或析构,不能再依赖它的原始值(除非你手动置空了)。

三、移动拷贝(移动构造函数)详解

1. 定义与语法

移动构造函数 :用一个源对象(通常是右值)初始化一个新对象,过程是直接接管源对象的资源,源对象被置为合法的空状态,没有深拷贝。

语法原型:

cpp 复制代码
class Person {
public:
    // 移动构造函数:参数是右值引用&&,通常加noexcept
    Person(Person &&other) noexcept;
};

2. 触发时机

  • 用临时对象初始化新对象:Person p = Person("temp", 20);

  • 函数返回局部对象(RVO 优化失效时):

    cpp 复制代码
    Person create() { Person p("Alice", 20); return p; }
    Person p = create(); // 触发移动构造
  • std::move 强制左值触发:Person p2 = std::move(p1);

3. 核心实现逻辑(带堆资源的例子)

以带 char* name 成员的类为例,移动构造的关键是「零拷贝接管资源 + 源对象置空」:

cpp 复制代码
Person(Person &&other) noexcept 
    : name(other.name), age(other.age) // 直接接管源对象的指针和数据
{
    // 必须把源对象置空!防止析构时重复释放
    other.name = nullptr; 
    other.age = 0;
}

对比拷贝构造:拷贝构造会 new 一块新内存,把 other.name 的内容复制过去;而移动构造直接拿指针,全程没有内存分配和数据复制,开销几乎为 0。


四、移动赋值运算符详解

1. 定义与语法

移动赋值运算符 :把一个源对象(通常是右值)的资源转移给一个已经存在的对象,同样是转移所有权,源对象置空。

语法原型:

cpp 复制代码
class Person {
public:
    // 移动赋值运算符:返回*this支持链式赋值,参数是右值引用&&,加noexcept
    Person& operator=(Person &&other) noexcept;
};

2. 触发时机

  • 用临时对象赋值给已有对象:p = Person("temp", 20);
  • std::move 强制左值赋值:p2 = std::move(p1);

3. 核心实现逻辑(关键步骤)

移动赋值比移动构造多一步:释放目标对象自身的旧资源,防止内存泄漏。完整实现:

cpp 复制代码
Person& operator=(Person &&other) noexcept {
    // 1. 自移动判断:防止p = std::move(p); 这种自移动操作
    if (this != &other) {
        // 2. 释放目标对象自身的旧资源(必须!否则会内存泄漏)
        delete[] name;
        // 3. 接管源对象的资源
        name = other.name;
        age = other.age;
        // 4. 把源对象置空,防止析构时重复释放
        other.name = nullptr;
        other.age = 0;
    }
    // 5. 返回*this,支持链式赋值:p1 = p2 = std::move(p3);
    return *this;
}

五、移动语义 vs 拷贝语义:核心区别

用一张表把两者的本质差异讲透:

维度 拷贝构造 / 拷贝赋值 移动构造 / 移动赋值
核心行为 复制资源,创建独立副本 转移资源所有权,不复制
源对象状态 不变,仍然持有资源 被置空,不再持有资源
性能开销 高(深拷贝需要内存分配、复制) 极低(仅指针赋值,零拷贝)
适用对象 可共享所有权的对象 独占所有权 / 临时对象
资源管理 多个对象持有同一份资源的副本 同一时间只有一个对象持有资源
典型场景 shared_ptr、普通值对象 unique_ptrstd::thread、临时对象

六、面试必考点:关键细节

1. 为什么要加 noexcept

  • 标准库容器(比如 vector)扩容时,会优先使用noexcept 的移动构造函数,来保证异常安全。
  • 如果移动构造没有 noexcept,容器扩容时会直接「降级」使用拷贝构造,移动语义直接失效,优化白写。
  • 原因:如果移动过程中抛出异常,容器无法安全回滚,所以必须用 noexcept 保证移动构造不会抛出异常。

2. 编译器默认生成规则

C++11 及以后,编译器会默认生成移动构造和移动赋值,但有严格前提:

  • 没有手动定义拷贝构造、拷贝赋值、析构函数中的任何一个;
  • 类的所有非静态成员和基类都支持移动语义。

只要你写了拷贝相关的函数,编译器就不会默认生成移动版本,需要自己手动实现。

3. 自移动判断的必要性

移动赋值里的 if (this != &other),是为了防止 p = std::move(p); 这种自移动操作:

  • 如果没有判断,会先 delete[] name(释放自己的资源),再接管 other.name(其实就是自己的 name),导致访问已释放的内存,直接崩溃。

4. 源对象置空的必要性

如果不把源对象的指针置空,源对象析构时会 delete[] name,而这个指针已经被新对象接管了,会导致double free ,程序崩溃。置空后,源对象析构时 delete nullptr 是安全的。


七、两种核心使用场景

场景 1:性能优化

对于大型可拷贝对象,比如 vector<string>、自定义的大型类,临时对象的深拷贝开销极大,移动语义可以直接转移资源,不用复制数据。比如:

cpp 复制代码
// 函数返回大型vector,移动语义避免了拷贝
vector<int> func() {
    vector<int> v(1000000, 0);
    return v; // 触发移动构造,直接转移vector的底层数组
}

场景 2:独占资源设计

有些资源天生不能被复制,只能被转移所有权,这类类会禁用拷贝构造 / 拷贝赋值(=delete),只开放移动语义,实现「只可移动、不可拷贝」。典型例子:

  • std::unique_ptr:独占智能指针,禁用拷贝,只能移动,保证资源只有一个持有者。
  • std::thread:线程只能被一个对象持有,不能复制,只能移动转移线程所有权。
  • 文件句柄、socket、管道:如果复制了,两个对象析构时会重复关闭,导致崩溃,所以只能移动。

这类场景下,移动不是「优化版的拷贝」,而是对象传递的唯一合法方式


八、对比单例模式

单例模式和独占资源类的区别:

  • 单例:既禁用拷贝,也禁用移动,确保全局只有一个实例,连所有权转移都不允许。
  • 独占资源类(如 unique_ptr):禁用拷贝,允许移动,允许所有权转移,但不允许多个持有者。

九、完整可运行示例代码

下面是一个带堆资源的类,完整实现了拷贝、移动、析构,你可以跑一下,直观看到每个函数的调用时机:

cpp 复制代码
#include <iostream>
#include <utility>
#include <cstring>

class Person {
private:
    char* name;
    int age;

public:
    // 普通构造
    Person(const char* name, int age) 
        : age(age) {
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
        std::cout << "普通构造函数调用" << std::endl;
    }

    // 拷贝构造(深拷贝)
    Person(const Person& other) {
        age = other.age;
        name = new char[strlen(other.name) + 1];
        strcpy(name, other.name);
        std::cout << "拷贝构造函数调用" << std::endl;
    }

    // 拷贝赋值(深拷贝)
    Person& operator=(const Person& other) {
        if (this != &other) {
            delete[] name;
            age = other.age;
            name = new char[strlen(other.name) + 1];
            strcpy(name, other.name);
        }
        std::cout << "拷贝赋值运算符调用" << std::endl;
        return *this;
    }

    // 移动构造
    Person(Person&& other) noexcept 
        : name(other.name), age(other.age) {
        other.name = nullptr;
        other.age = 0;
        std::cout << "移动构造函数调用" << std::endl;
    }

    // 移动赋值
    Person& operator=(Person&& other) noexcept {
        if (this != &other) {
            delete[] name;
            name = other.name;
            age = other.age;
            other.name = nullptr;
            other.age = 0;
        }
        std::cout << "移动赋值运算符调用" << std::endl;
        return *this;
    }

    // 析构
    ~Person() {
        delete[] name;
        std::cout << "析构函数调用" << std::endl;
    }

    void print() const {
        if (name) {
            std::cout << "Name: " << name << ", Age: " << age << std::endl;
        } else {
            std::cout << "对象已被移动,资源为空" << std::endl;
        }
    }
};

// 测试函数:返回临时对象
Person createPerson() {
    Person p("Alice", 20);
    return p;
}

int main() {
    std::cout << "=== 测试1:临时对象初始化,触发移动构造 ===" << std::endl;
    Person p1 = createPerson();
    p1.print();

    std::cout << "\n=== 测试2:临时对象赋值,触发移动赋值 ===" << std::endl;
    Person p2("Bob", 30);
    p2 = Person("Temp", 18);
    p2.print();

    std::cout << "\n=== 测试3:std::move强制触发移动构造 ===" << std::endl;
    Person p3 = std::move(p1);
    p3.print();
    p1.print(); // p1被移动后,资源为空

    std::cout << "\n=== 测试4:std::move强制触发移动赋值 ===" << std::endl;
    Person p4("Charlie", 40);
    p4 = std::move(p3);
    p4.print();
    p3.print(); // p3被移动后,资源为空

    return 0;
}

十、口述精简版

移动构造和移动赋值是 C++11 引入的移动语义的核心接口,基于右值引用实现,核心是资源所有权转移,而非复制。

  1. 移动构造:用源对象初始化新对象,直接接管源对象资源,源对象置空,无深拷贝;触发时机包括临时对象初始化、函数返回局部对象、std::move 强制左值。
  2. 移动赋值:把源对象资源转移给已存在的对象,需要先释放目标对象自身的旧资源,再接管源对象资源,源对象置空;触发时机包括临时对象赋值、std::move 强制左值赋值。
  3. 和拷贝的区别:拷贝是复制资源,创建独立副本;移动是转移所有权,源对象不再持有资源,开销极低。
  4. 核心用途:一是性能优化,避免大型对象、临时对象的无谓深拷贝;二是实现独占资源的传递,比如 std::unique_ptr、std::thread 这类只可移动不可拷贝的类型,保证资源不被重复持有。
  5. 关键细节:移动函数通常加 noexcept,否则标准库容器会降级使用拷贝;需要自移动判断和源对象置空,防止内存泄漏和 double free。
相关推荐
syker3 小时前
AIFerric深度学习框架:自研全栈AI基础设施的技术全景
开发语言·c++
xvhao20133 小时前
单源、多源最短路
数据结构·c++·算法·深度优先·动态规划·图论·图搜索算法
笑鸿的学习笔记5 小时前
qt-C++语法笔记之Qt Graphics View 框架中的类型辨析完全指南
c++·笔记·qt
山居秋暝LS5 小时前
安装C++版opencv和opencv_contrib
开发语言·c++·opencv
谭欣辰5 小时前
LCS(最长公共子序列)详解
开发语言·c++·算法
Cando学算法5 小时前
鸽笼原理(抽屉原理)
c++·算法·学习方法
郝学胜-神的一滴6 小时前
跨平台动态库与头文件:从原理到命名的深度解析
linux·c++·程序人生·unix·cmake
代码中介商6 小时前
C++ 仿函数(Functor)深度解析:从基础到应用
开发语言·c++
王老师青少年编程7 小时前
csp信奥赛C++高频考点专项训练之字符串 --【字符串基础】:[NOIP 2018 普及组] 标题统计
c++·字符串·csp·高频考点·信奥赛·专项训练·标题统计