前言:C++相比于java、python来说并没有自动垃圾回收机制(GC),需要我们手动管理内存,C++的内存管理和我们C语言那里保存一致即可,甚至有了new和detele后管理内存比C语言还要方便而且更好支持自定义类型,关于C语言的内存管理在我之前的文章:【C语言】动态内存管理详细解析
1.C/C++的内存分布
在经典的32位/64位操作系统中,C/C++程序的内存通常被划分为五个主要区域。为了方便理解,我们通常按照内存地址从低到高或从高到低来排列。以下是标准的内存布局分布图:

1.栈区
栈区一般用来存放函数的局部变量、函数参数、返回值以及函数调用的上下文(比如返回地址),这个区域并不用我们主动的管理内存,只要是编译器替我们完成,当我们进入一个函数时,变量被压入栈中;当函数返回时,这些变量被弹出销毁。栈区有以下的几个特点:
- 地址增长方向从高地址向低地址生长(向下生长)与堆区正好相对
- 生命周期随着作用域(如函数、循环块)的结束而自动销毁
- 大小限制: 空间非常有限(通常默认几MB,比如Linux默认8MB,Windows默认1MB)。如果声明了过大的局部变量或递归过深,就会导致栈溢出,相比之下堆区一般会比栈区大很多
- 在性能上因为仅仅是移动栈顶指针(寄存器),所以分配速度比较快
2.堆区
堆区通常存放我们通过代码动态分配的内存(比如new、malloc、calloc)申请的内存空间,在C语言中我们用malloc, calloc, realloc 分配,使用 free 释放,而在C++中使用 new 分配,使用 delete 释放。关于堆区有下面几个特点:
- 地址增长方向从低地址向高地址生长(向上生长)
- 生命周期完全由我们控制。如果分配了不释放,就会造成内存泄漏
- 空间很大,几乎受限于操作系统的虚拟内存大小,尤其是64位下达到了恐怖的数字(2^64)
- 性能相比于栈区来说因为分配和释放需要调用操作系统API,容易产生内存碎片,速度相对较慢
3. 全局/静态存储区
这个区域用来存放全局变量和静态变量,它在程序的整个生命周期内都存在。它在底层又被细分为两块:
- 已初始化数据段 (.data):存放已经显式初始化且非零的全局变量和静态变量
- 未初始化数据段 (.bss): 存放未初始化或初始化为零的全局变量和静态变量
- 操作系统在加载程序时,会自动将 .bss 段的内存全部清零,因此未初始化的全局/静态变量默认值都是 0
4 只读数据区
存放程序中不可修改的常量数据。比如字符串字面量(如 “Hello World”)和被 const 修饰且在编译期就能确定的全局变量。同样是严格只读的。如果用指针强行指向这里并试图修改,程序会直接崩溃。
5 代码段
存放程序编译后的机器指令(也就是我们的代码编译出来的二进制文件)和函数体。这个区域有以下一个特点:
- 只读: 操作系统为了防止程序在运行中意外修改自己的指令,将这块内存设置为只读。如果尝试修改会触发异常
- 共享 : 如果同一个程序运行了多个实例(比如开了两个同样的软件),它们在物理内存中会共享同一份代码区
下面我们来看一个道经典的例题,先来来看下面的代码:
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) * 4);
int* ptr2 = (int*)calloc(4, sizeof(int));
int* ptr3 = (int*)realloc(ptr2, sizeof(int) * 4);
free(ptr1);
free(ptr3);
}
提问:
选项: A.栈 B.堆 C.数据段(静态区) D.代码段(常量区)
globalVar在哪里?____
staticGlobalVar在哪里?____
staticVar在哪里?____
localVar在哪里?____
num1 在哪里?____
char2在哪里?____
*char2在哪里?___
pChar3在哪里?____
*pChar3在哪里?____
ptr1在哪里?____
*ptr1在哪里?____
答案解析
- C:全局变量,生命周期贯穿整个程序,存放在全局/静态存储区
- C:静态全局变量,虽然它的作用域被限制在当前源文件内,但它的物理存储位置和全局变量一样,都在数据段
- C:静态局部变量。虽然它写在函数内部,但有 static 修饰,这意味着它只会被初始化一次,且函数结束后不会被销毁。它同样被挪到了数据段
- A:普通局部变量,没有 static 修饰,进入函数时自动在栈上分配,函数退出时自动销毁
- A:num1 是一个局部数组的名字。在 C/C++ 中,局部数组的所有元素(包括 1, 2, 3, 4 以及后面自动补的 0)都是在栈上连续开辟的空间
- A:char2 是一个局部数组名。char char2[] = “abcd”; 的底层原理是:编译器在常量区存放了 “abcd”,当程序运行到这一行时,在栈上开辟了 5 个字节的空间,并把常量区的 “abcd\0” 拷贝到了栈上的这块空间里。所以 char2 代表的是栈上的这块空间
- A:*char2 代表数组的第一个元素,即字符 ‘a’。既然整个数组都在栈上,它的元素自然也都在栈上
- A:pChar3 是一个指针变量。只要是函数内部定义的普通局部变量,不管它是什么类型(指针、整型、结构体),变量本身一定在栈上
- D:解引用 *pChar3 代表它所指向的内容。pChar3 指向的是 “abcd” 这个字符串字面量。字符串字面量是不可修改的,存放在只读数据区/常量区
- A:与第 8 题同理
- B:解引用 *ptr1 代表它指向的动态内存空间。因为这块空间是通过 malloc 申请出来的,malloc/calloc/realloc/new 申请的内存都在堆区
2.C++的内存管理方式
C++里依旧可以用C语言的malloc和free那套,但是C++提供了new和detele这两个关键字来管理内存,虽然底层依旧是C语言那套,但是在套了一层马甲后功能更加的强大,下面来介绍这两个关键字的用法
2.1操作内置类型:
对于 int, double 等基础类型,可以直接分配并选择性地初始化:
int main()
{
// 1. 仅分配内存,不初始化(是一个随机值)
int* p1 = new int;
// 2. 分配内存,并初始化为 0
int* p2 = new int();
// 3. 分配内存,并初始化为指定值 (例如 10)
int* p3 = new int(10);
delete p1;
delete p2;
delete p3;
return 0;
}
2.2操作自定义类型
如果用C语言的malloc申请空间的话对于自定义类型是不会对申请的类进行初始化的,但是new会自动调用类的构造函数,再这个类实例化时自动的初始化这个对象。同理,用delete会自动的调用这个类的析构函数,然后才是释放这个对象的空间:
class A
{
public:
A(int a = 0)
:_a(a)
{
std::cout << "A()" << std::endl;
}
A(const A& aa)
{
std::cout << "A(const A& aa)" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
private:
int _a = 1;
int _b = 1;
};
int main()
{
//会自动的调用构造函数进行初始化
A* p1 = new A(1);
//先调用析构函数,然后再用free释放(底层)
delete p1;
return 0;
}
2.3 数组用法
无论对于内置类型还是自定义类型,我们都可以使用new来创建数组:
int main()
{
//申请大小为3个整形大小的整形数组
int* arr1 = new int[3];
//申请大小为3个整形大小的整形数组,并都初始化为0
int* arr2 = new int[3]();
//申请大小为10个整形大小的整形数组,并将前三个元素初始化为1、2、3
int* arr3 = new int[10] {1, 2, 3};
return 0;
}
调试观察:

对于内置类型也是一样的:
int main()
{
A* p1 = new A[10];
//注意这里要使用delete[]释放,否则会产生未定义行为
delete[] p1;
return 0;
}
3.new和delete的底层原理简单介绍
3.1 operator new与operator delete函数
new和delete是我们进行动态内存申请和释放的操作符,而operator new与operator delete函数是系统提供的全局函数,new在底层调用的正是operator new全局函数来申请空间,而delete正是调用的operator delete函数来释放空间
我们可以来看看operator new和operator delete的底层代码:
//operator new 实现
void *__CRTDECL operator new(size_t size) _THROW1(_STD bad_alloc)
{
void *p;
while ((p = malloc(size)) == 0)
{
if (_callnewh(size) == 0)
{
static const std::bad_alloc nomem;
_RAISE(nomem);
}
}
return (p);
}
//operator delete 实现
void operator delete(void *pUserData)
{
_CrtMemBlockHeader * pHead;
RTCCALLBACK(_RTC_Free_hook, (pUserData, 0));
if (pUserData == NULL)
return;
_mlock(_HEAP_LOCK);
__TRY
pHead = pHdr(pUserData);
_ASSERTE(_BLOCK_TYPE_IS_VALID(pHead->nBlockUse));
_free_dbg( pUserData, pHead->nBlockUse );
__FINALLY
_munlock(_HEAP_LOCK);
__END_TRY_FINALLY
return;
}
//. 底层 free 的宏定义
#define free(p) free_dbg(p, _NORMAL_BLOCK)
可以看到实际上new、delete其实就是malloc、free套了层马甲,相当于是这两个的升级版。底层其实还是malloc和free,我们可以看看这个关系图:
【 new 操作符 (关键字) 】 —— 程序员在代码里写下的字眼
│
├── 步骤 1:调用 【 operator new 函数 】 (获取物理内存)
│ │
│ └── 底层循环调用 【 malloc() 】 (向操作系统 C 库要内存)
│
└── 步骤 2:调用 【 类的构造函数 】 (在要来的内存上建房子)
【 delete 操作符 (关键字) 】
│
├── 步骤 1:调用 【 类的析构函数 】 (拆除房子,清理内部资源)
│
└── 步骤 2:调用 【 operator delete 函数 】 (归还物理内存)
│
└── 底层调用 【 free() 】 (把内存还给 C 库和操作系统)
可以看到虽然底层还是malloc、free但是C++的new和delete明显功能更加强大而且更好的支持了内置类型
在C语言阶段,当我们申请内存失败了我们会判断时候返回空指针,但是在C++中,如果资源申请失败了并不会返回一个空指针,而是会抛异常,关于抛异常因为涉及到继承、多态的知识所以我想放到后面再讲解,反正我们平常写一些小练习小程序的基本是不会失败的,但是到正经项目中肯定是要处理这种情况的。美国有个火箭就是一个未被处理的异常导致陨落了,感兴趣可以看看这个视频为什么战斗机禁用 90% 的 C++ 功能
3.2 定位new表达式
定位new表达式是在已分配的原始内存空间中调用构造函数初始化一个对象,这个先了解一下即可,说实话我感觉挺偏的:
int main()
{
A* p1 = new A(1);
//申请空间
A* p2 = (A*)operator new(sizeof(A));
//定位new,调用构造函数
new(p2)A(2);
//析构
p2->~A();
operator delete(p2);
delete p1;
return 0;
}
上面的代码p1、p2在实例化和析构上是一样的,是不是有一种脱裤子放屁的感觉,但是存在即合理听过定位new在内存池上会有应用,这里我就不展开了因为我也不懂
3.3 new和delete总结
1.new的原理:
- 调用 operator new 函数,向操作系统申请刚好容纳该对象的内存空间
- 在这块刚申请到的内存空间上,执行该类的构造函数,完成对象内部数据的初始化
2.delete的原理:
- 准备释放的空间上,先执行该类的析构函数,清理对象内部占用的其他资源(比如释放内部指针、关闭文件等)
- 调用 operator delete 函数,把对象本身占用的这块内存还给操作系统
3.new T[N]的原理:
- 调用 operator new[] 函数(实际上是调用 operator new),一口气申请足以容纳 N 个对象的总内存
- 在这块连续的内存上,循环执行 N 次构造函数
4.delete[] 的原理:
- 循环执行 N 次析构函数,依次完成这 N 个对象内部资源的清理
- 调用 operator delete[] 函数(实际是调用 operator delete),将整块连续空间一次性释放
3.4new[]与delete错误搭配问题(图一乐):
我们先来看下面的代码,有两个类:
class A//有构造函数
{
public:
A()
{
std::cout << "A()" << std::endl;
}
A(const A& aa)
{
std::cout << "A(const A& aa)" << std::endl;
}
~A()
{
std::cout << "~A()" << std::endl;
}
private:
int _a = 1;
int _b = 1;
};
class B//无构造函数
{
private:
int _a = 1;
int _b = 1;
};
可以运行报错的程序:
int main()
{
A* p1 = new A[3];
delete p1;
return 0;
}
可以正常运行的程序:
int main()
{
B* p2 = new B[3];
delete p2;
return 0;
}
那么问题来了,同样是错误的搭配,为什么A类报错了,但是B类却正常运行呢?当我错误搭配使用时会产生未定义的行为,因为类 A 提供了自定义析构函数,而类 B 没有。这导致编译器在底层为它们分配数组内存时,采用了不同的内存布局
&mesp;&mesp;类A有一个自定义的析构函数 ~A()。当编译器看到 new A[3] 时,它知道在释放这块内存时,必须调用 3 次析构函数,为了在 delete[] p1 时知道到底要调用多少次析构函数,编译器会在实际分配的内存块头部偷偷多分配一点空间(通常是 4 或 8 个字节),用来记录数组的元素个数。这个隐藏的记录通常被称为Cookie
当我们调用 delete p1时编译器以为 p1 指向的是一个单个对象,于是它只对第一个元素调用了一次析构函数 ~A()接着,它试图把 p1 指向的地址直接交给底层的内存释放函数(如 C 语言中的 free())去释放,底层释放函数要求传入的地址必须是当初分配时的原始起始地址。但由于 Cookie 的存在,p1 实际上比原始地址偏移了几个字节。这时候把一个错误的、偏移过的指针交给了内存管理器,直接导致堆损坏:

那为什么B可以成功运行呢?类 B 没有自定义析构函数(且它的成员变量 _a 和 _b 都是基本类型),编译器认为 B 是一个平凡析构类型,当编译器看到 new B[3] 时,它知道释放这块内存时不需要执行任何额外的析构代码。既然不需要调用析构函数,编译器为了优化内存和性能,干脆就不生成那个记录数组大小的 Cookie了
当我们调用 delete p2 时编译器以为 p2 是单个对象,不调用析构函数,它把 p2 直接交给底层内存释放函数,因为没有 Cookie,p2 指向的地址恰好就是底层内存分配的原始起始地址。内存管理器一看,地址是对的,就顺利把这一整块(包含 3 个 B 对象的空间)物理内存给释放了。因此没有报错:

但我们正常正确的搭配使用就可以了,何必自找麻烦
4.malloc/free和new/delete的区别总结
我们可以把上面的几点总结成一个表格:
| 维度 | malloc / free (C 风格) | new / delete (C++ 风格) |
|---|---|---|
| 1. 语法属性 | 是 标准库函数 | 是 C++ 运算符 / 关键字 |
| 2. 尺寸与类型 | 必须手动计算并传递字节大小(如 sizeof(int));返回 void*,使用时必须强转类型。 | 后面直接跟类型/对象个数,自动计算大小(如 new int[10]);返回具体类型指针,无需强转。 |
| 3. 初始化 | 申请的空间不会初始化,里面全是垃圾随机值。 | 可以通过括号/大括号进行合法的初始化。 |
| 4. 失败处理机制 | 申请失败时返回 NULL,因此代码里必须判空。 | 申请失败时抛出 bad_alloc 异常,无需判空,但需要捕获异常。 |
| 5. 对象生命周期 | 只会开辟空间,绝不会调用自定义类型的构造函数与析构函数。 | 申请空间后自动调用构造函数; 释放空间前自动调用析构函数。 |
完
2093

被折叠的 条评论
为什么被折叠?



