14.3.1 终止程序
C 标准库提供了多种终止程序的方式。它们的区别在于是否调用已注册的回调,以及是否刷新并关闭打开的流。
1. 正常终止:return 与 exit
从 main 返回,等价于调用 exit(status)。exit 会执行正常收尾流程:逆序调用 atexit 注册函数、刷新并关闭标准 I/O 流,并完成标准规定的其他终止清理步骤。只要程序需要“按常规流程退出”,这是首选路径。
#include <stdio.h>
#include <stdlib.h>
static void on_exit_1(void) { puts("on_exit_1"); }
static void on_exit_2(void) { puts("on_exit_2"); }
int main(void) {
if (atexit(on_exit_1) != 0 || atexit(on_exit_2) != 0) {
return EXIT_FAILURE;
}
return EXIT_SUCCESS;
}2
3
4
5
6
7
8
9
10
11
12
两个回调都注册成功时,输出:
on_exit_2 on_exit_1
上例中回调按注册逆序执行,输出顺序是 on_exit_2 后 on_exit_1。
2. 快速终止:quick_exit 与 at_quick_exit
quick_exit(C11 起)用于尽快结束程序,只做一小部分收尾。它只调用 at_quick_exit 注册的回调,不调用 atexit 回调,也不要求执行完整的流刷新和关闭。程序需要有限的清理、又不想走完整的退出流程时,可以使用它。
3. 立即终止:_Exit 与 abort
_Exit 会直接终止进程,不执行已注册回调;abort 会导致异常终止并触发 SIGABRT。这两条路径都不应被当成常规业务分支使用,它们更适合处理不可恢复错误或运行时严重故障。
4. 习题
阅读下面的程序,实际编译运行,回答四个问题。每题都要写出完整输出和进程的退出状态。
#include <stdio.h>
#include <stdlib.h>
static void first(void) {
puts("first");
}
static void second(void) {
puts("second");
}
static void give_up(void) {
puts("give up");
exit(2);
}
int main(void) {
if (atexit(first) != 0 || atexit(second) != 0) {
return EXIT_FAILURE;
}
puts("start");
give_up();
puts("never");
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
- 原样运行,输出是什么,退出状态是多少?
- 把
give_up中的exit(2)换成_Exit(2),输出与退出状态如何变化? - 换成
quick_exit(2)呢? - 把
exit(2)换成return;,让give_up正常返回呢?
编译命令:gcc -std=c17 -Wall -Wextra term.c -o term。
下面的程序用编译选项 MODE 选择终止方式。补全两个清理函数,并增加 MODE == 4 的分支(调用 abort()),使五种模式的行为如下:
MODE为 0 和 1:执行full_cleanup,它向标准错误写atexit handler,向run.log写入full cleanup后关闭文件。MODE为 2:执行quick_cleanup,它向标准错误写at_quick_exit handler,向run.log写入quick cleanup,刷新并关闭文件。MODE为 3 和 4:不执行任何清理。
#include <stdio.h>
#include <stdlib.h>
#ifndef MODE
#define MODE 0
#endif
static FILE *log_file;
static void full_cleanup(void) {
/* 补全 */
}
static void quick_cleanup(void) {
/* 补全 */
}
int main(void) {
log_file = fopen("run.log", "w");
if (log_file == NULL) {
return EXIT_FAILURE;
}
if (atexit(full_cleanup) != 0 || at_quick_exit(quick_cleanup) != 0) {
fclose(log_file);
return EXIT_FAILURE;
}
fputs("work done\n", log_file);
fflush(log_file);
#if MODE == 1
exit(3);
#elif MODE == 2
quick_exit(4);
#elif MODE == 3
_Exit(5);
#endif
return 0;
}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
28
29
30
31
32
33
34
35
36
37
用 gcc -std=c17 -Wall -Wextra -DMODE=0 terminate_lab.c -o terminate_lab 依次编译运行五种模式,记录每次的标准错误输出、run.log 的内容和退出状态,并解释 MODE 3 与 4 的清理函数为什么一次都没有被调用。MODE 4 的退出状态由平台决定,只需说明它不是 0 且程序异常结束。
最后做一组对照:删掉 main 里那句 fflush(log_file),重新跑 MODE 1 和 MODE 3,记录 run.log 的内容。标准只要求 exit 路径刷新并关闭全部流,_Exit 不做任何刷新;如果你的实现仍然把 MODE 3 的数据写了出来,说明它在标准要求之外多做了一步。答案里写清你观察到的是哪一种,并说明为什么写法上不应依赖 _Exit 帮你刷新。
下面的程序想把记录条数写进日志,但运行后 app.log 里只有三行 record,没有 records: 3。找出原因并修正。
#include <stdio.h>
#include <stdlib.h>
static FILE *log_file;
static int records;
static void close_log(void) {
if (log_file != NULL) {
fclose(log_file);
log_file = NULL;
}
}
static void write_summary(void) {
if (log_file != NULL) {
fprintf(log_file, "records: %d\n", records);
}
}
int main(void) {
log_file = fopen("app.log", "w");
if (log_file == NULL) {
return EXIT_FAILURE;
}
atexit(write_summary);
atexit(close_log);
for (int i = 0; i < 3; ++i) {
fputs("record\n", log_file);
++records;
}
return EXIT_SUCCESS;
}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
28
29
30
31
32
- 说明两个回调实际执行的先后顺序,以及
write_summary为什么什么也没有写。 - 只调整两行
atexit调用的顺序,使日志末尾出现records: 3,给出修正后的完整app.log。 - 修正后把
main的return EXIT_SUCCESS改成exit(EXIT_FAILURE),日志内容和退出状态分别是什么?这说明return与exit在本程序里是什么关系。
实现一个小型清理栈,把需要收尾的动作集中登记一次,使正常返回与 quick_exit 两条路径执行同一组清理:
int cleanup_push(void (*fn)(void));要求:
- 最多登记 8 个函数;
fn为空或已满时返回 -1,成功返回 0。 - 第一次成功登记时,把内部调度函数同时注册到
atexit与at_quick_exit;任一注册失败时cleanup_push返回 -1,不登记函数。 - 调度函数按登记顺序的逆序调用各函数,调用后清空计数,使重复退出不会执行两次。
用下面的 main 验收:
static void close_a(void) { puts("close A"); }
static void close_b(void) { puts("close B"); }
static void close_c(void) { puts("close C"); }
int main(void) {
if (cleanup_push(close_a) != 0 || cleanup_push(close_b) != 0 ||
cleanup_push(close_c) != 0) {
return EXIT_FAILURE;
}
puts("work");
return EXIT_SUCCESS;
}2
3
4
5
6
7
8
9
10
11
12
运行两次:第一次原样,第二次把最后的 return EXIT_SUCCESS; 换成 quick_exit(0);。两种情况的输出都应当是:
work
close C
close B
close A2
3
4
写出完整程序,说明调度函数为什么只在第一次 cleanup_push 时注册,而不是每次登记都注册一遍;以及在 quick_exit 路径上 atexit 注册的函数为什么不会参与清理。