Loading

快乐Linux —— 8.3 文件库函数IO & 常见问题

参考:

https://blog.51cto.com/wodesteve/1306629

http://www.ruanyifeng.com/blog/2006/04/post_213.html 换行与回车

https://linux.die.net/man/

http://bbs.chinaunix.net/thread-889260-1-1.html signed char 与 char 类型

简述

在进行文件操作时,经常遇到一些奇怪的问题,在这里总结总结常见问题并探究其原因。

不同的读写函数

文本文件读写函数
  1. int fgetc(FILE *stream);

    从文件中以 unsigned char 读取每个字符,并将它转化为 int 型返回。当到文件末尾时或读取错误时,返回 EOF

  2. char *fgets(char *buff, int maxcount, FILE *stream);

    从文件读取字符串到 buff ,遇到以下三种情况停止:

    1. 遇到换行时,读取换行符后停止,末尾添加 \0 返回 buff。
    2. 到达文件末尾
      1. 若已读取到字符,则末尾添加 \0 返回buff。
      2. 若没有读取到字符,则返回 NULL。
    3. 读取到了maxcount-1个字符,末尾添加 \0 返回 buff。

    也就是说 fgets 会自动为读取的字符串后加 \0 ,如果有字符序列 ‘A’ ’B’ ‘\0’ ,读取出来末尾会有两个 ‘\0’。第二个参数maxcount 可以放心的设置为缓冲区的大小。

  3. int fscanf(FILE *stream, const char *format, ...);

    成功时返回匹配成功的数量,失败返回EOF。

  4. int fputc(int ch, FILE *stream);

    把int 类型转化为 unsigned char 再写入流中。

    成功时返回值是把 unsigned char 转化为 int。失败返回EOF

    注意以下的坑:

  5. int fputs(const char *str, FILE *stream);

    遇到 str 中的 \0 停止,不把写入流中。

    成功时返回非负数,失败返回EOF

  6. int fprintf(FILE *stream, const char *format, ...);

    同样遇到 \0 停止, 不把它写入流中.

    成功时返回写入流中的字节数,失败时返回负数.

二进制文件读写函数
  1. 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相结合使用。

  2. 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 工作原理。

  1. 打开方式影响系统对 \n 的处理而对操作函数没有限制.

  2. \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

  3. 在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

  1. ch = fgetc(fp)

    当读取 0xff 这个字符,fgetc 以 unsigned char 类型获取这个字符,将其转化为 int 作为返回值。unsigned char 扩展为 int型,前面加0,也就是最终返回 int 类型 0x000000ff 。然后再把其截断赋值给 char 类型,所以 ch 里面保存 0xff 。

  2. ch != EOF

    char 类型与 EOF 进行比较,因为EOF被定义为 -1 所以是 int 类型。将char 扩展为 int ,属于signed 类型扩展,前面加符号位 也就编程 0xffffffff 再与 -1 进行比较,这两个是相同的,程序错误认为到达文件末尾。

  3. 当真正到达文件末尾,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,所以中文文本文件有可能也出错。

解决这个问题至少有一种办法:

  1. 将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)。当打开流时会清除这些指示器,读取不正确时设置相应的指示器。

当读取不正确,一般有三种情况:

  1. 读取失败 设置错误指示器
  2. 匹配失败 不设置任何指示器
  3. 已到达文件末尾 设置文件末尾指示器

相应的函数

int feof(FILE *stream); 检查文件是否到达文件末尾,若到达返回非0,否则返回0

int ferror(FILE *stream); 检查是否流错误,若错误返回非0,否则返回0

clearerr(FILE *stream); 清除流中的文件末尾指示器和错误指示器

总结

在进行文件操作时,为避免一些奇怪的错误,尽量以二进制形式打开文件,使用二进制形式(fread , fwrite)进行读写。

posted @ 2019-11-28 22:02  沉云  阅读(206)  评论(0编辑  收藏  举报