函数(function)是具有名称、可重复调用的代码块:接收实参,执行一组语句,并可向调用方返回一个结果。C 程序由函数组织而成,程序入口 main 本身也是一个函数。将重复逻辑封装为函数,有利于划分模块、减少重复代码并便于阅读与维护。

函数的特性与限制

在学习具体语法之前,先明确 C 语言函数在标准模型下的几条约定:

  • 除变参函数等进阶话题外,常见函数具有固定个数的形式参数。
  • 一次调用只能返回一个值;若不需要返回值,返回类型应声明为 void。
  • 标量类型的参数默认按值传递:形参得到的是实参值的副本,在函数内修改形参不会改变调用方的变量。若要通过形参修改调用方的对象,需要传递地址,相关内容见指针一章。

定义与调用

下列代码定义函数 foo:接收一个 int 形参 bar,返回其两倍:

int foo(int bar) {
    return bar * 2;
}

int main(void) {
    foo(1);
    return 0;
}

调用函数使用函数名加圆括号的形式,例如 foo(1) 将整数 1 按值传给形参 bar,再执行函数体中的语句。

函数的声明与定义

C 语言要求:在调用点之前,编译器必须已知该函数的返回类型与参数列表。声明(declaration)只给出函数原型,不包含函数体;定义(definition)则包含完整的实现。可以先在文件前部声明,再在稍后或另一翻译单元中定义:

#include <stdio.h>

int foo(int bar);   /* 声明 */

int main(void) {
    printf("foo 的返回值是 %d\n", foo(1));
    return 0;
}

int foo(int bar) {  /* 定义 */
    return bar + 1;
}

声明通常写在源文件顶部,或放在头文件(.h)中供多个源文件包含,以避免重复定义并保证调用处类型一致。

返回类型为 void 的函数

若函数只执行操作、不向调用方返回数值,返回类型写 void。无参时建议写 void,以明确表示不接受参数:

void moo(void) {
    /* 无返回值 */
}

int main(void) {
    moo();
    return 0;
}

void 函数不能使用带表达式的 return;若需提前结束执行,可写不带值的 return;。这与返回整型 0 的 main 含义不同:void 表示调用方不接收返回值。

库函数与自定义函数

C 程序中的函数大致可分为两类:

  • 库函数:由标准库或第三方库提供,例如 printf、scanf、sqrt。使用前需通过 #include 包含相应头文件,如 <stdio.h>、<math.h>。
  • 自定义函数:由程序员在工程中自行编写。按是否带参数、是否返回值,可组合为无参无返、有参无返、无参有返、有参有返四种常见形式。
C 语言函数定义与调用示例运行
#include <stdio.h>

int square(int x) {       /* 有参数有返回值 */
    return x * x;
}

void greet(void) {        /* 无参数无返回值 */
    printf("你好!\n");
}

示例:计算平方

#include <stdio.h>

int square(int x) {
    return x * x;
}

int main(void) {
    int result = square(5);
    printf("5 的平方是:%d\n", result);
    return 0;
}

从语法上看,函数定义包含四部分:

  • 返回类型:函数值的类型;无返回值时为 void。
  • 函数名:标识符,命名规则与变量相同,在同一作用域内不可重复。
  • 参数列表:形参的类型与名称;无参时可写空括号或 void。
  • 函数体:由花括号括起的语句序列。

参数传递:按值传递

C 语言的函数参数只有按值传递这一种机制:调用时把实参的值复制给形参。标准 C 没有 C++ 那样的引用形参(例如 int &x),也不存在单独的“引用传递”语法。

教材或交流中常说的“传地址”“传指针”,在语言层面仍然是把指针变量的值(即地址)按值传给形参;函数拿到的是地址的副本,但可以通过该地址访问并修改调用方的对象。指针的声明、取地址与解引用,在指针一章系统说明;数组作为函数参数时数组名也会退化为首地址,本质同样是指针按值传递。

常见说法在 C 语言中的含义
按值传递基本且唯一的参数传递方式;形参是实参值的副本
按引用传递标准 C 不支持;需改调用方变量时请传指针(或数组名)
传指针 / 传地址按值传递指针;形参保存地址副本,可通过解引用修改所指对象

下列示例中,addOne 的形参类型为 int,修改的是 x 的副本,因此 main 中的 num 在调用后仍为原值:

#include <stdio.h>

void addOne(int x) {
    x = x + 1;
    printf("函数里的 x:%d\n", x);
}

int main(void) {
    int num = 5;
    addOne(num);
    printf("main 里的 num:%d\n", num);
    return 0;
}

输出中函数内 x 为 6,而 main 里 num 仍为 5。若要在函数内修改调用方的 int 变量,需把地址传给指针形参,例如 addOnePtr(&num),在函数内通过 *p 修改:

void addOnePtr(int *p) {
    if (p != NULL) {
        *p = *p + 1;
    }
}

int main(void) {
    int num = 5;
    addOnePtr(&num);   /* 传递 num 的地址;实参仍按值传入指针 */
    /* 此时 num 变为 6 */
    return 0;
}

这里 &num 是地址,形参 p 得到的是该地址的副本;通过 *p 访问的是 main 中的 num,因此能实现“在函数内改调用方变量”的效果,但传递机制本身仍是按值传递。

调用过程与栈帧

每次函数调用时,运行环境通常在栈上为该次调用分配一块区域,称为栈帧(stack frame),用于存放形参、返回地址以及函数内的局部变量等;函数返回后,对应栈帧被回收。由此可得:

  • 不同调用之间的局部变量相互独立,同一函数被多次调用时,各次调用的局部量互不共享。
  • 栈空间有限,递归层数过深可能导致栈溢出,相关内容在递归一节讨论。

本节介绍了函数的定义、声明、调用方式、按值传递(含传指针时的含义)以及栈帧的基本概念。掌握这些之后,可进一步学习指针与递归等话题。