为什么变量需要extern关键字,而函数不需要?

4

在C语言中,默认情况下,文件作用域内的变量和函数都具有外部链接。为什么变量需要关键字extern,而在其他地方定义的函数不需要呢?请注意,这个问题有两个方面:考虑到文件作用域的声明默认具有外部链接:

  • 为什么历史上对于函数的声明是否有extern没有区别?或者客观地说,为什么不需要这样的区别?
  • 为什么历史上对于变量的声明是否有extern有区别?或者客观地说,为什么需要这样的区别?

我们可以使用以下两个源文件(tu代表“翻译单元”)来进行最小化示例。

tu1.c:

extern int i = 123;

tu2.c:

#include <stdio.h>

extern int i;

int main(void) {
  //extern int i;
  ++i;
  printf("%d\n", i);
  return 0;
}

我们可以使用GCC编译它们,方法如下:
gcc -c tu1.c
gcc -c tu2.c
gcc -o myprogram tu1.o tu2.o

(GCC在第一条指令中发出警告'i' initialized and declared 'extern',因为它错误地认为extern“应该保留给非定义性声明”。我们可以安全地忽略它。)

让我们比较编译器对源代码稍微不同版本的处理方式:

  • tu2.c的文件作用域中添加extern int i;(对以上代码无更改):
    myprogram的输出是124,符合预期。
  • main中(而不是在文件作用域)添加extern int i;
    这同样有效(但这种风格not recommended:"一个函数不应该需要使用extern声明变量。")。
  • tu2.c中未声明i
    这将无法工作;对于行i++;,我们会得到以下错误:'i' undeclared (first use in this function)
  • tu2.c的文件作用域中添加int i;而没有extern
    这将失败,并出现错误:multiple definition of `i'
我在思考最后一个案例的理由:如果裸的(没有externint i;默认为外部链接,为什么我们需要显式提供关键字extern答案似乎在标准中(C99:6.9.2外部对象定义)中,根据该标准,int i;是一个试探性定义,在编译时实例化为实际定义。逻辑是提供extern关键字指示编译器将结果构造视为不是定义的声明。但是如果是这样的话:为什么对于函数原型,相同的逻辑不适用,因为众所周知extern是隐含的?

我有一种感觉,正确的答案与“C中试探性定义背后的理由是什么?”相关或接近,但我想知道如果变量和函数在上述方面被视为相同,会发生什么问题。


有一个关于C++的类似问题和一篇相关的Peter Goldsborough的文章

针对习惯于使用C++编程的人:

  • 在C中,文件作用域(包括const)变量和函数默认为外部链接。
  • 在C++中,有一个微妙的区别,即const全局变量默认为内部链接。

3
我基本上认为这是一种方便方式,可以节省一些打字。 没有歧义:函数声明的最后一个 ) 后面的 ; 告诉您正在查看声明,而不是定义。因此,可以假定为 extern。 (对于变量有一个旧的“共同模型”,在其中 extern 也基本上是可选项,但它已经不再流行了。) - Steve Summit
2
函数默认情况下是 extern 类型的,这是由 C 标准规定的。 - 0___________
2
函数原型并未定义它。变量上没有这样的东西。 int x; 将总是定义变量 xint foo(void) 不定义函数 foo - 0___________
@SteveSummit 这是一个有趣的观点。但是我们是否可以类似地检查变量声明后是否有=?无论如何,在这种情况下,我假设问题与告诉编译器该符号已在其他地方定义或将要定义有关(也就是说,这涉及内存分配,它区分了定义和声明)。我可能漏掉了一些东西。总之,为什么我们不能默认情况下对变量(在文件范围内)假定为extern呢?这会给编译器带来不便吗?或者,如果这是关于向后兼容性,具体是什么? - Lover of Structure
@0___________ 是的,但这是一个语言设计问题。我知道标准,但为什么它是这样的呢? - Lover of Structure
2
@LoverofStructure,简单来说,函数定义与声明的区别在于它包含了函数主体。变量的声明/定义则不同,你需要使用额外的关键字来指示你正在做什么——声明还是定义。 - 0___________
3个回答

5
因为函数声明的形式表明它是否是一个定义。
没有主体的函数声明不是一个定义:
void foo(void);

带有函数体的函数声明就是定义:

void foo(void) { }

在使用int x;时,存在歧义。早期的C实现方式不同。我们可以通过指定初始化器来确切地标记它为定义:

int x = 0;

然而,仅仅使用int x;,一些C语言实现将其视为定义(并且可能允许多个这样的定义合并为一个,因此在多个翻译单元中包含的头文件中出现的声明将导致链接程序中只有一个定义)。
为了消除歧义,使用没有初始化器的extern使其成为一个声明而不是定义。
如果我们从头开始设计C语言,我们可以制定一个规则,即在任何函数之外的int x;是一个声明而不是定义,而int x = 0;是一个定义。因此,语言的当前状态并非是逻辑上的必然性;它是语言发展历史的结果。
(然而,这样的规则与我们在函数内部使用声明的方式相矛盾。在函数内部,我们习惯于int x;是一个定义。如果我们采用上述规则来处理函数外部的声明,我们要么必须接受函数内部和函数外部声明之间的对比,要么必须在函数内部也采用相同的规则。)

1
如果我们从头开始设计 C 语言,我们可以制定一个规则:在任何函数之外的 int x; 是一个声明而不是定义,而 int x = 0; 则是一个定义。在我看来,这样的规则会让代码难以阅读。 - 0___________
如果我从头开始设计C语言,我会将int x;int x = 0;放在任何函数之外视为语法错误。:-D - DevSolar
毕竟,现在没有人会从头开始设计C语言了:几十年前已经完成了。我们现在要做的就是修复可以修复的问题,而不会破坏(太多)兼容性。已经有很多受C启发的语言,从头开始设计,以克服C的问题和限制,或多或少地可能会添加一些其他类型的问题。我认为这是一种相当正常的风险。 - LuC
顺便说一下,使用C11/C17,extern int i3 = 3;是合法的(我从标准的第6.9.2条款中选了这个例子),这只会增加更多的混淆... - LuC
1
@Luc:C标准对于extern int i3 = 3;不需要进行诊断。Clang选择发出警告,这是标准允许的。 - Eric Postpischil
显示剩余2条评论

1

需要明确区分声明定义

函数原型显然是一个声明,而不是定义。

int x;是定义,因此需要其他内容来向编译器表明我们不定义对象x,只是声明它。extern int x;就可以做到这一点。


1

一个没有函数体的函数声明显然只是一个声明。这就是为什么存储类说明符extern是隐式的。

另一方面,一个没有extern的变量声明也是一个定义。在变量声明T x;中添加extern意味着“某个地方应该有一个类型为T的变量x的定义”。

如果你练习模块化编程,那么extern变量声明就没有用处,因为所有在函数外声明的变量都应该具有static存储类,并通过模块API中的一个函数访问。但在某些情况下,您可能需要直接访问或分配变量的额外效率,但这时extern声明应放置在模块的头文件中。


网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接