傲世仙尊头像

内存管理-new与malloc的本质区别和delete前的4个字节

内存管理:new/delete 与 malloc/free 的本质区别,与 delete[] 前面那 4 个字节

我的github:(https://github.com/xcx55/ubuntu-linux-project)
感谢各位大佬参观我的github!!

离开类和对象,进内存管理。这章要回答两件事:C++ 为什么要在 C 的 malloc/free 之外再造一套 new/delete? 以及 delete 和 delete[] 到底差在哪?——后者那个"差 4 个字节"的细节,是理解整章的钥匙。


一、内存区域划分,与编译期那把"锁"

先摆一张表(Linux 下进程地址空间从高到低):

区域存放内容特点
栈局部变量、函数参数、返回值向下生长,函数栈帧销毁即释放
堆malloc/new 出来的空间向上生长,手动申请手动释放
数据段(静态区)全局变量、static 变量程序运行期间一直存在
代码段(常量区)可执行代码、字符串字面量、只读常量只读,生命周期最长

一个让我纠结了很久、后来想通的问题,写在笔记的第一行:

为什么会有编译期这个东西?明明使用地址就可以访问到想要的一切,硬件上地址随便访问,但就是要在语法层面来加上锁!

答案是规范性。编译器要求:相同域才能访问到相同域的量,无论 static 还是 const。理论上拿到地址就能访问一切,但编译器把这条野路子堵死了——除了指针强转这种主动"破域"的手法,其余只能在相同域内相互访问。

我的理解:这段代码通过了编译器检查,就直接转换为地址,硬件直接找——编译期管的是"你能不能写这行代码",编译通过之后硬件层面就只是在认地址了。

成员函数为什么在"常量区/代码段"

成员函数存放在代码段,和静态区一样——硬件对此并无要求,是编译器在检查。

有意思的是检查的次数:对非 static 成员函数,编译器查两次:

  1. 是否为相同域(类域检查);
  2. 是否有 this 指针(C++ 创始者规定的)。

而 static 成员函数只查一次——因为它没有 this,自然不用查第二项。这也解释了为什么 A::GetCount() 能直接调用:没有 this 需要传,就不需要一个对象作为调用载体。

const 并没有改变生命周期

一个容易记错的点:

Const 并无修改变量生命周期的能力,加上 const 仅仅是模仿常量区的只读性。

以字符串为例,两条语句看着像,其实差得远:

const char* pchar3 = "abcd";   // ①
char char2[] = "abcd";         // ②
  • ①的第一个 "abcd" 是常量区本体,所以它本身就在常量区,生命周期长且只读。pchar3 只是个指向它的指针;
  • ②的 "abcd" 是数组:把常量区的那份拷贝到栈区,在栈区重新分配空间。所以你可以改 char2[0],改不了 pchar3[0]。

另外,const 是在链接期之后才被划到常量区的;链接期之前,const 变量属于文件域(只在当前 .cpp 内可见)。这也是为什么 const 全局变量能直接写进头文件——每个 .cpp 各拿各的那份,不会在链接时撞车。

二、C 语言动态内存回顾

函数作用
malloc开指定字节的空间,不初始化
calloc字节量 × 单字节量,每个字节初始化为 0
realloc扩容:目标地址 + 总字节数,保留原有数据
free释放

realloc 分两种情况:原地扩容(后面还有足够空间,直接往后延伸,地址不变)和异地扩容(后面不够,重新找一块,把旧数据拷过去,返回新地址)。这个区分在后面写 vector 的扩容时会再遇到。

三、new 与 malloc 的第一个区别:它会调构造函数

看 C++ 这边的写法:

int* p1 = new int;              // 不开初始化
int* p2 = new int(10);          // 初始化为 10
int* p3 = new int[10]{1,2,3};   // 数组 + 列表初始化

New 甚至不用强转——malloc 返回 void*,必须写 (int*)malloc(...);new 因为有类型信息,直接返回对应类型指针。

但真正的分水岭在自定义类型:

动态内存开辟会调用构造函数。如果只是用 malloc,就无法以文件域访问类域 private 量——而且我们不能手动调用构造函数,new 就可以调用构造函数。

malloc 只负责"给你一块字节",它不知道这块字节里要放一个 Date;new 做的是"开空间 + 调用构造函数初始化"两件事。

还有一句挺有意思的吐槽:

编译器就是懒。你构建就必须要调用构造函数啊,参数要合适——编译器只会调默认构造,其余不认识,也懒。

这正好和上一篇的初始化列表接上:对于自定义类型成员,编译器只会自动调默认构造函数;想让它调别的构造函数,你得在初始化列表里手动把参数写清楚。

new 失败的判断方式与 malloc 不同

失败时判断方式
malloc返回 NULLif (p == NULL)
new抛异常try/catch

所以不能以返回值来判断 new 是否开辟失败——new 失败不会返回空指针,而是走异常机制(C++ 的 bad_alloc)。

四、operator new 与 operator delete:new 底下那层

operator new 和 operator delete 是系统自带的全局函数。而 new 之所以能开空间,是因为它自动转换为 operator new —— 这本质上就是运算符重载(new 也是运算符)。

所以完整的链条是:

new    →  operator new    →  构造函数
delete →  析构函数        →  operator delete

展开一点:

  • operator new[] → operator new → 构造函数
  • 析构函数 → operator delete[] → operator delete

operator new 内部最终还是调 malloc(在 malloc 失败时按 new 的语义抛异常),operator delete 内部最终是 free。这就是 new/delete 与 malloc/free 的关系:不是两套平行的东西,而是 new/delete 在 malloc/free 之上加了"构造/析构"这层语义。

五、delete[] 前面那 4 个字节

这是整章最漂亮的细节。看一个问题:

class A { int _a; };   // 4 字节

A* p = new A[10];      // 开 10 个 A,是多少字节?

第一反应是 40 字节——不是,是 44 个字节。

那多出来的 4 个字节是干什么的?

为什么要在指针首位储存个数?因为要 delete,知道调用多少次析构函数。

也就是说,当你写 new A[10] 时,编译器实际申请了 44 字节:开头 4 字节存"数组元素个数",后面 40 字节才是真正的 10 个 A。返回的指针 p 指向的是第 5 个字节,即真正数据的开头。

这解释了两件常见的事:

① 为什么 new[] 必须配 delete[]

delete[] p 的执行过程是:先从头指针往前偏移 4 个字节,读出个数 n,然后循环 n 次调用析构函数,最后整体释放空间。

而 delete p 不会做"往前偏移"这个动作,它会直接把 p 当作一整块空间的起始地址去释放——会崩溃掉,因为程序知道这个 pa 的前面还有 4 个字节。释放地址和申请地址对不上,堆管理器直接报错。

② 有没有显式定义析构,行为不一样

  • 显式定义了析构函数 → 计算机认为"有资源要释放" → 需要记录个数 → 多申请 4 字节;
  • 没有显式定义析构 → 说明没有资源要释放 → 不调用析构函数 → 也就不用存个数 → 不额外申请。

这个"按需分配"的设计很符合 C++ 一贯的脾气:不为你用不到的东西付出任何成本。但千万别利用这个特性去手动释放——结果难料。

delete[] 的完整逻辑串一遍:告诉计算机"要 free 的这是一个数组类型",然后从头部取个数 N → 循环 N 次(每次:调用析构函数清理内部资源)→ 再整体 free 掉空间。边析构边 free 掉的指针都是原生内置类型。

一句话收尾,也是笔记里反复出现的那条原则:

析构函数只负责"清理内部资源",绝对不负责"释放自己"。


转载自 CSDN-专业IT技术社区

原文链接:https://blog.csdn.net/xiongditian/article/details/168335312

文章来源转载

评论

评论列表

微信小程序
QQ小程序

关于作者

点赞数:0
关注数:0
粉丝:0
文章:0
关注标签:0
加入于:--