8. 函数(上):函数的定义与声明、参数传递(值传递)、返回值、函数原型

函数这东西,说白了就是C语言的积木块。你写再复杂的程序,最后拆开来看,都是一堆函数在互相调用。我刚开始学C的时候,总觉得函数是个麻烦事——明明可以把代码全塞在main里,干嘛要拆来拆去?后来在项目里吃过亏才明白,不拆函数,代码根本没法维护。

嗯,咱们今天就把函数的上半部分讲透。从定义到声明,从参数传递到返回值,再到函数原型。这些都是基本功,但基本功往往最容易被忽视。

8.1 函数的定义——你总得告诉编译器“怎么做”

函数的定义,就是告诉编译器:这个函数具体要干什么。它由两部分组成——函数头和函数体。

// 函数定义示例
int add(int a, int b) {
    int sum = a + b;
    return sum;
}

你看,int是返回类型,add是函数名,int a, int b是参数列表。花括号里就是函数体,具体干活的地方。

我在项目中遇到过一个问题:有个同事把函数定义写得特别长,一个函数干了五六件事。结果后来要改其中一个逻辑,牵一发而动全身。我的建议是——一个函数只做一件事。如果它做的事情超过三行,你就该考虑拆分了。

核心要点:函数定义 = 返回类型 + 函数名 + 参数列表 + 函数体。缺一不可。

8.2 函数的声明——先打个招呼

函数的声明,也叫函数原型。它告诉编译器:嘿,这个函数存在,参数长这样,返回值是那个类型。至于具体怎么实现,你往后看就行。

// 函数声明(原型)
int add(int a, int b);

// 或者可以省略参数名,只写类型
int add(int, int);

为什么要声明?因为C语言是顺序编译的。如果你把函数定义写在main函数后面,编译器在main里看到add()的时候,它根本不认识。这时候就需要提前声明一下。

我个人习惯是把所有函数声明放在文件开头,或者放在一个头文件里。这样不管函数定义在文件的哪个位置,都不会出问题。

注意:如果函数定义在调用之前,可以省略声明。但为了代码清晰,我建议你永远都写声明。这不是必须的,但这是一个好习惯。

8.3 参数传递(值传递)——你给的是复印件,不是原件

C语言的参数传递,默认都是值传递。什么意思?就是把实参的值复制一份,传给形参。函数内部改的是副本,不影响原来的变量。

#include <stdio.h>

void change(int x) {
    x = 100;  // 改的是副本
}

int main() {
    int a = 10;
    change(a);
    printf("%d\n", a);  // 输出还是10,不是100
    return 0;
}

为什么会这样?因为change(a)的时候,编译器把a的值10复制了一份,传给了x。x在函数里被改成100,但a还是原来的a。

我曾经在项目里踩过这个坑。当时写了一个交换两个变量的函数,传进去两个int,结果函数里交换得热火朝天,外面纹丝不动。后来才意识到——值传递嘛,你交换的是副本,原件根本没动。

小技巧:如果你想在函数里修改外面的变量,就得用指针。这个我们后面会详细讲。

8.4 返回值——函数干完活,总得给个交代

函数执行完了,有时候需要把结果告诉调用者。这就是返回值的作用。

int max(int a, int b) {
    if (a > b) {
        return a;
    } else {
        return b;
    }
}

返回值用return语句。一个函数可以有多个return,但每次调用只会执行其中一个。return后面的值,类型必须和函数声明时的返回类型一致。

如果函数不需要返回值,就用void。比如我们刚才的change函数,它只是改了一个副本,不需要返回什么。

注意:return语句会立即结束函数的执行。return后面的代码不会被执行。所以如果你在return之后还写了代码,编译器会给你警告,或者直接报错。

8.5 函数原型——编译器的“信任状”

函数原型就是函数的声明。它告诉编译器函数的接口信息:返回类型、函数名、参数类型和个数。这样编译器在遇到函数调用时,就能检查参数类型是否匹配,返回值是否被正确使用。

// 函数原型
double calculateArea(double radius);

int main() {
    double area = calculateArea(5.0);  // 编译器知道要传double,返回double
    return 0;
}

double calculateArea(double radius) {
    return 3.14159 * radius * radius;
}

如果没有函数原型,编译器在main里看到calculateArea(5.0),它会自己猜——猜返回类型是int,猜参数类型。猜对了还好,猜错了就是各种诡异bug。

我记得有一次,一个同事忘了写函数原型,结果传了一个float进去,函数定义里用的是double。编译器没报错,但运行结果完全不对。查了两天才发现是类型不匹配。从那以后,我写任何函数都会先写原型。

强烈建议:在C99及以后的标准中,调用没有原型的函数是不允许的。所以请务必为每个函数提供原型。这不是可选项,这是必须项。

8.6 知识体系总览

下面这张图,把本章的核心知识点串起来了。你可以把它当作一个快速回顾的路线图。

函数(上) 函数的定义 返回类型 + 函数名 + 参数 + 函数体 函数的声明(原型) 告诉编译器:函数存在,接口长这样 参数传递(值传递) 实参复制给形参,改形参不影响实参 返回值 return 语句,返回结果给调用者 函数原型 = 声明,是编译器的“信任状”

8.7 避坑指南

最后,我把自己这些年踩过的坑,总结成几条,你写代码的时候多留个心眼。

  • 忘记写函数原型:编译器会默认返回int,导致类型不匹配。我曾经因为这个查了两天bug。
  • 值传递的误解:以为函数里改了参数,外面也会变。记住,你改的是副本。
  • return语句后还有代码:写了也白写,编译器会警告,但不会执行。
  • 函数定义和声明不一致:参数类型、个数、返回类型,必须一模一样。否则编译报错。
  • void函数里写return:可以写return;,但不能写return 值;

我的习惯:写任何函数之前,先把原型写好。然后写函数体。最后在main里调用。这个顺序能帮你避免很多低级错误。

好了,函数的上半部分就讲到这里。定义、声明、参数传递、返回值、函数原型——这几个概念你搞清楚了,写函数就不会出大问题。下一章我们继续讲函数的下半部分,包括作用域、生命周期、递归这些更深入的内容。


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