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

1、三之法则

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

  1. 析构函数 (Destructor):释放资源(如 delete 动态内存)。
  2. 拷贝构造函数(Copy Constructor):定义深拷贝逻辑,避免多个对象共享同一资源。
  3. 拷贝赋值运算符(Copy Assignment Operator):处理赋值时的资源释放和深拷贝。

1.1 典型问题

若仅定义析构函数而未定义拷贝操作,默认的浅拷贝会导致两个对象共享同一资源。例如:

cpp 复制代码
class Bad {
public:
    Bad() : data(new int(0)) {}
    ~Bad() { delete data; } // 析构函数释放内存
    // 未定义拷贝构造函数和赋值运算符
};

Bad a, b = a; // 默认浅拷贝,a.data 和 b.data 指向同一内存
// 析构时两次 delete 同一地址,导致未定义行为

1.2 解决方案

显式定义三个函数,确保资源深拷贝和正确释放:

cpp 复制代码
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):高效转移资源而非复制。

典型优化:移动语义允许将临时对象的资源直接转移,避免不必要的深拷贝

cpp 复制代码
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 之法则。

实现方式:将资源封装在具有完整语义的成员对象中,利用其自动生成的特殊成员函数。

cpp 复制代码
#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,而非裸指针:

cpp 复制代码
#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 编译器生成默认构造函数的条件

当类没有任何用户定义的构造函数时,编译器自动生成 "合成默认构造函数"。

示例

cpp 复制代码
#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 编译器不生成默认构造函数的情况

用户定义了任何构造函数(哪怕是带参数的)

cpp 复制代码
#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 显式要求编译器生成默认构造函数:

cpp 复制代码
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;
}

类成员 / 基类没有默认构造函数

若类的成员或基类是 "无默认构造函数的类型",编译器无法生成合成默认构造函数(因无法初始化该成员 / 基类)。

cpp 复制代码
#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 成员:

cpp 复制代码
class A {
public:
    // 用户定义构造函数,显式初始化B成员
    A() : b(10) {}  // 给B传参,调用B的带参构造函数
private:
    B b;
};

int main() {
    A a;  // 正确:A的构造函数显式初始化B
    return 0;
}
4.1.3 显式禁止默认构造函数(=delete)

若希望禁止类的默认初始化(如单例模式),可显式删除默认构造函数:

cpp 复制代码
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 编译器生成拷贝构造函数的条件

当类没有用户定义的拷贝构造函数时,编译器自动生成 "合成拷贝构造函数"。

示例

cpp 复制代码
#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 合成拷贝构造函数的隐患

若类包含指针成员 (指向动态内存),合成拷贝构造函数的浅拷贝会导致两个对象的指针指向同一块内存,析构时触发 "双重释放"(未定义行为,通常导致程序崩溃)。

cpp 复制代码
#include <iostream>
#include <cstring>
using namespace std;

class String {
public:
    // 带参构造函数:动态分配内存
    String(const char* str) {
        len = strlen(str);
        buf = new char[len + 1];  // 分配内存(+1存'\0')
        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;
}

解决方案:

用户定义拷贝构造函数(深拷贝)

通过自定义拷贝构造函数,为新对象重新分配内存,避免指针指向同一块地址:

cpp 复制代码
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 编译器不生成 / 删除拷贝构造函数的情况

用户定义了拷贝构造函数: 编译器不再生成合成版本,完全依赖用户定义的逻辑:

cpp 复制代码
class A {
public:
    A() {}
    // 用户定义拷贝构造函数
    A(const A& other) {
        cout << "自定义拷贝构造函数" << endl;
    }
};

int main() {
    A a1;
    A a2 = a1;  // 调用用户定义的拷贝构造函数 → 输出:自定义拷贝构造函数
    return 0;
}

定义了移动构造 / 移动赋值运算符: 为避免浅拷贝与移动语义冲突,编译器会隐式删除合成拷贝构造函数:

cpp 复制代码
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 编译器生成移动构造函数的条件

当类没有用户定义的拷贝构造函数、拷贝赋值运算符、移动赋值运算符或析构函数时,编译器自动生成 "合成移动构造函数"。

cpp 复制代码
#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 编译器不生成移动构造函数的情况

用户定义了拷贝构造 / 拷贝赋值 / 移动赋值 / 析构

编译器认为用户可能需要自定义资源管理逻辑,因此不生成合成移动构造函数:

cpp 复制代码
#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;
}

类成员 / 基类无法移动

若成员或基类是 "无移动构造函数且无拷贝构造函数" 的类型,编译器会隐式删除合成移动构造函数:

cpp 复制代码
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 自定义移动构造函数(解决指针成员的移动)

若类包含指针成员,需自定义移动构造函数,转移资源所有权后将源对象指针置空(避免析构冲突):

cpp 复制代码
#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) 拷贝构造、拷贝赋值、移动赋值、析构函数 浅移动(转移资源所有权)
相关推荐
感哥1 天前
C++ 迭代器
c++
tkevinjd2 天前
C++线程池学习 Day07
c++
TangHao19872 天前
第一章 基础(Chapter 1 fundentals)
c++
沐怡旸2 天前
【底层机制】std::move 解决的痛点?是什么?如何实现?如何正确用?
c++·面试
tongsound3 天前
ros2 humble slam仿真环境搭建(turtlebot3 & Gazebo)
c++·docker
沐怡旸4 天前
【底层机制】std::weak_ptr解决的痛点?是什么?如何实现?如何正确用?
c++·面试
River4164 天前
Javer 学 c++(十六):对象特性篇(上)
c++·后端
感哥4 天前
C++ 左值、右值、左值引用、右值引用
c++
感哥4 天前
C++ 模板
c++