5. 拷贝构造函数:什么是拷贝构造函数?浅拷贝与深拷贝、拷贝构造函数的调用时机

拷贝构造函数,说白了就是用一个已经存在的对象,去构造另一个新对象。我刚开始学C++时,总觉得这玩意儿有点多余——直接赋值不就行了?后来踩过几次坑才明白,拷贝构造函数背后藏着不少门道。

5.1 什么是拷贝构造函数

拷贝构造函数是一种特殊的构造函数。它的参数是同类型的常量引用。嗯,这里要注意,参数必须是引用,否则会无限递归调用自己——你想想看,传值本身就要调用拷贝构造,这不就死循环了吗?

class Person {
public:
    // 拷贝构造函数
    Person(const Person& other) {
        name = other.name;
        age = other.age;
    }
private:
    std::string name;
    int age;
};

如果你不写,编译器会帮你生成一个默认的拷贝构造函数。它做的事情很简单:把每个成员变量逐个拷贝过去。这在大多数情况下够用了,但一旦涉及动态内存,问题就来了。

5.2 浅拷贝 vs 深拷贝

这两个概念,我当年理解了好一阵子。咱们直接看代码:

浅拷贝——复制指针,不复制数据

class ShallowArray {
public:
    ShallowArray(int size) {
        data = new int[size];
        size_ = size;
    }
    // 默认拷贝构造函数——浅拷贝
    // ShallowArray(const ShallowArray& other) 
    //     : data(other.data), size_(other.size_) {}
    
    ~ShallowArray() {
        delete[] data;  // 两个对象指向同一块内存,会double free!
    }
private:
    int* data;
    int size_;
};

浅拷贝只复制指针的值。两个对象的data指向同一块内存。一个对象析构时释放了内存,另一个对象再析构就崩溃了。我在项目中遇到过这种问题,排查了一下午才发现是浅拷贝惹的祸。

深拷贝——复制指针指向的数据

class DeepArray {
public:
    DeepArray(int size) {
        data = new int[size];
        size_ = size;
    }
    
    // 深拷贝构造函数
    DeepArray(const DeepArray& other) {
        size_ = other.size_;
        data = new int[size_];          // 重新分配内存
        for (int i = 0; i < size_; ++i) {
            data[i] = other.data[i];    // 逐个复制数据
        }
    }
    
    ~DeepArray() {
        delete[] data;
    }
private:
    int* data;
    int size_;
};

深拷贝会重新分配内存,然后把数据复制过去。每个对象都有自己的独立内存,互不干扰。这样析构时就不会出问题了。

核心区别一句话:浅拷贝复制指针地址,深拷贝复制指针指向的内容。

我曾经踩过的坑:写了一个字符串类,用了浅拷贝。两个字符串对象指向同一块字符数组,一个修改了内容,另一个也跟着变了。更惨的是析构时double free,程序直接崩溃。从那以后,凡是涉及动态内存的类,我第一件事就是检查拷贝构造函数。

5.3 拷贝构造函数的调用时机

搞清楚什么时候会调用拷贝构造函数,比背定义重要得多。我总结了三种常见场景:

场景 示例代码 说明
1. 用对象初始化另一个对象 Person p2 = p1; 最直接的调用方式
2. 函数传参(传值) void func(Person p); 形参是实参的拷贝
3. 函数返回对象(传值) Person createPerson(); 返回时创建临时对象

咱们看个完整例子:

#include <iostream>
#include <string>

class Person {
public:
    Person(const std::string& name) : name_(name) {
        std::cout << "普通构造: " << name_ << std::endl;
    }
    
    Person(const Person& other) : name_(other.name_) {
        std::cout << "拷贝构造: " << name_ << std::endl;
    }
    
    ~Person() {
        std::cout << "析构: " << name_ << std::endl;
    }
private:
    std::string name_;
};

void printPerson(Person p) {  // 场景2:传值调用拷贝构造
    std::cout << "打印: " << std::endl;
}

Person createPerson() {       // 场景3:返回对象
    Person p("临时对象");
    return p;
}

int main() {
    Person p1("张三");        // 普通构造
    Person p2 = p1;           // 场景1:拷贝构造
    
    printPerson(p2);          // 场景2:传值拷贝
    
    Person p3 = createPerson(); // 场景3:返回拷贝
    
    return 0;
}

一个小技巧:如果你不想让对象被拷贝,可以把拷贝构造函数声明为delete。比如Person(const Person&) = delete;。这在设计单例类或资源独占类时特别有用。

5.4 知识体系总览

下面这张图把拷贝构造函数的要点串起来了:

拷贝构造函数知识体系 拷贝构造函数 定义:const T& 参数 浅拷贝:复制指针 深拷贝:复制数据 double free / 数据共享 独立内存 / 安全析构 三种调用场景 ① 对象初始化:Person p2 = p1; ② 函数传参:void func(Person p) ③ 函数返回:return 对象

这张图把拷贝构造函数的定义、浅拷贝与深拷贝的区别、以及三种调用时机都串起来了。我个人建议你把它保存下来,写代码时对照着看,思路会清晰很多。

总结一下:拷贝构造函数不是可有可无的。只要你的类管理了动态资源(堆内存、文件句柄、网络连接等),就必须实现深拷贝。否则,浅拷贝带来的问题会让你在调试时抓狂。我当年就因为这个,一个bug调了两天——嗯,从那以后我再也不敢偷懒了。


公众号:蓝海资料掘金营,微信deep3321