【c++面向对象编程】第4篇:类与对象(三):拷贝构造函数与深浅拷贝问题

目录

一、一个崩溃的程序

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

调用时机(三个场景)

[三、浅拷贝 vs 深拷贝](#三、浅拷贝 vs 深拷贝)

浅拷贝(默认行为)

深拷贝(正确的做法)

四、什么时候必须自己写拷贝构造函数?

一个反面例子:vector的浅拷贝问题

五、完整的例子:安全的动态数组

六、三个常见的坑

[1. 拷贝构造函数参数不用引用 → 无限递归](#1. 拷贝构造函数参数不用引用 → 无限递归)

[2. 忘了const导致无法拷贝const对象](#2. 忘了const导致无法拷贝const对象)

[3. 浅拷贝发生在你没想到的地方](#3. 浅拷贝发生在你没想到的地方)

七、这一篇的收获


一、一个崩溃的程序

先看这段代码,你觉得它会崩溃吗?

cpp

复制代码
class StringWrapper {
private:
    char* data;
public:
    StringWrapper(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    ~StringWrapper() {
        delete[] data;
    }
    
    void print() { cout << data << endl; }
};

int main() {
    StringWrapper s1("Hello");
    StringWrapper s2 = s1;   // 用s1初始化s2
    s1.print();
    s2.print();
    return 0;
}   // 程序在这里崩溃!

运行结果:可能正常输出,也可能输出乱码,最后大概率崩溃

原因很简单:s1s2里面的data指针指向了同一块内存 。当程序结束,s2先析构,delete[]了那块内存;然后s1析构,再次delete[]同一块内存------重复释放,程序崩溃

这就是浅拷贝带来的灾难。


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

拷贝构造函数是一种特殊的构造函数:

  • 参数是本类对象的const引用

  • 已有的对象 去创建新的对象时自动调用

  • 如果你不写,编译器会生成一个默认的(逐成员复制)

语法长这样:

cpp

复制代码
class MyClass {
public:
    // 拷贝构造函数
    MyClass(const MyClass& other) {
        // 拷贝逻辑
    }
};

调用时机(三个场景)

cpp

复制代码
class Demo {
public:
    Demo() { cout << "普通构造" << endl; }
    Demo(const Demo& other) { cout << "拷贝构造" << endl; }
    ~Demo() { cout << "析构" << endl; }
};

Demo makeDemo() {
    Demo d;
    return d;  // 场景3:返回值
}

int main() {
    Demo a;           // 普通构造
    Demo b = a;       // 场景1:用a初始化b → 拷贝构造
    Demo c(a);        // 场景2:直接传参 → 拷贝构造
    Demo d = makeDemo(); // 场景3:返回值(可能被优化掉,不一定调用)
}

关键点:"="在这里不是赋值,是初始化 。赋值是后面讲的重载operator=


三、浅拷贝 vs 深拷贝

浅拷贝(默认行为)

编译器生成的默认拷贝构造函数做的事很简单:把每个成员变量的值原样复制

cpp

复制代码
// 编译器生成的默认版本(概念上)
StringWrapper(const StringWrapper& other) 
    : data(other.data)  // 只复制指针的值,不复制指针指向的内容
{}

对于intdouble这种值类型,浅拷贝没问题。但对于指针,复制的是地址,不是地址里的内容。

浅拷贝的问题

  • 两个对象指向同一块内存

  • 一个修改,另一个也跟着变(可能不是你想要的效果)

  • 一个释放,另一个变成悬空指针

  • 重复释放导致崩溃

深拷贝(正确的做法)

深拷贝的做法:不复制指针的值,而是复制指针指向的内容

cpp

复制代码
class StringWrapper {
private:
    char* data;
public:
    // 普通构造函数
    StringWrapper(const char* str) {
        data = new char[strlen(str) + 1];
        strcpy(data, str);
    }
    
    // 拷贝构造函数(深拷贝)
    StringWrapper(const StringWrapper& other) {
        // 1. 分配新内存
        data = new char[strlen(other.data) + 1];
        // 2. 复制内容
        strcpy(data, other.data);
        cout << "深拷贝:" << data << endl;
    }
    
    // 析构函数
    ~StringWrapper() {
        delete[] data;
        cout << "释放:" << data << endl;
    }
    
    void print() { cout << data << endl; }
    
    // 后面会讲赋值运算符重载
};

现在运行之前会崩溃的例子:

cpp

复制代码
int main() {
    StringWrapper s1("Hello");
    StringWrapper s2 = s1;  // 深拷贝:s2有自己独立的内存
    s1.print();  // Hello
    s2.print();  // Hello
    return 0;    // 分别释放两块内存,不冲突
}

内存布局对比:

text

复制代码
浅拷贝:
s1.data ──→ [H][e][l][l][o][\0]
s2.data ──→ ↑ (指向同一块)

深拷贝:
s1.data ──→ [H][e][l][l][o][\0]
s2.data ──→ [H][e][l][l][o][\0] (另一块内存)

四、什么时候必须自己写拷贝构造函数?

三法则(Rule of Three):如果类需要自定义析构函数,那么它几乎一定也需要自定义拷贝构造函数和拷贝赋值运算符。

具体来说,以下情况必须写拷贝构造函数:

  1. 类里有指针成员,并且构造函数里用new分配了内存

  2. 类里有文件句柄、数据库连接等需要"独占"的资源

  3. 类里有互斥锁(mutex)(两个对象拥有同一个锁会导致死锁)

一句话:默认的逐成员复制对你的资源管理方式不适用时

一个反面例子:vector的浅拷贝问题

cpp

复制代码
class IntVector {
private:
    int* arr;
    int size;
public:
    IntVector(int n) : size(n) {
        arr = new int[n];
        for(int i=0; i<n; i++) arr[i] = i;
    }
    
    ~IntVector() { delete[] arr; }
    
    // 没有写拷贝构造函数 → 浅拷贝!
    
    void set(int idx, int val) { arr[idx] = val; }
    int get(int idx) { return arr[idx]; }
};

int main() {
    IntVector v1(5);
    IntVector v2 = v1;  // 浅拷贝,v2.arr指向v1.arr同一块内存
    
    v2.set(0, 999);     // 修改v2
    cout << v1.get(0);  // 输出999!v1被意外修改了
    
    // 程序结束,两次delete[]同一块内存 → 崩溃
}

这就是所谓的"意外的共享状态"。


五、完整的例子:安全的动态数组

cpp

复制代码
#include <iostream>
#include <cstring>
using namespace std;

class SafeArray {
private:
    int* data;
    int size;
    
public:
    // 普通构造函数
    SafeArray(int n) : size(n) {
        data = new int[n];
        for (int i = 0; i < n; i++) {
            data[i] = 0;
        }
        cout << "构造:分配了" << n << "个int" << endl;
    }
    
    // 拷贝构造函数(深拷贝)
    SafeArray(const SafeArray& other) : size(other.size) {
        data = new int[size];
        for (int i = 0; i < size; i++) {
            data[i] = other.data[i];
        }
        cout << "拷贝构造:深拷贝了" << size << "个int" << endl;
    }
    
    // 析构函数
    ~SafeArray() {
        delete[] data;
        cout << "析构:释放了" << size << "个int" << endl;
    }
    
    void set(int idx, int val) {
        if (idx >= 0 && idx < size) data[idx] = val;
    }
    
    int get(int idx) const {
        if (idx >= 0 && idx < size) return data[idx];
        return -1;
    }
    
    void print() const {
        cout << "[";
        for (int i = 0; i < size; i++) {
            cout << data[i] << (i < size-1 ? ", " : "");
        }
        cout << "]" << endl;
    }
};

int main() {
    SafeArray a(5);
    for (int i = 0; i < 5; i++) a.set(i, i * 10);
    a.print();  // [0, 10, 20, 30, 40]
    
    SafeArray b = a;  // 拷贝构造
    b.set(0, 999);
    
    cout << "a: "; a.print();  // [0, 10, 20, 30, 40]  ← 没被影响
    cout << "b: "; b.print();  // [999, 10, 20, 30, 40] ← 独立修改
    
    return 0;
}

输出:

text

复制代码
构造:分配了5个int
[0, 10, 20, 30, 40]
拷贝构造:深拷贝了5个int
a: [0, 10, 20, 30, 40]
b: [999, 10, 20, 30, 40]
析构:释放了5个int
析构:释放了5个int

完美!两个对象互不干扰,各释放各的内存。


六、三个常见的坑

1. 拷贝构造函数参数不用引用 → 无限递归

cpp

复制代码
class Bad {
public:
    Bad(Bad other) {  // ❌ 传值,会再次调用拷贝构造,无限递归
        // ...
    }
};

参数必须用引用 ,通常是const引用:

cpp

复制代码
Bad(const Bad& other) { }  // ✅

2. 忘了const导致无法拷贝const对象

cpp

复制代码
class Demo {
public:
    Demo(Demo& other) { }  // 参数不是const
};

const Demo d1;
Demo d2 = d1;  // ❌ 错误!不能将const转为非const引用

3. 浅拷贝发生在你没想到的地方

函数传参也会调用拷贝构造函数:

cpp

复制代码
void func(SafeArray arr) {  // 传值,会调用拷贝构造
    // ...
}

SafeArray a(10);
func(a);  // 这里发生了一次深拷贝

如果数组很大,深拷贝的开销不小。想避免拷贝?用引用传参:

cpp

复制代码
void func(const SafeArray& arr) {  // 不拷贝
    // ...
}

七、这一篇的收获

你现在应该明白:

  • 拷贝构造函数:用已有对象创建新对象时调用

  • 浅拷贝:默认行为,只复制指针的值,导致两个对象共享内存

  • 深拷贝:自己实现,分配新内存并复制内容,对象各自独立

  • 三法则:需要析构函数 → 就需要拷贝构造和拷贝赋值

💡 小作业:修改上面的SafeArray,故意去掉拷贝构造函数,观察程序会出什么问题。然后加上拷贝构造函数,验证深拷贝解决了问题。


下一篇预告 :第5篇《类与对象(四):赋值运算符重载》------=不只是初始化,还有赋值。拷贝构造和赋值运算符有什么区别?什么时候调用哪个?为什么赋值要返回引用?下篇揭晓。

相关推荐
j_xxx404_1 小时前
Linux共享内存原理与实战:从内核到C++实现|附源码
linux·运维·开发语言·c++·人工智能
C雨后彩虹1 小时前
猴子爬山问题
java·数据结构·算法·华为·面试
y = xⁿ1 小时前
20天速通LeetCodeday13:关于回溯
算法
计算机安禾1 小时前
【c++面向对象编程】第1篇:从C到C++:面向对象编程思想入门
c语言·c++·算法
Master_oid1 小时前
机器学习41:利用KNN算法实现手写数字识别
深度学习·算法·机器学习
liuhuizuikeai1 小时前
菜品抽奖活动MFC+服务端
c++·windows·mfc
ouliten1 小时前
C++笔记:Lambda表达式
c++·笔记
金玉满堂@bj1 小时前
Python 后端开发 从零到就业完整教程(2026 企业级完整版)
开发语言·python
OYangxf1 小时前
力扣hot100【子串专题】
算法·leetcode·职场和发展