C 语言控制流与函数
更新时间:2026-08-25。本文是
languages/c/主题入门层第 2 篇。控制流决定"代码怎么执行",函数决定"代码怎么组织"。这两个是写任何 C 程序的骨架,也是理解后续调用约定、栈帧(专家层)的前提。
本文要回答的问题
for/while/do-while三种循环怎么选?循环里的break/continue区别?- 函数传参时,为什么改形参不影响实参?怎么才能让函数"改到"外部的值?
- 局部变量、全局变量、
static变量的生命周期各是什么?
一、分支结构
1.1 if / else if / else
c
if (x > 0) {
/* 正数 */
} else if (x == 0) {
/* 零 */
} else {
/* 负数 */
}坑:if (x = 5) 是赋值不是比较!结果是 5(非零),恒为真。这是 C 最经典的笔误。写成 if (5 == x) 可预防(把常量放左边,写错会编译报错)。
1.2 switch / case
c
switch (c) {
case 'A':
printf("A\n");
break; // 不写 break 会"贯穿"到下一个 case
case 'B':
printf("B\n");
break;
default:
printf("other\n");
}要点:
case后面必须是整型常量(不能是浮点、字符串)。- 忘记
break会"贯穿"(fall-through)——有时是故意的(多个 case 共用处理),但要注释说明。
二、循环结构
2.1 三种循环对比
| 循环 | 适用场景 | 结构 |
|---|---|---|
for | 已知循环次数 | for (初始化; 条件; 步进) |
while | 未知次数,先判断后执行 | while (条件) |
do-while | 至少执行一次 | do {} while (条件); |
c
// for:遍历数组(经典)
for (int i = 0; i < 10; i++) {
printf("%d\n", i);
}
// while:读文件直到 EOF
while ((c = getchar()) != EOF) {
/* 处理字符 */
}
// do-while:至少执行一次
do {
printf("菜单\n");
} while (choice != 0);2.2 break 与 continue
| 语句 | 作用 |
|---|---|
break | 跳出整个循环 |
continue | 跳过本次,进入下一轮 |
c
for (int i = 0; i < 5; i++) {
if (i == 2) continue; // 跳过 i=2
if (i == 4) break; // i=4 时退出
printf("%d\n", i); // 打印 0 1 3
}三、函数
3.1 函数定义与声明
c
// 声明(原型):告诉编译器函数存在
int add(int a, int b);
// 定义:函数体
int add(int a, int b) {
return a + b;
}为什么需要声明:C 编译器从上往下读,调用前必须"见过"函数(声明或定义),否则报 implicit declaration。头文件(.h)就是用来放声明的。
3.2 返回值与 void
c
void print_hello(void) { // 无返回值
printf("hello\n");
}
int get_value(void) { // 返回 int
return 42;
}3.3 值传递 vs 指针传递(重点)
C 函数默认是值传递:形参是实参的副本,函数内改形参不影响外部:
c
void change(int x) {
x = 100; // 改的是副本,外部不变
}
void change_ptr(int *p) {
*p = 100; // 通过指针改外部变量
}
int main(void) {
int n = 1;
change(n); // n 还是 1
change_ptr(&n); // n 变成 100
return 0;
}理解:想让函数修改外部变量,必须传它的地址(指针)。这是 C 与许多高级语言的关键差异,也是理解指针的必经之路。
四、作用域与生命周期
4.1 三类变量对比
| 变量类型 | 作用域 | 生命周期 | 存储位置 |
|---|---|---|---|
| 局部变量 | 函数内({} 内) | 进入函数创建,返回销毁 | 栈 |
| 全局变量 | 整个文件 | 程序开始到结束 | 数据段 |
static 局部 | 函数内 | 程序开始到结束(值保留) | 数据段 |
4.2 static 的两种用法
c
// 1. static 局部变量:值在函数调用间保留
int counter(void) {
static int count = 0; // 只初始化一次
return ++count;
}
// 2. static 全局变量/函数:限制为文件内部可见(隐藏符号)
static int internal_var; // 其他 .c 文件访问不到counter() 第一次返回 1,第二次返回 2…… static 让局部变量"记住"了上次的值。
4.3 返回局部变量地址是错误
c
int *bad(void) {
int x = 42;
return &x; // 错误!x 是局部变量,函数返回后栈帧销毁
}返回后 x 的内存已被回收,指针变成悬垂——这是指针与内存管理里的经典陷阱。
五、递归
c
int factorial(int n) {
if (n <= 1) return 1; // 终止条件
return n * factorial(n - 1); // 递归调用
}要点:
- 必须有终止条件,否则无限递归 → 栈溢出(stack overflow)。
- 每次递归调用占用一层栈帧,深度过大会耗尽栈——与栈内存布局相关。
六、与本站主线衔接
- 函数调用 = 栈帧:函数调用的本质是压栈/弹栈,专家层 内存布局与 ABI 会讲调用约定。
- 分支预测:
if/switch对应 CPU 分支预测,见 L4 分支预测。 - 循环优化:编译器会优化循环,
-O2可能消除空循环,见 demos/cpu-demo。
一句话总结
控制流与函数是 C 的骨架——if (x=5) 是赋值陷阱、break 跳循环 continue 跳本次、函数默认值传递改外部要传指针、局部变量随函数返回销毁;掌握这些,才能写出正确的 C 程序并理解后面的栈帧与调用约定。
上一篇:数据类型与内存表示 下一篇:数组、字符串与结构体