12、函数(一):函数的定义与声明、形参与实参、return语句、函数的调用过程

函数这东西,说白了就是C语言的积木块。你把一段逻辑封装起来,给它起个名字,以后想用的时候直接喊它一声就行。我刚开始学C的时候,总觉得函数是个麻烦事——明明可以直接写代码,干嘛非要绕一圈?后来做项目才明白,没有函数,代码就是一坨浆糊。

今天咱们就把函数的老底翻一翻。定义、声明、参数、返回值、调用过程,一个都不放过。

函数的定义:你得告诉编译器长什么样

函数定义,就是告诉编译器:我这个函数叫什么、要什么参数、返回什么类型、里面干什么事。语法很简单:

返回类型 函数名(参数列表) {
    // 函数体
    // 干活的代码
    return 返回值;
}

举个例子,写一个加法函数:

int add(int a, int b) {
    int sum = a + b;
    return sum;
}

这里 int 是返回类型,add 是函数名,int a, int b 是参数列表。函数体里做了加法,然后用 return 把结果送回去。

我的习惯:函数名一定要见名知意。别写什么 func1doSomething 这种名字。你想想看,三个月后你自己回来看代码,看到 func1 是什么感觉?反正我经历过,那叫一个痛苦。

函数的声明:先打个招呼

有时候你把函数定义写在后面,但前面已经用到它了。这时候编译器会报错——它不认识这个函数啊。解决办法就是先声明,后使用

声明就是告诉编译器:嘿,后面有个函数长这样,你先记着。语法就是把定义的第一行抄一遍,末尾加分号:

int add(int a, int b);  // 声明

声明可以放在文件开头,也可以放在头文件里。实际项目中,我习惯把所有函数声明集中到一个 .h 文件里,这样管理起来清爽。

注意:声明和定义的区别一定要搞清楚。声明只是打个招呼,定义才是真正干活。如果你只声明不定义,链接的时候就会报错——链接器找不到函数的具体实现。

形参与实参:名字不同,本质也不同

这两个概念,很多初学者容易搞混。我当年也迷糊过一阵子。

  • 形参(形式参数):写在函数定义里的参数,比如上面 add 函数里的 int a, int b。它们只是占个位置,没有实际的值。
  • 实参(实际参数):调用函数时传进去的具体值,比如 add(3, 5) 里的 35

调用的时候,实参的值会拷贝给形参。这就是所谓的值传递。函数内部修改形参,不会影响实参本身。

void swap(int x, int y) {
    int temp = x;
    x = y;
    y = temp;
    // 这里交换的是形参,外面的实参纹丝不动
}

int main() {
    int a = 10, b = 20;
    swap(a, b);
    printf("a=%d, b=%d\n", a, b);  // 输出:a=10, b=20
    return 0;
}

为什么会这样?因为 swap 函数拿到的是 ab副本,不是它们本人。你想真正交换两个变量的值?得用指针。这个后面再细说。

核心理解:形参是实参的复印件。复印件上改来改去,原件不会变。

return语句:把结果送回来

return 有两个作用:

  1. 返回值:把函数计算的结果传回调用处。
  2. 结束函数:遇到 return,函数立刻停止,后面的代码不执行了。
int check(int score) {
    if (score < 0) {
        return -1;  // 非法分数,直接返回错误码
    }
    if (score > 100) {
        return -1;  // 同样返回错误码
    }
    return 0;  // 分数合法
}

这里用了多个 return,每个都代表一种情况。一旦命中某个条件,函数就结束了。

避坑指南:我曾经在一个项目里写过这样的代码——函数末尾忘了写 return。编译器没报错,但运行时返回值是垃圾值。那次排查了整整两个小时,最后发现是 return 漏了。嗯,从那以后我每次写函数都会检查:所有路径都有 return 吗?

如果函数返回类型是 void,表示不返回任何值。这时候可以写 return; 来提前结束函数,也可以不写。

函数的调用过程:栈帧的舞蹈

函数调用的时候,底层发生了什么?我画个图帮你理解。

函数调用过程:栈帧的压入与弹出 主调函数 (main) int a = 10, b = 20; int result = add(a, b); // 调用 add 函数 ① 压栈(调用) 被调函数 (add) int add(int x, int y) { return x + y; } ② 弹栈(返回) 运行时栈 main 栈帧 a=10, b=20 返回地址 add 栈帧 x=10, y=20 返回地址 ↓ 栈增长方向 ← 栈底(高地址) ← 栈顶(低地址) 调用时压入栈帧,返回时弹出栈帧。形参 x、y 是实参 a、b 的副本。

调用过程大致分几步:

  1. 压栈:把实参的值拷贝给形参,把返回地址(函数执行完后该回到哪里)压入栈中。
  2. 跳转:程序跳转到函数的第一条指令开始执行。
  3. 执行:函数体里的代码一条条跑。
  4. 返回:遇到 return,把返回值放到指定位置(通常是寄存器),然后弹出栈帧,回到调用处继续执行。

这个过程,说白了就是栈帧的压入和弹出。每次调用函数,栈上就多一块空间(栈帧),里面放着局部变量、参数、返回地址。函数返回了,这块空间就释放了。

关键点:栈帧是后进先出的。你调用 A,A 调用 B,B 调用 C……返回的时候是 C→B→A→main。这就是递归能工作的底层基础。

实战建议

说了这么多,给你几个实在的建议:

  • 函数不要太长:我个人的习惯是,一个函数不超过 50 行。超过就考虑拆分成小函数。你想想看,一个 200 行的函数,读起来是什么感觉?
  • 一个函数只做一件事:比如上面那个 check 函数,只做分数合法性检查。别把打印、计算、保存全塞进去。
  • 参数不要太多:超过 3 个参数,就考虑用结构体打包。否则调用的时候你自己都记不清顺序。
  • 返回值要明确:成功返回什么,失败返回什么,提前约定好。我见过一个项目,有的函数返回 0 表示成功,有的返回 1 表示成功……那叫一个乱。
我的经验:写函数之前,先想清楚它的输入、输出、副作用。输入就是参数,输出就是返回值,副作用就是它会不会修改全局变量、会不会打印东西。尽量让函数没有副作用,这样测试起来轻松很多。

函数这东西,看着简单,但用好了能让代码质量上一个台阶。多写、多拆、多思考,慢慢就有感觉了。


公众号:蓝海资料掘金营,微信deep3321