7

在UNIX 环境中的高级编程(第 2 版)一书中,作者在第 5.5 节(标准 I/O 库的流操作)中写道:

当打开文件进行读写时(类型中的加号),将应用以下限制。

  • fflush如果没有中间的, fseek, fsetpos, 或,输出不能直接跟在输入后面rewind
  • fseek如果没有中间的fsetpos, 或rewind, 或遇到文件结尾的输入操作,输入不能直接跟在输出后面。

我对此感到困惑。有人可以解释一下吗?例如,在什么情况下输入输出函数调用违反上述限制会导致程序出现意外行为?我想限制的原因可能与库中的缓冲有关,但我不太清楚。

4

2 回答 2

4

不允许穿插输入和输出操作。例如,您不能使用格式化输入来查找文件中的特定点,然后从该点开始写入字节。这允许实现假设在任何时候,唯一的 I/O 缓冲区将只包含要读取(给您)或写入(给操作系统)的数据,而不进行任何安全检查。

f = fopen( "myfile", "rw" ); /* open for read and write */
fscanf( f, "hello, world\n" ); /* scan past file header */
fprintf( f, "daturghhhf\n" ); /* write some data - illegal */

但是,如果您fseek( f, 0, SEEK_CUR );在 thefscanf和 the之间进行操作,这没关系,fprintf因为这会更改 I/O 缓冲区的模式而不重新定位它。

为什么这样做?据我所知,因为操作系统供应商经常希望支持自动模式切换,但失败了。该stdio规范允许有缺陷的实现是兼容的,而自动模式切换的工作实现只是实现了一个兼容的扩展。

于 2013-01-16T08:54:14.913 回答
3

不清楚你在问什么。

你的基本问题是“为什么书上说我不能这样做?” 好吧,这本书说你不能这样做,因为 POSIX/SUS/等。标准说它在fopen规范中是未定义的行为,它与ISO C 标准(N1124 工作草案,因为最终版本不是免费的)7.19.5.3 保持一致。

那么你问,“在什么情况下,输入输出函数调用违反上述限制会导致程序出现意外行为?”

未定义的行为总是会导致意外的行为,因为重点是不允许您期待任何事情。(参见上面链接的 C 标准中的 3.4.3 和 4。)

但最重要的是,甚至不清楚他们可以指定什么是有意义的。看这个:

int main(int argc, char *argv[]) {
  FILE *fp = fopen("foo", "r+");
  fseek(fp, 0, SEEK_SET);
  fwrite("foo", 1, 3, fp);
  fseek(fp, 0, SEEK_SET);
  fwrite("bar", 1, 3, fp);
  char buf[4] = { 0 };
  size_t ret = fread(buf, 1, 3, fp);
  printf("%d %s\n", (int)ret, buf);
}

那么,这是否应该打印出来,3 foo因为那是磁盘上的内容,或者3 bar因为那是“概念文件”中的内容,或者0因为在写完之后什么都没有,所以你在 EOF 阅读?如果您认为有一个明显的答案,请考虑这样一个事实,即它可能bar 已经被刷新——甚至它已经被部分刷新,因此磁盘文件现在包含boo.

如果您要问更实际的问题“我可以在某些情况下摆脱它吗?”,好吧,我相信在大多数 Unix 平台上,上面的代码会给您偶尔的段错误,但是3 xyz(3 个未初始化的字符,或者更复杂的情况下 3 个字符在缓冲区被覆盖之前碰巧在缓冲区中)其余时间。所以,不,你不能逃避它。

最后,你说,“我猜限制的原因可能与库中的缓冲有关,但我不太清楚。” 这听起来像你在问基本原理。

你是对的,它是关于缓冲的。正如我在上面所指出的,这里确实没有直观的正确做法——但也要考虑实施。请记住,Unix 的方式一直是“如果最简单和最有效的代码足够好,那就去做”。

您可以通过三种方式实现类似 stdio 的功能:

  1. 使用共享缓冲区进行读写,并根据需要编写代码来切换上下文。这会有点复杂,并且会比您理想的情况更频繁地刷新缓冲区。
  2. 使用两个单独的缓冲区和缓存样式代码来确定一个操作何时需要从另一个缓冲区复制和/或使另一个缓冲区无效。这更加复杂,并且使FILE对象占用两倍的内存。
  3. 使用共享缓冲区,只是不允许在没有显式刷新的情况下交错读取和写入。这是非常简单的,并且尽可能高效。
  4. 使用共享缓冲区,并在交错读取和写入之间隐式刷新。这几乎同样简单,几乎同样高效,而且更安全,但除了安全之外,在任何方面都没有更好。

因此,Unix 选择了#3,并对其进行了记录,SUS、POSIX、C89 等对这种行为进行了标准化。

你可能会说,“来吧,它不可能那么低效。” 好吧,您必须记住,Unix 是为 1970 年代的低端系统设计的,其基本理念是,除非有一些实际的好处,否则即使是一点点效率也不值得。但是,最重要的是,考虑到 stdio 必须处理诸如 and 之类的琐碎函数getcputc而不仅仅是诸如 and 之类的花哨的东西fscanffprintf并且向那些函数(或宏)添加任何使它们慢 5 倍的任何东西都会在很多实际情况下产生巨大的差异-世界密码。

如果您查看来自 *BSD、glibc、Darwin、MSVCRT 等的现代实现(其中大部分是开源的,或者至少是商业但共享源的),它们中的大多数都以相同的方式做事。一些添加了安全检查,但它们通常会给您一个交错而不是隐式刷新的错误——毕竟,如果您的代码错误,最好告诉您您的代码错误,而不是尝试 DWIM。

例如,看看早期的 Darwin (OS X) fopen, fread, and fwrite(之所以选择它是因为它既漂亮又简单,并且具有易于链接的代码,具有语法颜色但也可复制粘贴)。所fread要做的就是从缓冲区中复制字节,并在缓冲区用完时重新填充缓冲区。没有比这更简单的了。

于 2013-01-30T02:13:09.093 回答