1. C++内存分区模型
C++程序运行时,内存通常被划分为以下几个区域:
| 代码区 | 编译后的二进制代码(函数体、只读常量) | 程序运行期 | 操作系统管理 |
| 全局/静态存储区 | 全局变量、静态变量、常量(const全局) | 程序运行期 | 编译时分配,自动释放 |
| 栈区 | 局部变量、函数参数、返回地址等 | 函数调用期间 | 编译器自动管理 |
| 堆区 | 动态分配的内存(new/malloc) | 由程序员控制 | 程序员手动分配和释放 |
| 常量区(在全局区中) | 字符串字面量、const修饰的全局常量 | 程序运行期 | 只读,不可修改 |
1.1 代码区
存放CPU执行的机器指令,通常为只读,防止程序意外修改。函数体的二进制代码就存放在这里。
代码区是共享和共读的。
代码区是共享的,共享的目的是当多进程运行同一个可执行文件时(例如多用户同时运行同一个程序,或同一个程序被多次启动),操作系统只需要在物理内存中加载一份代码,然后让多个进程的虚拟地址空间映射到同一份物理代码页上。
好处:
1.节省内存,避免加载相同代码浪费物理空间。
2.提高效率:减少磁盘I/O和内存占用。
注意:这种共享是只读的共享,代码是只读的,任何进程不能修改它,否则会破坏其他进程的执行
1.2 全局/静态存储区(包含常量区)
-
全局变量(在所有函数外定义)
-
static修饰的变量(无论是全局还是局部)
-
已初始化的全局变量和未初始化的全局变量分属两个相邻区域(.data和.bss段)
-
生命周期从程序开始到结束。
-
该区域的数据在程序结束后由操作系统释放。
-
该区域还包含常量区,字符串常量和其他常量。
示例:
int globalVar = 10; // .data 段
static int staticVar = 20; // .data 段
int uninitGlobal; // .bss 段,自动初始化为0
1.3 栈区
由编译器自动分配释放,存放函数的参数值、局部变量等。栈内存分配速度快,但空间有限(通常几MB)。函数调用时,参数和局部变量压栈,函数返回时出栈。
注意:不返回局部变量的地址,栈区开辟的数据由编译器自动释放
局部变量存放在栈区,栈区数据在函数执行完后自动释放
#include <iostream>
using namespace std;
void func()
{
int a = 10; //在栈区,执行后自动释放
return &a;//返回地址
}
int main()
{
cout << "1" << endl;
int* p = func();
cout << *p << endl;
cout << *p << endl;
system("pause");
return 0;
}
结果:乱码,报错

示例:
void func() {
int a = 5; // 栈上分配
char buf[100]; // 栈上数组
}
1.4 堆区
由程序员显式分配和释放(new/delete或malloc/free)。若未释放,程序结束时可能由操作系统回收,但长期运行的程序会内存泄漏。堆空间大,但分配和释放较慢,可能产生内存碎片。
栈区即使有数据地址,数据本身也被释放了,不能使用:而堆区的数据不会被自动释放,因此指针指向的数据可以被解引用
堆区的数据不能通过变量直接访问,只能通过指针间接访问
2.5 常量区
存放字符串字面量和const修饰的全局常量。该区域只读,修改会导致未定义行为(通常段错误)。
示例:
const char* p = "hello"; // "hello"存储在常量区,p指向它
char* q = "world"; // 危险:修改*q会崩溃,应使用const char*
2. new/delete操作符深入解析
new和delete是C++中用于动态内存管理的操作符,它们不仅分配内存,还会调用对象的构造函数和析构函数。
2.1 new的三种用法
(1) 分配单个对象
int* p = new int; // 未初始化
int* q = new int(10); // 初始化为10
MyClass* obj = new MyClass; // 调用默认构造函数
(2) 分配数组
int* arr = new int[100]; // 未初始化
MyClass* objs = new MyClass[10]; // 调用10次默认构造函数
(3) 定位new(placement new)
在已分配的内存上构造对象,不分配新内存。
#include <new>
char buffer[sizeof(MyClass)];
MyClass* p = new (buffer) MyClass; // 在buffer上构造对象
2.2 new的内部实现流程
对于new T(T为类型),编译器大致转换为以下步骤:
调用operator new(sizeof(T))分配原始内存(内部通常调用malloc)。
若分配成功,在获得的内存上调用T的构造函数。
返回指向该对象的指针。
对于new T[n]:
调用operator new[](sizeof(T) * n + 额外空间),额外空间通常用于存储数组元素个数(以便delete[]知道调用多少次析构函数)。
依次调用每个元素的构造函数。
返回指向第一个元素的指针。
operator new可以被重载,提供自定义的内存分配策略。
2.3 delete的内部实现流程
对于delete p:
调用p所指向对象的析构函数。
调用operator delete(p)释放内存(内部通常调用free)。
对于delete[] p:
根据存储的元素个数,逆序调用每个元素的析构函数。
调用operator delete[]释放内存。
2.4 new[]与delete[]必须配对使用
-
new[]分配的内存布局中可能在对象数组前有一个“数量”字段,若用delete释放,会导致:
-
只调用第一个元素的析构函数,其他元素未析构(资源泄漏)。
-
释放的指针不是原始分配地址,导致内存释放错误。
-
-
反之,new分配的对象用delete[]释放,也会导致未定义行为。
示例:
int* p = new int[10];
delete p; // 错误!应该用 delete[] p
MyClass* q = new MyClass;
delete[] q; // 错误!应该用 delete q
3. new/delete与malloc/free的区别
| 属性 | C++操作符 | C/C++标准库函数 |
| 是否需要头文件 | 不需要 | 需要<cstdlib>或<malloc.h> |
| 返回类型 | 类型严格匹配的指针 | void*,需强制转换 |
| 分配失败 | 抛出std::bad_alloc异常 | 返回NULL |
| 构造函数/析构函数 | 自动调用 | 不调用 |
| 内存大小计算 | 由编译器根据类型计算 | 需手动指定字节数 |
| 重载 | 可以重载operator new | 不可重载 |
| 数组支持 | new[]自动记录元素个数 | 需自行记录 |
示例:
// malloc/free
int* p = (int*)malloc(sizeof(int) * 10);
if (p == nullptr) { /* 处理失败 */ }
free(p);
// new/delete
int* q = new int[10];
// 若失败抛出异常,可用try-catch或nothrow版本
delete[] q;
4. 常见内存问题与最佳实践
4.1 内存泄漏
分配后忘记释放,尤其是异常路径。
解决:使用智能指针(std::unique_ptr、std::shared_ptr)、RAII、容器(std::vector)代替裸指针。
4.2 野指针
指针指向已释放的内存或非法地址。
解决:释放后置nullptr,避免重复释放;初始化指针为nullptr。
4.3 重复释放(double free)
同一地址释放两次,导致堆损坏。
解决:遵循谁分配谁释放原则,使用智能指针自动管理。
4.4 内存越界
访问数组边界之外。
解决:使用std::array、std::vector,或者手动检查边界。
4.5 混用new[]与delete
解决:严格配对使用,用std::vector管理动态数组。
4.6 最佳实践建议
-
优先使用STL容器和智能指针,减少手动内存管理。
-
若必须手动管理,遵循“在构造函数中分配,在析构函数中释放”的RAII原则。
-
为自定义类重载operator new/delete时,同时重载new[]/delete[]。
-
使用nothrow版本的new以避免异常,或者用try-catch处理bad_alloc。
#include <new>
int* p = new (std::nothrow) int[100];
if (p == nullptr) {
// 处理分配失败
}
5. 总结
本文详细介绍了C++的内存分区模型,包括代码区、全局/静态区、栈区、堆区和常量区,并深入分析了new/delete的工作机制。理解这些底层原理有助于:
-
定位和避免内存错误
-
优化程序性能(如减少堆分配,使用栈)
-
正确实现自定义内存管理
-
写出更安全、更高效的C++代码
网硕互联帮助中心




评论前必须登录!
注册