缓冲标准I / O库

时间:2013-01-16 08:44:26

标签: c stdio buffering

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

  

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

     
      
  • 如果没有介入fflushfseekfsetposrewind,则输入无法直接输入。
  •   
  • 如果没有插入fseekfsetposrewind,或者遇到文件结尾的输入操作,则输入无法直接输出。
  •   

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

2 个答案:

答案 0 :(得分:4)

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

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 );fscanf之间执行fprintf,这是可以的,因为这会更改I / O缓冲区的模式而不重新定位它。

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

答案 1 :(得分:3)

目前尚不清楚你在问什么。

你的基本问题是“为什么这本书说我不能这样做?”好吧,这本书说你不能这样做因为POSIX / SUS /等。标准表示它是fopen specification中未定义的行为,它与ISO C standard(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 已经已经被刷新了 - 或者甚至它被部分刷新了,所以磁盘文件现在包含{{1 }}

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

最后,你说,“我猜这些限制的原因可能与图书馆的缓冲有关,但我不太清楚。”这听起来像是在询问理由。

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

有三种方法可以实现像stdio这样的东西:

  1. 使用共享缓冲区进行读写,并根据需要编写代码以切换上下文。这将会有点复杂,并且会比理想情况更频繁地刷新缓冲区。
  2. 使用两个单独的缓冲区和缓存样式代码来确定一个操作何时需要从另一个缓冲区复制和/或使其无效。这甚至更复杂,并使3 xyz对象占用了两倍的内存。
  3. 使用共享缓冲区,并且不允许交错读取和写入,两者之间没有显式刷新。这很简单,尽可能高效。
  4. 使用共享缓冲区,并在交错读取和写入之间隐式刷新。这几乎同样简单,几乎同样有效,而且更安全,但除了安全之外,其他任何方式都没有更好。
  5. 所以,Unix用#3来记录它,并且SUS,POSIX,C89等标准化了这种行为。

    你可能会说,“来吧,它不能 效率低下。”那么,你必须记住,Unix是专为20世纪70年代的低端系统设计的,而且基本的理念是,除非有一些实际的好处,否则它的效率甚至不值得。但是,最重要的是,考虑到stdio必须处理诸如FILEgetc之类的琐碎功能,而不仅仅是像putcfscanf这样的花哨的东西,并为这些功能添加任何东西(或者它们使它们变慢5倍会使很多现实世界的代码产生巨大的差异。

    如果你看一下现代实现,例如* BSD,glibc,Darwin,MSVCRT等(大多数是开源的,或者至少是商业但共享的源),他们中的大多数都是同样的方式。一些添加安全检查,但它们通常会给你一个错误的交错而不是隐式刷新 - 毕竟,如果你的代码错了,最好告诉你你的代码错误而不是尝试DWIM。

    例如,查看早期的达尔文(OS X)fopenfreadfwrite(之所以选择,因为它很简单,并且具有易于链接的语法颜色代码但也复制 - 可以)。 fprintf所要做的就是从缓冲区中复制字节,如果缓冲区用完则重新填充缓冲区。你不能比这更简单。