为什么有这段代码:
extern void x;
导致:
$ cl t555.c /std:c11 /Za
t555.c(1): error C2182: 'x': illegal use of type 'void'
这里什么是非法的?
上。用例:
$ cat t555a.c t555.p.S
#include <stdio.h>
extern void x;
int main(void)
{
printf("%pn", &x);
return 0;
}
.globl x
x:
.space 4
$ gcc t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && gcc t555a.o t555.p.o && ./a.exe
t555a.c: In function ‘main’:
t555a.c:7:20: warning: taking address of expression of type ‘void’
7 | printf("%pn", &x);
| ^
0x1004010c0
$ clang t555a.c -std=c11 -pedantic -Wall -Wextra -c && as t555.p.S -o t555.p.o && clang t555a.o t555.p.o && ./a.exe
t555a.c:7:20: warning: ISO C forbids taking the address of an expression of type 'void' [-Wpedantic]
printf("%pn", &x);
^~
1 warning generated.
00007FF76E051120
这是一个有趣的案例。声明具有外部链接的 void 类型的标识符x
似乎没有违反任何约束,但它几乎无法使用。
void
"是无法完成的不完整对象类型"(C 2018 6.2.5 19)。当声明对象的标识符时没有链接,该类型必须"在其声明符末尾完成"(6.7 7)。但是,对于具有外部链接的标识符,情况并非如此;我们可以声明extern int a[]; extern struct foo b;
并定义a
,并在以后b
,甚至在另一个翻译单元中也是如此。
如果不使用x
,我认为它不会违反任何约束。如果程序尝试使用它,则 6.9 5 将适用:
。如果在表达式中使用了用外部链接声明的标识符(而不是作为结果为整数常量的
sizeof
或_Alignof
运算符的操作数的一部分),则在整个程序中的某个地方,标识符应该只有一个外部定义;否则,不得超过一个。
但是我们不能在 C 代码中定义x
,因为它的类型不完整,并且其类型无法完成。只要它没有定义,我们就不能在表达式中使用x
,而不是作为sizeof
或_Alignof
的操作数,由于上述段落,我们也不能将其与sizeof
或_Alignof
一起使用,因为这些运算符需要一个完整的类型。
我们可以想象x
是在 C 之外定义的,并与这个 C 代码相关联。因此,某些程序集模块可能会为 C 代码未知的x
提供定义。当然,C 代码不能在没有类型定义的情况下使用对象的值。但它可以使用x
的地址。例如,它可以用作指针值的哨兵或其他标记。例如,我们可以将指向另一个例程的指针列表作为指针列表传递,其中子列表由&x
分隔,整个列表的末尾由空指针标记。(所以两个子列表(&a
、&b
、&c
)和(&d
、&e
、&f
)将作为(void *[]) { &a, &b, &c, &x, &d, &e, &f, NULL };
传递。
但是,使用 Clang 编译printf("%pn", &x);
并使用-pedantic
会产生错误消息"ISO C 禁止采用类型为'void'的表达式的地址"。这样做的核心原因似乎是 6.3.2.1 1 将void
类型的对象排除为左值:
-
左值是一个表达式(对象类型不是
void
),可能指定一个对象;
和 6.5.3.2 1 要求一元&
的操作数为左值:
- 一元
&
运算符的操作数应为函数指示符、[]
或一元*
运算符的结果,或指定对象的左值......
这可能是 C 标准中设计不完整的部分,因为它不排除const void
成为左值,Clang 毫无怨言地编译了extern const void x; printf("%pn", &x);
,但标准似乎没有理由在这方面区别对待const void
和void
。
一方面,Microsoft可能已经得出结论,没有办法使用此x
,因此在找到extern void x
后立即为其发出诊断,而不是在代码尝试使用此x
时发生错误。但是,虽然编译器可以自由地发出其他诊断消息,但它应该接受符合标准的程序。也就是说,对于符合 C 标准的编译器,诊断可能是警告,但可能不是阻止编译的错误。
补充说明
注意到一元&
的约束允许"[]
或一元*
运算符的结果",我对此进行了测试:
static void foo(void *p)
{
printf("%pn", &*p);
}
在这里,*p
本身是void
类型的左值,这对于&
是允许的,因为约束特别允许它,而&x
似乎是一个非常相似的表达式,取void
的地址,但约束不允许它,因为x
既不是左值也不是*
的结果。好奇。