快乐Linux —— 8.3 文件库函数IO & 常见问题
参考:
https://blog.51cto.com/wodesteve/1306629
http://www.ruanyifeng.com/blog/2006/04/post_213.html 换行与回车
http://bbs.chinaunix.net/thread-889260-1-1.html signed char 与 char 类型
简述
在进行文件操作时,经常遇到一些奇怪的问题,在这里总结总结常见问题并探究其原因。
不同的读写函数
文本文件读写函数
-
int fgetc(FILE *stream);
从文件中以 unsigned char 读取每个字符,并将它转化为 int 型返回。当到文件末尾时或读取错误时,返回 EOF
-
char *fgets(char *buff, int maxcount, FILE *stream);
从文件读取字符串到 buff ,遇到以下三种情况停止:
- 遇到换行时,读取换行符后停止,末尾添加 \0 返回 buff。
- 到达文件末尾
- 若已读取到字符,则末尾添加 \0 返回buff。
- 若没有读取到字符,则返回 NULL。
- 读取到了maxcount-1个字符,末尾添加 \0 返回 buff。
也就是说 fgets 会自动为读取的字符串后加 \0 ,如果有字符序列 ‘A’ ’B’ ‘\0’ ,读取出来末尾会有两个 ‘\0’。第二个参数maxcount 可以放心的设置为缓冲区的大小。
-
int fscanf(FILE *stream, const char *format, ...);
成功时返回匹配成功的数量,失败返回EOF。
-
int fputc(int ch, FILE *stream);
把int 类型转化为 unsigned char 再写入流中。
成功时返回值是把 unsigned char 转化为 int。失败返回EOF
注意以下的坑:
-
int fputs(const char *str, FILE *stream);
遇到 str 中的 \0 停止,不把写入流中。
成功时返回非负数,失败返回EOF
-
int fprintf(FILE *stream, const char *format, ...);
同样遇到 \0 停止, 不把它写入流中.
成功时返回写入流中的字节数,失败时返回负数.
二进制文件读写函数
-
size_t fread(void *buff, size_t element_size, size_t element_num, FIEL *stream);
参数分别是是 :获取输入的地址,一个元素的大小,元素数量,流。
返回值是:成功读取到的元素个数。
手册上有这么一句话:fread() does not distinguish between end-of-file and error, and callers
must use feof(3) and ferror(3) to determine which occurred.意思是fread 不能区分 文件末尾 和 错误,必须和 feof 和 ferror相结合使用。
-
size_t fwrite(void *buff, size_t element_size, size_t element_num, FIEL *stream);
参数分别是是 :输出内容的地址,一个元素的大小,元素数量,流。
返回值是:成功输出的元素个数。
文件操作模式
FILE *fopen(const char *path, const char *mode);
FILE *freopen(const char *path, const char *mode, FILE *stream);
成功返回文件指针,失败返回NULL.
先直接上结论:以文本文件或二进制文件不同形式打开可以混用读写函数。
区别在于当使用文本文件形式打开时。
- 写模式 \n 系统自动将其转化为 \r\n 。
- 文本读模式 \r\n 自动转化为 \n 。
不同类型的读写函数是可以混用的,不过要小心某些文本形式读取函数对\n读取的匹配.
要小心某些文本文件读取函数的机制,例如下面。文件内容没有改变,只是读取方式改变了。
结果不同是因为 sacnf 本身的工作方式就是跳过\n \t 空格等。具体可以搜索scanf 工作原理。
打开方式影响系统对 \n 的处理而对操作函数没有限制.
\r(line feed) 回车 ascii 13 \n (carriage return) 换行 ascii 10
平常我们输出 \n 时,系统自动将 \n 转化为 \r\n.
你可以用 putchar('\n') 和 putchar('\r') 观察它们的输出 .你会发现 , 实际上stdout也是对 \n 进行处理 , 转化为 \r\n.
更多关于换行和回车的故事: http://www.ruanyifeng.com/blog/2006/04/post_213.html
在linux平台下有fopen手册有这样一句话:This is strictly for compatibility with C89 and has no effect; the 'b' is ignored on all POSIX conforming systems, including Linux.
意思是在打开模式中的b是为了与c89兼容,而所有兼容POSIX的系统,linux,这个b会被忽略。也就是说linux下打开文件只有一种模式,也就是二进制模式,不对文件进行任何多余的操作。
文件结束
#define EOF (-1)
首先需要澄清一点,并不是文件末尾都有 EOF 标记,以 EOF 标记判断是否到达文件末尾有时会得到错误的结果。
其次,定义变量,如果前面没有 unsigned 则一般编译器都默认为 signed 类型。后文为方便起见,也就省略了signed 详细见 http://bbs.chinaunix.net/thread-889260-1-1.html
先看一种常见的错误。
char ch;
while((ch=fgetc(fp))!=EOF)
{
putchar(ch);
}
以上的错误,当读写文本文件时一般不会出现,但当读写二进制文件时很可能会出错。
具体来说,当读写二进制文件 fgetc 读取字符 0xff 返回 0x000000ff 被截断为 0xff 赋值给 signed char 类型,然后与EOF 判断会得到 false 结果,也就是误以为到达文件结尾。下面就这个判断过程进行解析。
( ch = fgetc(fp) ) != EOF
-
ch = fgetc(fp)
当读取 0xff 这个字符,fgetc 以 unsigned char 类型获取这个字符,将其转化为 int 作为返回值。unsigned char 扩展为 int型,前面加0,也就是最终返回 int 类型 0x000000ff 。然后再把其截断赋值给 char 类型,所以 ch 里面保存 0xff 。
-
ch != EOF
char 类型与 EOF 进行比较,因为EOF被定义为 -1 所以是 int 类型。将char 扩展为 int ,属于signed 类型扩展,前面加符号位 也就编程 0xffffffff 再与 -1 进行比较,这两个是相同的,程序错误认为到达文件末尾。
-
当真正到达文件末尾,fget 函数会返回 -1
关于为什么 -1 就是 signed int 类型, 这块有个 字面值常量 的知识点在这里简单说说。
其实一般的数字,默认都是signed int,例如 12,012,0x12等
unsigned 类型是末尾加u或U 例如 unsigned int 12u ,012u, 0x12u
long 类型是末尾加l或L 12L;
12ul -> usigned long 类型。
从上面的分析中我们可以预测一下可能出错的场景。
-
我们知道一般能显示的 英文文本文件其 ascii 字符范围是 0x0~0x7f 没有所以不会出现问题。
-
而二进制文件 字符范围是从 0~255 也就是 0x0~0xff 文件字符数一多,极有可能包含 0xff 所以有时会出错。
-
一个中文字符是由两个字符编码的,其字符范围 0x0~0xff,所以中文文本文件有可能也出错。
解决这个问题至少有一种办法:
-
将char 改成 int
这样在比较时直接是 0x000000ff 与 EOF 比较不会出现错误认为到达文件末尾。
将char 改成 unsigned char 也是错误的,而这个错误原因是不能识别文件结束,也就是说循环永真。
int feof(EILE * _stream);
feof 检测文件末尾指示器是否被设置,如果设置了,则返回非0值,否则返回0;
像下面这种程序一般都会出现多读一个 -1 的问题
具体原因是,当读取文件中最后一个字符数,fgetc 读取成功所以没有设置文件末尾指示器,所以feof 返回0.继续执行读取错误设置文件末尾指示器,返回-1给ch,然后feof 返回非0值。
读取不正确
每个流都有与之相关的两个指示器: 错误指示器(error indicator), 文件末尾指示器(end-of-file indicator)。当打开流时会清除这些指示器,读取不正确时设置相应的指示器。
当读取不正确,一般有三种情况:
- 读取失败 设置错误指示器
- 匹配失败 不设置任何指示器
- 已到达文件末尾 设置文件末尾指示器
相应的函数
int feof(FILE *stream);
检查文件是否到达文件末尾,若到达返回非0,否则返回0
int ferror(FILE *stream);
检查是否流错误,若错误返回非0,否则返回0
clearerr(FILE *stream);
清除流中的文件末尾指示器和错误指示器
总结
在进行文件操作时,为避免一些奇怪的错误,尽量以二进制形式打开文件,使用二进制形式(fread , fwrite)进行读写。