C++: 拷贝控制

最后更新:2026-08-26

第29课我们学了构造函数进阶。

但如果你要拷贝对象赋值对象移动对象,C++ 有一套拷贝控制规则

理解这套规则,才能写出正确的类。


1. 三五法则(Rule of Three/Five)

(1) 1.1 什么是三五法则?

如果你的类需要自定义以下任何一个函数,那么你必须自定义所有相关的函数:

版本 需要管的函数 说明
三法则(C++98) 1. 析构函数br2. 拷贝构造函数br3. 拷贝赋值运算符 有动态内存时需要
五法则(C++11) 三法则 +br4. 移动构造函数br5. 移动赋值运算符 C++11 引入移动语义

(2) 1.2 为什么要有三五法则?

如果你需要自定义其中一个,说明类里有需要特殊处理的资源(如动态内存、文件句柄)。

示例:不遵循三法则的后果(难度⭐⭐)

▶ 示例 2:代码示例(难度⭐)

CPP
#include <iostream>
#include <cstring>

class BadString {
private:
 char* data;
 
public:
 // 构造函数
 BadString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // ❌ 没有自定义拷贝构造函数、拷贝赋值运算符、析构函数
 // 编译器会生成默认的(浅拷贝)
};

int main() {
 BadString s1("Hello");
 BadString s2 = s1; // ❌ 浅拷贝:s1.data 和 s2.data 指向同一块内存
 
 // main 结束时,s2 先析构(释放 data)
 // 然后 s1 析构(再释放 data,但已经释放过了)→ 崩溃!
 
 return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
Hello
Hello

后果: 程序崩溃(重复释放同一块内存)。

修复: 遵循三法则,自定义所有三个函数。



2. 拷贝赋值运算符*

(1) 2.1 基本用法

CPP
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // 构造函数
 MyString(const char* s) {
 data = new char[strlen(s) + 1];
 strcpy(data, s);
 }
 
 // 拷贝构造函数
 MyString(const MyString& other) {
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 
 // 拷贝赋值运算符
 MyString& operator=(const MyString& other) {
 if (this != &other) { // 防止自赋值
 delete data; // 释放旧内存
 data = new char[strlen(other.data) + 1];
 strcpy(data, other.data);
 }
 return *this;
 }
 
 // 析构函数
 ~MyString() {
 delete data;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2("World");
 
 s2 = s1; // ✅ 调用拷贝赋值运算符
 
 return 0;
}

💡 重点: 拷贝赋值运算符要检查自赋值if (this != &other))。



3. 移动语义(C++11)*

(1) 3.1 为什么需要移动?

如果你有一个临时对象(马上就要销毁了),拷贝它的内容很浪费——不如直接"偷"它的资源

示例:用移动避免不必要的拷贝(难度⭐⭐)

CPP
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 
public:
 // 移动构造函数(C++11)
 MyString(MyString&& other) noexcept {
 data = other.data; // 偷资源
 other.data = nullptr; // 把 other 设为空
 }
 
 // 移动赋值运算符(C++11)
 MyString& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data; // 释放旧资源
 data = other.data; // 偷资源
 other.data = nullptr; // 把 other 设为空
 }
 return *this;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = std::move(s1); // ✅ 调用移动构造函数
 // 现在 s1.data 是 nullptr,s2.data 指向原来的内存
 
 return 0;
}

💡 提示: std::move() 把左值转换成右值引用,触发移动构造函数或移动赋值运算符。



4. = default 和 = delete*

(1) 4.1 用 = default 让编译器生成默认实现*

如果你的类不需要特殊拷贝逻辑,可以用 = default 让编译器生成默认的。

CPP
#include <iostream>
#include <string>

class Student {
private:
 std::string name;
 int age;
 
public:
 // 让编译器生成默认的拷贝构造函数、拷贝赋值运算符、析构函数
 Student(const Student& other) = default;
 Student& operator=(const Student& other) = default;
 ~Student() = default;
};

int main() {
 Student s1;
 Student s2 = s1; // ✅ 调用默认的拷贝构造函数
 
 return 0;
}

💡 提示: 如果类里只有 STL 类型(std::stringstd::vector 等),用 = default 就行。


(2) 4.2 用 = delete 禁止拷贝*

如果你不想让类被拷贝(如单例模式),可以用 = delete 禁止。

CPP
#include <iostream>

class Singleton {
private:
 Singleton() {} // 构造函数私有化
 
public:
 // 禁止拷贝
 Singleton(const Singleton&) = delete;
 Singleton& operator=(const Singleton&) = delete;
 
 static Singleton& getInstance() {
 static Singleton instance;
 return instance;
 }
};

int main() {
 Singleton& s = Singleton::getInstance();
 // Singleton s2 = s; // ❌ 编译报错:拷贝构造函数已被删除
 
 return 0;
}


5. 实战:完整的 MyString 类*

▶ 示例 1:实现完整的 MyString 类(难度⭐⭐⭐)

CPP 📖 仅展示
#include <iostream>
#include <cstring>

class MyString {
private:
 char* data;
 int length;
 
public:
 // 构造函数
 MyString(const char* s = "") {
 length = strlen(s);
 data = new char[length + 1];
 strcpy(data, s);
 }
 
 // 拷贝构造函数
 MyString(const MyString& other) {
 length = other.length;
 data = new char[length + 1];
 strcpy(data, other.data);
 }
 
 // 移动构造函数
 MyString(MyString&& other) noexcept {
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 
 // 拷贝赋值运算符
 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& operator=(MyString&& other) noexcept {
 if (this != &other) {
 delete data;
 data = other.data;
 length = other.length;
 other.data = nullptr;
 other.length = 0;
 }
 return *this;
 }
 
 // 析构函数
 ~MyString() {
 delete data;
 }
 
 // 其他成员函数
 void print() const {
 if (data) std::cout << data;
 }
 
 int size() const {
 return length;
 }
};

int main() {
 MyString s1("Hello");
 MyString s2 = s1; // 拷贝构造函数
 
 s1.print(); // Hello
 std::cout << std::endl;
 
 s2.print(); // Hello
 std::cout << std::endl;
 
 return 0;
}
逻辑代码 59 行(超过 40 行限制,仅展示)

输出:

TEXT 📖 仅展示
(程序输出)

▶ 示例 3:深拷贝示例(难度⭐)

CPP
#include <iostream>

class Buffer {
    int* data;
    int size;
public:
    Buffer(int s) : size(s), data(new int[s]) {}
    ~Buffer() { delete[] data; }

    // 深拷贝构造函数
    Buffer(const Buffer& other) : size(other.size), data(new int[other.size]) {
        for (int i = 0; i < size; i++) data[i] = other.data[i];
    }

    int& operator[](int i) { return data[i]; }
};

int main() {
    Buffer b1(3);
    b1[0] = 1; b1[1] = 2; b1[2] = 3;

    Buffer b2 = b1; // 深拷贝
    b2[0] = 100;

    std::cout << "b1[0]=" << b1[0] << ", b2[0]=" << b2[0] << std::endl;

    return 0;
}
▶ 试一试

输出:

TEXT 📖 仅展示
b1[0]=1, b2[0]=100
💡 提示:深拷贝会复制指针指向的内容,而不是只复制指针值。修改副本不影响原对象。


Q:什么时候该自定义拷贝构造函数? A:当类里有指针成员(指向动态内存)时。

如果类里只有基本类型(intdouble)或 STL 类型(std::stringstd::vector),不用自定义——编译器自动生成的就能正确工作(STL 类型自己实现了深拷贝)。

Q:=default 和 =delete 有什么用? A:> - = default:让编译器生成默认的实现(逐成员拷贝) > - = delete:禁止某个函数(如禁止拷贝)

推荐用 = default 代替手写拷贝逻辑(如果不需要特殊逻辑)。

Q:如果不写拷贝构造函数,会怎样? A:编译器会自动生成一个默认拷贝构造函数(逐成员拷贝)。

但如果类里有指针成员,默认的会做浅拷贝(只拷贝指针的值,不拷贝指向的内容),导致重复释放内存。



❓ 常见问题

Q 什么时候需要自定义拷贝构造函数?
A 当类管理动态内存或资源时(如指针成员),必须自定义深拷贝,否则默认的浅拷贝会导致重复释放。
Q 移动构造函数和拷贝构造函数有什么区别?
A 拷贝构造函数会复制资源(深拷贝),移动构造函数转移资源所有权(浅拷贝),效率更高。

📖 小节


📝 作业

  1. 基础题 (Difficulty ⭐): 定义一个 Point 类,包含 x、y 两个成员。

  2. 遵循三法则,自定义拷贝构造函数、拷贝赋值运算符、析构函数

  3. main 里测试拷贝

  4. 进阶题 (Difficulty ⭐⭐): 定义一个 DynamicArray 类(动态数组),包含:

  5. int* data(指向堆上的数组)

  6. int size(数组大小)

遵循五法则,实现所有特殊的成员函数。

  1. 挑战题 (Difficulty ⭐⭐⭐): 扩展 DynamicArray 类,添加:
  2. void push_back(int x):往数组末尾添加元素(如果空间不够,扩容为原来的2倍)
  3. void pop_back():删除末尾元素

6. 🚀 下一步*

学会了拷贝控制和移动语义,接下来我们学习 继承(第31课)—— 让类之间能形成"父子"关系,实现代码复用!

Web-Tutorial.com

Web-Tutorial 技术团队

由多位开发者共同维护的编程教程平台。每篇教程由对应领域的开发者编写和审核,确保内容准确可靠。如发现任何问题,欢迎向我们反馈。

100%

🙏 帮我们做得更好

我们是刚上线的编程教程站,几个人的小团队,精力有限。页面虽经检查,难免还有疏漏——链接失效、排版错乱、内容有误、语言生硬……

如果您发现了,麻烦告诉我们,我们会在收到反馈后第一时间进行修复,再次感谢您的光临 🙏