指针与递归:小白也能懂的 C 语言教程
一、指针
先建立画面
内存就是一排格子,每个格子有一个地址,每个格子里存数据。
地址: 0x1000 0x1004 0x1008
┌──────┐ ┌──────┐ ┌──────┐
值: │ 42 │ │ 100 │ │0x1000│
└──────┘ └──────┘ └──────┘
变量: a b p(指针)
指针就是:一个专门用来存”地址”的变量。
p 里存的是 a 的地址(0x1000),所以说”p 指向 a”。
三个关键符号
| 符号 | 含义 | 比喻 |
|---|---|---|
&a | 取 a 的地址 | ”告诉我 a 住哪儿” |
int *p | 声明 p 是指针 | ”p 是个存地址的变量” |
*p | 解引用,跟着地址找到那个格子 | ”去 p 指向的地方” |
一句话记住:
p是门牌号,*p是房间里的东西。
代码示例
int a = 42; // a 在地址 0x1000,值是 42
int *p = &a; // p 存的是 a 的地址(0x1000)
printf("%d\n", a); // 输出 42
printf("%d\n", *p); // 也输出 42(通过地址找到 a)
*p = 99; // 通过 p,把 a 的值改成 99
printf("%d\n", a); // 输出 99,a 真的被改了!
常见用途
1. 函数修改外部变量
// ❌ 这样不行,函数拿到的是副本,改不了外面的 x
void add_one(int n) {
n = n + 1;
}
// ✅ 传地址进去,才能真正修改
void add_one(int *n) {
*n = *n + 1;
}
int x = 5;
add_one(&x);
printf("%d\n", x); // 输出 6
2. 指针和数组
数组名本身就是指针,指向第一个元素的地址。
int arr[3] = {10, 20, 30};
int *p = arr; // p 指向 arr[0]
printf("%d\n", *p); // 10
printf("%d\n", *(p+1));// 20(向后移动一格)
printf("%d\n", p[2]); // 30(也可以用下标)
⚠️ 最容易犯的错:野指针
// ❌ 危险!p 没有初始化,指向随机地址
int *p;
*p = 99; // 程序崩溃!
// ✅ 声明时赋值,或者先置为 NULL
int a = 42;
int *p = &a; // 安全
// 或者
int *q = NULL; // 先置空,后续再赋值
二、递归
核心思想
递归就一句话:函数自己调用自己,但每次问题变小一点,直到遇到最简单的情况(出口)就停下来。
用阶乘来理解
5! = 5 × 4 × 3 × 2 × 1
递归的思路:
factorial(5)=5 × factorial(4)factorial(4)=4 × factorial(3)factorial(3)=3 × factorial(2)factorial(2)=2 × factorial(1)factorial(1)=1← 出口,直接给答案,不再往下问
然后再一层层往回返回结果:
调用阶段(展开) 返回阶段(收回)
────────────────────────────────────────────
factorial(5) → 等待... ← 5 × 24 = 120 ✓
factorial(4) → 等待... ← 4 × 6 = 24
factorial(3) → 等待...← 3 × 2 = 6
factorial(2) → 等待...← 2 × 1 = 2
factorial(1) = 1 ← 出口!直接返回
对应的代码
int factorial(int n) {
if (n == 1) return 1; // 出口:最简单的情况
return n * factorial(n-1); // 递归:问题变小一点
}
int result = factorial(5);
printf("%d\n", result); // 输出 120
写递归只需要想两件事
- 出口是什么? 什么时候直接给答案,不再往下问
- 大问题怎么变成小问题?
factorial(n)怎么用factorial(n-1)表达
再看一个例子:斐波那契数列
F(n) = F(n-1) + F(n-2),前两项是 1。
1, 1, 2, 3, 5, 8, 13, 21 ...
int fib(int n) {
if (n == 1 || n == 2) return 1; // 出口:前两项都是 1
return fib(n-1) + fib(n-2); // 递归:用前两项之和
}
printf("%d\n", fib(6)); // 输出 8
⚠️ 最容易犯的错:忘记出口
// ❌ 危险!没有出口,无限递归,程序崩溃(栈溢出)
int factorial(int n) {
return n * factorial(n-1); // 一直调用,永远不停
}
// ✅ 一定要有出口
int factorial(int n) {
if (n == 1) return 1; // 出口在这里!
return n * factorial(n-1);
}
三、两者对比总结
| 指针 | 递归 | |
|---|---|---|
| 核心是什么 | 存地址,通过地址操作数据 | 函数调用自己,问题逐渐变小 |
| 关键操作 | & 取地址,* 解引用 | 找出口,定递推关系 |
| 最常见的坑 | 野指针(未初始化就使用) | 忘记出口(无限递归) |
| 调试思路 | 打印地址和值,确认指向正确 | 打印每层的参数和返回值 |
四、快速记忆口诀
指针:
&取地址,*去找它,p是门牌,*p是家。
递归:
先找出口,再缩问题;一层层下去,一层层回来。