C++ 类的行为 | 行为像值的类、行为像指针的类、swap函数处理自赋值

文章目录

  • 概念
  • 行为像值的类
  • 行为像指针的类
    • 概念
    • 引用计数
      • 动态内存实现计数器
  • 类的swap
    • 概念
    • swap实现自赋值


概念

行为像值的类和行为像指针的类这两种说法其实蛮拗口的,这也算是 《C++Primer》 翻译的缺点之一吧。。。

其实两者的意思分别是:

  • 行为像值的类: 每个类的对象都有自己的实现
  • 行为像指针的类: 所有类的对象共享类的资源(类似于 shared_ptr 智能指针,每有一个对象持有该资源则引用计数+1,每有一个对象释放该资源则引用计数-1,引用计数为0时释放内存)

本篇博客的内容跟 类 和 智能指针 两篇博客有关。不了解的同学可以先看看这两篇博客。


行为像值的类

对于类管理的资源,每个对象都应该有一份自己的拷贝(实现)。如下面的 string类型的指针 ,使用拷贝构造函数 or 赋值运算符时,每个对象拷贝的都是 指针成员ps 指向的 string 而非 ps本身 。换言之,每个对象 都有一个ps 而不是 给ps加引用计数

class A
{int i = 0;string* ps;
public:A(const string &s = string()): ps(new string(s)), i(0) {}A(const A &a): ps(new string(*a.ps)), i(a.i) {}A& operator=(const A&);~A() { delete ps; }
};A& A::operator=(const A& a)
{string* newps = new string(*a.ps); // 将a.ps指向的值拷贝到局部临时对象newps中delete ps;  // 销毁ps指向的内存,避免旧内存泄漏ps = newps; i = a.i;return *this; // 返回此对象的引用
}

为什么不能像下面这样实现赋值运算符呢?

A& A::operator=(const A& a)
{delete ps;  // 销毁ps指向的内存,避免内存泄漏ps = new string(*(a.ps)); i = a.i;return *this; // 返回此对象的引用
}

这是因为如果 a*this同一个对象delete ps 会释放 *thisa 指向的 string。接下来,当我们在 new表达式 中试图拷贝*(a.ps)时,就会访问一个指向无效内存的指针(即空悬指针),其行为和结果是未定义的。

因此,第一种实现方法可以确保销毁 *this 的现有成员操作是绝对安全的,不会产生空悬指针


行为像指针的类

概念

对于行为类似指针的类,使用拷贝构造函数 or 赋值运算符时,每个对象拷贝的都是 ps本身 而非 指针成员ps 指向的 string。换言之,每有一个对象都是 给指向string的ps加引用计数

因此,析构函数不能粗暴地释放 ps 指向的 string ,只有当最后一个指向 stringA类对象 销毁时,才可以释放 string 。我们会发现这个特性很符合 shared_ptr 的功能,因此我们可以使用 shared_ptr 来管理 像指针的类 中的资源

但是,有时我们需要程序员直接管理资源,因此就要用到 引用计数(reference count) 了。


引用计数

工作方式:

  • 每个构造函数(拷贝构造函数除外)都要创建一个引用计数,用来记录有多少对象与正在创建的对象共享状态。当我们创建一个对象时,只有一个对象共享状态,因此将计数器初始化为1。
  • 拷贝构造函数不分配新的计数器,而是拷贝给定对象的数据成员,包括计数器。拷贝构造函数递增共享的计数器,指出给定对象的状态又被一个新用户所共享。
  • 析构函数递减计数器,指出共享状态的用户少了一个。如果计数器变为0,则析构函数释放状态。
  • 拷贝赋值运算符递增右侧运算对象的计数器,递减左侧运算对象的计数器。如果左侧运算对象的计数器变为0,意味着它的共享状态没有用户了,拷贝赋值运算符就必须销毁状态。

唯一的难题是确定在哪里存放引用计数。计数器不能直接作为 A对象 的成员。举个例子:

A a1("cmy");
A a2(a1); // a2和a1指向相同的string
A a3(a2); // a1、a2、a3都指向相同的string

如果计数器保存在每个对象中,创建 a2 时可以递增 a1 的计数器并拷贝到 a2 中。可创建 a3 时,诚然可以更新 a1 的计数器,但怎么找到 a2 并将它的计数器更新呢?

那么怎么处理计数器呢?


动态内存实现计数器

class A
{int i = 0;string *ps;size_t *use; // 记录有多少个对象共享*ps的成员
public:A(const string &s = string()): ps(new string(s)), i(0), use(new size_t(1)) {}A(const A &a): ps(new string(*a.ps)), i(a.i), use(a.use) { ++*use; }A& operator=(const A&);~A() {}
};
A::~A(){if(--*use == 0){ // 引用计数变为0delete ps; // 释放string内存delete use; // 释放计数器内存}
}
A& A::operator=(const A& a)
{++*(a.use); // 之所以将计数器自增操作放这么前// 是为了防止自赋值时计数器自减导致ps、use直接被释放if(--(*use) == 0){delete ps;delete use;}ps = a.ps;i = a.i;use = a.use;return *this; // 返回此对象的引用
}

类的swap

概念

我们在设计类的 swap 时,虽然逻辑上是这样:

A tmp = a1;
a1 = a2;
a2 = tmp;

但如果真的这样实现的话,还需要创建一个新的对象 tmp,效率是很低的,造成了内存空间的浪费。因此我们实际上希望的是这样的逻辑实现:

string *tmp = a1.ps;
a1.ps = a2.ps;
a2.ps = tmp;

创建一个 string类型 总比创建一个 A类对象 要省内存。具体实现:

class A
{friend void swap(A&, A&);
};
inline void swap(A& a1, A& a2){using std::swap;swap(a1.ps, a2.ps);swap(a1.i, a2.i);
}

swap实现自赋值

使用拷贝和交换的赋值运算符:

A& A::operator=(A a){ // 传值,使用拷贝构造函数通过实参(右侧运算对象)拷贝生成临时量aswap(*this, a); // a现在指向*this曾使用的内存return *this; // a的作用域结束,被销毁,delete了a中的ps
}

上面重载的赋值运算符参数并不是一个引用,也就是说 a 是右侧运算对象的一个副本。

在函数体中,swap 交换了 a 和 *this 中的数据成员。*thisps 指向右侧运算对象中 string 的一个副本;*this 原来的 ps 存入 a 中。但函数体执行完,a 作为局部变量被销毁,deletea 中的 ps,即 释放掉了左侧运算对象(*this)中原来的内存。

这个技术的有趣之处是它自动处理了自赋值情况且天然就是异常安全的。

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

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

相关文章

C++ 右值引用 | 左值、右值、move、移动语义、引用限定符

文章目录C11为什么引入右值?区分左值引用、右值引用move移动语义移动构造函数移动赋值运算符合成的移动操作小结引用限定符规定this是左值or右值引用限定符与重载C11为什么引入右值? C11引入了一个扩展内存的方法——移动而非拷贝,移动较之拷…

且谈关于最近软件测试的面试

前段时间有新的产品需要招人,安排和参加了好几次面试,下面就谈谈具体的面试问题,在面试他人的同时也面试自己。 面试问题是参与面试同事各自设计的,我也不清楚其他同事的题目,就谈谈自己设计的其中2道题。 过去面试总是…

C++ 多态 | 虚函数、抽象类、虚函数表

文章目录多态虚函数重写重定义(参数不同)协变(返回值不同)析构函数重写(函数名不同)final和override重载、重写、重定义抽象类多态的原理虚函数常见问题解析虚函数表多态 一种事物,多种形态。换…

C++ 运算符重载(一) | 输入/输出,相等/不等,复合赋值,下标,自增/自减,成员访问运算符

文章目录输出运算符<<输入运算符>>相等/不等运算符复合赋值运算符下标运算符自增/自减运算符成员访问运算符输出运算符<< 通常情况下&#xff0c;输出运算符的第一个形参是一个 非常量ostream对象的引用 。之所以 ostream 是非常量是因为向流写入内容会改变…

C++ 重载函数调用运算符 | 再探lambda,函数对象,可调用对象

文章目录重载函数调用运算符lambdalambda等价于函数对象lambda等价于类标准库函数对象可调用对象与function可调用对象function函数重载与function重载函数调用运算符 函数调用运算符必须是成员函数。 一个类可以定义多个不同版本的调用运算符&#xff0c;互相之间应该在参数数…

C++ 运算符重载(二) | 类型转换运算符,二义性问题

文章目录类型转换运算符概念避免过度使用类型转换函数解决上述问题的方法转换为 bool显式的类型转换运算符类型转换二义性重载函数与类型转换结合导致的二义性重载运算符与类型转换结合导致的二义性类型转换运算符 概念 类型转换运算符&#xff08;conversion operator&#…

Tomcat中JVM内存溢出及合理配置

Tomcat本身不能直接在计算机上运行&#xff0c;需要依赖于硬件基础之上的操作系统和一个Java虚拟机。Tomcat的内存溢出本质就是JVM内存溢出&#xff0c;所以在本文开始时&#xff0c;应该先对Java JVM有关内存方面的知识进行详细介绍。 一、Java JVM内存介绍 JVM管理两种类型的…

俄罗斯农民乘法 | 快速乘

文章目录概念概念 俄罗斯农民乘法经常被用于两数相乘取模的场景&#xff0c;如果两数相乘已经超过数据范围&#xff0c;但取模后不会超过&#xff0c;我们就可以利用这个方法来拆位取模计算贡献&#xff0c;保证每次运算都在数据范围内。 A 和 B 两数相乘的时候我们如何利用加…

Linux网络编程 | socket选项设定 及 网络信息API

文章目录读取和设置 socket 选项SO_REUSEADDRSO_RCVBUF 和 SO_SNDBUFSO_RCVLOWAT 和 SO_SNDLOWATSO_LINGER 选项网络信息APIgethostbyname 和 gethostbyaddrgetservbyname 和 getservbyportgetaddrinfogetnameinfo读取和设置 socket 选项 正如 fcntl 系统调用是控制文件描述符…

Linux | 高级I/O函数

文章目录创建文件描述符的函数pipe函数dup函数、dup2函数读取或写入数据readv函数、writev函数零拷贝sendfile函数splice函数tee函数进程间通信——共享内存mmap函数 和 munmap函数控制文件描述符fcntl函数创建文件描述符的函数 pipe函数 不再赘述&#xff0c;详情见我的另一…

分布式理论:CAP、BASE | 分布式存储与一致性哈希

文章目录分布式理论CAP定理BASE理论分布式存储与一致性哈希简单哈希一致性哈希虚拟节点分布式理论 CAP定理 一致性&#xff08;Consistency&#xff09;&#xff1a; 在分布式系统中的所有数据副本&#xff0c;在同一时刻是否一致&#xff08;所有节点访问同一份最新的数据副…

Tomcat服务器性能优化

一、概述 本文档主要介绍了Tomcat的性能调优的原理和方法。可作为公司技术人员为客户Tomcat系统调优的技术指南&#xff0c;也可以提供给客户的技术人员作为他们性能调优的指导手册。 二、调优分类 由于Tomcat的运行依赖于JVM&#xff0c;从虚拟机的角度我们把Tomcat的调整分为…

分布式系统概念 | 分布式事务:2PC、3PC、本地消息表

文章目录分布式事务2PC&#xff08;二阶段提交协议&#xff09;执行流程优缺点3PC&#xff08;三阶段提交协议&#xff09;执行流程优缺点本地消息表&#xff08;异步确保&#xff09;分布式事务 分布式事务就是指事务的参与者、支持事务的服务器、资源服务器以及事务管理器分…

数据结构算法 | 单调栈

文章目录算法概述题目下一个更大的元素 I思路代码下一个更大元素 II思路代码132 模式思路代码接雨水思路算法概述 当题目出现 「找到最近一个比其大的元素」 的字眼时&#xff0c;自然会想到 「单调栈」 。——三叶姐 单调栈以严格递增or递减的规则将无序的数列进行选择性排序…

最长下降子序列

文章目录题目解法DP暴搜思路代码实现贪心二分思路代码实现题目 给出一组数据 nums&#xff0c;求出其最长下降子序列&#xff08;子序列允许不连续&#xff09;的长度。&#xff08;类似于lc的最长递增子序列&#xff09; 示例&#xff1a; 输入&#xff1a; 6 // 数组元素个…

Linux 服务器程序规范、服务器日志、用户、进程间的关系

文章目录服务器程序规范日志rsyslogd 守护进程syslog函数openlog函数setlogmask函数closelog函数用户进程间的关系进程组会话系统资源限制改变工作目录和根目录服务器程序后台化服务器程序规范 Linux 服务器程序一般以后台进程&#xff08;守护进程[daemon]&#xff09;形式运…

IO模型 :阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO

文章目录IO模型阻塞IO非阻塞IO信号驱动IO多路复用IO异步IOIO模型 根据各自的特性不同&#xff0c;IO模型被分为阻塞IO、非阻塞IO、信号驱动IO、异步IO、多路复用IO五类。 最主要的两个区别就是阻塞与非阻塞&#xff0c;同步与异步。 阻塞与非阻塞 阻塞与非阻塞最主要的区别就…

Tomcat服务器集群与负载均衡实现

一、前言 在单一的服务器上执行WEB应用程序有一些重大的问题&#xff0c;当网站成功建成并开始接受大量请求时&#xff0c;单一服务器终究无法满足需要处理的负荷量&#xff0c;所以就有点显得有点力不从心了。另外一个常见的问题是会产生单点故障&#xff0c;如果该服务器坏掉…

Linux服务器 | 事件处理模式:Reactor模式、Proactor模式

文章目录Reactor模式Proactor模式同步I/O模型模拟Proactor模式两者的优缺点ReactorProactor同步I/O模型通常用于实现 Reactor 模式&#xff0c;异步I/O模型通常用于实现 Proactor 模式。&#xff08;不是绝对的&#xff0c;同步I/O也可模拟出 Proactor 模式&#xff09; React…

Linux服务器 | 服务器模型与三个模块、两种并发模式:半同步/半异步、领导者/追随者

文章目录两种服务器模型及三个模块C/S模型P2P模型I/O处理单元、逻辑单元、存储单元并发同步与异步半同步/半异步模式变体&#xff1a;半同步/半反应堆模式改进&#xff1a;高效的半同步/半异步模式领导者/追随者模式组件 &#xff1a;句柄集、线程集、事件处理器工作流程两种服…