当前位置: 首页 > news >正文

C++ 中的浅拷贝与深拷贝:概念、规则、示例与最佳实践(笔记)

一文吃透:什么是浅拷贝?什么是深拷贝?默认拷贝做了什么?哪些类型需要你亲自写“深拷贝”?Rule of Three/Five/Zerocopy-swap、移动语义、智能指针与标准容器的真实拷贝语义是什么?

目录

  • 1. 两个术语的最短定义

  • 2. C++ 里“拷贝”发生在何处

  • 3. “默认拷贝”(编译器生成)的真实行为

  • 4. 浅拷贝的典型灾难:双重释放与悬垂指针

  • 5. 正确实现深拷贝:Rule of Three/Five、copy-swap

  • 6. 移动语义与深/浅拷贝的关系

  • 7. 标准库类型的拷贝语义(必须搞清)

  • 8. 何时选深拷贝,何时避免拷贝

  • 9. 高频面试/作业陷阱清单

  • 10. 完整示例:从踩坑到优雅

  • 11. 总结与实践建议


1. 两个术语的最短定义

  • 浅拷贝(Shallow Copy):只复制“指针值/句柄”等外壳,不复制其指向/管理的底层资源。两个对象共享同一块资源。

  • 深拷贝(Deep Copy):复制对象自身的同时,也复制它所管理的底层资源(开辟新存储、逐元素拷贝等),两个对象彼此独立。

判断标准不是“代码写得多与少”,而是拷贝后两个对象是否共享底层资源

2. C++ 里“拷贝”发生在何处

以下场景会触发拷贝构造拷贝赋值

  • 以值传参/以值返回(未被移动/优化省略时)

  • 用一个对象初始化另一个对象:T b = a; / T b(a);

  • 赋值:b = a;

  • 容器在扩容、插入时复制元素(若没有移动可用)

  • 标准算法需要复制元素时

此外还有移动构造/移动赋值(C++11+),下文详解。

3. “默认拷贝”(编译器生成)的真实行为

当你没有显式定义拷贝构造/拷贝赋值时,编译器会做成员逐一拷贝(memberwise copy)

  • 内置类型int/double/struct POD…)值拷贝

  • 类成员:调用它们各自的拷贝构造/赋值

  • 指针成员:只是拷贝指针的值,不复制指针指向的数据(这就是浅拷贝)

这意味着:一旦你的类自己“管理资源”(裸指针指向堆内存、文件句柄、套接字、GPU 缓冲等),默认拷贝往往是危险的浅拷贝。

4. 浅拷贝的典型灾难:双重释放与悬垂指针

下面是反例

#include <cstring>
#include <iostream>class BufferBad {
public:BufferBad(size_t n): size_(n), data_(new char[n]) { std::memset(data_, 0, n); }// ❌ 没有自定义拷贝构造/拷贝赋值,编译器会生成“浅拷贝”~BufferBad() { delete[] data_; }void fill(char c) { std::memset(data_, c, size_); }char* data() { return data_; }private:size_t size_;char*  data_;
};int main() {BufferBad a(8);a.fill('A');BufferBad b = a; // ⚠️ 浅拷贝:b.data_ 和 a.data_ 指向同一块内存// 程序结束时 ~BufferBad() 被调用两次 → 同一指针 delete[] 两次 → 未定义行为(炸)
}

问题本质:浅拷贝导致两个对象共享一份资源,而析构会释放两次;如果其中一个对象释放后另一个继续访问,这又变成悬垂指针

5. 正确实现深拷贝:Rule of Three/Five、copy-swap

5.1 Rule of Three / Five / Zero

  • Rule of Three:如果类自己管理资源,你通常要同时自定义:

    1. 析构函数(~T()

    2. 拷贝构造(T(const T&)

    3. 拷贝赋值(T& operator=(const T&)

  • Rule of Five(C++11+):在上述三者基础上加上:
    4. 移动构造(T(T&&) noexcept
    5. 移动赋值(T& operator=(T&&) noexcept

  • Rule of Zero:如果你把资源交给标准库类型(如 std::vector/std::string/智能指针)管理,那么不需要自己写上面这些特殊成员函数(默认生成就好)。

5.2 手写一个“深拷贝 + 移动”的安全类

#include <cstring>
#include <utility>
#include <stdexcept>class Buffer {
public:explicit Buffer(size_t n = 0) : size_(n), data_(n ? new char[n] : nullptr) {if (data_) std::memset(data_, 0, n);}// 深拷贝:拷贝构造Buffer(const Buffer& other) : size_(other.size_), data_(other.size_ ? new char[other.size_] : nullptr) {if (data_) std::memcpy(data_, other.data_, size_);}// 深拷贝:拷贝赋值(强烈推荐 copy-swap 写法)Buffer& operator=(Buffer other) {  // ← 这里按值传参,构造一个临时副本(调用拷贝或移动构造)swap(other);                   // 与临时副本交换 → 强异常安全 & 自然处理自赋值return *this;                  // 临时副本析构时释放旧资源}// 移动构造(转移所有权,不分配/拷贝数据)Buffer(Buffer&& other) noexcept : size_(other.size_), data_(other.data_) {other.size_ = 0;other.data_ = nullptr;}// 移动赋值(同理,用 swap 即可)Buffer& operator=(Buffer&& other) noexcept {swap(other);return *this;}~Buffer() { delete[] data_; }void fill(char c) {if (!data_) throw std::runtime_error("empty buffer");std::memset(data_, c, size_);}void swap(Buffer& rhs) noexcept {std::swap(size_, rhs.size_);std::swap(data_, rhs.data_);}size_t size() const noexcept { return size_; }const char* data() const noexcept { return data_; }private:size_t size_;char*  data_;
};

要点

  • 深拷贝:在拷贝构造中重新 new,并 memcpy 原数据。

  • 拷贝赋值:用 copy-swap 惯用法 实现,天然处理异常安全与自赋值,代码简洁。

  • 移动语义:移动构造/赋值只做指针偷取与置空,避免分配与拷贝。

6. 移动语义与深/浅拷贝的关系

  • 移动(move)既不是浅拷贝也不是深拷贝:它是**“转移资源所有权”**的第三种策略。

  • 有了移动语义,容器扩容、返回局部对象等场景可避免昂贵的深拷贝。

  • 当右值可用且你支持 T(T&&)/operator=(T&&) 时,标准库会优先移动而非拷贝。

7. 标准库类型的拷贝语义(必须搞清)

  • std::stringstd::vector<T>std::arraystd::map容器/字符串深拷贝“自己的存储”(逐元素拷贝),但元素本身如何拷贝取决于元素类型的拷贝语义。

    • std::vector<int>:拷贝就是复制每个 int

    • std::vector<char*>:只会浅拷贝指针值(指针指向的外部内存不会被复制)。

  • std::unique_ptr<T>独占所有权不可拷贝(拷贝会编译失败),可移动。这能从语法上阻止“浅拷贝共享同一裸指针”的灾难。

  • std::shared_ptr<T>共享所有权,拷贝会增加引用计数——这不是深拷贝数据本身,而是浅拷贝指针 + 引用计数管理。多个 shared_ptr 指向同一对象,最后一个销毁时才释放。

  • std::span<T>非拥有视图,拷贝只是复制“视图”,绝不复制底层数据。

误区澄清:“容器拷贝是深拷贝”这句话要加前提——它只对容器自身所拥有的存储成立;元素如果是指针/句柄,容器不会替你复制指针所指向的资源

8. 何时选深拷贝,何时避免拷贝

  • 你的类自己拥有并负责释放底层资源(裸指针/句柄/文件/GPU 内存):要么实现深拷贝 + 移动,要么禁止拷贝(=delete)只允许移动

  • 性能敏感并且“复制”成本高:优先提供移动;必要时设计共享语义shared_ptr/引用计数/写时复制 COW)。

  • 能用 Rule of Zero 就不要手写特殊成员:把资源交给 std::vectorstd::stringstd::unique_ptr 等来管理。

9. 高频面试/作业陷阱清单

  1. 默认拷贝 + 裸指针 → 双重释放/悬垂指针(经典坑)。

  2. 只写了析构或拷贝构造,却忘了拷贝赋值/移动成员 → 违反 Rule of Three/Five。

  3. shared_ptr 拷贝是共享所有权,不是深拷贝底层对象(很多人误会)。

  4. 容器里放指针:容器拷贝只是浅拷贝这些指针;要深拷贝请放对象本体或自定义克隆逻辑。

  5. 自赋值未处理:x = x; 可能崩;用 copy-swap 最省心。

  6. 异常安全:先构造副本,再交换,保证强异常安全。

  7. 移动操作缺 noexcept:容器优化可能退化为拷贝,性能直线下降。

10. 完整示例:从踩坑到优雅

10.1 错误版:浅拷贝导致双删

class ImageBad {
public:explicit ImageBad(size_t n) : n_(n), pixels_(new uint8_t[n]) {}~ImageBad() { delete[] pixels_; }// ❌ 未定义拷贝语义 → 默认浅拷贝(拷贝指针值)
private:size_t   n_;uint8_t* pixels_;
};

10.2 正确版 A:深拷贝 + 移动 + copy-swap

#include <cstring>
#include <utility>class Image {
public:explicit Image(size_t n = 0) : n_(n), pixels_(n ? new uint8_t[n] : nullptr) {}Image(const Image& rhs) : n_(rhs.n_), pixels_(rhs.n_ ? new uint8_t[rhs.n_] : nullptr) {if (pixels_) std::memcpy(pixels_, rhs.pixels_, n_);}Image& operator=(Image rhs) { // 拷贝或移动进来swap(rhs);                // 与临时对象交换return *this;             // rhs 析构释放旧资源}Image(Image&& rhs) noexcept : n_(rhs.n_), pixels_(rhs.pixels_) {rhs.n_ = 0; rhs.pixels_ = nullptr;}Image& operator=(Image&& rhs) noexcept {swap(rhs);return *this;}~Image() { delete[] pixels_; }void swap(Image& other) noexcept {std::swap(n_, other.n_);std::swap(pixels_, other.pixels_);}private:size_t   n_   = 0;uint8_t* pixels_ = nullptr;
};

10.3 正确版 B:Rule of Zero(推荐)

根本不自己管裸指针,资源交给标准库:

#include <vector>
#include <cstdint>class ImageSafe {
public:explicit ImageSafe(size_t n = 0) : pixels_(n) {}   // vector 自管内存// 无需写析构/拷贝/赋值/移动:默认全 OK(Rule of Zero)private:std::vector<uint8_t> pixels_;
};

优点:代码最短、异常安全、天然深拷贝元素、自动支持移动、性能友好。


11. 总结与实践建议

  • 判断标准:拷贝后是否共享底层资源?共享 = 浅,不共享 = 深。

  • 默认拷贝是成员逐一拷贝:一旦自己管理资源,默认拷贝几乎必错。

  • 遵循 Rule of Three/Five/Zero

    • 能 Rule of Zero 就 Rule of Zero(容器/智能指针)。

    • 必须手写就三/五件套 + copy-swap + noexcept move

  • 移动语义优先:让昂贵对象在容器/返回值场景下移动而非拷贝。

  • 别把裸指针放进容器,放对象或智能指针(并理解其所有权语义)。

  • shared_ptr 拷贝不是深拷贝,而是共享所有权;如果需要克隆底层对象,请自定义 clone() 或自定义拷贝逻辑。

附:一个小型演示 main(可直接测试)

#include <iostream>
#include <vector>
#include <string>// 放上面定义的 Buffer 或 Image 类...int main() {// 1) 深拷贝验证Buffer a(4);a.fill('X');Buffer b = a;      // 调用拷贝构造:深拷贝Buffer c; c = a;   // 调用拷贝赋值:深拷贝(copy-swap)// 2) 移动验证Buffer d = std::move(a); // a 资源被转移,a 置空Buffer e; e = std::move(b);// 3) Rule of Zero:vector 深拷贝元素std::vector<int> v1 = {1,2,3};std::vector<int> v2 = v1; // 拷贝了每个 int,互不影响v1[0] = 99;std::cout << v1[0] << " " << v2[0] << "\n"; // 99 1// 4) 指针元素只是浅拷贝指针值(演示语义,勿在生产中这样做)std::vector<const char*> p1 = {"abc", "def"};std::vector<const char*> p2 = p1; // 只是拷贝了指针std::cout << p1[0] << " " << p2[0] << "\n"; // 都指向同一字符串常量return 0;
}

最后一句话

在现代 C++ 中,能不用裸指针就不用,能交给 std::vector / std::string / 智能指针 管理就交给它们。这样你几乎不再需要自己写“深拷贝”,自然获得正确、简洁且高性能的代码。

http://www.dtcms.com/a/546684.html

相关文章:

  • 建站seo课程wordpress 小说 采集器
  • 淘宝平台 API 接口接入文档和参数说明调用示例
  • 网络建设网站电商网站开发的引言
  • 做网站商城需要申请商标吗个人网站可以做淘客
  • 个人网站 前置审批中国最顶尖的平面设计公司
  • 海淀区手机网站设计服务6微信商城平台
  • 深圳专门做写字楼的网站大气物流网站模块
  • 网站工商标识做网站的公司负责wordpress连通公众号
  • ggsci 4.0.0来了,新增400+套配色!
  • PostIn零基础学习,安装与配置
  • 上海做建材上什么网站好南通网站制作哪个好
  • 做湲兔费网站视颍中国建设银行总部网站
  • Taro多端适配技术解析
  • 学做网站 空间 域名网站建设的一般步骤包含哪些
  • 网站开发的目的 实习报告住房与建设局网站
  • 专门做超市dm网站路飞 wordpress
  • 亚马逊卖家做自己网站北京棋森建设有限公司网站
  • 律师推广网站排名iis里如何装php网站
  • 类似17做网店的网站wordpress插件关闭更新
  • 【人工智能数学基础】如何理解方差与协方差?
  • 那个网站可以做学历认证女装网站模板
  • Modbus Simulator
  • 网页设计网站期末作业石河子做网站
  • 温岭做网站的公司有哪些网页设计尺寸怎么测量
  • 怎么做网络乞丐网站在putty上怎样安装wordpress
  • 无锡网站建设818gx家装室内设计
  • 网站建设需要洽谈什么在centos上搭建wordpress
  • 域名代理商网站西安优秀的集团门户网站建设公司
  • 宠物出售的网站怎么做网站扁平化设计风格
  • MySQL 复合查询全解析:从单表到多表的实战进阶