C函数中extern关键字的作用

219
在C语言中,我没注意到在函数声明前使用extern关键字有任何影响。 起初,我认为在单个文件中定义extern int f();会强制你在文件作用域之外实现它。但是我发现这两个声明:
extern int f();
int f() {return 0;}

extern int f() {return 0;}

使用gcc -Wall -ansi编译可以正常完成,没有任何来自gcc的警告。甚至不接受//注释。

在函数定义之前使用extern会有什么影响吗?还是它只是一个可选关键字,对函数没有任何副作用。

如果是后一种情况,我不明白为什么标准设计者要在语法中加入多余的关键字。

编辑:澄清一下,我知道在变量中使用extern是有用的,但我只是问有关extern函数中的使用。


7
看我的回答,它并不总是多余的。每当你需要在模块之间共享一些东西,而不想在公共头文件中公开它时,它非常有用。然而,在现代编译器下,对于公共头文件中的每个函数都使用 'extern' 关键字几乎没有任何好处,因为编译器可以自己解决这个问题。 - Tim Post
1
@Ed..如果volatile int foo是foo.c中的全局变量,并且bar.c需要它,则bar.c必须将其声明为extern。这确实有其优点。除此之外,您可能需要共享一些函数,但不希望在公共头文件中公开。 - Tim Post
在同一个文件中,extern声明和函数定义重复:https://dev59.com/gWkw5IYBdhLWcg3wGWtk - user246672
2
@Barry 如果有的话,另一个问题就是这个问题的重复。2009年与2012年。 - Elazar Leibovich
当然。在这种情况下,年龄无关紧要,并且是一种新奇性偏见的呼吁,因为它是对System III C功能的讨论。另一个则有极其简洁的TL;DR回答。 - user246672
显示剩余2条评论
10个回答

175
我们有两个文件,foo.c和bar.c。
以下是foo.c。
#include <stdio.h>

volatile unsigned int stop_now = 0;
extern void bar_function(void);

int main(void)
{
  while (1) {
     bar_function();
     stop_now = 1;
  }
  return 0;
}

现在,这是bar.c

#include <stdio.h>

extern volatile unsigned int stop_now;

void bar_function(void)
{
   if (! stop_now) {
      printf("Hello, world!\n");
      sleep(30);
   }
}

我们可以看到,foo.c和bar.c之间没有共享的头文件,但是当在链接时bar.c需要在foo.c中声明某些东西,而foo.c需要从bar.c中获取一个函数。

使用“extern”,你告诉编译器后面的内容将会在链接时找到(非静态);在当前的编译过程中不要为它预留任何空间,因为它将在后面遇到。在这方面,函数和变量被同等对待。

如果需要在模块之间共享一些全局变量,且不想在头文件中放置/初始化它,则此方法非常有用。

技术上,库公共头文件中的每个函数都是“extern”,但是标记它们并没有多少或者根本没有好处,具体取决于编译器。大多数编译器可以自己找出来。如你所见,这些函数实际上是定义在其他地方的。

在上面的例子中,main()只会打印一次hello world,但是会继续进入bar_function()。还要注意,在这个例子中,bar_function()不会返回(因为这只是一个简单的示例)。如果这看起来不够实际,请想象当信号被处理时(因此是易失性的),stop_now被修改。

对于信号处理程序、不想放在头文件或结构中的互斥量等,externs非常有用。大多数编译器将优化以确保不为外部对象保留任何内存,因为它们知道它们将在定义对象的模块中保留内存。然而,一般来说,在原型公共函数时指定它的作用并不大,现代编译器已经能够自己处理。


77
在 bar_function 前面加上 extern,并不影响你的代码编译。 - Elazar Leibovich
2
@Elazar,我已经注意到,使用现代编译器将函数原型化为extern是相当无用的 :) - Tim Post
3
@Tim: 那你还没有享受到与我一同工作的代码所带来的可疑特权。有时,头文件中也包含静态函数定义。这很丑陋,并且99.99%的时间都是不必要的(我可能会夸大它的必要性次数或两个数量级)。当人们误解头文件仅在其他源文件将使用信息时才需要时,通常会发生这种情况。头文件被(滥用)用于存储一个源文件的声明信息,而不期望其他任何文件包含它。偶尔,由于更为扭曲的原因而发生此类情况。 - Jonathan Leffler
4
@Jonathan Leffler - 这确实令人怀疑! 我之前曾经遗传过一些相当草率的代码,但我可以诚实地说,我从未见过有人在头文件中放置静态声明。不过听起来你的工作很有趣和有趣 :) - Tim Post
2
@Razzle 哇,我简直不敢相信你是第一个发现这个问题的人(包括我在内!)这个 bug 竟然悄悄地存在了十多年。真是太神奇了。已经修复 :) - Tim Post
显示剩余19条评论

99
据我所记得的标准,所有函数声明默认都被视为“extern”,因此不需要显式地指定它。
这并不意味着这个关键字没有用处,因为它也可以与变量一起使用(在这种情况下,它是解决链接问题的唯一解决方案)。但对于函数来说,是可选的。

28
作为标准设计者,我将禁止在函数中使用extern,因为它只会给语法增加噪音。 - Elazar Leibovich
8
向后兼容可能会很麻烦。 - MathuSum Mut
3
@ElazarLeibovich 实际上,在这种特殊情况下,“禁止它”才会给语法增加噪音。 - Lightness Races in Orbit
2
限制关键字如何“增加”噪音,我不太明白,但我想这是一个品味问题。 - Elazar Leibovich
2
允许在函数中使用“extern”是有用的,因为它向其他程序员指示该函数在另一个文件中定义,而不是在当前文件中定义,并且也没有在其中一个包含的头文件中声明。 - DimP
显示剩余3条评论

29
你需要区分两个独立的概念:函数定义和符号声明。"extern"是一个链接修饰符,它向编译器提供一个关于后续引用的符号在哪里定义的提示(提示是“不在这里”)。
如果我写下:
extern int i;

在C文件中的文件范围(在函数块之外),你是在说“这个变量可能在其他地方定义”。
extern int f() {return 0;}

既是函数 f 的声明,也是函数 f 的定义。在这种情况下,定义会覆盖 extern。

extern int f();
int f() {return 0;}

首先是一个声明,接着是定义。
如果你想同时声明和定义一个文件作用域的变量,使用extern是错误的。例如,
extern int i = 4;

根据编译器的不同,会出现错误或警告。

如果你明确不想定义一个变量,使用extern是很有用的。

让我解释一下:

假设文件a.c包含以下内容:

#include "a.h"

int i = 2;

int f() { i++; return i;}

文件 a.h 包含以下内容:
extern int i;
int f(void);

文件 b.c 包含以下内容:
#include <stdio.h>
#include "a.h"

int main(void){
    printf("%d\n", f());
    return 0;
}

在头文件中使用extern是有用的,因为它在链接阶段告诉编译器,“这是一个声明,而不是定义”。如果我删除a.c中定义i、为其分配空间并赋值的那一行,程序应该在编译时出现未定义的引用错误。这告诉开发者他引用了一个变量,但尚未定义它。另一方面,如果我省略了"extern"关键字,并删除int i = 2这一行,程序仍然可以编译 - i将被定义为默认值0。
文件作用域变量如果您没有显式赋值,将隐式定义为默认值0或NULL - 这与您在函数顶部声明的块作用域变量不同。extern关键字避免了这种隐式定义,从而帮助避免错误。
对于函数来说,在函数声明中,这个关键字确实是多余的。函数声明没有隐式定义。

你是不是想在第三段中删除 int i = 2 这一行?并且,如果声明了 int i;,编译器会为该变量分配内存,但如果声明了 extern int i;,编译器将不会为其分配内存,而是在其他地方寻找该变量,这种说法正确吗? - Frozen Flame
实际上,如果省略了 "extern" 关键字,程序将无法编译,因为 a.c 和 b.c 中的 i 会被重新定义(由于 a.h)。 - Nixt

19

extern关键字在不同的环境下有不同的形式。如果一个声明可用,extern关键字会将链接性设置为翻译单元中之前指定的那个链接性。如果没有这样的声明,extern则指定外部链接。

static int g();
extern int g(); /* g has internal linkage */

extern int j(); /* j has tentative external linkage */

extern int h();
static int h(); /* error */

以下摘自C99草案(n1256)相关段落:

6.2.2 标识符的链接属性

[...]

4 如果在一个作用域中声明了一个带有存储类别说明符extern的标识符,并且先前已经有该标识符的声明可见,23)如果之前的声明指定为内部或外部链接,则稍后的声明中该标识符的链接与之前声明中指定的链接相同。如果没有先前的声明可见,或者先前的声明没有指定链接,则该标识符具有外部链接。

5 如果函数的标识符声明没有存储类别说明符,则其链接属性的确定方法与它声明为具有存储类别说明符extern时完全相同。如果对象的标识符在文件范围内并且没有存储类别说明符,则它的链接属性为外部链接。


这是标准行为。C99草案可在此处获取:http://www.open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf。实际标准并非免费(草案对于大多数目的已足够)。 - dirkgently
1
我刚在gcc中测试了一下,发现"extern int h();static int h() {return 0;}"和"int h();static int h() {return 0;}"都被接受并产生了相同的警告。这只适用于C99而不是ANSI吗?你能否引用草案中的确切部分,因为这似乎对于gcc来说并不正确。 - Elazar Leibovich
3
@dirkgently,我的真正问题是在函数声明中使用extern是否有任何影响,如果没有为什么可以在函数声明中添加extern。答案是没有影响,并且在不太标准的编译器中曾经有过影响。 - Elazar Leibovich
@ElazarLeibovich "使用函数声明时是否使用extern会有影响":好的,不要忘记上面引用的标准所说的。如果该函数在作用域中已经有现有的声明/定义,则'extern'可以表示静态函数。我认为,如果您特别想要外部链接,则不需要使用任何内容,但如果您不确定或更喜欢在程序的其他地方控制该旋钮,则使用'extern'。因此,对于函数来说,在声明(C99)中使用'extern'似乎总是安全的,但实际上可能会引用静态(文件范围)函数,具体取决于上下文。 - Jose_X
@ElazarLeibovich "使用extern与函数声明有什么影响吗?" [我更新了早期的评论,因为我犯了一个错误。正确的版本是:] 答案似乎是,在C99中对于函数而言,使用extern或不使用都无关紧要。在任何情况下,该函数可能具有文件范围限制,即可能具有外部链接(尽管通常它将具有外部链接)。在gcc中,请确保使用-std=C99,尽管我认为这个特定功能至少在一些年前是不正确的。..而C89则不同。 - Jose_X
显示剩余7条评论

16

内联函数有关于 extern 的特殊规定。(请注意,内联函数是 C99 或 GNU 扩展;它们不在原始的 C 中。)

对于非内联函数,默认值已经为 extern,因此不需要再次声明。

请注意,C++ 的规则是不同的。例如,必须在从 C++ 调用的 C 函数的 C++ 声明中使用 extern "C",而且还有关于 inline 的不同规则。


2
这是唯一一个既正确又实际回答了问题的答案。 - user269597

11

换句话说,extern是多余的,没有任何作用。

这就是为什么,10年后:

请查看 提交记录 ad6dad0, 提交记录 b199d71, 提交记录 5545442 (2019年4月29日) 作者是 Denton Liu (Denton-L)
(由 Junio C Hamano -- gitster --提交记录 4aeeef3 中合并, 2019年5月13日)

*.[ch]: remove extern from function declarations using spatch

There has been a push to remove extern from function declarations.

Remove some instances of "extern" for function declarations which are caught by Coccinelle.
Note that Coccinelle has some difficulty with processing functions with __attribute__ or varargs so some extern declarations are left behind to be dealt with in a future patch.

This was the Coccinelle patch used:

  @@
    type T;
    identifier f;
    @@
    - extern
    T f(...);

and it was run with:

  $ git ls-files \*.{c,h} |
    grep -v ^compat/ |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place

这并不总是直接的:
请参见commit 7027f50(由Denton Liu (Denton-L)于2019年9月4日提交)。 (由Denton Liu -- Denton-L --commit 7027f50中合并,2019年9月5日)

compat/*.[ch]: remove extern from function declarations using spatch

In 5545442 (*.[ch]: remove extern from function declarations using spatch, 2019-04-29, Git v2.22.0-rc0), we removed externs from function declarations using spatch but we intentionally excluded files under compat/ since some are directly copied from an upstream and we should avoid churning them so that manually merging future updates will be simpler.

In the last commit, we determined the files which taken from an upstream so we can exclude them and run spatch on the remainder.

This was the Coccinelle patch used:

@@
type T;
identifier f;
@@
- extern
  T f(...);

and it was run with:

$ git ls-files compat/\*\*.{c,h} |
    xargs spatch --sp-file contrib/coccinelle/noextern.cocci --in-place
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

Coccinelle has some trouble dealing with __attribute__ and varargs so we ran the following to ensure that no remaining changes were left behind:

$ git ls-files compat/\*\*.{c,h} |
    xargs sed -i'' -e 's/^\(\s*\)extern \([^(]*([^*]\)/\1\2/'
$ git checkout -- \
    compat/regex/ \
    compat/inet_ntop.c \
    compat/inet_pton.c \
    compat/nedmalloc/ \
    compat/obstack.{c,h} \
    compat/poll/

请注意,从Git 2.24(2019年第四季度)开始,任何不必要的extern都将被删除。

请参阅 提交 65904b8 (2019年9月30日),作者为Emily Shaffer (nasamuffin)
协助者:Jeff King (peff)
请参阅 提交 8464f94 (2019年9月21日),作者为Denton Liu (Denton-L)
协助者:Jeff King (peff)
(由Junio C Hamano -- gitster --合并于提交 59b19bc,2019年10月7日)

promisor-remote.h:从函数声明中删除extern

在创建此文件时,每次引入新的函数声明时,都包含一个extern
然而,从5545442*.[ch]:使用spatch从函数声明中删除extern,2019-04-29,Git v2.22.0-rc0)开始,我们一直在积极尝试防止在函数声明中使用extern,因为它们是不必要的。

请删除这些多余的extern


3
extern 关键字告诉编译器函数或变量具有外部链接性 - 换句话说,它可以从定义它的文件以外的文件中访问。从这个意义上讲,它与 static 关键字的含义相反。在定义时使用 extern 有点奇怪,因为没有其他文件能够看到该定义(否则会导致多个定义)。通常情况下,将 extern 放置在具有外部可见性的某些声明(例如头文件)中,并将其定义放在其他地方。

2
声明一个函数为extern意味着它的定义将在链接时解析,而不是在编译期间解析。与常规函数不同,它没有声明为extern,可以在任何源文件中定义(但不能在多个源文件中定义,否则会出现链接器错误,说明您已经给出了该函数的多个定义),包括声明为extern的源文件。因此,在您的情况下,链接器会在同一文件中解析函数定义。我认为这样做并没有太大用处,但进行这种实验可以更好地了解语言的编译器和链接器如何工作。

3
“ IOW,extern是多余的,没有任何作用。如果您这样表述会更清晰。” - Elazar Leibovich
@ElazarLeibovich 我们的代码库中遇到了类似的情况,得出了相同的结论。这里所有的答案都可以归纳为你的一行代码。它没有实际效果,但对于可读性可能会有所帮助。 很高兴在线上见到你,而不仅仅是在聚会中 :) - Aviv

2
在C语言中,函数默认情况下被隐式定义为extern,无论是否实际声明了该关键字。
因此,以下代码:
    int f() {return 0;}

编译器会将其视为:
    extern int f() {return 0;}

基本上,普通函数定义和使用 extern 关键字的函数定义没有语义上的区别,就像这个例子一样。您可以在https://www.geeksforgeeks.org/understanding-extern-keyword-in-c/阅读更深入的解释。

请更具体一些。 - Abhishek Dutt
请告诉我一件事,由于在定义中进行了内存分配。我们知道 "extern int x = 4" 不是有效的,那么在 C 中如何在函数定义之前使用 extern 关键字呢? - Abhishek Jaiswal

1
它没有效果的原因是因为在链接时,链接器尝试解析extern定义(在您的情况下为extern int f())。无论它在同一文件中还是在不同的文件中找到它,只要找到就可以了。
希望这回答了您的问题。

1
那么为什么允许在任何函数中添加 extern 呢? - Elazar Leibovich
2
请勿在您的帖子中发布无关的垃圾邮件。谢谢! - Mac

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