step 0.1 先来1题
关于c++的inline关键字,以下说法正确的是(4.)
1. 使用inline关键字的函数会被编译器在调用处展开 <--------不一定都会,有些会被编译器拒绝。
2. 头文件中可以包含inline函数的声明 <--------可以?会有警告(所以我们姑且认为不可以)
3. 可以在同一个项目的不同源文件内定义函数名相同但实现不同的inline函数 <--------可以是可以,也编译通过,但会产生意想不到的结果,所以还是不推荐.
4. 定义在Class声明内的成员函数默认是inline函数 <--------ok
5. 优先使用Class声明内定义的inline函数 <----------|
6. 优先使用Class实现的内inline函数的实现 <----------|--这两个其实没有谁优先的,例如函数体太大有循环就不推荐使用inline
step 0.2 函数的调用过程
主要是通过这个来深♂入♂
理解为什么函数调用存在一定开销。
函数的调用过程主要有(模糊)
一个大坑 - movq为AT&T指令,而我们课本上学的是MASM指令,两者的方向是反的(我鈤了你大爷!)
AT&T的mov语法(movq为复制8个字节,双字) -movq src,dest
call指令 - 主要是2部分操作组成:pushl %eip & ljmp
ret指令 - 主要是 popl %eip.
-
以下两条指令用于切换堆栈上下文.
- enter指令 -
等价于
- enter指令 -
push ebp
ebp ← esp
- leave指令 -
等价于
esp ← ebp
pop ebp
(其中 esp - 栈顶指针;ebp - 栈基址指针)
- 下面详细阐述一下函数调用的具体过程.
示例代码 test_func_.c
int foo2(){ <---------为啥要两个函数,主要是因为假如下面的foo不调用其他函数的话,在foo的汇编代码里就不会有第30行了,即栈顶指针就不用了,想想也是合理,这样减少了一些代码量
int kkkkk[4];
return 0;
}
int foo(int k1, int k2, int k3){
int s1[5] = {1,2,3,4,5};
int s = 1;
foo2();
return k1+s;
}
int main(){
int s=5;
int s2=40;
foo(400, 500, 600);
return 0;
}
gcc -S test_func_.c -o test_func_.s
的汇编结果:(不要怕,只看最关键的地方)
1 .file "test_func_.c"
2 .text
3 .globl foo2
4 .type foo2, @function
5 foo2: <---------foo2加进来的原因是为了更好地展示调用函数的一般流程
6 .LFB0:
7 .cfi_startproc
8 pushq %rbp
9 .cfi_def_cfa_offset 16
10 .cfi_offset 6, -16
11 movq %rsp, %rbp
12 .cfi_def_cfa_register 6
13 movl $0, %eax
14 popq %rbp
15 .cfi_def_cfa 7, 8
16 ret
17 .cfi_endproc
18 .LFE0:
19 .size foo2, .-foo2
20 .globl foo
21 .type foo, @function
22 foo: <---------主要关心这个
23 .LFB1:
24 .cfi_startproc
25 pushq %rbp <------------旧基址进栈
26 .cfi_def_cfa_offset 16
27 .cfi_offset 6, -16
28 movq %rsp, %rbp <------------新基址为当前的栈顶
29 .cfi_def_cfa_register 6
30 subq $64, %rsp <-----------当本函数有调用其他函数的动作时,才会把栈顶向下移,亲测。例如把上面的foo2及相关调用删除,这句就没了。
31 movl %edi, -52(%rbp) <-----------一系列的操作都是用栈基址寄存器+偏移量来操作的
32 movl %esi, -56(%rbp)
33 movl %edx, -60(%rbp)
34 movl $1, -32(%rbp)
35 movl $2, -28(%rbp)
36 movl $3, -24(%rbp)
37 movl $4, -20(%rbp)
38 movl $5, -16(%rbp)
39 movl $1, -36(%rbp)
40 movl $0, %eax
41 call foo2
42 movl -36(%rbp), %eax
42 movl -36(%rbp), %eax
43 movl -52(%rbp), %edx
44 addl %edx, %eax
45 leave
46 .cfi_def_cfa 7, 8
47 ret
48 .cfi_endproc
49 .LFE1:
50 .size foo, .-foo
51 .globl main
52 .type main, @function
53 main:
54 .LFB2:
55 .cfi_startproc
56 pushq %rbp
57 .cfi_def_cfa_offset 16
58 .cfi_offset 6, -16
59 movq %rsp, %rbp
60 .cfi_def_cfa_register 6
61 subq $16, %rsp
62 movl $5, -8(%rbp)
63 movl $40, -4(%rbp)
64 movl $600, %edx
65 movl $500, %esi
66 movl $400, %edi
67 call foo
68 movl $0, %eax
69 leave
70 .cfi_def_cfa 7, 8
71 ret
72 .cfi_endproc
73 .LFE2:
74 .size main, .-main
75 .ident "GCC: (Ubuntu 4.8.4-2ubuntu1~14.04.3) 4.8.4"
76 .section .note.GNU-stack,"",@progbits
现在的编译器优化呀,我真是too young!
总结如下面这个图:
安利一下这个在线流程图编辑
总结一下,
- 函数调用通过call(调用处)/ret(被调用函数内部)实现ip入栈和跳转.
- 进入函数的栈上下文切换是在函数体内部进行栈基址入栈和栈基址赋值.
- 进入函数的栈上下文切换有可能不对%esp进行减.
- 退出函数时的栈上下文切换通过leave指令实现.
反正总之,只要是函数调用至少需要多4~5条指令,假如这个函数本身的指令很少,那是十分影响效率的。
step 1. 深入
- 什么是内联函数?(为什么要用内联)
函数的调用存在一定代价,如果函数本身执行的逻辑很少(假设一个极端情况就是函数本身执行的时间小于函数的调用代价),这样就造成效率低下。(*)
C中这种情境下常用宏来解决。但宏本身又缺少参数检查等机制,难以debug.
inline函数就在这种情况下采用,即结合了宏和函数两种机制的优点.- inline vs. 宏:
- 宏不能:
预处理器不能进行类型安全检查,或者进行自动类型转换。
对象外的宏展开不能访问对象的私有成员。 - 而对象外的内联函数调用却可以:
安全检查:内联函数参数在编译阶段进行安全检查。
自动类型转换:内联函数参数可以进行自动类型转换。
访问私有成员:这个是宏无法办到的。
编译器将在调用处展开,省去函数调用的代价。
- 宏不能:
- inline vs. 宏:
- 如何使用内联函数?
- 定义在类声明中的函数将默认被认为是内联的。
- 编译器可能拒绝内联:函数体过大或者存在循环/递归等。
- 在函数定义处写inline关键字,仅在声明处写inline不会起作用
(UPDATE:而且在声明处写inline还会造成一个警告 -inline function ‘void base::foo()’ used but never defined
)。 - ** 多文件调用内联:如果内联函数的定义不在本文件中(例如base.h声明了一个类,base.cc定义其的某个成员函数为内联,而在main.cc中调用base的某个对象的此成员函数)将可能**产生一个链接阶段的错误,类似于
也可能没有出错,但会产生歧义,跟编译器实现有关(?)。例如main.cc:(.text+0x21): undefined reference to `base::foo()' collect2: error: ld returned 1 exit status
附1 PHASE_4
的例子。 - 解决上一点最好的办法是,把inline函数的定义搬到.h中,谁需要调用inline谁就include我这个.h免得出现意想不到的结果。(这也是实际场景中经常做的。)
附1.
main.cc
#include "../common.h"//include stdio.h and so on.
#include "base.h"
//inline void base::foo(){ kk = 200001; }//<-----------在PHASE_2/3去掉注释
int test(base & kk);
int main()
{
base b(1000);
b.foo();
b.bar();
test(b);
b.bar();
return 0;
}
test.cc
#include "base.h"
//inline void base::foo(){ kk = 100001; } //<-----------在PHASE_3/4去掉注释
int test(base & kk){
kk.foo();
return 0;
}
base.cc
#include "base.h"
inline void base::foo(){ kk = 10; }
base::base(int s)
: kk(s){
}
base.h
#ifndef BASE_H
#define BASE_H
#include "../common.h"
class base{
public:
base(int s);
//inline void foo();
/*
base.h:8:18: error: inline function ‘void base::foo()’ used but never defined [-Werror]
inline void foo();
^
cc1plus: all warnings being treated as errors
*/
void foo();
void bar(){ printf("%d\n",kk);}
private:
int kk;
};
#endif
Makefile(顺便复习了一把Makfile!@!!)
target=test
main_src=main.cc test.cc
base_src=base.cc
base_target=$(patsubst %.cc,%.o,$(base_src))
CXX=g++
CXXFLAGS=-Werror
.PHONY:clean
all:$(main_src) $(base_target)
$(CXX) $(CXXFLAGS) $^ -o $(target)
$(base_target):$(base_src) #如果Make命令运行时没有指定目标,默认会执行Makefile文件的第一个目标。
$(CXX) -c $^ -o $@
clean:
rm -rf $(target) $(base_target)
- PHASE_1 main.cc & test.cc都没有inline base::foo的定义,出错。
/tmp/ccINLKCW.o: In function `main':
main.cc:(.text+0x21): undefined reference to `base::foo()'
/tmp/cc1MfEWZ.o: In function `test(base&)':
test.cc:(.text+0x14): undefined reference to `base::foo()'
collect2: error: ld returned 1 exit status
make: *** [all] Error 1
- PHASE_2 main.cc中的注释行去掉注释:
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
200001
200001
- PHASE_3 test.cc中的注释行也去掉注释,同时main.cc保持与PHASE_2一致。
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
200001
200001
- PHASE_4 只保留test.cc的inline定义.
root@vm1:/home/work/share/toys/CSE274.me/02_Cpp_Intro/testinline# ./test
100001
100001
这里PHASE_4,也是inline函数定义不在本文件中,但没有出现ld错误。