C++基础: Rule of five/zero/three

news/2025/3/19 10:27:11/

Rule of five

在 C++ Core Guidelines 中,有这样的一条指导原则:

C.21: If you define or =delete any copy, move, or destructor function, define or =delete them all

就是说如果你定义了或者删除(=delete): 拷贝构造函数, 移动构造函数, 拷贝赋值函数, 移动赋值函数, 析构函数中的任意一个函数, 那么你需要定义或者删除它们全部. 注意: 普通的构造函数不在其中.

在通常情况下, 编译器会尝试为用户生成上面的 5 个函数. 编译器生成的函数的行为:

  • 拷贝构造函数:按顺序拷贝构造每个成员变量
  • 拷贝赋值运算符:按顺序拷贝赋值每个成员变量
  • 移动构造函数:按顺序移动构造每个成员变量
  • 移动赋值运算符:按顺序移动赋值每个成员变量
  • 析构函数:按逆序销毁每个成员变量

但是如果用户定义了其中之一, 那么编译器的行为就会发生变化. 具体如下:

ruleoffive

读者很难记住这个表格中的所有信息, 所以这会让使用者产生困惑. 为了让读者有个深切的体验, 后面我将举一个例子, 带大家一步步了解.

Simple String

我们以实现一个简单的字符串类为例:

  1. 因为我们在构造的时候分配了存储空间, 所以需要加一个析构函数释放对应存储.

    struct SString {SString(char const* cp) : data_(new char[strlen(cp) + 1]) {strcpy(data_, cp);}~SString() { delete[] data_; }private:char* data_;
    };
    
  2. 如果现在有这样一个函数, 使用传值方式调用, 那该如何?

    void fun(SString val) {//...
    }
    

    这个时候因为编译器生成的拷贝函数会按位拷贝, 也就是两个实例的data_指针指向同一个地址, 这样会造成调用两次析构函数, 有重复释放的问题. 因此我们此时需要修改拷贝构造函数.
    于是代码进一步变成这样:

    struct SString {SString(char const* cp) : data_(new char[strlen(cp) + 1]) {strcpy(data_, cp);}~SString() { delete[] data_; }SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {strcpy(data_, rhs.data_);}private:char* data_;
    };
    
  3. 这还没完, 如果此时有一个赋值语句, 程序就会出问题.

    SString src{"I’m going to be copied"};
    SString dst{"I have a value"};
    // …
    dst = src;
    

    此时我们需要自定义一个拷贝赋值函数. 不能使用默认的拷贝赋值.

    #include <cstdlib>
    #include <cstring>
    #include <utility>struct SString {SString(char const* cp) : data_(new char[strlen(cp) + 1]) {strcpy(data_, cp);}~SString() { delete[] data_; }SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {strcpy(data_, rhs.data_);}SString& operator=(SString const& rhs) {char* newdata = new char[strlen(rhs.data_) + 1];strcpy(newdata, rhs.data_);std::swap(newdata, data_);delete[] newdata;return *this;}private:char* data_;
    };void fun(SString val) {//...
    }int main() {SString s("Hello, World!");fun(s);SString src{"I’m going to be copied"};SString dst{"I have a value"};// …dst = src;return 0;
    }
    
  4. 此时的代码还有问题. 下面的情况下我们希望调用移动构造函数, 但是实际上会调用拷贝构造函数. 加个打印语句可以验证这一点.

    SString s{"I'm temporary"};
    fun(std::move(s));
    

    为了正确支持 move, 我们需要定义一个移动构造函数.

    struct SString {SString(char const* cp) : data_(new char[strlen(cp) + 1]) {strcpy(data_, cp);}~SString() { delete[] data_; }SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {std::cout << "copy constructor" << std::endl;strcpy(data_, rhs.data_);}SString& operator=(SString const& rhs) {std::cout << "copy assign" << std::endl;char* newdata = new char[strlen(rhs.data_) + 1];strcpy(newdata, rhs.data_);std::swap(newdata, data_);delete[] newdata;return *this;}SString(SString&& rhs) noexcept : data_(rhs.data_) {rhs.data_ = nullptr;std::cout << "move constructor" << std::endl;}private:char* data_;
    };
    
  5. 最后一个需要面对的场景就是移动赋值. 加上打印语句可以看出来当前的移动赋值调用了拷贝赋值函数.

    SString src{"hello"};
    SString dst{"world"};
    dst = std::move(src);
    

    需要实现一个移动赋值函数.

    struct SString {SString(char const* cp) : data_(new char[strlen(cp) + 1]) {strcpy(data_, cp);}~SString() { delete[] data_; }SString(SString const& rhs) : data_(new char[strlen(rhs.data_) + 1]) {std::cout << "copy constructor" << std::endl;strcpy(data_, rhs.data_);}SString& operator=(SString const& rhs) {std::cout << "copy assign" << std::endl;char* newdata = new char[strlen(rhs.data_) + 1];strcpy(newdata, rhs.data_);std::swap(newdata, data_);delete[] newdata;return *this;}SString(SString&& rhs) noexcept : data_(rhs.data_) {rhs.data_ = nullptr;std::cout << "move constructor" << std::endl;}SString& operator=(SString&& rhs) {std::cout << "move assign" << std::endl;delete[] data_;data_ = rhs.data_;rhs.data_ = nullptr;return *this;}private:char* data_;
    };
    

所以, 为了一个简单的类, 我们需要定义 5 个函数. 能避免还是要尽量避免这样做.

Rule of zero

在 C++ Core Guidelines 中, C.20这样写:

C.20: If you can avoid defining any default operations, do

C.20的意思是: 如果你可以避免定义任何默认操作, 那么你就应该避免.

对上面的简单字符串类来说, 如果你只是扩展功能, 那么完全不用自己管理空间, 托管给std::string(或者其他 RAII 类)即可.

struct EString {EString(char const * cp) : data_(cp) {}std::string data_;
};

Rule of three

在 C++11 之前, 没有移动构造和移动赋值函数, 所以减去这两个就剩 3 个函数了. 也就是他们基于类似的考虑, 但是数量不一样而已.

总结

实际工作中如果有能用的工具类尽量使用工具类(Rule of zero), 当然如果你是那个要造轮子的人, 那么请遵从 Rule of Five 实现全部必要的函数.


http://www.ppmy.cn/news/1580294.html

相关文章

调试 Rust + WebAssembly 版康威生命游戏

1. 启用 Panic 日志 1.1 让 Panic 信息显示在浏览器控制台 如果 Rust 代码发生 panic!()&#xff0c;默认情况下不会在浏览器开发者工具中显示详细的错误信息。这使得排查问题变得困难。 我们可以使用 console_error_panic_hook 这个 Rust crate&#xff0c;将 Panic 信息打…

解决uni-app授权弹框华为审核拒绝

背景&#xff1a; 在使用定位、相机、文件、电话&#xff0c;需要用户同意授权时&#xff0c;华为和vivo需要告知用户使用权限目的。 方案&#xff1a; 在uni授权时&#xff0c;弹框告诉授权目的&#xff0c;效果如下&#xff1a; 代码&#xff1a; const perListener {//…

AI爬虫 :Crawl4AI的安装和详细使用案例(开源 LLM 友好型网络爬虫)

更多内容请见: 爬虫和逆向教程-专栏介绍和目录 文章目录 1. Crawl4AI概述1.1 Crawl4AI 介绍1.2 Crawl4AI 做什么?1.3 Crawl4AI 的核心理念1.4 Crawl4AI v0.5.0 新功能2. Crawl4AI的安装和第一个案例2.1 Crawl4AI 的安装2.2 初始设置2.3 诊断2.4 第一个案例2.5 高级安装(可选…

Linux top 命令详解:从入门到高级用法

Linux top 命令详解&#xff1a;从入门到高级用法 在 Linux 系统中&#xff0c;top 是一个强大的实时监控工具&#xff0c;用于查看系统资源使用情况和进程状态。它可以帮助你快速了解 CPU、内存、负载等信息&#xff0c;是系统管理员和开发者的日常利器。本文将从基本用法开始…

Netty基础—8.Netty实现私有协议栈一

大纲 1.私有协议介绍 2.私有协议的通信模型 3.私有协议栈的消息定义 4.私有协议栈链路的建立 5.私有协议栈链路的关闭 6.私有协议栈的心跳机制 7.私有协议栈的重连机制 8.私有协议栈的重复登录保护 9.私有协议栈核心的ChannelHandler 10.私有协议栈的客户端和服务端 …

The Rust Programming Language 学习 (六)

包和crate和模块 包和crate crate 是一个二进制项或者库。crate root 是一个源文件&#xff0c;Rust 编译器以它为起始点&#xff0c;并构成你的 crate 的根模块,包&#xff08;package&#xff09;是提供一系列功能的一个或者多个 crate。一个包会包含有一个 Cargo.toml 文件…

LeetCode[142] 环形链表 II

哈希表匹配法 set存储遍历过的节点每次遍历查询set中是否有该节点 有&#xff0c;则代表该节点为环的起点无&#xff0c;则插入set中&#xff0c;继续遍历链表 /*** Definition for singly-linked list.* struct ListNode {* int val;* ListNode *next;* ListNo…

【软件工程】01_软件工程的概述

1. 定义 软件是计算机系统中与硬件相互依存的另一部分&#xff0c;它是包括程序&#xff0c;数据及其相关文档的完整集合。 2. 软硬件失效 3. 软件危机 软件危机&#xff08;Software Crisis&#xff09;&#xff1a;指由于落后的软件生产方式无法满足迅速增长的计算机软件需求…