前面咱们已经学过了基础数据类型以及数组和字符串,也介绍了输入输出和控制流。这次咱们就来了解一下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 更便宜!
代码解析
- 流程控制:程序从
main开始。当执行到calculate_unit_price(price_a, volume_a)时,程序暂停main中的执行,跳到calculate_unit_price函数内部。 - 参数传递:
price_a(2.0) 和volume_a(300) 的值被复制给了函数里的price和volume。 - 返回值:函数计算出
0.0066...,通过return语句把这个结果扔回给main,并赋值给unit_price_a。 - 复用性:我们写了一次计算逻辑,用了两次(分别计算 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
代码解析
- 复印件效应:当调用
modify_value(my_num)时,C语言把my_num(5) 复印了一份给变量v。函数里把v改成了 999,这完全不影响外面的my_num。这就解释了为什么"回到 main 函数,数值依然是 5"。 - 正确的交互方式:如果你想从函数里得到结果,应该使用 返回值 (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) 是挂在门上的门牌号。
(注意:每次运行你的程序,地址可能都会变,因为操作系统分配给程序的内存位置是不固定的)
代码解析
&运算符:这是"取地址"(Address-of)运算符。&house_a的意思就是"告诉我变量house_a在内存里的地址"。%p格式符:这是printf专门用来打印指针(地址)的格式。输出通常是十六进制的(带有0x或者只是数字),代表内存中的字节编号。- 内存布局:你可以看到
house_a和house_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
代码解析
- 函数指针类型:
typedef int (*OperationFunc)(int, int);这行代码定义了一种新的类型叫OperationFunc。它不是一个整数,而是一个"指向函数的指针",这个函数必须接收两个int并返回一个int。 - 传递行为:在
calculator(a, b, add)中,我们把add这个函数名直接当参数传了进去。这就像把"加法说明书"交给了计算器。 - 动态调用:在
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
代码解析
- 返回策略:
get_operation函数根据输入的字符(+或*),决定返回哪个函数的地址。这就像一个工厂,你下订单(+),它就给你发一台加法机器。 - 安全性:如果输入了不支持的模式,函数返回
NULL。所以在调用前,我们检查了if (my_func != NULL),这是非常重要的安全习惯,防止程序崩溃。 - 灵活性:通过这种方式,我们的主程序逻辑(获取函数 -> 执行函数)和具体的算法实现(加法、乘法)完全解耦了。
总结
C语言的函数非常强大。 1. 基础用法:声明、定义、调用。 2. 参数传递:默认是复制一份(值传递)。 3. 地址:数据的内存门牌号。 4. 高阶用法:函数本身也有地址,可以被传递,也可以被返回。这让C语言拥有了极高的灵活性,是实现复杂系统(如操作系统回调、插件系统)的基础。
CycleUser