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

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

        系统中的动态资源、文件句柄(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:// 默认构造函数,创建一个无效的socketSocketDescriptor() : 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(); // 关闭当前对象的fdfd = 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 socketSocketDescriptor 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_ptrresources_.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;
}

相关文章:

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

系统中的动态资源、文件句柄&#xff08;socket描述符、文件描述符&#xff09;是有限的&#xff0c;在类中若涉及对此类资源的操作&#xff0c;但是未做到妥善的管理&#xff0c;常会造成资源泄露问题&#xff0c;严重的可能造成资源不可用。或引发未定义行为&#xff0c;进而…...

计算机毕业设计 智慧物业服务系统的设计与实现 Java实战项目 附源码+文档+视频讲解

博主介绍&#xff1a;✌从事软件开发10年之余&#xff0c;专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ &#x1f345;文末获取源码联系&#x1f345; &#x1f447;&#x1f3fb; 精…...

Python软体中使用SpaCy进行命名实体识别

Python软体中使用SpaCy进行命名实体识别 命名实体识别(Named Entity Recognition,NER)是自然语言处理(NLP)中的一个重要任务,它涉及识别文本中的命名实体,例如人名、地名、组织名等。SpaCy是一种流行的NLP库,提供了高效的NER功能。在本文中,我们将介绍如何使用SpaCy进…...

华为云技术深度解析:以系统性创新加速智能化升级

华为云技术深度解析&#xff1a;以系统性创新加速智能化升级 在当今数字化转型的浪潮中&#xff0c;云计算作为关键的基础设施&#xff0c;正以前所未有的速度推动着各行各业的智能化升级。作为全球领先的云服务提供商&#xff0c;华为云凭借其深厚的技术积累和创新实力&#…...

推理攻击-Python案例

1、本文通过推理攻击的方式来估计训练集中每个类别的样本数量、某样本是否在训练集中。 2、一种简单的实现方法&#xff1a;用模型对训练数据标签进行拟合&#xff0c;拟合结果即推理为训练集中的情况。 3、了解这些案例可以帮助我们更好的保护数据隐私。 推理攻击&#xff08;…...

find_box_3d

参数 &#xff08;ObjectModel3DScene, SideLen1, SideLen2, SideLen3, MinScore, GenParam : GrippingPose, Score, ObjectModel3DBox, BoxInformation) 入参介绍 1&#xff0c;ObjectModel3DScene&#xff0c; 输入的3d模型&#xff0c;这个模型最好是由xyx三通道点…...

Visual Studio2017编译GDAL3.0.2源码过程

一、编译环境 操作系统&#xff1a;Windows 10企业版 编译工具&#xff1a;Visual Studio 2017旗舰版 源码版本&#xff1a;gdal3.0.2 二、生成解决方案 打开Visual Studio 2017的x64本机生成工具&#xff0c;切换到gdal3.0.2源码根目录&#xff1b;执行generate_vcxproj.b…...

计算机网络——email

pop3拉出来 超出ASCII码范围就不让传了 这样就可以传更大的文件...

【Linux】信号知识三把斧——信号的产生、保存和处理

目录​​​​​​​ 1、关于信号的前置知识 1.1.什么是信号&#xff1f; 1.2.为什么要学习信号&#xff1f; 1.3.如何学习信号&#xff1f; 1.4.一些常见的信号 1.5.信号的处理方式 1.6.为什么每一个进程都可以系统调用&#xff1f; 2.信号的产生 2.1.kill命令产生信号…...

【国庆要来了】基于Leaflet的旅游路线WebGIS可视化实践

前言 转眼2024年的国庆节马上就要来临了&#xff0c;估计很多小伙伴都计划好了旅游路线。金秋十月&#xff0c;不管是选择出门去看看风景&#xff0c;还是选择在家里看人。从自己生活惯了的城市去别人生活惯了的城市&#xff0c;去感受城市烟火、去感受人文风景&#xff0c;为2…...

Element-UI Plus 暗黑主题切换及自定义主题色

1. 暗黑主题切换 在main.js中引入下面文件 import element-plus/theme-chalk/dark/css-vars.css安装 vueuse/core pnpm add vueuse/coreApp.vue 添加下面代码 使用了 useDark() 的页面才会从 localStorage中读取当前主题状态&#xff0c;否则&#xff0c;刷新页面就会恢复默…...

人工智能与机器学习原理精解【31】

文章目录 卷积神经网络CNN定义数学原理与公式计算与定理架构例子例题 全连接层的前馈计算定义数学原理与公式计算过程示例 参考文献 卷积神经网络 CNN 即卷积神经网络&#xff08;Convolutional Neural Networks&#xff09;&#xff0c;是一类包含卷积计算且具有深度结构的前…...

如何安全地大规模部署 GenAI 应用程序

大型语言模型和其他形式的生成式人工智能(GenAI) 的广泛使用带来了许多组织可能没有意识到的安全风险。幸运的是&#xff0c;网络和安全提供商正在寻找方法来应对这些前所未有的威胁。 随着人工智能越来越深入地融入日常业务流程&#xff0c;它面临着泄露专有信息、提供错误答…...

verilog实现FIR滤波系数生成(阶数,FIR滤波器类型及窗函数可调)

在以往采用 FPGA 实现的 FIR 滤波功能&#xff0c;滤波器系数是通过 matlab 计算生成&#xff0c;然后作为固定参数导入到 verilog 程序中&#xff0c;这尽管简单&#xff0c;但灵活性不足。在某些需求下&#xff08;例如捕获任意给定台站信号&#xff09;需要随时修改滤波器的…...

OSPF的不规则区域

1.远离骨干非骨干区域 2.不连续骨干 解决方案 tunnel ---点到点GRE 在合法与非ABR间建立隧道&#xff0c;然后将其宣告于OSPF协议中&#xff1b; 缺点&#xff1a;1、周期和触发信息对中间穿越区域造成资源占用&#xff08;当同一条路由来自不同区域&#xff0c;路由器会先…...

大数据新视界 --大数据大厂之 Ibis:独特架构赋能大数据分析高级抽象层

&#x1f496;&#x1f496;&#x1f496;亲爱的朋友们&#xff0c;热烈欢迎你们来到 青云交的博客&#xff01;能与你们在此邂逅&#xff0c;我满心欢喜&#xff0c;深感无比荣幸。在这个瞬息万变的时代&#xff0c;我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...

总结TypeScript相关知识

目录 引入认识特点安装使用变量声明类型推导 JS 和 TS 共有类型number类型boolean类型string类型Array类型null和undefined类型object类型symbol类型对象类型函数类型 可选和只读type 和 interface索引签名类型断言非空类型断言类型缩小严格赋值检测现象TS 新增类型字面量类型a…...

pdf怎么编辑修改内容?详细介绍6款pdf编辑器功能

■ pdf怎么编辑修改内容&#xff1f; PDF&#xff08;Portable Document Format&#xff09;作为一种广泛使用的文件格式&#xff0c;具有特点包括兼容性强、易于传输、文件安全性高、跨平台性、可读性强、完整性、可搜索性、安全性、可压缩性。 PDF文件本身是不可以直接进行编…...

【Blender Python】4.获取场景对象的几种方式

概述 有时候我们需要获取场景中已经添加或存在的对象。本节就总结在Blender Python中获取场景中对象的一些方法。 通过名称获取 py.data的objects()方法返回一个对象集合&#xff0c;可以使用键名或者下标形式获取具体的对象。 在默认新建的场景中&#xff0c;存在三个对象…...

鸿蒙harmonyos next flutter通信之EventChannel获取ohos系统时间

建立通道 flutter代码&#xff1a; EventChannel eventChannel EventChannel("com.xmg.eventChannel"); ohos代码&#xff1a; //定义eventChannelprivate eventChannel: EventChannel | null null//定义eventSinkprivate eventSink: EventSink | null null//建…...

Python 代码编写规范

本规范旨在为 Python 项目的代码编写提供一致性指南。它遵循 Python 社区的 PEP 8 标准&#xff0c;并结合了通用的编程最佳实践。 1. 编码风格 1.1 缩进 使用 4 个空格 作为缩进&#xff0c;不要使用制表符&#xff08;Tab&#xff09;。 def example():if True:print(&quo…...

k8s中pod的管理

一、资源管理 1.概述 说到k8s中的pod&#xff0c;即荚的意思&#xff0c;就不得不先提到k8s中的资源管理&#xff0c;k8s中可以用以下命令查看我们的资源&#xff1a; kubectl api-resources 比如我们现在需要使用k8s开启一个东西&#xff0c;那么k8s通过apiserver去对比etc…...

JavaScript中引用数据类型的浅拷贝

在JavaScript中&#xff0c;数据类型被分为“基本数据类型”和“引用数据类型”两大类。基本数据类型包括数值型、字符型、逻辑型、未定义型(undefined)、空型(null)和ES6新增的Symbol类型&#xff0c;引用数据类型包括数组、对象和函数。 当我们在程序中执行变量赋值操作的时候…...

自闭症寄宿学校陕西:提供综合发展的教育环境

在陕西这片古老而充满希望的土地上&#xff0c;有一所特殊的学校——星贝育园康复中心&#xff0c;它如同一座灯塔&#xff0c;照亮了无数自闭症儿童及其家庭前行的道路。这所全国规模较大的广泛性发育障碍全托寄宿制儿童康复训练机构&#xff0c;不仅以其专业的康复训练和独特…...

JS模块化工具requirejs详解

文章目录 JS模块化工具requirejs详解一、引言二、RequireJS 简介1、什么是 RequireJS2、RequireJS 的优势 三、RequireJS 的使用1、配置 RequireJS1.1、基础配置 2、定义模块3、加载模块 四、总结 JS模块化工具requirejs详解 一、引言 随着前端技术的快速发展&#xff0c;Jav…...

JavaScript中的异步编程:从回调到Promise

在JavaScript中&#xff0c;异步编程是一项至关重要的技能&#xff0c;它允许我们在不阻塞主线程的情况下执行耗时操作&#xff0c;如网络请求、文件读取或定时任务。随着JavaScript的发展&#xff0c;异步编程的模式也在不断演进&#xff0c;从最初的回调函数&#xff0c;到现…...

windows下DockerDesktop命令行方式指定目录安装

windows下DockerDesktop指定目录安装(重新安装) 因为DcokerDesktop占用内存较大, 并且拉去镜像后占用本地空间较多,所以建议安装时就更改默认安装路径和镜像存储路径 这里,展示了从下载到安装的过程: 首先下载DcokerDesktop;找到Docker Desktop Installer.exe 并重命名为 do…...

排查和解决JVM OOM实战

JVM OOM介绍 Java内存区域布局 下面的分析中都是基于JDK 8开始的。关于JMM不过多介绍每个区域的作用。OOM不单只会发生在堆内存&#xff0c;也可能是因为元空间或直接内存泄漏导致OOM&#xff0c;此时在OOM的详细信息中会有不同体现。 Java OOM的类别 java.lang.OutOfMemory…...

【Swift官方文档】7.Swift集合类型

集合类型 使用数组、集合和字典来组织数据。Swift 提供了三种主要的集合类型&#xff1a;数组、集合和字典&#xff0c;用于存储值的集合。数组是有序的值集合。集合是无序的唯一值集合。字典是无序的键值对集合。 Swift 中的数组、集合和字典始终清晰地指明它们可以存储的值…...

QT调用最新的libusb库

一&#xff1a;下载libusb文件 下载最新的库的下载网站&#xff1a;https://libusb.info/ 下载&#xff1a; 解压后目录如下&#xff1a; 二&#xff1a;库文件添加QT中 根据自己的编译器选择库&#xff1a; ①将头文件中添加libusb.h ②源文件中添加libusb-1.0.lib ③添加…...