写C语言的人,迟早要面对一个灵魂拷问:明明上一行代码还在给变量赋值,怎么下一行跳到一个函数里执行了一堆东西,又跳回来了?函数这玩意儿到底是怎么工作的?

先拆定义:函数是一段"签名+身体"的组合

C语言里,函数定义分两部分:签名(返回类型+函数名+形参)和身体(花括号里的代码块)。

int max(int a, int b) {
    return a > b ? a : b;
}

这里有三个关键信息:返回类型int、函数名max、形参列表(int a, int b)。调用者传两个整数进来,函数比较后返回较大的那个。

理解函数定义的关键不是背语法,而是搞清楚谁在调用、谁被调用、谁负责返回。多想想main调用了你写的函数,你的函数里又调用了printf——每一层都是独立的执行单元。

再拆调用:栈帧是函数的临时工位

调用一个函数到底发生了什么?用max(3, 5)举例。

调用时编译器做了三件事:把实参3和5压入栈,把调用后的下一条指令地址压入栈,然后跳转到max函数的机器码地址去执行。

函数执行时在自己的栈帧(stack frame)里工作,包含形参、局部变量、返回地址等。执行结束后返回值存入特定寄存器,恢复调用方的栈帧,跳回保存的返回地址。

main也是函数,它的栈帧存的是命令行参数和局部变量。每次函数调用就是一次压栈动作,每次return就是一次弹栈。

int main() {
    int x = 3;
    int y = 5;
    int z = max(x, y);  // 调用max
    printf("%d\n", z);
    return 0;
}

执行到第5行时发生了一次栈帧创建:保存main的现场,分配max的栈帧。return a > b ? a : b执行完毕后销毁max的栈帧,返回值交给main继续用。

调用过程的本质是执行流的临时转移。CPU执行到max(x, y)时,把当前的"进度"存好,跳过去执行max的代码,执行完再跳回来。这就是"调用"的全部含义。

值传递:C语言唯一传参方式

C语言只有值传递,没有引用传递。传给函数的永远是实参的副本。

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
}
int main() {
    int x = 3, y = 5;
    swap(x, y);  // 无效,交换的是副本
    return 0;
}

为什么交换失败?因为swap收到的不是x和y本身,而是它们值的拷贝。函数内部改的是自己栈帧里的拷贝,x和y纹丝不动。

想交换就得传地址,在函数内部通过地址间接修改:void swap(int *a, int *b)。传的不是值,是地址的副本,但通过地址可以访问到原始变量。

声明与定义:提前跟编译器打个招呼

如果被调用函数写在调用者后面,编译器不知道它的签名,会报"函数隐式声明"警告。解法是提前声明:int max(int a, int b);——只写签名不写身体,告诉编译器"这个函数存在,细节在别处"。

大多数人用的是头文件配合源文件的模式:.h里放声明,.c里放定义,编译器通过声明检查调用是否正确,链接器负责把定义和调用绑定在一起。

全局变量与静态函数:控制可见范围

全局变量定义在所有函数之外,整个文件都能访问。但全局变量会破坏模块封装,让函数之间产生隐式依赖。

想限制函数只在当前文件内可用,用static关键字:static int helper(int x) { ... }。它在链接阶段不暴露给其他编译单元,避免了命名冲突,也提示后来者"这是内部实现细节,别在外部调用"。

递归:函数调自己

递归的核心是每一层调用都在栈上创建新的栈帧,互不干扰。

int factorial(int n) {
    if (n <= 1) return 1;
    return n * factorial(n - 1);
}

每次factorial调用自己,都会分配新栈帧。条件是控制层数,防止无限递归把栈撑爆。每一层返回后上一层继续执行乘法。

实际开发中最容易犯的两个错

第一,用函数时忘了包含头文件。 少了声明,编译器帮你瞎猜返回类型int,参数类型也不检查,编译通过运行时崩溃。

第二,传数组时误以为传了副本。 数组名在表达式中会隐式转为首元素指针,void process(int arr[])和void process(int *arr)完全等价。你在函数里改arr[0],外面数组确实变了。

搞清楚函数定义与调用这件事,核心其实就两点:把代码拆成独立单元分别写好,让CPU在执行流中跳来跳去完成整体逻辑。剩下的都是这个基本机制在不同层面的展开。