我很惊讶还没有人引用C11 规范。为长引用道歉,但我相信它是相关的。
7.5 错误
标头定义了几个宏...
...和
errno
它扩展为具有类型int
和线程本地存储持续时间的可修改左值(201),其值由几个库函数设置为正错误数。如果为了访问实际对象而禁止宏定义,或者程序定义了名称为 的标识符errno
,则行为未定义。
初始线程中的值errno
在程序启动时为零(errno
其他线程中的初始值为不确定值),但绝不会被任何库函数设置为零。(202) errno 的值可以通过以下方式设置为非零一个库函数调用,无论是否有错误,只要
errno
在本国际标准的函数描述中没有记录使用。
(201) 宏errno
不必是对象的标识符。它可能会扩展为函数调用产生的可修改左值(例如,*errno()
)。
(202) 因此,errno
用于错误检查的程序应在库函数调用之前将其设置为零,然后在后续库函数调用之前对其进行检查。当然,库函数可以保存errno
on entry 的值,然后将其设置为零,只要errno
在返回之前如果 ' 的值仍然为零,则恢复原始值。
“线程本地”的意思register
是out。类型int
表示位域已出(IMO)。所以&errno
在我看来是合法的。
持续使用诸如“它”和“价值”之类的词表明该标准的作者并没有考虑&errno
成为非常数。我想人们可以想象一个&errno
在特定线程中不是恒定的实现,但要按照脚注所说的方式使用(设置为零,然后在调用库函数后检查),它必须是故意对抗的,并且可能需要专门的编译器支持只是为了对抗。
简而言之,如果规范确实允许非常量&errno
,我认为这不是故意的。
[更新]
R. 在评论中提出了一个很好的问题。经过思考,我相信我现在知道了他问题的正确答案,以及原始问题的正确答案。让我看看我能不能说服你,亲爱的读者。
R. 指出 GCC 在顶层允许这样的事情:
register int errno asm ("r37"); // line R
这将声明errno
为寄存器中保存的全局值r37
。显然,这将是一个线程局部可修改的左值。那么,一个符合标准的 C 实现可以这样声明errno
吗?
答案是否定的。当您或我使用“声明”一词时,我们通常会想到一个口语化和直观的概念。但是标准并没有口语化或直观地表达;它说话准确,而且它的目的只是使用定义明确的术语。在“声明”的情况下,标准本身定义了该术语;当它使用这个术语时,它使用的是它自己的定义。
通过阅读规范,您可以准确地了解“声明”是什么以及它不是什么。换句话说,该标准描述了语言“C”。它没有描述“某种不是 C 的语言”。就标准而言,“带有扩展的 C”只是“某种不是 C 的语言”。
因此,从标准的角度来看,行 R根本不是声明。它甚至不解析!不妨读一读:
long long long __Foo_e!r!r!n!o()blurfl??/**
就规范而言,这与 R 行一样是“声明”;即,一点也不。
因此,当 C11 规范在第 6.5.3.2 节中说:
一元运算&
符的操作数应该是一个函数指示符、一个[]
或一元运算符的结果*
,或者是一个左值,它指定一个不是位域且未使用寄存器存储类说明符声明的对象。
...它的意思是非常精确的东西,不涉及像 Line R 这样的东西。
现在,考虑引用的int
对象的声明。errno
(注意:我不是指errno
name的声明,因为如果是一个宏,当然可能没有这样的声明errno
。我的意思是底层int
对象的声明。)
上面的语言说你可以获取一个左值的地址,除非它指定一个位域或者它指定一个对象 "declared" register
。底层对象的规范errno
说它是一个int
具有线程本地持续时间的可修改左值。
现在,规范确实没有说errno
必须声明底层对象。也许它只是通过一些实现定义的编译器魔法出现。但同样,当规范说“使用寄存器存储类说明符声明”时,它使用了自己的术语。
因此,要么底层errno
对象在标准意义上被“声明”,在这种情况下,它不能既是register
线程本地的;或者它根本没有被声明,在这种情况下它没有被声明register
。不管怎样,因为它是一个左值,你可以取它的地址。
(除非它是位域,但我认为我们同意位域不是类型的对象int
。)