这一章在干嘛?
函数的定义与声明(原型)、传值调用的真正含义、ADT 黑盒思想、递归的执行模型,最后到 stdarg 可变参数——printf 家族的底层机制。
7.1 定义、原型与缺省认定
/* 定义:返回类型 函数名(参数表) { ... } */
int add(int a, int b)
{
return a + b;
}
int add(int, int); /* 原型(声明):只写类型,告诉编译器长这样 */
原型是现代 C 的基石:编译器据此检查调用处的参数类型/个数并做必要转换。没有原型时,编译器对未声明的函数按「缺省认定」处理——返回 int、参数做默认提升(char/short 升 int,float 升 double)——一旦实际不符就是隐性 bug。所以:先声明(或直接把定义放前面)再调用,头文件就是原型的集中营。
K&R 旧式声明(了解即可):
int add(); 参数表为空 = 不检查参数,与原型 int add(void) 完全不同。新代码一律写全原型。
7.2 传值调用与 ADT 黑盒
C 的一切实参都是按值拷贝:函数拿到的是副本,改副本动不了原件。想改原件,把「地址」当值传(第 6 章 swap),或者传「指向可变数据的指针」:
void bump(int n) { n++; } /* 改副本:无效 */
void bump2(int *n) { (*n)++; } /* 改目标:有效 */
void bump3(int arr[]) { arr[0] = 99; } /* 数组参数退化为指针:改的是原数组! */
bump(x); /* x 不变 */
bump2(&x); /* x = x + 1 */
bump3(buf); /* buf[0] 变 99 —— 因为传进来的是地址副本 */
ADT(抽象数据类型)与黑盒:把数据结构的使用方式(接口)和实现细节隔开——调用方只知道「函数名、参数、语义」,不知道内部怎么存。头文件放接口,.c 文件藏实现,static 收私状态。这是第 17 章堆栈/队列/树的预演。
数组参数的两个坑:
① 数组名传参退化为指针,sizeof 在函数内测不出数组长度,必须另传长度参数;② 声明 void f(int a[]) 与 void f(int *a) 完全等价,方括号只是给人看的。
7.3 递归:追踪与迭代对比
递归 = 函数调用自己。执行模型是每次调用一个栈帧:参数、局部变量、返回地址都独立一份,没到终止条件就一直往下压栈。经典的阶乘与斐波那契:
long fact(int n) long fib(int n)
{ {
if (n <= 1) if (n <= 2)
return 1; /* 终止 return 1;
return n * fact(n - 1); return fib(n-1) + fib(n-2);
} } /* 大量重复计算! */
| 对比项 | 递归 | 迭代 |
|---|---|---|
| 可读性 | 贴合「分治」定义,树/回溯类问题自然 | 循环直白但状态管理啰嗦 |
| 开销 | 每层一个栈帧,深度大有栈溢出风险 | 常数个变量,O(1) 空间 |
| 重复计算 | 朴素递归可能指数级重复(如 fib) | 天然不重复 |
选择标准:问题本身递归定义(树遍历、汉诺塔、目录遍历)→ 递归写得漂亮;线性累加/累乘 → 迭代更稳。递归深度可观或存在重复子问题时,改迭代或加缓存。
7.4 可变参数:stdarg 机制
printf(a, b, c…) 这类「参数个数不定」的函数靠 <stdarg.h> 实现:
#include <stdarg.h>
int sum(int count, ...) /* 至少一个具名参数,后接 ... */
{
va_list ap;
va_start(ap, count); /* ap 定位到第一个可变参数 */
int total = 0;
while (count--)
total += va_arg(ap, int);/* 逐个按指定类型取出 */
va_end(ap); /* 收尾 */
return total;
}
printf("%d\n", sum(3, 10, 20, 30)); /* 60 */
三条限制:
① 取参数必须知道类型——类型错了读出来就是垃圾(printf 的 %d/%f 就是在替你指定类型);② 无法知道参数总个数,得靠具名参数或约定传递;③ … 前至少要有一个具名参数。
1. C 是传值调用,那 swap(int *x, int *y) 为什么有效?
传进来的仍是「值」——地址的副本。但 *x 解引用后操作的是地址指向的原变量本身,所以能交换外部数据。指针的值是拷贝的,指向的目标不是。
2. 数组名作函数参数时 sizeof 为什么不对了?
数组名传参退化为指向首元素的指针,函数内 sizeof(a) 得到的是指针大小(如 8),不是数组总大小。所以必须同时传长度参数;void f(int a[]) 与 void f(int *a) 等价。
3. 朴素递归 fib 为什么慢?怎么救?
fib(n) 展开成两棵子树,同一子问题被反复计算,复杂度指数级。改迭代(两个变量滚动)或加记忆化缓存已算过的值,降到线性。深度大的递归还有栈溢出风险。