类的特殊成员函数——三之法则、五之法则、零之法则
系统中的动态资源、文件句柄(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;
}
相关文章:
类的特殊成员函数——三之法则、五之法则、零之法则
系统中的动态资源、文件句柄(socket描述符、文件描述符)是有限的,在类中若涉及对此类资源的操作,但是未做到妥善的管理,常会造成资源泄露问题,严重的可能造成资源不可用。或引发未定义行为,进而…...
计算机毕业设计 智慧物业服务系统的设计与实现 Java实战项目 附源码+文档+视频讲解
博主介绍:✌从事软件开发10年之余,专注于Java技术领域、Python人工智能及数据挖掘、小程序项目开发和Android项目开发等。CSDN、掘金、华为云、InfoQ、阿里云等平台优质作者✌ 🍅文末获取源码联系🍅 👇🏻 精…...
Python软体中使用SpaCy进行命名实体识别
Python软体中使用SpaCy进行命名实体识别 命名实体识别(Named Entity Recognition,NER)是自然语言处理(NLP)中的一个重要任务,它涉及识别文本中的命名实体,例如人名、地名、组织名等。SpaCy是一种流行的NLP库,提供了高效的NER功能。在本文中,我们将介绍如何使用SpaCy进…...
华为云技术深度解析:以系统性创新加速智能化升级
华为云技术深度解析:以系统性创新加速智能化升级 在当今数字化转型的浪潮中,云计算作为关键的基础设施,正以前所未有的速度推动着各行各业的智能化升级。作为全球领先的云服务提供商,华为云凭借其深厚的技术积累和创新实力&#…...
推理攻击-Python案例
1、本文通过推理攻击的方式来估计训练集中每个类别的样本数量、某样本是否在训练集中。 2、一种简单的实现方法:用模型对训练数据标签进行拟合,拟合结果即推理为训练集中的情况。 3、了解这些案例可以帮助我们更好的保护数据隐私。 推理攻击(…...
find_box_3d
参数 (ObjectModel3DScene, SideLen1, SideLen2, SideLen3, MinScore, GenParam : GrippingPose, Score, ObjectModel3DBox, BoxInformation) 入参介绍 1,ObjectModel3DScene, 输入的3d模型,这个模型最好是由xyx三通道点…...
Visual Studio2017编译GDAL3.0.2源码过程
一、编译环境 操作系统:Windows 10企业版 编译工具:Visual Studio 2017旗舰版 源码版本:gdal3.0.2 二、生成解决方案 打开Visual Studio 2017的x64本机生成工具,切换到gdal3.0.2源码根目录;执行generate_vcxproj.b…...
计算机网络——email
pop3拉出来 超出ASCII码范围就不让传了 这样就可以传更大的文件...
【Linux】信号知识三把斧——信号的产生、保存和处理
目录 1、关于信号的前置知识 1.1.什么是信号? 1.2.为什么要学习信号? 1.3.如何学习信号? 1.4.一些常见的信号 1.5.信号的处理方式 1.6.为什么每一个进程都可以系统调用? 2.信号的产生 2.1.kill命令产生信号…...
【国庆要来了】基于Leaflet的旅游路线WebGIS可视化实践
前言 转眼2024年的国庆节马上就要来临了,估计很多小伙伴都计划好了旅游路线。金秋十月,不管是选择出门去看看风景,还是选择在家里看人。从自己生活惯了的城市去别人生活惯了的城市,去感受城市烟火、去感受人文风景,为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中读取当前主题状态,否则,刷新页面就会恢复默…...
人工智能与机器学习原理精解【31】
文章目录 卷积神经网络CNN定义数学原理与公式计算与定理架构例子例题 全连接层的前馈计算定义数学原理与公式计算过程示例 参考文献 卷积神经网络 CNN 即卷积神经网络(Convolutional Neural Networks),是一类包含卷积计算且具有深度结构的前…...
如何安全地大规模部署 GenAI 应用程序
大型语言模型和其他形式的生成式人工智能(GenAI) 的广泛使用带来了许多组织可能没有意识到的安全风险。幸运的是,网络和安全提供商正在寻找方法来应对这些前所未有的威胁。 随着人工智能越来越深入地融入日常业务流程,它面临着泄露专有信息、提供错误答…...
verilog实现FIR滤波系数生成(阶数,FIR滤波器类型及窗函数可调)
在以往采用 FPGA 实现的 FIR 滤波功能,滤波器系数是通过 matlab 计算生成,然后作为固定参数导入到 verilog 程序中,这尽管简单,但灵活性不足。在某些需求下(例如捕获任意给定台站信号)需要随时修改滤波器的…...
OSPF的不规则区域
1.远离骨干非骨干区域 2.不连续骨干 解决方案 tunnel ---点到点GRE 在合法与非ABR间建立隧道,然后将其宣告于OSPF协议中; 缺点:1、周期和触发信息对中间穿越区域造成资源占用(当同一条路由来自不同区域,路由器会先…...
大数据新视界 --大数据大厂之 Ibis:独特架构赋能大数据分析高级抽象层
💖💖💖亲爱的朋友们,热烈欢迎你们来到 青云交的博客!能与你们在此邂逅,我满心欢喜,深感无比荣幸。在这个瞬息万变的时代,我们每个人都在苦苦追寻一处能让心灵安然栖息的港湾。而 我的…...
总结TypeScript相关知识
目录 引入认识特点安装使用变量声明类型推导 JS 和 TS 共有类型number类型boolean类型string类型Array类型null和undefined类型object类型symbol类型对象类型函数类型 可选和只读type 和 interface索引签名类型断言非空类型断言类型缩小严格赋值检测现象TS 新增类型字面量类型a…...
pdf怎么编辑修改内容?详细介绍6款pdf编辑器功能
■ pdf怎么编辑修改内容? PDF(Portable Document Format)作为一种广泛使用的文件格式,具有特点包括兼容性强、易于传输、文件安全性高、跨平台性、可读性强、完整性、可搜索性、安全性、可压缩性。 PDF文件本身是不可以直接进行编…...
【Blender Python】4.获取场景对象的几种方式
概述 有时候我们需要获取场景中已经添加或存在的对象。本节就总结在Blender Python中获取场景中对象的一些方法。 通过名称获取 py.data的objects()方法返回一个对象集合,可以使用键名或者下标形式获取具体的对象。 在默认新建的场景中,存在三个对象…...
鸿蒙harmonyos next flutter通信之EventChannel获取ohos系统时间
建立通道 flutter代码: EventChannel eventChannel EventChannel("com.xmg.eventChannel"); ohos代码: //定义eventChannelprivate eventChannel: EventChannel | null null//定义eventSinkprivate eventSink: EventSink | null null//建…...
Lombok 的 @Data 注解失效,未生成 getter/setter 方法引发的HTTP 406 错误
HTTP 状态码 406 (Not Acceptable) 和 500 (Internal Server Error) 是两类完全不同的错误,它们的含义、原因和解决方法都有显著区别。以下是详细对比: 1. HTTP 406 (Not Acceptable) 含义: 客户端请求的内容类型与服务器支持的内容类型不匹…...
基于uniapp+WebSocket实现聊天对话、消息监听、消息推送、聊天室等功能,多端兼容
基于 UniApp + WebSocket实现多端兼容的实时通讯系统,涵盖WebSocket连接建立、消息收发机制、多端兼容性配置、消息实时监听等功能,适配微信小程序、H5、Android、iOS等终端 目录 技术选型分析WebSocket协议优势UniApp跨平台特性WebSocket 基础实现连接管理消息收发连接…...
关于nvm与node.js
1 安装nvm 安装过程中手动修改 nvm的安装路径, 以及修改 通过nvm安装node后正在使用的node的存放目录【这句话可能难以理解,但接着往下看你就了然了】 2 修改nvm中settings.txt文件配置 nvm安装成功后,通常在该文件中会出现以下配置&…...
MODBUS TCP转CANopen 技术赋能高效协同作业
在现代工业自动化领域,MODBUS TCP和CANopen两种通讯协议因其稳定性和高效性被广泛应用于各种设备和系统中。而随着科技的不断进步,这两种通讯协议也正在被逐步融合,形成了一种新型的通讯方式——开疆智能MODBUS TCP转CANopen网关KJ-TCPC-CANP…...
视频字幕质量评估的大规模细粒度基准
大家读完觉得有帮助记得关注和点赞!!! 摘要 视频字幕在文本到视频生成任务中起着至关重要的作用,因为它们的质量直接影响所生成视频的语义连贯性和视觉保真度。尽管大型视觉-语言模型(VLMs)在字幕生成方面…...
vue3 定时器-定义全局方法 vue+ts
1.创建ts文件 路径:src/utils/timer.ts 完整代码: import { onUnmounted } from vuetype TimerCallback (...args: any[]) > voidexport function useGlobalTimer() {const timers: Map<number, NodeJS.Timeout> new Map()// 创建定时器con…...
大语言模型(LLM)中的KV缓存压缩与动态稀疏注意力机制设计
随着大语言模型(LLM)参数规模的增长,推理阶段的内存占用和计算复杂度成为核心挑战。传统注意力机制的计算复杂度随序列长度呈二次方增长,而KV缓存的内存消耗可能高达数十GB(例如Llama2-7B处理100K token时需50GB内存&a…...
HashMap中的put方法执行流程(流程图)
1 put操作整体流程 HashMap 的 put 操作是其最核心的功能之一。在 JDK 1.8 及以后版本中,其主要逻辑封装在 putVal 这个内部方法中。整个过程大致如下: 初始判断与哈希计算: 首先,putVal 方法会检查当前的 table(也就…...
【JVM面试篇】高频八股汇总——类加载和类加载器
目录 1. 讲一下类加载过程? 2. Java创建对象的过程? 3. 对象的生命周期? 4. 类加载器有哪些? 5. 双亲委派模型的作用(好处)? 6. 讲一下类的加载和双亲委派原则? 7. 双亲委派模…...
Selenium常用函数介绍
目录 一,元素定位 1.1 cssSeector 1.2 xpath 二,操作测试对象 三,窗口 3.1 案例 3.2 窗口切换 3.3 窗口大小 3.4 屏幕截图 3.5 关闭窗口 四,弹窗 五,等待 六,导航 七,文件上传 …...
