/* 定义:返回类型 函数名(参数表) { ... } */
int add(int a, int b)
{
return a + b;
}
int add(int, int); /* 原型(声明):只写类型,告诉编译器长这样 */原型是现代 C 的基石:编译器据此检查调用处的参数类型/个数并做必要转换。没有原型时,编译器对未声明的函数按「缺省认定」处理——返回 int、参数做默认提升(char/short 升 int,float 升 double)——一旦实际不符就是隐性 bug。所以:先声明(或直接把定义放前面)再调用,头文件就是原型的集中营。
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 章堆栈/队列/树的预演。
递归 = 函数调用自己。执行模型是每次调用一个栈帧:参数、局部变量、返回地址都独立一份,没到终止条件就一直往下压栈。经典的阶乘与斐波那契:
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) | 天然不重复 |
选择标准:问题本身递归定义(树遍历、汉诺塔、目录遍历)→ 递归写得漂亮;线性累加/累乘 → 迭代更稳。递归深度可观或存在重复子问题时,改迭代或加缓存。
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 */