跳到主要内容
极客日志极客日志面向AI+效率的开发者社区
首页博客GitHub 精选镜像工具UI配色美学隐私政策关于联系
搜索内容 / 工具 / 仓库 / 镜像...⌘K搜索
注册
博客列表
C++算法

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝辨析

综述由AI生成本文深入解析 C++ 中拷贝构造函数与赋值运算符的调用机制及差异。重点阐述了浅拷贝在指针成员场景下导致的重复释放与野指针风险,并通过 String 类与 MyArray 类示例演示了深拷贝的正确实现方式。内容涵盖三法则原则、自赋值处理、链式赋值支持及智能指针替代方案,旨在帮助开发者规避内存泄漏问题,掌握对象生命周期管理的核心技巧。

蜜桃汽水发布于 2026/3/15更新于 2026/6/1115 浏览
C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝辨析

C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝辨析

拷贝触发场景示意图

在 C++ 开发中,对象的生命周期管理是核心难点之一。很多内存泄漏和野指针问题,根源都在于对拷贝构造函数和赋值运算符的理解不够深入。今天我们就来聊聊这两个概念,以及它们背后的深拷贝与浅拷贝机制。

一、拷贝构造函数的概念与触发场景

拷贝构造函数是一种特殊的构造函数,它的作用是用一个已存在的对象来初始化一个新对象。它的参数必须是本类对象的常量引用(const 类名&)。

1. 基本语法

class ClassName {
public:
    // 普通构造函数
    ClassName(参数列表);
    
    // 拷贝构造函数
    ClassName(const ClassName& other);
};

这里有两个关键点需要注意:

  1. 参数类型:必须是常量引用。用 const 是为了防止修改实参,用引用则是为了避免无限递归调用拷贝构造函数本身。
  2. 默认行为:如果你没有手动定义,编译器会生成一个默认的拷贝构造函数,它执行的是简单的成员变量值拷贝。

2. 什么时候会被调用?

在实际开发中,以下三种情况会自动触发拷贝构造函数:

  1. 使用一个对象初始化另一个新对象。
  2. 函数参数为类对象(值传递)。
  3. 函数返回值为类对象(值传递)。
代码验证

我们通过一个简单的 Person 类来看看这些场景是如何触发的。

#include <iostream>
#include <string>
using namespace std;

class Person {
public:
    string name;
    int age;

    // 普通构造函数
    Person(string n, int a) : name(n), age(a) {
        cout <<  << endl;
    }

    
    ( Person& other) {
        ->name = other.name;
        ->age = other.age;
        cout <<  << endl;
    }
};


{
    cout <<  << p.name << endl;
}


{
    ;
     p;
}

{
    
    ;

    
    Person p2 = p1;

    
    (p1);

    
    Person p3 = ();

     ;
}
"普通构造函数被调用"
// 拷贝构造函数
Person
const
this
this
"拷贝构造函数被调用"
// 场景 2:函数参数为类对象(值传递)
void func(Person p)
"函数内对象姓名:"
// 场景 3:函数返回值为类对象(值传递)
Person getPerson()
Person p("王五", 30)
return
int main()
// 普通构造创建对象
Person p1("张三", 20)
// 场景 1:使用 p1 初始化 p2
// 场景 2:值传递传递对象
func
// 场景 3:值传递返回对象
getPerson
return
0

运行结果会显示多次'拷贝构造函数被调用',这印证了上述的触发条件。

二、浅拷贝与深拷贝的核心区别

理解了拷贝构造函数,接下来要解决一个更棘手的问题:当类中包含指针成员时,默认拷贝会发生什么?

1. 浅拷贝的隐患

默认的拷贝构造函数实现的是浅拷贝。它只是把成员变量的值原封不动地复制过去。如果成员变量是指针,那么两个对象的指针就会指向同一块堆内存。

问题演示
#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
    char* str; // 指针成员

public:
    // 普通构造函数:分配堆内存
    String(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
        cout << "普通构造函数:分配内存" << endl;
    }

    // 析构函数:释放堆内存
    ~String() {
        delete[] str;
        cout << "析构函数:释放内存" << endl;
    }

    void show() {
        cout << str << endl;
    }
};

int main() {
    String s1("Hello C++");
    // 浅拷贝:s2.str 与 s1.str 指向同一块内存
    String s2 = s1;
    
    s1.show();
    s2.show();
    return 0;
}

这段代码运行起来似乎没问题,但一旦程序结束,析构函数会被调用两次。由于 s1 和 s2 指向同一块内存,第二次 delete 会导致重复释放内存,引发程序崩溃。此外,修改其中一个对象的内容,另一个也会跟着变,破坏了对象的独立性。

2. 深拷贝的实现

深拷贝的核心思想是:为指针成员重新分配内存,并将原数据拷贝到新内存中。这样每个对象都有自己独立的内存空间。

重写拷贝构造函数
#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
    char* str;

public:
    String(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
        cout << "普通构造函数:分配内存" << endl;
    }

    // 手动实现深拷贝构造函数
    String(const String& other) {
        // 为新对象分配独立内存
        this->str = new char[strlen(other.str) + 1];
        // 拷贝数据
        strcpy(this->str, other.str);
        cout << "深拷贝构造函数:分配独立内存" << endl;
    }

    ~String() {
        delete[] str;
        cout << "析构函数:释放内存" << endl;
    }

    void show() {
        cout << str << endl;
    }

    // 提供修改字符串的方法,验证独立性
    void setStr(const char* s) {
        delete[] str;
        str = new char[strlen(s) + 1];
        strcpy(str, s);
    }
};

int main() {
    String s1("Hello C++");
    String s2 = s1; // 调用深拷贝构造函数

    cout << "修改前:" << endl;
    s1.show();
    s2.show();

    // 修改 s1 的内容
    s1.setStr("Hello Deep Copy");
    cout << "修改后:" << endl;
    s1.show();
    s2.show();

    return 0;
}

现在,修改 s1 不会影响 s2,且析构时各自释放各自的内存,安全得多。

三、赋值运算符重载与深拷贝

除了拷贝构造函数,赋值运算符(=)的默认行为也是浅拷贝。如果类中有指针成员,必须手动重载赋值运算符来实现深拷贝。

1. 核心原则

实现赋值运算符重载时,有三点必须注意:

  1. 处理自赋值:防止 a = a 导致内存提前释放。
  2. 释放原有内存:避免内存泄漏。
  3. 返回对象引用:支持链式赋值(如 a = b = c)。

2. 代码实现

#include <iostream>
#include <cstring>
using namespace std;

class String {
private:
    char* str;

public:
    String(const char* s = "") {
        str = new char[strlen(s) + 1];
        strcpy(str, s);
        cout << "普通构造函数:分配内存" << endl;
    }

    // 深拷贝构造函数
    String(const String& other) {
        this->str = new char[strlen(other.str) + 1];
        strcpy(this->str, other.str);
        cout << "深拷贝构造函数:分配独立内存" << endl;
    }

    // 重载赋值运算符,实现深拷贝
    String& operator=(const String& other) {
        // 1. 处理自赋值
        if (this == &other) {
            return *this;
        }
        // 2. 释放当前对象的原有内存
        delete[] this->str;
        // 3. 分配新内存并拷贝数据
        this->str = new char[strlen(other.str) + 1];
        strcpy(this->str, other.str);
        cout << "赋值运算符重载:深拷贝" << endl;
        // 4. 返回当前对象引用
        return *this;
    }

    ~String() {
        delete[] str;
        cout << "析构函数:释放内存" << endl;
    }

    void show() {
        cout << str << endl;
    }
};

int main() {
    String s1("Hello C++");
    String s2;
    // 调用赋值运算符重载
    s2 = s1;
    s1.show();
    s2.show();

    // 测试链式赋值
    String s3;
    s3 = s2 = s1;
    cout << "链式赋值后 s3:";
    s3.show();

    return 0;
}

四、拷贝构造函数与赋值运算符的区别

虽然两者都涉及对象复制,但本质不同:

特性拷贝构造函数赋值运算符
调用时机用已有对象创建新对象时调用给已存在的对象赋值时调用
参数要求必须是 const 类名&通常是 const 类名&
内存操作分配新内存,无原有内存需要释放需先释放当前对象原有内存
返回值无返回值(构造函数特性)必须返回 类名&,支持链式赋值
默认实现默认浅拷贝默认浅拷贝

五、实战案例:自定义数组类

为了巩固理解,我们设计一个支持动态扩容的 MyArray 类,完整实现深拷贝逻辑。

需求分析

  1. 成员变量:int* arr(存储数组数据)、int size(数组大小)。
  2. 核心功能:构造函数分配内存、深拷贝构造、赋值运算符重载、打印数组、析构函数释放内存。
  3. 目标:保证多个对象的数组数据独立。

完整代码

#include <iostream>
#include <cstring>
using namespace std;

class MyArray {
private:
    int* arr; // 动态数组指针
    int size; // 数组大小

public:
    // 构造函数:创建指定大小的数组
    MyArray(int s = 0) : size(s) {
        if (size > 0) {
            arr = new int[size];
            memset(arr, 0, sizeof(int) * size);
        } else {
            arr = nullptr;
        }
        cout << "构造函数:创建大小为 " << size << " 的数组" << endl;
    }

    // 深拷贝构造函数
    MyArray(const MyArray& other) {
        this->size = other.size;
        if (this->size > 0) {
            this->arr = new int[this->size];
            for (int i = 0; i < this->size; i++) {
                this->arr[i] = other.arr[i];
            }
        } else {
            this->arr = nullptr;
        }
        cout << "深拷贝构造函数:拷贝大小为 " << size << " 的数组" << endl;
    }

    // 赋值运算符重载:深拷贝
    MyArray& operator=(const MyArray& other) {
        if (this == &other) {
            return *this;
        }
        // 释放当前对象原有内存
        if (this->arr != nullptr) {
            delete[] this->arr;
        }
        // 拷贝大小并分配新内存
        this->size = other.size;
        if (this->size > 0) {
            this->arr = new int[this->size];
            for (int i = 0; i < this->size; i++) {
                this->arr[i] = other.arr[i];
            }
        } else {
            this->arr = nullptr;
        }
        cout << "赋值运算符重载:深拷贝数组" << endl;
        return *this;
    }

    void setValue(int index, int value) {
        if (index >= 0 && index < size) {
            arr[index] = value;
        } else {
            cout << "⚠️ 索引越界" << endl;
        }
    }

    void printArray() {
        if (arr == nullptr) {
            cout << "数组为空" << endl;
            return;
        }
        cout << "数组元素:";
        for (int i = 0; i < size; i++) {
            cout << arr[i] << " ";
        }
        cout << endl;
    }

    // 析构函数:释放内存
    ~MyArray() {
        if (arr != nullptr) {
            delete[] arr;
            arr = nullptr;
        }
        cout << "析构函数:释放数组内存" << endl;
    }
};

int main() {
    // 创建数组对象
    MyArray arr1(5);
    arr1.setValue(0, 10);
    arr1.setValue(1, 20);
    arr1.setValue(2, 30);
    arr1.printArray();

    // 深拷贝构造新对象
    MyArray arr2 = arr1;
    arr2.setValue(0, 100);
    cout << "修改 arr2 后:" << endl;
    arr1.printArray();
    arr2.printArray();

    // 赋值运算符重载
    MyArray arr3(3);
    arr3 = arr1;
    arr3.setValue(1, 200);
    cout << "修改 arr3 后:" << endl;
    arr1.printArray();
    arr3.printArray();

    return 0;
}

六、开发规范与建议

1. 三法则原则

当类中包含指针成员或需要管理资源时,必须同时实现拷贝构造函数、赋值运算符重载、析构函数。这三者缺一不可,合称'三法则'(Rule of Three)。

2. 优先使用智能指针

在现代 C++(C++11 及以上)开发中,建议优先使用 unique_ptr、shared_ptr 等智能指针替代裸指针。它们能自动管理内存,从根本上避免手动实现深拷贝的繁琐和潜在错误。

3. 常见问题排查

  • 忘记处理自赋值:在赋值运算符开头加 if (this == &other) 判断。
  • 释放内存后未置空:析构或赋值后,将指针置为 nullptr,防止野指针。
  • 内存分配失败:生产环境中应添加异常处理逻辑,增强健壮性。

七、总结

拷贝构造函数负责创建新对象,赋值运算符负责更新已存在对象。浅拷贝仅复制值,深拷贝则确保指针指向的内存独立。遵循三法则原则,合理使用深拷贝,是保障 C++ 程序内存安全的关键技能。

目录

  1. C++ 拷贝构造函数与赋值运算符:深拷贝与浅拷贝辨析
  2. 一、拷贝构造函数的概念与触发场景
  3. 1. 基本语法
  4. 2. 什么时候会被调用?
  5. 代码验证
  6. 二、浅拷贝与深拷贝的核心区别
  7. 1. 浅拷贝的隐患
  8. 问题演示
  9. 2. 深拷贝的实现
  10. 重写拷贝构造函数
  11. 三、赋值运算符重载与深拷贝
  12. 1. 核心原则
  13. 2. 代码实现
  14. 四、拷贝构造函数与赋值运算符的区别
  15. 五、实战案例:自定义数组类
  16. 需求分析
  17. 完整代码
  18. 六、开发规范与建议
  19. 1. 三法则原则
  20. 2. 优先使用智能指针
  21. 3. 常见问题排查
  22. 七、总结
  • 💰 8折买阿里云服务器限时8折了解详情
  • Magick API 一键接入全球大模型注册送1000万token查看
  • 🤖 一键搭建Deepseek满血版了解详情
  • 一键打造专属AI 智能体了解详情
极客日志微信公众号二维码

微信扫一扫,关注极客日志

微信公众号「极客日志V2」,在微信中扫描左侧二维码关注。展示文案:极客日志V2 zeeklog

更多推荐文章

查看全部
  • 基于 TRAE CN 与 MasterGo MCP 实现设计稿转前端代码
  • 学生与教育工作者免费获取 GitHub Copilot 权限指南
  • 在 Windows 10/11 上使用 VMware 17 Pro 安装 macOS 官方镜像虚拟机
  • 2025 无人机四大顶会 16 篇精选论文解读
  • 大疆无人机常见故障提示与处理指南
  • 按下 F5 后,浏览器前端究竟发生了什么?
  • stable-diffusion-webui 照片艺术化风格迁移指南
  • 基于 Docker 部署的 AI 量化分析平台搭建与波浪理论实战
  • 使用 Claude 与 Android Studio 联动开发 WebView 项目模板
  • Python 版本选择指南:演进历史与项目决策建议
  • DeepSeek-V3 技术报告详解:架构、训练与评估核心分析
  • whisperX 入门实战:环境搭建与语音识别全流程
  • K-means 聚类算法原理与实现详解
  • Python 技术生态全景:从 Web 开发到数据科学实战
  • DeepSeek-R1 复现方案:Open-R1 项目实践
  • JavaScript Streams API 核心概念与原理
  • Spring Boot 零基础入门:快速构建 Java Web 应用
  • Agentic RAG:基于多文档的 AI Agent 智能体构建指南
  • 基于 CSANMT 的实时中英对照翻译服务实战
  • 二叉树转字符串的递归实现与边界处理详解

相关免费在线工具

  • 加密/解密文本

    使用加密算法(如AES、TripleDES、Rabbit或RC4)加密和解密文本明文。 在线工具,加密/解密文本在线工具,online

  • Gemini 图片去水印

    基于开源反向 Alpha 混合算法去除 Gemini/Nano Banana 图片水印,支持批量处理与下载。 在线工具,Gemini 图片去水印在线工具,online

  • Base64 字符串编码/解码

    将字符串编码和解码为其 Base64 格式表示形式即可。 在线工具,Base64 字符串编码/解码在线工具,online

  • Base64 文件转换器

    将字符串、文件或图像转换为其 Base64 表示形式。 在线工具,Base64 文件转换器在线工具,online

  • Markdown转HTML

    将 Markdown(GFM)转为 HTML 片段,浏览器内 marked 解析;与 HTML转Markdown 互为补充。 在线工具,Markdown转HTML在线工具,online

  • HTML转Markdown

    将 HTML 片段转为 GitHub Flavored Markdown,支持标题、列表、链接、代码块与表格等;浏览器内处理,可链接预填。 在线工具,HTML转Markdown在线工具,online