类的特殊成员函数——三之法则、五之法则、零之法则
系统中的动态资源、文件句柄(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//建…...

SpringBoot+uniapp 的 Champion 俱乐部微信小程序设计与实现,论文初版实现
摘要 本论文旨在设计并实现基于 SpringBoot 和 uniapp 的 Champion 俱乐部微信小程序,以满足俱乐部线上活动推广、会员管理、社交互动等需求。通过 SpringBoot 搭建后端服务,提供稳定高效的数据处理与业务逻辑支持;利用 uniapp 实现跨平台前…...

PL0语法,分析器实现!
简介 PL/0 是一种简单的编程语言,通常用于教学编译原理。它的语法结构清晰,功能包括常量定义、变量声明、过程(子程序)定义以及基本的控制结构(如条件语句和循环语句)。 PL/0 语法规范 PL/0 是一种教学用的小型编程语言,由 Niklaus Wirth 设计,用于展示编译原理的核…...
Android Bitmap治理全解析:从加载优化到泄漏防控的全生命周期管理
引言 Bitmap(位图)是Android应用内存占用的“头号杀手”。一张1080P(1920x1080)的图片以ARGB_8888格式加载时,内存占用高达8MB(192010804字节)。据统计,超过60%的应用OOM崩溃与Bitm…...
Python Einops库:深度学习中的张量操作革命
Einops(爱因斯坦操作库)就像给张量操作戴上了一副"语义眼镜"——让你用人类能理解的方式告诉计算机如何操作多维数组。这个基于爱因斯坦求和约定的库,用类似自然语言的表达式替代了晦涩的API调用,彻底改变了深度学习工程…...
git: early EOF
macOS报错: Initialized empty Git repository in /usr/local/Homebrew/Library/Taps/homebrew/homebrew-core/.git/ remote: Enumerating objects: 2691797, done. remote: Counting objects: 100% (1760/1760), done. remote: Compressing objects: 100% (636/636…...
redis和redission的区别
Redis 和 Redisson 是两个密切相关但又本质不同的技术,它们扮演着完全不同的角色: Redis: 内存数据库/数据结构存储 本质: 它是一个开源的、高性能的、基于内存的 键值存储数据库。它也可以将数据持久化到磁盘。 核心功能: 提供丰…...
第八部分:阶段项目 6:构建 React 前端应用
现在,是时候将你学到的 React 基础知识付诸实践,构建一个简单的前端应用来模拟与后端 API 的交互了。在这个阶段,你可以先使用模拟数据,或者如果你的后端 API(阶段项目 5)已经搭建好,可以直接连…...

客户案例 | 短视频点播企业海外视频加速与成本优化:MediaPackage+Cloudfront 技术重构实践
01技术背景与业务挑战 某短视频点播企业深耕国内用户市场,但其后台应用系统部署于东南亚印尼 IDC 机房。 随着业务规模扩大,传统架构已较难满足当前企业发展的需求,企业面临着三重挑战: ① 业务:国内用户访问海外服…...

PH热榜 | 2025-06-08
1. Thiings 标语:一套超过1900个免费AI生成的3D图标集合 介绍:Thiings是一个不断扩展的免费AI生成3D图标库,目前已有超过1900个图标。你可以按照主题浏览,生成自己的图标,或者下载整个图标集。所有图标都可以在个人或…...
Windows 下端口占用排查与释放全攻略
Windows 下端口占用排查与释放全攻略 在开发和运维过程中,经常会遇到端口被占用的问题(如 8080、3306 等常用端口)。本文将详细介绍如何通过命令行和图形化界面快速定位并释放被占用的端口,帮助你高效解决此类问题。 一、准…...