写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在执行流中跳来跳去完成整体逻辑。剩下的都是这个基本机制在不同层面的展开。
