extern 关键字对 C 函数的影响
Posted
技术标签:
【中文标题】extern 关键字对 C 函数的影响【英文标题】:Effects of the extern keyword on C functions 【发布时间】:2010-10-25 19:00:51 【问题描述】:在 C 语言中,我没有注意到在函数声明之前使用的 extern
关键字的任何影响。
起初,我认为在单个文件中定义extern int f();
时强制您在文件范围之外实现它。但是我发现两者都有:
extern int f();
int f() return 0;
和
extern int f() return 0;
编译得很好,没有来自 gcc 的警告。我用gcc -Wall -ansi
;它甚至不会接受//
cmets。
在函数定义之前使用extern
有什么影响吗?或者它只是一个对函数没有副作用的可选关键字。
在后一种情况下,我不明白为什么标准设计者会选择在语法中添加多余的关键字。
编辑:澄清一下,我知道extern
在变量中的用法,但我只是在函数中询问extern
。
【问题讨论】:
根据我在尝试将其用于某些疯狂的模板目的时所做的一些研究,大多数编译器不支持 extern,因此实际上并没有做任何事情。 它并不总是多余的,请参阅我的回答。任何时候您需要在模块之间共享您不希望在公共标头中的内容时,它都非常有用。然而,在公共头文件中“外部化”每个函数(使用现代编译器)几乎没有好处,因为他们可以自己解决。 @Ed .. 如果 volatile int foo 是 foo.c 中的全局变量,并且 bar.c 需要它,则 bar.c 必须将其声明为 extern。它确实有它的优点。除此之外,您可能需要共享一些您不希望在公共标头中公开的功能。 另见:***.com/questions/496448/… @Barry 如果有的话,另一个问题是这个问题的副本。 2009 年与 2012 年 【参考方案1】:在 C 中,函数被隐式定义为 extern
,无论关键字是否实际声明。
所以,代码:
int f() return 0;
编译器会将其视为
extern int f() return 0;
本质上,典型函数定义与前面带有extern
关键字的函数定义之间没有语义差异,如本例所示。你可以在https://www.geeksforgeeks.org/understanding-extern-keyword-in-c/阅读更深入的解释。
【讨论】:
请更具体。 请告诉我一件事,因为在定义中,内存分配已经完成。我们知道 "extern int x=4" 是无效的,那么我们如何在 c 中的函数定义之前使用 extern 关键字呢?【参考方案2】:我们有两个文件,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”,您是在告诉编译器,它后面的任何内容都将在链接时找到(非静态);不要在当前通行证中为它保留任何东西,因为它稍后会遇到。在这方面,函数和变量被平等对待。
如果您需要在模块之间共享一些全局并且不想将其放在/初始化它在标头中,这非常有用。
从技术上讲,库公共头文件中的每个函数都是“外部”,但是根据编译器的不同,将它们标记为这样几乎没有好处。大多数编译器可以自己解决这个问题。如您所见,这些函数实际上是在其他地方定义的。
在上面的例子中,main() 只会打印一次 hello world,但会继续输入 bar_function()。另请注意, bar_function() 在此示例中不会返回(因为它只是一个简单的示例)。想象一下 stop_now 在服务信号时被修改(因此,不稳定),如果这看起来不够实用的话。
外部对象对于信号处理程序、您不想放入标头或结构中的互斥体等非常有用。大多数编译器会优化以确保它们不会为外部对象保留任何内存,因为他们知道他们会将其保留在定义对象的模块中。然而,在对公共函数进行原型设计时,用现代编译器指定它并没有什么意义。
希望有帮助:)
【讨论】:
如果没有 bar_function 之前的 extern,您的代码将可以正常编译。 @Elazar ,注意损坏的编译器。 @Elazar,我已经注意到,将函数原型设计为 extern 对于现代编译器来说是毫无用处的 :) @Tim:那么你没有使用我使用的代码的可疑特权。这有可能发生。有时标头还包含静态函数定义。这是丑陋的,而且 99.99% 的时间都是不必要的(我可能会偏离一个或两个数量级,夸大了它的必要性)。它通常发生在人们误解为只有在其他源文件将使用该信息时才需要标头;标头(ab)用于存储一个源文件的声明信息,并且不希望其他文件包含它。有时,它会出于更扭曲的原因而发生。 @Jonathan Leffler - 确实很可疑!我之前继承了一些相当粗略的代码,但老实说,我从未见过有人在标题中放置静态声明。不过,听起来你的工作相当有趣:)【参考方案3】:IOW,extern 是多余的,什么都不做。
这就是为什么,10 年后:
像Coccinelle 这样的代码转换工具将倾向于flagextern
in function declaration 进行删除;
git/git
之类的代码库遵循该结论并从其代码中删除 extern
(对于 Git 2.22,2019 年第二季度)。
参见commit ad6dad0、commit b199d71、commit 5545442(2019 年 4 月 29 日)Denton Liu (Denton-L
)。(由 Junio C Hamano -- gitster
-- 合并于 commit 4aeeef3,2019 年 5 月 13 日)
*.[ch]
:使用spatch
从函数声明中删除extern
已推动从函数声明中删除
extern
。删除一些被 Coccinelle 捕获的函数声明的“
extern
”实例。 请注意,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 (Denton-L
) 的commit 7027f50(2019 年 9 月 4 日)。(由 Denton Liu -- Denton-L
-- 合并于 commit 7027f50,2019 年 9 月 5 日)
compat/*.[ch]
:使用 spatch 从函数声明中删除extern
在 5545442 (
*.[ch]
: 从函数声明中删除extern
使用 spatch, 2019-04-29, Git v2.22.0-rc0),我们使用spatch
从函数声明中删除了 externs,但我们故意排除了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__
和可变参数时遇到了一些麻烦,所以 我们运行以下命令以确保没有剩余的更改被留下 后面:$ 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
都会被丢弃。
见commit 65904b8(2019 年 9 月 30 日)Emily Shaffer (nasamuffin
)。
求助者:Jeff King (peff
).
请参阅commit 8464f94(2019 年 9 月 21 日)Denton Liu (Denton-L
)。
帮助者:Jeff King (peff
)。(由 Junio C Hamano -- gitster
-- 合并于 commit 59b19bc,2019 年 10 月 7 日)
promisor-remote.h
:从函数声明中删除extern
在创建此文件期间,每次引入新的函数声明时,都会包含一个
extern
。 但是,从5545442 开始(*.[ch]
: removeextern
from function declarations usingspatch
, 2019-04-29, Git v2.22.0-rc0),我们一直在积极尝试防止使用 externs在函数声明中,因为它们是不必要的。删除这些虚假的
extern
s。
【讨论】:
【参考方案4】:您需要区分两个独立的概念:函数定义和符号声明。 “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
很有用。
让我解释一下:
假设文件 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 关键字避免了这种隐式定义,从而有助于避免错误。
对于函数,在函数声明中,关键字确实是多余的。函数声明没有隐式定义。
【讨论】:
您是要删除 -3 段中的int i = 2
行吗?声明是否正确,看到int i;
,编译器将为该变量分配内存,但看到extern int i;
,编译器不会分配内存而是在别处搜索变量?
实际上如果省略“extern”关键字,程序将无法编译,因为在 a.c 和 b.c 中重新定义了 i(由于 a.h)。【参考方案5】:
它不起作用的原因是因为在链接时链接器会尝试解析外部定义(在您的情况下为extern int f()
)。不管是在同一个文件中找到还是在不同文件中找到,只要找到就行了。
希望这能回答你的问题。
【讨论】:
那为什么允许在任何函数中添加extern
?
请不要在您的帖子中放置不相关的垃圾邮件。谢谢!【参考方案6】:
声明一个函数 extern 意味着它的定义将在链接时解析,而不是在编译时解析。
与未声明为 extern 的常规函数不同,它可以在任何源文件中定义(但不能在多个源文件中定义,否则您将收到链接器错误,说明您已经给出了函数的多个定义),包括它被声明为 extern 的那个。所以,在你的情况下,链接器在同一个文件中解析函数定义。
我不认为这样做会有多大用处,但是进行此类实验可以更好地了解该语言的编译器和链接器的工作原理。
【讨论】:
IOW,extern 是多余的,什么都不做。这么说就清楚多了。 @ElazarLeibovich 我刚刚在我们的代码库中遇到了类似的案例,并得出了相同的结论。所有这些通过这里的答案都可以总结在你的一个班轮中。它没有实际效果,但可能对可读性很好。很高兴在网上见到你,而不仅仅是在聚会上:)【参考方案7】: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) 如果先前声明指定 internal 或 外部链接,后面声明时标识符的链接与 在先前声明中指定的链接。如果没有可见的先前声明,或者如果先前的声明 声明不指定链接,则标识符有外部链接。
5 如果函数标识符的声明没有存储类说明符,则其链接 完全确定就好像它是使用存储类说明符 extern 声明的。如果 对象标识符的声明具有文件范围且没有存储类说明符, 它的链接是外部的。
【讨论】:
它是标准的,还是你只是告诉我一个典型的编译器的行为?如果是标准,我会很高兴获得该标准的链接。不过谢谢! 这是标准行为。 C99 草案可在此处获得:open-std.org/JTC1/SC22/WG14/www/docs/n1256.pdf>。但实际标准不是免费的(草案对于大多数用途来说已经足够好了)。 我刚刚在 gcc 中测试了它,它是 "extern int h();static int h() return 0;" 和 "int h();static int h() return 0 ;" 接受相同的警告。是只有 C99 而不是 ANSI?你能把草案中的确切部分介绍给我吗,因为这似乎不适用于 gcc。 再次检查。我对 gcc 4.0.1 进行了同样的尝试,但在应该出现的地方出现了错误。如果您无法访问其他编译器,也可以尝试使用 comeau 的在线编译器或 codepad.org。阅读标准。 @dirkgently,我真正的问题是使用带有函数声明的 exetrn 有什么影响,如果没有,为什么可以将 extern 添加到函数声明中。答案是否定的,没有效果,而且曾经有过使用不那么标准的编译器的效果。【参考方案8】:内联函数有special rules 关于extern
的含义。 (请注意,内联函数是 C99 或 GNU 扩展;它们不在原始 C 中。
对于非内联函数,extern
不需要,因为它默认开启。
请注意,C++ 的规则是不同的。例如,您要从 C++ 调用的 C 函数的 C++ 声明中需要 extern "C"
,而对于 inline
有不同的规则。
【讨论】:
这是这里唯一一个既正确又实际回答问题的答案。【参考方案9】:据我记得标准,所有函数声明默认都被认为是“extern”,所以没有必要明确指定。
这并没有使这个关键字无用,因为它也可以与变量一起使用(在这种情况下 - 它是解决链接问题的唯一解决方案)。但是有了这些功能 - 是的,它是可选的。
【讨论】:
那么作为标准设计者,我将禁止将 extern 与函数一起使用,因为它只会给语法增加噪音。 向后兼容可能会很痛苦。 @ElazarLeibovich 实际上,在这种特殊情况下,禁止会增加语法的噪音。 如何限制关键字添加噪音超出了我的想象,但我想这是一个品味问题。 允许对函数使用“extern”很有用,因为它向其他程序员表明该函数是在另一个文件中定义的,而不是在当前文件中,也没有在其中一个文件中声明包含的标题。【参考方案10】:extern
关键字通知编译器该函数或变量具有外部链接 - 换句话说,它在定义它的文件之外的文件中可见。从这个意义上说,它与static
关键字的含义相反。在定义时放置extern
有点奇怪,因为没有其他文件可以看到定义(或者它会导致多个定义)。通常,您将extern
放在具有外部可见性的某个点(例如头文件)的声明中,然后将定义放在其他地方。
【讨论】:
以上是关于extern 关键字对 C 函数的影响的主要内容,如果未能解决你的问题,请参考以下文章