1、三之法则
如果一个类需要显式定义以下三个特殊成员函数中的任意一个,通常需要同时定义全部三个:
- 析构函数(Destructor):释放资源(如
delete动态内存)。 - 拷贝构造函数(Copy Constructor):定义深拷贝逻辑,避免多个对象共享同一资源。
- 拷贝赋值运算符(Copy Assignment Operator):处理赋值时的资源释放和深拷贝。
1.1 典型问题
若仅定义析构函数而未定义拷贝操作,默认的浅拷贝会导致两个对象共享同一资源。例如:
class Bad { public: Bad() : data(new int(0)) {} ~Bad() { delete data; } // 析构函数释放内存 // 未定义拷贝构造函数和赋值运算符 }; Bad a, b = a; // 默认浅拷贝,a.data 和 b.data 指向同一内存 // 析构时两次 delete 同一地址,导致未定义行为
1.2 解决方案
显式定义三个函数,确保资源深拷贝和正确释放:
class Good { public: Good() : data(new int(0)) {} ~Good() { delete data; } Good(const Good& other) : data(new int(*other.data)) {} // 深拷贝 Good& operator=(const Good& other) { if (this != &other) { delete data; data = new int(*other.data); // 深拷贝 } return *this; } };
2、五之法则
C++11 及更高版本,引入移动语义后。
核心规则:如果一个类需要显式定义以下五个特殊成员函数中的任意一个,通常需要同时定义全部五个:
- 析构函数(同三法则)。
- 拷贝构造函数(同三法则)。
- 拷贝赋值运算符(同三法则)。
- 移动构造函数(Move Constructor):通过 “窃取” 资源(如转移指针所有权)避免深拷贝。
- 移动赋值运算符(Move Assignment Operator):高效转移资源而非复制。
典型优化:移动语义允许将临时对象的资源直接转移,避免不必要的深拷贝
class Efficient { public: Efficient() : data(new int(0)) {} ~Efficient() { delete data; } // 拷贝操作(深拷贝) Efficient(const Efficient& other) : data(new int(*other.data)) {} Efficient& operator=(const Efficient& other) { /* 同三法则 */ } // 移动操作(资源转移) Efficient(Efficient&& other) noexcept : data(other.data) { other.data = nullptr; // 置空源对象,防止重复释放 } Efficient& operator=(Efficient&& other) noexcept { if (this != &other) { delete data; data = other.data; other.data = nullptr; } return *this; } };
编译器行为:
- 若用户定义拷贝操作,编译器不会自动生成移动操作。
- 若用户定义移动操作,编译器会删除拷贝操作(标记为
=delete)。 - 若用户定义析构函数,编译器不会自动生成移动操作,可能导致意外的深拷贝。
3、零之法则
现代 C++(推荐优先使用)。
核心规则:尽量不手动定义任何特殊成员函数,而是通过 RAII(资源获取即初始化) 和标准库组件(如智能指针、容器)自动管理资源。
0 之法则的本质是 “资源管理与类的分离”:
- 类的职责应聚焦于 “业务逻辑”(如数据聚合、行为封装),而非 “资源管理”。
- 若类需要使用资源(如动态内存),应通过资源管理类(如
std::vector、std::string、std::unique_ptr)间接持有资源,而非自己管理。 - 由于资源管理类已正确实现了三 / 五法则,外层类无需干预,编译器生成的默认特殊成员函数会自动调用成员的对应函数(“逐成员操作”),行为正确。
3.1 适用场景与示例
适用场景
当类的所有成员都是 “自管理资源” 的类型(如内置类型、标准库容器、智能指针等),且类本身不直接持有需要手动释放的资源(如裸指针指向的动态内存、文件描述符)时,适用 0 之法则。
实现方式:将资源封装在具有完整语义的成员对象中,利用其自动生成的特殊成员函数。
#include <iostream> #include <string> #include <vector> // 遵循0之法则:不声明任何特殊成员函数 class Student { public: // 仅包含自管理资源的成员 std::string name; // string管理动态内存 int age; // 内置类型(无资源) std::vector<int> scores; // vector管理动态数组 }; int main() { Student s1{"Alice", 18, {90, 85, 95}}; // 1. 拷贝初始化(调用编译器生成的拷贝构造函数) Student s2 = s1; // s2.name、s2.scores均为s1的深拷贝(string和vector的拷贝是深拷贝) std::cout << "s2.name: " << s2.name << ", s2.scores[0]: " << s2.scores[0] << std::endl; // 2. 移动初始化(调用编译器生成的移动构造函数) Student s3 = std::move(s1); // 资源从s1转移到s3,s1.name和scores变为空 std::cout << "s3.name: " << s3.name << ", s1.name: " << s1.name << std::endl; // 3. 析构时:编译器生成的析构函数自动调用成员的析构函数(string和vector释放资源) return 0; }
Student类未声明任何特殊成员函数,但编译器自动生成的版本完全正确:
- 拷贝时,
std::string和std::vector的深拷贝确保资源不冲突; - 移动时,资源高效转移,避免冗余拷贝;
- 析构时,成员的析构函数自动释放资源,无内存泄漏。
3.2 与 “资源管理类” 的配合
关键是 “将资源管理委托给专用类”。例如,若需要管理动态内存,应使用std::unique_ptr或std::shared_ptr,而非裸指针:
#include <memory> // 正确:通过智能指针管理资源,遵循0之法则 class ResourceUser { public: // 使用unique_ptr管理动态内存(资源管理委托给智能指针) std::unique_ptr<int> data = std::make_unique<int>(42); }; int main() { ResourceUser u1; ResourceUser u2 = std::move(u1); // 调用编译器生成的移动构造,unique_ptr转移所有权 // u1.data变为nullptr,u2.data持有资源,析构时自动释放 return 0; }
分析:ResourceUser无需定义任何特殊成员函数:
std::unique_ptr已实现正确的移动语义(禁止拷贝,支持移动);- 编译器生成的移动构造函数会调用
unique_ptr的移动构造,实现资源安全转移; - 析构时,
unique_ptr的析构函数自动释放内存,无泄漏。
4、构造函数生成规则
4.1 默认构造函数
默认构造函数是无参数或 “所有参数均有默认值” 的构造函数,用于无初始化器的对象创建(如 A a;)。编译器仅在特定条件下生成,且行为受成员类型影响。
4.1.1 编译器生成默认构造函数的条件
当类没有任何用户定义的构造函数时,编译器自动生成 “合成默认构造函数”。
示例
#include <iostream> #include <string> using namespace std; // 类中无任何用户定义的构造函数 → 编译器生成合成默认构造函数 class Person { public: // 成员变量(无初始化列表) string name; // 类类型成员(string有默认构造函数) int age; // 内置类型成员 }; int main() { // 1. 局部对象:合成默认构造函数的行为 Person p1; // 调用编译器生成的默认构造函数 cout << "p1.name: " << p1.name << endl; // string默认初始化为空字符串 → 输出空 cout << "p1.age: " << p1.age << endl; // 内置类型局部对象未初始化 → 输出随机值(未定义行为) // 2. 全局对象:内置类型会默认初始化(区别于局部对象) static Person p2; // 全局/静态对象,内置类型成员会初始化为0 cout << "p2.name: " << p2.name << endl; // 空字符串 cout << "p2.age: " << p2.age << endl; // 0(全局对象特性) return 0; }
输出结果(局部对象 age 为随机值,全局对象 age 为 0):
p1.name: p1.age: 32767 // 随机值(不同环境可能不同) p2.name: p2.age: 0
关键说明:合成默认构造函数对成员的初始化规则:
- 类类型成员(如
string):调用其自身的默认构造函数; - 内置类型成员(如
int):局部对象中不初始化(值随机),全局 / 静态对象中初始化为 0; - 数组成员:对每个元素按上述规则初始化。
4.1.2 编译器不生成默认构造函数的情况
用户定义了任何构造函数(哪怕是带参数的)
#include <iostream> using namespace std; class Person { public: // 用户定义了带参数的构造函数 → 编译器不再生成默认构造函数 Person(string name, int age) : name(name), age(age) {} void print() { cout << "name: " << name << ", age: " << age << endl; } private: string name; int age; }; int main() { Person p1("Alice", 20); // 正确:调用用户定义的带参构造函数 p1.print(); // 输出:name: Alice, age: 20 // Person p2; // 错误:编译器未生成默认构造函数,无匹配的构造函数 return 0; }
解决方案:用 =default 显式要求编译器生成默认构造函数:
class Person { public: // 用户定义带参构造函数 Person(string name, int age) : name(name), age(age) {} // 显式要求编译器生成默认构造函数(C++11起) Person() = default; // 等价于编译器合成的默认构造函数 void print() { cout << "name: " << name << ", age: " << age << endl; } private: string name; int age; }; int main() { Person p2; // 正确:调用显式生成的默认构造函数 p2.print(); // 输出:name: , age: 32767(局部对象age随机) return 0; }
类成员 / 基类没有默认构造函数
若类的成员或基类是 “无默认构造函数的类型”,编译器无法生成合成默认构造函数(因无法初始化该成员 / 基类)。
#include <iostream> using namespace std; // 类B:无默认构造函数(只有带参构造函数) class B { public: B(int x) : val(x) {} // 无默认构造函数 private: int val; }; // 类A:包含B类型成员,且无用户定义的构造函数 class A { private: B b; // B无默认构造函数 → 编译器无法生成A的合成默认构造函数 }; int main() { // A a; // 错误:编译器无法初始化成员B(无默认构造函数) return 0; }
解决方案:用户定义 A 的构造函数,显式初始化 B 成员:
class A { public: // 用户定义构造函数,显式初始化B成员 A() : b(10) {} // 给B传参,调用B的带参构造函数 private: B b; }; int main() { A a; // 正确:A的构造函数显式初始化B return 0; }
4.1.3 显式禁止默认构造函数(=delete)
若希望禁止类的默认初始化(如单例模式),可显式删除默认构造函数:
class Singleton { public: // 显式删除默认构造函数 → 禁止默认初始化 Singleton() = delete; // 提供静态方法获取唯一实例 static Singleton& getInstance() { static Singleton instance; return instance; } }; int main() { // Singleton s; // 错误:默认构造函数已被删除 Singleton& s = Singleton::getInstance(); // 正确 return 0; }
4.2 拷贝构造函数
拷贝构造函数用于 “用已有对象初始化新对象”(如 A a = b; 或 A a(b);),原型通常为 A(const A& other)。编译器生成的合成拷贝构造函数默认执行浅拷贝(逐成员复制)。
4.2.1 编译器生成拷贝构造函数的条件
当类没有用户定义的拷贝构造函数时,编译器自动生成 “合成拷贝构造函数”。
示例
#include <iostream> #include <string> using namespace std; class Person { public: // 无用户定义的拷贝构造函数 → 编译器生成合成拷贝构造函数 Person(string name, int age) : name(name), age(age) {} void print() { cout << "name: " << name << ", age: " << age << " (地址: " << this << ")" << endl; } private: string name; // 类类型成员(string的拷贝构造是深拷贝) int age; // 内置类型成员(直接复制值) }; int main() { Person p1("Bob", 25); // 调用带参构造函数 Person p2 = p1; // 调用编译器生成的合成拷贝构造函数(浅拷贝) Person p3(p1); // 同上,拷贝初始化的另一种形式 p1.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff4e0) p2.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff4f8) → 新对象,值相同 p3.print(); // 输出:name: Bob, age: 25 (地址: 0x7ffeefbff510) → 新对象,值相同 return 0; }
关键说明:合成拷贝构造函数的 “浅拷贝” 逻辑:
- 对类类型成员(如
string):调用其自身的拷贝构造函数(string的拷贝是深拷贝,因此安全); - 对内置类型成员(如
int):直接复制其值; - 对数组成员:逐元素复制(若数组元素是内置类型,仍是浅拷贝)。
4.2.2 合成拷贝构造函数的隐患
若类包含指针成员(指向动态内存),合成拷贝构造函数的浅拷贝会导致两个对象的指针指向同一块内存,析构时触发 “双重释放”(未定义行为,通常导致程序崩溃)。
#include <iostream> #include <cstring> using namespace std; class String { public: // 带参构造函数:动态分配内存 String(const char* str) { len = strlen(str); buf = new char[len + 1]; // 分配内存(+1存' ') strcpy(buf, str); cout << "构造函数:分配内存 " << (void*)buf << endl; } // 析构函数:释放动态内存 ~String() { cout << "析构函数:释放内存 " << (void*)buf << endl; delete[] buf; // 释放内存 } void print() { cout << "buf: " << buf << " (地址: " << (void*)buf << ")" << endl; } private: char* buf; // 指针成员(指向动态内存) int len; }; int main() { String s1("Hello"); // 调用构造函数,分配内存 String s2 = s1; // 调用合成拷贝构造函数(浅拷贝:s2.buf = s1.buf) s1.print(); // 输出:buf: Hello (地址: 0x55f8d7a7a2a0) s2.print(); // 输出:buf: Hello (地址: 0x55f8d7a7a2a0) → 与s1.buf指向同一块内存 // 程序结束时:先析构s2,释放0x55f8d7a7a2a0;再析构s1,再次释放同一地址 → 双重释放 return 0; }
解决方案:
用户定义拷贝构造函数(深拷贝)
通过自定义拷贝构造函数,为新对象重新分配内存,避免指针指向同一块地址:
class String { public: String(const char* str) { len = strlen(str); buf = new char[len + 1]; strcpy(buf, str); cout << "构造函数:分配内存 " << (void*)buf << endl; } // 自定义拷贝构造函数(深拷贝) String(const String& other) { len = other.len; buf = new char[len + 1]; // 为新对象分配独立内存 strcpy(buf, other.buf); // 复制内容(而非指针) cout << "拷贝构造函数:分配内存 " << (void*)buf << endl; } ~String() { cout << "析构函数:释放内存 " << (void*)buf << endl; delete[] buf; } private: char* buf; int len; }; int main() { String s1("Hello"); // 构造:分配内存 0x55e7b9c6a2a0 String s2 = s1; // 拷贝构造:分配内存 0x55e7b9c6a2c0(独立内存) // 析构时分别释放两块内存,无双重释放 return 0; }
4.2.3 编译器不生成 / 删除拷贝构造函数的情况
用户定义了拷贝构造函数: 编译器不再生成合成版本,完全依赖用户定义的逻辑:
class A { public: A() {} // 用户定义拷贝构造函数 A(const A& other) { cout << "自定义拷贝构造函数" << endl; } }; int main() { A a1; A a2 = a1; // 调用用户定义的拷贝构造函数 → 输出:自定义拷贝构造函数 return 0; }
定义了移动构造 / 移动赋值运算符: 为避免浅拷贝与移动语义冲突,编译器会隐式删除合成拷贝构造函数:
class A { public: A() {} // 用户定义移动构造函数 A(A&& other) noexcept { cout << "自定义移动构造函数" << endl; } }; int main() { A a1; // A a2 = a1; // 错误:合成拷贝构造函数被隐式删除(因定义了移动构造) A a3 = std::move(a1); // 正确:调用移动构造函数 return 0; }
4.3 移动构造函数
移动构造函数用于 “将源对象的资源转移到新对象”(避免拷贝开销),原型通常为 A(A&& other) noexcept(右值引用参数)。编译器生成的合成移动构造函数默认执行浅移动(转移资源所有权)。
4.3.1 编译器生成移动构造函数的条件
当类没有用户定义的拷贝构造函数、拷贝赋值运算符、移动赋值运算符或析构函数时,编译器自动生成 “合成移动构造函数”。
#include <iostream> #include <string> using namespace std; // 类中无用户定义的拷贝构造、拷贝赋值、移动赋值、析构 → 编译器生成合成移动构造函数 class Person { public: Person(string name, int age) : name(name), age(age) { cout << "构造函数:" << name << endl; } void print() { cout << "name: " << name << ", age: " << age << endl; } private: string name; // string有移动构造函数(转移资源) int age; // 内置类型:移动等价于拷贝(无资源) }; int main() { Person p1("Charlie", 30); // 构造:Charlie // 用std::move将p1转为右值,触发合成移动构造函数 Person p2 = std::move(p1); cout << "p2: "; p2.print(); // 输出:name: Charlie, age: 30(资源转移到p2) cout << "p1: "; p1.print(); // 输出:name: , age: 30(p1的string被置空,age无资源) return 0; }
关键说明:合成移动构造函数的 “浅移动” 逻辑:
- 对类类型成员(如
string):调用其移动构造函数(转移资源,源对象成员被置空); - 对内置类型成员(如
int):直接复制值(因无资源所有权,移动与拷贝等价); - 对数组成员:逐元素移动(若元素是类类型且支持移动)。
4.3.2 编译器不生成移动构造函数的情况
用户定义了拷贝构造 / 拷贝赋值 / 移动赋值 / 析构
编译器认为用户可能需要自定义资源管理逻辑,因此不生成合成移动构造函数:
#include <iostream> #include <string> using namespace std; class Person { public: Person(string name) : name(name) {} // 用户定义了析构函数 → 编译器不生成移动构造函数 ~Person() { cout << "析构函数:" << name << endl; } private: string name; }; int main() { Person p1("David"); // 无移动构造函数,因此调用拷贝构造函数(而非移动) Person p2 = std::move(p1); // 等价于 Person p2 = p1; return 0; }
类成员 / 基类无法移动
若成员或基类是 “无移动构造函数且无拷贝构造函数” 的类型,编译器会隐式删除合成移动构造函数:
class B { public: B() {} // 删除移动构造和拷贝构造 B(const B&) = delete; B(B&&) = delete; }; class A { private: B b; // B无法移动 → A的合成移动构造函数被删除 }; int main() { A a1; // A a2 = std::move(a1); // 错误:A的移动构造函数被删除 return 0; }
4.3.3 自定义移动构造函数(解决指针成员的移动)
若类包含指针成员,需自定义移动构造函数,转移资源所有权后将源对象指针置空(避免析构冲突):
#include <iostream> #include <cstring> using namespace std; class String { public: String(const char* str) { len = strlen(str); buf = new char[len + 1]; strcpy(buf, str); cout << "构造:" << (void*)buf << endl; } // 自定义移动构造函数 String(String&& other) noexcept : buf(other.buf), len(other.len) { other.buf = nullptr; // 源对象指针置空,避免析构时重复释放 other.len = 0; cout << "移动构造:" << (void*)buf << endl; } ~String() { if (buf) { // 仅当buf非空时释放 cout << "析构:" << (void*)buf << endl; delete[] buf; } else { cout << "析构:空指针" << endl; } } private: char* buf; int len; }; int main() { String s1("Hello"); // 构造:0x55d8b7a8a2a0 String s2 = std::move(s1); // 移动构造:0x55d8b7a8a2a0(转移资源) // 析构s2:释放0x55d8b7a8a2a0;析构s1:buf为空,无释放 return 0; }
4.4 比较
| 构造函数类型 | 生成条件(无用户定义以下函数) | 关键行为 | 常见问题与解决方案 |
|---|---|---|---|
| 默认构造函数 | 任何构造函数 | 成员默认初始化(局部内置类型随机) | 需默认初始化时用=default;成员无默认构造时显式初始化 |
| 拷贝构造函数 | 拷贝构造函数 | 浅拷贝(逐成员复制) | 指针成员需自定义深拷贝,避免双重释放 |
| 移动构造函数(C++11) | 拷贝构造、拷贝赋值、移动赋值、析构函数 | 浅移动(转移资源所有权) |