当前位置: 首页 > article >正文

类的特殊成员函数——三之法则、五之法则、零之法则

        系统中的动态资源、文件句柄(socket描述符、文件描述符)是有限的,在类中若涉及对此类资源的操作,但是未做到妥善的管理,常会造成资源泄露问题,严重的可能造成资源不可用。或引发未定义行为,进而引起程序崩溃、表现出意外的行为、损坏数据,或者可能看似正常工作但在其它情况下出现问题。

        三之法则和五之法则可以很好地解决上述问题。它们帮助开发者理解和管理类的拷贝控制操作,避免常见的资源泄露、重复释放等问题,并优化程序的性能。零之法则关注类的特殊成员函数的声明和使用。在实际开发中,应根据类的具体需求来决定是否需要自定义这些特殊的成员函数。

一、三之法则

1、概念

        三之法则,也称为“三大定律”或“三法则”,它指出,如果类定义了以下三个特殊成员函数之一:析构函数、拷贝构造函数或拷贝赋值运算符,则开发者通常也需要定义其它两个特殊成员函数,以确保类的拷贝控制和资源管理行为的正确性。

2、使用场景

        三之法则主要是为了避免资源泄露、重复释放或其它由于浅拷贝导致的错误。

        默认情况下,编译器会为类生成默认的析构函数、拷贝构造函数和拷贝赋值运算符,但这些默认实现通常只进行浅拷贝,即只复制对象的成员变量的值,而不复制成员变量所指向的资源。如果成员变量是指针,并且指向动态分配的内存,则浅拷贝会导致两个对象共享同一块内存,从而在销毁时发生重复释放的错误。

3、如何实现

定义所有需要的特殊成员函数:如果类需要自定义其中一个特殊成员函数,那么通常也需要自定义其他两个成员函数,以确保对象的拷贝和赋值行为符合预期。

理解资源管理:了解类所管理的资源,并决定是否需要自定义特殊成员函数来管理这些资源的拷贝和赋值。

使用RAII:将资源的生命周期与对象的生命周期绑定,简化资源管理,降低资源泄露风险。

4、示例

4.1 类中包含指针私有成员

#include <iostream>
#include <cstring>

class Point {  
public:  
    Point(size_t n = 0) : numCoords(n), coords(n ? new double[n] : nullptr)
    {
        if (coords) {
            std::memset(coords, 0, n * sizeof(double));
        }
    }

    ~Point()
    {
        delete[] coords;
    }

    Point(const Point &other) : numCoords(other.numCoords), coords(new double[other.numCoords])
    {
        std::memcpy(coords, other.coords, numCoords * sizeof(double));
    }
   
    Point &operator=(const Point &other)
    {  
        if (this != &other) {
            delete[] coords;
            numCoords = other.numCoords;
            coords = new double[numCoords];
            std::memcpy(coords, other.coords, numCoords * sizeof(double));
        }
        return *this;
    }  

private:  
    double *coords; 
    size_t numCoords;
};  

int main() 
{
    Point p1(3);
    Point p2 = p1;
    Point p3;
    p3 = p1;
    return 0;
}

二、五之法则

1、概念

        五之法则在C++11及以后版本引入,它在三之法则的基础上增加了两个新的特殊成员函数:移动构造函数和移动赋值运算符,以支持移动语义。

2、使用场景

        五之法则的引入是为了进一步提高程序的性能,特别是在处理大型对象或资源密集型对象时。通过允许对象之间的资源移动而不是复制,可以减少不必要的内存分配和释放操作,从而提高程序的运行效率。

3、如何实现

定义所有五个特殊成员函数:如果类需要移动语义,则应该定义所有五个特殊成员函数(析构函数、拷贝构造函数、拷贝赋值运算符、移动构造函数和移动赋值运算符)。

使用noexcept关键字:在C++11及以后版本中,移动构造函数和移动赋值运算符通常会被标记为noexcept,表明它们不会抛出异常。这有助于编译器优化代码,并允许在更多情况下使用移动语义。

理解移动语义:了解移动语义的工作原理,并决定何时以及如何使用它来优化程序的性能。

4、示例

4.1 socket描述符

#include <iostream>
#include <cstdio>
#include <cstring>
#include <unistd.h>
#include <sys/types.h>
#include <sys/socket.h>
#include <netinet/in.h>
#include <stdexcept>

class SocketDescriptor {
public:
    // 默认构造函数,创建一个无效的socket
    SocketDescriptor() : fd(-1) {}

    // 构造函数,接受一个已创建的socket文件描述符
    explicit SocketDescriptor(int socket_fd) : fd(socket_fd)
    {
        if (fd == -1) {
            throw std::runtime_error("Invalid socket descriptor");
        }
    }

    // 析构函数,关闭socket文件描述符  
    ~SocketDescriptor()
    {
        closeSocket();
    }

    // 禁用拷贝构造函数
    SocketDescriptor(const SocketDescriptor&) = delete;

    // 禁用拷贝赋值运算符
    SocketDescriptor& operator=(const SocketDescriptor&) = delete; 

    // 移动构造函数
    SocketDescriptor(SocketDescriptor&& other) noexcept : fd(other.fd)
    {
        other.fd = -1; // 将原对象的fd置为无效
    }

    // 移动赋值运算符
    SocketDescriptor& operator=(SocketDescriptor&& other) noexcept
    {
        if (this != &other) {
            closeSocket(); // 关闭当前对象的fd
            fd = other.fd; // 转移资源
            other.fd = -1; // 将原对象的fd置为无效
        }
        return *this;
    }

    // 获取socket文件描述符
    int get() const
    {
        return fd;
    }

    // 创建一个新的socket并管理它
    static SocketDescriptor createSocket(int domain, int type, int protocol)
    {
        int fd = socket(domain, type, protocol);
        if (fd == -1) {
            throw std::runtime_error("Failed to create socket");
        }
        return SocketDescriptor(fd);
    }

// 关闭socket文件描述符
private:
    void closeSocket()
    {
        if (fd != -1) {
            ::close(fd);
            fd = -1;
        }
    }

private:
    int fd;
};

int main()
{
    try {
        // 创建一个新的TCP socket
        SocketDescriptor sock = SocketDescriptor::createSocket(AF_INET, SOCK_STREAM, 0);
  
        // 打印socket文件描述符
        std::cout << "Socket descriptor: " << sock.get() << std::endl; 
    } catch (const std::exception& e) {
        std::cerr << "Error: " << e.what() << std::endl;
    }

    return 0;
}

4.2 拷贝"大"数据

#include <iostream>
#include <algorithm> // 用于std::copy
#include <stdexcept> // 用于std::runtime_error 和 std::bad_alloc 的异常处理

class LargeData {
public:
    LargeData() = default;
    // 构造函数,接受数据大小并初始化数据
    explicit LargeData(size_t dataSize)
    {
        try {
            data = new char[dataSize];
            this->dataSize = dataSize;
            std::fill_n(data, dataSize, 0); // 初始化数据为0(可选)
        } catch (const std::bad_alloc&) {
            throw std::runtime_error("Memory allocation failed in LargeData constructor");
        }
    }

    // 析构函数,释放动态分配的数据
    ~LargeData()
    {
        delete[] data;
    }

    // 拷贝构造函数,执行深拷贝
    LargeData(const LargeData& other) : dataSize(other.dataSize)
    {
        try {
            data = new char[dataSize];
            std::copy(other.data, other.data + dataSize, data);
        } catch (const std::bad_alloc&) {
            throw std::runtime_error("Memory allocation failed in LargeData copy constructor");
        }
    }

    // 拷贝赋值运算符,执行深拷贝
    LargeData& operator=(const LargeData& other)
    {
        if (this == &other) {
            return *this; // 自赋值检查
        }

        // 释放当前对象的资源
        char* oldData = data;

        // 分配新资源并复制数据
        try {
            dataSize = other.dataSize;
            data = new char[dataSize];
            std::copy(other.data, other.data + dataSize, data);
        } catch (const std::bad_alloc&) {
            // 如果新分配失败,恢复旧状态并抛出异常
            dataSize = 0; // 或者保持原大小,但这可能会导致不一致状态
            data = oldData; // 这里实际上不应该这样做,因为oldData可能已经被释放或指向无效内存
            throw std::runtime_error("Memory allocation failed in LargeData copy assignment operator");
        }
        return *this;
    }

    // 移动构造函数,接管资源(之前已经正确实现)
    LargeData(LargeData&& other) noexcept : dataSize(0), data(nullptr)
    {
        *this = std::move(other); // 委托给移动赋值运算符
    }

    // 移动赋值运算符,接管资源并释放旧数据(之前已经正确实现)
    LargeData& operator=(LargeData&& other) noexcept
    {
        if (this == &other) {
            return *this; // 自赋值检查
        }

        // 释放当前对象的资源
        delete[] data;

        // 接管其他对象的资源
        data = other.data;
        dataSize = other.dataSize;

        // 重置其他对象的资源指针以避免悬挂指针
        other.data = nullptr;
        other.dataSize = 0;

        return *this;
    }

    // 获取数据大小
    size_t getSize() const
    {
        return dataSize;
    }

    // 获取数据指针(注意:返回的指针不应被删除)
    const char* getData() const
    {
        return data;
    }

    // 非const版本的getData,用于修改数据(通常不推荐这样做,但为了完整性而提供)
    char* getData()
    {
        return data;
    }

    // 设置数据(示例函数,用于修改数据内容,注意大小必须匹配)
    void setData(const char* newData, size_t newSize)
    {
        if (newSize != dataSize) {
            throw std::runtime_error("New data size does not match existing data size");
        }
        std::copy(newData, newData + newSize, data);
    }

private:
    size_t dataSize; // 数据大小
    char* data;      // 指向动态分配数据的指针
};

int main()
{
    try {
        // 创建大量数据对象
        LargeData data1(1024); // 1KB数据

        // 使用拷贝构造函数
        LargeData data2 = data1; // data2是data1的深拷贝

        // 使用拷贝赋值运算符
        LargeData data3;
        data3 = data1; // data3现在是data1的深拷贝

        // 清理资源(由析构函数自动处理)
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

三、零之法则

1、概念

        C++的零之法则是指,如果可能,类应该避免声明任何特殊成员函数。鼓励让编译器自动生成这些特殊成员函数,以简化类的设计和管理。

2、使用场景

简化设计:零之法则通过减少需要编写的代码量,简化类的设计。当类不需要显式管理资源时,遵循零之法则可以使类的接口更加清晰。

减少错误:手动编写特殊成员函数容易引入错误,特别是当类的成员变量较多或类型复杂时。编译器生成的特殊成员函数通常更加健壮。

利用标准库:零之法则鼓励使用标准库组件(如std::string、std::vector等)来管理资源。

提高可维护性:遵循零之法则的类更加简洁,更易于理解和维护。

3、如何实现

避免显式声明特殊成员函数:除非类需要显式管理资源,否则让编译器自动生成这些函数。

使用组合而非继承:组合优于继承是面向对象设计中的一个重要原则。通过组合,可以将其它类的实例作为当前类的成员变量,从而避免复杂的继承关系和虚函数的开销。

利用智能指针:对于需要动态分配内存的场景,使用C++11及以后版本中引入的智能指针(如std::unique_ptr、std::shared_ptr等)。这些智能指针可以自动管理内存,减少内存泄漏的风险。

4、示例

4.1 合理使用C++标准库管理内存

#include <iostream>
#include <stdexcept>
#include <string>

class LargeData {
public:
    // 默认构造函数,创建一个空的LargeData对象
    LargeData() = default;

    // 构造函数,接受数据大小(以字节为单位)并初始化一个相应大小的空字符串
    explicit LargeData(size_t dataSize) : data(dataSize, '\0') {}

    // 获取数据大小(以字节为单位)
    size_t getSize() const
    {
        return data.size();
    }

    // 获取数据(返回const char*以避免修改数据)
    const char* getData() const
    {
        return data.c_str();
    }

    // 非const版本的getData,用于修改数据(通常不推荐这样做,但为了完整性而提供)
    // 注意:这允许调用者修改数据,但他们必须确保不超出字符串的边界。
    char* getData() 
    {
        return &data[0];
    }

    // 设置数据(注意:大小必须匹配,否则抛出异常)
    void setData(const char* newData, size_t newSize)
    {
        if (newSize != data.size()) {
            throw std::runtime_error("New data size does not match existing data size");
        }
        data.assign(newData, newSize);
    }

    // 为了方便,添加一个接受std::string的setData重载
    void setData(const std::string& newData)
    {
        if (newData.size() != data.size()) {
            throw std::runtime_error("New data size does not match existing data size");
        }
        data = newData;
    }

private:
    std::string data; // 使用std::string来存储数据
};

int main() 
{
    try {
        // 创建LargeData对象,大小为1024字节
        LargeData data1(1024);

        // 使用拷贝构造函数(由编译器隐式生成)
        LargeData data2 = data1; // data2是data1的一个深拷贝(因为std::string是深拷贝的)

        // 使用拷贝赋值运算符(由编译器隐式生成)
        LargeData data3;
        data3 = data1; // data3现在是data1的一个深拷贝

        // 注意:不需要手动清理资源,因为std::string会处理它
    } catch (const std::exception& e) {
        std::cerr << "Exception: " << e.what() << std::endl;
    }

    return 0;
}

4.2 智能指针管理资源

#include <iostream>
#include <memory>
#include <vector>

// 一个简单的类,表示资源
class Resource {
public:
    Resource(int value) : value_(value)
    {
        std::cout << "Resource created with value: " << value_ << std::endl;
    }
    ~Resource()
    {
        std::cout << "Resource destroyed with value: " << value_ << std::endl;
    }
    void display() const
    {
        std::cout << "Resource value: " << value_ << std::endl;
    }
private:
    int value_;
    // 禁止拷贝和赋值
    Resource(const Resource&) = delete;
    Resource& operator=(const Resource&) = delete;
};

// 一个管理类,使用智能指针管理资源
class ResourceManager {
public:
    void addResource(int value)
    {
        // 使用std::make_unique来创建unique_ptr
        resources_.push_back(std::make_unique<Resource>(value));
    }

    void displayAllResources() const
    {
        for (const auto& resource : resources_) {
            resource->display();
        }
    }

private:
    std::vector<std::unique_ptr<Resource>> resources_;
};

int main()
{
    // 创建ResourceManager对象
    ResourceManager manager;

    // 添加资源
    manager.addResource(10);
    manager.addResource(20);
    manager.addResource(30);

    // 显示所有资源
    manager.displayAllResources();

    // ResourceManager对象离开作用域时,其析构函数会调用,所有unique_ptr会自动释放资源
    return 0;
}

http://www.kler.cn/news/335956.html

相关文章:

  • 《Windows PE》4.1.4 手工重构导入表
  • 数据结构——七种排序(java)实现
  • AI类课程的笔记
  • 租拼车平台|小区租拼车管理|基于java的小区租拼车管理信息系统小程序设计与实现(源码+数据库+文档)
  • 线性代数杂谈(1)——基础矩阵
  • Java中循环练习题
  • 【分布式微服务云原生】掌握 Redis Cluster架构解析、动态扩展原理以及哈希槽分片算法
  • 若依权限设计与自定义新增用户
  • 最通俗的语言搞懂【大模型】的来龙去脉
  • 单片机原理及其应用:新手快速入门
  • ade20k 街景图像【数据集】及其【论文出处】ADE20K数据集 超过25000张图像的语义分割数据集
  • 蓝桥杯省赛真题打卡day4
  • 【AI大模型】深入Transformer架构:编码器部分的实现与解析(下)
  • 消费者Rebalance机制
  • 【C++ Primer Plus】4
  • Perl 子程序(函数)
  • 《普林斯顿概率论读本》中文版目录
  • 摩尔平台今日学习点
  • Maven 入门
  • NineData云原生智能数据管理平台新功能发布|2024年9月版