C++Primer - 动态内存管理

news2025/4/9 22:39:40

欢迎阅读我的 【C++Primer】专栏

专栏简介:本专栏主要面向C++初学者,解释C++的一些基本概念和基础语言特性,涉及C++标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级程序设计技术。希望对读者有帮助!

在这里插入图片描述
在这里插入图片描述

目录

  • 13.5动态内存管理类
    • StrVec类的设计
    • StrVec类定义
    • 使用construct
    • free成员
    • 拷贝控制成员
    • 在重新分配内存的过程中移动而不是拷贝元素
    • 移动构造函数和std::move
    • reallocate成员

13.5动态内存管理类

某些类需要在运行时分配可变大小的内存空间。这种类通常可以(并且如果它们确实可以的话,一般应该)使用标准库容器来保存它们的数据。例如,我们的StrBlob类使用一个vector来管理其元素的底层内存。

但是,这一策略并不是对每个类都适用;某些类需要自己进行内存分配。这些类一般来说必须定义自己的拷贝控制成员来管理所分配的内存。

例如,我们将实现标准库vector类的一个简化版本。我们所做的一个简化是不使用
模板,我们的类只用于string。因此,它被命名为StrVec。

StrVec类的设计

回忆一下,vector类将其元素保存在连续内存中。为了获得可接受的性能,vector预先分配足够的内存来保存可能需要的更多元素。vector的每个添加元素的成员函数会检查是否有宇间容纳更多的元素。如果有,成员函数会在下一个可用位置构造一个对象。如果没有可用空间,vector就会重新分配空间:它获得新的空间,将已有元素移动到新空间中,释放旧空间,并添加新元素。

我们在StrVvec类中使用类似的策略。我们将使用一个allocator来获得原始内存。由于allocator分配的内存是未构造的,我们将在需要添加新元素时用allocator的construct成员在原始内存中创建对象。类似的,当我们需要删除一个元素时,我们将使用destroy成员来销毁元素。

每个StrVec有三个指针成员指向其元素所使用的内存:

  • elements,指向分配的内存中的首元素
  • first_free,指向最后一个实际元素之后的位置
  • cap,指向分配的内存末尾之后的位置

除了这些指针之外,StrVec还有一个名为alloc的静态成员,其类型为allocator。alloc成员会分配StrVec使用的内存。我们的类还有4个工具函数:

alloc_n_copy会分配内存,并拷贝一个给定范围中的元素。free会销毁构造的元素并释放内存。

*chk_n_alloc保证StrVec至少有容纳一个新元素的空间。如果没有空间添加新元素, reallocate在内存用完时为StrVec分配新内存。

虽然我们关注的是类的实现,但我们也将定义vector接口中的一些成员。

StrVec类定义

有了上述实现概要,我们现在可以定义StrVec类,如下所示:

//类vector类内存分配策略的简化实现

class StrVec {
public:
StrVec()://allocator成员进行默认初始化
elements(nullptr),first_free(nullptr),cap(nullptzr){}

StrVec(constStrVecg)}//拷贝构造函数
StrVec&operator=(const StrVec&)}//拷贝赋值运算符
~StrVec();//析构函数

void push_back(const std::string&);//拷贝元素
size_t size()const{return first_free - elements;}
size_t size_capactty()const{return cap - elements;}
std::string*begin()const {return elements;}
std::string*end()const{return first_free;}

private:
static std::allocator<std::string>alloc;//分配元素
//被添加元素的函数所使用
void chk_n_alloc()
{
    if(size()==capacity()) reallocate();
    //工具函数,被拷贝构造函数、赋值运算符和析构函数所使用
    std::pair<std::string,std::string*>alloc_n_copy(conststd::string*,conststd::string*);
    void free();// 销毁元素并释放内存
    void reallocate();//获得更多内存并拷贝已有元素
    std::string*elements;//指向数组首元素的指针
    std::string*first_free;//指向数组第一个空闲元素的指针
    std::string*cap;//指向数组尾后位置的指针
}

类体定义了多个成员:

  • 默认构造函数(隐式地)默认初始化alloc并(显式地)将指针初始化为nullptr,表明没有元素。
  • size成员返回当前真正在使用的元素的数目,等于ftrst_free-elements。
  • capacity成员返回StrVec可以保存的元素的数量,等价于cap-elements。
  • 当没有空间容纳新元素,即cap==first_free时,chk_n_alloc会为StrVec重新分配内存。
  • begin和end成员分别返回指向首元素(即elements)和最后一个构造的元素
    之后位置(即first_free)的指针。

使用construct

函数push_back调用chk_n_alloc确保有空间容纳新元素。如果需要,chk_n_alloc会调用reallocate。当chk_n_alloc返回时,push_back知道必有空间容纳新元素。它要求其allocator成员来construct新的尾元素:

void StrVec::push_back(const string&s)
{
    chk_n_alloc();//确保有空间客纳新元素
    // 在first_free指向的元素中构造s的副本
    alloc.construct(first_free++,s)
}

当我们用allocator分配内存时,必须记住内存是未构造的。为了使用此原始内存,我们必须调用constzuct,在此内存中构造一个对象。传递给construct的第一个参数必须是一个指针,指向调用allocate所分配的未构造的内存空间。剩余参数确定用哪个构造函数来构造对象。在本例中,只有一个额外参数,类型为string,因此会使用string的拷贝构造函数。

值得注意的是,对construct的调用也会递增first_freey表示已经构造了一个新元素。它使用前置递增,因此这个调用会在first_free当前值指定的地址构造一个对象,并递增first_free指向下一个未构造的元素。

alloc_n_copy成员

我们在拷贝或赋值StrVec时,可能会调用alloc_n_copy成员。类似vector,我们的StrVec类有类值的行为。当我们拷贝或赋值StrVec时,必须分配独立的内存,并从原StrVec对象拷贝元素至新对象。

alloc_n_copy成员会分配足够的内存来保存给定范围的元素,并将这些元素拷贝到新分配的内存中。此函数返回一个指针的pair,两个指针分别指向新空间的开始位置和拷贝的尾后的位置:

pair<string*,string*>
StrVec::alloc_n_copy(conststring*b,conststring*e)
{
    //分配空间保存给定范围中的元素
    auto data=alloc.allocate(a-b);
    //初始化并返回一个pair,该pair由data和uninitialized_copy的返回值构成
    return〔data,uninitialized_copy(b,e,data)};
}

alloc_n_copy用尾后指针减去首元素指针,来计算需要多少空间。在分配内存之后,它必须在此空间中构造给定元素的副本。

它是在返回语句中完成拷贝工作的,返回语句中对返回值进行了列表初始化。返回的pair的first成员指向分配的内存的开始位置;second成员则是uninitialized_copy的返回值,此值是一个指针,指向最后一个构造元素之后的位置。

free成员

free成员有两个责任:首先destroy元素,然后释放StrVec自己分配的内存空间。for循环调用allocator的destroy成员,从构造的尾元素开始,到首元素为止,逆序销毁所有元素:

void StrVec::free()
{
    //不能传递给deallocate一个空指针,如果elements为0,函数什么也不做
    if(elements){
        //递序销毁旧元素
        for(autoP=first_free;p!=elements;/*空*/)
        alloc.destroy(--p);
        alloc.deallocate(elements,cap-elements);
    }
}

destroy函数会运行string的析构函数。string的析构函数会释放string自己分配的内存空间。

一旦元素被销毁,我们就调用deallocate来释放本StrVec对象分配的内存空间。我们传递给deallocate的指针必须是之前某次allocate调用所返回的指针。因此,在调用deallocate之前我们首先检查elements是否为空。

拷贝控制成员

实现了alloc_n_copy和free成员后,为我们的类实现拷贝控制成员就很简单了。

拷贝构造函数调用allocn_copy:

StrVec::StrVec(const_StrVec&s)
{
    //调用alloc_n_copy分配空间以客纳与s中一样多的元素
    autonewdata=alloc_n_copy(s.begin(),s-.end());
    elements=newdata.first;
    first_free=cap=newdata.second;
}

并将返回结果赋予数据成员。alloc_n_copy的返回值是一个指针的pairz。其first成员指向第一个构造的元素,second成员指向最后一个构造的元素之后的位置。由于alloc_n_copy分配的家间恰好容纳给定的元素,cap也指向最后一个构造的元素之后的位置。

析构函数调用free:

StrVec::~StrVec(){free();}

拷贝赋值运算符在释放已有元素之前调用alloc_n_copy,这样就可以正确处理自赋值了

StrVec& StrVec::operator=(const StrVec& rhs)
{
    //调用alloc_n_copy分配内存,大小与rhs中元素占用空间一样多
    auto data = alloc_n_copy(rhs.begin(),rhs.end());
    free();
    elements = data.first;
    first_free = cap = data.second;
    return *this;
}

类似拷贝构造函数,拷贝赋值运算符使用alloc_n_copy的返回值来初始化它的指针。

在重新分配内存的过程中移动而不是拷贝元素

在编写reallocate成员函数之前,我们稍微思考一下此函数应该做什么。它应该

  • 为一个新的、更大的string数组分配内存
  • 在内存空间的前一部分构造对象,保存现有元素
  • 销毁原内存空间中的元素,并释放这块内存

观察这个操作步骤,我们可以看出,为一个StrVec重新分配内存空间会引起从旧内存空间到新内存空间逐个拷贝string。虽然我们不知道string的实现细节,但我们知道string具有类值行为。当拷贝一个string时,新string和原string是相互独立的。改变原string不会影响到副本,反之亦然。

由于string的行为类似值,我们可以得出结论,每个string对构成它的所有字符都会保存自己的一份副本。拷贝一个string必须为这些字符分配内存空间,而销毁一个string必须释放所占用的内存。

拷贝一个string就必须真的拷贝数据,因为通常情况下,在我们拷贝了一个string之后,它就会有两个用户。但是,如果是reallocate拷贝StrVec中的string,则在拷贝之后,每个string只有唯一的用户。一旦将元素从旧空间拷贝到了新空间,我们就会立即销毁原string。

因此,拷贝这些string中的数据是多余的。在重新分配内存空间时,如果我们能邀免分配和释放string的额外开销,StrVec的性能会好得多。

移动构造函数和std::move

通过使用新标准库引入的两种机制,我们就可以避免string的拷贝。首先,有一些标准库类,包括string,都定义了所谓的“移动构造函数“。关于string的移动构造函数如何工作的细节,以及有关实现的任何其他细节,目前都尚未公开。但是,我们知道,移动构造函数通常是将资源从给定对象“移动“而不是拷贝到正在创建的对象。而且我们知道标准库保证“移后源“(moved-from)string仍然保持一个有效的、可析构的状态。对于string,我们可以想象每个string都有一个指向char数组的指针。可以假定string的移动构造函数进行了指针的拷贝,而不是为字符分配内存空间然后拷贝字符。

我们使用的第二个机制是一个名为move的标准库函数,它定义在utility头文件中。目前,关于move我们需要了解两个关键点。首先,当reallocate在新内存中构造string时,它必须调用move来表示希望使用string的移动构造函数。如果它漏掉了move调用,将会使用string的拷贝构造函数。其次,我们通常不为move提供一个using声明。当我们使用move时,直接调用std::move而不是move。

reallocate成员

了解了这些知识,现在就可以编写reallocate成员了。首先调用allocate分配新内存空间。我们每次重新分配内存时都会将StrVec的容量加借。如果StrVec为空,我们将分配容纳一个元素的空间:

void StrVec::reallocate()
{
    //我们将分配当前大小两倍的肉存空间
    auto newcapacity = size()?2*size():1
    //分配新内存
    auto newdata = alloc.allocate(newcapacity);
    //将数据从旧内存移动到新内存
    auto dest=newdata//指向新数组中下一个空闵位置
    auto elem=elements;//指向旧数组中下一个元素
    for(size_t i=0;i!=size();++i)
    {
        alloc.construct(dest++,std::move(*xelem++));
    }

    free();//一旦我们移动完元素就释放旧内存空间
    //更新我们的数据结构,执行新元素
    elements=newdata;
    first_free= dest;
    cap = elements + newcapacity;
}

for循环遍历每个已有元素,并在新内存空间中construct一个对应元素。我们使用dest指向构造新string的内存,使用elem指向原数组中的元素。我们每次用后置递增运算将dest(和elem)推进到各自数组中的下一个元素。

construct的第二个参数是move返回的值。调用move返回的结果会令construct使用string的移动构造函数。由于我们使用了移动构造函数,这些string管理的内存将不会被拷贝。相反,我们构造的每个string都会从elem指向的string那里接管内存的所有权。

在元素移动完毕后,我们调用free销毁旧元素并释放StrVec原来使用的内存。string成员不再管理它们曾经指向的内存;其数据的管理职责已经转移给新StrVec内存中的元素了。我们不知道旧strVec内存中的string包含什么值,但我们保证对它们执行string的析构函数是安全的。

剩下的就是更新指针,指向新分配并已初始化过的数组了。first_free和cap指针分别被设置为指向最后一个构造的元素之后的位置及指向新分配空间的尾后位置。

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

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

相关文章

DeepSeek本地部署(Ollama)

1. Ollama 安装 Ollama 官网地址&#xff1a; https://ollama.com/安装包网盘地址: https://pan.baidu.com 2. Deepseek 部署 根据自己电脑配置和应用需求选择不同模型&#xff0c;配置不足会导致运行时候卡顿。 版本安装指令模型大小硬盘&#xff08;存储&#xff09;显卡…

第二期:深入理解 Spring Web MVC [特殊字符](核心注解 + 进阶开发)

前言&#xff1a; 欢迎来到 Spring Web MVC 深入学习 的第二期&#xff01;在第一期中&#xff0c;我们介绍了 Spring Web MVC 的基础知识&#xff0c;学习了如何 搭建开发环境、配置 Spring MVC、编写第一个应用&#xff0c;并初步了解了 控制器、视图解析、请求处理流程 等核…

论伺服电机在轨道式巡检机器人中的优势及应用实践​

一、引言​ 1.1 研究背景与意义​ 在现代工业生产、电力系统、轨道交通等诸多领域&#xff0c;保障设施设备的安全稳定运行至关重要。轨道式巡检机器人作为一种高效、智能的巡检工具&#xff0c;正逐渐在这些领域崭露头角。它能够沿着预设轨道&#xff0c;对目标区域进行全方位…

(51单片机)独立按键控制流水灯LED流向(独立按键教程)(LED使用教程)

源代码 如上图将7个文放在Keli5 中即可&#xff0c;然后烧录在单片机中就行了 烧录软件用的是STC-ISP&#xff0c;不知道怎么安装的可以去看江科大的视频&#xff1a; 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?…

react-router children路由报错

项目场景&#xff1a; 写个路由页面&#xff0c;引发的问题 问题描述 报错&#xff1a; An absolute child route path must start with the combined path of all its parent routes. 代码&#xff1a; import { createBrowserRouter } from "react-router-dom";…

Socket编程TCP

Socket编程TCP 1、V1——EchoServer单进程版2、V2——EchoServer多进程版3、V3——EchoServer多线程版4、V4——EchoServer线程池版5、V5——多线程远程命令执行6、验证TCP——Windows作为client访问Linux7、connect的断线重连 1、V1——EchoServer单进程版 在TcpServer.hpp中实…

文件映射mmap与管道文件

在用户态申请内存&#xff0c;内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说&#xff0c;把磁盘里的数据与内存的用户态建立一一映射关系&#xff0c;让读写内存等价于读写磁盘&#xff0c;支持随机访问。 管道文件&#xff1a;进程间通信机…

代码随想录回溯算法03

93.复原IP地址 本期本来是很有难度的&#xff0c;不过 大家做完 分割回文串 之后&#xff0c;本题就容易很多了 题目链接/文章讲解&#xff1a;代码随想录 视频讲解&#xff1a;回溯算法如何分割字符串并判断是合法IP&#xff1f;| LeetCode&#xff1a;93.复原IP地址_哔哩哔…

批量改CAD图层颜色——CAD c#二次开发

一个文件夹下大量图纸&#xff08;几百甚至几千个文件&#xff09;需要改图层颜色时&#xff0c;可采用插件实现&#xff0c;效果如下&#xff1a; 转换前&#xff1a; 转换后&#xff1a; 使用方式如下&#xff1a;netload加载此dll插件&#xff0c;输入xx运行。 附部分代码如…

【内网安全】DHCP 饿死攻击和防护

正常情况&#xff1a;PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包&#xff1a;大量的DHCP Discover包 此时模…

10种电阻综合对比——《器件手册--电阻》

二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件&#xff0c;通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻&#xff0c;适合高密度电路板&#xff1b;精度高、稳定性好&#xff0c;便于自动化生产 广泛应用于…

剑指Offer(数据结构与算法面试题精讲)C++版——day6

剑指Offer&#xff08;数据结构与算法面试题精讲&#xff09;C版——day6 题目一&#xff1a;不含重复字符的最长子字符串题目二&#xff1a;包含所有字符的最短字符串题目三&#xff1a;有效的回文 题目一&#xff1a;不含重复字符的最长子字符串 这里还是可以使用前面&#x…

freertos韦东山---事件组以及实验

事件组的原理是什么&#xff0c;有哪些优点&#xff0c;为啥要创造出这个概念 在实时操作系统&#xff08;如 FreeRTOS&#xff09;中&#xff0c;事件组是一种用于任务间同步和通信的机制&#xff0c;它的原理、优点及存在意义如下&#xff1a; 事件组原理 数据结构&#xf…

架构师面试(二十六):系统拆分

问题 今天我们聊电商系统实际业务场景的问题&#xff0c;考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速&#xff0c;DAU在 10W&#xff1b;整个电商系统按水平分层架构进行设计&#xff0c;包括【入口网关…

Java中的同步和异步

一、前言 在Java中&#xff0c;同步&#xff08;Synchronous&#xff09;和异步&#xff08;Asynchronous&#xff09;是两种不同的任务处理模式。核心区别在任务执行的顺序控制和线程阻塞行为。 二、同步&#xff08;Synchronous&#xff09; 定义&#xff1a;任务按顺序执行…

在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus

一、安装环境信息说明 硬件资源&#xff08;GB 和 GiB 的主要区别在于它们的换算基数不同&#xff0c;GB 使用十进制&#xff0c;GiB 使用二进制&#xff0c;导致相同数值下 GiB 表示的容量略大于 GB&#xff1b;换算关系&#xff1a;1 GiB ≈ 1.07374 GB &#xff1b;1 GB ≈ …

NO.64十六届蓝桥杯备战|基础算法-简单贪心|货仓选址|最大子段和|纪念品分组|排座椅|矩阵消除(C++)

贪⼼算法是两极分化很严重的算法。简单的问题会让你觉得理所应当&#xff0c;难⼀点的问题会让你怀疑⼈⽣ 什么是贪⼼算法&#xff1f; 贪⼼算法&#xff0c;或者说是贪⼼策略&#xff1a;企图⽤局部最优找出全局最优。 把解决问题的过程分成若⼲步&#xff1b;解决每⼀步时…

瑞萨RA4M2使用心得-KEIL5的第一次编译

目录 前言 环境&#xff1a; 开发板&#xff1a;RA-Eco-RA4M2-100PIN-V1.0 IDE&#xff1a;keil5.35 一、软件的下载 编辑瑞萨的芯片&#xff0c;除了keil5 外还需要一个软件&#xff1a;RASC 路径&#xff1a;Releases renesas/fsp (github.com) 向下找到&#xff1a; …

数据分析-Excel-学习笔记

Day1 复现报表聚合函数&#xff1a;日期联动快速定位区域SUMIF函数SUMIFS函数环比、同比计算IFERROR函数混合引用单元格格式总结汇报 拿到一个Excel表格&#xff0c;首先要看这个表格个构成&#xff08;包含了哪些数据&#xff09;&#xff0c;几行几列&#xff0c;每一列的名称…

整车CAN网络和CANoe

车载网络中主要包含有Can网络,Lin网络,FlexRay,Most,以太网。 500kbps:500波特率,表示的数据传输的速度。表示的是最大的网速传输速度。也就是每秒 500kb BodyCan车身Can InfoCan娱乐信息Can 车身CAN主要连接的是ESB电动安全带 ADB自适应远光灯等 PTCan动力Can 底盘Can