本文是《5.C/C++内存管理》的复习整理,按"内存区域划分 → C 的动态内存方式 → C++ 的 new/delete → 底层原理 → 高级用法 → 对比总结"的顺序梳理,代码统一放在
namespace Jianyi下方便测试。文末附高频面试问法,方便查漏补缺。
一、C/C++ 内存分布
1.1 一段代码引出的问题
先看一段很经典的"考察内存分布"的代码:
cpp
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 |
C(数据段) | 全局变量,编译期就分配好地址,属于静态区 |
staticGlobalVar |
C(数据段) | 加了 static,本质还是全局数据,只是链接属性变了(外部→内部),存储位置不变 |
staticVar |
C(数据段) | 函数内的静态局部变量,生命周期跟全局变量一样,存在数据段 |
localVar |
A(栈) | 非静态局部变量,函数调用时在栈上分配 |
num1 |
A(栈) | 数组本身是局部变量,整个数组都开在栈上 |
char2 |
A(栈) | char2 是数组变量本身,在栈上;但它的内容 "abcd" 是被拷贝进这块栈空间的 |
*char2 |
A(栈) | 解引用取的是数组第一个字符,这块内存在栈上 |
pChar3 |
A(栈) | pChar3 是一个指针变量,指针变量本身是局部变量,存在栈上 |
*pChar3 |
D(代码段/常量区) | pChar3 指向 的字符串常量 "abcd" 存放在常量区,pChar3 只是存了这块常量区的地址 |
ptr1 |
A(栈) | ptr1 这个指针变量本身在栈上 |
*ptr1 |
B(堆) | ptr1 指向的是 malloc 申请出来的空间,在堆上 |
易错对比点(面试常问) :
char2和pChar3看起来都存了"abcd",但本质完全不同------char2是一个数组 ,"abcd"是被拷贝进栈上这块数组空间的,内容可以修改 ;pChar3是一个指针 ,它指向的是位于常量区的字符串常量,*pChar3试图修改会导致未定义行为(很多平台上直接崩溃)。这是一个非常经典的考点。
1.2 内存区域划分图
C/C++ 程序运行时的内存,从高地址到低地址大致可以划分为(内核空间用户不可读写,不算在内):
bash
高地址
┌─────────────────────────┐
│ 内核空间(用户不可读写)│
├─────────────────────────┤
│ 栈(向下增长 ↓) │ 非静态局部变量 / 函数参数 / 返回值
├─────────────────────────┤
│ │
├─────────────────────────┤
│ 内存映射段 │ 文件映射、动态库、匿名映射
├─────────────────────────┤
│ │
├─────────────────────────┤
│ 堆(向上增长 ↑) │ malloc/new 动态申请的空间
├─────────────────────────┤
│ 数据段 │ 全局数据、静态数据
├─────────────────────────┤
│ 代码段 │ 可执行代码 / 只读常量
└─────────────────────────┘
低地址
要点梳理:
- **栈(stack)**又叫堆栈,存放非静态局部变量、函数参数、返回值等,栈是向下增长的(地址越用越小)。
- 内存映射段是高效的 I/O 映射方式,用来装载共享的动态库;用户也可以用系统接口创建共享内存实现进程间通信(Linux 课程会详细展开,这里先了解概念)。
- **堆(heap)**用于程序运行时动态内存分配,堆是向上增长的(地址越用越大)------注意和栈的增长方向刚好相反,这也是经典面试题"栈和堆的区别"里常考的一点。
- 数据段 :存储全局数据和静态数据(包括
static修饰的全局/局部变量)。 - 代码段 :存放可执行的代码,以及只读常量(比如字符串字面量
"abcd")。
二、C 语言中的动态内存管理方式
C 语言提供了 4 个动态内存管理函数:malloc、calloc、realloc、free。
cpp
namespace Jianyi
{
void Test()
{
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, sizeof(int) * 10);
// 这里需要再手动 free(p2) 吗?
// 不需要!realloc 内部已经处理好了旧内存:
// 如果原地扩容成功,p2 和 p3 其实是同一块地址;
// 如果原地放不下,realloc 会在别处开辟新空间、把旧数据拷过去,
// 并自动释放掉 p2 原来的那块空间。
// 所以 p2 这个指针在 realloc 调用之后就已经失效了,
// 不能再对 p2 单独调用 free,只需要 free(p3) 即可。
free(p3);
}
}
【面试题 1】malloc / calloc / realloc 的区别?
| 函数 | 作用 |
|---|---|
malloc(size) |
申请一块 size 字节的空间,不做初始化,里面是随机的脏数据 |
calloc(n, size) |
申请 n × size 字节的空间,并且把这块空间清零(初始化为 0) |
realloc(ptr, newSize) |
调整 ptr 指向空间的大小到 newSize:原地能扩容就原地扩;原地放不下就在别处重新开辟、把旧数据拷贝过去、并释放旧空间,返回新地址 |
【面试题 2】malloc 的实现原理?
这是一个比较深的问题,简单来说:malloc 底层通常基于操作系统提供的 brk/sbrk(扩展堆顶)或 mmap(大块内存走内存映射)向操作系统申请一大块内存,再通过空闲链表(free list)+ 内存池 的方式对这块内存进行切分管理,尽量减少内存碎片、提高分配效率。感兴趣可以深入读一下 glibc 里 ptmalloc 的实现(glibc中malloc实现原理)。这个问题面试时不一定要求手写实现,但要能说出"分级管理、空闲链表、系统调用申请大块内存"这几个关键词。
三、C++ 内存管理方式:new 和 delete
C 语言那一套内存管理方式在 C++ 里依然能用,但有些场景就无能为力了(比如自定义类型对象 申请空间时,光开辟内存是不够的,还需要初始化 )。因此 C++ 又提出了自己的方式:通过 new 和 delete 操作符进行动态内存管理。
3.1 new/delete 操作内置类型
cpp
namespace Jianyi
{
void Test()
{
// 动态申请一个 int 类型的空间
int* ptr4 = new int;
// 动态申请一个 int 类型的空间并初始化为 10
int* ptr5 = new int(10);
// 动态申请 3 个 int 类型的空间(连续空间)
int* ptr6 = new int[3];
delete ptr4;
delete ptr5;
delete[] ptr6; // 注意:数组要配套用 delete[]
}
}
注意(重点) :申请和释放单个元素 的空间,用
new和delete;申请和释放连续空间 (数组),用new[]和delete[];两者必须匹配起来使用 ,new[]配delete(或反过来)是未定义行为,很多编译器不会报错但实际上是隐患,面试中经常被追问"如果不匹配会发生什么"。
3.2 new/delete 操作自定义类型
cpp
namespace Jianyi
{
class A
{
public:
A(int a = 0) : _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
void Test()
{
// new/delete 和 malloc/free 最大的区别:
// new/delete 对于【自定义类型】除了开空间,还会调用构造函数和析构函数
A* p1 = (A*)malloc(sizeof(A)); // 只开空间,不会调用构造函数
A* p2 = new A(1); // 开空间 + 调用构造函数
free(p1); // 只释放空间,不会调用析构函数
delete p2; // 调用析构函数 + 释放空间
// 内置类型上,new/malloc 几乎是一样的
int* p3 = (int*)malloc(sizeof(int)); // C 方式
int* p4 = new int; // C++ 方式
free(p3);
delete p4;
// 数组同理,要配套用 new[]/delete[]
A* p5 = (A*)malloc(sizeof(A) * 10);
A* p6 = new A[10];
free(p5);
delete[] p6;
}
}
注意(核心结论) :在申请自定义类型 的空间时,
new会调用构造函数,delete会调用析构函数,而malloc与free不会 。这是new/delete相比malloc/free最本质的进步------它把"开空间"和"初始化对象/清理资源"这两件事整合到了一起。
四、operator new 与 operator delete 函数(重点)
4.1 关系梳理
这里有一组非常容易被搞混的名字,一定要分清楚:
new/delete:是用户使用的操作符,程序员平时写代码用的就是它们。operator new/operator delete:是系统提供的全局函数 ,new操作符在底层会调用operator new这个全局函数来申请空间,delete操作符在底层会调用operator delete这个全局函数来释放空间。
也就是说,new 不是直接去问操作系统要内存的,而是"包了一层":new 内部调 operator new 申请空间 → 再在这块空间上执行构造函数。
4.2 operator new 的实现(简化版源码解读)
cpp
/*
operator new:该函数实际通过 malloc 来申请空间。
如果 malloc 申请空间成功,直接返回;
如果申请失败,尝试执行"空间不足应对措施",
如果用户设置了该应对措施,就继续重试申请,否则抛异常。
*/
void* operator new(size_t size)
{
void* p;
while ((p = malloc(size)) == 0)
{
if (/* 没有设置应对措施 */ true)
{
// 申请内存失败,抛出 bad_alloc 类型异常
static const std::bad_alloc nomem;
throw nomem;
}
// 否则继续循环重试
}
return p;
}
4.3 operator delete 的实现(简化版源码解读)
cpp
/*
operator delete:该函数最终是通过 free 来释放空间的
*/
void operator delete(void* pUserData)
{
if (pUserData == NULL)
return;
// ......内部最终会调用 free(pUserData) 完成释放
}
小结 :通过看这两个函数的(简化)实现可以确认:operator new 实际也是通过 malloc 来申请空间的 ------如果 malloc 成功就直接返回;如果失败,就执行用户提供的"空间不足应对措施",用户提供了就继续申请,没提供就抛异常。operator delete 最终是通过 free 来释放空间的 。所以从最底层看,new/delete 依然是建立在 malloc/free 之上的一层封装,只是多加了"构造/析构"和"异常机制"。
五、new 和 delete 的实现原理
5.1 内置类型
如果申请的是内置类型 的空间,new/malloc、delete/free 基本类似,不同的地方是:
new/delete申请和释放的是单个元素 的空间,new[]/delete[]申请的是连续空间;new在申请空间失败时会抛异常 ,而malloc会返回 NULL (这也是为什么用malloc必须判空,而new一般不需要判空,但需要考虑捕获异常)。
5.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[]/delete[]本质上是在new/delete的基础上,加了一层"批量处理 N 个对象"的逻辑,底层最终还是落到同一套operator new/operator delete上。这也解释了为什么new[]一定要配delete[]------只有delete[]才知道要执行 N 次 析构函数,普通的delete只会析构一个对象,后面 N-1 个对象的资源就泄漏了。
六、定位 new 表达式(placement-new)(了解)
6.1 概念
定位 new 表达式 是在已经分配好的原始内存空间中,调用构造函数初始化一个对象。
使用格式:
new (place_address) type
// 或者
new (place_address) type(initializer-list)
其中 place_address 必须是一个指针,initializer-list 是类型的初始化列表(构造函数参数)。
6.2 使用场景
定位 new 表达式在实际工程中一般是配合内存池使用 的。因为内存池分配出来的内存是"裸内存",没有经过初始化,如果要在这块裸内存上构造一个自定义类型的对象,就必须使用定位 new 表达式来显式调用构造函数进行初始化------这正是内存池、对象池这类底层组件的核心技术之一。
cpp
namespace Jianyi
{
class A
{
public:
A(int a = 0) : _a(a)
{
cout << "A():" << this << endl;
}
~A()
{
cout << "~A():" << this << endl;
}
private:
int _a;
};
int main()
{
// p1 现在指向的只是一段与 A 对象大小相同的裸内存,
// 还不能算是一个真正的 A 对象,因为构造函数没有执行
A* p1 = (A*)malloc(sizeof(A));
new(p1) A; // 定位 new:在 p1 这块空间上调用构造函数
// 注意:如果 A 的构造函数有参数,这里需要传参
p1->~A(); // 用完要显式调用析构函数(因为不是用 delete 释放的)
free(p1); // 最后用 free 释放这块裸内存
// 也可以配合 operator new 一起使用
A* p2 = (A*)operator new(sizeof(A));
new(p2) A(10);
p2->~A();
operator delete(p2);
return 0;
}
}
这段代码体现的核心思路是 :malloc/operator new 只负责"要一块裸内存",定位 new 只负责"在这块内存上把对象构造出来",两者是解耦的------这正是内存池能够复用内存块、又能正确构造不同对象的关键所在。
七、malloc/free 和 new/delete 的区别(高频总结题)
共同点 :都是从堆上 申请空间,并且都需要用户手动释放。
不同点:
malloc/free是函数 ,new/delete是操作符。malloc申请的空间不会初始化 ,new可以初始化 (如new int(10))。malloc申请空间时需要手动计算空间大小并传递 ,new只需在其后跟上空间的类型即可,如果是申请多个对象,用[]指定对象个数即可,编译器自动算大小。malloc的返回值是void*,使用时必须强转 成目标类型指针;new不需要,因为new后面跟的就是空间的类型,编译器直接返回对应类型的指针。malloc申请空间失败时返回NULL,因此使用时必须判空 ;new申请失败时不需要判空,但需要考虑捕获异常 (new失败会抛bad_alloc)。- 申请自定义类型 对象时,
malloc/free只会开辟/释放空间,不会 调用构造函数与析构函数;而new在申请空间后会调用构造函数 完成对象的初始化,delete在释放空间前会调用析构函数完成资源的清理释放------这是两者最核心、最常被考到的区别。
八、复习自测 / 面试高频问法
- C/C++ 程序运行时的内存从上到下大致分为哪几个区域?各自存什么?
- 栈和堆的增长方向分别是怎样的?
- 局部字符数组
char2[]和字符指针const char* pChar3都存了同样的字符串内容,两者的存储位置有什么本质区别?为什么*pChar3不能被修改? malloc、calloc、realloc三者分别做了什么?realloc扩容失败/成功时,原来的指针还有效吗?new/delete和malloc/free最本质的区别是什么?(提示:构造/析构)operator new和new是什么关系?operator new底层是靠什么申请内存的?new T[N]和delete[]的原理分别是什么?为什么new[]必须配delete[],不能用普通的delete?- 什么是定位 new 表达式(placement-new)?它一般用在什么场景?
- 用定位 new 构造出来的对象,应该怎么正确地销毁和释放内存?(提示:不能直接
delete) - 总结一下
malloc/free和new/delete的所有区别。

