在C99中,没有“静态”或“外部”的“内联”曾经有用吗?


96

当我尝试构建此代码时

inline void f() {}

int main()
{
    f();
}

使用命令行

gcc -std=c99 -o a a.c

我收到一个链接器错误(对的未定义引用f)。如果我使用static inlineextern inline而不是just inline,或者使用进行编译,则该错误消失-O(因此该函数实际上是内联的)。

这种行为似乎在C99标准的6.7.4(6)段中定义:

如果翻译单元中某个函数的所有文件范围声明都包含inline不带的函数说明符extern,则该翻译单元中的定义为内联定义。内联定义不为函数提供外部定义,也不禁止在另一个翻译单元中使用外部定义。内联定义提供了外部定义的替代方法,翻译器可以使用该替代方法在同一翻译单元中实现对该函数的任何调用。未指定对函数的调用是使用内联定义还是使用外部定义。

如果我正确理解了所有这些内容,则具有inline上面示例中定义的功能的编译单元仅在存在相同名称的外部函数的情况下一致地编译,并且我永远不知道自己的函数还是外部函数被调用。

这种行为不是完全愚蠢吗?在inline不使用C99的情况下staticextern在C99中定义功能是否有用?我想念什么吗?

答案摘要

当然,我错过了一些东西,而且行为也不愚蠢。:)

正如Nemo解释的那样,想法是放置函数的定义

inline void f() {}

在头文件中,只有一个声明

extern inline void f();

在相应的.c文件中。仅extern声明触发外部可见的二进制代码的生成。inline在.c文件中确实没有使用-仅在标头中有用。

正如乔纳森回答中所引用的C99委员会基本原理所阐明的那样,inline所有内容都与编译器优化有关,该优化要求函数定义在调用现场可见。这只能通过将定义放在标头中来实现,当然,标头中的定义一定不能在每次编译器看到它时都发出代码。但是由于没有强制编译器实际内联函数,因此外部定义必须存在于某处。




@Earlz:感谢您的链接。我的问题是有关标准的引用一段背后的理由,如果有用不完的情况下,为inlinestaticextern,虽然。不幸的是,这些问题都没有涵盖。
Sven Marnach 2011年

@Nemo:在发布我的问题之前,我先阅读了该问题和答案。同样,我不是在问“它的行为如何?” 而是“这种行为背后的想法是什么”?我很确定我在这里错过了一些东西。
Sven Marnach 2011年

1
是的,这就是为什么我说“边界线”。我个人认为这是一个完全不同的问题
Earlz 2011年

Answers:


40

实际上,这个出色的答案还可以回答您的问题,我认为:

extern内联有什么作用?

这个想法是可以在头文件中使用“内联”,然后在.c文件中使用“外部内联”。“外部内联”就是您如何指示编译器哪个对象文件应包含(外部可见的)生成代码。

[更新,详细说明]

我认为.c文件中“内联”(没有“静态”或“外部”)没有任何用处。但是在头文件中这是有道理的,并且它需要某些.c文件中的相应“外部内联”声明才能实际生成独立代码。


1
谢谢,我开始明白了!
Sven Marnach 2011年

8
是的,直到现在我才真正了解这一点,因此感谢您提出:-)。奇怪的是,非外部的“内联” 定义放在标头中(但不一定会导致生成任何代码),而“外部的内联” 声明位于.c文件中,实际上导致代码被生成。
Nemo

3
这是否意味着我inline void f() {}在标题和extern inline void f();.c文件中进行了编写?因此,实际的函数定义在标头中,而.c文件在这种情况下仅包含声明,与通常的顺序相反?
Sven Marnach 2011年

1
@endolith:我不明白你的问题。我们正在讨论inline 没有 static没有extern。当然static inline可以,但是这不是这个问题和答案的意思。
Nemo 2012年

2
@MatthieuMoy您需要使用-std=c99而不是-std=gnu89
a3f

27

根据标准(ISO / IEC 9899:1999)本身:

附录J.2未定义行为

  • ...
  • 具有外部链接的inline功能是通过功能说明符声明的,但未在同一转换单元(6.7.4)中定义。
  • ...

C99委员会写了基本原理,其中说:

6.7.4功能说明符

C99的一个新的特点:inline关键字,改编自C ++,是一个函数说明符只能在函数声明被使用。对于需要在呼叫现场可见函数定义的程序优化而言,这很有用。(请注意,标准并未尝试指定这些优化的性质。)

如果函数具有内部链接,或者具有外部链接,并且调用与外部定义位于同一转换单元中,则可以确保可见性。在这些情况下, inline关键字在函数的声明或定义中的存在除了表明优先选择该函数的调用优先于未使用inline关键字声明的其他函数的调用之外,没有其他作用。

对于具有外部链接的函数的调用,可见性是一个问题,其中该调用与该函数的定义位于不同的转换单元中。在这种情况下,inline关键字允许包含调用的翻译单元也包含函数的本地或内联定义。

程序可以包含具有外部定义的翻译单元,具有内联定义的翻译单元以及具有声明但没有功能定义的翻译单元。后一个翻译单元中的调用将照常使用外部定义。

函数的内联定义被认为是与外部定义不同的定义。如果func在可见内联定义的地方发生了对具有外部链接的某些函数的调用,则其行为与对__func具有内部链接的另一个函数的调用相同 。合格程序不得依赖于调用哪个函数。这是标准中的内联模型。

符合标准的程序不得依赖于使用内联定义的实现,也不得依赖于使用外部定义的实现。函数的地址始终是与外部定义相对应的地址,但是当使用该地址调用函数时,可以使用内联定义。因此,以下示例可能无法达到预期的效果。

inline const char *saddr(void)
{
    static const char name[] = "saddr";
    return name;
}
int compare_name(void)
{
    return saddr() == saddr(); // unspecified behavior
}

由于实现可能将内联定义用于对其中一个的调用,saddr而将外部定义用于对另一个的调用,因此不能保证等于操作的求值等于1(true)。这表明内联定义中定义的静态对象与外部定义中的相应对象不同。这激发了限制,甚至无法定义const这种类型的非对象。

内联以可以使用现有链接器技术实现的方式添加到标准中,并且C99内联的子集与C ++兼容。这是通过要求将包含内联函数定义的一个翻译单元指定为提供该函数的外部定义的单元来实现的。因为该规范仅由一个声明组成,该声明要么缺少inline关键字,要么包含inlineand extern,所以C ++转换程序也将接受它。

C99中的内联确实以两种方式扩展了C ++规范。首先,如果inline在一个翻译单元中声明了一个函数,则无需inline在其他所有翻译单元中声明该函数 。例如,这允许将库函数内联到库函数中,但只能通过其他地方的外部定义来使用。为外部函数使用包装器函数的替代方法需要一个附加名称;例如:如果翻译人员实际上没有进行内联替换,那么这也可能会对性能产生不利影响。

其次,将内联函数的所有定义“完全相同”的要求替换为以下要求:程序的行为不应取决于是否使用可见的内联定义或外部定义来实现程序的行为。功能。这允许将内联定义专门用于特定翻译单元中。例如,库函数的外部定义可能包含一些参数验证,而对于从同一库中的其他函数进行的调用则不需要这些参数验证。这些扩展确实提供了一些优势。而关心兼容性的程序员可以简单地遵守更严格的C ++规则。

请注意,这是没有合适的实现来提供的标准头的标准库函数内置定义,因为这会打破一些旧的代码,包括它们的头后redeclares标准库函数。该inline关键字仅旨在为用户提供建议功能内联的可移植方式。由于标准标头不必是可移植的,因此实现具有以下其他选择:

#define abs(x) __builtin_abs(x)

或其他用于内联标准库函数的非便携式机制。


谢谢,这非常详细-几乎和标准本身一样可读。:-)我知道我一定很想念东西。您能提供参考吗?
Sven Marnach 2011年

我只是想过可以使用Google查找链接:open-std.org/jtc1/sc22/wg14/www/C99RationaleV5.10.pdf
Sven Marnach 2011年

@Sven:我从您的搜索中借用了URL,并将其放入答案中。我使用的文档是我先前保存的原理(在2005年)的副本,但也是V5.10。
乔纳森·勒夫勒

4
再次感谢。我从答案中获得的最有价值的见解是,有一个文档包含了C99标准的基本原理(可能还有其他标准的文档)。Nemo的答案给了我一条鱼,而那条鱼却教我钓鱼。
Sven Marnach 2011年

0

>我收到一个链接器错误(对的未定义引用f

在这里工作:Linux x86-64,GCC 4.1.2。可能是编译器中的错误;我在标准的引用段落中没有看到禁止给定程序的任何内容。注意使用if而不是iff

内联定义提供了外部定义替代方法,翻译器使用该外部定义在同一翻译单元中实现对该函数的任何调用。

因此,如果您知道函数的行为f并希望在一个紧密的循环中调用它,则可以将其定义复制粘贴到模块中以防止函数调用。,您可以提供一个对于当前模块而言等效的定义(但跳过输入验证或您可以想象的任何优化)。但是,编译器编写器可以选择优化程序大小。


2
该标准指出,编译器可能始终假设有一个具有相同名称的外部函数,并调用它而不是内联版本,因此我认为这不是编译器错误。我尝试使用gcc 4.3、4.4和4.5,都给出了链接器错误。
Sven Marnach 2011年
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.