动态内存与内存分区
定义与作用
C++ 程序的内存分为多个区域,理解各区域的分配时机、生命周期和管理方式,是编写安全高效 C++ 程序的根基:
- 栈(Stack):自动变量,编译器管理,速度快
- 堆(Heap):动态分配,程序员通过
new/delete管理 - 静态区(Data):全局变量、静态变量,程序启动分配、结束释放
- 常量区(Text/ROData):字面量、constexpr 数据
int global = 1; // 静态区
static int s = 2; // 静态区
void f() {
int local = 3; // 栈
int* p = new int(4); // p 在栈,*p 在堆
delete p;
}
核心原理
内存分区模型
new/delete 的工作流程
完整示例
示例一:演示四类内存区域
场景说明:通过一个综合程序追踪各内存区域对象的生命周期。
#include <iostream>
#include <string>
#include <memory>
// 静态区:全局变量
int global_counter = 0;
// 静态区:静态成员
class Demo {
public:
static int class_var; // 静态数据成员在静态区
static constexpr int const_var = 100; // 常量区
Demo(int id) : id_(id) {
++global_counter;
std::cout << " [堆/栈] 构造对象 #" << id_
<< " (global_counter=" << global_counter << ")" << std::endl;
}
~Demo() {
std::cout << " [析构] 对象 #" << id_
<< " (global_counter=" << global_counter << ")" << std::endl;
--global_counter;
}
void print() const {
std::cout << " Demo #" << id_ << std::endl;
}
private:
int id_;
};
int Demo::class_var = 42; // 静态区定义
// 常量区:字符串字面量
const char* greet = "Hello, C++ Memory Model!"; // greet 在静态区,"..." 在常量区
// 静态区:函数内静态变量
void count_calls() {
static int call_count = 0; // 静态区,只在首次调用初始化
++call_count;
std::cout << " count_calls 被调用第 " << call_count << " 次" << std::endl;
}
int main() {
std::cout << "===== 内存分区演示 =====" << std::endl;
std::cout << "\n--- 静态区 & 常量区 ---" << std::endl;
std::cout << "global_counter: " << global_counter
<< " (地址: " << &global_counter << ")" << std::endl;
std::cout << "Demo::class_var: " << Demo::class_var
<< " (地址: " << &Demo::class_var << ")" << std::endl;
std::cout << "Demo::const_var: " << Demo::const_var << std::endl;
std::cout << "greet: " << greet
<< " (字符串在常量区)" << std::endl;
count_calls();
count_calls();
std::cout << "\n--- 栈 Stack ---" << std::endl;
{
int stack_int = 10;
double stack_double = 3.14;
Demo stack_obj(1); // 栈上构造
std::cout << " stack_int 地址: " << &stack_int << std::endl;
std::cout << " stack_double 地址: " << &stack_double << std::endl;
std::cout << " stack_obj 地址: " << &stack_obj << std::endl;
} // 离开作用域 → 自动析构
std::cout << " [离开作用域后] global_counter=" << global_counter << std::endl;
std::cout << "\n--- 堆 Heap ---" << std::endl;
Demo* heap_obj = new Demo(2); // 堆上构造
std::cout << " heap_obj 指针在栈 (地址: " << &heap_obj << ")" << std::endl;
std::cout << " *heap_obj 对象在堆 (地址: " << heap_obj << ")" << std::endl;
// 堆对象必须手动释放
delete heap_obj;
std::cout << " [delete 后] global_counter=" << global_counter << std::endl;
// 堆数组
std::cout << "\n--- 堆数组 ---" << std::endl;
Demo* arr = new Demo[3]{3, 4, 5}; // 连续堆空间,3 个对象
std::cout << "arr[0] 地址: " << &arr[0] << std::endl;
std::cout << "arr[1] 地址: " << &arr[1] << std::endl;
std::cout << "arr[2] 地址: " << &arr[2] << std::endl;
delete[] arr; // 必须使用 delete[]
std::cout << " [delete[] 后] global_counter=" << global_counter << std::endl;
// 推荐:用智能指针自动管理堆对象
std::cout << "\n--- unique_ptr(堆但自动释放)---" << std::endl;
{
auto smart = std::make_unique<Demo>(6);
smart->print();
// 离开作用域自动 delete
}
std::cout << " [离开作用域后] global_counter=" << global_counter << std::endl;
return 0;
}
预期输出:
===== 内存分区演示 =====
--- 静态区 & 常量区 ---
global_counter: 0 (地址: 0x...)
Demo::class_var: 42 (地址: 0x...)
Demo::const_var: 100
greet: Hello, C++ Memory Model! (字符串在常量区)
count_calls 被调用第 1 次
count_calls 被调用第 2 次
--- 栈 Stack ---
[堆/栈] 构造对象 #1 (global_counter=1)
stack_int 地址: 0x...
stack_double 地址: 0x...
stack_obj 地址: 0x...
[析构] 对象 #1 (global_counter=1)
[离开作用域后] global_counter=0
--- 堆 Heap ---
[堆/栈] 构造对象 #2 (global_counter=1)
heap_obj 指针在栈 (地址: 0x...)
*heap_obj 对象在堆 (地址: 0x...)
[析构] 对象 #2 (global_counter=1)
[delete 后] global_counter=0
--- 堆数组 ---
[堆/栈] 构造对象 #3 (global_counter=1)
[堆/栈] 构造对象 #4 (global_counter=2)
[堆/栈] 构造对象 #5 (global_counter=3)
arr[0] 地址: 0x...
arr[1] 地址: 0x...
arr[2] 地址: 0x...
[析构] 对象 #5 (global_counter=3)
[析构] 对象 #4 (global_counter=2)
[析构] 对象 #3 (global_counter=1)
[delete[] 后] global_counter=0
--- unique_ptr(堆但自动释放)---
[堆/栈] 构造对象 #6 (global_counter=1)
Demo #6
[析构] 对象 #6 (global_counter=1)
[离开作用域后] global_counter=0
逐段分析:
- 栈地址向低地址方向增长(取决于实现),堆向高地址增长
static局部变量在静态区,只初始化一次——call_count跨调用保持值constexpr和字符串字面量在常量区(只读)new分配在堆,返回栈上的指针;delete释放堆内存new T[n]必须配对delete[]——否则未定义行为- 智能指针让堆对象自动释放——防止内存泄漏
示例二:内存泄漏检测与对比
#include <iostream>
#include <vector>
#include <memory>
// 模拟资源类
struct Resource {
int id;
static int alive_count;
explicit Resource(int id) : id(id) {
++alive_count;
std::cout << " 构造 Resource #" << id
<< " (存活: " << alive_count << ")" << std::endl;
}
~Resource() {
--alive_count;
std::cout << " 析构 Resource #" << id
<< " (存活: " << alive_count << ")" << std::endl;
}
};
int Resource::alive_count = 0;
// ====== 版本 A:裸指针——容易泄漏 ======
void raw_pointer_version() {
std::cout << "\n--- 裸指针版 ---" << std::endl;
Resource* r1 = new Resource(1);
Resource* r2 = new Resource(2);
// 模拟异常
if (r1->id > 0) {
delete r1; // 手动释放 r1
throw std::runtime_error("异常发生!r2 未被释放!");
}
delete r1;
delete r2;
}
// ====== 版本 B:智能指针——自动安全 ======
void smart_pointer_version() {
std::cout << "\n--- 智能指针版 ---" << std::endl;
auto r1 = std::make_unique<Resource>(1);
auto r2 = std::make_unique<Resource>(2);
// 模拟异常
if (r1->id > 0) {
throw std::runtime_error("异常发生!但 r1 和 r2 都会自动释放");
}
}
int main() {
std::cout << "===== 内存泄漏对比 =====" << std::endl;
std::cout << "初始存活: " << Resource::alive_count << std::endl;
// 裸指针版——异常导致泄漏
try {
raw_pointer_version();
} catch (const std::exception& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
std::cout << "异常后存活: " << Resource::alive_count
<< " (如果 > 0 说明泄漏!)" << std::endl;
}
std::cout << "\n清理后存活: " << Resource::alive_count << std::endl;
// 智能指针版——异常仍安全
try {
smart_pointer_version();
} catch (const std::exception& e) {
std::cerr << "捕获异常: " << e.what() << std::endl;
std::cout << "异常后存活: " << Resource::alive_count
<< " (= 0 说明安全)" << std::endl;
}
return 0;
}
预期输出:
===== 内存泄漏对比 =====
初始存活: 0
--- 裸指针版 ---
构造 Resource #1 (存活: 1)
构造 Resource #2 (存活: 2)
析构 Resource #1 (存活: 1)
捕获异常: 异常发生!r2 未被释放!
异常后存活: 1 (如果 > 0 说明泄漏!)
清理后存活: 1
--- 智能指针版 ---
构造 Resource #1 (存活: 2)
构造 Resource #2 (存活: 3)
析构 Resource #2 (存活: 2)
析构 Resource #1 (存活: 1)
捕获异常: 异常发生!但 r1 和 r2 都会自动释放
异常后存活: 1 (= 0 说明安全)
注:智能指针版在异常发生后 Resource 仍为 1,因为 stack 展开时析构了所有 RAII 对象。之前裸指针版残留的 Resource #2 也在这个时间点存在。
易错场景与面试考点
易错场景
| 场景 | 错误表现 | 正确做法 |
|---|---|---|
new[] 配 delete | 未定义行为 | 严格 new→delete,new[]→delete[] |
| 异常中裸指针泄漏 | 析构函数不执行 | 用智能指针 |
| 返回栈对象地址 | 悬挂指针 | 返回值对象或智能指针 |
| 循环引用 | shared_ptr 内存泄漏 | 用 weak_ptr 打破循环 |
用 malloc 配 delete | 未定义行为 | 全部用 new/delete 或全部用 malloc/free |
常见面试问题
C++ 内存分区及各自特点?——栈(自动变量,编译器管理)、堆(new/delete 动态分配)、静态区(全局/静态变量,程序生命周期)、常量区(字面量/constexpr,只读)、代码区。
new 和 malloc 的区别?——new 是运算符,自动计算大小、调用构造函数、返回正确类型指针。malloc 是函数,需要手动计算大小、不调构造函数、返回 void*。delete 和 free 同理。
new[]为什么必须配对delete[]?——new[]在对象数组前存储了元素数量信息,delete[]需要读取这个值来确定调用多少次析构函数。用delete会导致释放错误或只析构第一个元素。什么是内存泄漏?如何检测?——分配的内存未释放,导致可用内存减少。检测工具:Valgrind(Linux)、AddressSanitizer、Visual Studio 诊断工具、CRT 调试堆。
栈溢出和堆溢出的区别?——栈溢出:递归过深或局部变量过大;堆溢出(OOM):new 失败抛 std::bad_alloc。栈大小通常 1~8MB,堆大小接近可用 RAM。
小结
- 四区模型:栈(自动)| 堆(new/delete)| 静态区(全局/静态)| 常量区(字面量)
new= 分配原始内存 + 调用构造函数;delete= 调用析构函数 + 释放原始内存new[]必须搭配delete[]——数组释放需知道元素数量- 智能指针是防止内存泄漏的根本手段——异常安全、自文档化
- 永远避免裸 new/delete——优先用
make_unique/make_shared