为什么允许指向内联函数的指针
Why are pointers to inline functions allowed?
我有两个问题:
1) 为什么C++中允许指向内联函数的指针?我读到过内联函数的代码只是被复制到函数调用语句中,并且内联函数中没有编译时内存分配。既然内联函数没有固定的内存地址,为什么可以存在指向内联函数的指针呢?
2) 考虑以下代码:
inline void func()
{
int n=0;
cout<<(&n);
}
不应该在每次调用func()
时打印n
地址的不同值吗?[因为我认为每次复制内联函数代码时,都必须重新分配局部变量(而在正常函数的情况下,则会进行重新初始化)]
我是一个初学者,我问这个问题是为了强化我的概念。如果我哪里错了,请纠正我。
1)为什么c++中允许指向内联函数的指针?
因为内联函数和其他函数一样,指向它们是使用函数可以做的事情之一。内联函数在这方面并不特别。
我读过内联函数的代码只是被复制到函数调用语句中,并且内联函数中没有编译时内存分配。
您(也许还有您阅读过的材料)混合了两个相关且名称相似的概念。
内联函数在所有使用它的翻译单元中定义,而非内联函数仅在一个翻译单元中根据一个定义规则的要求定义。这就是函数的内联声明的含义;它放宽了一个定义规则,但也提出了在所有使用它的翻译单元中定义的额外要求(如果odr没有放宽,这是不可能的)。
内联扩展(或内联)是一种优化,通过将调用的函数复制到调用方的框架中来避免函数调用。函数调用可以内联扩展,无论函数是否已内联声明。并且已经声明为内联的函数不一定是内联扩展的。
但是,函数不能在未定义的翻译单元中内联扩展(除非链接时间优化执行扩展)。因此,在内联声明允许的所有TU中定义的要求,也使得函数的内联扩展成为可能,因为允许在调用函数的所有TUs中定义函数。但优化并不能得到保证。
2)是否应该在每次调用func()时都打印n地址的不同值?
内联扩展确实会导致局部变量位于调用程序的框架中,是的。但是,如果呼叫来自不同的帧,则无论扩展如何,它们的位置都会有所不同。
通常,任何已内联扩展的函数都会生成一个常规的非扩展版本。如果取函数的地址,它将指向未展开的函数。如果编译器能够证明对函数的所有调用都是内联的,那么编译器可能会选择根本不提供非扩展版本。这要求函数具有内部链接,而获取函数的地址通常会使这种证明变得非常困难或不可能。
inline
关键字最初是向编译器提示,程序员认为此函数是内联的候选函数-编译器不需要遵守这一点。
在现代用法中,它与内联几乎没有关系——现代编译器"在背后"自由内联(或不内联)函数,这些都是优化技术的一部分。
代码转换(包括内联)是在C++中的"好像"规则下完成的,这基本上意味着编译器可以随心所欲地转换代码,只要执行"好像"原始代码是按照编写的方式执行的。该规则促进了C++中的优化。
也就是说,一旦一个地址取自一个函数,它就必须存在(即该地址必须有效)。这可能意味着它不再内联,但仍然可以(优化器将应用适当的分析)。
既然内联函数没有固定的内存地址,为什么可以存在指向内联函数的指针?
不,它只是一个提示,在很大程度上与链接有关,而不是实际的内联。这为定义头文件中的函数提供了燃料,这可以说是当前的主要用途。
是否应该在每次调用
func()
时打印不同的n
地址值?
n
可能是一个局部变量,基于函数执行时的堆栈位置。也就是说,函数inline
,它涉及链接,链接器将通过翻译单元合并函数。
如评论中所述;
。。。如果将示例更改为
static int n
,则对该函数的每次调用都必须打印一个常数值(当然是在单个程序运行中)。。。无论代码是否内联,这都是正确的。
这也是链接需求对局部变量n
的影响。
您阅读了旧材料。现在使用inline
的主要原因是允许在头文件中使用函数体。inline
关键字与函数的使用向链接器发出信号,表明跨翻译单元的函数的所有实例都可以被组合;在多个单元中包含的标头中具有非内联函数会由于违反"一个定义规则"而导致未定义的行为。
C++17还添加了内联变量,这些变量具有与可以在头中定义变量相同的属性,并且所有定义都由链接器组合,而不是导致ODR冲突。
您所说的"代码被复制到调用函数"称为内联,与inline
关键字无关。编译器将根据优化设置来决定是否对非内联函数和内联函数执行此操作。
内联函数并不总是内联的。它只是表示程序员希望这个函数内联。编译器可以内联任何函数,无论是否使用了内联关键字。
如果使用了函数的地址,那么该函数很可能没有内联在最终可执行文件中,至少在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 -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等语言的编译器充分利用了这种优化。
†免责声明:我的组装技能几乎不存在。我可能在这里胡说八道
- QMetaObject invokeMethod的基于函数指针的语法
- C++-试图将函数指针推回到另一个CPP文件中的矢量时出错
- c++r值引用应用于函数指针
- 模板函数指针和lambda
- 是否可以将llvm::FunctionType转换为C/C++原始函数指针
- 带有类的函数指针
- () 函子后面的括号,而不是函数指针?
- 全局作用域中函数指针的赋值
- 使用"Task"函数指针队列定义作业管理器
- 将成员函数指针作为参数传递给模板方法
- 如何创建对象函数指针C++映射?
- 匹配函数指针作为模板参数?
- 通过函数指针定义类范围之外的方法
- 存储在类中的函数指针
- C++从函数指针数组调用函数
- 将返回值存储在函数指针数组的指针中是如何工作的?
- 整数键映射到头文件中的成员函数指针
- 从类成员函数到类 C 函数指针的转换
- 如何将内联匿名函数分配给C++函数指针
- 将字符缓冲区强制转换为函数指针