c++ 类方法和普通方法调用有何不同?

本文将简单对比分析普通方法和类方法调用的不同之处,测试环境为 MacOS。

这里不考虑虚函数和类静态方法,有关虚函数的内容,可以看我以前发的文章。

1、域

我们知道如果调用的普通方法在前面没有声明或定义,会导致报错:

image.png

因为 c/c++ 的解析文件的符号顺序是从上往下,所以图中的 test 函数需要挪到 main 函数之前。

而类/结构体这种域结构的,方法的顺序并不重要,不影响解析结果:

image.png

如图,这里的构造方法可以正常地调用到后面的 fun 函数。

1.1、符号解析过程

简单解释下为什么类中解析符号不会出错,拿 LLVM/clang 编译器举例:

When parsing an inline member function declaration/definition, it does full parsing and semantic analysis of the declaration, leaving the definition for later.

Specifically, the body of an inline method definition is lexed and the tokens are kept in a special buffer for later (this is done by Parser::ParseCXXInlineMethodDef).

Once the parser has finished parsing the class, it calls Parser::ParseLexedMethodDefs that does the actual parsing and semantic analysis of the saved method bodies.

At this point, all the types declared inside the class are available, so the parser can correctly disambiguate wherever required.

注意:这里的 inline 不是代表内联,是指函数在 c++ 类/结构体里面。

2、汇编对比调用方式

#include <stdio.h>

class A{
public:
  A(){ classfun(); };

  void classfun(){
    printf("123\n");
  }
};

void normalfun(){
  printf("456\n");
}

int main(){
  A a;
  normalfun();
}

通过 MachOView 发现类方法变成了 indirect symbol:

image.png

接下来要上汇编了,不过不用慌,在指令傍边我会给出注释,看多了就熟悉了,而且大部分指令对我们不重要,关注调用方式就好:

  • main
                     _main:
0000000100007ea4         sub        sp, sp, #0x20                    ; 开辟栈空间
0000000100007ea8         stp        x29, x30, [sp, #0x10]            ; 保存栈底指针和 LR (函数返回地址)
0000000100007eac         add        x29, sp, #0x10                   ; 更新栈底,这几步都是调用约定
0000000100007eb0         sub        x8, x29, #0x1                    ; 取栈下偏移一个字节的地址,因为这个类只有 1 字节大小
0000000100007eb4         mov        x0, x8                           ; 给 x0 保存,用作给函数传参数,即 this 指针
0000000100007eb8         str        x8, [sp]                         ; 先存好这个地址先
0000000100007ebc         bl         __ZN1AC1Ev                       ; A::A()
0000000100007ec0         ldr        x8, [sp]                         ; 加载回这个地址
0000000100007ec4         mov        x0, x8                           ; 再入参调用函数,this 指针
0000000100007ec8         bl         imp___stubs___ZN1A8classfunEv    ; A::classfun()
0000000100007ecc         bl         __Z9normalfunv                   ; normalfun()
0000000100007ed0         movz       w9, #0x0
0000000100007ed4         mov        x0, x9
0000000100007ed8         ldp        x29, x30, [sp, #0x10]
0000000100007edc         add        sp, sp, #0x20
0000000100007ee0         ret
                        ; endp

可以清楚地看到调用类方法时,会准备第一个参数为 this 指针,即对象存储的地址。

这里 A 类对象是在 main 函数栈内存存储的对象,只是在函数调用中传递了这个栈内存地址,因为 main 函数栈一直在,所以用起来像在堆一样。

  • A::A()
                     __ZN1AC1Ev:        // A::A()
0000000100007ee4         sub        sp, sp, #0x20
0000000100007ee8         stp        x29, x30, [sp, #0x10]
0000000100007eec         add        x29, sp, #0x10                 ; 前几句继续调用约定
0000000100007ef0         str        x0, [sp, #0x8]                 ; 保存好对象要放的地址
0000000100007ef4         ldr        x8, [sp, #0x8]                 ; 放到 x8
0000000100007ef8         mov        x0, x8                         ; 又给 x0,其实是要给下面调用函数传参
0000000100007efc         str        x8, [sp]                       ; 再放一份到栈上
0000000100007f00         bl         __ZN1AC2Ev                     ; 又有一个 A::A(),实际是真正的执行体
0000000100007f04         ldr        x8, [sp]
0000000100007f08         mov        x0, x8
0000000100007f0c         ldp        x29, x30, [sp, #0x10]
0000000100007f10         add        sp, sp, #0x20
0000000100007f14         ret
                        ; endp

                     __ZN1AC2Ev:        // A::A()
0000000100007f40         sub        sp, sp, #0x20                  
0000000100007f44         stp        x29, x30, [sp, #0x10]
0000000100007f48         add        x29, sp, #0x10                 ; 前几句继续调用约定
0000000100007f4c         str        x0, [sp, #0x8]
0000000100007f50         ldr        x8, [sp, #0x8]
0000000100007f54         mov        x0, x8
0000000100007f58         str        x8, [sp]
0000000100007f5c         bl         imp___stubs___ZN1A8classfunEv  ; A::classfun()
0000000100007f60         ldr        x0, [sp]
0000000100007f64         ldp        x29, x30, [sp, #0x10]
0000000100007f68         add        sp, sp, #0x20
0000000100007f6c         ret
                        ; endp

A::A() 构造函数被分成了两部分:准备阶段和实际的执行体。

  • imp___stubs___ZN1A8classfunEv
                     imp___stubs___ZN1A8classfunEv:        // A::classfun()
0000000100007f3c         nop                                      
0000000100007f40         ldr        x16, =__ZN1A8classfunEv      ; __ZN1A8classfunEv
0000000100007f44         br         x16                          ; __ZN1A8classfunEv
                        ; endp

普通方法 normalfun() 是直接跳转过去执行,而类方法这里是通过寄存器跳转到 A::classfun() 的地址执行,加了一个间接跳转的操作。

为什么这里给加了个间接跳转的操作呢?我猜测优化级别低可能会是这种情况。

当用了 -O1 优化时,果然是直接调用了;当用了 -O2 优化时,整个类都被优化掉了。

3、小结

好了,得到了一个人尽皆知、教科书上都写着的结论:类方法第一个参数是 this 指针。

4、参考 & 推荐阅读

How Clang handles the type / variable name ambiguity of C/C++

评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值