[cpp deep dive]一些奇怪的关键字_不那么奇怪的inline

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指令 -
      等价于
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!
总结如下面这个图:

[cpp deep dive]一些奇怪的关键字_不那么奇怪的inline_第1张图片
堆栈分析 [如果太小,请点这个](https://www.processon.com/view/57938177e4b06834741b7e17)

安利一下这个在线流程图编辑

总结一下,

  • 函数调用通过call(调用处)/ret(被调用函数内部)实现ip入栈和跳转.
  • 进入函数的栈上下文切换是在函数体内部进行栈基址入栈和栈基址赋值.
  • 进入函数的栈上下文切换有可能不对%esp进行减.
  • 退出函数时的栈上下文切换通过leave指令实现.

反正总之,只要是函数调用至少需要多4~5条指令,假如这个函数本身的指令很少,那是十分影响效率的。

step 1. 深入

  • 什么是内联函数?(为什么要用内联)
    函数的调用存在一定代价,如果函数本身执行的逻辑很少(假设一个极端情况就是函数本身执行的时间小于函数的调用代价),这样就造成效率低下。(*)
    C中这种情境下常用宏来解决。但宏本身又缺少参数检查等机制,难以debug.
    inline函数就在这种情况下采用,即结合了宏和函数两种机制的优点.
    • 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错误。

你可能感兴趣的:([cpp deep dive]一些奇怪的关键字_不那么奇怪的inline)