c++拷贝构造函数

在C++中,拷贝构造函数(Copy Constructor)是一种特殊的构造函数,它用于用一个已存在的同类对象来初始化另一个正在创建的新对象。

一、 拷贝构造函数是什么?

1. 定义与形式

它的参数是本类对象的引用 (通常加 const 修饰,防止原对象被修改)。其标准签名如下:

cpp 复制代码
class MyClass {
public:
    //自定义的拷贝构造函数
    MyClass(const MyClass& other){}

    //有了拷贝构造函数,就必须要自己写默认的构造函数了,不然就没办法MyClass obj1这样定义类对象了
    MyClass() {
        // 可以在这里初始化成员变量
    }
};
2. 什么时候会被自动调用?

拷贝构造函数在以下三种"对象初始化"的场景下会被隐式调用:

  1. 用一个对象初始化另一个新对象:

    cpp 复制代码
    #include "MyClass.h"
    
    int main() {
        MyClass obj1;
        MyClass obj2 = obj1; // 或 MyClass obj2(obj1);
        return 0;
    }
  2. 函数参数以值传递(Pass by Value)方式传入对象:

    cpp 复制代码
    void func(MyClass obj); 
    MyClass obj1;
    func(obj1); // 这里会调用拷贝构造函数将 obj1 复制一份传进函数
  3. 函数返回值是以值传递方式返回一个对象:

    cpp 复制代码
    MyClass createObj() {
        MyClass temp;
        return temp; // 可能触发拷贝(或被编译器优化为移动/返回值优化 RVO)
    }

二、 为什么要有拷贝构造函数的存在?

如果没有显式定义拷贝构造函数,C++ 编译器会为我们自动生成一个默认的拷贝构造函数。那么,为什么语言本身需要这样一个机制,以及为什么有时我们必须自己写它呢?

1. 核心原因:实现对象的安全复制

当我们要把一个对象的所有状态(成员变量)复制给另一个新对象时,拷贝构造函数提供了一种标准、规范的方式。它保证了对象在创建时就能获得与源对象相同初始状态。

2. 区分"浅拷贝"与"深拷贝"(为什么有时需要自己写)

编译器自动生成的默认拷贝构造函数执行的是浅拷贝(Shallow Copy)------它只是按字节(逐个成员)把原对象的属性值复制到新对象中。

  • 浅拷贝的隐患(指针/动态内存) : 如果类里面包含指针,且指针指向了堆区动态分配的内存,浅拷贝会导致两个对象的指针指向同一块内存地址。

    1. 当其中一个对象被销毁并释放了该内存时,另一个对象的指针就会变成悬空指针(Dangling Pointer)。

    2. 当两个对象先后析构时,会试图对同一块内存释放两次(Double Free),导致程序崩溃。

  • 解决方案------深拷贝(Deep Copy) : 正因为浅拷贝在遇到动态资源管理时的致命缺陷,我们需要自定义拷贝构造函数 来实行深拷贝:不仅复制指针的值,还要为新对象申请独立的内存空间,并把原内存里的内容复制过去。

三、 代码对比示例

浅拷贝引发的问题(编译器默认行为):
cpp 复制代码
#include <iostream>
#include <cstring>
#include <windows.h> // 必须包含这个头文件,使vscode支持中文输出

class Shallow {
public:
    int* data;
    
    Shallow(int d) {
        data = new int(d); // 申请堆内存
    }

    // 什么都不写才是默认的拷贝构造函数(浅拷贝)
    //如果这样写了,哪怕函数体是空,也不是默认的构造函数
    /**Shallow(const Shallow& other) {
        
    }**/
    
    ~Shallow() {
        std::cout << "[准备析构] this=" << this << " data=" << data << "\n";
        delete data; // 析构时释放内存
    }
};

int main() {

    // 设置控制台输出编码为 UTF-8
    SetConsoleOutputCP(65001);

    Shallow a(10);

    {
        Shallow b = a;   // b 和 a 共享内存
        std::cout << "*(a.data)=" << *(a.data) << " \n";
        std::cout << "*(b.data)=" << *(b.data) << " \n";
    } // b 析构,释放 data

    std::cout << " b 析构后, a.data=" << a.data << " *a.data=" << *(a.data) << "\n";  
    return 0;
}

运行结果

cpp 复制代码
*(a.data)=10 
*(b.data)=10 
[准备析构] this=0xb345fffbd0 data=0x2710cd94a50
 b 析构后, a.data=0x2710cd94a50 *a.data=215569920
[准备析构] this=0xb345fffbd8 data=0x2710cd94a50

由上述代码可以看到,浅拷贝在类存在指针成员变量的时候,如果使用了默认的拷贝构造方法初始化另一个对象,有可能会在对象析构时对同一块内存释放两次

深拷贝(自定义拷贝构造函数):
cpp 复制代码
#include <iostream>
#include <windows.h> // 必须包含这个头文件,使vscode支持中文输出

class Deep {
public:
    int* data;
    
    Deep(int d) {
        data = new int(d);
    }
    
    // 自定义拷贝构造函数(深拷贝)
    Deep(const Deep& other) {
        std::cout << "执行深拷贝构造,为新对象重新申请独立的内存,并复制值\n";
        data = new int(*(other.data));
    }
    
    ~Deep() {
        std::cout << "[准备析构] this=" << this << " data=" << data << "\n";
        delete data; // 各自释放各自的内存,互不影响
    }
};

int main() {

    // 设置控制台输出编码为 UTF-8
    SetConsoleOutputCP(65001); 
     
    Deep a(10);

    {
        Deep b = a;   // b 和 a 共享内存
        std::cout << "*(a.data)=" << *(a.data) << " \n";
        std::cout << "*(b.data)=" << *(b.data) << " \n";
    } // b 析构,释放 data

    std::cout << "after b dtor, a.data=" << a.data << " *a.data=" << *a.data << "\n";

    return 0; // 安全退出
}

运行结果

cpp 复制代码
执行深拷贝构造,为新对象重新申请独立的内存,并复制值
*(a.data)=10 
*(b.data)=10 
[准备析构] this=0xfc07ff6e0 data=0x14e59038d70
after b dtor, a.data=0x14e59038d50 *a.data=10
[准备析构] this=0xfc07ff6e8 data=0x14e59038d50

可以看到,程序不会再出现 会对同一块内存释放两次和卡了一小会的问题,两次析构的是不一样的内存

总结

  • 是什么:用已有对象初始化新对象的特殊构造函数。

  • 为什么要有 :为了能够复制对象。如果类中只包含基本数据类型(如 int、double),编译器默认的浅拷贝就够用了;但如果类中含有指针、文件句柄、网络连接等动态资源 ,就需要通过自定义拷贝构造函数来实现深拷贝,以保证内存安全和程序的健壮性(这也是C++三大件------拷贝构造、赋值运算符重载、析构函数需要配合"Rule of Three/Five"原则编写的原因)

ps:简单补充一下什么是Rule of Three/Five"原则

Rule of Three(三法则,C++11 之前)

如果你需要自定义以下三者中的任意一个,那几乎肯定也需要定义另外两个:

  1. 析构函数

  2. 拷贝构造函数

  3. 拷贝赋值运算符

Rule of Five(五法则,C++11 及以后)

C++11 引入了移动语义,新增了两个特殊成员函数。五法则建议,如果你需要定义(或 =delete)以下五个中的任意一个,就应该考虑全部五个:

  1. 析构函数

  2. 拷贝构造函数

  3. 拷贝赋值运算符

  4. 移动构造函数 (C++11)

  5. 移动赋值运算符 (C++11)

现代首选:Rule of Zero(零法则)

虽然三/五法则很重要,但在现代 C++ 中,最好的做法通常是"零法则" :不要自己手动管理资源。

相关推荐
爱喝雪碧的可乐36 分钟前
MATLAB
开发语言·算法·matlab
H.莓飛2 小时前
【C++】【STL】vector_OJ题
linux·服务器·开发语言·数据结构·c++·visual studio
Rabitebla2 小时前
【Linux操作系统】进程篇(十一):进程程序替换——从底层原理到 exec 函数族
linux·服务器·c++·笔记·学习·算法
沫璃染墨3 小时前
从零入门计算机网络系列(十五):TCP可靠传输机制详解——从序号与确认应答到乱序处理
服务器·网络·c++·tcp/ip·tcp
青少儿编程课堂3 小时前
蓝桥杯·全国青少年信息素养大赛·电子学会Python/C++考级:Bellman-Ford与SPFA含负权最短路及负环检测全解析
c++·python·算法·bfs·信息学竞赛
旖旎夜光3 小时前
【C++ 基础入门】类和对象(下):初始化列表、static 成员、友元与内部类
c++·笔记·学习
j7~4 小时前
【C++ 八股文】(篇四)C++11 新特性
开发语言·c++·面试·c++11·八股文
eggwyw4 小时前
C++中大括号的用法合集
开发语言·c++
xiaoye-duck4 小时前
《C++ 项目:手写大模型接入 SDK》本地 Ollama 部署与适配
开发语言·网络·c++·语言模型