内联函数及其实现方式

4
这应该对那些回答的人来说很容易。这个问题有一个逻辑上的答案,但我想问一下以确认。
我对程序流程的理解很简单。函数及其相关指令位于内存中的某个位置。这个内存位置是用于存储这些指令的唯一位置。当调用时,该函数第一个指令的位置被存储在程序流中。这个内存指针指示CPU在内存中查找所需函数的指令的位置。跳转到这个位置并执行指令后,恢复正常的程序流,并将CPU跳回到原始地址指令所在的位置,继续执行后续指令。
我理解内联函数是直接粘贴到它们被调用的位置中的。因此,当编写源文件并定义内联函数时,实际上存在多个内存位置,其中包含该函数的指令集(即它被调用的确切位置)。因此,可以说内联函数没有类似非内联函数的source内存位置吗?
此外,在编译过程中,编译器是否只是将内联函数粘贴到它被调用的位置,并使用传递给它的参数删除/替换函数定义的任意参数名称?
5个回答

4
据我了解,inline函数会直接粘贴到调用它们的位置中。但是,即使函数被标记为inline,编译器可能不会将其内联。如果一个翻译单元中有4个对inline函数的调用,编译器可能决定内联其中一个,其余的调用应该使用普通的函数调用。因此,在这种情况下,存在“源”位置和内联位置。在编译过程中,inline函数必须与普通函数调用具有相同的含义。

这个决定考虑了哪些因素?我在微控制器上实现了一个内联函数的基本实现,程序的字长取决于我是否实现了内联。 - sherrellbc
@sherrellbc,这基本上取决于编译器进行优化的方式。inline 的作用只是提供一个提示,表明调用应该尽可能快。 - ldav1s
类似于 register 修饰符似乎是这样的。谢谢。 - sherrellbc
从提示编译器的意义上讲,是的,它类似于“register”。支持内联的编译器通常具有“真正”或“始终”内联函数的(非便携式)选项或扩展。 - ldav1s
即使每个调用都被内联,如果其地址被取出,编译器仍然必须生成一个函数。而且,“inline”与“register”类似,编译器可以忽略它,但实际上它们非常不同,因为现代编译器完全忽略“register”(除了您不能获取其中一个的地址)。 - Jim Balter

3
几乎和你想的一样。 首先 - 这取决于语言/编译器。在C和GCC或MSVCC中,如果您告诉函数是“inline”函数,并不意味着它会被内联!这取决于编译器及其实现,您的inline关键字/pragma仅是一个提示。
此外,如果函数标记为“inline”,则会在原始调用的每个位置复制该函数,但是...如果您正在编译库(静态或动态),则不会删除它。但是,如果编译器决定将其内联,则确实将代码粘贴在一起并优化调用。
顺便说一下,在Haskell(GHC)中,“inline” pragma不仅是一个提示,而且您可以确定编译器将执行内联过程。个人认为,我希望有一个C ++编译器,它会真正地使我告诉他的每个函数内联 :)

在编译动态库的情况下,函数定义不会被删除,因为共享该库的其他程序也需要它。这似乎很明显,但我对 DLL 不是很了解,所以我只是想确认一下。 - sherrellbc
@sherrellbc:确切地说。当然,如果该函数可以在库外访问(而不是私有或受保护的)。 - Wojciech Danilo

2

我的理解与你的相同。内联编译会移除函数并将其替换为该函数内的命令。任何参数都会被替换为传递给它们的值。正如你所说,执行过程中没有跳转,也没有将函数添加到调用栈中。


2
编译器可以自由选择是否将声明为内联的函数进行内联。内联时,会有以下好处:
  • 执行速度快。(因为没有跳转指令,这将刷新CPU缓存)
  • 内存使用
    • 代码内存:如果您内联使用在许多地方使用的函数,则代码大小将增加。
    • 堆栈使用:如果您的内联函数使用更多变量,则将使用更多堆栈空间。

编辑:何时使用内联

  1. 使用内联函数代替#define。(宏展开是在预处理期间完成的,在此期间编译器根本不在图片中。但对于内联函数,编译器在图片中,可以进行错误检查...)
  2. 如果函数非常小,请使用内联函数。
  3. 如果该函数经常在许多地方使用,请使用内联函数。

不确定我是否同意第二个要点——因为所有复制的代码,指令集将会更大。因此,您需要(潜在地要多得多)更多的内存来保存指令集,而一个额外的堆栈帧所需的内存(尤其是可内联的函数)并不高。 - Scott Mermelstein
1
@ScottMermelstein:编辑了我的答案...感谢您的评论。 - user1814023
你的清单中的#1是指任何类似函数的宏吗?例如,如果您有一个返回两个数字中最小值的宏? - sherrellbc
@sherrellbc:是的...宏扩展是在预处理时完成的,编译器完全不在画面中。但对于内联函数,编译器在画面中,可以进行错误检查等操作... - user1814023

1

ISO/IEC 9899:TC2 的第112页告诉我们:

使用 inline 函数说明符声明的函数是内联函数... 将函数设为内联函数建议调用该函数时尽可能快。(118) 这种建议的有效程度是实现定义的。(119)

该部分的脚注告诉我们:

(118)通过使用替代通常函数调用机制的方法,例如“内联替换”,可以实现功能。 内联替换不是文本替换,也不会创建新函数。 因此,在函数体中使用的宏扩展使用函数体出现时的定义,而不是调用函数的位置; 标识符引用发生在函数体出现的作用域中的声明。 同样,函数具有单个地址,无论外部定义之外发生多少内联定义。
(119)例如,实现可能永远不执行内联替换,或者只对内联声明范围内的调用执行内联替换。
我在这里加粗了一些内容,以纠正或更正您对“inline”的理解。如果您希望强制或期望特定的“inline”行为,请参考您的编译器实现说明。关于(非)文本替换和单个地址的小细节很有趣。

这有点令人困惑,因为我对这个领域并不完全精通。文本替换是什么意思?从这段话中我了解到,在调用宏的函数内展开宏时,使用编译器在进入调用函数时可用的定义(即,如果在该函数内编写新的宏定义,则不会在同一范围内的宏调用中实现)。内联函数只有一个地址和多个定义的可能性是什么意思?我认为一个函数可能只有一个定义。 - sherrellbc
关于“文本替换”...与其声明一个函数为inline,我们可以将函数定义的主体直接复制并粘贴到调用它的代码中,将其参数重命名为已在作用域内的变量。这样做肯定会更快地“调用”,但会占用更多的代码空间。如果宏定义在编译器解析原始函数定义和解析新粘贴的内联代码之间发生了更改,则该方法也会受到宏扩展更改的影响。我所描述的就是文本替换。 - Darren Stone

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