深入理解C++三五零法则

三五零法则就是三法则(The Rule of Three)、五法则(The Rule of Five)、零法则(The Rule of Zero)。
三五零法则是和C++的特殊成员函数有关,特别是那些涉及对象如何被创建、复制、移动和销毁的函数。这些法则提供了指导原则,帮助开发者设计和实现那些管理资源(如动态内存、文件句柄等)的类。

特殊成员函数

析构函数

~X()

  • 调用每个类成员和基类的析构函数
  • 负责在对象生命周期结束时是否其占用的资源

拷贝构造函数

X(X const& other)

  • 调用每个类成员和基类的拷贝构造函数
  • 通过另一个同类型的现有对象来初始化新对象
  • 自定义对象如何被拷贝,是深拷贝还是浅拷贝

拷贝赋值运算符

X& operator=(X const& other)

  • 调用每个类成员和基类的拷贝赋值运算符
  • 通过另一个同类型的现有对象赋值给自己

移动构造函数

X(X&& other)

  • 调用每个类成员和基类的移动构造函数
  • 通过移动而非拷贝来初始化一个对象,通常涉及资源的转移,使得原对象变为无效状态

移动赋值运算符

X& operator=(X&& other)

  • 调用每个类成员和基类的移动赋值运算符
  • 以赋值的形式将对象资源转移

特殊成员函数中还有一个是默认构造函数,但其不涉及资源的管理,所以和三五零法则没有什么关系,这里就不介绍了。

编译器和特殊成员函数

class X {
public:
	int a = 1;
};

int main()
{
	X a, b;
	a.a = 2;
	b.a = 3;
	a = b;
	std::cout << "a.a = " << a.a << std::endl;

	return 0;
}

上面代码的输出结果是:
a.a = 3
我们思考一下,为什么X类中没有显示定义拷贝赋值运算符,但a = b;能够有效地把b对象的数据赋值给a对象。
其实,在很多时候,自定义类的特殊成员函数的实现几乎是一样的,为了提升语言的易用性,编译器会在满足某些条件下的时候提供默认行为。
下图展示的是编译器隐式定义特殊成员函数的条件:
未命名文件 (8).png

该图引用了Howard Hinnant的演讲稿

注意
上图只是说明了用户显式定义某函数时,会影响编译器对其他函数的处理。
编译器是否隐式定义某函数,不仅仅取决于用户显式定义了什么,还与类成员对应类型和基类是否支持对应函数有关。比如基类不支持默认构造,那么派生类的默认构造函数会被标记为delete。

三五零法则

三法则

法则内容

三法则规定,如果一个类需要显式定义以下其中一项时,那么它必须显示定义这全部的三项:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数

案例说明

根据RAII原则,当类手动管理至少一个动态分配的资源时,通常需要实现上述函数。

class Student {
public:
    Student(char* name, int id) {
        this->id = id;
        this->name = new char[strlen(name) + 1];
        strcpy(this->name, name);
    }
    
    ~Student() {
        delete[] this->name;
    }

private:
    int id;
    char* name;
};

在这个示例中,我们有一个Student类手动管理了动态分配的资源(即name),构造函数为name分配内存,析构函数释放分配的内存。
但是当Student的对象被复制时会发生什么?

Student s1("Tom", 12);
Student s2 = s1;

当构造s2时,将执行Student的默认拷贝构造函数(因为用户没有显式定义拷贝构造函数)。默认的拷贝构造函数将每个成员进行浅拷贝,这意味着s1.names2.name都指向同一块内存。
main()函数结束时会发生什么?s2的析构函数将被调用,这将释放name所指向的内存,然后s1的析构函数被调用,它将再次尝试释放name指向的内存,但是这块内存已经被释放了!这就导致重复释放内存。
为了避免这种情况,需要提供适当的复制操作:

// 拷贝构造函数
Student(const Student& other) {
    this->id = other.id;
    this->name = new char[strlen(other.name) + 1];
    strcpy(this->name, other.name);
}

// 拷贝赋值运算符
Student& operator=(const Student& rhs) {
    // 防止自拷贝
    if (this != &rhs) {
        this->id = rhs.id;
    
        // delete old data
        if (this->name) {
          delete[] this->name;
        }
    
        this->name = new char[strlen(rhs.name) + 1];
        strcpy(this->name, rhs.name);
    }

    return *this;
}

拷贝构造函数和拷贝赋值运算符都执行动态分配资源的深拷贝。

五法则

法则内容

五法则是三法则的扩展。五法则规定,如果一个类需要显式定义以下其中一项时,建议显式定义全部的五项:

  • 拷贝构造函数
  • 拷贝赋值运算符
  • 析构函数
  • 移动构造函数
  • 移动赋值运算符

除了三法则中的三项外,我们还建议实现移动语义。与拷贝操作相比,移动操作更加高效,因为它们利用已分配的内存并避免不必要的拷贝操作。
不实现移动语义通常不被视为错误。如果缺少移动语义,编译器通常会尽可能使用效率较低的复制操作。如果一个类不需要移动操作,我们可以轻松跳过这些操作。但是,使用它们会提高效率。

因为用户显式定义三法则中的任意一项时,会阻止编译器隐式定义移动语义,导致失去优化的机会。
该法则只是建议,不做强制要求。

案例说明

我们还是在三法则的案例基础上添加移动语言:

// 移动构造函数
Student(Student&& other) {
    this->id = other.id;
    this->name = other.name;
    other.name = nullptr;
}

// 移动赋值运算符
Student& operator=(Student&& rhs) {
    // 防止自移动
    if (this != &rhs) {
        this->id = rhs.id;
        
        // 删除原数据(防止内存泄漏)
        if (this->name) {
            delete[] this->name;
        }
        
        this->name = rhs.name;
        rhs.name = nullptr;
    }
    return *this;
}

调用代码:

Student s1("John", 10);
Student s2 = s1; // 调用拷贝构造函数
Student s3;
s3 = s1; // 调用拷贝赋值运算符

Student s4("Jane", 12);
Student s5 = std::move(s4); // 调用移动构造函数
Student s6;
s6 = std::move(s5); // 调用移动赋值运算符

使用std::move()可以强制调用移动语义。

零法则

法则内容

如果没有显式定义任何特殊成员函数,则编译器会隐式定义所有特殊成员函数(成员变量也会影响隐式定义)。
零法则就是建议优先选择不需要显式定义特殊成员函数的情况。

简单来说,如果类需要管理动态资源(如动态内存、文件句柄、网络连接等)就需要遵循五法则;如果类不需要管理动态资源,那最好不要显式定义析构函数、拷贝/移动构造函数、拷贝/移动赋值运算符。
如果类的所有成员都遵循零法则,那么整个类也遵循零法则。

零法则说到底就是建议使用智能指针和其他资源管理工具,以自动处理资源的创建和销毁。这样大多数类都无需直接管理资源,从而可以避免许多常见的资源管理错误,如资源泄漏、重复释放等。通过遵循零法则,开发者可以编写更简洁、更安全的代码。

相关推荐

  1. C++ 大/大法则(__cplusplus 前向兼容)

    2024-06-08 07:30:03       40 阅读
  2. 深入理解c++ 继承

    2024-06-08 07:30:03       52 阅读

最近更新

  1. docker php8.1+nginx base 镜像 dockerfile 配置

    2024-06-08 07:30:03       94 阅读
  2. Could not load dynamic library ‘cudart64_100.dll‘

    2024-06-08 07:30:03       100 阅读
  3. 在Django里面运行非项目文件

    2024-06-08 07:30:03       82 阅读
  4. Python语言-面向对象

    2024-06-08 07:30:03       91 阅读

热门阅读

  1. Docker 容器中运行Certbot获取和管理 SSL 证书

    2024-06-08 07:30:03       27 阅读
  2. 【leetcode】LRU & LFU

    2024-06-08 07:30:03       29 阅读
  3. 力扣1574.删除最短的子数组使剩余数组有序

    2024-06-08 07:30:03       26 阅读
  4. setattr前端接收方法深度解析

    2024-06-08 07:30:03       28 阅读
  5. VmWare的网络配置说明

    2024-06-08 07:30:03       23 阅读
  6. WPF添加动画过渡效果

    2024-06-08 07:30:03       24 阅读
  7. 2024华为OD机试真题-出租车计费-C++(C卷D卷)

    2024-06-08 07:30:03       34 阅读
  8. Android系统中xml的解压与压缩

    2024-06-08 07:30:03       31 阅读
  9. 京准电子 GPS网络时间服务器为工业4.0保驾护航

    2024-06-08 07:30:03       29 阅读
  10. github使用教程

    2024-06-08 07:30:03       29 阅读
  11. 2、Spring之Bean生命周期~扫描

    2024-06-08 07:30:03       25 阅读
  12. spring boot中常用的多线程案例

    2024-06-08 07:30:03       27 阅读
  13. Android基础-Fragment详解

    2024-06-08 07:30:03       32 阅读