GESP 6级
new / delete 动态内存
6级 · 指针/STL/复杂度
💡 为什么重要
普通变量在程序运行前就确定了大小(编译时分配),但有时我们运行时才知道需要多少空间(比如让用户输入数组大小)。动态内存分配让我们在程序运行时向系统"申请"内存。链表、树等动态数据结构都依赖 new/delete。GESP 6级考试经常考查 new/delete 的正确用法和内存泄漏问题。
📖 前置知识
- 指针基础(指针声明、& 取地址、* 解引用)
- 数组的基本使用
- 变量的作用域概念
🔍 栈内存 vs 堆内存
计算机给程序分配的内存分为两个主要区域:
- 栈(Stack):自动管理。你声明的普通变量(如
int a = 5;)都在栈上。函数结束时自动释放。速度快,但空间有限(通常几MB)。 - 堆(Heap):手动管理。用 new 申请的内存都在堆上。必须手动用 delete 释放。空间大,但需要自己管。
打个比方:栈像是图书馆的阅读座位(用完自动回收),堆像是你租的储物间(需要你自己打扫归还)。
📐 核心语法
int *p = new int; // 在堆上申请1个int的空间
int *p = new int(10); // 申请并初始化为10
int *arr = new int[n]; // 申请n个int的数组
delete p; // 释放单个变量
delete[] arr; // 释放数组(注意[])
★ new 返回的是指针(地址)
★ 用完必须 delete,否则内存泄漏
★ delete后指针变为悬空指针,建议置为nullptr
int *p = new int(10); // 申请并初始化为10
int *arr = new int[n]; // 申请n个int的数组
delete p; // 释放单个变量
delete[] arr; // 释放数组(注意[])
★ new 返回的是指针(地址)
★ 用完必须 delete,否则内存泄漏
★ delete后指针变为悬空指针,建议置为nullptr
💻 代码详解
📝 示例1:new/delete 基本用法
#include <iostream>
using namespace std;
int main() {
// 1. 动态分配单个变量
int *p = new int; // 在堆上申请1个int大小的空间
// new返回这块空间的地址,赋给p
*p = 42; // 通过指针给这块空间赋值
cout << *p << endl; // 输出42
delete p; // 释放p指向的堆内存
// ★ 用完一定要delete!
// 2. 动态分配并初始化
int *q = new int(100); // 申请1个int,并初始化为100
cout << *q << endl; // 输出100
delete q; // 释放
// 3. 动态分配数组
int n;
cout << "输入数组大小: ";
cin >> n; // 运行时才知道大小!
int *arr = new int[n]; // 动态申请n个int的数组
for (int i = 0; i < n; i++) {
arr[i] = i * 10; // 和普通数组一样用下标访问
}
for (int i = 0; i < n; i++) {
cout << arr[i] << " "; // 0 10 20 ...
}
cout << endl;
delete[] arr; // 释放数组,注意delete后面的[]
// ★ 数组用 delete[],不是 delete!
return 0;
}
📝 示例2:动态二维数组
#include <iostream>
using namespace std;
int main() {
int n, m;
cout << "输入行数和列数: ";
cin >> n >> m;
// 动态创建 n 行 m 列的二维数组
int **grid = new int*[n]; // 申请n个指针(n行)
for (int i = 0; i < n; i++) {
grid[i] = new int[m](); // 每行申请m个int,()表示初始化为0
}
// 使用:和普通二维数组一样
grid[0][0] = 1;
grid[1][2] = 5;
// 打印
for (int i = 0; i < n; i++) {
for (int j = 0; j < m; j++) {
cout << grid[i][j] << " ";
}
cout << endl;
}
// 释放:先释放每行,再释放指针数组
for (int i = 0; i < n; i++) {
delete[] grid[i]; // 先释放每一行
}
delete[] grid; // 再释放行指针数组
// ★ 顺序不能反!先内后外
return 0;
}
📝 示例3:内存泄漏演示
#include <iostream>
using namespace std;
int main() {
// ★ 反面教材:内存泄漏
int *p = new int(10); // 申请了一块堆内存
// 如果这里直接让p指向别处...
p = new int(20); // p现在指向新的堆内存
// 之前的那块内存(存着10的)就没有指针指向它了
// 程序再也无法释放它 → 这就是"内存泄漏"!
delete p; // 只能释放后面的20那块
// 前面的10那块永远泄漏了
// ★ 正确做法:
int *q = new int(10);
delete q; // 先释放
q = new int(20); // 再申请新的
delete q; // 用完再释放
return 0;
}
⚠️ 易错点
- 忘记 delete:程序结束前不释放,堆内存泄漏。小程序影响不大,但大程序会越来越占内存
- delete 和 delete[] 混用:用 new[] 申请的数组必须用 delete[] 释放,用 new 申请的单变量用 delete。混用是未定义行为,可能崩溃
- 释放后继续使用:delete p 后,p 变成悬空指针(指向已释放的内存)。再 *p 读写都是危险的。释放后建议
p = nullptr; - 重复释放:对同一个指针 delete 两次,第二次释放的内存可能已被别人使用,导致崩溃
- 用 delete 释放栈内存:只有 new 申请的内存才能 delete,栈上的变量不能 delete
- 返回局部变量的地址:函数返回局部变量的指针是危险的,因为函数结束后局部变量就不存在了
🎯 练习建议
- 入门:用 new 动态创建一个 int,赋值后打印,最后 delete
- 进阶:让用户输入 n,动态创建数组,输入 n 个数,求最大值,最后 delete[]
- 挑战:动态创建一个 n×m 的二维数组,实现矩阵转置(行变列,列变行)
- 思考:如果用 new 创建的数组要扩容怎么办?(提示:需要创建新数组、复制数据、释放旧数组)
- 尝试故意制造内存泄漏,用任务管理器观察程序内存占用变化