14.3 程序支持
本节介绍三组与程序运行直接相关的标准库能力:结束程序、访问执行环境,以及在函数调用之间进行非局部跳转。
1. 本节内容
- 14.3.1 终止程序:比较从
main返回以及调用exit、quick_exit、_Exit、abort时的不同效果。 - 14.3.2 环境访问:使用命令行实参、环境表项和
system。 - 22.2 跳转:理解
setjmp与longjmp的语义和限制。
先学习各种终止方式会执行哪些清理,再阅读环境访问。setjmp 和 longjmp 会绕过通常的函数返回路径,应在掌握普通控制流后再学习。
2. 习题
下面的程序把命令行参数当作自己的终止状态:
#include <stdlib.h>
int main(int argc, char *argv[]) {
if (argc != 2) {
return EXIT_FAILURE;
}
return atoi(argv[1]);
}2
3
4
5
6
7
8
编译后分别用 0、1、255、256、-1、300 运行,每次运行后立刻在 shell 里查看状态:bash 用 echo $?,cmd 用 echo %ERRORLEVEL%。把六次结果列成表。
- 哪些结果与原参数值相同,哪些不同?不同的那几个说明终止状态只能表达多大的范围?
EXIT_SUCCESS与EXIT_FAILURE分别保证是什么值?为什么程序互操作时应当只用这两个宏?main的返回值经过什么路径变成父进程能看到的状态?和exit(atoi(argv[1]))相比,这里用return有区别吗?
写一个按行统计文件的工具 count,用 atexit 在程序结束时打印汇总:
$ ./count f1.txt f2.txt f3.txt
f1.txt: 3
f2.txt: 2
f3.txt: 0
summary: 3 files, 5 lines2
3
4
5
要求:
- 每个命令行参数是一个文件名,用
fopen以读方式打开,逐个字符读到EOF统计换行符个数;最后一行没有换行符时也算一行。 - 每个文件统计完立即关闭,并把行数加到全局汇总里。
- 某个文件打不开时,用
perror输出原因,跳过它继续处理后面的文件,最终退出状态为 1;全部成功时退出状态为 0。 - 汇总行由
atexit登记的回调打印,这样中途return或exit都能保证它出现。注意回调的签名是void (*)(void),它拿不到参数,累计量只能放在文件作用域的对象里。 - 一个参数都没有时,向标准错误打印
usage: count FILE...并返回 2,此时不打印汇总行。
测试数据:
f1.txt 的内容为三行,每行以换行符结尾;
f2.txt 的内容为两行,最后一行没有换行符;
f3.txt 是空文件。2
3
验收:
./count f1.txt f2.txt f3.txt输出上面四行,退出状态 0。./count f1.txt missing.txt f2.txt在标准错误上出现missing.txt: No such file or directory,标准输出仍有f1.txt: 3、f2.txt: 2与汇总行summary: 2 files, 5 lines,退出状态 1。./count不打印汇总行,退出状态 2。
最后回答:把 atexit(summary) 挪到参数个数检查之前,./count 的输出会变成什么?为什么把注册放在检查之后就避免了这种情况?
写两个程序(也可以合成一个,用第一个参数区分):
child:把命令行给的整数原样作为main的返回值;parent:用system启动child,打印system返回的值。
用 11410 那张表中的六个参数值各跑一遍 parent,把 parent 打印的 system 返回值与 11410 记录的 shell 状态并排列出。
然后回答:
- 两列在你的平台上是一致还是不一致?如果不一致,
system的返回值是 shell 状态左移了若干位,还是另外编码过? system的返回值编码方式由谁决定?标准对它作了统一规定吗?- 写
system(cmd) == 3来判断“子进程以状态 3 结束”的可移植性如何?要可移植地判断成功,应该怎么写? - 把
system的参数换成NULL时它返回什么、含义是什么?在你的程序里何时应当先查一次system(NULL)?
下面的程序把标准输出设为全缓冲(setvbuf 的用法见 13.5 其他文件操作),先记一行日志,然后用断言检查一个在发布版本中会被移除的前提:
#include <assert.h>
#include <stdio.h>
int main(void) {
static char buf[8192];
setvbuf(stdout, buf, _IOFBF, sizeof buf);
puts("starting");
int n = 0;
assert(n > 0);
puts("finished");
return 0;
}2
3
4
5
6
7
8
9
10
11
12
13
(1) 原样编译运行,把标准输出重定向到文件,记录文件内容、标准错误上的内容与退出状态。
(2) 用 -DNDEBUG 重新编译运行,记录三项结果。说明断言被移除后,程序走了哪条路径。
(3) 解释为什么文件里看不到 starting:断言失败时走的是哪条终止路径,它是否刷新 buf 里的数据?
(4) 不改断言,让 starting 在断言失败前一定落到文件里。给出修改后的程序,并说明你把 fflush 放在哪一行、为什么这样放。
(5) 按 14.1 的说法,n > 0 这种由外部输入决定的条件该不该用 assert?如果 n 来自命令行,程序应该怎么改?