C++ 三之法则、五之法则和零之法则

1、三之法则

如果一个类需要显式定义以下三个特殊成员函数中的任意一个,通常需要同时定义全部三个:

  1. 析构函数(Destructor):释放资源(如 delete 动态内存)。
  2. 拷贝构造函数(Copy Constructor):定义深拷贝逻辑,避免多个对象共享同一资源。
  3. 拷贝赋值运算符(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 及更高版本,引入移动语义后。

核心规则:如果一个类需要显式定义以下五个特殊成员函数中的任意一个,通常需要同时定义全部五个:

  1. 析构函数(同三法则)。
  2. 拷贝构造函数(同三法则)。
  3. 拷贝赋值运算符(同三法则)。
  4. 移动构造函数(Move Constructor):通过 “窃取” 资源(如转移指针所有权)避免深拷贝。
  5. 移动赋值运算符(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::vectorstd::stringstd::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::stringstd::vector的深拷贝确保资源不冲突;
  • 移动时,资源高效转移,避免冗余拷贝;
  • 析构时,成员的析构函数自动释放资源,无内存泄漏。

3.2 与 “资源管理类” 的配合

关键是 “将资源管理委托给专用类”。例如,若需要管理动态内存,应使用std::unique_ptrstd::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) 拷贝构造、拷贝赋值、移动赋值、析构函数 浅移动(转移资源所有权)
发表评论

评论已关闭。

相关文章