extern关键字对C函数的影响


171

在C语言中,我没有注意到extern函数声明之前使用的关键字的任何作用。起初,我认为extern int f();在单个文件中定义时会迫使您在文件范围之外实现它。但是我发现两者:

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

extern int f() {return 0;}

编译就很好,没有来自gcc的警告。我用过gcc -Wall -ansi; 它甚至不会接受//评论。

使用extern before函数定义有什么影响吗?还是只是一个可选关键字,对功能没有副作用。

在后一种情况下,我不明白为什么标准设计者选择用多余的关键字来填充语法。

编辑:为澄清起见,我知道externin变量的用法,但我只问externin 函数


根据我在尝试将其用于一些疯狂的模板目的时所做的研究,大多数编译器都没有以预期的形式支持extern,所以实际上并没有做任何事情。
Ed James

4
它并不总是多余的,请参阅我的回答。每当您需要在公共头文件中不需要的模块之间共享某些内容时,它都是非常有用的。但是,(使用现代编译器)在公共头文件中“外部化”每个函数几乎没有好处,因为他们可以自行解决。
蒂姆·波斯特

@Ed ..如果volatile int foo是foo.c中的全局变量,而bar.c需要它,则bar.c必须将其声明为extern。它确实有其优势。除此之外,您可能需要共享一些不想在公共头文件中公开的功能。
蒂姆·波斯特


2
@Barry如果有的话,另一个问题就是这个问题的重复。2009年vs 2012年
Elazar Leibovich

Answers:


138

我们有两个文件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)
{
   while (! stop_now) {
      printf("Hello, world!\n");
      sleep(30);
   }
}

如您所见,在foo.c和bar.c之间没有共享头,但是bar.c在链接时需要在foo.c中声明的内容,而foo.c在链接时需要来自bar.c的函数。

通过使用“ extern”,您告诉编译器在链接时将发现其后的内容(非静态的)。请不要在当前通行证中为其保留任何内容,因为稍后会遇到。在这方面,功能和变量被平等对待。

如果您需要在模块之间共享一些全局变量,并且不想将其放在标头中/将其初始化,这将非常有用。

从技术上讲,库公共头文件中的每个函数都是“外部”,但是根据编译器的不同,将它们标为此类几乎没有好处。大多数编译器可以自行解决。如您所见,这些功能实际上是在其他地方定义的。

在上面的示例中,main()仅打印一次hello world,但继续输入bar_function()。还要注意,在此示例中,bar_function()不会返回(因为它只是一个简单的示例)。试想一下,如果信号处理似乎不够实用,则在服务信号时(因此,易失性)会修改stop_now。

对于信号处理程序,不想放在标头或结构中的互斥体之类的东西,外部变量非常有用。大多数编译器都会进行优化,以确保它们不为外部对象保留任何内存,因为它们知道它们将保留在定义对象的模块中。但是,再一次,在对公共函数进行原型设计时,用现代编译器指定它毫无意义。

希望有帮助:)


56
您的代码将在bar_function之前没有extern的情况下正常编译。
Elazar Leibovich,2009年

2
@Tim:那么您就没有使用我使用的代码的可疑特权。这有可能发生。有时标头还包含静态函数定义。这很丑陋,而且不必要的时间为99.99%(我可能会减少一两个或两个数量级,高估了必要的频率)。当人们误解了仅当其他源文件将使用该信息时才需要标头时,通常会发生这种情况。标头用于存储一个源文件的声明信息,并且预期没有其他文件包含它。有时,它是出于更扭曲的原因而发生的。
乔纳森·莱夫勒

2
@Jonathan Leffler-确实值得怀疑!我以前继承了一些相当粗略的代码,但是老实说,我从未见过有人在标头中放置静态声明。听起来,您的工作似乎很有趣,也很有趣:)
Tim Post

1
“函数原型不在标头中”的缺点在于,您不会自动独立检查in中的函数定义bar.c和in中的声明之间的一致性foo.c。如果在中声明了函数,foo.h 并且两个文件都包含foo.h,则标头将强制两个源文件之间保持一致。没有它,如果bar_functionin 的定义bar.c改变了,但是in 的声明foo.c没有改变,那么在运行时就会出错。编译器无法发现问题。正确使用标头后,编译器将发现问题。
乔纳森·勒夫勒

1
函数声明中的extern像“ unsigned int”中的“ int”一样是多余的。在原型不是正向声明时使用“ extern”是一个好习惯。但是,除非任务是边缘情况,否则它实际上应该放在没有“ extern”的标头中。 stackoverflow.com/questions/10137037/...

82

就我所记得的标准而言,默认情况下所有函数声明都被视为“ extern”,因此无需明确指定它。

但这并没有使该关键字无用,因为它也可以与变量一起使用(在这种情况下,这是解决链接问题的唯一解决方案)。但是有了功能-是的,它是可选的。


21
然后,作为标准设计师,我将禁止在函数中使用extern,因为这只会给语法增加噪音。
Elazar Leibovich,2009年

3
向后兼容可能会很麻烦。
MathuSum Mut

1
@ElazarLeibovich实际上,在这种情况下,不允许这样做会增加语法的噪音。
Lightness Races in Orbit

1
限制关键字如何增加噪音已经超出了我的范围,但我想这只是一个问题。
Elazar Leibovich

但是,允许对函数使用“ extern”很有用,因为它向其他程序员表明该函数是在另一个文件中定义的,而不是在当前文件中定义的,并且也没有在包含的头文件中声明。
DimP

23

您需要区分两个单独的概念:函数定义和符号声明。“ extern”是一个链接修饰符,它是向编译器提供的有关后面定义的符号定义位置的提示(提示是“ not here”)。

如果我写

extern int i;

在C文件的文件范围(功能块外部)中,则表示“变量可以在其他位置定义”。

extern int f() {return 0;}

既是函数f的声明,也是函数f的定义。在这种情况下,定义优先于外部。

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

首先是一个声明,然后是定义。

extern如果要声明并同时定义文件范围变量,则使用错误。例如,

extern int i = 4;

将给出错误或警告,具体取决于编译器。

extern如果您明确希望避免定义变量,则使用很有用。

让我解释:

假设文件ac包含:

#include "a.h"

int i = 2;

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

文件啊包括:

extern int i;
int f(void);

并且文件bc包含:

#include <stdio.h>
#include "a.h"

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

标头中的extern很有用,因为它在链接阶段告诉编译器“这是一个声明,而不是定义”。如果删除ac中定义i的行,为其分配空间并为其分配值,则程序应无法使用未定义的引用进行编译。这告诉开发人员他已经引用了变量,但尚未定义它。另一方面,如果我省略了“ extern”关键字,并删除了该int i = 2行,则程序仍会编译-我将使用默认值0进行定义。

如果未为文件作用域变量明确赋值,则隐式定义为默认值0或NULL-与在函数顶部声明的块作用域变量不同。extern关键字避免了此隐式定义,从而有助于避免错误。

对于函数,在函数声明中,关键字确实是多余的。函数声明没有隐式定义。


您是要删除第int i = 2-3段中的行吗?是否正确声明(看到)int i;,编译器将为该变量分配内存,但是,看到extern int i;,编译器将不分配内存,而是在其他位置搜索变量?
冰冻的火焰

实际上,如果省略“ extern”关键字,由于在ac和bc中重新定义了i(由于ah),该程序将无法编译。
Nixt

15

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声明的完全相同。如果对象标识符的声明具有文件范围且没有存储类说明符,则其链接是外部的。


它是标准的,还是您只是在告诉我典型的编译器行为?如果是标准,我很乐意链接到该标准。但是谢谢!
Elazar Leibovich,2009年

这是标准行为。可在此处获得C99草案:< open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf >。实际的标准并不是免费的(草案对于大多数目的来说已经足够好了)。
2009年

1
我刚刚在gcc中对其进行了测试,并且“ extern int h(); static int h(){return 0;}”和“ int h(); static int h(){return 0;}”均被接受警告。仅C99而不是ANSI吗?您能否请我参考草案中的确切部分,因为这对于gcc似乎并不正确。
Elazar Leibovich,2009年

再检查一遍。我在gcc 4.0.1上尝试了相同的操作,但在应有的位置出现错误。如果您无权使用其他编译器,请尝试使用comeau的在线编译器或codepad.org。阅读标准。
2009年

2
@dirkgently,我真正的问题是将exetrn与函数声明一起使用是否有任何效果,如果没有,为什么可以在函数声明中添加extern?答案是否定的,没有效果,而且使用不那么标准的编译器曾经有效果。
Elazar Leibovich

11

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

对于非内联函数,extern由于默认情况下处于启用状态,因此不需要。

请注意,C ++的规则不同。例如,extern "C"要从C ++调用的C函数的C ++声明中需要,并且有关的规则也不同inline


这是唯一正确且答案正确的答案。
robinjam

4

IOW,extern是多余的,什么也不做。

这就是十年后的原因:

提交ad6dad0提交b199d71提交5545442(2019年4月29日)由丹顿刘(Denton-L
(由Junio C gitsterHamano合并--4aeeef3号提交中,2019年5月13日)

*.[ch]extern使用从函数声明中删除spatch

有人试图extern从函数声明中删除。

extern对于Coccinelle捕获的函数声明,请删除一些“ ” 实例。
请注意,Coccinelle在处理带有__attribute__或varargs的函数时遇到一些困难,因此一些extern声明将留在以后的补丁中进行处理。

这是使用的Coccinelle补丁:

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

它与运行:

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

但是,这并不总是那么简单:

参见Denton Liu()的commit 7027f50(2019年9月4日(通过合并丹顿柳- -提交7027f50,2019年9月5日)Denton-L
Denton-L

compat/*.[ch]extern使用spatch从函数声明中删除

在5545442(*.[ch]extern使用spatch从函数声明中删除,2019-04-29,Git v2.22.0-rc0)中,我们使用了从函数声明中删除了externs,spatch但我们有意排除了下面的文件,compat/因为有些文件是直接从上游复制的,我们应该避免搅动它们,以便手动合并将来的更新将更加简单。

在上一次提交中,我们确定了从上游获取的文件,因此我们可以排除它们并spatch在其余文件上运行。

这是使用的Coccinelle补丁:

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

它与运行:

$ 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在处理__attribute__和varargs时遇到了一些麻烦,因此我们执行以下操作以确保不遗留任何剩余更改:

$ 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都将被丢弃。

参见Emily Shaffer(提交65904b8(2019年9月30日。 帮助:Jeff King(。 参见Denton Liu(提交的8464f94(2019年9月21日。 帮助:Jeff King((由Junio C Hamano合并--commit 59b19bc中,2019年10月7日)nasamuffin
peff
Denton-L
peff
gitster

promisor-remote.hextern从函数声明中删除

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

删除这些虚假extern的。


3

extern关键字通知编译器该函数或变量具有外部链接-换句话说,从定义该函数或变量的文件以外的文件中可见该关键字。在这种意义上,它与static关键字具有相反的含义。extern定义时放一些奇怪,因为没有其他文件可以看到该定义(或者会导致多个定义)。通常,您extern在某个位置具有外部可见性的声明(例如头文件)中将其放置在其他位置。


2

声明函数extern意味着将在链接时而不是在编译期间解析其定义。

与未声明为extern的常规函数​​不同,它可以在任何源文件中定义(但不能在多个源文件中定义),否则会出现链接器错误,提示您已给出函数的多个定义),其中包括因此,在通常情况下,链接程序会在同一文件中解析函数定义。

我认为这样做不是很有用,但是进行此类实验可以更好地了解该语言的编译器和链接器的工作方式。


2
IOW,extern是多余的,什么也不做。如果这样说的话会更清楚。
Elazar Leibovich

@ElazarLeibovich我刚刚在我们的代码库中遇到了类似的案例,并得出了相同的结论。所有通过这里的答案可以总结在您的班轮里。它没有实际效果,但可能会提高可读性。很高兴在网上看到您,而不仅仅是在聚会上见:)
Aviv

1

之所以无效,是因为链接器在链接时尝试解析extern定义(在您的情况下extern int f())。只要在相同文件或不同文件中找到它,都没有关系。

希望这能回答您的问题。


1
那么为什么要允许添加extern任何功能呢?
Elazar Leibovich

2
请不要在帖子中放置无关的垃圾邮件。谢谢!
Mac
By using our site, you acknowledge that you have read and understand our Cookie Policy and Privacy Policy.
Licensed under cc by-sa 3.0 with attribution required.