为什么gcc允许类型为void(非指针)的extern声明?

为什么gcc允许类型为void的extern声明? 这是扩展还是标准C? 这有可接受的用途吗?

我猜这是一个扩展,但我没有发现它提到:
http://gcc.gnu.org/onlinedocs/gcc-4.3.6/gcc/C-Extensions.html

$ cat extern_void.c extern void foo; /* ok in gcc 4.3, not ok in Visual Studio 2008 */ void* get_foo_ptr(void) { return &foo; } $ gcc -c extern_void.c # no compile error $ gcc --version | head -n 1 gcc (Debian 4.3.2-1.1) 4.3.2 

将foo定义为void类型当然是编译错误:

 $ gcc -c -Dextern= extern_void.c extern_void.c:1: error: storage size of 'foo' isn't known 

为了进行比较,Visual Studio 2008在extern声明中给出了错误:

 $ cl /c extern_void.c Microsoft (R) 32-bit C/C++ Optimizing Compiler Version 15.00.21022.08 for 80x86 Copyright (C) Microsoft Corporation. All rights reserved. extern_void.c extern_void.c(1) : error C2182: 'foo' : illegal use of type 'void' 

奇怪的是(或者也许并不那么奇怪……)它让我觉得gcc接受这个是正确的。

如果这被声明为static而不是extern ,则它将具有内部链接,并且§6.9.2/ 3将适用:

如果对象的标识符声明是暂定定义并且具有内部链接,则声明的类型不应是不完整的类型。

如果它没有指定任何存储类(在本例中为extern ),则适用§6.7/ 7:

如果声明对象的标识符没有链接,则对象的类型应在其声明符的末尾完成,或者如果它具有初始化器,则由init-declarator的末尾完成; 在函数参数(包括原型)的情况下,需要完成的是调整后的类型(见6.7.5.3)。

我在其中任何一种情况下, void都行不通,因为(§6.2.5/ 19):

void类型[…]是不完整的类型,无法完成。

但是,这些都不适用。 这似乎只留下§6.7.2/ 2的要求,这似乎允许声明类型为void的名称:

每个声明中的声明说明符中应至少给出一个类型说明符,并在每个结构声明和类型名称的说明符限定符列表中给出。 每个类型说明符列表应为以下集合之一(用逗号分隔,当一行上有多个集合时); 类型说明符可以按任何顺序出现,可能与其他声明说明符混合。

  • 空虚
  • 烧焦
  • 签名的char

[…更多类型省略]

我不确定这是非常有意的 – 我怀疑void是真的用于派生类型(例如,指向void的指针)或函数的返回类型之类的东西,但是我找不到任何直接指定该限制的东西。

我找到了宣布的唯一合法用途

 extern void foo; 

foo是链接符号(由链接器定义的外部符号)时,表示未指定类型的对象的地址。

这实际上很有用,因为链接符号通常用于传达内存的范围; ie .text section start address,.text section length等。

因此,使用这些符号的代码通过将它们转换为适当的值来记录它们的类型是很重要的。 例如,如果foo实际上是内存区域的长度:

 uint32_t textLen; textLen = ( uint32_t )foo; 

或者,如果foo是同一内存区域的起始地址:

 uint8_t *textStart; textStart = ( uint8_t * )foo; 

在我所知道的“C”中引用链接符号的唯一替代方法是将其声明为外部数组:

 extern uint8_t foo[]; 

我实际上更喜欢void声明,因为它清楚地表明链接器定义的符号没有内在的“类型”。

GCC(也是LLVM C前端)绝对是错误的。 Comeau和MS似乎都报告了错误。

OP的片段至少有两个明确的UB和一个红鲱鱼:

从N1570开始

[UB#1]在托管环境中缺少main

J2。 未定义的行为

[…]托管环境中的程序未使用其中一个指定的表单(5.1.2.2.1)定义名为main的函数。

[UB#2]即使我们忽略了上述内容,仍然存在获取明确禁止的void表达式地址的问题:

6.3.2.1左值,数组和函数指示符

1左值是一个表达式(对象类型不是void),可能指定一个对象; 64)

和:

6.5.3.2地址和间接运营商

约束

1T一元&运算符的操作数应该是函数指示符,[]或一元*运算符的结果,或者是一个左值 ,它指定一个不是位字段的对象,并且不用寄存器存储类声明符。

[注意:强调lvalue mine]另外,标准中有一节专门针对void

6.3.2.2无效

1不应以任何方式使用void表达式(具有void类型的表达式)的(不存在)值,并且不应对此类表达式应用隐式或显式转换(void除外)。

文件范围定义是主表达式(6.5)。 所以,取foo表示的对象的地址。 BTW,后者调用UB。 因此明确排除了这一点。 还有待弄清楚的是,如果删除extern限定符使上述有效或不符合:

在我们的例子中,对于foo ,根据§6.2.2/ 5:

5 […]如果对象的标识符声明具有文件范围而没有存储类说明符,则其链接是外部的。

即使我们遗漏了extern我们仍然会extern同样的问题。

C的链接器交互语义的一个限制是它没有提供允许数字链接时间常量的机制。 在某些项目中,静态初始值设定项可能需要包含在编译时不可用但在链接时可用的数值。 在某些平台上,这可以通过在某处(例如在汇编语言文件中)定义标签来实现,该标签的地址(如果转换为int )将产生感兴趣的数值。 然后可以在C文件中使用extern定义,以使该东西的“地址”可用作编译时常量。

这种方法非常特定于平台(就像使用汇编语言的任何东西一样),但它使一些构造成为可能,否则会有问题。 它的一个有点讨厌的方面是,如果标签在C中定义为类似unsigned char[]的类型,那将传达这样的印象:地址可能被解除引用或对其执行算术。 如果编译器接受void foo; ,然后(int)&foo将使用与任何其他`void *相同的指针到整数语义将foo的链接器分配地址转换为整数。

我不认为我曾经为此目的使用过void (我总是使用extern unsigned char[] )但是如果将某些内容定义为合法的扩展名,则认为void会更清晰(C标准中没有任何内容要求在任何地方都存在任何创建链接器符号的能力,该符号可以用作除一个特定的非void类型之外的任何东西;在没有方法存在的平台上创建C程序可以定义为extern void的链接器标识符,就没有需要编译器允许这样的语法)。