[C++] string管理:深浅拷贝写时拷贝

Kevin的技术博客.png

文章目录

  • 拷贝问题的引入
    • 问题代码
      • `string`类的构造函数
      • `String` 类的析构函数
      • 测试入口函数(问题)
      • 详细分析
  • 浅拷贝
  • 深拷贝
  • 传统版与现代版的`String`类
    • 传统`String`类
    • 现代版`String`类
  • 写时拷贝
    • 先构造的对象后析构的影响
    • 写时拷贝举例及测试样例
      • 代码举例
      • 测试用例

拷贝问题的引入

问题代码

string类的构造函数

String(const char* str = "")
{if (nullptr == str){assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);
}

这个构造函数分配了动态内存来存储字符串,并复制了传入的 str 字符串。但当你用 new 分配内存并用 _str 变量存储时,你并没有处理已有 _str 的情况,例如拷贝构造或赋值操作。这样会在对象被拷贝或赋值时出现问题。

String 类的析构函数

~String()
{if (_str){delete[] _str;_str = nullptr;}
}

析构函数负责释放动态分配的内存。这是一个好的实践,但在没有拷贝构造函数的情况下,如果多个对象指向同一块内存,析构函数会尝试释放相同的内存多次,导致程序崩溃

测试入口函数(问题)

void TestString()
{String s1("hello bit!!!");String s2(s1); // 这里调用了拷贝构造函数
}

TestString 函数中,s1 是一个 String 对象,s2 是通过 s1 进行拷贝构造的。**如果没有拷贝构造函数,编译器会生成一个默认的拷贝构造函数,该默认构造函数仅逐位复制成员变量,对于指针类型,这会导致 **s1****s2** 指向同一块内存区域。这样,当 **s1****s2** 的析构函数被调用时,会尝试释放同一块内存,导致程序崩溃。 **

image.png

详细分析

通过以上代码及解析可以发现,在VS下,当没有拷贝构造函数的话,会直接将被构造的那个对象中成员的指针指向拿来构造的对象的指针指向的空间。当程序结束时,因为有析构函数,所以会将两个对象进行析构,又因为两个对象中的指针指向的是同一块空间,所以会对同一块空间析构两次,造成程序崩溃。

由此 -> 引出深浅拷贝的概念

浅拷贝

浅拷贝也称为位拷贝,当不存在拷贝构造函数或者重载的赋值运算符时,编译器会将对象中的值拷贝过来。如果对象中包含指针等资源管理信息,这种方式会导致多个对象共享同一份资源。当一个对象销毁时,会将该资源释放掉,而其他对象不知道资源已被释放,继续操作会导致访问违规。

class ShallowCopy {
public:ShallowCopy(int* data) : data_(data) {}int* getData() const { return data_; }
private:int* data_;
};void example() {int* data = new int(42);ShallowCopy obj1(data);ShallowCopy obj2 = obj1;delete data;// obj2.getData() 现在是悬空指针,继续访问会出错
}

在上述例子中,obj1obj2共享同一个指针data,当删除data后,obj2中存储的指针变成悬空指针。

深拷贝

深拷贝是为了解决浅拷贝的问题,每个对象都有一份独立的资源,不与其他对象共享。这样,当一个对象销毁时,其他对象的资源不会受到影响。例如:

class DeepCopy {
public:DeepCopy(int* data) : data_(new int(*data)) {}DeepCopy(const DeepCopy& other) : data_(new int(*other.data_)) {}~DeepCopy() { delete data_; }int* getData() const { return data_; }
private:int* data_;
};void example() {int* data = new int(42);DeepCopy obj1(data);DeepCopy obj2 = obj1;delete data;// obj1 和 obj2 都有独立的 data
}

在上述例子中,obj1obj2都有各自独立的data,删除原始data指针后,它们的资源仍然有效。

image.png

传统版与现代版的String

传统String

传统版的String类使用深拷贝来管理字符串资源。以下是其示例代码:

class String {
public:String(const char* str = "") {if (nullptr == str) {assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);}String(const String& s) : _str(new char[strlen(s._str) + 1]) {strcpy(_str, s._str);}String& operator=(const String& s) {if (this != &s) {char* pStr = new char[strlen(s._str) + 1];strcpy(pStr, s._str);delete[] _str;_str = pStr;}return *this;}~String() {delete[] _str;}private:char* _str;
};

在这个版本中,每个String对象都有独立的字符串数据,通过拷贝构造函数和赋值运算符重载实现深拷贝。

现代版String

现代版的String类使用资源管理技术,如智能指针或“写时拷贝”(Copy-On-Write, COW),来优化资源管理。以下是其示例代码:

class String {
public:String(const char* str = "") {if (nullptr == str) {assert(false);return;}_str = new char[strlen(str) + 1];strcpy(_str, str);}String(const String& s) : _str(nullptr) {String temp(s._str);swap(_str, temp._str);}String& operator=(String s) {swap(_str, s._str);return *this;}~String() {delete[] _str;}private:char* _str;
};

现代代码的灵活之处就在于swap(_str, temp._str);
当使用swap(_str, temp._str);时,swap底层会将_str指向的空间与temp._str指向的空间相互交换。这样的话就可以将_str指向已经构造好的temp._str的空间,然后temp._str指向的之前_str不需要的空间会在temp._str生命周期结束的时候通过析构函数进行释放。
简单理解为:**temp._str**承包了构造和析构的活,而**_str**只是负责与**temp._str**交换一下需要的空间地址。

写时拷贝

写时拷贝是一种优化技术,只有在需要修改时才执行深拷贝,而读取操作仍然共享资源。实现写时拷贝通常需要引用计数来管理资源。

关键点:

  • 引用计数:每个共享资源都有一个引用计数,当一个对象引用该资源时,引用计数增加;当对象销毁时,引用计数减少。
  • 深拷贝触发:当一个对象试图修改共享资源时,如果引用计数大于1,则执行深拷贝,这样修改不会影响其他共享该资源的对象。
  • 析构函数:当对象销毁时,如果引用计数减为0,则释放资源。

先构造的对象后析构的影响

考虑如下情景:

  1. 对象A的构造:对象A创建时分配资源,引用计数为1。
  2. 对象B的构造(通过拷贝构造):对象B通过拷贝构造从对象A创建,引用计数增加到2。
  3. 对象B的析构:当对象B销毁时,引用计数减少到1,但资源不释放,因为对象A仍在使用该资源。
  4. 对象A的析构:当对象A销毁时,引用计数减少到0,资源被释放。

由于对象B是从对象A拷贝构造而来的,在对象B修改资源前引用计数已经增加,因此写时拷贝能够正常工作。因为对象的生命周期顺序(先构造的对象后析构),确保了引用计数正确管理资源的分配和释放。

总结:
因为对象的析构顺序是反向的,即后构造的对象先析构,这种顺序确保了在写时拷贝机制中,资源的引用计数能够正确地管理和释放。通过引用计数,我们可以确定资源在没有对象使用时才被释放,从而保证了写时拷贝的正确性和效率

写时拷贝举例及测试样例

代码举例

class String {
public:String(const char* str = "") : _str(new char[strlen(str) + 1]), _count(new int(1))  // 一个string对象刚开始的 _count 就是 1{strcpy(_str, str);}String(const String& s) : _str(s._str), _count(s._count) {++(*_count);}String& operator=(const String& s) {if (this != &s) {// 确保在没有对象再引用该资源时,正确地释放内存以避免内存泄漏if (--(*_count) == 0) {delete[] _str;delete _count;}_str = s._str;_count = s._count;++(*_count);}return *this;}~String() {if (--(*_count) == 0) {delete[] _str;delete _count;}}void modify(const char* newStr) {if (*_count > 1) {--(*_count);_str = new char[strlen(newStr) + 1];strcpy(_str, newStr);_count = new int(1);}else {delete[] _str;  // 先释放旧的字符串内存_str = new char[strlen(newStr) + 1];strcpy(_str, newStr);}}const char* c_str() const {return _str;}private:char* _str;int* _count;
};

在这个拷贝函数中:

  • 共享资源:新对象_str指向原对象的字符串数据_str,新对象的引用计数指针_count也指向原对象的引用计数。
  • 增加引用计数++(*_count)表示将引用计数加1,这样可以跟踪有多少个对象共享这份资源。
  • 修改操作:当modify方法被调用时,如果引用计数大于1,则进行深拷贝并独立修改,否则直接修改原有字符串。
  • 析构函数:减少引用计数,当引用计数为0时释放资源。

当深拷贝触发时,即需要改变资源指向时,会进行_count数值的确认

if (--(*_count) == 0) {delete[] _str;delete _count;
}

当确认已经没有其他对象共享该资源时会进行销毁。

测试用例

void testCopyOnWrite() {String s1("Hello");String s2 = s1;std::cout << "修改前:" << std::endl;std::cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << std::endl;s1.modify("World");std::cout << "修改后:" << std::endl;std::cout << "s1: " << s1.c_str() << ", s2: " << s2.c_str() << std::endl;
}int main() {testCopyOnWrite();return 0;
}

输出:

修改前:
s1: Hello, s2: Hello
修改后:
s1: World, s2: Hello
  1. **s1****s2**共享资源:在创建s2时,s2通过拷贝构造函数共享s1的资源,引用计数为2。
  2. 修改**s1**
    • 调用modify方法时,检查引用计数。
    • 由于引用计数大于1(说明有其他对象共享资源),s1进行深拷贝:分配新内存,将新字符串复制到新内存,并初始化新的引用计数。
    • 此时,s2仍然保持原来的字符串内容不变。

通过这种方式,写时拷贝机制可以有效地管理共享资源,确保在需要修改时进行深拷贝,避免不必要的内存拷贝操作。


image.png

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

如若内容造成侵权/违法违规/事实不符,请联系一条长河网进行投诉反馈,一经查实,立即删除!

相关文章

浅谈Llama3.1,从结构、训练过程、影响到数据合成

Llama3.1系列模型的开源&#xff0c;真让大模型格局大震&#xff0c;指标上堪比最好的闭源模型比如GPT 4o和Claude3.5&#xff0c;让开源追赶闭源成为现实。 这里给大家分享一篇俊林兄&#xff08;知乎张俊林&#xff09;的一篇解读&#xff0c;主要对LLaMA3.1的模型结构、训练…

1.1 操作系统的基本概念

文章目录 操作系统的概念(定义)操作系统的目标和功能操作系统作为计算机系统资源的管理者操作系统向上层提供方便易用的服务命令接口程序接口 操作系统作为最接近硬件的层次 操作系统的特征&#xff08;4个&#xff09;并发共享互斥共享方式同时共享方式 虚拟虚拟处理器&#x…

【性能优化】在大批量数据下使用 HTML+CSS实现走马灯,防止页面卡顿(二)

上一篇只是简单演示了’下一张’的操作和整体的设计思路,这两天把剩余功能补全了,代码经过精简,可封装当成轮播组件使用,详细如下. 代码 <template><div class"container"><button click"checkNext(last)">上一张</button><b…

Vue Router基础

Router 的作用是在单页应用&#xff08;SPA&#xff09;中将浏览器的URL和用户看到的内容绑定起来。当用户在浏览不同页面时&#xff0c;URL会随之更新&#xff0c;但页面不需要从服务器重新加载。 1 Router 基础 RouterView RouterView 用于渲染当前URL路径对应的路由组件。…

Linux--Socket编程预备

目录 1. 理解源 IP 地址和目的 IP 地址 2.端口号 2.1端口号(port)是传输层协议的内容 2.2端口号范围划分 2.3理解 "端口号" 和 "进程 ID" 2.4理解 socket 3.传输层的典型代表 3.1认识 TCP 协议 3.2认识 UDP 协议 4. 网络字节序 5. socket 编程接…

边缘计算网关项目(含上报进程、32Modbus采集进程、设备搜索响应进程源码)

目录 边缘层 架构说明 包含知识点 数据上报进程 功能描述 功能开发 上报线程 数据存储线程 指令处理线程 项目源码 上报模块.c代码&#xff1a; 上报模块Makefile代码&#xff1a; STM32采集模块.c代码 设备搜索响应模块Linux部分.c代码 设备搜索响应模块Qt端代码.h …

流量录制与回放:jvm-sandbox-repeater工具详解

在软件开发和测试过程中&#xff0c;流量录制与回放是一个非常重要的环节&#xff0c;它可以帮助开发者验证系统在特定条件下的行为是否符合预期。本文将详细介绍一款强大的流量录制回放工具——jvm-sandbox-repeater&#xff0c;以及如何利用它来提高软件测试的效率和质量。 …

《Cross-Modal Dynamic Transfer Learning for Multimodal Emotion Recognition》

Multi-modal系列论文研读目录 文章目录 Multi-modal系列论文研读目录1.ABSTRACT2.INDEX TERMS3.INTRODUCTION4.RELATED WORKSA. MULTIMODAL EMOTION RECOGNITION 多模态情感识别1) CONVENTIONAL FUSION METHODS 常规融合方法2) TRANSFORMER-BASED FUSION METHODS 基于变压器的融…

C#测试控制台程序调用Quartz.NET的基本用法

Quartz.Net是常用的任务调用框架之一&#xff0c;既能在客户端程序中使用&#xff0c;也支持在网页程序后台调用。本文结合参考文献4中的示例代码学习其在控制台程序中的基本用法。   VS2022新建控制台项目&#xff0c;在Nuget包管理器中搜索并安装Quartz包&#xff0c;如下所…

IDEA在编译的时候报Error: java: 找不到符号符号: 变量 log lombok失效问题

错误描述 idea因为lombok的报错: java: You arent using a compiler supported by lombok, so lombok will not work and has been disabled.Your processor is: com.sun.proxy.$Proxy8Lombok supports: sun/apple javac 1.6, ECJ 原因&#xff1a;这是由于Lombok的版本过低的…

若依 ruoyi poi Excel合并行的导入

本文仅针对文字相关的合并做了处理 &#xff0c;图片合并及保存需要另做处理&#xff01;&#xff01; 目标&#xff1a;Excel合并行内容的导入 结果&#xff1a; 1. ExcelUtil.java 类&#xff0c;新增方法&#xff1a;判断是否是合并行 /*** 新增 合并行相关代码&#xff1a;…

matlab 绘制参数方程

matlab 绘制参数方程 绘制参数方程绘制结果 绘制参数方程 clc; clear; close all;axis_length 100;% 定义参数t的范围 t 0:0.01:100;% 计算x和y的值 x t.^2 1; y 4*t - t.^2;% 绘制函数图像 plot(x, y); xlabel(x); ylabel(y); title(Plot of the curve xt^21, y4t-t^2…

Uprecise软件的基本功能

UPrecise 是和芯星通独立开发的评估软件&#xff0c; 旨在帮助用户便捷地对公司产品进行可视化操作。 用户可通过该软件以串口或端口的方式与接收机进行交互并直观地查看其状态信息&#xff0c;连接后 UPrecise 将自动识别接收机的波特率和类型&#xff0c;动态显示该类型接收机…

Python3网络爬虫开发实战(2)爬虫基础库

文章目录 一、urllib1. urlparse 实现 URL 的识别和分段2. urlunparse 用于构造 URL3. urljoin 用于两个链接的拼接4. urlencode 将 params 字典序列化为 params 字符串5. parse_qs 和 parse_qsl 用于将 params 字符串反序列化为 params 字典或列表6. quote 和 unquote 对 URL的…

FastAPI(七十三)实战开发《在线课程学习系统》接口开发-- 回复留言

源码见&#xff1a;"fastapi_study_road-learning_system_online_courses: fastapi框架实战之--在线课程学习系统" 之前文章分享FastAPI&#xff08;七十二&#xff09;实战开发《在线课程学习系统》接口开发-- 留言列表开发&#xff0c;这次我们分享如何回复留言 按…

Layui修改表格分页为英文

Layui修改表格分页为英文 1.前言2.Laypage属性 1.前言 主要记录初次使用Layui没有好好看官方文档踩坑&#xff0c;修改了源码才发现可以自定义 使用的Layui版本2.9.14 2.Laypage属性 Laypage属性中带的有自定义文本的属性 示例代码 table.render({.......page: {skipText: …

Linux:传输层(1) -- UDP协议

1. 端口号 同一台主机的不同端口号(Port)标记了主机上不同的进程&#xff0c;如下图所示&#xff1a; 在 TCP/IP 协议中 , 用 " 源IP", "源端口号", "目的IP", "目的端口号", "协议号" 这样一个五元组来标识一个通信 ( 可…

QT开发(QT的基本概述和环境的安装)

QT的概述 一.QT的介绍背景1.1 什么是QT1.2QT的发展史1.3 Qt支持的平台1.4QT版本1.5QT的优点1.6QT的应用场景 二.搭建QT开发环境2.1 QT的开发工具的下载2.2 QT环境变量配置 三.QT的三种基类四.QT Hello World程序4.1使用按钮实现4.1.1 代码方式实现4.1.2 可视化操作实现 一.QT的…

GO内存分配详解

文章目录 GO内存分配详解一. 物理内存(Physical Memory)和虚拟内存(Virtual Memory)二. 内存分配器三. TCMalloc线程内存(thread memory)页堆(page heap)四. Go内存分配器mspanmcachemcentralmheap五. 对象分配流程六. Go虚拟内存ArenaGO内存分配详解 这篇文章中我将抽丝剥茧,…

基于STM32瑞士军刀--【FreeRTOS开发】学习笔记(一)|| RISC / 底层代码执行步骤 / 汇编指令

本篇文章基于韦东山老师讲课笔记和自己理解编写。 RISC ARM芯片属于精简指令集计算机(RISC&#xff1a;Reduced Instruction Set Computing)&#xff0c;它所用的指令比较简单&#xff0c;有如下特点&#xff1a; ① 对内存只有读、写指令 ② 对于数据的运算是在CPU内部实现 …