这是一个有趣的案例。声明标识符似乎没有违反任何约束x
具有外部链接的 void 类型,但它几乎无法使用。
void
“是一个无法完成的不完整对象类型”(C 2018 6.2.5 19)。当对象的标识符声明为无链接时,类型必须“在其声明符末尾处完成”(6.7 7)。但对于具有外部链接的标识符来说情况并非如此;我们可以声明extern int a[]; extern struct foo b;
并定义a
and b
后来,甚至在另一个翻译单位。
If x
没有使用,我没有看到它违反任何约束。如果程序尝试使用它,则 6.9 5 将适用:
...如果在表达式中使用通过外部链接声明的标识符(而不是作为操作数的一部分)sizeof
or _Alignof
其结果是整型常量的运算符),在整个程序的某处应该有一个标识符的外部定义;否则,不得超过一个。
但我们无法定义x
在C代码中,因为它有一个不完整的类型,并且它的类型无法完成。只要它没有定义,我们就不能使用x
在表达式中而不是作为操作数sizeof
or _Alignof
,由于上面的段落,我们也不能将它与sizeof
or _Alignof
,因为这些运算符需要完整的类型。
我们可以想象x
在 C 外部定义并与此 C 代码链接。因此某些汇编模块可能会提供以下定义x
C 代码不知道这一点。当然,如果没有类型的定义,C 代码就无法使用对象的值。但它可以使用地址x
。例如,它可以充当指针值的哨兵或其他标记。E.g.,我们可以将指针列表的列表作为指针列表传递给另一个例程,其中子列表由&x
整个列表的末尾由空指针标记。 (所以两个子列表(&a
, &b
, &c
) and (&d
, &e
, &f
) 将被传递为(void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };
.)
然而,编译printf("%p\n", &x);
与 Clang 并使用-pedantic
产生错误消息“ISO C 禁止获取‘void’类型表达式的地址”。其核心原因似乎是 6.3.2.1 1 排除了 的对象void
类型不再是左值:
- An lvalue是一个表达式(对象类型不是
void
) 可能指定一个对象;...
6.5.3.2 1 要求操作数为一元&
成为左值:
- 一元的操作数
&
运算符应是函数指示符、函数的结果[]
或一元*
运算符,或指定对象的左值...
这可能是 C 标准设计不完整的部分,因为它并不排除const void
不再是左值,并且 Clang 编译extern const void x; printf("%p\n", &x);
没有抱怨,但似乎没有理由以标准来对待const void
and void
在这方面有所不同。
一方面,微软可能已经得出结论,没有办法使用这个x
因此,一旦出现问题,我们就会立即对其进行诊断extern void x
被发现而不是让代码尝试使用它时发生错误x
。然而,虽然编译器可以自由地发出额外的诊断消息,但它应该接受符合要求的程序。也就是说,对于符合C标准的编译器,诊断可能是警告,但可能不是阻止编译的错误。
补充说明
注意到一元的约束&
允许“结果[]
或一元*
运算符”,我对此进行了测试:
static void foo(void *p)
{
printf("%p\n", &*p);
}
Here, *p
其本身是类型的左值void
,并且这是允许的&
因为约束明确允许它,而&x
看起来是一个非常相似的表达式,取一个地址void
,但约束不允许,因为x
既不是左值也不是结果*
。好奇的。