goto
goto 会把控制流跳转到同一函数内的某个标号。它功能强,但应谨慎使用。现代 C 工程里,goto 最常见、也最合理的用途是统一错误清理路径。
1. 基本语法
goto label;
/* ... */
label:
statement;2
3
4
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
标号具有函数作用域,但跳转目标必须在同一函数内。
2. 典型清理模式
#include <stdio.h>
#include <stdlib.h>
int work(void) {
FILE *fp = NULL;
int *buf = NULL;
int rc = -1;
fp = fopen("data.txt", "r");
if (fp == NULL) {
goto out;
}
buf = malloc(1024 * sizeof *buf);
if (buf == NULL) {
goto out;
}
rc = 0;
out:
free(buf);
if (fp != NULL) {
fclose(fp);
}
return rc;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
这类写法比多层 if 嵌套更容易保证“每条失败路径都能完整释放资源”。
3. 使用边界
不要把 goto 当作任意流程控制工具,尤其不要跨越对象初始化语义造成难以推断的状态。若结构化语句已经足够表达意图,优先使用结构化语句。
4. 标签组织建议
当函数存在多段清理逻辑时,标签可按资源释放顺序命名,例如 out_free_buf、out_close_fp、out。这种命名让读者不用来回追踪也能看懂跳转目的,同时减少未来增删资源时的出错概率。
5. 与可变修改类型的约束
goto 还有一个容易忽略的语义边界:不能从某个作用域外部跳入“声明了可变修改类型对象”的内部作用域。遇到这类结构时,应改写控制流或调整作用域边界,而不是强行跳入。
6. 前向跳转优先
在清理路径场景里,goto 通常采用“向前失败跳转到函数尾部”模式。相较于在函数内部来回跳转,单向前跳更容易验证路径完整性,也更不容易引入隐藏状态。
7. 清理标签应保持幂等
统一失败出口通常会执行“可能已经执行过,也可能还没执行”的释放逻辑,因此标签后的清理代码应尽量写成幂等形式:free(NULL) 允许直接调用,文件句柄在关闭前先判空,状态位在释放后及时复位。这样一来,不同失败路径复用同一标签时更不容易出现二次释放或漏释放。
#include <stdio.h>
#include <stdlib.h>
int load_all(void) {
FILE *fp = NULL;
char *buf = NULL;
int rc = -1;
fp = fopen("a.txt", "rb");
if (fp == NULL) {
goto out;
}
buf = malloc(4096);
if (buf == NULL) {
goto out;
}
rc = 0;
out:
free(buf);
if (fp != NULL) {
fclose(fp);
}
return rc;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
运行结果:该代码块主要用于语法或结构说明,单独运行通常无终端输出。
8. 让跳转语义可检验
goto 的风险不在语法本身,而在“路径是否还能被读者快速检验”。如果每个 goto 都指向少量、命名清晰的尾部标签,且标签顺序与资源获取顺序一致,那么控制流就仍然是可追踪的。反过来,若同一函数存在大量双向跳转或跨很远距离的回跳,就应考虑把逻辑拆成更小的函数,再用 return 组合。