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

news2024/9/21 12:22:31

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.coloradmin.cn/o/1947293.html

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

相关文章

BGP之选路MED

原理概述 当一台BGP路由器中存在多条去往同一目标网络的BGP路由时&#xff0c;BGP协议会对这些BGP路由的属性进行比较&#xff0c;以确定去往该目标网络的最优BGP路由。BGP路由属性的比较顺序为Preferred Value属性、Local Preference属性、路由生成方式、AS_Path属性、Origin属…

react中路由跳转以及路由传参

一、路由跳转 1.安装插件 npm install react-router-dom 2.路由配置 路由配置&#xff1a;react中简单的配置路由-CSDN博客 3.实现代码 // src/page/index/index.js// 引入 import { Link, useNavigate } from "react-router-dom";function IndexPage() {const …

大数据之Oracle同步Doris数据不一致问题

数据同步架构如下&#xff1a; 出现的问题&#xff1a; doris中的数据条数 源库中的数据条数 总数完全不一致。 出现问题的原因&#xff1a; 在Dinky中建立表结构时&#xff0c;缺少对主键属性的限制 primary key(ID) not enforced 加上如上语句&#xff0c;数据条数解决一致 …

WPF+Mvvm项目入门完整教程-仓储管理系统(二)

目录 一、搭建一个主界面框架二、实现步骤1.主界面区域划分2.主界面区域实现 一、搭建一个主界面框架 主要实现主界面的框架样式和基础功能。这里特别说明一下&#xff0c;由于MvvmLight 已经过时不在维护&#xff0c;本项目决定将MvvmLight框架变更为 CommunityToolkit.Mvvm …

标题:探索pdf2image:将PDF文档转化为图像的Python魔法

标题&#xff1a;探索pdf2image&#xff1a;将PDF文档转化为图 像的Python魔法 背景 在数字时代&#xff0c;我们经常需要处理各种格式的文档&#xff0c;尤其是PDF文件。PDF以其跨平台的可读性和稳定性而广受欢迎。然而&#xff0c;有时我们需要将PDF文件转换成图像格式&am…

Golang | Leetcode Golang题解之第282题给表达式添加运算符

题目&#xff1a; 题解&#xff1a; func addOperators(num string, target int) (ans []string) {n : len(num)var backtrack func(expr []byte, i, res, mul int)backtrack func(expr []byte, i, res, mul int) {if i n {if res target {ans append(ans, string(expr))}…

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 编程接…

【数据结构】--- 栈和队列

前言 前面学习了数据结构的顺序表、单链表、双向循环链表这些结构&#xff1b;现在就来学习栈和队列&#xff0c;这里可以简单的说栈和队列是具有特殊化的线性表 一、栈 1.1、栈的概念和结构 栈是一种遵循先入后出逻辑的线性数据结构。 栈是一种特殊的线性表&#xff0c;它只允…

矿场运输车4G视频监控管理解决方案

一、背景介绍 随着科技的不断进步和智能化时代的来临&#xff0c;矿业运输行业也在寻求更高效率与安全的管理手段。矿场运输车4G视频监控管理解决方案是一种基于4G网络技术的视频监控系统&#xff0c;专门用于监测和管理矿场内运输车辆的工作状态和安全情况。该方案为矿场运输…

【linux】在多核CPU下,好像看到不同进程在不同CPU调度

在2353这行打印的情况来看&#xff0c;操作系统好像给不同的进程分配不同的CPU&#xff0c;从上图来看&#xff0c;同一个进程好像基本使用的相同的CPU&#xff1a; 其实摸索syscall文件系统操作&#xff0c;本意是想找到内核文件系统中文件的创建&#xff0c;写入&#xff0c;…

C 观察者模式 Demo

目录 一、基础描述 二、Demo 最近需要接触到 MySQL 半同步插件&#xff0c;发现其中用到了观察者模式&#xff0c;之前没在 C 中用过&#xff0c;遂好奇心驱使下找了找资料&#xff0c;并写了个 Demo。 一、基础描述 观察者设计模式&#xff08;Observer Pattern&#xff0…

ts踩坑!使用可选链 ?.处理可能遇到的 undefined 或 null 值的情况,但是仍然收到一个关于可能为 undefined 的警告!

在 TypeScript 中&#xff0c;当你使用可选链&#xff08;Optional Chaining&#xff09;?. 时&#xff0c;你其实已经处理了可能遇到的 undefined 或 null 值的情况。但是&#xff0c;如果你仍然收到一个关于可能为 undefined 的警告&#xff0c;这可能是因为 TypeScript 的类…

Mybatis——快速入门

介绍 MyBatis是一款优秀的持久层&#xff08;Dao层&#xff09;框架&#xff0c;用于简化JDBC的开发。MyBatis 底层是基于 JDBC 实现的&#xff0c;它封装了 JDBC 的大部分功能&#xff0c;使得数据库操作更加便捷和高效。同时&#xff0c;MyBatis 也保留了 JDBC 的灵活性&…

unity2D游戏开发03状态控制

多态和动画 建立player-idle动画&#xff0c;取玩家最后两个图片 选中playcontroller控制器 将玩家动画拖进去 右键player-idle,选择set as layer Default state 右键点击Any State ,点击Make Transition 结果 动画参数 动画参数是动画控制器定义的变量&#xff0c;点击Param…

Matlab arrayfun 与 bsxfun——提高编程效率的利器!

许多人知道 MATLAB 向量化编程&#xff0c;少用 for 循环 可以提高代码运行效率&#xff0c;但关于代码紧凑化编程&#xff0c; arrayfun 与 bsxfun 两个重要函数却鲜有人能够用好&#xff0c;今天针对这两个函数举例说明其威力。 Matlab arrayfun 概述 arrayfun 是 Matlab …

one-api 源码调试配置

本文主要介绍通过 VSCode 调试 one-api 源码。 一、环境配置 1.1 VSCode 和 one-api 安装 首先,确保已经安装了 VSCode(下载链接)和 one-api 源码(下载链接)已下载并安装了依赖 1.2 安装 Go 插件 在 VSCode 中,安装 Go 插件。 1.3 安装 dlv 调试包 可以通过下载源码…

EEtrade:现货黄金盈利计算方法

现货黄金交易作为一种极具吸引力的投资方式&#xff0c;其盈利计算涉及多个关键因素&#xff0c;投资者需深入理解这些因素&#xff0c;才能准确评估交易结果&#xff0c;并制定科学的投资策略。 一、现货黄金基本盈利计算&#xff1a; 利润公式&#xff1a; 利润 (收盘价 -…

docker部署mysql8.x版本,编写shell脚本自动部署安装mysql

docker部署mysql8.x版本&#xff0c;编写shell脚本自动部署安装mysql **1.**先自行安装好docker环境&#xff0c;docker的镜像注册中心最好是国内的&#xff0c;例如执行一下命令直接修改docker配置&#xff0c; cat <<EOF > /etc/docker/daemon.json {"regist…

LabVIEW学习-LabVIEW处理带分隔符的字符串从而获取数据

带分隔符的字符串很好处理&#xff0c;只需要使用"分隔符字符串至一维字符串数组"函数或者"一维字符串数组至分隔符字符串"函数就可以很轻松地处理带分隔符地字符串。 这两个函数所在的位置为&#xff1a; 函数选板->字符串->附加字符串函数->分…

在STM32嵌入式中C/C++语言对栈空间的使用

像STM32这样的微控制器在进入main函数之前需要对栈进行初始化。可以说栈是C语言运行时的必要条件。我们知道栈实际上是一块内存空间&#xff0c;那么这块空间都用来存储什么呢&#xff1f;有什么办法能够优化栈空间的使用&#xff1f; 栈空间保存的内容 栈是一个先入后出的数据…