Lesson 0007B · 指针与内存模型

① 程序设计基础 — 指针进阶课

上节课(0007):& 取地址,* 解引用,引用是别名,new / delete 分配释放。
这节课:把指针放回内存里看 —— 栈、堆、地址到底长什么样,以及为什么 new 了不 delete 会出事。
三句话速览
  1. 局部变量在栈上,函数一返回它就没了。
  2. new 出来的在堆上,要自己 delete。
  3. 别把局部变量的地址 return 出去。

1. 先记住:内存分四区

你写的 C++ 程序运行时,操作系统给了它一块内存。习惯上分成四块:

区域放什么谁管理什么时候消失
代码区编译后的机器指令系统程序结束
全局区全局变量、static 变量系统程序结束
栈 stack局部变量、函数参数系统自动函数返回后
堆 heapnew 出来的对象程序员手动delete 之后

你之前写的 int a = 10; 大多数情况在栈上;写 int* p = new int; 时,p 这个指针本身在栈上,*p 那块内容在堆上。

2. 栈:进函数就有,出函数就没了

void f() {
    int x = 10;          // x 在栈上
}                        // 函数结束,x 自动消失

int main() {
    f();
    // 这里已经没有 x 了,别想再用 x
    return 0;
}

栈的好处:不用管清理,系统自动回收。坏处:生命周期太短,函数一返回就没了。

经典错误:返回局部变量的地址
int* bad() {
    int x = 10;
    return &x;           // x 在栈上,函数返回后 x 就死了
}                        // 返回的地址指向已死的内存 → 悬空指针

int main() {
    int* p = bad();
    cout << *p;         // 未定义行为:可能碰巧对,可能乱码,可能崩
}

结论:局部变量的地址不要 return 出去。想让数据活得比函数久,就要用堆。

3. 堆:你 new,你就要 delete

int* good() {
    int* p = new int(10);   // 在堆上创建 int,初值 10
    return p;               // 返回堆地址:合法!因为堆内存还活着
}

int main() {
    int* p = good();
    cout << *p;            // 10
    delete p;               // 不用了,手动释放
    p = nullptr;            // 释放后清空指针,防止误用
    return 0;
}

堆的好处:生命周期自己控制。坏处:忘了 delete 就会内存泄漏 —— 程序一直占着那块内存不还。

4. 动态数组:new[] 配 delete[]

int n;
cin >> n;

int* arr = new int[n];       // 大小运行时才定,所以叫“动态”
for (int i = 0; i < n; i++) {
    arr[i] = i * i;
}

for (int i = 0; i < n; i++) {
    cout << arr[i] << " ";
}

delete[] arr;                // 数组必须用 delete[],不能写 delete
arr = nullptr;
其实你早就在用堆:vector<int> v; 内部就是帮你自动 new[] / delete[]。所以能用 vector 就少自己写 new[]。这节课学这些,是为了理解 vector 底层到底发生了什么。

5. 悬空指针:delete 后别再用

int* p = new int(5);
delete p;        // 内存还回去了
// 但 p 仍然保存着原来的地址

cout << *p;     // 悬空指针:访问已释放的内存,危险!
p = nullptr;     // 正确做法:立刻把 p 清空

把指针想象成“门禁卡”:delete 之后卡作废了,但你还攥着;再刷就可能出问题。最稳妥的是作废后直接扔掉 —— 赋成 nullptr。

6. 指针为什么 +1 不是地址 +1

int arr[] = {10, 20, 30};
int* p = arr;         // p 指向 arr[0]

cout << *p;           // 10
p++;                  // 不是地址加 1 字节,而是跳到下一个 int
cout << *p;           // 20

指针 +1 会自动跳过“一个元素的大小”。int* 就跳 4 字节,double* 就跳 8 字节。这正是 arr[i] 和 *(arr + i) 等价的原因。

7. 什么时候用指针,什么时候用引用

场景选择原因
函数想修改传入变量引用 int&语法简单,不能为空
数据要活过函数结束堆指针 new栈内存活不到函数外
可能是“没有对象”指针 nullptr引用不能为空
要遍历数组指针或下标两者等价,优先下标更安全
传大对象但不想改const 引用不拷贝,也不误改

8. 坑点清单

9. 小测

Q1. new int(10) 创建的对象存放在哪里?
Q2. 函数里 return 一个局部变量的地址,会怎样?
Q3. int* arr = new int[8]; 用完应该怎样释放?
Q4. int* p = arr; 之后 p++ 表示什么?

10. 动手练习

题目 1:写一个“数组生成器”
写函数 int* makeSquares(int n),在堆上创建长度 n 的数组,让 arr[i] = i * i,并返回数组地址。
在 main 里打印数组,然后 delete[] 并把指针置为 nullptr。
题目 2(选做):找 bug
下面代码哪里错了?
int* f() { int x = 5; return &x; }
int main() { int* p = f(); cout << *p; }
请用一句话说明:为什么不能这么写,怎么改成合法版本。
卡住时的自查顺序
  1. 这个变量到底在栈上还是堆上?在它前面加一行 cout << &x; 看地址也行。
  2. 函数返回之后,被返回的那块内存还活着吗?栈上的已经不在了。
  3. new 和 delete 的形态对得上吗(new[] 对 delete[])。
  4. 还不对:把程序交给我,我们用 -fsanitize=address 跑一遍,它会直接指出哪一行访问了已释放的内存。

本课小结

下一课预告
0007C 拷贝控制 —— 前面所有麻烦都是“一块内存有两个名字”。那如果一个类里就有个 new 出来的指针成员呢?IntArray b = a; 会复制指针还是复制数据?析构时会不会删两次?这一课讲的就是深拷贝、浅拷贝,和著名的“三法则”。

← 上一课:指针与引用 · 课程对照表

视频 黑马 C++ 核心编程:内存四区、new 运算符、引用本质

书籍 C++ Primer Plus 第 4 章(指针)、第 12 章(动态内存)