跳至内容

拷贝语义:深拷贝与浅拷贝

学习浅拷贝和深拷贝的区别,如何为拥有资源的类实现正确的拷贝操作,以及拷贝并交换惯用法。

为什么拷贝语义很重要

当你在 C++ 中拷贝一个对象时,究竟拷贝了什么?对于简单类型,答案很直接 ── 所有成员逐位拷贝。但当对象通过指针拥有资源时,默认(编译器生成的)拷贝构造函数执行浅拷贝:它拷贝指针的值,而不是它指向的数据。这意味着两个对象现在共享同一资源,导致双重释放错误和数据损坏。

理解拷贝语义至关重要,因为拷贝在许多地方隐式发生:按值传递、从函数返回、插入容器以及赋值。

浅拷贝问题

编译器生成的拷贝构造函数拷贝每个成员。对于指针,这拷贝的是地址 ── 而不是指向的数据。这是浅拷贝,对于拥有资源的类来说是危险的。

shallow_copy_bug.cpp
#include <iostream>
#include <cstring>

class BrokenString {
    char* data_;
    std::size_t len_;

public:
    BrokenString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    ~BrokenString() {
        delete[] data_;  // frees the buffer
    }

    // No copy constructor defined!
    // Compiler generates shallow copy: copies the pointer

    void print() const {
        std::cout << data_ << '\n';
    }
};

int main() {
    BrokenString a{"Hello"};
    {
        BrokenString b = a;  // shallow copy: b.data_ == a.data_
        b.print();           // "Hello" — works for now
    }  // b destroyed: delete[] b.data_ (same pointer as a.data_!)

    // a.data_ is now a dangling pointer — UNDEFINED BEHAVIOR
    a.print();  // may crash, print garbage, or appear to work
    return 0;
}

实现深拷贝

深拷贝分配新内存并拷贝实际数据。这确保每个对象都拥有自己独立的资源副本。

deep_copy.cpp
#include <iostream>
#include <cstring>
#include <algorithm>

class SafeString {
    char* data_;
    std::size_t len_;

public:
    explicit SafeString(const char* s)
        : len_{std::strlen(s)}
        , data_{new char[std::strlen(s) + 1]}
    {
        std::strcpy(data_, s);
    }

    // Deep copy constructor
    SafeString(const SafeString& other)
        : len_{other.len_}
        , data_{new char[other.len_ + 1]}
    {
        std::strcpy(data_, other.data_);
    }

    // Deep copy assignment with self-assignment check
    SafeString& operator=(const SafeString& other) {
        if (this == &other) return *this;  // self-assignment guard

        // Allocate new resource BEFORE releasing old one
        char* new_data = new char[other.len_ + 1];
        std::strcpy(new_data, other.data_);

        // Release old resource
        delete[] data_;

        // Adopt new resource
        data_ = new_data;
        len_ = other.len_;
        return *this;
    }

    ~SafeString() { delete[] data_; }

    void print() const {
        std::cout << data_ << " (len=" << len_ << ")\n";
    }
};

int main() {
    SafeString a{"Hello"};
    SafeString b = a;     // deep copy: b gets its own buffer
    SafeString c{"World"};
    c = a;                // deep copy assignment
    a.print();  // Hello (len=5)
    b.print();  // Hello (len=5)
    c.print();  // Hello (len=5)
    return 0;
}

拷贝并交换惯用法

拷贝并交换惯用法提供了一种优雅的、异常安全的拷贝赋值运算符实现。它复用拷贝构造函数和 swap 函数,以最少的代码重复提供强异常安全保证。

copy_and_swap.cpp
#include <iostream>
#include <cstring>
#include <utility>

class Buffer {
    char* data_;
    std::size_t size_;

public:
    explicit Buffer(std::size_t size)
        : size_{size}, data_{new char[size]{}} {}

    Buffer(const Buffer& other)
        : size_{other.size_}, data_{new char[other.size_]}
    {
        std::memcpy(data_, other.data_, size_);
    }

    ~Buffer() { delete[] data_; }

    // Copy-and-swap: take parameter by VALUE (invokes copy ctor)
    Buffer& operator=(Buffer other) {   // copy happens here
        swap(*this, other);             // swap resources
        return *this;
    }   // 'other' destroyed here, freeing our old resource

    // Non-throwing swap — essential for strong exception safety
    friend void swap(Buffer& a, Buffer& b) noexcept {
        using std::swap;
        swap(a.data_, b.data_);
        swap(a.size_, b.size_);
    }

    char& operator[](std::size_t i) { return data_[i]; }
    std::size_t size() const { return size_; }
};

int main() {
    Buffer a{5};
    a[0] = 'H'; a[1] = 'i';

    Buffer b{10};
    b = a;  // copy-and-swap: exception-safe, self-assignment-safe

    std::cout << b[0] << b[1] << " (size " << b.size() << ")\n";
    return 0;
}

禁用拷贝

某些类型不应该是可拷贝的 ── 例如,表示唯一资源的类型,如文件句柄、网络连接或线程。使用 =delete 来清晰地传达并强制执行这一点:

class UniqueConnection {
public:
    UniqueConnection(const UniqueConnection&) = delete;
    UniqueConnection& operator=(const UniqueConnection&) = delete;
    // Move operations can still be enabled
};

在 C++11 之前,这通过将拷贝操作声明为 private 且不提供定义来实现 ── 这是一种令人困惑的做法,=delete 优雅地取代了它。

常见陷阱

一种朴素的拷贝赋值实现 ── 先删除旧资源再分配新资源 ── 在两方面是有缺陷的:(1)自赋值(a = a)会先删除数据然后尝试从中拷贝,(2)如果 new 在 delete 之后抛出异常,对象会处于无效状态。拷贝并交换惯用法解决了这两个问题:拷贝在任何修改之前完成,而 swap 是 noexcept 的。

最佳实践

避免拷贝错误的最佳方式是永远不要直接管理原始资源。使用 std::string 而不是 char*,使用 std::vector 而不是原始数组,使用 std::unique_ptr 而不是拥有所有权的原始指针。当你的所有成员都有正确的拷贝语义时,编译器生成的拷贝操作自动就是正确的。这就是零法则在实践中的体现。

关键要点
  • 编译器生成的拷贝是浅拷贝 ── 它拷贝指针的值,而不是指向的数据
  • 拥有资源的类需要深拷贝构造函数和赋值运算符
  • 拷贝并交换惯用法免费提供异常安全和自赋值安全
  • 使用 =delete 显式禁止唯一资源类型的拷贝
  • 最佳策略是零法则:使用 RAII 封装器,这样你就永远不需要自定义拷贝逻辑

测验 — 检验你的知识

(15 经验值)

1. 当你对一个通过原始指针拥有堆内存的对象进行浅拷贝时,会发生什么?

2. 为什么拷贝并交换惯用法按值传递参数?

3. 推荐使用哪种方法来完全避免编写自定义拷贝操作?