
◆ 博主名称: 晓此方-CSDN博客
大家好,欢迎来到晓此方的博客。
⭐️C++系列个人专栏:
⭐️踏破千山志未空,拨开云雾见晴虹。 人生何必叹萧瑟,心在凌霄第一峰
目录
3.4.1operator new 与 operator delete 函数(重要点进行讲解)
3.6总结malloc/free 和 new/delete 的区别(面试常考)
0.1概要&序論
这里是此方,久しぶりです!。本文内容极长,将详细介绍C语言内存管理包括:malloc、calloc、relloc、free、常见内存管理错误等内容和C++内存管理包括:new、delete以及他们的底层原理,最后会总结C/C++内存管理的区别。内容干货极其丰富!这里是「此方」。让我们现在开始吧!
一,布局模型与常见误区解析
1.1C/C++内存布局
C++的内存管理和c语言一致,以下是一图流分析其内存布局:

我们来简要说明一下:
- 栈又叫堆栈,存放非静态局部变量/函数参数/返回值等等,栈是向下增长的。
- 内存映射段是高效的I/O映射方式,用于装载一个共享的动态内存库。用户可使用系统接口创建共享共享内存,做进程间通信。(暂时不用管,现在只需要了解一下)
- 堆用于程序运行时动态内存分配,堆是可以上增长的。(本文主要介绍)
- 数据段(俗称静态区)--存储全局数据和静态数据。
- 代码段--可执行的代码/只读常量。
我们可以这么理解:某种程度上说,分区分的是生命周期。
- 栈内存在栈帧销毁时销毁。
- 静态区内存一直到程序运行结束。
- 堆内存的如果不去free/delete一直都在。
因此:实际上需要我们程序员管理的内存——只有堆区内存。
1.2内存布局易误解点
题目:
char char2[] = "abcd"; const char* pChar3 = "abcd"
- char2在哪里?____
- *char2在哪里?____
- pChar3在哪里?____
- *pChar3在哪里?____
答案:栈,栈,栈,代码段(最后一个是不是猜错了doge)
解释:
- char2代表该数组的首元素指针存放在栈中很合理。
- “abcd”只读字符串在代码段中,char2[]数组将该字符串拷贝一份到栈区中,*char自然指向被拷贝在在栈上的那个数组的首元素。
- pChar3是指向“abcd”的指针,存放在栈中合理。
- 但pchar3并没有像char2一样拷贝一份字符串到栈中,所以解引用的结果自然就在代码段上。
二,复习C语言的内存管理方法
头文件:<stdlib.h>
2.1malloc
C 语言提供了一个动态内存开辟的函数:
void* malloc(size_t size);
- 这个函数向内存申请一块连续可用的空间,并返回指向这块空间的指针。
- 如果开辟成功,则返回一个指向开辟好空间的指针。
- 如果开辟失败,则返回一个 NULL 指针,因此 malloc 的返回值一定要做检查。
- 返回值的类型是 void*,所以 malloc 函数并不知道开辟空间的类型,具体在使用的时候由使用者自己来决定。
- 如果参数 size 为 0,malloc 的行为在标准中是未定义的,取决于编译器。
一个鲜明的例子:
int* ret = (int*) malloc ( sizeof(int) * 10 ) ;
if (ret == NULL){
perror ( "malloc fail" );
exit (1);
}
2.2calloc
C 语言还提供了一个函数叫 calloc,calloc 函数也用来动态内存分配。原型如下:
void* calloc(size_t num, size_t size);
- 函数的功能是为 num 个大小为 size 的元素开辟一块空间并把每个字节初始化为 0。
- 与函数 malloc 的区别只在于初始化。
一个鲜明的例子:
int *p = (int*)calloc(10, sizeof(int));
if(NULL != p){
for(int i=0; i<10; i++){
printf("%d ", *(p+i));
}
}
2.3relloc
realloc 函数的出现让动态内存管理更加灵活。
有时我们会发现过去申请的空间太小了,有时候我们又会觉得申请的空间过大了,那为了合理的使用内存,我们一定会对内存的大小做灵活的调整。那 realloc 函数就可以做到对动态开辟内存大小的调整。
函数原型如下:
void* realloc(void* ptr, size_t size);
- ptr 是要调整的内存地址
- size 调整之后新大小
- 返回值为调整之后的内存起始位置。
- 这个函数调整原内存空间大小的基础上,还会将原来内存中的数据移动到新的空间。
realloc 在调整内存空间的是存在两种情况:
- 情况1:原有空间之后有足够大的空间
- 情况2:原有空间之后没有足够大的空间

- 当是情况1的时候,要扩展内存就直接原有内存之后直接追加空间,原来空间的数据不发生变化。
- 当是情况2的时候,原有空间之后没有足够多的空间时,扩展的方法是:在堆空间上另找一个合适大小的连续空间来使用。这样函数返回的是一个新的内存地址。
2.4free
C 语言提供了一个函数 free,专门是用来做动态内存的释放和回收的,函数原型如下:
void free(void* ptr);
- free 函数用来释放动态开辟的内存。
- 如果参数 ptr 指向的空间不是动态开辟的,那 free 函数的行为是未定义的。
- 如果参数 ptr 是 NULL 指针,则函数什么事都不做。
2.5罗列常见的内存管理错误
- 对 NULL 指针的解引用操作。
- 对动态开辟空间的越界访问。
- 对非动态开辟内存使用 free 释放。
- 使用 free 释放一块动态开辟内存的一部分(这个很容易忘记)
- 对同一块动态内存多次释放。
- 动态开辟内存忘记释放(内存泄漏)(这个很容易发生)
三,C++内存管理方法
3.1new/delete管理体系
3.1.1开辟单个空间与释放
void Test(){
int * ptr = new int;
delete ptr;
}
new开辟了int大小的空间给ptr。并delete销毁它。整个过程相对于C语言非常简单。
- new:开辟空间。——对标malloc
- delete:释放空间。——对标free
我们也可以在开辟空间的时候初始化:如下:对标calloc,为ptr开辟空间的时候并同时初始化为1.
int * ptr = new int (1);
3.1.2开辟多个连续的空间与释放
void Test(){
int * ptr = new int[10];
delete [] ptr;
}
如上,我们开辟了十个int大小的空间给ptr指针,让其指向这个10*sizeof(int)大小的数组。值得注意的是,在delete释放内存的时候要加上[]。
同样的,我们可以在开辟空间的时候初始化:
int * ptr = new int[5]{1,2,3,4,5};
int * ptr = new int[10]{1,2,3,4,5};
int * ptr = new int[10]{0};
结合上面的代码,初始化一共有三种情况:
- 完全初始化。按照你初始化的来。
- 部分初始化,剩下未初始化是部分默认是0。
- 直接给0,全部初始化为0。
C++搞出来这个不是单纯是为了优化C语言那一套的写法。以上都是表层的内容。接下来深入讲解
3.2C+++针对自定义类型的内存管理
对于自定义类型,C++在申请空间的时候会自动调用构造函数,释放空间的是后会自动调用析构函数。
演示代码:如图,我们new了10个A类类型的空间。并delete释放。于是构造函数和析构函数就被调用了10次。

3.2.1调用构造的重要性
我们来看一段链表代码:在如下代码中,我们创建了多个ListNode节点,并通过new动态分配内存:
struct ListNode{
int val;
ListNode* next;
ListNode(int x)
: val(x)
, next(nullptr)
{}
};
int main(){
A* p1 = new A;
A* p2 = new A(1);
delete p1;
delete p2;
ListNode* n1 = new ListNode(1);
ListNode* n2 = new ListNode(1);
ListNode* n3 = new ListNode(1);
ListNode* n4 = new ListNode(1);
n1->next = n2;
n2->next = n3;
n3->next = n4;
}
同样的,发生了两个步骤:
- 分配内存:为ListNode类型的对象分配堆空间;
- 调用构造函数:自动执行构造函数初始化成员变量。
若没有构造函数的自动调用:
- val可能是未定义值(如垃圾数据);
- next可能指向随机地址,导致后续访问崩溃;
- 链表连接毫无逻辑,引发严重运行时错误。
因此new自动调用构造函数,确保了每个节点在创建时就处于已知的状态。
3.2.2对象开辟空间的方法
1,有默认构造函数并开辟一个对象大小空间
A* p1 = new A;
2,有默认构造函数并开辟多个对象大小空间
A* p3 = new A[3];
3,没默认构造函数并开辟一个对象大小空间
A* p2 = new A(2, 2);
4,拷贝构造多个对象大小空间(最基础版)
A aa1(1, 1);
A aa2(2, 2);
A aa3(3, 3);
A* p3 = new A[3]{aa1, aa2, aa3};
5,拷贝构造多个对象大小空间(使用匿名对象版)编译器会自己优化。
A* p4 = new A[3]{ A(1,1), A(2,2), A(3,3) };
6,拷贝构造多个对象大小空间(使用C++11特性版)多参数构造隐式类型转换。
A* p5 = new A[3]{ {1,1}, {2,2}, {3,3} };
3.3C++内存开辟失败与抛异常
一般情况下我们不再使用C语言的malloc那一套方法。为什么我们以前要写perror那一套检查法,而C++没有?C++我们引入了一种更加先进的方法:C++异常(我会在C++进阶会详细讲)
malloc失败后返回空,new失败后不是返回空而是抛异常。(我们在C++检查返回值是没有用的),内存开辟失败并不常见,先造一个简易的失败模拟器:

如图,弹窗提示开辟内存失败。(开辟内存太多而无法实现),现在出现了这种异常,在C++中,我们要尝试并捕获这种异常:
try
{
void* pa = new char[1024 * n];
}
catch (const exception& e)
{
}
- "try{ }catch"这一套用来捕获异常。
- exception是标准库异常类类型,不可修改。
- e是异常类型变量。
try
{
void* pa = new char[1024 * n];
}
catch (const exception& e)
{
cout << e.what() << endl;
}
如上代码,捕获异常后,我们还要知道到底发生了什么回事:异常类型变量e调用what()函数可以帮助我们。

如上,what()函数返回异常信息"内存申请失败:bad allocation",说明我们异常捕获成功,并没有出现弹窗。
插曲:摩尔定律
英特尔(Intel)联合创始人之一戈登·摩尔(Gordon Moore)在 1965年 提出的一个经验性预测,它描述了半导体技术发展速度的一个趋势。摩尔最初观察到,集成电路上可容纳的晶体管数量大约每 18到24个月 便会增加一倍,同时成本保持不变。后来,这个定律常被引申为:
集成电路(IC)上的晶体管数量大约每两年翻一番,性能也随之提升一倍。
最初,我们使用的32位计算机,它的内存空间是2^32字节,也就是大约4GB。但是现在的64位计算机他的内存来到了2^64字节。2^64字节 = 18446744073709551616 字节(18万4467亿)
| 特性 | 32位系统 (典型例子) | 64位系统 |
|---|---|---|
| 总虚拟地址空间 | 4GB左右 | 18,400,000,000 GB |
| 内核空间划分 | 例如 1GB (固定或可选2GB) | 例如 128TB (或更大,非固定比例) |
| 用户空间划分 | 例如 3GB (或2GB),其中大部分用于堆等 | 例如 128TB (或更大),堆等用户内存从中分配 |
| 堆大小限制 | 受限于总用户空间 (约 2-3GB) | 受限于进程虚拟地址空间上限和物理内存,远超32位限制 |
上面介绍了C++内存开辟的所有使用方法,但是知道这些显然不够,让我们深入底层再探讨探讨
3.4C++内存开辟的底层逻辑
3.4.1operator new 与 operator delete 函数(重要点进行讲解)
new 和 delete 是用户进行动态内存申请和释放的操作符,operator new 和 operator delete 是系统提供的全局函数(虽然有operator,但是他们不是任何一个函数的函数重载),new 在底层调用 operator new 全局函数来申请空间,delete 在底层通过 operator delete 全局函数来释放空间。
3.4.1.1operator new
看看这个函数的底层实现:
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
// try to allocate size bytes
void *p;
while ((p = malloc(size)) == 0)
if (_callnewh(size) == 0)
{
// report no memory
// 如果申请内存失败了,这里会抛出bad_alloc 类型异常
static const std::bad_alloc nomem;
_RAISE(nomem);
}
return (p);
}
该函数实际通过malloc来申请空间,当malloc申请空间成功时直接返回;申请空间失败,尝试执行空间不足应对措施。如果改应对措施用户设置了,则继续申请,否则抛异常。
3.4.1.2operator delete
operator delete: 该函数最终是通过free来释放空间的,为什么这么说?看底层实现。
这是operator delete的底层:
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
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;
}
这一句:_free_dbg( pUserData, pHead->nBlockUse );delete正在调用函数_free_dbg来释放空间。
我们再看看free的底层:
#define free(p) _free_dbg(p, _NORMAL_BLOCK)
宏定义_free_dbg这个函数为free()。所以free本质上就是_free_dbg,而delete调用了它。C++的设计师在设计他的时候沿用了C语言的底层原理。
3.4.2new和delete的实现原理
3.4.2.1内置类型
如果申请的是内置类型的空间,new和malloc,delete和free基本类似,不会调用构造析构函数。不同的地方是:
- new/ delete申请和释放的是单个元素的空间,new[ ]和delete[ ]申请和释放的是连续空间,
- new在申请空间失败时会抛异常,malloc会返回NULL。
3.4.2.2自定义类型
new的原理
- 调用operator new函数申请空间
- 在申请的空间上执行构造函数,完成对象的构造
new开空间的时候是不会自己搞一套开空间的方法,它会去调用malloc,但是它不会自己去调用那一套C的malloc,而是一套包装形态的malloc:即:opreator new。原因也很简单,为了C++的异常那一套操作。
delete的原理
- 在空间上执行析构函数,完成对象中资源的清理工作
- 调用operator delete函数释放对象的空间
new T[N]的原理
- 调用operator new[]函数,在operator new[]中实际调用operator new函数完成N个对象空间的申请
- 在申请的空间上执行N次构造函数
delete[]的原理
- 在释放的对象空间上执行N次析构函数,完成N个对象中资源的清理
- 调用operator delete[]释放空间,实际在operator delete[]中调用operator delete来释放空间
3.4.3从汇编代码中看底层调用操作
我们用来测试的类Asser:
class Asser{
public:
Asser(int x = 0, int y = 1)
:_x(x)
,_y(y)
{}
~Asser(){
_x = 0;
_y = 0;
}
private:
int _x;
int _y;
};
new的底层

如上图,我们在为str开辟Asser大小的空间时,先后调用了operator new函数和构造函数
delete的底层

如上图,delete底层调用了Asser::`scalar deleting destructor' (07FF6EFA71055h) ,但是实际上,这个函数是operator delete函数和析构函数的包装。

3.5C/C++内存开辟的各种错乱情况
3.5.1C/C++混用
int main()
{
int* ptr=new int ;
free(ptr);
return 0;
}
程序会不会i崩溃?会不会内存泄漏?
先说结论:不会崩溃也不会内存泄漏。
- 内置类型不涉及构造和析构,没有调用构造函数的内置类型不会发生内存泄漏。这里new等同于malloc
- 调用free调用的是free_dbg,delete也是调用free_dbg,free等同于delete。
对内置类型可以这么写,但是不建议你这么写。但是自定义类型你不能这么搞,用free你少调用了一个析构函数。如果自定义类型的析构函数释放了xxx内存,那么这个时候就会出现内存泄漏。但是程序不会崩溃。
3.5.2new/delete多对单
int main()
{
Asser* ptr =new Asser[10];
delete ptr;
return 0;
}
先说结论:可能崩溃也可能内存泄漏。
内存泄漏原理:new int [10]开辟一整块空间并调用10次构造函数。delete[] 释放一整块空间。但是只调用了一次析构函数。如果类内部申请了空间,那么这些空间就有可能被内存泄漏。
崩溃原理:释放空间时释放内存的一部分。(有点复杂,下面详细分析)

如上图,我们运行程序,确实崩溃了。转到反汇编并监视:size:

我们看到,实际上new调用operator new开辟空间的时候开辟了88个字节的空间而不是80个字节。这多出来的8个字节空间是用来存放“开了多少个对象的”。

如上图,一目了然,空间开辟后我们的ptr指针指向的是80个字节的开始,而不是整个被开辟空间的开始,这个就导致了delete的时候我们不能完全释放掉所有的空间。导致崩溃。
但是值得注意的是,内置类型不会崩溃:
int* str = new int[10];
delete str;
因为内置类型事实上我们就开辟了40个字节的空间,并没有额外的4字节空间来存放“开了多少个对象”,也就没有了从中间释放空间的情况。
实际上自定义类型有这个额外空间而内置类型没有的本质是因为delete[]str,这个空间存放的“开了多少个对象”的这个数值,是在编译的时候给[]用的。内置类型既然没有析构函数,也就不需要采用额外开辟空间来存放析构函数的调用个数。
3.6总结malloc/free 和 new/delete 的区别(面试常考)
共同点是:都是从堆上申请空间,并且需要用户手动释放。不同的地方是:
- malloc 和 free 是函数,new 和 delete 是操作符
- malloc 申请的空间不会初始化,new 可以初始化
- malloc 申请空间时,需要手动计算空间大小并传递,new 只需在其后跟上空间的类型即可,如果是多个对象,[] 中指定对象个数即可
- malloc 的返回值为 void*,在使用时必须强转,new 不需要,因为 new 后跟的是空间的类型
- malloc 申请空间失败时,返回的是 NULL,因此使用时必须判空,new 不需要,但是 new 需要捕获异常
- 申请自定义类型对象时,malloc/free 只会开辟空间,不会调用构造函数与析构函数,而 new 在申请空间后会调用构造函数完成对象的初始化,delete 在释放空间前会调用析构函数完成空间中资源的清理释放
3.7placement-new
定位 new (placement new) 是 C++ 中new操作符的一种特殊形式。它并不分配新的内存,而是允许你在已经分配好的内存中创建一个对象。
它的基本语法如下:
new (place_address) type
new (place_address) type(initializer_list)
- place_address: 这是一个指针,指向一块你预先已经分配好的、足够容纳
type类型对象的内存空间。 - type: 你想要在该内存中创建的对象的类型。
- initializer_list: 用于初始化新创建对象的参数列表。
3.8拆分使用new
说白了就是显式调用构造和析构函数初始化/释放。
int main(){
A* p1 = new A(1);
delete p1;
A* p2 = (A*)operator new(sizeof(A));
new(p2)A(1);
p2->~A();
operator delete(p2);
return 0;
}
以上两套是一回事,看到这里,读者一定会想,“这不是脱裤子放屁吗?”
没错,实际上,拆分使用new在99%的场景下是用不到的。在极少数的情况下,会有用,如“池化技术中的内存池”(这个以后会讲)。
剩下一些内存泄漏的规则,我们暂时不谈,因为没法讲,我放到了指针指针再说。
好的,本期内容就到这里,如果对你有帮助,还不要忘了点赞三联一波哦,我是此方,我们下期再见。
转载自CSDN-专业IT技术社区
原文链接:https://blog.csdn.net/Z2314246476/article/details/156543552



