inline关键字仅仅是对编译器的建议,编译器有权力决定一个函数是否在调用处嵌入。因为内联函数要在调用处展开,编译器必须能在每一个调用处能看到该函数的定义,因此最好将函数实现放在头文件中(而且实现在类定义中的成员函数即便不加inline关键字也会自动成为内联函数)。在实现文件中该函数之前要加上inline关键字的方式是有问题的:如果调用的obj文件在函数定义之前生成,那么该处就无法嵌入内联函数了。如果普通函数需要成为内联函数,在定义时加上inline关键字。
- 包含了递归、循环等结构的函数一般不会被内联。
- 虚拟函数一般不会内联,但是如果编译器能在编译时确定具体的调用函数,那么仍然会就地展开该函数。
- 如果通过函数指针调用内联函数,那么该函数将不会内联而是通过call进行调用。
- 构造和析构函数一般会生成大量代码,因此一般也不适合内联。
- 如果内联函数调用了其他函数也不会被内联。
如果想要阻止某函数被内联,可以在函数体前加上 __attribute__((noinline)) 。
有时函数是否原地展开与编译时指定的优先级有关,下面就是一个例子:
inline int foo(int x) {
return x+42;
}
int main(int argc, char* argv[])
{
printf("%d\n", foo(42));
return 0;
}
使用如下命令行得到汇编代码,g++ -S -Wall -DDEBUG -D_GNU_SOURCE -std=c++11 -I/usr/include test.cpp:
.section .text._Z3fooi,"axG",@progbits,_Z3fooi,comdat
.weak _Z3fooi
.type _Z3fooi, @function
_Z3fooi:
//rbp寄存器的值入栈,保存main函数的栈基地址
pushq %rbp
//将rsp寄存器的值写入到rbp寄存器,将main函数的栈顶指针赋予foo函数的栈底指针
movq %rsp, %rbp
//此时edi寄存器中的值是42,放入foo栈底向上4byte处
movl %edi, -4(%rbp)
//将42放入eax寄存器
movl -4(%rbp), %eax
//立即数21与eax寄存器中的值(42)相加
addl $21, %eax
//恢复main函数的栈底
popq %rbp
//返回
ret
.size _Z3fooi, .-_Z3fooi
.section .rodata
.LC0:
.string "%d\n"
.text
.globl main
.type main, @function
main:
//rbp寄存器的值入栈,保存_start函数的栈基地址
pushq %rbp
//将rsp寄存器的值写入到rbp寄存器,将_start函数的栈顶指针赋予main函数的栈底指针
movq %rsp, %rbp
//rsp寄存器的值减去立即数16,即main函数的栈大小为16
subq $16, %rsp
//将edi寄存器中的值放入rbp寄存器中的值减4的位置(栈底向上4byte)
movl %edi, -4(%rbp)
//将rsi寄存器中的值放入rbp寄存器中的值减16的位置(栈底向上16byte)
movq %rsi, -16(%rbp)
//立即数42放入edi寄存器
movl $42, %edi
//调用函数foo
call _Z3fooi
//eax寄存器的值(63)放入esi寄存器
movl %eax, %esi
//LC0段地址放入edi寄存器
movl $.LC0, %edi
movl $0, %eax
call printf
movl $0, %eax
leave
ret
.size main, .-main
使用如下命令行生成汇编代码:g++ -S -O -Wall -DDEBUG -D_GNU_SOURCE -std=c++11 -I/usr/include test.cpp(增加了一个优化选项)
.section .rodata.str1.1,"aMS",@progbits,1
.LC0:
.string "%d\n"
.globl main
.type main, @function
main:
subq $8, %rsp
movl $63, %esi
movl $.LC0, %edi
call printf
movl $0, %eax
addq $8, %rsp
ret
.size main, .-main
可见foo函数并未被调用,而是直接将立即数63放入esi寄存器作为printf函数的参数。