了解 K&R 的 putc 宏:K&R 第 8 章(Unix 系统接口)练习 2

Understanding K&R's putc macro: K&R Chapter 8 (The Unix System Interface) Exercise 2

一段时间以来,我一直在尝试了解 K&R 的 putc 版本,但资源不足(google、堆栈溢出、clcwiki 不完全符合我的要求而且我没有朋友或同事可以求助)。我会先解释上下文,然后再要求澄清。

本章课文介绍了一个描述文件的数据结构的例子。该结构包括一个字符缓冲区,用于一次读取和写入大块。然后他们要求 reader 编写标准库 putc 的一个版本。

作为 reader 的线索,K&R 编写了一个支持缓冲和非缓冲读取的 getc 版本。他们还编写了 putc 宏的框架,让用户自己编写函数 _flushbuf()。 putc 宏看起来像这样(p 是指向文件结构的指针):

int _flushbuf(int, FILE *);
#define putc(x,p)        (--(p)->cnt >= 0 \ 
                       ? *(p)->ptr++ = (x) : _flushbuf((x),p)
typedef struct {
        int   cnt;  /*characters left*/
        char *ptr;  /*next character position*/
        char *base; /*location of buffer*/
        int   flag; /*mode of file access*/
        int   fd;   /*file descriptor*/
} FILE;

令人困惑的是,宏中的条件实际上是在测试结构的缓冲区是否已满(这在文中有说明)- 作为旁注,getc 中的条件完全相同,但意味着缓冲区为空。奇怪?

这里是我需要澄清的地方:我认为在 putc 中缓冲写入有一个很大的问题;由于写入 p 仅在 _flushbuf() 中执行,而 _flushbuf() 仅在文件结构的缓冲区已满时调用,因此仅在缓冲区完全填满时才进行写入。缓冲读取的大小始终是系统的 BUFSIZ。除了完全 'BUFSIZ' 个字符之外,不会写入任何内容,因为 _flushbuf() 永远不会在 putc.

中调用

putc 对于无缓冲写入来说工作得很好。但是宏的设计使得缓冲写入几乎完全没有意义。这是正确的,还是我在这里遗漏了什么?为什么会这样?我真的很感谢这里的所有帮助。

如果你写得足够多,缓冲区最终会变满。如果不这样做,您最终将关闭文件(或者运行时会在 main() returns 时为您完成)并且 fclose() 调用 _flushbuf() 或其等效项。或者您将手动 fflush() 流,这也相当于 _flushbuf().

如果你写几个字符然后调用sleep(1000),你会发现很长一段时间没有打印任何东西。这确实是它的工作方式。

getc 和 putc 中的测试是相同的,因为在一种情况下,计数器记录有多少字符可用,而在另一种情况下,它记录有多少 space 可用。

我认为您可能误读了 putc() 宏中发生的事情;那里有很多运算符和符号,它们都很重要(而且它们的执行顺序很重要!)才能正常工作。为了帮助更好地理解它,让我们将其替换为实际用法,然后将其展开,直到您可以看到发生了什么。

让我们从 putc('a', file) 的简单调用开始,如下例所示:

FILE *file = /* ... get a file pointer from somewhere ... */;

putc('a', file);

现在用宏代替对 putc() 的调用(这是简单的部分,由 C 预处理器执行;另外,我认为您在你提供的版本,所以我将把它插入到它所属的末尾):

FILE *file = /* ... get a file pointer from somewhere ... */;

(--(file)->cnt >= 0 ? *(file)->ptr++ = ('a') : _flushbuf(('a'),file));

嗯,这不是一堆乱七八糟的符号。让我们去掉不需要的括号,然后将 ?...: 转换为它实际上在幕后的 if 语句:

FILE *file = /* ... get a file pointer from somewhere ... */;

if (--file->cnt >= 0)
    *file->ptr++ = 'a';
else
    _flushbuf('a', file);

这更接近了,但仍然不太清楚发生了什么。让我们将增量和减量移动到单独的语句中,以便更容易看到执行顺序:

FILE *file = /* ... get a file pointer from somewhere ... */;

--file->cnt;
if (file->cnt >= 0) {
    *file->ptr = 'a';
    file->ptr++;
}
else {
    _flushbuf('a', file);
}

现在,内容重新排序后,应该更容易看清发生了什么。首先,我们减少 cnt,剩余字符的计数。如果这表明还有空间,那么可以安全地将 a 写入文件的缓冲区,在文件的当前写指针处,然后我们将写指针向前移动。

如果没有剩余空间,那么我们调用_flushbuf(),将文件(其缓冲区已满)和我们要写入的字符传递给它但不能。据推测,_flushbuf() 将首先将整个缓冲区写入实际的底层 I/O 系统,然后写入该字符,然后可能会将 ptr 重置为缓冲区的开头和 cnt 到一个大数字表示缓冲区能够再次存储大量数据。

那么为什么这会导致缓冲写入?答案是 _flushbuf() 调用仅在缓冲区已满时执行 "every once in a while,"。将一个字节写入缓冲区很便宜,而执行实际的 I/O 却很昂贵,因此这导致 _flushbuf() 被调用的次数相对较少(每 BUFSIZ 个字符仅调用一次)。