C语言中的`inline`关键字有什么用途?

86
我在stackoverflow上看到了几个关于C语言中"inline"的问题,但仍不太清楚。下面是需要翻译的内容:
1. static inline void f(void) {}static void f(void) {}没有实际区别。
2. 在C语言中,inline void f(void) {}无法像C++那样工作。它在C语言中是如何工作的?
3. extern inline void f(void);实际上做了什么?
我从未真正在我的C程序中使用过inline关键字,当我看到其他人的代码中有这个关键字时,它几乎总是static inline,对此我看不出与只用static有什么区别。

1
两个评论都是误导性的。当然,在C语言中inline是有用的,而链接指向的是一个C++问题,这只会增加混淆。 - Antti Haapala -- Слава Україні
https://en.cppreference.com/w/c/language/inline - qwr
6个回答

70

一个 C 程序可以通过两个目标进行优化:减少代码大小或获得最佳性能(时间)。 通常情况下,这是线上的两个相反极端。为了减少代码大小,程序会以一种牺牲性能的方式进行优化。而且,将编译器设置为以执行时间为优化目标时,生成的二进制文件通常具有较大的代码大小。 由于可能与需求相关,通常需要在满足需求之间进行权衡。

内联函数通常被视为实现更高性能(即减少执行时间)的方法。

内联函数:

gcc.gnu.org 上说:

通过声明函数为内联函数,您可以指示 GCC 更快地调用该函数。GCC 可以通过将该函数的代码集成到其调用者的代码中来实现这一点。这样做可以通过消除函数调用开销来加快执行速度;此外,如果任何实际参数值是常数,它们的已知值可能允许在编译时进行简化,从而无需包含内联函数的全部代码。对代码大小的影响不太可预测;根据具体情况,目标代码的大小可能会增大或缩小。

所以,它告诉编译器将函数构建到使用它的代码中,以提高执行时间。
如果你声明小函数,比如设置/清除标志或一些位切换操作,这些操作会被重复执行,使用"inline"关键字可以在时间上带来很大的性能差异,但代价是代码大小增加。

非静态内联和静态内联

再次参考gcc.gnu.org

当一个内联函数不是静态的时候,编译器必须假设可能会有其他源文件中的调用;由于全局符号在任何程序中只能定义一次,该函数不能在其他源文件中定义,因此其中的调用不能被集成。因此,非静态内联函数总是按照通常的方式单独编译。


extern inline?

再次,gcc.gnu.org,说的很清楚:

如果在函数定义中同时指定了inline和extern关键字,那么该定义仅用于内联。无论如何,该函数都不会被单独编译,即使你明确引用了它的地址。这样的地址成为外部引用,就像你只声明了函数而没有定义它一样。

内联和extern的组合几乎具有宏的效果。使用方法是将函数定义放在一个带有这些关键字的头文件中,并在库文件中放置另一个不带inline和extern的定义副本。头文件中的定义导致大多数对该函数的调用被内联。如果还有任何对该函数的使用,它们将引用库中的唯一副本。


总结一下:
1. 对于inline void f(void){}inline的定义只在当前的翻译单元中有效。
2. 对于static inline void f(void) {},由于存储类是static,标识符具有内部链接,inline的定义在其他翻译单元中是不可见的。
3. 对于extern inline void f(void);,由于存储类是extern,标识符具有外部链接,inline的定义也提供了外部定义。

关于extern inline的最后一条引用只适用于C90。这也在GCC文档中提到了。请参考此编译器探索链接进行比较:https://godbolt.org/z/7cdnMfnW5。对于C90,不会生成汇编代码来自`extern inline函数,对于其他C标准,会生成具有外部链接的代码,请参见汇编指令.globl extern_inline_fn`。 - undefined

41
注意:在本答案中涉及到的 .c 文件和 .h 文件,我假设您已经正确编写了代码,即 .c 文件仅包括 .h 文件。区别是 .h 文件可以被多个翻译单元包含。

static inline void f(void) {}static void f(void) {} 没有实际区别。

在 ISO C 中,这是正确的。它们的行为相同(当然,假设您没有在同一翻译单元中以不同方式重新声明它们!),唯一的实际影响可能是导致编译器进行不同的优化。

C 中的 inline void f(void) {} 不像 C++ 那样工作。在 C 中如何工作? extern inline void f(void); 实际上是做什么的?

这在这个回答这个线程中有解释。

在 ISO C 和 C++ 中,您可以自由地在头文件中使用 inline void f(void) {},但原因不同!

在 ISO C 中,它根本不提供外部定义。在 ISO C++ 中,它提供了一个外部定义;但是,C++ 有一个额外的规则(C 没有),即如果有多个 inline 函数的外部定义,则编译器会解决这个问题并选择其中一个。

在ISO C的.c文件中,extern inline void f(void);的使用意味着需要在头文件中配对使用inline void f(void) {}。它会导致函数的外部定义在该翻译单元中被发出。如果不这样做,那么就没有外部定义,因此可能会出现链接错误(不确定是否任何特定调用f都链接到外部定义)。换句话说,在ISO C中,您可以手动选择外部定义的位置;或者通过在所有地方使用static inline来完全禁止外部定义;但在ISO C++中,编译器会选择是否以及外部定义将放置在哪里。
在GNU C中,情况有所不同(下文详细说明)。进一步复杂化问题的是,GNU C++允许您在C ++代码中编写static inlineextern inline...我不想猜测那会做什么。
许多程序员不知道自己在做什么,只是把一些看起来可以工作的东西组合在一起。另一个因素是你正在查看的代码可能是为GNU C编写的,而不是ISO C。
GNU C中,简单的inline与ISO C的行为不同。它实际上会发出一个外部可见的定义,因此在两个翻译单元中包含具有简单inline函数的.h文件将导致未定义的行为。因此,如果程序员想要在GNU C中提供inline优化提示,则需要使用static inline。由于static inline适用于ISO C和GNU C,自然而然的人们最终选择了它,并且看到它似乎可以工作而没有出现错误。

我认为静态内容和使用该标签没有区别。

这个标签只是为了提供速度优化提示给编译器,但在现代编译器中已经显得多余了。


在一个 .c 文件中的 "extern inline void f(void);" 应该和头文件中使用的 "static inline void f(void) {}" 相对应。你不应该移除 static 吗? - user3810155
2
我刚刚阅读了最近的C标准草案,其中指出:“如果翻译单元中一个函数的所有文件作用域声明都包括内联函数说明符而没有extern,则该翻译单元中的定义是内联定义。内联定义不提供函数的外部定义,并且不会禁止在另一个翻译单元中进行外部定义。”这是否意味着,如果我符合内联定义的要求,那么extern声明就是不必要的? - user3810155
1
@xiver77,你关于去掉static的说法是正确的;我刚刚重新阅读了标准,并意识到其中一个规范答案曾经误读过它。 "不确定调用函数是否使用内联定义或外部定义。" 仅适用于具有外部链接的内联函数的调用,因此对static inline的调用不能调用外部定义版本。(实际上,这似乎为编译器提供了整个程序优化的障碍) - M.M
我不确定你在最新问题中的意思。在一个TU中,如果没有extern void f(void);,那么inline void f(void) {}意味着它既是内联定义,又是具有外部链接的函数,因此对函数的调用可能会链接到外部定义,而不是本TU中的内联定义。 - M.M
我现在明白了。所以如果没有extern声明,如果编译器使用内联定义,我可能不会得到链接器错误,但是如果编译器选择外部定义,则会出现链接器错误。我刚刚用gcc进行了测试,有和没有优化。没有优化时,由于没有外部定义,我会得到一个链接器错误。 - user3810155
@xiver77 是的,那听起来是正确的,就像在这个问题的例子中一样。 - M.M

8

从C11规范的6.7.4函数说明符部分

使用内联函数说明符声明的函数是内联函数。将函数设置为内联函数意味着调用该函数尽可能地快。138)这样的建议对效果的影响程度是实现定义的。139)

138)例如,通过使用替代通常的函数调用机制(如内联替换)等方法。内联替换不是文本替换,也不会创建新函数。因此,例如,在函数体内使用的宏扩展使用函数体出现时的定义,而不是调用函数的位置;标识符也引用了函数体出现处的作用域中的声明。同样,该函数只有一个地址,无论外部定义之外发生多少次内联定义。

139)例如,实现可能从不执行内联替换,或者仅对内联声明的范围内的调用执行内联替换。

它建议编译器该函数被广泛使用,并要求在调用该函数时优先考虑速度。但是随着现代智能编译器的出现,这可能更或多或少地无关紧要,因为编译器可以决定是否应该将函数内联,而且可能会忽略用户的内联请求,因为现代编译器可以非常有效地决定如何调用函数。

static inline void f(void) {}static void f(void) {}没有实际区别。

所以,对于大多数现代编译器来说,它们之间没有任何区别。对于任何编译器来说,都没有实际/可观察的输出差异。

C中的inline void f(void) {}与C++的方式不同。在C中它是如何工作的?

在C++中,任何地方使用内联函数必须在所有地方使用内联函数,并且链接器不会报告多个定义错误(定义必须相同)。

extern inline void f(void);实际上是做什么的?

这将为f提供外部链接。因为f可能存在于其他编译单元中,编译器可以选择不同的调用机制来加速调用,或者完全忽略inline


2
一个函数,其中所有的声明(包括定义)都使用inline关键字而不是extern。
在同一个翻译单元中必须有一个定义。标准将此称为内联定义。
不会发出独立的目标代码,因此此定义无法从另一个翻译单元调用。
在这个例子中,所有的声明和定义都使用了inline而不是extern关键字:
// a declaration mentioning inline     
inline int max(int a, int b);

// a definition mentioning inline  
inline int max(int a, int b) {  
  return a > b ? a : b;  
}

这里有一个参考链接,可以更清晰地了解C语言中的内联函数并了解内联和外部使用方法。


2

如果你理解它们的来龙去脉,那你就会知道它们为什么在那里。

"inline"和"const"都是C++的创新,后来逐渐被改进到了C语言中。这些创新以及后来的模板和lambda等创新的一个隐含设计目标是削减预处理器的最常见用例(特别是“#define”),从而尽量减少对预处理器阶段的使用和需求。

语言中预处理器阶段的出现严重限制了提供语言分析和翻译的透明度的能力。这使得原本应该是易于翻译的shell脚本转变成了更复杂的程序,例如“f2c”(Fortran转换为C)和最初的C++编译器“cfront”(C++转换为C); 在较小程度上,还有“indent”实用程序。如果你曾经不得不处理像这样的转换器输出(我们也曾经),或者实际上制作自己的转换器,那么你就会知道这是多么重要的问题。

顺便提一下,“indent”实用程序在整个问题上都会遇到困难,只是妥协地将宏调用视为普通变量或函数调用,并跳过“#include”的部分。这个问题也会出现在其他可能想要进行源代码转换/翻译的工具中,例如自动化重构、重新编码和重构工具;也就是说,这些工具更加智能地自动化了程序员所做的事情。
因此,理想情况是将对预处理器阶段的依赖降至最低限度。这是一个好的目标,独立于过去如何遇到这个问题。
随着越来越多的用例被知晓并甚至在使用中标准化,它们被正式封装为语言创新。
“#define”的一个常见用例是创建显式常量。在很大程度上,现在可以通过“const”关键字和(在C++中)“constexpr”来处理这个问题。
“#define”的另一个常见用例是使用宏创建函数。其中许多内容现在已经被“inline”函数封装,这就是它的作用所在。在C++中,“lambda”结构将其推进了一步。

从其首次外部发布 - 1985年2月的E版开始,C++中就同时存在"const"和"inline"关键字(我们是将其转录并修复的人。在2016年之前,它只以几百页的糟糕剪切打印品的形式存在。)

后来又添加了其他创新,比如cfront 3.0版本中的"template"(在ANSI X3J16会议上被接受于1990年),以及更近期的lambda构造和"constexpr"。


-2

正如“内联”一词所说,“内”和“联”,将此关键字添加到函数中会影响程序在运行时的表现。当程序被编译时,函数内部编写的代码会被粘贴在函数调用下面,由于函数调用比内联代码更昂贵,因此这优化了代码。

因此,static inline void f(void) {}static void f(void) {},在这里内联关键字确实会在运行时产生差异。但是当函数有太多行代码时,它不会影响运行时。

如果在函数之前添加static,则该函数的生命周期为整个程序的生命周期。并且该函数的使用仅限于该文件。

要了解extern的相关信息,请参考- C函数上extern关键字的影响


1
inline 关键字首次添加到编译器中(早在第一个 C 标准之前),这就是它的意图。但是自那时以来,它的含义和用法已经发生了很大的变化。 - M.M

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