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语言拥有了极高的灵活性,是实现复杂系统(如操作系统回调、插件系统)的基础。

Category: C
Category
Tagcloud
Hack RTL-SDR Lens SKill Book GPT-OSS Discuss Poem OpenWebUI Mac Conda FckZhiHu Python Junck Translate Data Virtual Machine OpenCL Communicate Shit Kivy IDE Software VTK Tape Programming MayaVi Chat Memory Cursor Learning Remote VM Prompt Nvidia Photo Code Game Hardware Raspbian AI,Data Science Science Microscope GlumPy 耳机 LTO Story Qwen3 Geology LlamaFactory Code Generation Radio C Windows Camera HBase QGIS Tools Ollama Windows11 Agent Hadoop Linux FuckZhihu n8n Translation ML Ubuntu AI CUDA PVE Video GIS RaspberryPi Photography ChromeBook History Hackintosh Virtualization PyOpenCL AIGC Mount&Blade 音频 SandBox PHD VirtualMachine TUNA Pyenv Visualization Tool macOS NixOS FuckChunWan Scholar 蓝牙 Life LLM University QEMU LTFS