[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!
总结如下面这个图:

堆栈分析 [如果太小,请点这个](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错误。

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,547评论 6 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,399评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,428评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,599评论 1 274
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,612评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,577评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,941评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,603评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,852评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,605评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,693评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,375评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,955评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,936评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,172评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,970评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,414评论 2 342

推荐阅读更多精彩内容