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

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

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

学在前面

1、深拷贝和浅拷贝

        深拷贝和浅拷贝是两种不同的对象复制方式,它们涉及到对象的内存管理和数据成员的处理方式。若类拥有资源类(指针、文件句柄)的成员,类的对象间进行复制时,若资源重新进行分配,为深拷贝;否则为浅拷贝。以下以类中包含指针数据成员为例进行描述。

1.1 概念

        深拷贝:复制后新对象和旧对象的指针成员占用不同的内存空间。

        浅拷贝:复制后新对象和旧对象的指针成员占用相同的内存空间。

1.2 特征对比

        深拷贝

  • 复制对象的所有成员值。
  • 对于指针类型的成员,分配新的内存区域,并复制指针指向的实际数据。
  • 修改源对象或新对象的指针指向的数据,不会影响另一个对象。

        浅拷贝

  • 复制对象的所有成员值。
  • 对于指针类型的成员,仅复制指针值,而不复制指针指向的实际数据。
  • 如果源对象或新对象在生命周期内修改了指针指向的数据,则另一个对象也会受到影响。

1.3 示例

1.3.1  浅拷贝
#include <iostream>
#include <cstring>class ShallowCopy {
public:explicit ShallowCopy(const char *str){data_ = new char[strlen(str) + 1];strcpy(data_, str);}ShallowCopy(const ShallowCopy &other){data_ = other.data_;}~ShallowCopy(){delete[] data_;}void MemberAddress(const std::string &item) const{if (data_ == nullptr) {std::cout << "data_ is NULL" << std::endl;}std::cout << item << &data_ << std::endl;}
private:char *data_;
};int main()
{ShallowCopy obj1("ShallowCopy");ShallowCopy obj2 = obj1;obj1.MemberAddress("old obj address ");obj2.MemberAddress("new obj address ");return 0;
}

思考:

1、打印的地址预期一致,实际一致吗?

一致。

因为两个对象的指针成员在复制时,只是进行指针地址的复制,指向同一块内存。

2、代码会执行成功吗?

不会。

在obj1、obj2的作用域结束后,会执行析构函数,首先释放obj1的成员data_的内存,再释放obj2的成员data_的内存,由于两个内存指向同一地址,所以同一内存会释放两次,引发core dump,异常退出。

old obj address 0x7fff3e60bab0
new obj address 0x7fff3e60bab8
free(): double free detected in tcache 2
Aborted (core dumped)

3、 ShallowCopy obj2 = obj1;替换为如下两行可以吗?

ShallowCopy obj2;
obj2 = obj1;

 按照目前实现是不可以的,因为已自定义构造函数,默认的构造函数不会生成,obj2无对应的构造函数。

1.3.2  深拷贝
#include <iostream>
#include <cstring>
#include <string>class DeepCopy {
public:explicit DeepCopy(const char *str){data_ = new char[strlen(str) + 1];strcpy(data_, str);}DeepCopy(const DeepCopy &other){data_ = new char[strlen(other.data_) + 1];strcpy(data_, other.data_);}DeepCopy& operator=(const DeepCopy &other){if (this == &other) {return *this; // handle self-assignment}delete[] data_;data_ = new char[strlen(other.data_) + 1];strcpy(data_, other.data_);return *this;}~DeepCopy(){delete[] data_;}void MemberAddress(const std::string &item) const{if (data_ == nullptr) {std::cout << "data_ is NULL" << std::endl;} else {std::cout << item << ": " << &data_ << std::endl;}}private:char *data_;
};int main()
{DeepCopy copy1("Hello, World!");DeepCopy copy2 = copy1;  // Use the copy constructorcopy1.MemberAddress("old obj address of data_ ");copy2.MemberAddress("new obj address of data_ ");return 0;
}

执行结果:

old obj address of data_ : 0x7fff918a0970
new obj address of data_ : 0x7fff918a0978

1.4 图示深浅拷贝差异

浅拷贝

深拷贝

2、RAII

2.1  概念

RAII是Resource Acquisition Is Initialization的缩写,它是一种管理资源的技术,其核心思想是将资源的获取与对象的初始化绑定在一起,并通过对象的生命周期来自动管理资源的释放。

2.2  特征

资源获取即初始化:当对象被创建时,自动获取所需的资源,通常在构造函数中完成。

析构函数管理资源释放:当对象被销毁时,析构函数用于释放资源。

异常安全性:RAII机制确保即使在发生异常的情况下,资源也能被正确释放。

2.3 示例

2.3.1 文件句柄类
class FileHandle {
public:FileHandle(const std::string &filename, std::ios_base::openmode mode) {file_.open(filename, mode);if (!file_.is_open()) {throw std::runtime_error("Failed to open file: " + filename);}}~FileHandle() {if (file_.is_open()) {file_.close();}}private:std::fstream file_; 
};
2.3.2 RAII妙用——时间戳打点 
class TimestampLogger {
public:TimestampLogger(const std::string &description): description_(description), start_(std::chrono::steady_clock::now()) {}~TimestampLogger() {auto end = std::chrono::steady_clock::now();std::chrono::duration<double> elapsed = end - start_;std::cout << description_ << " took " << elapsed.count() << " seconds.\n";}// 禁用拷贝构造函数和赋值运算符,防止资源泄露或重复打点TimestampLogger(const TimestampLogger &) = delete;TimestampLogger &operator=(const TimestampLogger &) = delete;private:std::string description_;std::chrono::steady_clock::time_point start_;
};

一、三之法则

1、概念

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

2、使用场景

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

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

3、如何实现

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

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

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

4、示例

见1.3.1

二、五之法则

1、概念

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

2、使用场景

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

3、如何实现

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

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

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

4、示例

4.1 socket描述符

class SocketDescriptor {
public:SocketDescriptor() : fd(-1) {}explicit SocketDescriptor(int socket_fd) : fd(socket_fd){if (fd == -1) {throw std::runtime_error("Invalid socket descriptor");}}~SocketDescriptor(){closeSocket();}SocketDescriptor(const SocketDescriptor &) = delete;SocketDescriptor &operator=(const SocketDescriptor&) = delete; SocketDescriptor(SocketDescriptor &&other) noexcept : fd(other.fd){other.fd = -1;}SocketDescriptor &operator=(SocketDescriptor&& other) noexcept{if (this != &other) {closeSocket();fd = other.fd;other.fd = -1;}return *this;}private:void closeSocket(){if (fd != -1) {::close(fd);fd = -1;}}private:int fd;
};

4.2 拷贝"大"数据

class LargeData {
public:LargeData() = default;explicit LargeData(size_t dataSize){try {data = new char[dataSize];this->dataSize = dataSize;std::fill_n(data, dataSize, 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;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;}
private:size_t dataSize;char* data;
};

三、零之法则

1、概念

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

2、使用场景

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

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

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

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

3、如何实现

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

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

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

4、示例

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

4.2中类使用标准库函数重新实现,对象的内存管理交由标准库进行处理。

class LargeData {
public:LargeData() = default;explicit LargeData(size_t dataSize) : data(dataSize, '\0') {}private:std::string data; // 使用std::string来存储数据
};

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.rhkb.cn/news/440819.html

如若内容造成侵权/违法违规/事实不符,请联系长河编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

【redis-05】redis保证和mysql数据一致性

redis系列整体栏目 内容链接地址【一】redis基本数据类型和使用场景https://zhenghuisheng.blog.csdn.net/article/details/142406325【二】redis的持久化机制和原理https://zhenghuisheng.blog.csdn.net/article/details/142441756【三】redis缓存穿透、缓存击穿、缓存雪崩htt…

57.对称二叉树

迭代 class Solution {public boolean isSymmetric(TreeNode root) {if(rootnull){return true;}Deque<TreeNode> denew LinkedList<>();TreeNode l,r;int le;de.offer(root.left);de.offer(root.right);while(!de.isEmpty()){lde.pollFirst();rde.pollLast();if(…

BMC pam认证的使用

1.说明 1.1 文档参考资料 https://www.chiark.greenend.org.uk/doc/libpam-doc/html/Linux-PAM_ADG.htmlhttp://www.fifi.org/doc/libpam-doc/html/pam_appl-3.htmlpdf文档: https://fossies.org/linux/Linux-PAM-docs/doc/adg/Linux-PAM_ADG.pdflinux-pam 中文文档pam 旧文p…

<数据集>工程机械识别数据集<目标检测>

数据集格式&#xff1a;VOCYOLO格式 图片数量&#xff1a;2644张 标注数量(xml文件个数)&#xff1a;2644 标注数量(txt文件个数)&#xff1a;2644 标注类别数&#xff1a;3 标注类别名称&#xff1a;[dump truck, wheel loader, excavators] 序号类别名称图片数框数1dum…

SpringBootWeb快速入门!详解如何创建一个简单的SpringBoot项目?

在现代Web开发中&#xff0c;SpringBoot以其简化的配置和快速的开发效率而受到广大开发者的青睐。本篇文章将带领你从零开始&#xff0c;搭建一个基于SpringBoot的简单Web应用~ 一、前提准备 想要创建一个SpringBoot项目&#xff0c;需要做如下准备&#xff1a; idea集成开发…

wordpress Contact form 7发件人邮箱设置

此教程仅适用于演示站有留言的主题&#xff0c;演示站没有留言的主题&#xff0c;就别往下看了&#xff0c;免费浪费时间。 使用了Contact form 7插件的简站WordPress主题&#xff0c;在有人留言时&#xff0c;就会发邮件到网站的系统邮箱(一般与管理员邮箱为同一个)里。上面显…

SpringCloud简介 Ribbon Eureka 远程调用RestTemplate类 openfeign

〇、SpringCloud 0.区别于单体项目和soa架构&#xff0c;微服务架构每个服务独立&#xff0c;灵活。 1. spring cloud是一个完整的微服务框架&#xff0c;springCloud包括三个体系&#xff1a; spring cloud Netflix spring cloud Alibaba spring 其他 2.spring cloud 版本命名…

论文阅读笔记-LogME: Practical Assessment of Pre-trained Models for Transfer Learning

前言 在NLP领域,预训练模型(准确的说应该是预训练语言模型)似乎已经成为各大任务必备的模块了,经常有看到文章称后BERT时代或后XXX时代,分析对比了许多主流模型的优缺点,这些相对而言有些停留在理论层面,可是有时候对于手上正在解决的任务,要用到预训练语言模型时,面…

软件设计师(软考学习)

数据库技术 数据库基础知识 1. 数据库中的简单属性、多值属性、复合属性、派生属性简单属性&#xff1a;指不能够再分解成更小部分的属性&#xff0c;通常是数据表中的一个列。例如学生表中的“学号”、“姓名”等均为简单属性。 多值属性&#xff1a;指一个属性可以有多个值…

sql-labs:42~65

less42&#xff08;单引号闭合、报错回显&#xff09; login_useradmin login_password123 and if(11,sleep(2),1) # # 单引号闭合 ​ login_useradmin login_password123and updatexml(1,concat(0x7e,database(),0x7e),1)# # 报错回显…

mysql UDF提权(实战案例)

作者&#xff1a;程序那点事儿 日期&#xff1a;2024/09/29 16:10 什么是UDF? 全称 User Define Function &#xff08;用户自定义函数&#xff09;UDF提权&#xff0c;就是通过自定义函数&#xff0c;实现执行系统的命令。 dll&#xff08;windows&#xff0c;dll文件是c语…

5.人员管理模块(以及解决运行Bug)——帝可得管理系统

目录 前言一、页面修改表单展示修改 二、新增对话框修改三、修改对话框修改修改时展示创建时间 四、解决页面展示错误五 、 解决【java.lang.NullPointerException: null】 Bug 前言 提示&#xff1a;本篇完成人员管理模块的开发&#xff0c;具体需求、修改代码的路径和最终效…

车辆种类分类识别数据集,可以识别7种汽车类型,已经按照7:2:1比 例划分数据集,训练集1488张、验证集507张,测试集31张, 共计2026张。

车 车辆种类分类识别数据集&#xff0c;可以识别7种汽车类型&#xff0c;已经按照7:2:1比 例划分数据集&#xff0c;训练集1488张、验证集507张,测试集31张&#xff0c; 共计2026张。 数据集分为一类客车(tinycar) &#xff0c;类客车(midcar) &#xff0c;三类 客车(bigcar) ,…

重学SpringBoot3-集成Redis(六)之消息队列

更多SpringBoot3内容请关注我的专栏&#xff1a;《SpringBoot3》 期待您的点赞&#x1f44d;收藏⭐评论✍ 重学SpringBoot3-集成Redis&#xff08;六&#xff09;之消息队列 1. 什么是发布/订阅&#xff08;Pub/Sub&#xff09;&#xff1f;2. 场景应用3. Spring Boot 3 整合 R…

感知机学习算法

感知机 一、感知机简介二、感知机模型2.1 感知机的基本组成2.2 求和函数2.2.1 时间总合2.2.2 空间总合 2.3 激活函数2.4 学习算法2.4.1 赫布学习规则2.4.2 Delta学习规则 三、 结论参考文献 一、感知机简介 M-P神经元模型因其对生物神经元激发过程的极大简化而成为神经网络研究…

微信小程序学习实录10:轻松获取用户昵称、头像与登录openid实战攻略

在微信小程序开发中&#xff0c;获取用户的个人信息&#xff08;如昵称和头像&#xff09;以及用户的唯一标识OpenID是非常常见的需求。本文将详细介绍如何通过微信提供的API来实现这些功能&#xff0c;并提供一个完整的实战案例。 用户选择头像 微信提供了chooseAvatar组件&…

ROS基础入门——实操教程

ROS基础入门——实操教程 前言 本教程实操为主&#xff0c;少说书。可供参考的文档中详细的记录了ROS的实操和理论&#xff0c;只是过于详细繁杂了&#xff0c;看得脑壳疼&#xff0c;于是做了这个笔记。 Ruby Rose&#xff0c;放在这里相当合理 本文初编辑于2024年10月4日 C…

使用vscode调试wails项目(golang桌面GUI)

文章目录 安装 Golang 环境安装 NPM安装 VSCode安装 Go 插件安装 Go 插件依赖工具安装 Wails系统检查 准备项目Visual Studio Code 配置安装和构建步骤参考资料 安装 Golang 环境 访问 golang 官网下载环境安装包&#xff1a;https://go.dev/dl/ 安装 NPM 从 Node 下载页面 …

时序必读论文16|ICLR24 CARD:通道对齐鲁棒混合时序预测Transformer

论文标题&#xff1a;CARD: Channel Aligned Robust Blend Transformer for Time Series Forecasting 论文链接&#xff1a;https://arxiv.org/abs/2305.12095 代码链接&#xff1a;https://github.com/wxie9/CARD 前言 Transformer取得成功的一个关键因素是通道独立&#…

鸿蒙开发之ArkUI 界面篇 十九 Flex组件的特点

其语法格式是: Flex(参数对象){ 字组件1, 字组件2, 字组件3, 字组件4 } 这里你会发现&#xff0c;其实和Row容器&#xff0c;Colum容器的语法格式差不多&#xff0c;核心的关键是Colum、Row是不支持换行&#xff0c;实现FlexInterface接口&#xff0c;对外提供的属性是F…