为什么指向内联函数的指针是允许的?

我有两个问题:

1)为什么在C ++中允许内联函数的指针? 我已经读过,内联函数的代码只是被复制到函数调用语句中,内联函数中没有编译时内存分配。 那么为什么一个指针存在一个内联函数,因为内联函数没有固定的内存地址?

2)考虑下面的代码:

inline void func() { int n=0; cout<<(&n); } 

每次func()时,是否不打印n的地址的不同值? [因为我认为每次内联函数代码被复制,必须重新分配局部variables(而在正常function的情况下,重新初始化发生)]

我是初学者,为了加强我的观念,我问了这个问题。 如果我在任何地方都错了,请纠正我。

1)为什么在c ++中允许指向内联函数的指针?

因为内联函数就像任何其他函数一样,并且指向它们是可以用函数做的事情之一。 内联函数在这方面并不特别。

我已经读过内联函数的代码只是复制到函数调用语句,并没有内联函数的编译时内存分配。

你(也许是你读过的材料)混合了两个相关并且相似命名的概念。

内联函数在使用它的所有翻译单元中定义,而非内联函数仅在一个翻译单元中按照一个定义规则的要求定义。 这就是函数的内联声明的含义。 它放宽了一个定义规则,但也给所有使用它的翻译单元定义了额外的要求(如果不放松,这是不可能的)。

内联扩展(或内联)是一种优化,通过将被调用的函数复制到调用者的框架中来避免函数调用。 函数调用可以在线扩展,不pipe函数是否已经声明为内联。 而且一个内联的函数不一定是内联的。

但是,非内联函数不能在没有定义的翻译单元中内联扩展(除非链接时间优化执行扩展)。 因此,在内联声明允许的所有TU中定义的要求也使得跨翻译单元的函数的内联扩展成为可能。 但优化不能保证。

2)每次调用func()时,是否不打印n的地址的不同值?

内联扩展确实会导致本地variables位于调用者的框架中,是的。 但是,如果呼叫来自不同的框架,它们的位置将不同,无论扩展如何。

通常有一个常规的非扩展版本是由内嵌扩展的任何函数生成的。 如果一个函数的地址被采用,它将指向那个非扩展的函数。 如果编译器可以certificate所有对函数的调用都是内联的,那么编译器可能会根本不提供非扩展版本。 这就要求函数具有内部的联系,并且考虑到函数的地址通常使得这样的certificate非常困难或者不可能。

inline关键字最初是向编译器提示的,程序员认为这个函数是内联的候选者 – 编译器不需要这样做。

在现代应用中,与内联无关 – 现代编译器可以自由内联(或不包含)“背后”function,这些构成了优化技术的一部分。

代码转换(包括内联)是在C ++中的“as-if”规则下完成的 ,这基本上意味着编译器可以根据需要转换代码,只要执行原始代码的“as-if”如书面。 这个规则加速了C ++的优化。

也就是说,一旦一个函数被占用,就需要存在(即地址必须是有效的)。 这可能意味着它不再被内联,但仍然可以(优化器将应用适当的分析)。

那么为什么一个指针存在一个内联函数,因为没有内联函数的固定内存地址?

不,这只是一个暗示,很大程度上与联系而不是实际内联有关。 这促进了可以说是当前主要用途的东西,即在头文件中定义函数。

每次func()时,是否不打印n的地址的不同值?

它可能, n是一个局部variables,基于函数执行时的堆栈位置。 也就是说,函数inline ,它涉及到链接,链接器会将这些函数合并到翻译单元上。


正如评论中指出的那样;

…如果将示例更改为static int n ,则对函数的每次调用都必须打印一个常量值(当然,在单个程序运行中)…无论代码是否内联,情况都是如此。

这又是联系要求对局部variablesn

你读旧的材料。 现在使用inline主要原因是允许头文件中的函数体。 使用带有函数的inline关键字可以使链接器知道跨翻译单元的所有函数实例可以被组合; 在由多个单元包含的标题中具有非内联函数会导致由于一个定义规则违例而导致未定义的行为。

C ++ 17还添加了内联variables ,这些variables具有相同的属性,可以在头中定义该variables,并且所有定义都由链接器组合,而不会导致ODR违例。

你正在谈论的“代码复制到调用函数”的内容被称为内联 ,独立于inline关键字。 编译器将根据优化设置决定是否执行此操作,以实现非内联函数以及内联函数。

内联函数并不总是内联的。 它只是表示程序员想要这个函数被内联。 允许编译器内联任何函数,无论内联关键字是否被使用。

如果使用函数的地址,那么函数最有可能不会在最终的可执行文件中被内联,至less在GCC中:

当一个函数是内联和静态的时候,如果对函数的所有调用都被集成到调用者中,并且函数的地址从不被使用,那么函数自己的汇编代码就不会被引用。

GCC文档

除了已经提到的inline函数不需要实际内联(并且许多没有inline联的函数被现代编译器内联)之外, 通过函数指针内联一个调用也是完全可以想象的。 例:

 #include <iostream> int foo(int (*fun)(int), int x) { return fun(x); } int succ(int n) { return n+1; } int main() { int c=0; for (int i=0; i<10000; ++i) { c += foo(succ, i); } std::cout << c << std::endl; } 

在这里, foo(succ, i)可以作为一个整体被内联到i+1 。 事实上,这似乎发生g++ -O3 -Sfoosucc函数生成代码

 _Z3fooPFiiEi: .LFB998: .cfi_startproc movq %rdi, %rax movl %esi, %edi jmp *%rax .cfi_endproc .LFE998: .size _Z3fooPFiiEi, .-_Z3fooPFiiEi .p2align 4,,15 .globl _Z4succi .type _Z4succi, @function _Z4succi: .LFB999: .cfi_startproc leal 1(%rdi), %eax ret .cfi_endproc 

但是,它产生的代码, 从来没有提到任何一个main ,而是只包括一个新的专业_GLOBAL__sub_I__Z3fooPFiiEi

 .LFE999: .size _Z4succi, .-_Z4succi .section .text.startup,"ax",@progbits .p2align 4,,15 .globl main .type main, @function main: .LFB1000: .cfi_startproc movdqa .LC1(%rip), %xmm4 xorl %eax, %eax pxor %xmm1, %xmm1 movdqa .LC0(%rip), %xmm0 movdqa .LC2(%rip), %xmm3 jmp .L5 .p2align 4,,10 .p2align 3 .L8: movdqa %xmm2, %xmm0 .L5: movdqa %xmm0, %xmm2 addl $1, %eax paddd %xmm3, %xmm0 cmpl $2500, %eax paddd %xmm0, %xmm1 paddd %xmm4, %xmm2 jne .L8 movdqa %xmm1, %xmm5 subq $24, %rsp .cfi_def_cfa_offset 32 movl $_ZSt4cout, %edi psrldq $8, %xmm5 paddd %xmm5, %xmm1 movdqa %xmm1, %xmm6 psrldq $4, %xmm6 paddd %xmm6, %xmm1 movdqa %xmm1, %xmm7 movd %xmm7, 12(%rsp) movl 12(%rsp), %esi call _ZNSolsEi movq %rax, %rdi call _ZSt4endlIcSt11char_traitsIcEERSt13basic_ostreamIT_T0_ES6_ xorl %eax, %eax addq $24, %rsp .cfi_def_cfa_offset 8 ret .cfi_endproc .LFE1000: .size main, .-main .p2align 4,,15 .type _GLOBAL__sub_I__Z3fooPFiiEi, @function _GLOBAL__sub_I__Z3fooPFiiEi: .LFB1007: .cfi_startproc subq $8, %rsp .cfi_def_cfa_offset 16 movl $_ZStL8__ioinit, %edi call _ZNSt8ios_base4InitC1Ev movl $__dso_handle, %edx movl $_ZStL8__ioinit, %esi movl $_ZNSt8ios_base4InitD1Ev, %edi addq $8, %rsp .cfi_def_cfa_offset 8 jmp __cxa_atexit .cfi_endproc .LFE1007: .size _GLOBAL__sub_I__Z3fooPFiiEi, .-_GLOBAL__sub_I__Z3fooPFiiEi .section .init_array,"aw" .align 8 .quad _GLOBAL__sub_I__Z3fooPFiiEi .local _ZStL8__ioinit .comm _ZStL8__ioinit,1,1 

所以在这种情况下,实际的程序甚至不包含指向succ的函数指针 – 编译器已经发现这个指针总是指向同一个函数,因此能够在不改变行为的情况下消除整个事物。 当您经常通过函数指针调用小函数时,这可以提高性能。 这在function语言中是相当普遍的技术。 O'Caml和Haskell等语言的编译器很好地利用了这种优化。


免责声明:我的assembly技能几乎不存在。 我可能在这里说垃圾。