导读:本期聚焦于USDT程序员创作的《C++中的浅拷贝、深拷贝和写时拷贝有何区别?附代码讲解》,敬请观看详情。一个自定义字符串类在拷贝构造时只复制了指针,两个对象释放同一块内存导致程序崩溃,这类问题的根源正是浅拷贝。本文围绕C++对象复制语义展开,先区分浅拷贝与深拷贝的内存模型差异,再说明深拷贝如何通过独立分配内存避免双重释放;随后引入写时拷贝(COW)机制,解释它如何在共享内存与延迟复制之间取得平衡,并结合引用计数给出可运行的实现。文中包含完整代码示例,覆盖拷贝构造函数、赋值运算符重载、析构函数安全处理等关键点,帮助读者理解何时该用哪种拷贝策略。

在C++中,对象复制看似简单,实际涉及指针成员时很容易埋下隐患。比如一个字符串类内部持有 char* 指针,如果直接使用编译器生成的默认拷贝构造函数,只会复制指针地址,不会复制指针指向的内容。这样两个对象会指向同一块堆内存,析构时对同一地址执行两次 delete[],程序会直接崩溃。

C++中的浅拷贝、深拷贝和写时拷贝有何区别?附代码讲解

一、浅拷贝:复制了指针却没有复制内存

浅拷贝就是只复制成员变量的值,不复制指针指向的数据。对于基本类型成员,浅拷贝没有问题;但只要类里出现堆内存指针、文件句柄、网络连接等资源,浅拷贝就会留下重复释放、数据相互干扰等风险。

下面这段代码定义了一个简单的字符串类,它没有显式提供拷贝构造函数和赋值运算符。

#include <iostream>
#include <cstring>

class ShallowString {
private:
    char* _data;
public:
    ShallowString(const char* s = "") {
        _data = new char[strlen(s) + 1];
        strcpy(_data, s);
    }

    const char* c_str() const {
        return _data;
    }

    ~ShallowString() {
        delete[] _data;
    }
};

int main() {
    ShallowString a("hello");
    ShallowString b = a;  // 默认拷贝构造,只复制指针
    std::cout << a.c_str() << " / " << b.c_str() << std::endl;
    return 0;
}

这个程序在编译阶段不会报错,甚至输出也正常,原因是指针确实指向同一块有效内存。但退出 main 函数时,b 先析构并释放内存,随后 a 再次释放同一地址,触发未定义行为。这个场景在稍大的项目中非常常见,尤其是容器扩容、按值传参、返回值优化等过程都可能触发隐式拷贝。

浅拷贝并非一无是处。如果类本身不管理资源,或者资源是只读共享的,浅拷贝可以大幅减少复制开销。问题在于C++无法自动判断一个指针是否代表所有权,所以只要类负责释放资源,就要明确拷贝语义。

二、深拷贝:让每个对象独立持有资源

深拷贝的解决思路很直接:拷贝时不仅复制指针,还为新对象分配一块新内存,再把原内存里的内容复制过去。这样两个对象的指针指向不同的地址,析构时各释放各的,互相没有影响。

实现深拷贝需要同时重写拷贝构造函数和赋值运算符。拷贝构造函数负责从无到有创建独立数据,赋值运算符负责处理已有对象,并且要特别注意自赋值和异常安全。

#include <iostream>
#include <cstring>

class DeepString {
private:
    char* _data;
    size_t _len;
public:
    DeepString(const char* s = "") {
        _len = strlen(s);
        _data = new char[_len + 1];
        strcpy(_data, s);
    }

    DeepString(const DeepString& other) {
        _len = other._len;
        _data = new char[_len + 1];
        strcpy(_data, other._data);
    }

    DeepString& operator=(const DeepString& other) {
        if (this != &other) {
            char* tmp = new char[other._len + 1];
            strcpy(tmp, other._data);

            delete[] _data;
            _data = tmp;
            _len = other._len;
        }
        return *this;
    }

    ~DeepString() {
        delete[] _data;
    }

    char& operator[](size_t i) {
        return _data[i];
    }

    const char& operator[](size_t i) const {
        return _data[i];
    }

    const char* c_str() const {
        return _data;
    }
};

赋值运算符里先申请新的内存并复制数据,成功后再释放旧内存,这种先新后旧的顺序能避免“新内存分配失败导致原对象数据丢失”的问题。自赋值检查也是必须的,否则 a = a 会先把自己的内容删掉。

深拷贝的缺点是空间和时间成本较高。当对象很大、复制频率很高,而程序中又存在大量只读访问时,每次拷贝都完整复制整块数据并不经济。这也是写时拷贝出现的重要背景。

三、写时拷贝:共享数据,直到真正修改时才复制

写时拷贝(Copy-on-Write, COW)介于浅拷贝和深拷贝之间。它最初只共享同一份数据,并维护一个引用计数;当某个对象需要修改数据时,才为该对象复制一份独立副本,再在副本上修改。这样多个对象只读访问时几乎零开销,写操作时才付出复制成本。

实现写时拷贝的关键是引入引用计数。所有共享对象指向一个堆上的控制块,控制块里保存数据指针和引用数量。拷贝构造时只增加引用计数,不复制数据;析构时减少引用计数,归零时释放数据。修改数据前先检查引用计数,如果大于1则分离出一份新数据。

#include <iostream>
#include <cstring>

class CowString {
private:
    struct Data {
        char* ptr;
        size_t ref_count;

        Data(const char* s = "") {
            ptr = new char[strlen(s) + 1];
            strcpy(ptr, s);
            ref_count = 1;
        }

        ~Data() {
            delete[] ptr;
        }
    };

    Data* _data;

    void release() {
        if (_data != nullptr) {
            --_data->ref_count;
            if (_data->ref_count == 0) {
                delete _data;
            }
            _data = nullptr;
        }
    }

    void copy_on_write() {
        if (_data->ref_count > 1) {
            Data* old = _data;
            Data* fresh = new Data(old->ptr);
            --old->ref_count;
            _data = fresh;
        }
    }

public:
    CowString(const char* s = "") {
        _data = new Data(s);
    }

    CowString(const CowString& other) {
        _data = other._data;
        ++_data->ref_count;
    }

    CowString& operator=(const CowString& other) {
        if (this != &other) {
            ++other._data->ref_count;
            release();
            _data = other._data;
        }
        return *this;
    }

    ~CowString() {
        release();
    }

    char& operator[](size_t i) {
        copy_on_write();
        return _data->ptr[i];
    }

    const char& operator[](size_t i) const {
        return _data->ptr[i];
    }

    const char* c_str() const {
        return _data->ptr;
    }
};

这段代码里,copy_on_write 函数在引用计数大于1时分离数据。注意常量版本的 operator[] 没有调用它,因为只读访问不需要复制;非常量版本才触发复制。这样设计可以让只读遍历、打印等操作保持共享,而写操作自动获得独立数据。

写时拷贝也不是完美方案。引用计数需要原子操作或加锁来保证多线程安全,这会带来额外开销;并且 char& 返回引用后,外部可能长期持有这个引用,导致后续复制判断变得复杂。现代C++标准库在 std::string 的某些历史实现中曾使用COW,但C++11之后由于移动语义和标准约束,主流实现已经基本放弃COW。尽管如此,理解COW对掌握资源管理、缓存设计和共享不可变数据仍然很有价值。

四、三种策略如何选择

浅拷贝适合不含堆资源或者明确共享同一资源的场景,但需要避免析构时重复释放。深拷贝适合对象较小、复制次数不多、要求完全独立的场景,它的语义最清晰,调试成本最低。写时拷贝适合读多写少的大对象,能显著降低只读复制开销,但实现复杂度上升,需要处理引用计数、线程安全和引用逃逸问题。

C++11之后移动语义提供了另一种优化思路:资源所有权可以直接转移,不需要复制内容。移动构造和移动赋值可以替代很多原本需要深拷贝的临时对象场景,而且实现起来比写时拷贝更简单、更安全。因此实际项目中,优先考虑深拷贝加移动语义,只有当写操作比例极低、共享读占绝对主导时,才考虑写时拷贝。

无论采用哪种策略,拷贝控制函数都是一个整体:拷贝构造、拷贝赋值、移动构造、移动赋值、析构函数需要一起设计。只要类管理资源,就不要依赖编译器默认生成的拷贝行为,否则浅拷贝带来的重复释放会在程序退出时突然爆发,排查起来非常困难。

C++浅拷贝C++深拷贝写时拷贝修改时间:2026-09-24 00:14:43

免责声明:已尽一切努力确保本网站所含信息的准确性。网站作品多为原创整理与精心创作,观点力求客观中立。本站旨在免费分享,内容仅供个人学习、研究或参考使用。若引用了第三方作品,版权归原作者所有。如内容涉及您的权益,请联系我们进行处理Email:chomcom@qq.com。
引用或转载本作品时,请注明当前出处:https://www.ipipp.com/html/0924/61098.html,基于非商业用途的前提下,欢迎转载或二创本作品。
内容垂直聚焦
专注技术核心技术栏目,确保每篇文章深度聚焦于实用技能。从代码技巧到架构设计,为用户提供无干扰的纯技术知识沉淀,精准满足专业提升需求。
知识结构清晰
覆盖从开发到部署的全链路。AI、前端、编程、数据库、服务器、建站、系统层层递进,构建清晰学习路径,帮助用户系统化掌握开发与运维所需的核心技术。
深度技术解析
拒绝泛泛而谈,深入技术细节与实践难点。无论是数据库优化还是服务器配置,均结合真实场景与代码示例进行剖析,致力于提供可直接应用于工作的解决方案。
专业领域覆盖
精准对应开发生命周期。从前端界面到后端编程,从数据库操作到服务器运维,形成完整闭环,一站式满足全栈工程师和运维人员的技术需求。
即学即用高效
内容强调实操性,步骤清晰、代码完整。用户可根据教程直接复现和应用于自身项目,显著缩短从学习到实践的距离,快速解决开发中的具体问题。
持续更新保障
专注既定技术方向进行长期、稳定的内容输出。确保各栏目技术文章持续更新迭代,紧跟主流技术发展趋势,为用户提供经久不衰的学习价值。