LongCat-Image-Editn开源大模型部署:免conda、免torch编译,纯镜像开箱即用

核心内容摘要

RexUniNLU真实案例分享:11类NLP任务在真实业务文本中的输出效果
嵌入式开发高手必备:STM32多路EXTI中断共享处理函数的实战技巧与避坑指南

CHORD-X系统网络协议分析基础:理解视频流传输与API调用

个人主页小张同学作者简介C研发方向学习者个人专栏 《C语言》《数据结构》《C深度剖析从入门到深耕》⭐️人生格言无视中断不弃热枕方得坚持之道。

前言如果你写过 C/C 项目大概率遇到过这些 “崩溃瞬间”调试时突然弹出的 “内存访问冲突” 弹窗、上线后监控面板里莫名飙升的内存占用、排查了 3 天才找到的 “隐藏式内存泄漏”…… 这些问题的根源几乎都指向同一个核心 ——内存管理。

目录

C/C中的内存分布

1 内存分布的题目

2 概念说明

C语言中的动态内存管理方式

1 malloc

2 calloc

3 realloc

4 综合问题

C的内存管理方式

1 new/delete操作内置类型

2 new/delete操作自定义类型

3 申请空间失败时的处理

operator new与operator delete函数重点讲解

1 operator new函数

2 operator delete函数

3 free的实现

4 扩展知识补充

new和delete的底层

实现原理

1 内置类型

2 自定义类型

定位new表达式placement-new了解即可

1 使用格式

2 使用场景

malloc/free与new/delete的对比

1 共同点

2 不同点

C/C中的内存分布我们从C语言开始就接触到了内存的相关问题对内存都有一定的了解那么我们先从一个内存分布的小题目入手

1 内存分布的题目int globalVar 1; static int staticGlobalVar 1; void Test() { static int staticVar 1; int localVar 1; int num1[10] { 1, 2, 3, 4 }; char char2[] abcd; const char* pChar3 abcd; int* ptr1 (int*)malloc(sizeof(int) *

; int* ptr2 (int*)calloc(4, sizeof(int)); int* ptr3 (int*)realloc(ptr2, sizeof(int) *

; free(ptr

; free(ptr

; }选择题选项:A.栈B.堆C.数据段(静态区)D.代码段(常量区)globalVar在哪里____staticGlobalVar在哪里____staticVar在哪里____localVar在哪里____num1 在哪里____char2在哪里____*char2在哪里___pChar3在哪里____*pChar3在哪里____ptr1在哪里____*ptr1在哪里___答案与解析globalVar在哪里_C_staticGlobalVar在哪里_C_staticVar在哪里_C_localVar在哪里_A_num1 在哪里_A_globalVar全局变量在静态区 staticGlobalVar静态全局变量在静态区staticVar静态局部变量在静态区 localVar局部变量在栈区num1局部变量在栈区char2在哪里_A_*char2在哪里_A_pChar3在哪里_A_*pChar3在哪里_D_ptr1在哪里_A_*ptr1在哪里_B_char(等价于char2[0]第一个元素的地址)局部变量在栈区 char2是一个数组把后面常量串拷贝过来到数组中(数组在栈上)*char2是数组的第一个元素的值所以*char2在栈上pChar3局部变量在栈区 *pChar3得到的是字符串常量字符在代码段ptr1局部变量在栈区 *ptr1得到的是动态申请空间的数据在堆区大家可以看图结合着解析一块理解

2 概念说明

栈又叫堆栈——非静态局部变量/函数参数/返回值等等栈是向下增长的。

内存映射段是高效的I/O映射方式用于装载一个共享的动态内存库。

用户可使用系统接口创建共享共享内存做进程间通信。

Linux里面会学到这块现在只需要了解一下

堆用于程序运行时动态内存分配堆是可以上增长的。

数据段--存储全局数据和静态数据。

代码段--可执行的代码/只读常量。

C语言中的动态内存管理方式

1 mallocmalloc函数的功能是开辟指定字节大小的内存空间如果开辟成功就返回该空间的首地址如果开辟失败就返回一个NULL。

传参时只需传入需要开辟的字节个数。

malloc不会初始化所开辟的内存空间

2 calloccalloc函数的功能也是开辟指定大小的内存空间如果开辟成功就返回该空间的首地址如果开辟失败就返回一个NULL。

calloc函数传参时需要传入开辟的内存用于存放的元素个数和每个元素的大小。

calloc函数开辟好内存后会将空间内容中的每一个字节都初始化为0。

3 reallocrealloc函数可以调整已经开辟好的动态内存的大小第一个参数是需要调整大小的动态内存的首地址第二个参数是动态内存调整后的新大小。

realloc函数与上面两个函数一样如果开辟成功便返回开辟好的内存的首地址开辟失败则返回NULL。

realloc函数调整动态内存大小的时候有三种扩展情况

原地扩。

需扩展的空间后方有足够的空间可供扩展此时realloc函数直接在原空间后方进行扩展并返回该内存空间首地址(即原来的首地址)。

异地扩。

需扩展的空间后方没有足够的空间可供扩展此时realloc函数会在堆区中重新找一块满足要求的内存空间把原空间内的数据拷贝到新空间中并主动将原空间内存释放(即还给操作系统)返回新内存空间的首地址。

扩充失败。

需扩展的空间后方没有足够的空间可供扩展并且堆区中也没有符合需要开辟的内存大小的空间。

结果就是开辟内存失败返回一个NULL。

4 综合问题void Test () { //

malloc/calloc/realloc的区别是什么 int* p1 (int*)malloc(4 * sizeof (int)); int* p2 (int*)calloc(4, sizeof (int)); int* p3 (int*)realloc(p2, sizeof(int)*

; // 这里需要free(p

吗 free(p3 ); }这里是不需要free(p

的。

因为我们在realloc时有两种情况第一种是原地扩容那么p2和p3的地址是一样的释放一次就行了。

第二种异地扩容原来p2指向的内存块已经被realloc自动释放了所以也是不需要考虑手动释放p2的。

详见上方realloc部分面试题

malloc/calloc/realloc的区别malloc分配指定字节内存不初始化数据随机。

calloc分配内存并初始化为 0需指定元素个数和单个元素字节数。

realloc调整已分配内存大小可扩容或缩容可能复用原内存或重新分配并复制数据有两种情况原地扩容或者异地扩容这个在上面也提到过。

malloc的

实现原理基于brk调整数据段地址和 mmap映射新内存区域系统调用结合内存池管理按大小分类维护空闲块分配时快速匹配减少系统调用开销这个问题可以通过这个视频来深入了解【CTF】GLibc堆利用入门-机制介绍_哔哩哔哩_bilibili大家如果想更多的了解C语言内存管理的相关内容可以到草莓熊大佬的CSDN博客中去了解【C语言动态内存管理】--动态内存分配的意义malloc和freecalloc和realloc常见的动态内存的错误动态内存经典笔试题分析柔性数组

总结C/C中程序内存区域划分-CSDN博客

C的内存管理方式C语言内存管理方式在C中可以继续使用但有些地方就无能为力而且使用起来比较麻烦因 此C又提出了自己的内存管理方式通过new和delete操作符进行动态内存管理。

1 new/delete操作内置类型#includeiostream using namespace std; int main() { // 动态申请一个int类型的空间 int* p1 new int; // 单个对象 // 动态申请10个int类型的空间 int* p2 new int[10];// 数组 // 动态申请一个int类型的空间并初始化为10 int* p3 new int(

; // 单个对象 // 前四个初始化1234后面的默认初始化为0 int* p4 new int[10] {1, 2, 3, 4}; // 数组 delete p1; delete[] p2; delete p3; delete[] p4; return 0; }注意申请和释放单个元素的空间使用new和delete操作符申请和释放连续的空间使用new[]和delete[]。

注意匹配起来使用。

大家可能会觉得这些操作我们之前使用malloc和手动赋值初始化等操作也可以实现虽然没这么方便但也不是完全不行那C到底为什么要引入新的new/delete呢我们接着往下看看自定义类型的操作。

2 new/delete操作自定义类型#includeiostream using namespace std; class A { public: A(int a

: _a(a) { cout A(): this endl; } ~A() { cout ~A(): this endl; } private: int _a; }; struct ListNode { ListNode* _next; int _val; ListNode(int val) :_next(nullptr) , _val(val) { //... } }; int main() { // new/delete 和 malloc/free 最大区别是 // new/delete对于【自定义类型】除了开空间还会调用构造函数/析构函数 //mallc只能开空间不调用构造初始化 A* p1 (A*)malloc(sizeof(A)); //free也是只能销毁不能调用析构 free(p

; A* p2 new A; A* p3 new A(

; delete p2; delete p3; ListNode* n1 new ListNode(

; ListNode* n2 new ListNode(

; ListNode* n3 new ListNode(

; //而malloc需要ListBuyNode详见数据结构—单链表部分 // new/delete 和 malloc/free对内置类型是几乎是一样的 int* p4 (int*)malloc(sizeof(int)); int* p5 new int; free(p

; delete p5; return 0; }我们可以发现new/delete在处理自定义类型的时候会比malloc等操作好很多会调对应的构造函数和析构函数。

注意new/delete 和 malloc/free 最大区别是 new/delete对于【自定义类型】除了开空间还会调用构造函数/析构函数。

3 申请空间失败时的处理malloc在空间申请失败后会返回NULL而new在申请空间失败时会抛异常。

#includeiostream using namespace std; //这个程序退出码不为0异常退出跟malloc不一样new申请失败会抛异常 void func() { int i 1; int* ptr nullptr; do { if (i

//为了方便调试在 { int x 0; } ptr new int[1024 * 1024];//throw cout i : ptr \n; } while (ptr); cout i : ptr \n; } int main() { //捕获异常 try { func();//可以直接跳跃函数到异常 } catch (const exception e) { cout e.what() \n;//打印错误信息 } return 0; }这里利用了一个小技巧方便调试用编译器那个条件断点停在第493次。

我们发现到495次时出现异常直接跳转到打印错误信息

operator new与operator delete函数重点讲解new和delete是用户进行动态内存申请和释放的操作符operator new 和 operator delete是系统提供的全局函数new 在底层调用 operator new全局函数来申请空间delete 在底层通过operator delete全局函数来释放空间。

下面大家来看看他们的底层代码

1 operator new函数/* operator new 该函数实际通过malloc来申请空间当malloc申请空间成功时直接返回申请空间 失败尝试执行空间不足应对措施如果改应对措施用户设置了则继续申请否 则抛异常。

*/ void* __CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc) { // try to allocate size bytes void* p; while ((p malloc(size))

if (_callnewh(size)

{ // report no memory // 如果申请内存失败了这里会抛出bad_alloc 类型异常 static const std::bad_alloc nomem; _RAISE(nomem); } return (p); }

2 operator delete函数/* operator delete: 该函数最终是通过free来释放空间的 */ void operator delete(void* pUserData) { _CrtMemBlockHeader* pHead; RTCCALLBACK(_RTC_Free_hook, (pUserData,

); if (pUserData NULL) return; _mlock(_HEAP_LOCK); /* block other threads */ __TRY /* get a pointer to memory block header */ pHead pHdr(pUserData); /* verify block type */ _ASSERTE(_BLOCK_TYPE_IS_VALID(pHead-nBlockUse)); _free_dbg(pUserData, pHead-nBlockUse); __FINALLY _munlock(_HEAP_LOCK); /* release other threads */ __END_TRY_FINALLY return; }

3 free的实现free其实是一个宏函数是_free_dbg(p, _NORMAL_BLOCK)的别名。

/* free的实现 */ #define free(p) _free_dbg(p, _NORMAL_BLOCK)通过上述两个全局函数的实现知道operator new 实际也是通过malloc来申请空间如果 malloc 申请空间成功就直接返回否则执行用户提供的空间不足应对措施如果用户提供该措施就继续申请否则就抛异常。

operator delete 最终是通过free来释放空间的。

4 扩展知识补充#includeiostream using namespace std; class A { public: A(int a

:_a(a) { cout A(); this \n; } ~A() { cout ~A(); this \n; } private: int _a; }; int main() { //A* p3 (A*)operator new(sizeof(A));//可以显示调用 //可以看看反汇编 A* p1 new A(

; delete p1; A* p2 new A[10];//看反汇编为啥会是44或48而不是40; //MSVC通常添加4字节计数32位或8字节计数64位 //但是把析构注释掉就又是40了 delete[] p2; return 0; }我们先通过转到反汇编来看一下new和delete的底层反汇编从底层反汇编来看new在使用时会先调用operator new再调用构造函数delete会先调用一个封装好的函数这个函数再去先调用析构再调用operator delete的。

下面我们来解决一下为什么在有构造函数的情况下p2底层申请的空间是48或44而不是40这是因为在 C 中当使用new[ ]动态分配对象数组时如果类 A 有析构函数编译器会在分配的内存块开头额外存储一个 “数组长度信息”用于在delete[ ]时知道需要调用多少次析构函数。

具体分析

有析构函数的情况假设 sizeof(A) 是 4比如 A 是简单的类包含一个 int 成员等。

当用new A[10] 时编译器实际分配的内存不仅要存 10 个A对象共 10×440 字节还要额外存一个 “数组长度”MSVC通常添加4字节计数32位或8字节计数64位用来记录数组元素个数10。

所以总分配内存是 40444 或 40848 字节这就是反汇编看到44/48的原因。

后续delete[ ]时会根据这个额外存储的长度信息调用 10 次A的析构函数再释放内存。

没有析构函数的情况如果把A的析构函数注释掉编译器判断不需要额外记录 “数组长度”因为这里的A类没啥需要释放的资源不需要调用析构函数也就不需要知道要调用多少次。

此时分配的内存就是 10×440 字节所以反汇编看到的是 40 。

简单来说类是否有析构函数会影响new[ ]时是否额外分配 “数组长度信息” 的空间从而导致总分配内存大小不同。

值得一提是这也就是为啥我们前面提到new和delete要匹配使用如果不匹配的话那这里释放的就有问题了从中间那个位置开始释放掉了。

但是我们知道空间的释放只可以从起始位置开始释放不可以分段释放。

new和delete的底层

实现原理

1 内置类型如果申请的是内置类型的空间new和mallocdelete和free基本类似不同的地方是new/delete申请和释放的是单个元素的空间new[]和delete[]申请的是连续空间而且new在申请空间失败时会抛异常malloc会返回NULL。

2 自定义类型new的原理

先调用operator new函数申请空间

在申请的空间上执行构造函数完成对象的构造delete的原理

先在空间上执行析构函数完成对象中资源的清理工作

调用operator delete函数释放对象的空间new T[N]的原理

调用operator new[]函数在operator new[]中实际调用operator new函数完成N个对象空间的申请。

在申请的空间上执行N次构造函数delete[ ]的原理

在释放的对象空间上执行N次析构函数完成N个对象中资源的清理

调用operator delete[]释放空间实际在operator delete[]中调用operator delete来释放空间

定位new表达式placement-new了解即可定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象。

1 使用格式new (place_address) type或者new (place_address) type(initializer-list)place_address必须是一个指针initializer-list是类型的初始化列表

2 使用场景定位new表达式在实际中一般是配合内存池使用。

因为内存池分配出的内存没有初始化所以如果是自定义类型的对象需要使用new的定义表达式进行显示调构造函数进行初始化。

#includeiostream using namespace std; class A { public: A(int a

:_a(a) { cout A(); this \n; } ~A() { cout ~A(); this \n; } private: int _a; }; //定位new表达式的使用 int main() { // p1现在指向的只不过是与A对象相同大小的一段空间还不能算是一个对象因为构造函数没有执行 A* p1 (A*)operator new(sizeof(A)); //显示调用构造函数 new(p

A(

;//1为初始化的值 //有参数就需要传这里其实可以传也可以不传看你实现的构造函数 //p1-A(

; p1-~A();//析构可以直接这样用构造不行; operator delete(p

; A* p2 (A*)operator new(sizeof(A)); new(p

A; p2-~A(); operator delete(p

; return 0; }关于为什么可以p1-~A()而不能p1-A()构造函数Constructor的特殊性构造函数没有地址构造函数不是普通的成员函数它在编译时没有函数指针构造函数不可被直接调用因为调用构造函数意味着创建一个新对象这需要特定的上下文内存位置构造函数的调用是隐式的由编译器在对象创建时自动安排析构函数Destructor的特殊性析构函数有地址虽然不能直接取地址但可以通过指针调用析构函数清理已有对象它操作的是一个已经存在的对象析构函数的调用可以是显式的在对象的生命周期结束时下面给大家一个具体的应用场景大家不需看懂代码体会其中定位new的应用即可

malloc/free与new/delete的对比

1 共同点都是从堆上申请空间并且需要用户手动释放。

2 不同点

malloc和free是函数new和delete是操作符

malloc申请的空间不会初始化new可以初始化

malloc申请空间时需要手动计算空间大小并传递new只需在其后跟上空间的类型即可如果是多个对象[]中指定对象个数即可

malloc的返回值为void*, 在使用时必须强转new不需要因为new后跟的是空间的类型

malloc申请空间失败时返回的是NULL因此使用时必须判空new不需要但是new需要捕获异常

申请自定义类型对象时malloc/free只会开辟空间不会调用构造函数与析构函数而new在申请空间后会调用构造函数完成对象的初始化delete在释放空间前会调用析构函数完成空间中资源的清理释放这些特点大家不用硬背结合着用法、核心特性、原理来记忆就可以其中第六点是最重要的。

本篇博客的完整原代码小张同学的CPP仓库——gitee.com往期回顾C 类和对象五初始化列表、static、友元、内部类等7大知识点全攻略-CSDN博客C 类和对象四const成员函数、取地址运算符重载全精讲-CSDN博客C 类和对象三拷贝构造函数与赋值运算符重载之核心实现-CSDN博客C 类和对象二实例化、this指针、构造函数、析构函数详解-CSDN博客结语C 内存管理重要在实践出真知从内存分布到new/delete底层每一个知识点都需要在代码里体会摸透希望这篇博客能帮你扫清疑惑。

欢迎大家在评论区里讨论如果文章对你有帮助的话欢迎评论点赞收藏加关注感谢大家的支持。

户外刺激挑战任务Ic-户外刺激挑战任务应用

百度百家号客服电话人工服务

123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123 123