C++第五讲:内存管理
这一章是C/C++ 面试第一高频考点,也是理解后续 STL 容器、智能指针的基础。
一、C/C++ 程序内存分区(必考选择题)
1. 内存分区总览
程序运行时,内存会被划分为以下 5 个主要区域(从高地址到低地址):
| 区域 | 存储内容 | 特点 |
|---|---|---|
| 内核空间 | 操作系统内核代码和数据 | 用户代码不可访问 |
| 栈(Stack) | 非静态局部变量、函数参数、返回值 | 向下增长,自动分配释放,速度快,空间有限 |
| 内存映射段 | 动态库、共享内存、文件映射 | 用于进程间通信和动态库加载 |
| 堆(Heap) | 动态申请的内存(malloc/new) | 向上增长,手动分配释放,速度慢,空间大 |
| 数据段(静态区) | 全局变量、静态变量(static 修饰) | 程序运行期间一直存在,程序结束后系统释放 |
| 代码段(常量区) | 可执行代码、只读常量 | 只读,不可修改 |
2. 经典面试题解析
看下面这段代码,判断每个变量 / 值所在的内存区域:
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"; // char2在栈,"abcd"在代码段(常量)
const char* pChar3 = "abcd"; // pChar3在栈,"abcd"在代码段
int* ptr1 = (int*)malloc(4*sizeof(int)); // ptr1在栈,指向堆
free(ptr1);
}
答案速记
-
所有
static修饰的变量 + 全局变量 → 数据段 -
局部变量、数组、指针变量本身 → 栈
-
malloc/new申请的空间 → 堆 -
字符串常量(如
"abcd")、可执行代码 → 代码段
易错点
-
char char2[] = "abcd":数组char2在栈,会把代码段的"abcd"拷贝一份 到栈中,所以可以修改char2[0] = 'x' -
const char* pChar3 = "abcd":pChar3指向代码段的常量,所以不能修改*pChar3(会崩溃)
二、C 语言动态内存管理
C 语言通过 4 个函数管理堆内存:malloc、calloc、realloc、free,都在<stdlib.h>头文件中。
1. 四个核心函数详解
1.1 malloc:申请空间
cpp
void* malloc(size_t size);
-
作用:申请size 字节的堆空间
-
特点:不初始化,空间内容是随机值
-
返回值:成功返回空间首地址,失败返回
NULL -
示例:
cppint* p = (int*)malloc(4*sizeof(int)); // 申请4个int的空间 if (p == NULL) { // 必须判空! perror("malloc fail"); return; }
1.2 calloc:申请并初始化空间
cpp
void* calloc(size_t num, size_t size);
-
作用:申请num 个 size 字节 的空间,并将所有字节初始化为
0 -
示例:
cppint* p = (int*)calloc(4, sizeof(int)); // 4个int,初始值都是0
1.3 realloc:调整已申请空间的大小
cpp
void* realloc(void* ptr, size_t new_size);
-
作用:将
ptr指向的空间调整为new_size字节 -
两种扩容方式:
-
原地扩容:原空间后面有足够空间,直接扩展,返回原地址
-
异地扩容:原空间后面空间不足,申请新空间→拷贝数据→释放原空间→返回新地址
-
-
注意:不需要手动 free 原空间,realloc 成功会自动释放原空间
-
示例:
cppint* p = (int*)malloc(4*sizeof(int)); int* new_p = (int*)realloc(p, 10*sizeof(int)); // 扩容到10个int if (new_p != NULL) { p = new_p; // 扩容成功,更新指针 }
1.4 free:释放空间
cpp
void free(void* ptr);
-
作用:释放
ptr指向的堆空间 -
注意事项:
-
不能释放
NULL指针(free (NULL) 什么也不做,合法) -
不能重复释放同一块空间(会崩溃)
-
不能释放栈上的空间(只能释放堆空间)
-
释放后必须将指针置为
NULL,避免野指针
-
2. 面试高频题:malloc/calloc/realloc 的区别
-
初始化:malloc 不初始化,calloc 初始化为 0,realloc 不初始化新空间
-
参数:malloc 传总字节数,calloc 传个数和单个大小,realloc 传原指针和新总大小
-
扩容:只有 realloc 可以调整已申请空间的大小
三、C++ 动态内存管理:new/delete
C 语言的内存管理方式在 C++ 中可以继续使用,但对于自定义类型 不够方便(不会调用构造 / 析构函数),因此 C++ 引入了new和delete操作符。
1. 操作内置类型
和 malloc/free 基本类似,只是语法更简洁:
cpp
// 1. 申请单个int空间
int* p1 = new int;
// 2. 申请单个int并初始化为10
int* p2 = new int(10);
// 3. 申请3个int的数组
int* p3 = new int[3];
// 4. 申请3个int的数组并初始化(C++11)
int* p4 = new int[3]{1,2,3};
// 释放单个空间
delete p1;
delete p2;
// 释放数组空间(必须加[]!)
delete[] p3;
delete[] p4;
核心规则
-
申请单个元素 →
new+delete -
申请数组 →
new[]+delete[] -
必须匹配使用!否则会导致内存泄漏或程序崩溃
2. 操作自定义类型(核心区别)
new/delete 会自动调用构造函数和析构函数,malloc/free 不会,这是 C++ 和 C 动态内存管理的本质区别。
cpp
class A {
public:
A(int a = 0) : _a(a) {
cout << "A()构造:" << this << endl;
}
~A() {
cout << "~A()析构:" << this << endl;
}
private:
int _a;
};
int main() {
// malloc:只开空间,不调用构造函数
A* p1 = (A*)malloc(sizeof(A));
free(p1); // 只释放空间,不调用析构函数
cout << "-----------------" << endl;
// new:1. 开空间 2. 调用构造函数初始化
A* p2 = new A(10);
// delete:1. 调用析构函数清理资源 2. 释放空间
delete p2;
cout << "-----------------" << endl;
// 数组:调用10次构造函数
A* p3 = new A[10];
// 数组:调用10次析构函数
delete[] p3;
return 0;
}
四、new/delete 底层原理
1. operator new 与 operator delete
new和delete是用户使用的操作符,它们的底层会调用系统提供的两个全局函数:
-
operator new:负责申请空间,底层调用malloc -
operator delete:负责释放空间,底层调用free
1.1 operator new 的特点
-
申请失败时抛
bad_alloc异常 ,而不是返回NULL -
本质是对 malloc 的封装,增加了异常处理机制
1.2 operator delete 的特点
-
底层直接调用
free释放空间 -
处理空指针时安全(operator delete (NULL) 什么也不做)
2. new 的完整执行流程
2.1 单个对象 new 的流程
cpp
A* p = new A(10);
-
调用
operator new(sizeof(A))申请空间 -
在申请的空间上调用 A 的构造函数,传入参数 10
-
返回空间的首地址
2.2 数组 new [] 的流程
cpp
A* p = new A[10];
-
调用
operator new[](10*sizeof(A))申请空间 -
在申请的空间上依次调用 10 次 A 的默认构造函数
-
返回空间的首地址
3. delete 的完整执行流程
3.1 单个对象 delete 的流程
cpp
delete p;
-
在 p 指向的空间上调用 A 的析构函数清理资源
-
调用
operator delete(p)释放空间
3.2 数组 delete [] 的流程
cpp
delete[] p;
-
在 p 指向的空间上依次调用 10 次 A 的析构函数(从后往前)
-
调用
operator delete[](p)释放空间
五、定位 new(placement-new)
1. 作用
在已经分配好的原始内存空间上,手动调用构造函数初始化对象(不申请新空间)。
2. 语法格式
cpp
new(地址) 类型(初始化列表);
3. 使用场景
配合内存池使用:内存池提前申请好一大块空间,需要创建对象时,用定位 new 在已有的空间上初始化,避免频繁向系统申请内存,提高效率。
4. 代码示例
cpp
int main() {
// 1. 先申请一块原始空间(不初始化)
A* p = (A*)malloc(sizeof(A));
// 此时p指向的空间还不是一个A对象,因为构造函数没执行
// 2. 用定位new调用构造函数,初始化对象
new(p) A(10); // 等价于A::A(p,10);
// 3. 使用对象
// ...
// 4. 手动调用析构函数清理资源
p->~A();
// 5. 释放原始空间
free(p);
return 0;
}
六、malloc/free vs new/delete(面试必背)
| 对比项 | malloc/free | new/delete |
|---|---|---|
| 本质 | C 标准库函数 | C++ 操作符 |
| 初始化 | 不初始化 | 可以初始化(自定义类型调用构造) |
| 参数 | 手动计算总字节数 | 自动计算类型大小,数组指定个数 |
| 返回值 | void*,需要强转 |
直接返回对应类型指针 |
| 失败处理 | 返回NULL,必须判空 |
抛bad_alloc异常,需要捕获 |
| 自定义类型 | 只开 / 释放空间,不调用构造 / 析构 | 自动调用构造 / 析构函数 |
| 数组处理 | 手动计算数组大小 | new[]/delete[]自动处理 |
七、常见内存错误与避坑指南
-
不匹配使用 new/delete 和 new []/delete []
-
错误:
int* p = new int[10]; delete p; -
后果:内存泄漏或崩溃
-
-
重复释放同一块空间
-
错误:
free(p); free(p); -
后果:程序崩溃
-
-
释放后未置空指针,形成野指针
-
错误:
free(p); // p变成野指针 -
正确:
free(p); p = NULL;
-
-
使用已经释放的空间
-
错误:
free(p); *p = 10; -
后果:非法访问,程序崩溃
-
-
内存泄漏
-
原因:申请的空间没有释放,程序结束后系统才会回收
-
危害:长期运行的程序(如服务器)会逐渐耗尽内存
-
八、快速巩固练习题
-
写出下面代码的输出结果,并解释原因:
cppclass Test { public: Test() { cout << "Test()" << endl; } ~Test() { cout << "~Test()" << endl; } }; int main() { Test* p1 = (Test*)malloc(sizeof(Test)); free(p1); Test* p2 = new Test[3]; delete[] p2; return 0; } -
为什么 new [] 和 delete [] 必须匹配使用?