【一分钟学C++】三五零法则

在这里插入图片描述

竹杖芒鞋轻胜马,谁怕?一蓑烟雨任平生~
公众号: C++学习与探索  |  个人主页: rainInSunny  |  个人专栏: Learn OpenGL In Qt

自动生成的函数

  理解这些规则之前,首先要了解 C++ 编译器帮助我们自动生成的一些特殊函数。包括了默认构造函数、默认析构函数、默认拷贝构造函数、默认拷贝赋值函数、默认移动构造函数、默认移动赋值函数。这些函数并不是在所有情况下都会默认生成,是否生成存在着一些规则:

  • 特殊成员函数是编译器可能自动生成的函数:默认构造函数,析构函数,拷贝操作,移动操作。
  • 移动操作仅当类没有显式声明移动操作,拷贝操作,析构函数时才自动生成。
  • 拷贝构造函数仅当类没有显式声明拷贝构造函数时才自动生成,并且如果用户声明了移动操作,拷贝构造就是delete。拷贝赋值运算符仅当类没有显式声明拷贝赋值运算符时才自动生成,并且如果用户声明了移动操作,拷贝赋值运算符就是 delete。当用户声明了析构函数,拷贝操作的自动生成已被废弃。

简单验证

  以下测试在 C++11 标准下进行,自定义析构函数场景,测试代码:

#include <iostream>

class Foo
{
private:
    int i;
public:
    Foo()
    {
        std::cout << "Default Constructor" << std::endl;
    }
    // Foo(const Foo& f)
    // {
    //     std::cout << "Copy Constructor" << std::endl
    // }
    // Foo& operator=(const Foo&)
    // {
    //     std::cout << "Copy Assignment" << std::endl
    //     return *this;
    // }
    // Foo(Foo&&)     //提供移动操作的支持
    // {
    //     std::cout << "Move Constructor" << std::endl;
    // }
    // Foo& operator=(Foo&&)
    // {
    //     std::cout << "Move Assignment" << std::endl;
    //     return *this;
    // }

    ~Foo() {}; //这里自定义析构函数
};

int main(int argc, const char * argv[])
{
    // insert code here...
    Foo f;
    Foo f1 = f;
    f1 = std::move(f);
    std::cout << "Hello, World!\n";
    return 0;
}

  首先只定义构造和析构函数,得到结果。这里可以正常编译,应该是生成了默认拷贝操作函数,和查阅的部分资料不完全符合,有点疑惑。
在这里插入图片描述
  定义析构和拷贝操作,测试代码:

class Foo
{
private:
    int i;
public:
    Foo()
    {
        std::cout << "Default Constructor" << std::endl;
    }
    Foo(const Foo& f)
    {
        std::cout << "Copy Constructor" << std::endl;
    }
    Foo& operator=(const Foo&)
    {
        std::cout << "Copy Assignment" << std::endl;
        return *this;
    }
    // Foo(Foo&&);     //提供移动操作的支持
    // {
    //     std::cout << "Move Constructor" << std::endl;
    // }
    // Foo& operator=(Foo&&)
    // {
    //     std::cout << "Move Assignment" << std::endl;
    //     return *this;
    // }

    ~Foo() {}; //这里自定义析构函数
};

  得到结果显示 std::move 的赋值调用了拷贝赋值函数,证明在声明了拷贝操作的场景下编译器没有生成默认的移动操作函数。
在这里插入图片描述
  定义析构和移动操作,测试代码:

class Foo
{
private:
    int i;
public:
    Foo()
    {
        std::cout << "Default Constructor" << std::endl;
    }
    // Foo(const Foo& f)
    // {
    //     std::cout << "Copy Constructor" << std::endl;
    // }
    // Foo& operator=(const Foo&)
    // {
    //     std::cout << "Copy Assignment" << std::endl;
    //     return *this;
    // }
    Foo(Foo&&) //提供移动操作的支持
    {
        std::cout << "Move Constructor" << std::endl;
    }
    Foo& operator=(Foo&&)
    {
        std::cout << "Move Assignment" << std::endl;
        return *this;
    }

    ~Foo() {};  //这里自定义析构函数
};

  得到结果显示,由于自定义移动操作函数,编译器隐式删除了拷贝操作函数。
在这里插入图片描述
  这里不做更详细的测试了,因为是否生成这些默认函数存在一些规则,所以有三五零法则来指导如何编写这些函数更为合理。

三法则

  若某个类需要用户定义的析构函数、用户定义的复制构造函数或用户定义的复制赋值运算符,则它几乎肯定三者全部都需要。因为 C++ 在各种场合(按值传递/返回、操纵容器等)对对象进行复制和复制赋值,若可访问,这些特殊成员函数就会被调用,而且若用户不定义他们,则编译器会隐式定义。如果类对某种资源进行管理,而资源句柄是非类类型的对象(裸指针、POSIX 文件描述符等),则这些隐式定义的成员函数通常都不正确,其析构函数不做任何事,而复制构造函数/复制赋值运算符则进行“浅复制”(复制句柄的值,而不复制底层资源)。

class rule_of_three
{
    char* cstring; // 以裸指针为动态分配内存的句柄
 
    void init(const char* s)
    {
        std::size_t n = std::strlen(s) + 1;
        cstring = new char[n];
        std::memcpy(cstring, s, n); // 填充
    }
 public:
    rule_of_three(const char* s = "") { init(s); }
 
    ~rule_of_three()
    {
        delete[] cstring;  // 解分配
    }
 
    rule_of_three(const rule_of_three& other) // 复制构造函数
    { 
        init(other.cstring);
    }
 
    rule_of_three& operator=(const rule_of_three& other) // 复制赋值
    {
        if(this != &other) {
            delete[] cstring;  // 解分配
            init(other.cstring);
        }
        return *this;
    }
};

  通过可复制句柄来管理不可复制资源的类,可能必须将其复制赋值和复制构造函数声明为私有的并且不提供其定义,或将它们定义为弃置的。这是三之法则的另一种应用:删除其中之一而遗留其他被隐式定义很可能会导致错误。

五法则

  因为用户定义析构函数、复制构造函数或复制赋值运算符会阻止移动构造函数和移动赋值运算符的隐式定义,所以任何想要移动语义的类必须声明全部五个特殊成员函数:

class rule_of_five
{
    char* cstring; // 以裸指针为动态分配内存的句柄
 public:
    rule_of_five(const char* s = "")
    : cstring(nullptr)
    { 
        if (s) {
            std::size_t n = std::strlen(s) + 1;
            cstring = new char[n];      // 分配
            std::memcpy(cstring, s, n); // 填充
        } 
    }
 
    ~rule_of_five()
    {
        delete[] cstring;  // 解分配
    }
 
    rule_of_five(const rule_of_five& other) // 复制构造函数
    : rule_of_five(other.cstring)
    {}
 
    rule_of_five(rule_of_five&& other) noexcept // 移动构造函数
    : cstring(std::exchange(other.cstring, nullptr))
    {}
 
    rule_of_five& operator=(const rule_of_five& other) // 复制赋值
    {
         return *this = rule_of_five(other);
    }
 
    rule_of_five& operator=(rule_of_five&& other) noexcept // 移动赋值
    {
        std::swap(cstring, other.cstring);
        return *this;
    }
}

  与三之法则不同的是,不提供移动构造函数和移动赋值运算符通常不是错误,但会导致失去优化机会。

零法则

  有自定义析构函数、复制/移动构造函数或复制/移动赋值运算符的类应该专门处理所有权(这遵循单一责任原则)。其他类都不应该拥有自定义的析构函数、复制/移动构造函数或复制/移动赋值运算符。也就是说,如果一个类不负责资源的生命周期,就不应该自定义析构函数、复制/移动构造函数或复制/移动赋值运算符。

class rule_of_zero
{
    std::string cppstring;
 public:
    rule_of_zero(const std::string& arg) : cppstring(arg) {}
};

  当有意将某个基类用于多态用途时,可能必须将其析构函数声明为公开的虚函数。由于这会阻拦隐式移动(并弃用隐式复制)的生成,因而必须将各特殊成员函数声明为预置的.

class base_of_five_defaults
{
 public:
    base_of_five_defaults(const base_of_five_defaults&) = default;
    base_of_five_defaults(base_of_five_defaults&&) = default;
    base_of_five_defaults& operator=(const base_of_five_defaults&) = default;
    base_of_five_defaults& operator=(base_of_five_defaults&&) = default;
    virtual ~base_of_five_defaults() = default;
};

关注公众号:C++学习与探索,有惊喜哦~

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值