23

为什么 gcc 允许 void 类型的外部声明?这是扩展还是标准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'
4

4 回答 4

8

奇怪的是(或者也许不是那么奇怪......)在我看来,gcc 接受这一点是正确的。

如果声明 thisstatic而不是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 的要求,这似乎允许使用 type 声明名称void

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

  • 空白
  • 字符
  • 签名字符

[ ...省略了更多类型]

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

于 2012-04-06T05:23:29.590 回答
6

我发现了声明的唯一合法用途

extern void foo;

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

这实际上很有用,因为链接符号通常用于传达内存范围;即 .text 段起始地址、.text 段长度等。

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

uint32_t textLen;

textLen = ( uint32_t )foo;

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

uint8_t *textStart;

textStart = ( uint8_t * )foo;

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

extern uint8_t foo[];

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

于 2013-09-27T15:43:11.447 回答
1

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

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

从 N1570

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

J2。未定义的行为

[...] 托管环境中的程序未使用指定形式之一定义名为 main 的函数 (5.1.2.2.1)。

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

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

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

和:

6.5.3.2 地址和间接运算符

约束

1 一元 & 运算符的操作数应为函数指示符、[] 或一元 * 运算符的结果,或指定不是位域且未使用寄存器存储类声明的对象的左值说明符。

[注意:强调左值矿井] 另外,标准中有一节专门针对void

6.3.2.2 无效

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

文件范围定义是主要表达式 (6.5)。因此,正在获取由 表示的对象的地址foo。顺便说一句,后者调用 UB。因此,这被明确排除。还有待弄清楚的是,如果删除extern限定符使上述有效或无效:

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

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

即即使我们忽略了extern我们仍然会遇到同样的问题。

于 2012-04-07T12:05:34.987 回答
1

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

这种方法在很大程度上是特定于平台的(就像使用汇编语言的任何东西一样),但是它使一些否则会出现问题的构造成为可能。它的一个有点令人讨厌的方面是,如果在 C 中将标签定义为类似 的类型unsigned char[],则会传达出地址可能被取消引用或对其执行算术运算的印象。如果编译器将接受void foo;,则将(int)&foo使用foo与任何其他 `void*.

我认为我从来没有void为此目的使用过(我一直使用extern unsigned char[]),但如果void将其定义为合法扩展(C 标准中没有任何内容要求任何地方存在任何能力来创建链接器符号,可以用作除一种特定的非 void 类型以外的任何东西;在无法创建 C 程序可以定义为的链接器标识符的平台上extern void,编译器不需要允许这种语法)。

于 2015-04-28T18:39:57.510 回答