我有两个问题:
1)为什么在C ++中允许指向内联函数的指针? 我已经读过内联函数的代码只是被复制到函数调用语句中,内联函数中没有编译时内存分配。那么,为什么内联函数没有固定的内存地址,为什么指针可以存在于内联函数中?
2)考虑以下代码:
inline void func()
{
int n=0;
cout<<(&n);
}
每次调用n
时,是否不会打印func()
地址的不同值?
[因为我认为每次复制内联函数代码时,必须重新分配局部变量(而在正常函数的情况下,重新初始化会发生)]
我是初学者,为了加强我的概念,我问了这个问题。如果我在任何地方都错了,请纠正我。
答案 0 :(得分:56)
1)为什么在c ++中允许指向内联函数的指针?
因为内联函数与其他函数一样,并且指向它们是函数可以执行的操作之一。内联函数在这方面并不特别。
我已经读过内联函数的代码只是被复制到函数调用语句中,内联函数中没有编译时内存分配。
你(也许是你读过的材料)混合了两个相关的,同样命名的概念。
内联函数在使用它的所有翻译单元中定义,而非内联函数仅在一个定义规则的要求下在一个翻译单元中定义。这就是函数的内联声明意味着什么;它放宽了一个定义规则,但也提供了在使用它的所有翻译单元中定义的额外要求(如果没有放松,则这是不可能的。)
内联扩展(或内联)是一种优化,通过将被调用函数复制到调用者的框架中来避免函数调用。无论函数是否已内联声明,函数调用都可以内联扩展。并且内联声明的函数不一定是内联扩展的。
但是,在未定义函数的转换单元中,函数不能内联展开(除非链接时优化执行扩展)。因此,在内联声明允许的所有TU中定义的要求也允许通过允许在调用它的所有TU中定义函数来实现函数的内联扩展。但优化并不能保证。
2)每次调用func()时,是否不打印不同的n地址值?
内联扩展会导致局部变量位于调用者的框架中,是的。但是,如果呼叫来自不同的帧,它们的位置将不同,无论扩展如何。
通常会生成任何已内联展开的函数的常规非扩展版本。如果采用函数的地址,则将指向非扩展函数。如果编译器可以证明对函数的所有调用都是内联的,则编译器可能会选择不提供非扩展版本。这要求函数具有内部链接,并且获取函数的地址通常会使这种证明变得非常困难或不可能。
答案 1 :(得分:26)
inline
keyword最初是编译器的一个提示,程序员认为这个函数是内联的候选者 - 编译器不需要遵守这个。
在现代用法中,它与内联几乎没有任何关系 - 现代编译器可以自由内联(或不)函数&#34;在你后面&#34;,这些构成了优化技术的一部分。
代码转换(包括内联)是在C ++中的"as-if" rule 下完成的,这基本上意味着编译器可以根据需要转换代码,只要执行是& #34;作为-如果&#34;原始代码按写入方式执行。这条规则推动了C ++的优化。
也就是说,一旦获得函数的地址,就需要存在(即要求地址有效)。这可能意味着它不再内联,但仍然可以(优化器将应用适当的分析)。
那么为什么指针内联函数没有固定的内存地址,内联函数是否存在指针?
不,这只是一个提示,主要与联系有关,而不是实际的内联。这可以说是主要的当前用法,它定义了头文件中的函数。
每次调用
n
时,是否不会打印func()
的不同地址值?
可能,n
是一个局部变量,基于函数执行时的堆栈位置。也就是说,函数inline
,它与链接有关,链接器会将这些函数合并到翻译单元上。
...如果示例更改为
static int n
,则每次调用函数都必须打印一个常量值(当然在单个程序运行中)...无论是否为代码是否内联。
这又是连接要求对局部变量n
的影响。
答案 2 :(得分:14)
你读旧材料。现在使用inline
的主要原因是允许头文件中的函数体。使用带有函数的inline
关键字向链接器发出信号,表示可以组合跨翻译单元的所有函数实例;在多个单元中包含的标头中具有非内联函数会导致由于单一定义规则违规而导致的未定义行为。
C ++ 17还添加了inline variables,它具有与标头中定义的变量相同的属性,并且所有定义都由链接器组合而不是导致ODR违规。
你正在谈论的东西&#34;代码被复制到调用函数&#34;被称为内联,与inline
关键字无关。编译器将根据优化设置决定是否对非内联函数以及内联函数执行此操作。
答案 3 :(得分:5)
内联函数并不总是内联。它只是表明程序员希望内联这个函数。 The compiler is allowed to inline any function, regarless of whether inline keyword was used or not.
如果使用函数的地址,该函数很可能不会在最终的可执行文件中内联,至少在GCC中是这样的:
当函数既是内联函数又是静态函数时,如果对函数的所有调用都集成到调用者中,并且从不使用函数的地址,则永远不会引用函数自己的汇编程序代码。
答案 4 :(得分:3)
除了已经说过的一点,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 -S
为foo
和succ
函数生成代码
_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
的函数指针 - 编译器已经发现该指针始终引用相同的函数,因此能够消除整个没有改变行为的事情。当你经常通过函数指针调用小函数时,这可以大大提高性能。这是功能语言中相当普遍的技术;像O'Caml和Haskell这样的语言的编译器充分利用了这种优化。
† 免责声明:我的装配技巧几乎不存在。我可能会在这里谈论垃圾。