【C++ 从基础到项目实战】C++(四):动态内存管理——new/delete与堆栈原理

-






:22分钟 | 关键词:C++、new、delete、动态内存、堆与栈、static、内存泄漏、悬挂指针

引言

前几篇文章我们使用的变量大多在上自动分配、自动回收。但真实项目中,很多数据的大小在编译时无法确定——比如用户输入的行数、从文件读取的记录数。这时候就需要动态内存管理:自己向操作系统要内存,用完再自己还回去。这篇文章就来揭开 C++ 内存管理的神秘面纱。

一、内存分区:栈区 vs 堆区

把计算机内存想象成一个仓库,分成两个截然不同的区域:

`int globalVar = 10; // 全局区

void demo() {
int local = 50; // 栈区
int *heap = new int; // 堆区
*heap = 100;
delete heap;
}
`

| 特征 | 栈区 (Stack) | 堆区 (Heap) |
| 管理方式 | 编译器自动分配释放 | 程序员手动 new/delete |
| 分配速度 | 极快(移动栈指针) | 较慢(查找空闲块) |
| 大小限制 | 有限(通常几 MB) | 受系统内存限制(可达 GB) |
| 生命周期 | 离开作用域自动销毁 | 直到 delete 才释放 |
| 存储内容 | 局部变量、函数参数、返回地址 | 动态分配的数据 |

`
// 直观感受地址的差异
int main() {
int a = 10; // 栈
int *b = new int(20); // 堆

std::cout << "栈地址: " << &a << std::endl; // 高地址(靠近栈顶)
std::cout << "堆地址: " << b << std::endl; // 低地址

delete b;
}

`

二、new 和 delete:动态内存的租与还

#### 2.1 基本用法

`
// 分配单个对象
int *p = new int; // 不初始化
int *q = new int(10); // 初始化为 10
int *r = new int{20}; // C++11 统一初始化

  • p = 99;


delete p; // 释放单个对象
delete q;
delete r;
p = q = r = nullptr; // 释放后置空

`

`
// 分配数组
int *arr = new int[5]; // 未初始化
int *arr2 = new int[5]{1, 2, 3}; // 部分初始化:{1,2,3,0,0}

for (int i = 0; i < 5; ++i)
std::cout << arr2[i] << " "; // 1 2 3 0 0

delete[] arr; // ⚠️ 必须用 delete[] 释放数组
delete[] arr2;

`

#### 2.2 铁律:必须成对使用

| 分配 | 释放 | 错误配对后果 |
| new T | delete ptr | — |
| new T[n] | delete[] ptr | — |
| new T | delete[] ptr | 未定义行为 |
| new T[n] | delete ptr | 未定义行为 |

`
// ❌ 错误示例(切勿模仿)
int *arr = new int[5];
delete arr; // 大错特错!数组必须用 delete[]

`

#### 2.3 new 失败怎么办?

当系统内存不足时,new 会抛出 std::bad_alloc 异常:

`try {
int *huge = new int[1000000000]; // 可能失败
} catch (std::bad_alloc &e) {
std::cerr << "内存分配失败:" << e.what() << std::endl;
}
`
#### 2.4 C 语言对比

| C++ | C | 说明 |
| new | malloc() | C++ 会调用构造函数,C 不会 |
| delete | free() | C++ 会调用析构函数,C 不会 |
| new[] | calloc() | calloc 会将内存初始化为 0 |

三、内存泄漏:隐形杀手

内存泄漏就是借了内存不还——new 了但忘了 delete,导致那部分内存永远无法被再次使用:

`void leaker() {
int *p = new int[1000]; // 分配了 4000 字节
// ... 做一些操作 ...
// 忘记 delete[] p! ← 这就是泄漏
}

int main() {
for (int i = 0; i < 1000; ++i)
leaker(); // 每次调用泄漏 4KB,1000次 = 泄漏 4MB
}
`
#### 泄漏的危害

| 阶段 | 表现 |
| 短期 | 无明显影响 |
| 中期 | 系统变慢,可用内存减少 |
| 长期 | 内存耗尽→程序崩溃→系统其他程序也受影响 |
#### 防泄漏三原则

谁分配谁释放newdelete 放在同一责任域里
- RAII(资源获取即初始化):用智能指针(下篇文章讲)自动管理
- 释放后置空delete p; p = nullptr; 防止误用

四、悬挂指针与安全使用

#### 4.1 什么是悬挂指针?

指向已释放内存的指针就是悬挂指针——访问它 = 未定义行为:

`int *p = new int(42);
delete p; // p 所指内存已释放
// p 现在是悬挂指针!

  • p = 100; // 未定义行为!

`
#### 4.2 指针安全使用七条军规

`
// ✅ 1. 声明时初始化
int *ptr = nullptr;

// ✅ 2. 使用前检查
if (ptr != nullptr) {
*ptr = 10;
}

// ✅ 3. 确保 new/delete 配对

// ✅ 4. 不要重复释放
delete ptr; // 第一次释放 OK
// delete ptr; // ❌ 重复释放→崩溃

// ✅ 5. 释放后置空
ptr = nullptr;

// ✅ 6. 避免返回局部变量地址

// ✅ 7. 优先使用智能指针(下篇详细讲)

`

五、全局变量、局部变量与 static

变量声明的位置和 static 关键字决定了它的作用域生命周期

`#include

int globalVar = 100; // 全局变量:整个程序可见

void func() {
int local = 10; // 局部变量:函数内可见,离开即销毁
static int counter = 0; // 静态局部变量:只初始化一次,值在调用之间保持
counter++;

std::cout << "第 " << counter << " 次调用, local = "
<< local << std::endl;
local++;
}

int main() {
func(); // 第 1 次调用, local = 10
func(); // 第 2 次调用, local = 10(重新创建)
func(); // 第 3 次调用, local = 10
// counter 的值跨调用保持:1, 2, 3
}
`
#### 四种变量对比

| 变量类型 | 作用域 | 生命周期 | 初始化时机 |
| 全局变量 | 整个程序 | 程序启动→结束 | 程序启动时 |
| 局部变量 | 定义它的 {} 块 | 进入块→离开块 | 每次进入时 |
| 静态局部变量 | 定义它的函数 | 程序启动→结束 | 第一次执行到时 |
| 静态全局变量 | 当前源文件 | 程序启动→结束 | 程序启动时 |
#### static 关键字的三重身份

| 修饰目标 | 效果 |
| 局部变量 | 延长生命周期到程序结束,只在第一次执行时初始化 |
| 全局变量 | 限制作用域到当前源文件(避免跨文件命名冲突) |
| 类成员变量 | 属于类本身,所有对象共享(后续文章详细讲) |

六、数组传参的本质

数组作为函数参数时,并不拷贝整个数组,而是退化成了指针:

`void modify(int arr[], int size) {
std::cout << "sizeof(arr) = " << sizeof(arr) << std::endl;
// 输出 8(指针大小),而不是数组大小!

for (int i = 0; i < size; ++i)
arr[i] *= 2; // 直接修改原数组
}

int main() {
int data[5] = {1, 2, 3, 4, 5};
std::cout << "sizeof(data) = " << sizeof(data) << std::endl;
// 输出 20(5 × 4字节)

modify(data, 5);
// data 变为 {2, 4, 6, 8, 10}
}
`
>
⚠️ 由于数组退化为指针,函数内无法通过 sizeof 获取数组长度——必须显式传递 size 参数

小结

| 序号 | 知识点 | 一句话总结 |
| 1 | 栈 vs 堆 | 栈自动管理、速度快、容量小;堆手动管理、灵活、容量大 |
| 2 | new/delete | 堆上分配和释放,单个用 delete,数组用 delete[] |
| 3 | 内存泄漏 | 借了不还,程序越来越慢直到崩溃——谁分配谁释放 |
| 4 | 悬挂指针 | 指向已释放内存,访问即未定义行为——释放后置空 |
| 5 | static | 延长生命周期(局部)或限制作用域(全局) |
| 6 | 数组传参 | 退化为指针,必须额外传递长度 |
下一篇文章,我们将正式进入 C++ 面向对象编程——类与对象、构造函数与析构函数,看看如何用面向对象的方式组织代码。

本文是「C++ 从基础到项目实战」系列的第 4 篇。关注我,不错过后续更新。

posted @ 2026-06-27 08:45  AI视界尼奥  阅读(3)  评论(0)    收藏  举报