C 函数

函数是 C 程序组织代码的基本单元——把一段逻辑打包,起个名字,需要时调用。好的函数职责单一、命名清晰、可复用。本篇带你搞懂 C 函数的定义、声明、参数传递(值传递与指针传递)、递归,以及一个新手必踩的坑:为什么 C 函数改不了外面的变量。

一、函数定义

C 函数由四部分组成:返回类型 + 函数名 + 参数列表 + 函数体

#include <stdio.h>

// 函数定义:返回类型 + 名字 + 参数列表 + 函数体
int add(int a, int b) {
    return a + b;       // return 返回结果,函数结束
}

// 没有返回值用 void
void greet(void) {      // void 表示无参数
    printf("你好!\n");
}

// 参数也可以写 void 省略,但显式写 void 更规范
void say_hi() {         // C 风格:空 () 表示"任意参数"(不推荐)
    printf("Hi!\n");
}

int main(void) {
    int sum = add(3, 5);    // 调用函数,接收返回值
    printf("3 + 5 = %d\n", sum);

    greet();                // 调用无返回值函数
    say_hi();
    return 0;
}

几个要点:

二、函数声明(原型)

C 编译器是从上到下读代码的,如果一个函数定义在 main 后面,main 里调用它编译器会报错"未声明的标识符"。解决办法是在前面写函数原型(只声明签名,不写函数体):

#include <stdio.h>

// 函数原型(声明):写在 main 前面,告诉编译器"有这个函数"
// 这样 main 可以调用定义在后面的函数
int add(int a, int b);     // 末尾的分号!参数名可省略
double average(int, int);  // 只写类型也行

int main(void) {
    printf("%d\n", add(3, 5));
    printf("%.2f\n", average(10, 20));
    return 0;
}

// 真正的定义在后面
int add(int a, int b) {
    return a + b;
}

double average(int a, int b) {
    return (a + b) / 2.0;
}

// 小贴士:多文件项目里,函数原型写在头文件 .h
// 定义写在 .c,使用时 #include 头文件即可

函数原型的作用是告诉编译器:"存在这样一个函数,长这样,具体实现后面会给你"。这样编译器就能做类型检查——你调用时参数类型不对,它会报错。

多文件项目里,函数原型通常写在头文件(.h)里,实现写在 .c 里。用的时候 #include 头文件即可——这就是标准库 stdio.h 在做的事。

三、值传递(C 的核心规则!)

这是新手最容易困惑的一点:C 的函数参数默认都是值传递——把实参的值拷贝一份给形参,函数内改形参不影响外面的实参。

#include <stdio.h>

// C 的参数默认是"值传递"(拷贝一份)
void try_modify(int x) {
    x = 999;        // 改的是拷贝,不影响外面的变量
}

// 要修改外部变量,必须传指针
void modify(int *p) {
    *p = 999;       // 通过指针改外面的变量
}

int main(void) {
    int n = 1;

    try_modify(n);
    printf("n = %d\n", n);    // 1,没变

    modify(&n);                // 传地址
    printf("n = %d\n", n);    // 999,被修改了
    return 0;
}

为什么会这样?因为函数内部的 x 是一份拷贝,它和外面的 n 是两个完全独立的变量(在不同的内存位置)。函数返回时 x 就销毁了。

要修改外部变量,必须传指针——把变量的地址传进去,函数内通过解引用 *p 修改的就是外面的变量本身。这是 C 没有"引用"(reference)机制的代价,也是 C 最经典的设计模式。

四、用指针"返回多个值"

C 函数只能返回一个值。如果你需要"返回多个结果"(比如除法的商和余数),经典做法是传指针作为输出参数:

#include <stdio.h>

// C 函数只能返回一个值
// 要"返回多个结果",经典做法:传入指针作为输出参数
void divmod(int a, int b, int *quotient, int *remainder) {
    *quotient = a / b;
    *remainder = a % b;
}

// 另一种做法:返回结构体(后续 struct 篇讲)
// 还可以用全局变量,但不推荐(破坏封装)

void swap(int *a, int *b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main(void) {
    int q, r;
    divmod(17, 5, &q, &r);      // 传 q、r 的地址
    printf("17/5 = %d 余 %d\n", q, r);   // 3 余 2

    int x = 10, y = 20;
    swap(&x, &y);
    printf("x=%d, y=%d\n", x, y);       // x=20, y=10
    return 0;
}

这种写法在 C 标准库里随处可见——scanf 就是这么工作的,所以你写 scanf("%d", &n) 必须带 &,否则它没法把读到的值写进 n。交换两个变量的 swap 函数也是经典例子,必须传指针才能交换成功。

五、递归

递归是函数调用自己。它能优雅地解决"自相似"问题(阶乘、斐波那契、树遍历),但性能不如循环(函数调用有开销,栈空间有限)。

#include <stdio.h>

// 递归:函数调用自己
// 经典1:阶乘 n! = n * (n-1)!
long factorial(int n) {
    if (n <= 1) return 1;       // 基准情形(必须有,否则无限递归)
    return n * factorial(n - 1);
}

// 经典2:斐波那契 F(n) = F(n-1) + F(n-2)
long fib(int n) {
    if (n < 2) return n;
    return fib(n - 1) + fib(n - 2);
}

// 经典3:汉诺塔
void hanoi(int n, char from, char to, char via) {
    if (n == 1) {
        printf("%c -> %c\n", from, to);
        return;
    }
    hanoi(n - 1, from, via, to);
    printf("%c -> %c\n", from, to);
    hanoi(n - 1, via, to, from);
}

int main(void) {
    printf("5! = %ld\n", factorial(5));    // 120
    printf("fib(10) = %ld\n", fib(10));    // 55

    hanoi(3, 'A', 'C', 'B');
    return 0;
}

写递归的两条铁律:

注意示例里的斐波那契是低效的——它计算了大量的重复子问题(fib(5) 会算 fib(4)+fib(3),fib(4) 又算 fib(3)+fib(2),fib(3) 被算了两次)。实际项目里要么用循环,要么用记忆化(把算过的结果存起来)。

六、可变参数函数

C 支持定义"参数个数不确定"的函数,最典型的就是 printf。它通过 <stdarg.h> 提供的四个宏实现:

#include <stdio.h>
#include <stdarg.h>     // 可变参数需要的头文件

// 可变参数函数:参数个数不确定
// 用 va_list / va_start / va_arg / va_end 四个宏
int sum_n(int count, ...) {
    va_list args;
    va_start(args, count);     // count 是最后一个具名参数

    int total = 0;
    for (int i = 0; i < count; i++) {
        total += va_arg(args, int);   // 每次取一个 int
    }

    va_end(args);              // 清理
    return total;
}

// 自己实现一个简化版 printf 风格的函数
void log_ints(const char *tag, int count, ...) {
    va_list args;
    va_start(args, count);
    printf("%s: ", tag);
    for (int i = 0; i < count; i++) {
        printf("%d ", va_arg(args, int));
    }
    printf("\n");
    va_end(args);
}

int main(void) {
    printf("%d\n", sum_n(3, 10, 20, 30));         // 60
    printf("%d\n", sum_n(5, 1, 2, 3, 4, 5));      // 15
    log_ints("scores", 3, 95, 88, 92);
    return 0;
}

四个宏的作用:

可变参数函数没有类型检查——你传错类型编译器不会报错,运行时会得到垃圾值。这也是为什么 printf("%d", 3.14) 会输出奇怪结果——%d 期望 int,你给了 double,位模式被错误解读。

七、函数设计原则

八、main 函数的特殊性

main 是程序入口,有两种标准形式:

main 返回 0 表示成功,非 0 表示失败。操作系统或调用脚本可以根据这个返回值判断程序是否正常退出。

小结

函数四件套:返回类型、名字、参数、函数体。原型声明让定义写在后面也能用。C 默认值传递,要修改外部变量必须传指针。递归要有基准情形。可变参数用 stdarg.h 四个宏。下一篇我们看 C 的数组——一组连续的同类型数据。

← 上一篇 C 控制流

下一篇 C 数组

✈️💬