思维导图



一、 函数的基本结构
1.1 从重复劳动中解脱
如果你发现自己在复制粘贴代码,停下来,把它封装成函数。
语法结构:
c
返回值类型 函数名(参数列表) {
// 函数体
return 结果;
}
代码对比:
写法 A(混乱):
c
int main() {
// 计算 3 + 5
int a = 3, b = 5;
int sum1 = a + b;
printf("%d\n", sum1);
// 计算 10 + 20
int c = 10, d = 20;
int sum2 = c + d; // 如果逻辑复杂,这里要复制好多行
printf("%d\n", sum2);
return 0;
}
写法 B(封装函数,清爽):
c
// 定义一个加法工具
int add(int x, int y) {
return x + y;
}
int main() {
printf("%d\n", add(3, 5));
printf("%d\n", add(10, 20)); // 想用几次用几次
return 0;
}
1.2 为什么需要声明?
编译器是从上往下读代码的。如果函数 foo() 的定义写在 main() 后面,而 main() 中调用了 foo(),编译器会困惑:"这是个啥?"
错误示范:
c
int main() {
foo(); // 编译警告:implicit declaration of function 'foo'
}
void foo() {
printf("Hello");
}
正确示范(使用原型):
c
#include <stdio.h>
// 1. 函数声明(原型):告诉编译器 foo 是个啥
void foo(void);
int main() {
foo(); // 2. 函数调用:编译器现在认识 foo 了
return 0;
}
// 3. 函数定义:具体的实现
void foo(void) {
printf("Hello\n");
}
二、 形参、实参与值传递
2.1 核心机制:C 语言只有值传递
形参: 函数定义时的占位符变量(如 x, y)。
实参: 调用函数时传入的具体数值(如 10, 20)。
真理:
调用函数时,计算机会把实参的值拷贝 一份给形参。形参只是实参的副本 。在函数内部修改形参,永远不会影响外面的实参。
2.2 经典案例:Swap (交换) 为什么失败?
这是理解指针前最重要的一课。
错误示范(值传递):
c
void swap_fail(int x, int y) {
int temp = x;
x = y;
y = temp;
printf("[函数内] x=%d, y=%d (换成功了)\n", x, y);
}
int main() {
int a = 10, b = 20;
swap_fail(a, b);
printf("[函数外] a=%d, b=%d (根本没变!)\n", a, b);
}
解析: swap_fail 里的 x 和 y 只是 a 和 b 的复印件。你在复印件上涂改,原件怎么会变呢?
正确示范(模拟指针传递):
注:这里先展示代码,具体原理将在指针章节详解。关键在于传递的是变量的地址 &a。
c
void swap_success(int *x, int *y) {
int temp = *x; // 去地址里取值
*x = *y; // 修改地址里的值
*y = temp;
}
// 调用:swap_success(&a, &b);
三、 返回值设计
3.1 return 的语义
return 有两个作用:
1.带回结果 :给调用者一个交代。
2.立即终止 :函数执行到return会瞬间结束,后面的代码全都不看。
代码示例(利用 return 简化逻辑):
c
// 判断是否为偶数
int is_even(int n) {
if (n % 2 == 0) {
return 1; // 是偶数,直接返回,函数结束
}
// 根本不需要 else,因为如果上面执行了,这里根本不会走到
return 0;
}
3.2 如何返回多个值?
C 语言的 return 只能返回一个值。如果任务需要返回状态码和计算结果怎么办?
方案 A:使用指针参数(推荐)
这是一种非常工程化的写法:函数返回值只代表成功/失败,真正的数据通过指针参数带出来。
c
// 返回值 int 代表状态:1 成功,0 失败
// 真正的计算结果通过指针 result 传出
int safe_divide(int a, int b, double *result) {
if (b == 0) {
return 0; // 失败:除数为0
}
*result = (double)a / b; // 修改外部变量
return 1; // 成功
}
int main() {
double res;
// 这种写法在 C 语言源码中随处可见
if (safe_divide(10, 2, &res)) {
printf("计算成功,结果: %.2f\n", res);
} else {
printf("错误: 除数不能为0\n");
}
}
方案 B:返回结构体
c
typedef struct {
int status; // 0 失败, 1 成功
double value;
} Result;
Result divide(int a, int b) {
Result r = {0, 0.0};
if (b != 0) {
r.status = 1;
r.value = (double)a / b;
}
return r;
}
四、 作用域与静态变量
4.1 局部 vs 全局
代码示例:
c
int g_num = 100; // 全局变量:所有人都能改,危险!
void func() {
int x = 10; // 局部变量:只有 func 能看见
printf("%d", x);
}
int main() {
// printf("%d", x); // 报错!main 看不见 func 里的 x
printf("%d", g_num); // 可以访问全局变量
}
4.2 static 局部变量
这是一个神奇的关键字。它可以让局部变量拥有记忆力。
普通局部变量(健忘):
c
void normal_func() {
int i = 0; // 每次调用都会重新初始化为 0
i++;
printf("%d ", i);
}
// main 中调用 3 次,输出:1 1 1
Static 局部变量(过目不忘):
c
void static_func() {
static int i = 0; // 只在程序启动时初始化一次!
i++;
printf("%d ", i);
}
// main 中调用 3 次,输出:1 2 3
应用场景: 函数调用计数器、生成唯一 ID。
五、 递归 (Recursion)
5.1 递归的本质
函数自己调用自己。
核心口诀: 一定要有一个退出的条件(基准情况),否则就是死循环(栈溢出)。
代码示例:阶乘 (Factorial)
计算 n! = n * (n-1) * ... * 1
c
int factorial(int n) {
// 1. 基准情况 (Base Case):什么时候停?
if (n <= 1) return 1;
// 2. 递归步骤 (Recursive Step):把问题变小
return n * factorial(n - 1);
}
5.2 性能陷阱:斐波那契数列
递归版(反面教材):
c
int fib(int n) {
if (n <= 2) return 1;
return fib(n-1) + fib(n-2); // 极其低效!重复计算了无数次
}
解释: 计算 fib(50) 可能需要几分钟甚至几小时。
循环版(正面教材):
c
int fib_loop(int n) {
if (n <= 2) return 1;
int a = 1, b = 1, temp;
for (int i = 3; i <= n; i++) {
temp = a + b;
a = b;
b = temp;
}
return b; // 瞬间完成
}
六、 练习题
题目 1: 如果一个函数不需要参数,也不返回任何值,它的原型应该怎么写?
题目 2: 下面代码中 change 函数执行后,main 中的 val 是多少?
c
void change(int val) {
val = 100;
}
int main() {
int val = 5;
change(val);
printf("%d", val);
}
题目 3: 想要在函数中修改 main 函数里的 int 变量,参数类型应该是什么?
题目 4: 下列关于 return 的说法错误的是?
A. 函数可以有多个 return 语句。
B. void 函数不能有 return 语句。
C. return 会立即终止函数。
题目 5: 下面这个递归函数有什么问题?
c
void dead_loop(int n) {
printf("%d ", n);
dead_loop(n - 1);
}
题目 6: 局部变量如果不初始化,它的默认值是多少?
题目 7: 全局变量如果不初始化,它的默认值是多少?
题目 8: static 局部变量如果不初始化,它的默认值是多少?
题目 9: 下面代码会发生什么严重问题?
c
int* get_addr() {
int x = 10;
return &x; // 返回局部变量的地址
}
题目 10: 编写一个函数 max3,接收三个整数,返回其中的最大值。
题目 11: 计算 1+2+...+n。写出递归版的关键逻辑 sum(n)。
题目 12: C 语言支持函数重载(同名不同参,如 add(int) 和 add(float))吗?
题目 13: 将数组传递给函数时,实际上传递的是什么?
题目 14: 函数 int func(void) 和 int func() 在 C 语言(C99前)中有区别吗?
题目 15: 编写一个函数 is_prime(int n),判断 n 是否为素数,返回 1 或 0。
七、 解析
题 1 解析
答案: void func_name(void);
详解:
void在返回值位置表示"无返回值",在参数位置表示"不接受任何参数"。
题 2 解析
答案: 5。
详解:
值传递。
change函数修改的只是val的副本(复印件),不影响外面的val(原件)。
题 3 解析
答案: int* (整型指针)。
详解:
只有拿到地址,才能通过解引用修改原来的内存。
题 4 解析
答案: B。
详解:
void函数可以使用return;(不带值)来提前结束函数执行,这是合法的。
题 5 解析
答案: 缺少终止条件(Base Case)。
详解:
这会导致无限递归,最终耗尽栈内存,引发 Stack Overflow(栈溢出)崩溃。
题 6 解析
答案: 垃圾值 (Undefined)。
详解:
栈上的局部变量内存如果不手动清理,里面存的就是上次遗留的数据,可能是任何值。
题 7 解析
答案: 0。
详解:
全局变量存储在静态数据区(BSS段),编译器保证它们默认初始化为 0。
题 8 解析
答案: 0。
详解:
同上,
static变量具备静态存储期,也会自动初始化为 0。
题 9 解析
答案: 悬空指针 (Dangling Pointer)。
详解:
致命错误! 函数结束后
x所在的栈内存会被回收。返回的指针指向了一个"已经销毁"的区域,访问它会导致未定义行为。
题 10 解析
答案:
c
int max3(int a, int b, int c) {
int max = a;
if (b > max) max = b;
if (c > max) max = c;
return max;
}
题 11 解析
答案: return n + sum(n-1);
详解:
还要加上基准条件:
if (n==1) return 1;。
题 12 解析
答案: 不支持。
详解:
C 语言中函数名是唯一的标识符,不能重名。这是 C++ 的特性。
题 13 解析
答案: 数组首元素的地址。
详解:
数组作为参数时会退化为指针。传递数组并没有拷贝整个数组的数据,而是传递了地址(值传递的一种特殊情况,传递的是地址的值)。
题 14 解析
答案: 有区别。
详解:
int func(void)明确表示不接受任何参数 。
int func()在旧标准中表示参数个数不确定(为了兼容老代码,但不推荐使用)。
题 15 解析
答案:
c
int is_prime(int n) {
if (n <= 1) return 0;
for (int i = 2; i * i <= n; i++) {
if (n % i == 0) return 0; // 发现因子,不是素数
}
return 1; // 是素数
}

日期:2025年2月10日
专栏:C语言