8. 函数(一):函数的定义与调用、函数的参数传递(值传递)、函数的返回值、函数的声明与定义分离

函数,说白了就是一段可以重复使用的代码块。我刚开始学C语言时,总觉得函数是个麻烦事——明明可以直接写代码,干嘛非要绕个弯子?直到我在一个项目中写了三千多行的main函数,调试到崩溃……嗯,从那以后,我再也不敢不用函数了。

这一章我们聊聊函数的基础。别小看这些基础,我见过不少工作两三年的工程师,在函数声明和定义分离上栽跟头。咱们一步步来。

8.1 函数的定义与调用

函数的定义,就是告诉编译器:我要造一个功能模块,它叫什么、需要什么原料、会产出什么结果。调用呢,就是真正去用这个模块。

先看一个最简单的例子:

#include <stdio.h>

// 函数的定义
void sayHello() {
    printf("Hello, C World!\n");
}

int main() {
    // 函数的调用
    sayHello();
    return 0;
}

这里 sayHello 就是函数名,void 表示它没有返回值。调用时直接写函数名加括号就行。我个人习惯把函数定义放在 main 函数后面,但那样就需要提前声明——这个我们后面会讲。

核心要点:

  • 函数定义 = 函数头 + 函数体
  • 函数调用 = 函数名 + 实参列表
  • 调用前,编译器必须“见过”这个函数(定义或声明)

8.2 函数的参数传递(值传递)

这是初学者最容易迷糊的地方。值传递,说白了就是“复制一份数据”传给函数。函数里怎么改,都不影响原来的变量。

我举个例子:

#include <stdio.h>

void swap(int a, int b) {
    int temp = a;
    a = b;
    b = temp;
    printf("函数内部:a = %d, b = %d\n", a, b);
}

int main() {
    int x = 10, y = 20;
    printf("调用前:x = %d, y = %d\n", x, y);
    swap(x, y);
    printf("调用后:x = %d, y = %d\n", x, y);
    return 0;
}

运行结果:

调用前:x = 10, y = 20
函数内部:a = 10, b = 20
调用后:x = 10, y = 20

看到了吗?函数内部确实交换了,但 main 函数里的 x 和 y 纹丝不动。为什么会这样?因为传递的是 x 和 y 的副本,不是它们本身。

我曾经踩过的坑: 刚工作时写了一个排序函数,传了数组进去,在函数里交换元素,结果发现原数组没变。折腾了半天才想起来——数组名传进去其实是地址,但单个变量传进去是值拷贝。嗯,这个区别很重要。

值传递的特点:

  • 形参是实参的拷贝
  • 函数内修改形参不影响实参
  • 适合传递基本类型数据
  • 如果传递大型结构体,拷贝开销大——这时候要考虑传指针

8.3 函数的返回值

函数可以返回一个结果给调用者。用 return 语句实现。

#include <stdio.h>

int add(int a, int b) {
    int sum = a + b;
    return sum;  // 返回计算结果
}

int main() {
    int result = add(3, 5);
    printf("3 + 5 = %d\n", result);
    return 0;
}

这里 int add(int a, int b) 表示:这个函数返回一个 int 类型的值。return 后面跟的就是要返回的数据。

关于返回值,有几点要注意:

  • 返回值类型必须和函数声明一致
  • return 语句会立即结束函数执行
  • void 函数可以不写 return,也可以写 return; 提前结束
  • 一个函数可以有多个 return,但建议保持单一出口——我个人的代码规范里,除非特殊情况,否则只用一两个 return

小技巧: 函数返回值的类型可以很灵活。比如返回指针、返回结构体,甚至返回函数指针。但初学者先掌握基本类型就好。

8.4 函数的声明与定义分离

这是实际项目中必须掌握的技能。你想想看,一个项目几十个源文件,如果每个文件都要把函数定义写一遍,那不乱套了?

函数的声明,就是告诉编译器:“嘿,有个函数长这样,你先记着,具体实现后面再说。” 定义才是真正的实现。

看一个标准做法:

头文件 myfunc.h:

#ifndef MYFUNC_H
#define MYFUNC_H

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

#endif

源文件 myfunc.c:

#include "myfunc.h"

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

int subtract(int a, int b) {
    return a - b;
}

主文件 main.c:

#include <stdio.h>
#include "myfunc.h"

int main() {
    printf("10 + 5 = %d\n", add(10, 5));
    printf("10 - 5 = %d\n", subtract(10, 5));
    return 0;
}

这样做的优点很明显:

  • 代码模块化,每个 .c 文件只负责一组功能
  • 头文件作为接口文档,别人一看就知道这个模块提供了什么
  • 编译时只编译修改过的文件,链接时再合并——大型项目编译速度的关键

重要规则:

  • 声明可以多次,定义只能一次(One Definition Rule)
  • 头文件里只放声明,不放定义(除非是 inline 函数或模板)
  • 头文件要加 include guard(就是 #ifndef / #define / #endif 那套)

我曾经在一个项目里看到有人把函数定义写在头文件里,结果被两个 .c 文件包含,链接时报重定义错误。排查了半天才发现问题。嗯,这种坑踩过一次就记住了。

知识体系图

下面这张图帮你理清本章的核心脉络:

函数基础 — 知识体系 函数 定义与调用 函数头 + 函数体 / 函数名() 值传递 形参 = 实参的副本 返回值 return 语句 / 类型匹配 声明与定义分离 .h 声明 / .c 定义 调用前必须声明或定义 可多次调用 void 无返回值 return 立即结束函数 头文件加 include guard 声明可多次,定义仅一次

本章小结

函数是C语言模块化的基石。这一章我们讲了四个核心点:

  • 定义与调用:先定义后调用,或者先声明后调用
  • 值传递:传的是副本,函数内修改不影响原变量
  • 返回值:用 return 把结果传回来,类型要匹配
  • 声明与定义分离:头文件放声明,源文件放定义,这是工程化开发的基础

说实话,函数这部分内容看似简单,但很多细节在实际项目中才会暴露出来。比如值传递和指针传递的选择、返回局部变量地址的危险、头文件重复包含的问题……这些我们会在后续章节逐步深入。

你现在要做的,就是多写、多练。把每个概念都亲手敲一遍代码,看看运行结果。编程这东西,光看是学不会的。


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