C语言基础教程:函数、参数和地址

前面咱们已经学过了基础数据类型以及数组和字符串,也介绍了输入输出和控制流。这次咱们就来了解一下C语言中非常核心的几个概念:函数、参数、以及那个让很多人头疼的——地址。 函数,其实最开始可能就是为了将重复的代码聚在一起,给它起个名字,然后下次用的时候直接用这个名字,而不是再重复写一大堆。 当然,函数还有其他很多用处,比如将一个大任务拆分成几个小任务,这样代码看起来更清晰,也更容易调试。 调试的英文是debug,de是删除的意思,bug是虫子,这个词的出处大概是最早的时候的计算机,里面一旦有了虫子就可能导致运行出错,所以解决运行出错的过程最开始真是除虫,后来这个debug就成了调试的意思。

1. 函数是什么?

假如你去超市买东西,你会发现很多东西都有不同包装不同容量但同样内容,比如苏打水有三百毫升的卖两块,五百毫升的卖三块,那哪一个划算呢?

这时候你就很自然的有一个合计的过程: * 用三块除以五百毫升(3.0 / 500); * 用两块除以三百毫升(2.0 / 300); * 发现前者更便宜。

在这个过程中,你实际上是在心里构建了一个"计算单价"的逻辑。你把价格容量作为输入的两个参数,然后进行除法计算,最后得到单价这个结果。

函数,就是这个数学过程的代码化。

我们把这个计算逻辑封装起来,给它起个名字(比如 calculate_unit_price),以后无论遇到什么商品,只要把价格和容量扔给它,它就能告诉你单价。

  • 输入(参数):价格(2.0),容量(300)。
  • 处理过程(函数体):价格 ÷ 容量。
  • 输出(返回值):单价(0.0066...)。

2. 函数的声明、定义和调用

在C语言中,使用函数通常涉及三个步骤。

声明 (Declaration),就像是在名片盒里放一张名片。你告诉编译器:"嘿,将来会有一个叫 calculate_unit_price 的家伙,它需要两个数字,并且会还给你一个小数。" 这通常放在文件的最开头。

定义 (Definition)这是计算过程的具体实现。你具体写下这个函数是干什么的,怎么算的(比如用除法)。

调用 (Calling)这就是实际使用这个计算过程的时候。你在 main 函数里把苏打水的价格和容量喂给它,然后等着拿单价结果。

看看代码例子 ex1_lifecycle.c

// [引用: code_examples/functions/ex1_lifecycle.c]
#include <stdio.h>

// 1. 函数声明 (Declaration)
// 告诉编译器:有一个叫 calculate_unit_price 的函数
// 它需要两个数字(价格和容量),返回一个小数(单价)。
// 就像是给编译器一张"名片"。
double calculate_unit_price(double price, int volume);

// 2. 函数调用 (Calling)
// 在 main 函数里使用它
int main() {
    // 场景:苏打水比价
    double price_a = 2.0;
    int volume_a = 300;

    double price_b = 3.0;
    int volume_b = 500;

    printf("正在比较苏打水:\n");
    printf("方案 A: %.1f 元 / %d 毫升\n", price_a, volume_a);
    printf("方案 B: %.1f 元 / %d 毫升\n", price_b, volume_b);

    // 调用函数计算单价
    double unit_price_a = calculate_unit_price(price_a, volume_a);
    double unit_price_b = calculate_unit_price(price_b, volume_b);

    printf("\n方案 A 单价: %.4f 元/毫升\n", unit_price_a);
    printf("方案 B 单价: %.4f 元/毫升\n", unit_price_b);

    if (unit_price_b < unit_price_a) {
        printf("结论:方案 B 更便宜!\n");
    } else {
        printf("结论:方案 A 更便宜!\n");
    }
    return 0;
}

// 3. 函数定义 (Definition)
// 具体的计算过程
double calculate_unit_price(double price, int volume) {
    // 核心逻辑:价格除以容量
    return price / volume;
}

运行结果

正在比较苏打水:
方案 A: 2.0  / 300 毫升
方案 B: 3.0  / 500 毫升

方案 A 单价: 0.0067 元/毫升
方案 B 单价: 0.0060 元/毫升
结论:方案 B 更便宜!

代码解析

  1. 流程控制:程序从 main 开始。当执行到 calculate_unit_price(price_a, volume_a) 时,程序暂停 main 中的执行,calculate_unit_price 函数内部。
  2. 参数传递price_a (2.0) 和 volume_a (300) 的值被复制给了函数里的 pricevolume
  3. 返回值:函数计算出 0.0066...,通过 return 语句把这个结果扔回给 main,并赋值给 unit_price_a
  4. 复用性:我们写了一次计算逻辑,用了两次(分别计算 A 和 B),这就是函数的最大价值。

注意上面的过程,只要在主函数之前先声明了函数,就可以在主函数里调用它,在主函数后面实现也没问题。

2.4 程序的执行流程

不管你的代码写了多少行,定义了多少个函数,C语言程序的执行永远遵循一个铁律:万事始于 main,终于 main。

想象你在读一本故事书: 1. 开始 (Start):操作系统找到 main 函数,这就是故事的第一页。 2. 顺序阅读 (Sequential):程序会一行一行往下执行。 3. 插叙 (Function Call):当遇到函数调用(比如 calculate_unit_price)时,就像书里写"欲知后事,请翻到第50页"。程序会暂时停下手中的活,到那个函数里去执行。 4. 回归 (Return):当那个函数执行完(遇到 return),就像是看完了插叙部分,程序会跳回到刚才中断的地方,继续往下读。 5. 结局 (End):当 main 函数执行到 return 0; 时,故事全剧终。程序退出,并告诉操作系统:"我顺利讲完了"(0代表成功)。

3. 函数的参数传递和返回值

3.1 参数传递

这是C语言函数调用最基本的规则,也是初学者最容易误解的地方。

C语言默认采用"值传递"的方式。

我们可以用"传真机""复印机"来理解这个过程:

假如你有一张珍贵的藏宝图(变量),你想让朋友帮忙在上面标记路线(调用函数)。 在 C 语言里,你并不是把原本的藏宝图寄给他,而是复印了一份,把复印件给了他。

朋友在复印件上涂涂改改,甚至不小心把咖啡洒在上面毁了它,对你手里锁在保险柜里的原版藏宝图有影响吗? 完全没有影响。

这就是为什么在下面的例子 ex2_params.c 中,modify_value 函数里把 v 改成了 999,但 main 函数里的 my_num 依然是 5。因为函数里修改的那个 v,只是 my_num 的一张复印件而已。

那如果我真的想让函数修改原版藏宝图怎么办? 这时候,你就不能只发传真了。你得告诉朋友藏宝图锁在哪个保险柜里(即变量的地址),朋友拿着这个地址去找保险柜,打开它,就能修改原件了。这就是我们常说的"地址传递"(或者叫指针传递),这个我们会在下一节详细讲。

总结一下: 1. 值传递(默认):给复印件,安全,但不改变原值。 2. 地址传递(进阶):给地址(钥匙),危险,但能改变原值。

3.2 返回值

函数做完事情后,可以通过 return 语句把结果交还给你。就像自动贩卖机吐出饮料一样。

看这个例子 ex2_params.c

// [引用: code_examples/functions/ex2_params.c]
#include <stdio.h>

// 演示:值传递 (Pass by Value)
void modify_value(int v) {
    printf("  [函数内] 接收到的值: %d\n", v);
    v = 999; // 修改的是局部变量 v (复印件)
    printf("  [函数内] 已将值修改为: %d\n", v);
}

// 演示:返回值 (Return Value)
int square(int n) {
    return n * n; // 计算结果并送回
}

int main() {
    int my_num = 5;

    printf("main函数里的原始数值: %d\n", my_num);

    // 1. 尝试修改
    printf("正在调用 modify_value...\n");
    modify_value(my_num);

    // my_num 不会变,因为传给函数的是复印件
    printf("回到 main 函数,数值依然是: %d\n", my_num);

    // 2. 接收返回值
    printf("正在调用 square (平方计算)...\n");
    int sq = square(my_num);
    printf("%d 的平方是 %d\n", my_num, sq);

    return 0;
}

运行结果

main函数里的原始数值: 5
正在调用 modify_value...
  [函数内] 接收到的值: 5
  [函数内] 已将值修改为: 999
回到 main 函数,数值依然是: 5
正在调用 square (平方计算)...
5 的平方是 25

代码解析

  1. 复印件效应:当调用 modify_value(my_num) 时,C语言把 my_num (5) 复印了一份给变量 v。函数里把 v 改成了 999,这完全不影响外面的 my_num。这就解释了为什么"回到 main 函数,数值依然是 5"。
  2. 正确的交互方式:如果你想从函数里得到结果,应该使用 返回值 (return)。就像 square 函数那样,它计算出结果并明确地 return 回来,我们在 main 里用 int sq = ... 接住了这个结果。

4. 地址是什么?

上面的例子里面,其实已经涉及到了地址,如果你需要让一个函数能够对原值进行修改,就需要用到地址。

为了能修改"原件",或者传递大量数据(不用复印),我们需要了解地址

内存就像一个巨大的旅馆,每个变量都住在一个特定的房间里。 * 变量的值:住在房间里的人(数据)。 * 变量的地址:房间门上的门牌号(比如 0x7ff...)。

在C语言里,我们可以用 & 符号(取地址符)来看看变量住在哪里。

看看 ex3_address.c

// [引用: code_examples/functions/ex3_address.c]
#include <stdio.h>

int main() {
    int house_a = 100;
    int house_b = 200;

    printf("变量的值 (住在房间里的人):\n");
    printf("house_a = %d\n", house_a);
    printf("house_b = %d\n", house_b);

    printf("\n变量的地址 (房间的门牌号):\n");
    // & 操作符用于获取变量的地址
    // %p 是打印地址的专用格式符
    printf("house_a 的地址: %p\n", &house_a);
    printf("house_b 的地址: %p\n", &house_b);

    printf("\n比喻总结:\n");
    printf("数值 (100) 是住在房间里的客人。\n");
    printf("地址 (%p) 是挂在门上的门牌号。\n", &house_a);

    return 0;
}

运行结果

变量的值 (住在房间里的人):
house_a = 100
house_b = 200

变量的地址 (房间的门牌号):
house_a 的地址: 0000004476BEF750
house_b 的地址: 0000004476BEF74C

比喻总结:
数值 (100) 是住在房间里的客人。
地址 (0000004476BEF750) 是挂在门上的门牌号。

(注意:每次运行你的程序,地址可能都会变,因为操作系统分配给程序的内存位置是不固定的)

代码解析

  1. & 运算符:这是"取地址"(Address-of)运算符。&house_a 的意思就是"告诉我变量 house_a 在内存里的地址"。
  2. %p 格式符:这是 printf 专门用来打印指针(地址)的格式。输出通常是十六进制的(带有 0x 或者只是数字),代表内存中的字节编号。
  3. 内存布局:你可以看到 house_ahouse_b 的地址非常接近(相差4个字节,正好是一个 int 的大小),这说明它们在内存里是挨着住的。

5. 函数作为参数传递

在 Python 中,我们经常把函数当参数传,比如 sort(key=len)。C语言也可以!这通常被称为回调函数 (Callback)

虽然函数不是数据,但函数在内存里也有地址(代码存放的地方)。我们可以通过函数指针来传递函数。

想象你有一个万能计算器,你不仅传给它数字,还传给它"怎么算"的方法(加法函数或减法函数)。

ex4_callback.c

// [引用: code_examples/functions/ex4_callback.c]
#include <stdio.h>

// 定义两个简单的数学函数
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }

// 定义一个函数指针类型,名字叫 OperationFunc
// 它代表:接收两个 int,返回一个 int 的函数
typedef int (*OperationFunc)(int, int);

// 这是一个"万能计算器"函数
// 它的第三个参数是一个函数!
// 这意味着你可以把具体的计算逻辑传进来
void calculator(int x, int y, OperationFunc op) {
    int result = op(x, y); // 调用传进来的函数
    printf("计算器结果: %d\n", result);
}

int main() {
    int a = 10, b = 5;

    printf("正在将 'add' (加法) 函数作为参数传递:\n");
    // 把 add 函数像变量一样传进去
    calculator(a, b, add); 

    printf("\n正在将 'sub' (减法) 函数作为参数传递:\n");
    // 把 sub 函数传进去
    calculator(a, b, sub);

    return 0;
}

运行结果

正在将 'add' (加法) 函数作为参数传递:
计算器结果: 15

正在将 'sub' (减法) 函数作为参数传递:
计算器结果: 5

代码解析

  1. 函数指针类型typedef int (*OperationFunc)(int, int); 这行代码定义了一种新的类型叫 OperationFunc。它不是一个整数,而是一个"指向函数的指针",这个函数必须接收两个 int 并返回一个 int
  2. 传递行为:在 calculator(a, b, add) 中,我们把 add 这个函数名直接当参数传了进去。这就像把"加法说明书"交给了计算器。
  3. 动态调用:在 calculator 内部,op(x, y) 这行代码非常神奇。它不知道 op 具体是加法还是减法,它只知道"执行这个操作"。这让我们的 calculator 函数变得非常通用。

6. 函数作为返回值返回

既然可以把函数传进去,自然也可以把函数送出来。 这就像是一个"工厂",你告诉它你要什么模式,它就给你返回一个专门处理该模式的机器(函数)。

ex5_return_func.c

// [引用: code_examples/functions/ex5_return_func.c]
#include <stdio.h>

int add(int a, int b) { return a + b; }
int mul(int a, int b) { return a * b; }

// 定义函数指针类型
typedef int (*OperationFunc)(int, int);

// 这个函数返回一个函数指针!
// 根据输入的模式字符,决定返回加法函数还是乘法函数
OperationFunc get_operation(char mode) {
    if (mode == '+') {
        printf("  [选择器] 你选择了加法模式。\n");
        return add; // 返回加法函数的地址
    } else if (mode == '*') {
        printf("  [选择器] 你选择了乘法模式。\n");
        return mul; // 返回乘法函数的地址
    }
    return NULL;
}

int main() {
    int x = 6, y = 7;
    char mode = '+';

    printf("正在请求操作模式 '%c'...\n", mode);

    // 1. 获取对应的函数
    OperationFunc my_func = get_operation(mode);

    // 2. 调用获取到的函数
    if (my_func != NULL) {
        int result = my_func(x, y);
        printf("计算结果: %d\n", result);
    }

    mode = '*';
    printf("\n正在请求操作模式 '%c'...\n", mode);
    my_func = get_operation(mode);
    if (my_func != NULL) {
        printf("计算结果: %d\n", my_func(x, y));
    }

    return 0;
}

运行结果

正在请求操作模式 '+'...
  [选择器] 你选择了加法模式。
计算结果: 13

正在请求操作模式 '*'...
  [选择器] 你选择了乘法模式。
计算结果: 42

代码解析

  1. 返回策略get_operation 函数根据输入的字符(+*),决定返回哪个函数的地址。这就像一个工厂,你下订单(+),它就给你发一台加法机器。
  2. 安全性:如果输入了不支持的模式,函数返回 NULL。所以在调用前,我们检查了 if (my_func != NULL),这是非常重要的安全习惯,防止程序崩溃。
  3. 灵活性:通过这种方式,我们的主程序逻辑(获取函数 -> 执行函数)和具体的算法实现(加法、乘法)完全解耦了。

总结

C语言的函数非常强大。 1. 基础用法:声明、定义、调用。 2. 参数传递:默认是复制一份(值传递)。 3. 地址:数据的内存门牌号。 4. 高阶用法:函数本身也有地址,可以被传递,也可以被返回。这让C语言拥有了极高的灵活性,是实现复杂系统(如操作系统回调、插件系统)的基础。

分类: C