这一章在干嘛?

函数的定义与声明(原型)、传值调用的真正含义、ADT 黑盒思想、递归的执行模型,最后到 stdarg 可变参数——printf 家族的底层机制。

7.1 定义、原型与缺省认定

7.2 传值调用与 ADT 黑盒

7.3 递归:追踪与迭代对比

7.4 可变参数:stdarg 机制

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 就是在替你指定类型);② 无法知道参数总个数,得靠具名参数或约定传递;③ … 前至少要有一个具名参数。