C - 使用 extern 访问全局变量。案例分析

C - Using extern to access global variable. Case study

我以为 externs 是在编译单元之间共享变量。为什么下面的代码有效?它是如何工作的?这是好习惯吗?

#include <stdio.h>
int x = 50;

int main(void){ 
    int x = 10; 
    printf("Value of local x is %d\n", x);

    {
        extern int x;
        printf("Value of global x is %d\n", x);
    }

    return 0; 
}

打印出来:

Value of local x is 10
Value of global x is 50 

当您使用 extern 关键字时,链接器会在目标文件/库/档案中找到具有匹配名称的符号。简单来说,Symbol 就是函数和全局变量(局部变量只是栈上的一些 space),因此链接器可以在这里发挥它的魔力。

关于这是一个好的做法 - 全局变量通常不被认为是一个好的做法,因为它们会导致意大利面条代码和 'pollute' 符号池。

您可能(或可能不)有兴趣知道 GCC(4.9.1)和 clang(Apple LLVM 版本 6.0(clang-600.0.57)(基于 LLVM 3.5svn))对以下代码的可接受性,这是对问题中代码的微小改编:

#include <stdio.h>
static int x = 50;  // static instead of no storage class specifier

int main(void)
{
    int x = 10;
    printf("Value of local x is %d\n", x);

    {
        extern int x;
        printf("Value of global x is %d\n", x);
    }

    return 0;
}

我调用了源文件ext.c

$ clang -O3 -g -std=c11 -Wall -Wextra -Werror ext.c -o ext
$ gcc   -O3 -g -std=c11 -Wall -Wextra -Werror ext.c -o ext
ext.c: In function ‘main’:
ext.c:9:20: error: variable previously declared ‘static’ redeclared ‘extern’
         extern int x;
                    ^
ext.c: At top level:
ext.c:2:12: error: ‘x’ defined but not used [-Werror=unused-variable]
 static int x = 50;
            ^
cc1: all warnings being treated as errors
$

问题是确定哪个编译器是正确的,因为除非程序表现出未定义的行为,否则它们不可能都是正确的——如果你不厌其烦地读到最后,事实就是如此。

C11标准的相关部分是:

6.2.2 Linkages of identifiers

¶1 An identifier declared in different scopes or in the same scope more than once can be made to refer to the same object or function by a process called linkage.29) There are three kinds of linkage: external, internal, and none.

¶2 In the set of translation units and libraries that constitutes an entire program, each declaration of a particular identifier with external linkage denotes the same object or function. Within one translation unit, each declaration of an identifier with internal linkage denotes the same object or function. Each declaration of an identifier with no linkage denotes a unique entity.

¶3 If the declaration of a file scope identifier for an object or a function contains the storage class specifier static, the identifier has internal linkage.30)

这意味着上面代码中 x 的第一个或最外层的声明(定义)具有内部链接。

4 For an identifier declared with the storage-class specifier extern in a scope in which a prior declaration of that identifier is visible,31) if the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration. If no prior declaration is visible, or if the prior declaration specifies no linkage, then the identifier has external linkage.

这段需要下面详细解构

¶5 If the declaration of an identifier for a function has no storage-class specifier, its linkage is determined exactly as if it were declared with the storage-class specifier extern. If the declaration of an identifier for an object has file scope and no storage-class specifier, its linkage is external.

题中原代码中,第二句说x的第一个声明(定义)有外部链接

¶6 The following identifiers have no linkage: an identifier declared to be anything other than an object or a function; an identifier declared to be a function parameter; a block scope identifier for an object declared without the storage-class specifier extern.

在函数开头声明(定义)的 x 是 'a block scope identifier …',因此没有链接。

¶7 If, within a translation unit, the same identifier appears with both internal and external linkage, the behavior is undefined.

29) There is no linkage between different identifiers.
30) A function declaration can contain the storage-class specifier static only if it is at file scope; see 6.7.1.
31) As specified in 6.2.1, the later declaration might hide the prior declaration.


剖析第 4 段

第 4 段是这里的关键。重述并注释:

4 For an identifier declared with the storage-class specifier extern in a scope in which a prior declaration of that identifier is visible,31)

x 的第三个或最里面的声明是在该标识符的先前声明可见的范围内声明的——int x = 10; 声明是可见的(static int x = 50; 声明是不可见,已被可见声明遮蔽)。脚注指的是 §6.2.1 标识符的范围,但我认为它没有说出任何令人惊讶的事情(但是,如果您认为有必要,我将引用相关段落 — ¶2 和 ¶4)。

if the prior declaration specifies internal or external linkage, the linkage of the identifier at the later declaration is the same as the linkage specified at the prior declaration.

这不适用;先前的声明既没有指定内部链接也没有指定外部链接。

If no prior declaration is visible, or if the prior declaration specifies no linkage,

有一个可见的先前声明,并且该声明未指定任何链接。

then the identifier has external linkage.

所以,最里面的 x 有外部链接,最外面的 x 有内部链接,因此,第 7 段说结果行为是未定义的。这意味着两个编译器都是正确的;如果行为未定义,则任何行为都是正确的——并且允许不同的编译器对什么是正确的有不同的看法,而 GCC 和 clang 表现出不同的看法。总的来说,GCC 的 "it is a problem that should be reported" 观点对程序员来说更安全。

原代码中,最外层的x有外部链接,最里面的x也有外部链接,因此第7段不适用,最里面的声明x 指的是 x.

最外层的声明(和定义)

除了表明解释标准是一项艰苦的工作之外,整个答案(诽谤)还表明使用多个编译器(如果可能的话在不同的平台上)是一个好主意。它为您提供了最大的发现问题的机会。依赖于单个编译器会使您容易遗漏另一个编译器可能发现的问题。

除此之外还有 of Jonathan Leffler, see the answer to the same problem on gcc.gnu.org mailing list

这两个答案都是本着同样的精神给出的,使用了ISO/IEC9899的措辞。