上一章:【C陷阱与缺陷】第4章 连接器
下一章:【C陷阱与缺陷】第6章 预处理器
c语言没有标准输入输出函数,所有的输入输出都需要靠库函数。ANSI C标准也说明了这一点。不过ANSI C标准没提供执行“底层”I/O操作的read/write函数,但是大多数c编译器的实现都包含了这些库函数。
1. 返回整数的"getchar"函数
getchar()
函数一般返回标准输入文件的下一个字符(int
类型),当没有输入时返回EOF
。其声明为:
int getchar();
//getc(stdin);//getchar();相当于这个
下个例子:
#include <stdio.h>
int main() {
char c;
while ((c = getchar()) != EOF)
return 0;
}
可能运行不正常,因为变量c
被声明为char
类型,而不是int
类型。getchar()
返回的结果可能容不下,特别是EOF
,情况如下:
- 某些合法的输入字符被截断后,恰好与
EOF
相同,造成提前结束循环。 c
可能根本取不到EOF
,陷入死循环。- 巧合下能够正常运行,不过是因为
c = getchar()
这语句的操作时使得getchat()
返回值赋值给c
后,丢弃该值,并把getchar()
的返回值作为该语句的结果,这样就是检测getchar()
的返回值与EOF
的比较。这样就恰好能运行。
在很多实现中,getchar()
并不是函数,而是宏实现,这样可以加速调用的速度,putchar()
也一样。当真的使用这些函数时,速度可能会减慢。
2. 更新文件顺序
FILE *fopen(char *address, char *mode)
的第二个参数的打开方式表如下:
| 打开方式 | 说明 | | :------: | :----------------------------------------------------------: | | r | “只读”方式打开,不允许写入。文件必须存在 | | w | “只写”方式打开,不读。如果文件不存在就创建新的文件;如果存在就清空原文件。 | | a | 以“追加”方式打开。若文件存在,则在文件末尾添加,保留原文件;若不存在,则新建一个。 | | r+ | 以“读写”方式打开,可写入也可读取,可以随意更新文件。文件必须存在 | | w+ | 以“写入/更新”方式打开,相当与"r+"与"w",随意更新或读取。如果文件不存在就创建新文件;若文件存在就清空原文件。 | | a+ | 以“追加/更新”打开,相当于"r+"和"a",可读可写,随意更新。若文件不存在就更新;若文件存在就在末尾追加。 | | t | 与打开方式1到6组合,如果没有,默认是t | | b | 与打开方式1到6组合,打开二进制文件 |
这是可读写方式打开一个文件的操作。
struct record rec;
//TODO
FILE *fp;
fp = fopen(file_addr, "r+");
while (fread((char *)&rec, sizeof(rec), 1, fp) == 1) {
/*对rec操作*/
if (/*rec需要重新写入*/) {
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char *)&rec, sizeof(rec), 1, fp);
}
}
但别以为这样就可以自由地交错使用读写操作。这是为了兼容历史问题,在fread
与fwrite
之间并不能自由地切换。在fread
操作与fwrite
操作之间必须使用fseek
函数进行切换。当然,两个fwrite
之间或者两个fread
之间并不需要fseek
。上述程序需要改成这样:
struct record rec;
//TODO
FILE *fp;
fp = fopen(file_addr, "r+");
while (fread((char *)&rec, sizeof(rec), 1, fp) == 1) {
/*对rec操作*/
if (/*rec需要重新写入*/) {
fseek(fp, -(long)sizeof(rec), 1);
fwrite((char *)&rec, sizeof(rec), 1, fp);
fseek(fp, 0L, 1);
}
}
fseek
函数的用法如下:
int fseek(FILE *stream, long int offset, int origin);
//stream: FILE指针。
//offset: 一个long类型变量,为从origin位置移动offset字节。正就向前,负就向后,0就原地
//origin: 0表示文件头(SEEK_SET),1表示当前位置(SEEK_CUR),2表示文件末尾(SEEK_END)。
3. 缓冲输出与内存分配
程序输出有两种方式:1. 即时处理方式;2. 先暂存,然后大块写入方式。
方式2一般通过库函数setbuf
实现,设buf
为一个大小适当的数组,则通过语句
setbuf(stdout, buf);
通知输入/输出库,写入到stdout的输出使用buf作为输出的缓冲区,直到buf缓冲区已满或者调用fflush
(对于写操作打开的文件,调用fflush
会使得缓冲区内容被实际写入到文件中)。buf
缓冲区的大小定义在<stdio.h>
的BUFSIZ
中。下列错误例子:
#include <stdio.h>
int main(void) {
int c;
char buf[BUFSIZ];
setbuf(stdout, buf); //use buffer while output to stdout
while ((c = getchar()) != EOF) {
putchar(c);
}
return 0;
}
之所以错误是因为输出可能并不能放慢缓冲区,而且也没认为调用fflush
,所以在退出main
函数后,虽然会输出缓冲区,但此时buf数组已经被释放了,造成输出错误。解决方法:
- 声明静态缓冲区
c static char buf[BUFSIZ];
- 把
buf
数组的声明放在main
函数外。 - 动态分配
buf
内存。c char *malloc(); setbuf(stdout, malloc(BUFSIZ));
这里并不需要考虑malloc
是否分配成功,因为如果buf
是个NULL
指针,那么stdout
就不需要缓冲直接输出。
4. 使用errno检测错误
与操作系统相关的一些库函数,当执行失败时,会通过一个errno
的外部变量通知程序,函数调用失败。下面为错误例子:
/*调用库函数*/
if (errno) {
/*错误处理*/
}
因为在库函数调用没有失败时,没有强制要求一定设置errno
为0,这样可能会保留前一个失败库函数的errno
导致检测错误。下面这样改也是错误的:
errno = 0;
/*调用库函数*/
if (errno) {
/*错误处理*/
}
因为库函数成功调用,并没有强制使得errno
清零,也没有禁止设置errno
的值。因为i可能会有以下情况:当调用fopen
函数新建一个文件时,fopen
需要调用其他函数检测是否存在同名函数,这时就需要直到被调用的那个函数的errno
的具体值,这样就修改了errno
的值。就算fopen
正常执行并返回,errno
值也可能非0。
5. 库函数signal
所有c语言的实现都包含signal
库函数,这是个捕获异步事件的方式,开头加上:
#include <signal.h>
//调用方法:
signal(signal type, handler function);
signal type
是头文件signal.h
的某些常量,表示signal
函数要捕获的信号类型。handler function
是当指定事件发生时调用的事件处理函数。
因为c语言中,信号是真正的异步,所以信号可能出现在任何时候,比如在malloc
的调用过程中。
1. 当`malloc`的执行过程被信号中断,则`malloc`用于跟踪**内存分配的数据结构**就可能只有部分被更新,此时`signal`函数再调用`malloc`的时候,就会导致该数据结构完全崩溃。
2. `signal`函数使用long jmp退出时通常也是不安全的,因为信号可能在库函数没完成更新某些数据结构时发生,这就会导致更新混乱。
所以signal
函数最好只设置一些标志就返回,让主程序能够检测出标志的改变,发现信号的发生。
- 当程序发生算术运算时发生错误(除零/溢出)时,会产生相应的信号,某些机器在
signal
处理完这些信号时,返回到上一个状态,继续重新执行该算术运算,而且没有一个可移植的方法处理这些操作数,于是导致反复引发同一个信号。对于这个
对于这个错误,signal
函数唯一安全、可移植的操作就是打印出错信息,然后long jmp或者exit
退出程序。
处理信号时,很多操作都是不可移植的。所以最好的办法就是让signal
尽量简单,并组织在一起。
上一章:
DeathWatch:【C陷阱与缺陷】第4章 连接器zhuanlan.zhihu.com下一章:
DeathWatch:【C陷阱与缺陷】第6章 预处理器zhuanlan.zhihu.com