一 概述
二 类型
三 语句
四 函数
五 数据
六 内存
七 代码
附录
3. 控制流
语句(statement)以分号结束,执行一到多个动作。多条语句以 大括号包含 构成复合语句块(block)。按顺序执行,或循环、跳转。
块中声明的名字具有块作用域,仅在块内可见,遮蔽外部同名对象。
块内自动变量生命周期仅限于当前块,除非是静态(static)或外部(extern)引入。
选择
根据条件表达式,选择性执行子语句。最多只有一个分支命中执行。
int main (void) { int x = 10; char r; if (x > 10) { r = 'a'; } else if (x > 5) { r = 'b'; } else if (x > 3) { r = 'c'; } else { r = '0'; } printf("%c\n", r); return 0; }
如果不使用大括号语句块,那分支仅含其后一条语句。为避免误会,通常和 if 写成一行。
int main (void) { if (argc > 1) // { printf("%d\n", argc); // } 仅此行! printf("%s\n", argv[1]); // 并不属于 if 语句块。 return 0; }
note: ...this statement, but the latter is misleadingly indented as if it were guarded by the 'if'
如果是无大括号嵌套,则 else 分支属于最后一个 if。有鉴于此,还是以大括号规避歧义为佳。
int main (int argc, char *argv[]) { if (argc > 1) // { if (strcmp(argv[1], "1") == 0) puts("ok"); else puts("error"); // } return 0; }
warning: suggest explicit braces to avoid ambiguous 'else' [-Wdangling-else]
switch
条件表达式是整数类型。
分支必须以 break 跳出,否则在不匹配后续 case 的情况下贯穿执行下一语句块。
int main (int argc, char *argv[]) { if (argc < 2) return -1; char c = argv[1][0]; switch (c) { case 'a': // 如果匹配,因为没有 break case 'A': // | puts("A"); // <-----------------+ break; // 如果没有 break // | case 'b': // | case 'B': // | puts("B"); // <----------+ break; default: puts("error"); } return 0; }
可用 -Wimplicit-fallthrough
参数,让编译器对 break 缺失作出警告。
虽然 defautl 可选,且编译器确保它是最后选择。
但其代码位置如果不在最后,也须添加 break。为避免调整代码导致意外错误,总是为其添加 break 更好。
warning: this statement may fall through [-Wimplicit-fallthrough=] | puts("A"); | ^~~~~~~~~
另外,case 常量值必须唯一。比如,不能同时出现 'A'
和 65
。
error: duplicate case value | case 65 : | ^~~~ note: previously used here | case 'A': | ^~~~
如要在 case 块定义或声明变量,则须额外启用一个内嵌块。
switch (c) { case 'a': case 'A': // { int x = 100; printf("%d\n", x); // } puts("A"); break; }
error: a label can only be part of a statement and a declaration is not a statement | int x = 100; | ^~~
循环
三种循环语句:
while
:先判断条件,再决定是否执行循环。do ... while
: 先执行一次,再判断条件是否继续。for
: 三个表达式,分别用于初始化、控制和调整。
int x = 4; while (x--) { printf("%d\n", x); }
while (1) { // 或 true,无限循环。 puts("a"); }
相比较 while,do...while 有一些特定使用场合。
比如,IO 操作时,总是先尝试读取一次。如果成功,则继续循环操作。
int x = 4; do { printf("%d\n", x); } while (--x > 0);
在 for 三个表达式里:
- 初始化 (initialization):在控制表达式前,仅执行一次。
- 控制 (controlling):每次循环前测试,决定是否执行。
- 调整 (adjustment):每次循环后,控制表达式再次测试前执行。(调整计数器)
for (int i = 0; i < 5; i++) { printf("%d\n", i); }
变形用法:
for (int x = 0, y = 0; x < 5; x++, y--) { // 多个初始化和调整变量。 printf("%d: %d\n", x, y); }
for (;;) { // 无限循环。 }
for (; cond;) { // 等同 while (cond) }
跳转
无条件跳转语句。
goto <label>
: 跳转到所在函数内任意标签位置。continue
: 跳过后续代码,进入下一次循环。break
: 终止 switch 或循环语句执行。return
: 终止当前函数执行。
滥用 goto 会导致代码难以维护,比如像下面这样跳转到一个循环块内部。
对 goto 的口诛笔伐由来已久(1968, Edsger Dijkstra),但在系统层面却可以看到很多 goto 使用案例。
比如:Linux kernel、CPython interpreter、Go runtime。相比其他的设计模式,goto 简单而高效。
标签可以与变量同名,可放在任意语句之前。一个语句可以有多个标签。
int main (void) { goto a; while (1) { a: break; } return 0; }
用 goto/label
配合,实现错误处理案例。
int do_something(void) { f1 = fopen("a", "w"); if (f1 == NULL) goto ERR_F1; f2 = fopen("b", "w"); if (f2 == NULL) goto ERR_F2; m = malloc(10); if (m == NULL) goto ERR_MEM; free(obj); return 0; ERR_MEM: // 错误处理,注意次序。 fclose(f2); ERR_F2: fclose(f1); ERR_F1: return -1; }
注意:break/continue
无法配合 label
跳转嵌套循环,可用 goto
或标志变量代替。
int main (void) { for (int x = 0; x < 6; x++) { // 不能放在此处,那会重启 for.x for (int y = 0; y < 3; y++) { printf("%d:%d ", x, y); goto a; // continue outer } printf("\n"); // 可以放在 printf 前, a: ; // 或这里(do nothing)! } // label 后面应该是语句,而不是 "}" 。 return 0; }
longjmp
跨函数跳转,谨慎使用。
setjmp
: 首次执行,保存上下文,供 longjmp 跳回使用。longjmp
: 基于上下文跳转,向 setjmp 传值。所在函数不会返回。
#include <stdio.h> #include <stdlib.h> #include <setjmp.h> jmp_buf buf; void test () { longjmp(buf, 10); // no return. } int main (void) { int ret = setjmp(buf); // 首次调用保存上下文,返回 0。 if (ret != 0) { // longjmp 跳转回来,返回其所传值。 printf("long jump: %d\n", ret); return 1; } puts("exec longjmp."); test(); return 0; }
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论