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指针分别被设置为指向最后一个构造的元素之后的位置及指向新分配空间的尾后位置。
相关文章:
C++Primer - 动态内存管理
欢迎阅读我的 【CPrimer】专栏 专栏简介:本专栏主要面向C初学者,解释C的一些基本概念和基础语言特性,涉及C标准库的用法,面向对象特性,泛型特性高级用法。通过使用标准库中定义的抽象设施,使你更加适应高级…...
DeepSeek本地部署(Ollama)
1. Ollama 安装 Ollama 官网地址: https://ollama.com/安装包网盘地址: https://pan.baidu.com 2. Deepseek 部署 根据自己电脑配置和应用需求选择不同模型,配置不足会导致运行时候卡顿。 版本安装指令模型大小硬盘(存储)显卡…...
Amodal3R ,南洋理工推出的 3D 生成模型
Amodal3R 是一款先进的条件式 3D 生成模型,能够从部分可见的 2D 物体图像中推断并重建完整的 3D 结构与外观。该模型建立在基础的 3D 生成模型 TRELLIS 之上,通过引入掩码加权多头交叉注意力机制与遮挡感知注意力层,利用遮挡先验知识优化重建…...
第二期:深入理解 Spring Web MVC [特殊字符](核心注解 + 进阶开发)
前言: 欢迎来到 Spring Web MVC 深入学习 的第二期!在第一期中,我们介绍了 Spring Web MVC 的基础知识,学习了如何 搭建开发环境、配置 Spring MVC、编写第一个应用,并初步了解了 控制器、视图解析、请求处理流程 等核…...
论伺服电机在轨道式巡检机器人中的优势及应用实践
一、引言 1.1 研究背景与意义 在现代工业生产、电力系统、轨道交通等诸多领域,保障设施设备的安全稳定运行至关重要。轨道式巡检机器人作为一种高效、智能的巡检工具,正逐渐在这些领域崭露头角。它能够沿着预设轨道,对目标区域进行全方位…...
开源软件与自由软件:一场理念与实践的交锋
在科技的世界里,“开源软件”和“自由软件”这两个词几乎无人不知。很多人或许都听说过,它们的代码是公开的,可以供所有人查看、修改和使用。然而,若要细究它们之间的区别,恐怕不少朋友会觉得云里雾里。今天࿰…...
(51单片机)独立按键控制流水灯LED流向(独立按键教程)(LED使用教程)
源代码 如上图将7个文放在Keli5 中即可,然后烧录在单片机中就行了 烧录软件用的是STC-ISP,不知道怎么安装的可以去看江科大的视频: 【51单片机入门教程-2020版 程序全程纯手打 从零开始入门】https://www.bilibili.com/video/BV1Mb411e7re?…...
开发指南111-关闭所有打开的子窗口
门户系统是通过window.open通过单点登录的模式打开子系统的,这就要求门户系统退出时,关闭所有打开的子系统。 平台处理这一问题的核心原理如下: 主窗口定义: allChildWindows:[], //所有子窗口 pushChildWindow(childWindow){ …...
react-router children路由报错
项目场景: 写个路由页面,引发的问题 问题描述 报错: An absolute child route path must start with the combined path of all its parent routes. 代码: import { createBrowserRouter } from "react-router-dom";…...
双向链表示例
#include <stdio.h> #include <stdlib.h>// 定义双向链表节点结构体 typedef struct list {int data; // 数据部分struct list *next; // 指向下一个节点的指针struct list *prev; // 指向前一个节点的指针 } list_t;// 初始化链表,将链表的…...
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中实…...
当网页受到DDOS网络攻击有哪些应对方法?
分布式拒绝服务攻击也是人们较为熟悉的DDOS攻击,这类攻击会通过大量受控制的僵尸网络向目标服务器发送请求,以此来消耗服务器中的资源,致使用户无法正常访问,当网页受到分布式拒绝服务攻击时都有哪些应对方法呢? 建立全…...
文件映射mmap与管道文件
在用户态申请内存,内存内容和磁盘内容建立一一映射 读写内存等价于读写磁盘 支持随机访问 简单来说,把磁盘里的数据与内存的用户态建立一一映射关系,让读写内存等价于读写磁盘,支持随机访问。 管道文件:进程间通信机…...
4.4刷题记录(哈希表)
1.242. 有效的字母异位词 - 力扣(LeetCode) class Solution { public:bool isAnagram(string s, string t) {unordered_map<char,int>cnt_s,cnt_t;for(int i0;i<s.size();i){cnt_s[s[i]];}for(int i0;i<t.size();i){cnt_t[t[i]];}if(cnt_sc…...
代码随想录回溯算法03
93.复原IP地址 本期本来是很有难度的,不过 大家做完 分割回文串 之后,本题就容易很多了 题目链接/文章讲解:代码随想录 视频讲解:回溯算法如何分割字符串并判断是合法IP?| LeetCode:93.复原IP地址_哔哩哔…...
批量改CAD图层颜色——CAD c#二次开发
一个文件夹下大量图纸(几百甚至几千个文件)需要改图层颜色时,可采用插件实现,效果如下: 转换前: 转换后: 使用方式如下:netload加载此dll插件,输入xx运行。 附部分代码如…...
【内网安全】DHCP 饿死攻击和防护
正常情况:PC2可以正常获取到DHCP SERVER分别的IP地址查看DHCP SERCER 的ip pool地址池可以看到分配了一个地址、Total 253个 Used 1个 使用kali工具进行模拟攻击 进行DHCP DISCOVER攻击 此时查看DHCP SERVER d大量的抓包:大量的DHCP Discover包 此时模…...
【愚公系列】《高效使用DeepSeek》055-可靠性评估与提升
🌟【技术大咖愚公搬代码:全栈专家的成长之路,你关注的宝藏博主在这里!】🌟 📣开发者圈持续输出高质量干货的"愚公精神"践行者——全网百万开发者都在追更的顶级技术博主! 👉 江湖人称"愚公搬代码",用七年如一日的精神深耕技术领域,以"…...
AI时代编程教育启示录:为什么基础原理依然不可或缺?
李升伟 编译 在生成式AI重塑编程教育的今天,我作为拥有十年开发者关系团队管理经验、编程训练营教学经历的专业软件工程师,想与大家探讨这个新时代的编程教育之道。 平衡之道:基础原理与AI工具的博弈 当GitHub Copilot、Amazon Q Deve…...
10种电阻综合对比——《器件手册--电阻》
二、电阻 前言 10种电阻对比数据表 电阻类型 原理 特点 应用 贴片电阻 贴片电阻是表面贴装元件,通过将电阻体直接贴在电路板上实现电路连接 体积小、重量轻,适合高密度电路板;精度高、稳定性好,便于自动化生产 广泛应用于…...
剑指Offer(数据结构与算法面试题精讲)C++版——day6
剑指Offer(数据结构与算法面试题精讲)C版——day6 题目一:不含重复字符的最长子字符串题目二:包含所有字符的最短字符串题目三:有效的回文 题目一:不含重复字符的最长子字符串 这里还是可以使用前面&#x…...
freertos韦东山---事件组以及实验
事件组的原理是什么,有哪些优点,为啥要创造出这个概念 在实时操作系统(如 FreeRTOS)中,事件组是一种用于任务间同步和通信的机制,它的原理、优点及存在意义如下: 事件组原理 数据结构…...
架构师面试(二十六):系统拆分
问题 今天我们聊电商系统实际业务场景的问题,考查对业务系统问题的分析能力、解决问题的能力和对系统长期发展的整体规划能力。 一电商平台在早期阶段业务发展迅速,DAU在 10W;整个电商系统按水平分层架构进行设计,包括【入口网关…...
Spring 中的事务
🧾 一、什么是事务? 🧠 通俗理解: 事务 一组操作,要么全部成功,要么全部失败,不能只做一半。 比如你转账: A 账户扣钱B 账户加钱 如果 A 扣了钱但 B 没收到,那就出问…...
Java中的同步和异步
一、前言 在Java中,同步(Synchronous)和异步(Asynchronous)是两种不同的任务处理模式。核心区别在任务执行的顺序控制和线程阻塞行为。 二、同步(Synchronous) 定义:任务按顺序执行…...
vue2 vue3 响应式差异
vue2 响应式原理看这 链接: link 总结: object.defineproperty()是对属性的劫持,对属性劫持有两大缺陷 1. 需要遍历对象的所有属性,深层属性需递归,存在效率问题 2. 后添加的属性,无法获得响应式,因为劫持…...
唯一ID生成器设计方案
《亿级流量系统架构设计与实战》总结 1. 唯一ID的核心需求 • 全局唯一性:分布式系统中所有节点生成的ID不可重复。 • 趋势递增性(可选):ID按时间或序列递增,优化数据库写入性能。 • 高可用性:服务需72…...
OpenCV 图形API(16)将极坐标(magnitude 和 angle)转换为笛卡尔坐标(x 和 y)函数polarToCart()
操作系统:ubuntu22.04 OpenCV版本:OpenCV4.9 IDE:Visual Studio Code 编程语言:C11 描述 计算二维向量的 x 和 y 坐标。 polarToCart 函数根据 magnitude 和 angle 的对应元素表示的每个二维向量,计算其笛卡尔坐标:…...
在 Ubuntu24.04 LTS 上 Docker Compose 部署基于 Dify 重构二开的开源项目 Dify-Plus
一、安装环境信息说明 硬件资源(GB 和 GiB 的主要区别在于它们的换算基数不同,GB 使用十进制,GiB 使用二进制,导致相同数值下 GiB 表示的容量略大于 GB;换算关系:1 GiB ≈ 1.07374 GB ;1 GB ≈ …...
安装和配置Docker
其他版本的安装方式可直接参考官方网站,推荐通过官方网站提供的方式安装Dockers,下面只是个演示的示例,仅供参考 Install | Docker Docs 安装 Docker 的前置准备 1.虚拟机配置: 推荐配置 内存:4GB(最低…...
