Skip to content

类的拷贝

第一章 类的拷贝基础

1.1 默认构造函数

默认构造函数是当你不提供任何构造函数时,编译器自动生成的构造函数。它分为以下几种情况:

1.1.1 隐式生成的默认构造 🏭

cpp
class MyClass {
public:
    int value;
    double data;
};

MyClass obj;  // 调用隐式默认构造,value 和 data 未初始化(垃圾值)

1.1.2 编译器生成的条件

只有当你没有声明任何构造函数时,编译器才会生成隐式默认构造。如果你声明了带参数的构造函数但没有默认构造函数,则 MyClass obj; 会编译失败。

cpp
class MyClass {
public:
    int value;
    MyClass(int v) : value(v) {}  // 只声明了有参构造
};

MyClass obj;  // ❌ 编译错误:没有默认构造函数
MyClass obj(10);  // ✅ 正确

1.1.3 默认成员初始化器 (C++11)

cpp
class MyClass {
public:
    int value = 0;           // 默认值为 0
    double data{1.0};        // 默认值为 1.0
    std::string name = "unnamed";
};

MyClass obj;  // ✅ value=0, data=1.0, name="unnamed"

1.2 拷贝构造函数 🔄

拷贝构造函数用于创建一个新对象,作为已有对象的副本。

1.2.1 拷贝构造的形式

cpp
class MyClass {
public:
    int value;
    std::string name;

    // 参数必须是 const 引用,避免无限递归
    MyClass(const MyClass& other) : value(other.value), name(other.name) {}
};

1.2.2 拷贝构造的调用时机

cpp
MyClass obj1(10, "Alice");

MyClass obj2(obj1);       // ① 直接初始化
MyClass obj3 = obj1;      // ② 拷贝初始化(可能触发隐式拷贝)
MyClass obj4(obj1);       // ③ 作为函数参数(按值传递)

注意

按值传递参数时,会调用拷贝构造函数。在某些性能敏感的场景下,可以考虑使用 const& 引用传递来避免不必要的拷贝。

cpp
void func(MyClass obj) {}      // 会调用拷贝构造
void func(const MyClass& obj) {} // 不会调用拷贝构造,更推荐

1.2.3 隐式拷贝构造

如果你没有声明拷贝构造,编译器会生成一个浅拷贝的隐式拷贝构造。

cpp
class MyClass {
public:
    int value;
    double data;
};

MyClass obj1;
obj1.value = 10;

MyClass obj2 = obj1;  // 调用隐式浅拷贝构造,obj2.value == 10

1.3 浅拷贝 vs 深拷贝 ⚖️

这是理解类拷贝的关键概念。

1.3.1 浅拷贝 (Shallow Copy)

浅拷贝只拷贝成员变量的(对于指针,只拷贝指针的地址,不拷贝指针指向的数据)。

cpp
#include <cstring>
#include <iostream>

class MyString {
public:
    char* data;
    int length;

    MyString(const char* str) {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 编译器生成的隐式拷贝构造是浅拷贝!
    // 危险:obj2 的 data 指针指向 obj1 的同一块内存

    ~MyString() {
        delete[] data;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2 = s1;  // 浅拷贝:s2.data 和 s1.data 指向同一块内存

    std::cout << s1.data << std::endl;  // Hello
    std::cout << s2.data << std::endl;  // Hello(同一块内存!)

    // ❌ 双重删除!s1 和 s2 析构时都会 delete[] data
    return 0;
}

1.3.2 深拷贝 (Deep Copy)

深拷贝不仅拷贝指针,还拷贝指针指向的实际数据。

cpp
#include <cstring>
#include <iostream>

class MyString {
public:
    char* data;
    int length;

    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 自定义拷贝构造:深拷贝
    MyString(const MyString& other) : length(other.length) {
        data = new char[length + 1];
        strcpy(data, other.data);
    }

    ~MyString() {
        delete[] data;
    }
};

int main() {
    MyString s1("Hello");
    MyString s2 = s1;  // 深拷贝:s2.data 是独立的副本

    std::cout << s1.data << std::endl;  // Hello
    std::cout << s2.data << std::endl;  // Hello

    // ✅ 安全!两个对象各自拥有独立的内存
    return 0;
}

1.3.3 对比表格

特性浅拷贝深拷贝
拷贝内容只拷贝指针值(地址)拷贝指针指向的数据
内存情况多个指针指向同一块内存每个对象有独立内存
析构风险⚠️ 双重删除风险✅ 安全
性能快(仅拷贝指针)较慢(需分配新内存)

1.4 拷贝赋值运算符 📋

拷贝赋值运算符 operator= 用于将一个已存在对象的值赋给另一个对象。

1.4.1 默认拷贝赋值运算符

类似于拷贝构造,如果没有自定义,编译器会生成浅拷贝版本的拷贝赋值运算符。

cpp
class MyString {
public:
    char* data;
    int length;

    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 拷贝赋值运算符
    MyString& operator=(const MyString& other) {
        if (this != &other) {  // 防止自赋值
            delete[] data;      // 释放原有内存
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    ~MyString() {
        delete[] data;
    }
};

1.4.2 自赋值检查

编写拷贝赋值运算符时,必须检查自赋值情况:

cpp
MyString s("Hello");
s = s;  // 自赋值!如果没有检查,会先 delete[] data,导致问题

1.5 移动语义 (C++11) 🚀

移动语义是 C++11 引入的重要特性,用于避免不必要的拷贝。

1.5.1 移动构造函数

cpp
class MyString {
public:
    char* data;
    int length;

    MyString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 移动构造函数
    MyString(MyString&& other) noexcept : data(other.data), length(other.length) {
        other.data = nullptr;  // 防止析构时 delete[]
        other.length = 0;
    }

    ~MyString() {
        delete[] data;
    }
};

1.5.2 移动赋值运算符

cpp
MyString& operator=(MyString&& other) noexcept {
    if (this != &other) {
        delete[] data;
        data = other.data;
        length = other.length;
        other.data = nullptr;
        other.length = 0;
    }
    return *this;
}

1.5.3 std::move

使用 std::move 可以将左值转换为右值引用,触发移动语义:

cpp
MyString s1("Hello");
MyString s2 = std::move(s1);  // 调用移动构造,s2 获得资源,s1 变为空

1.6 五大构造/赋值函数的优先级 ⚡

这是非常重要的知识点,理解优先级能避免潜在的 bug。

1.6.1 编译器生成的条件

函数编译器自动生成的条件
默认构造未声明任何构造函数
拷贝构造未声明拷贝构造
拷贝赋值未声明拷贝赋值
移动构造未声明拷贝构造且未声明移动操作
移动赋值未声明拷贝赋值且未声明移动操作

重要规则

一旦你声明了任何拷贝构造(包括 = default),编译器就不会自动生成移动构造。反之亦然:声明了移动构造,编译器也不会生成拷贝构造。

这意味着:

cpp
class MyClass {
public:
    MyClass(const MyClass&) = default;  // 显式声明拷贝构造
    // 编译器不会生成隐式移动构造!
};

1.6.2 调用时的优先级规则

当同时存在多个构造函数可供调用时:

拷贝构造移动构造传递方式调用
MyClass obj(other)拷贝构造(左值优先拷贝
MyClass obj(std::move(other))移动构造(右值优先移动
MyClass obj(other)拷贝构造(退化为拷贝)
MyClass obj(other)移动构造(退化)
cpp
class MyClass {
public:
    int value;

    MyClass() : value(0) {}
    MyClass(const MyClass&) : value(0) { std::cout << "copy\n"; }
    MyClass(MyClass&&) noexcept : value(0) { std::cout << "move\n"; }
};

MyClass obj1;
MyClass obj2 = obj1;        // 输出 "copy"(左值调用拷贝构造)
MyClass obj3 = std::move(obj1);  // 输出 "move"(右值调用移动构造)

1.6.3 ⚠️ 常见陷阱:声明默认拷贝导致移动失效

你遇到的 bug 正是这个:

cpp
class MyClass {
public:
    int* ptr;
    int newValue;  // 后来新增的字段

    MyClass() : ptr(nullptr), newValue(0) {}

    // 你以为这样声明没事
    MyClass(const MyClass&) = default;  // ❌ 危险!

    // 但新增的字段不会被移动!
};

int main() {
    MyClass obj1;
    obj1.newValue = 100;

    MyClass obj2 = std::move(obj1);  // 调用移动构造!
    // newValue 没有被移动,只是一个未定义的垃圾值!
}

1.6.4 正确做法

方案一:如果不需要移动,确保拷贝完整

cpp
class MyClass {
public:
    int* ptr;
    int newValue;

    MyClass() : ptr(nullptr), newValue(0) {}

    // ✅ 手动实现,确保每个字段都被拷贝
    MyClass(const MyClass& other)
        : ptr(other.ptr ? new int(*other.ptr) : nullptr),
          newValue(other.newValue) {}
};

方案二:如果需要移动,一并实现

cpp
class MyClass {
public:
    int* ptr;
    int newValue;

    MyClass() : ptr(nullptr), newValue(0) {}

    MyClass(const MyClass& other)
        : ptr(other.ptr ? new int(*other.ptr) : nullptr),
          newValue(other.newValue) {}

    // ✅ 同时实现移动,确保新增字段也被正确移动
    MyClass(MyClass&& other) noexcept
        : ptr(other.ptr), newValue(other.newValue) {
        other.ptr = nullptr;
        // other.newValue 不需要处理,因为是值类型
    }
};

方案三:使用 = default 前确认没有遗漏

cpp
class MyClass {
public:
    int value;           // ✅ 只有简单成员,用 = default 是安全的
    double data;
    std::string name;

    MyClass(const MyClass&) = default;  // ✅ 编译器会正确处理
};

1.6.5 推荐使用场景

场景推荐做法
简单 POD 类型(int、double、string 等)= default 让编译器生成
有原始指针,需要深拷贝自定义拷贝构造和拷贝赋值
有资源(文件、socket、锁)自定义析构 + 五大函数全部显式定义
性能敏感,避免不必要的拷贝同时实现移动构造和移动赋值
使用智能指针管理资源= default 拷贝/移动,或直接用 = default

推荐

现代 C++ 中,优先使用智能指针std::unique_ptrstd::shared_ptr)和 STL 容器管理资源。它们已经实现了正确的拷贝/移动语义,使用 = default 即可。


1.7 总结:第一类知识点回顾 📝

cpp
class MyClass {
    int value;
    std::string name;
    int* ptr;

public:
    MyClass() = default;  // 默认构造

    MyClass(int v, const std::string& n);  // 带参构造

    // 方式一:简单类型用 = default
    MyClass(const MyClass&) = default;
    MyClass& operator=(const MyClass&) = default;

    // 方式二:需要深拷贝时手动实现
    MyClass(const MyClass& other) : value(other.value), name(other.name) {
        ptr = new int(*other.ptr);  // 深拷贝
    }

    MyClass& operator=(const MyClass& other) {
        if (this != &other) {
            delete ptr;
            value = other.value;
            name = other.name;
            ptr = new int(*other.ptr);  // 深拷贝
        }
        return *this;
    }

    // 如果需要移动语义,也要一并实现!
    MyClass(MyClass&& other) noexcept
        : value(other.value), name(std::move(other.name)), ptr(other.ptr) {
        other.ptr = nullptr;
    }

    MyClass& operator=(MyClass&& other) noexcept {
        if (this != &other) {
            delete ptr;
            value = other.value;
            name = std::move(other.name);
            ptr = other.ptr;
            other.ptr = nullptr;
        }
        return *this;
    }

    ~MyClass() { delete ptr; }
};

第二章 基类和派生类的拷贝

2.1 基类与派生类的关系 🔗

派生类继承自基类,派生类对象包含基类部分。这意味着拷贝派生类对象时,需要考虑基类部分的拷贝。

cpp
class Base {
public:
    int baseValue;
};

class Derived : public Base {
public:
    int derivedValue;
};

2.2 派生类的拷贝构造 🏗️

2.2.1 默认行为

如果派生类没有自定义拷贝构造,编译器会:

  1. 调用基类的默认构造(如果存在)
  2. 对派生类的成员进行浅拷贝
cpp
class Base {
public:
    int baseValue;
    Base() : baseValue(0) {}
};

class Derived : public Base {
public:
    int derivedValue;
};

Derived d1;
d1.baseValue = 10;
d1.derivedValue = 20;

Derived d2 = d1;  // 调用基类默认构造 + 派生类浅拷贝
// d2.baseValue = 10, d2.derivedValue = 20

2.2.2 自定义派生类拷贝构造

关键点:派生类的拷贝构造必须显式调用基类的拷贝构造!

cpp
class Base {
public:
    int baseValue;
    Base() : baseValue(0) {}
    Base(const Base& other) : baseValue(other.baseValue) {}
};

class Derived : public Base {
public:
    int derivedValue;

    // 自定义拷贝构造:必须显式调用基类拷贝构造
    Derived(const Derived& other)
        : Base(other),           // ✅ 显式调用基类拷贝构造
          derivedValue(other.derivedValue) {}
};

常见错误

如果不显式调用基类拷贝构造,编译器会调用基类的默认构造,可能导致基类部分未被正确拷贝:

cpp
Derived(const Derived& other)
    : derivedValue(other.derivedValue)  // ❌ 基类部分调用默认构造!
{
    // baseValue 未被正确拷贝
}

2.3 派生类的拷贝赋值运算符 📋

2.3.1 基本写法

派生类的拷贝赋值运算符同样需要考虑基类部分:

cpp
class Base {
public:
    int baseValue;
    Base& operator=(const Base& other) {
        baseValue = other.baseValue;
        return *this;
    }
};

class Derived : public Base {
public:
    int derivedValue;

    Derived& operator=(const Derived& other) {
        if (this != &other) {
            Base::operator=(other);      // ✅ 调用基类拷贝赋值
            derivedValue = other.derivedValue;
        }
        return *this;
    }
};

2.3.2 注意事项

  1. 不要重复调用基类的拷贝赋值:确保基类部分只被赋值一次
  2. 自赋值检查:虽然基类可能有自赋值检查,但派生类也需要
  3. 异常安全:如果可能,遵循异常安全的原则

2.4 基类与派生类拷贝的差异 ⚡

2.4.1 向上转型与拷贝

将派生类拷贝给基类时会发生对象切片(Object Slicing):

cpp
class Base {
public:
    int baseValue;
    Base(int v = 0) : baseValue(v) {}
};

class Derived : public Base {
public:
    int derivedValue;
    Derived(int b = 0, int d = 0) : Base(b), derivedValue(d) {}
};

Derived d(10, 20);
Base b = d;  // ⚠️ 对象切片!只拷贝了 Base 部分,derivedValue 被切掉

2.4.2 防止对象切片

如果需要保留派生类的完整信息,使用指针或引用:

cpp
Derived d(10, 20);
Base* pb = &d;      // ✅ 指向派生类对象
Base& rb = d;        // ✅ 引用派生类对象

// 通过基类指针调用虚函数,可以保留派生类行为

2.5 多继承情况下的拷贝 🔱

多继承时,派生类的拷贝构造需要调用所有直接基类的拷贝构造:

cpp
class Base1 {
public:
    int value1;
    Base1(const Base1& other) : value1(other.value1) {}
};

class Base2 {
public:
    int value2;
    Base2(const Base2& other) : value2(other.value2) {}
};

class Derived : public Base1, public Base2 {
public:
    int derivedValue;

    Derived(const Derived& other)
        : Base1(other),      // ✅ 调用 Base1 拷贝构造
          Base2(other),      // ✅ 调用 Base2 拷贝构造
          derivedValue(other.derivedValue) {}
};

2.6 虚基类情况下的拷贝 (菱形继承) 💎

菱形继承结构中,虚基类只会有一个实例,需要特别注意:

cpp
class Base {
public:
    int value;
    Base(const Base& other) : value(other.value) {}
};

class Derived1 : virtual public Base {
public:
    int d1;
    Derived1(const Derived1& other) : Base(other), d1(other.d1) {}
};

class Derived2 : virtual public Base {
public:
    int d2;
    Derived2(const Derived2& other) : Base(other), d2(other.d2) {}
};

class FinalClass : public Derived1, public Derived2 {
public:
    int finalValue;

    FinalClass(const FinalClass& other)
        : Base(other),        // ✅ 虚基类,只调用一次
          Derived1(other),
          Derived2(other),
          finalValue(other.finalValue) {}
};

提示

虚基类在初始化列表中只出现一次,且由最终派生类负责初始化。


2.7 完整示例 📦

cpp
#include <iostream>
#include <cstring>

class BaseString {
protected:
    char* data;
    int length;

public:
    BaseString(const char* str = "") {
        length = strlen(str);
        data = new char[length + 1];
        strcpy(data, str);
    }

    // 基类拷贝构造(深拷贝)
    BaseString(const BaseString& other) : length(other.length) {
        data = new char[length + 1];
        strcpy(data, other.data);
    }

    // 基类拷贝赋值
    BaseString& operator=(const BaseString& other) {
        if (this != &other) {
            delete[] data;
            length = other.length;
            data = new char[length + 1];
            strcpy(data, other.data);
        }
        return *this;
    }

    virtual ~BaseString() {
        delete[] data;
    }

    void print() const {
        std::cout << data << std::endl;
    }
};

class DerivedString : public BaseString {
private:
    int refCount;  // 引用计数

public:
    DerivedString(const char* str = "") : BaseString(str), refCount(1) {}

    // 派生类拷贝构造
    DerivedString(const DerivedString& other)
        : BaseString(other),   // ✅ 显式调用基类拷贝构造
          refCount(other.refCount) {}

    // 派生类拷贝赋值
    DerivedString& operator=(const DerivedString& other) {
        if (this != &other) {
            BaseString::operator=(other);  // ✅ 调用基类拷贝赋值
            refCount = other.refCount;
        }
        return *this;
    }

    void addRef() { ++refCount; }
    void release() { --refCount; }
};

int main() {
    DerivedString s1("Hello");
    DerivedString s2 = s1;  // 调用派生类拷贝构造

    s1.print();  // Hello
    s2.print();  // Hello

    return 0;
}

2.8 第二章要点总结 🎯

要点说明
派生类拷贝构造必须显式调用 Base(other)
派生类拷贝赋值必须调用 Base::operator=(other)
对象切片派生类赋给基类时会切片,使用指针/引用避免
多继承每个直接基类都要在初始化列表中调用拷贝构造
虚基类只初始化一次,由最终派生类负责
深拷贝考虑基类有深拷贝资源时,派生类也要正确处理

2.9 最佳实践 💡

cpp
// ✅ 推荐:使用 = default 让编译器自动生成简单拷贝
class Simple {
    int value;
    std::string name;
public:
    Simple() = default;
    Simple(const Simple&) = default;
    Simple& operator=(const Simple&) = default;
};

// ✅ 推荐:需要深拷贝时,显式定义并正确调用基类
class Complex : public Base {
    int* ptr;
public:
    Complex() : ptr(new int(0)) {}
    Complex(const Complex& other) : Base(other), ptr(new int(*other.ptr)) {}
    Complex& operator=(const Complex& other) {
        if (this != &other) {
            Base::operator=(other);
            delete ptr;
            ptr = new int(*other.ptr);
        }
        return *this;
    }
};

// ✅ 推荐:考虑使用智能指针管理资源
class Smart : public Base {
    std::unique_ptr<int> ptr;
public:
    Smart() : ptr(std::make_unique<int>(0)) {}
    Smart(const Smart& other) : Base(other), ptr(std::make_unique<int>(*other.ptr)) {}
    Smart& operator=(const Smart& other) {
        if (this != &other) {
            Base::operator=(other);
            ptr = std::make_unique<int>(*other.ptr);
        }
        return *this;
    }
};

提示

现代 C++ 中,优先使用智能指针 (std::unique_ptrstd::shared_ptr) 来管理资源,可以大大简化拷贝/移动语义的实现。