【问题标题】:Why does gcc allow extern declarations of type void (non-pointer)?为什么 gcc 允许 void 类型的外部声明(非指针)?
【发布时间】:2012-04-19 19:18:44
【问题描述】:

为什么 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'

【问题讨论】:

  • 有趣的是,即使使用-std=c89 -pedantic gcc 也很酷。
  • 据我了解,定义void 类型的变量格式不正确,但将extern 应用于不完整类型并不是格式错误,对于exampleextern 在数组上大小未知,稍后定义。但是,§6.2.5.19“void 类型包含一组空值;它是不完整的对象类型,无法完成。”,鉴于您的代码应被视为违反约束。它使用-pedantic 干净地编译的事实表明它不是扩展,这是 gcc 错误或 msvc 和 gcc 解释标准的方式中的歧义。

标签: c gcc


【解决方案1】:

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

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

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

【讨论】:

    【解决方案2】:

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

    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 声明,因为它清楚地表明链接器定义的符号没有内在的“类型”。

    【讨论】:

    • 您是否有可以链接到使用此方法的代码示例?
    • foo是实际地址的情况下,我认为应该在C代码中声明为地址。我唯一认为void 合适的情况是foo 不是真正的地址。恕我直言,如果标准指定 extern void 的含义是实现定义的,但前提是实现可以记录并对其认为合适的使用施加任何限制(包括完全取缔它)。
    • 我自己也遇到过这个用例,当我看到这个问题时,我想“我想知道 OP 是否在没有意识到的情况下处理链接器符号?”
    【解决方案3】:

    GCC(也就是 LLVM C 前端)肯定有问题。不过,Comeau 和 MS 似乎都报告了错误。

    OP 的 sn-p 至少有两个明确的 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 指定一个不是位域且未声明为的对象 寄存器存储类说明符。

    [注意:强调lvalue mine] 此外,标准中有一个部分专门针对void

    6.3.2.2 无效

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

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

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

    5 [...] 如果声明一个对象的标识符 有文件作用域并且没有存储类说明符,它的链接是 外部。

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

    【讨论】:

    • 获取值不可用的左值的地址是合法的(例如,从未写入的局部变量)。如果foo 被声明为void 类型的外部标识符,则表达式&foo 将产生void* 类型的合法值。 C 没有提供可以创建 foo 的链接时可解析定义的方法,但是如果另一种语言允许定义这样的标识符,我认为禁止 C 编译器允许 C 代码访问其地址没有任何用处。
    【解决方案4】:

    奇怪的是(或者也许不那么奇怪......)在我看来 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 的名称:

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

    • 无效
    • 字符
    • 签名字符

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

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

    【讨论】:

    • 我对第 6.9.2 节的阅读与您的完全相反:为具有文件范围的对象声明标识符,该对象没有初始化程序,也没有存储类说明符或存储-类说明符 static,构成一个暂定定义。这是否意味着这不是一个暂定的定义,因此不允许我们使用不完整的类型? Comeau,IMO,正确地抱怨。
    • @dirkgently:这绝对不是一个暂定定义,它被定义为(§6.9.2/2):“声明一个具有文件范围但没有初始化程序的对象的标识符,以及没有存储类说明符或存储类说明符为静态的,构成一个暂定定义。”我没有看到任何说不允许不完整类型的内容,只是因为它不是一个暂定的定义。
    • gcc 和 msvc 之间的歧义似乎在对 §6.2.5.19 的解释中,如果 void 是一个不完整的类型,它永远不会完成,那么参数归结为是否应该允许 extern 应用于一个永远不完整的不完整类型,我相信它为实现解释留下了空间。无论哪种方式,每个编译器都会出错,只是在编译或链接期间是否发出错误可能会有所不同,并且似乎可能是他们不希望有太多麻烦的边缘情况。
    • @JerryCoffin:根据 §6.7.9/3 使初始化有效。要初始化的实体的类型应为大小未知的数组或非可变长度的完整对象类型数组类型。现在,后一种情况在这种情况下不成立。这也解释了为什么没有关于如何初始化 void 类型的对象的定义。
    • 此外,来自 §J.2 未定义行为,此位:对象的标识符被声明为没有链接,并且对象的类型在其声明符之后或在其 init-declarator 之后不完整,如果它有一个初始化程序。相关:即使这是一个暂定定义,这也是 UB:An identifier for an object with internal linkage and an incomplete type is declared with a tentative definition (6.9.2).(再次来自 J.2)。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2020-02-21
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2012-02-08
    相关资源
    最近更新 更多