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:代码示例(难度⭐)
#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;
}
输出:
Hello
Hello
后果: 程序崩溃(重复释放同一块内存)。
修复: 遵循三法则,自定义所有三个函数。
2. 拷贝赋值运算符*
(1) 2.1 基本用法
#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 为什么需要移动?
如果你有一个临时对象(马上就要销毁了),拷贝它的内容很浪费——不如直接"偷"它的资源。
示例:用移动避免不必要的拷贝(难度⭐⭐)
#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 让编译器生成默认的。
#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::string、std::vector 等),用 = default 就行。
(2) 4.2 用 = delete 禁止拷贝*
如果你不想让类被拷贝(如单例模式),可以用 = delete 禁止。
#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 类(难度⭐⭐⭐)
#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;
}
输出:
(程序输出)
▶ 示例 3:深拷贝示例(难度⭐)
#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;
}
输出:
b1[0]=1, b2[0]=100
Q:什么时候该自定义拷贝构造函数? A:当类里有指针成员(指向动态内存)时。
如果类里只有基本类型(
int、double)或 STL 类型(std::string、std::vector),不用自定义——编译器自动生成的就能正确工作(STL 类型自己实现了深拷贝)。
Q:=default 和 =delete 有什么用? A:> -
= default:让编译器生成默认的实现(逐成员拷贝) > -= delete:禁止某个函数(如禁止拷贝)推荐用
= default代替手写拷贝逻辑(如果不需要特殊逻辑)。
Q:如果不写拷贝构造函数,会怎样? A:编译器会自动生成一个默认拷贝构造函数(逐成员拷贝)。
但如果类里有指针成员,默认的会做浅拷贝(只拷贝指针的值,不拷贝指向的内容),导致重复释放内存。
❓ 常见问题
📖 小节
- 拷贝构造函数:用同类型对象初始化新对象
- 拷贝赋值运算符:= 运算符重载
- 三/五法则:若自定义一个则通常需要全部
- 浅拷贝 vs 深拷贝:指针成员必须深拷贝
- delete 函数阻止拷贝:= delete
📝 作业
-
基础题 (Difficulty ⭐): 定义一个
Point类,包含 x、y 两个成员。 -
遵循三法则,自定义拷贝构造函数、拷贝赋值运算符、析构函数
-
在
main里测试拷贝 -
进阶题 (Difficulty ⭐⭐): 定义一个
DynamicArray类(动态数组),包含: -
int* data(指向堆上的数组) -
int size(数组大小)
遵循五法则,实现所有特殊的成员函数。
- 挑战题 (Difficulty ⭐⭐⭐):
扩展
DynamicArray类,添加: void push_back(int x):往数组末尾添加元素(如果空间不够,扩容为原来的2倍)void pop_back():删除末尾元素
6. 🚀 下一步*
学会了拷贝控制和移动语义,接下来我们学习 继承(第31课)—— 让类之间能形成"父子"关系,实现代码复用!