为什么将stderr、stdin和stdout定义为宏?

5

C99将这三个定义为宏,它们是“指向与标准错误、输入和输出流相关联的FILE对象的类型为‘指向FILE的指针’的表达式”。

鉴于此,这些表达式可能是const(即指针可能被写入以重定向到另一个文件或流),也可能具有内部或外部链接(即写入以重定向到另一个文件或流可能不会在两个单独编译的C文件之间持续)。因此,在C文件中尝试重定向stdout可能会产生意外行为。

(注意:在gcc-9.4.0中,它们似乎是映射到具有相同名称的具有外部链接的非const变量的宏,并附带注释:/* C89/C99 say they're macros. Make them happy. */。)

是否有官方原因导致这种情况还是可以安全地假设这是原始标准中的另一个“疏忽”,并且应该避免在C代码中重定向流以保持可移植性?


由于宏已经定义,指针表达式不能带有const限定符或具有内部链接。这些宏是预定义的。 - Vlad from Moscow
4
值得注意的是,C标准并没有指定这些宏的确切性质,例如它们是否是const或具有内部或外部链接。这取决于实现方式,并且可能因不同编译器和平台而异。虽然这些宏提供了方便,但通常建议在C代码中使用标准I/O函数和适当的流重定向函数进行更可移植和灵活的输入/输出操作,正如您所建议的那样。我不确定确切的原因,但这可能只是一个历史遗留问题。 - Eldinur
3
使用 #define stdout stdout 是可以的。原因很可能是历史原因。很长一段时间(197x-200x),大多数系统都使用像 #define stdin _ios[0](或可能是 &_ios[0] —— 名称在“保留给系统”的命名空间中)这样的宏。这意味着它们可以在全局范围内的文件流初始化器中使用(static FILE *err = stderr;)。GNU C库在“oughts”某个时候更改了它们的定义,现在不再可能这样做。 - Jonathan Leffler
宏绝对没有内部或外部链接。所有宏标识符都没有链接。然而,stdin等可能也是对象标识符,如果是这样,那么这些标识符将具有内部或外部链接。 - John Bollinger
2
一个 C 程序可以调用 freopen 来重定向标准流。 - Ian Abbott
2个回答

9
这些表达式可以是const或不是(即指针可能写入以重定向到另一个文件或流);它们可以具有内部或外部链接。
宏标识符本身没有链接,只有当它们的替换文本是标识符时,链接才有意义。
因此,在C文件中尝试重定向stdout可能会导致意外行为。
试图为stdout分配新值可能会失败或产生意外结果。但是,您已经发现执行这样的赋值不是更改所连接的stdout目标的可靠方法。
有一个C99正式基本原理文件,它并未涉及此点。

还是可以假设这是原始标准中的另一个“疏忽”吗?

我不知道你的意思。我认为可以非常安全地假设 stdinstdoutstderr 被指定为宏是有意的。并且从它们的规范直接得出结论,您不能依赖于对它们进行任何目的的分配。

我推断它们被指定为它们允许实现的灵活性。例如,它们可以作为函数调用或表查找来实现。它们不必是对象标识符。

重定向流应该在 C 代码内避免以保持可移植性吗?

不是的。为了严格遵守 C 语言规范(大致相当于“保持可移植性”),必须避免将 stdinstdoutstderr 分配给其他值。如果您想将其中一个标准流与不同的目标关联起来,则这是 freopen() 函数的主要目的,该函数在所有标准 C 版本中都可用。


请注意,标准规定(C11 §7.21.1.3 Files ¶6):控制流的FILE对象的地址可能是重要的;FILE对象的副本不必代替原始对象。换句话说,您不能简单地分配或复制FILE结构。 - Jonathan Leffler
没错,@JonathanLeffler,但我不认为有人建议这样做。就像你所知道的那样,stdinstdoutstderr扩展为类型为FILE *的表达式,而不是FILE,OP的抱怨是他们无法将另一个FILE的地址可移植地分配给其中一个。 - John Bollinger
@JohnBollinger - 有道理。当我提到“内部或外部链接”时,我指的是预编译器将扩展的表达式中的标识符。我提出这个问题的原因是我看到其他人在博客中分配stdout以重定向它,我想深入了解一下。在我的情况下,我想临时重新路由stdout进行测试,然后将其路由回来,并希望有一个简单的方法来实现这一点,但最终我决定在函数内包装vfprintf,以便我可以在流之间切换。 - Jonathon S.

7

出于历史原因,它是这样的。

您无法重新分配stdinstdoutstderr。如果您需要重新打开这些流,以便在其他地方执行其i/o操作,那么可以使用freopen

在最早的<stdio.h>实现中,存在一个全局数组。

FILE _iobf[20];

然后stdinstdoutstderr都是宏:

#define stdin &_iobf[0]
#define stdout &_iobf[1]
#define stderr &_iobf[2]

这个代码可以良好地工作,而且它非常紧凑。今天看起来似乎“奇怪”——你可能会问:“为什么有人会用那种方式编写它?”但重要的是,他们确实用那种方式编写了它,并且它保持了很多年,足够长以至于C标准不得不适应它,通过禁止符合规范的程序将新值分配给那些“变量”。

(关于“为什么会以那种方式编写它”,我认为答案是经济性。当<stdio.h>被发明时,C语言还很新,PDP-11只有64k的地址空间,而且你甚至不能总是使用全部。每个人都使用read()write()进行原始I/O,或者实现自己的特殊缓冲方案。一些人问:“为什么我们必须实现自己的缓冲区?为什么没有标准的缓冲区?”而另一些人则说:“每个人的需求略有不同;没有一个实现可以满足所有人。”这很像我们今天仍在讨论的关于为什么C语言没有标准链表实现的讨论。在某个时候,Mike Lesk宣布他将实现一个标准的I/O库,他相信可以提出一个实现,能够满足每个人的需求,或者足够好。但为了安抚反对者,并不影响已经接近64k上限的大型程序,保持内存占用量 - 代码和数据 - 最小化是非常重要的。我记不清所有的细节,但那个FILE _iobf[20];方案确实非常紧凑,我猜你可能会想出任何“显然更好”的方案 - 比如动态分配FILE结构而不是有一个固定的20个 - 将具有显着更大的内存占用量。)

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接