在学习完类与对象 之后,C++ 程序员往往会迎来另一个绕不开的话题------内存管理 。
那为什么要深入研究这一块?因为几乎所有 C++ 程序的问题,都能归结到内存的分配和释放:
- 全局变量为什么能"一直存在"?
- 局部变量为什么出了作用域就消失?
- 为什么**
malloc
** 分配的内存不能用**delete
** 释放?- 为什么数组
new[]
必须用**delete[]
**?
- 这些问题如果理解透彻,写代码时不仅能少踩坑,还能真正掌握对象生命周期与现代 C++ 的核心思想。本文将系统讲解 C++ 的内存分区、动态内存分配机制、new/delete 本质、placement new 等知识点,并结合代码和面试题,帮助读者全面吃透这一块内容。
一、C/C++ 内存分区
C/C++ 程序运行时,内存空间大体分为四个部分:
- 代码段(Text Segment)
存放程序的指令 和常量字符串 。常量字符串只读,例如
"abcd"
。
- 数据段(Data Segment)
存放全局变量、静态变量,进一步分为:
- 已初始化数据区:存放已赋初值的全局/静态变量。
- BSS 区:存放未显式初始化的全局/静态变量,系统自动清零。
- 堆(Heap)
动态分配 的内存区域,通过**
malloc/new
** 获取,通过**free/delete
** 释放。生命周期由程序员控制。
- 栈(Stack)
由编译器管理,存放函数参数 、返回地址 、局部变量。函数退出时,栈帧自动销毁。
扩展 1 :操作系统视角的进程内存布局
当一个 C++ 程序运行时,操作系统会为它分配一个独立的 虚拟地址空间 。
这个地址空间通常是 连续的逻辑地址 ,在底层通过页表映射到物理内存。
- 低地址区 :一般是代码段和数据段,内容比较固定。
- 中间区域 :堆 ,动态向高地址扩展。
- 高地址区 :栈 ,动态向低地址扩展。
- 最高区域 :内核空间(用户态进程不可访问)。
也就是说,堆和栈像两股力量:堆往上长,栈往下长 ,如果中间挤满了,就会触发**"堆栈冲突**",导致分配失败。
扩展 2 :常量区与数据段的区别
-
常量区 :
存放只读的常量数据 ,例如字符串字面量**
"hello"
**。这部分通常不可修改,试图修改会触发段错误(segmentation fault)。
-
数据段 :
存放全局变量、静态变量 。
即使在函数内部 声明
static int x = 10;
,它依然被放到数据段 ,而不是栈上。
来看样例:
cpp
int global = 100; // 数据段
const char* str = "hello"; // 指针在数据段,内容在常量区
这也解释了为什么全局变量和静态变量 在整个程序生命周期 都存在,而局部变量出了作用域就消失。
举例说明
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); // 指针在栈,内容在堆
free(ptr1);
}
面试高频考点:
char str[] = "abc";
和char* p = "abc";
有什么区别?前者在栈上分配数组,内容拷贝自常量区;后者在栈上分配一个指针,指向常量区,常量不可修改。
二、C 的动态内存:malloc / calloc / realloc / free
C 提供了四个常用的内存操作函数:
- malloc(size):分配指定大小的内存,但不初始化。
- calloc(n, size) :分配**
n * size
** 内存,并全部清零。- realloc(ptr, newsize):调整已有内存块大小,可能返回新地址。
- free(ptr):释放先前分配的内存。
示例:
cpp
int* p1 = (int*)malloc(4 * sizeof(int));
int* p2 = (int*)calloc(4, sizeof(int));
int* p3 = (int*)realloc(p2, 8 * sizeof(int));
free(p1);
free(p3);
注意:
malloc
不会清零,可能带有垃圾值;**calloc
**会初始化为 0。realloc
必须用新指针接收返回值,否则可能丢失旧内存导致泄漏。free(NULL)
安全无害,但重复释放会产生未定义行为。
三、C++ 的动态内存:new / delete
在 C++ 中,new/delete
是对**malloc/free
**的进一步封装。
cpp
class A {
public:
A(int a = 0) : _a(a) { cout << "A(int)" << endl; }
~A() { cout << "~A()" << endl; }
private:
int _a;
};
int main() {
A* p1 = (A*)malloc(sizeof(A)); // 只分配,不调用构造
A* p2 = new A(1); // 分配 + 调用构造
free(p1); // 只释放,不调用析构
delete p2; // 调用析构 + 释放
}
区别总结:
**
malloc/free
**不关心对象的构造和析构。
new/delete
自动调用构造/析构,异常安全性更强。
new
失败时抛出bad_alloc
异常 ,malloc
失败时返回NULL。
运行流程:malloc:分配内存 → 返回地址 → 程序员初始化。
new:分配内存 → 调用构造函数初始化 → 返回对象指针。
free:释放内存,不调用析构函数。
delete:先调用析构函数 → 再释放内存。
四、operator new 与 operator delete
实际上:
operator new(size_t size)
内部通常调用malloc
。- 如果分配失败,会调用**
new_handler
** ,仍失败则抛出bad_alloc
。operator delete(void* p)
最终会调用**free(p)
**。
这说明 new/delete
在本质上依赖于 malloc/free
,只是多了一层对象语义支持。
五、new[ ] 与 delete[ ]
new[]
会分配一块连续空间 ,并依次调用每个元素的构造函数;delete[]
会依次调用析构函数 ,然后再释放空间。
cpp
A* arr = new A[3]{ A(1), A(2), A(3) };
delete[] arr; // 正确
如果错误地用 delete
释放数组 ,而不是用**delete[]
** ,只会调用第一个元素的析构 ,导致内存泄漏。
运行流程:
- new[ ]:分配一大块连续空间 → 按元素个数依次调用构造函数。
- delete[ ]:依次调用每个元素的析构函数 → 最后释放整块内存。
六、placement new(定位 new)
placement new
允许在指定内存地址上构造对象。
cpp
void* buf = malloc(sizeof(A));
A* obj = new(buf) A(10); // 在 buf 内存上构造对象
obj->~A(); // 手动析构
free(buf); // 最终释放
- 程序员先用 malloc 或其他方式拿到一块**"原始内存**"。
- 在这块内存上调用placement new ,执行构造函数。
- 当不再需要时,手动调用对象的析构函数。
- 最后释放底层内存。
应用:
实现对象池,避免频繁的分配释放。
容器内部,如**
std::vector
** 在预分配的空间里构造对象。
七、常见错误
-
野指针:指针未初始化。
-
悬空指针:指向已释放内存。
-
重复释放 :多次
free
同一块内存。 -
混用接口 :
malloc
分配却用**delete
**释放,后果不可预测。
读者在编写 C++ 代码时可能会犯的一些常见错误 ↑
八、常见面试题
1. new
和 malloc
的区别?
malloc
只分配内存,不调用构造/析构,失败时返回 NULL。new
除了分配内存,还调用构造函数,失败时抛出**bad_alloc
**。- 类型安全:
new
不需要强制转换,malloc
需要。
2. 为什么 new[]
必须用 delete[]
?
因为数组中每个对象都要调用析构函数 。
delete
只析构第一个对象 ,delete[]
会依次调用所有对象的析构函数。
3. placement new
的应用场景?
- 内存池:避免频繁堆分配。
- 容器:在预分配空间上直接构造对象。
- 对象缓存:复用已分配的内存,减少开销。
4. realloc
失败时如何避免内存泄漏?
必须用新指针接收返回值:
cppint* tmp = realloc(p, newSize); if (tmp != NULL) { p = tmp; } else { // realloc 失败,原指针仍然有效 }
5. delete this
会发生什么?
合法的前提 :对象是通过**
new
** 创建的,且**delete this
** 是在成员函数中调用的。
运行流程:
- 析构函数被调用。
- 对象内存被释放。
- 此后访问该对象就是未定义行为。
如果对象不是在堆上创建 的(例如栈对象 ),
delete this
会导致程序崩溃。
九、总结
- 内存管理是 C++ 的基石。理解内存分区 、掌握
malloc/free
与new/delete
的区别 ,熟悉placement new
的应用场景,就能避免大多数常见问题。 - 更重要的是,这些知识让我们理解了对象的生命周期 ,也为智能指针、RAII 等现代 C++ 编程理念打下了坚实基础。