进阶篇:从手写深拷贝到 std::string 与移动语义(Rule of Five)
2026/5/13 2:51:21 网站建设 项目流程

在上一篇《C++ 浅拷贝 vs 深拷贝:从 0 开始一步一步讲透(Student 示例·含判断方法)》里,我们已经明确了结论:

  • 默认拷贝(编译器生成)对资源类通常是浅拷贝
  • 资源类想安全,就要自己实现深拷贝(或禁止拷贝 / 使用标准库替代)

这篇进阶主要解决三个“工程里一定会遇到”的点:

  1. 为什么拷贝赋值要先 delete 再 new?(自赋值怎么处理?)
  2. 为什么std::string不会出浅拷贝问题?
  3. 移动语义到底解决什么?Rule of Five 是什么?

1)拷贝赋值为什么要先deletenew

先回顾拷贝赋值发生的场景:

Student s1("Tom", 18); Student s2("Jack", 20); s2 = s1; // s2 已经存在

1.1 如果你不先释放旧资源,会发生什么?

s2原本有一块堆内存保存 "Jack"。
如果你直接new一块新内存覆盖name

name = new char[...]; // 覆盖了旧指针

那么旧的那块内存就“丢失了地址”,再也释放不了 →内存泄漏

所以拷贝赋值正确顺序必须是:

先释放旧资源,再申请新资源,再复制内容。

1.2 为什么要做“自赋值保护”?

自赋值:

s1 = s1;

如果你不判断,代码会这样走:

  • delete[] name;把自己资源删了
  • 然后还想从other.name拷贝(其实就是自己已经被删掉的那块内存)
  • 结果就是未定义行为(可能崩,可能乱码)

因此标准写法必须带上:

if (this == &other) return *this;

2)深拷贝赋值的标准写法(可直接套用)

Student& operator=(const Student& other) { if (this == &other) return *this; delete[] name; // 1) 释放旧资源 age = other.age; name = new char[strlen(other.name) + 1]; // 2) 申请新资源 strcpy(name, other.name); // 3) 拷贝内容 return *this; }

记住:拷贝构造不用先 delete(因为对象刚出生没旧资源),
拷贝赋值必须先 delete(因为对象早就有资源了)。

3)为什么std::string不会出浅拷贝问题?

上一节我们用char*是为了把“资源管理”的坑暴露出来。
工程中你更应该写成这样:

#include <string> class Student { public: std::string name; int age; };

然后:

Student s1; s1.name = "Tom"; Student s2 = s1; // 拷贝构造 Student s3; s3 = s1; // 拷贝赋值

不会崩溃,原因很简单:

std::string自己就是一个资源管理类(RAII),它内部已经把:析构 / 拷贝 / 移动 都实现好了。

所以你拷贝Student时,std::string会自动做正确的事情:

  • 该深拷贝时深拷贝
  • 该移动时移动
  • 自动释放资源,不会 double free

结论:能用标准库容器/字符串,就别手写new/delete管字符串。

4)移动语义:它不是“更安全”,而是“更快”

深拷贝是安全的,但可能很贵:大字符串/大数组拷贝成本高。

移动语义解决的是:

避免复制大块数据,改为“转移资源所有权”。

4.1 直觉理解

  • 拷贝:给你“复印一份资料”
  • 移动:把“资料原件”直接交给你,原来的那份清空

因此移动通常:

  • 不分配新内存
  • 不复制大量数据
  • 性能很好

5)Rule of Three vs Rule of Five(一定要记住)

Rule of Three(三法则)

如果你需要自定义这三个之一,通常就要考虑另外两个:

  1. 析构函数~T()
  2. 拷贝构造T(const T&)
  3. 拷贝赋值T& operator=(const T&)

因为这意味着你在“手动管理资源”。

Rule of Five(五法则)

C++11 之后加入了移动语义,所以资源类通常还要考虑:

  1. 移动构造T(T&&)
  2. 移动赋值T& operator=(T&&)

实战经验:大多数时候你用std::string / vector / unique_ptr
就不需要自己写五件套,标准库已经替你做了。

6)工程建议:三种策略怎么选?

当你写一个“资源类”时,通常三种选择:

策略 A:实现深拷贝(像上一篇 Student 那样)

  • 适用:确实需要“复制一份独立资源”
  • 成本:代码多、容易写错

策略 B:禁止拷贝(资源独占)

比如文件句柄、锁、socket 通常不希望被复制:

Student(const Student&) = delete; Student& operator=(const Student&) = delete;

策略 C:用标准库类型替代裸指针(最推荐)

  • std::string替代char*
  • std::vector替代手写动态数组
  • std::unique_ptr管理独占资源

这是最不容易出错、工程最常用的路线。

7)一句话总结(进阶版)

  • 拷贝构造/拷贝赋值只是“什么时候拷贝”
  • 浅/深拷贝才是“怎么拷贝”
  • 资源类拷贝赋值必须:自赋值保护 + 先释放旧资源再申请新资源
  • 工程里优先用std::string / vector / 智能指针,把深拷贝坑交给标准库处理
  • 移动语义解决的是“性能”,让资源转移比复制更高效

C++ 对象拷贝 / 移动 速查表

项目触发时机对象状态是否分配新内存性能成本是否需要手写典型用途
拷贝构造函数T(const T&)创建新对象时用另一个对象初始化新对象刚出生深拷贝时会中等资源类建议手写T b = a;
拷贝赋值运算符operator=已有对象被另一个对象覆盖对象已存在深拷贝时会中等资源类必须手写b = a;
移动构造函数T(T&&)创建新对象时接收临时对象新对象刚出生通常不会很低可选(性能优化)T b = std::move(a);
移动赋值运算符operator=(T&&)已有对象接收临时对象对象已存在通常不会很低可选(性能优化)b = std::move(a);

“人话理解版”

类型本质行为类比
拷贝构造复制资源复印一份资料
拷贝赋值先丢旧资料,再复印新资料覆盖旧文件
移动构造转移资源所有权把原件直接交给你
移动赋值先丢旧资料,再接收原件把旧文件扔掉,接收原件

需要专业的网站建设服务?

联系我们获取免费的网站建设咨询和方案报价,让我们帮助您实现业务目标

立即咨询