从汇编和编译器角度分析C++得this指针和成员函数寻址

(鄙人总结,希望和大家交流,切莫转载,谢谢!)

引入

先看一段这个代码:

#include 

class Moo {
 public:
  void Printf(int a) { ::printf("%p, %d\n", this, a); }
};

int main() {
  Moo *p = nullptr;
  p->Printf(1);

  p = new Moo;
  p->Printf(1);
  delete p;
}

执行结果是:

0000000000000000, 1
00000226001ABCC0, 1

可以看到,当指针p为空时,调用成员函数打印"this"指针为0,当p指向一个实际对象时,"this"为一个实际的内存地址。
问题来了,明明"this"没有在参数列表中出现,为什么在函数体内可以使用呢?是不是非静态成员函数自带了this这个关键字来引用对象本身。

历史

定义一个全局函数,其中传入Moo的指针类型

#include 

class Moo {
 public:
  void Printf(int a) { ::printf("%p, %d\n", this, a); }
};

// 增加全局函数
void Print(Moo *_this, int a) { ::printf("%p, %d\n", _this, a); }

int main() {
  Moo *p = nullptr;
  p->Printf(1);

  p = new Moo;
  p->Printf(1);

  Print(p, 1);
  delete p;
}

可以从执行结果中看到,Moo::PrintPrint的执行结果是相同的

(nil), 1
0x555874ccc2c0, 1
0x555874ccc2c0, 1

查看Intel asm汇编,在未优化的汇编代码中可以看到这两个函数的汇编指令是相同的。可以知道Moo::PrintPrint同样都是传入了Moo的指针

.LC0:
        .string "%p, %d\n"
Moo::Printf(int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi		# Moo类型指针,8位
        mov     DWORD PTR [rbp-12], esi		# int4位
        mov     edx, DWORD PTR [rbp-12]
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        leave
        ret
Print(Moo*, int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi		# Moo类型指针,8位
        mov     DWORD PTR [rbp-12], esi		# int4位
        mov     edx, DWORD PTR [rbp-12]
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        leave
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], 0
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 1
        mov     rdi, rax
        call    Moo::Printf(int)
        mov     edi, 1
        call    operator new(unsigned long)
        mov     QWORD PTR [rbp-8], rax
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 1
        mov     rdi, rax
        call    Moo::Printf(int)
        mov     rax, QWORD PTR [rbp-8]
        mov     esi, 1
        mov     rdi, rax
        call    Print(Moo*, int)
        mov     rax, QWORD PTR [rbp-8]
        test    rax, rax
        je      .L4
        mov     esi, 1
        mov     rdi, rax
        call    operator delete(void*, unsigned long)
.L4:
        mov     eax, 0
        leave
        ret

原因

非静态成员函数实际上的形参个数会多隐含一个"this",它用于引用对象本身。在成员函数执行的过程中,通过"this"找到对象的地址,于是可以按照内存布局,找到对象的所有非静态成员变量的地址。

拓展1

const修饰的对象只能调用const方法,究其原因也是因为这个"this"
看下面这段代码:

class Moo {
 public:
  void Printf(int a) { ::printf("%p, %d\n", this, a); }
  void Printf(int a) const { ::printf("%p, %d\n", this, a); }
};

这两个函数完成了重载,没有报错的原因在于this类型的不同:
从汇编和编译器角度分析C++得this指针和成员函数寻址_第1张图片
从汇编和编译器角度分析C++得this指针和成员函数寻址_第2张图片
可以看到在参数列表外面的右边使用const来修饰后,this被const修饰为底层const指针,即这个指针引用的对象的内容是不允许修改的

#include 

class Moo {
 public:
  void Printf(int a) { ::printf("non-const %p, %d\n", this, a); }
  void Printf(int a) const { ::printf("const %p, %d\n", this, a); }
  void HelloConst() const { ::printf("hello\n"); }
  void Hello() { ::printf("hello\n"); }
};

int main() {
  Moo *p1 = new Moo;        // Moo *
  Moo const *p2 = new Moo;  // const Moo *

  p1->Printf(233);
  p2->Printf(666);
  
  p1->HelloConst();
  // p2->Hello();

  delete p1;
  delete p2;
}
  • 未被const修饰的对象p1,编译器选择void Printf();
  • 未被const修饰的对象p1调用HelloConst是ok的,因为Moo *a; const Moo *b = a;是成立的。
  • 被const修饰的对象p2,因此编译器选择void Printf() const;
  • 被const修饰的对象p2调用Hello是错误的,因为const Moo *a; Moo *b = a;是不成立的。

从汇编代码层面分析,被const修饰和未被const修饰的两个函数体相同的同名函数,虽然生成的汇编指令一模一样,但在.text端的地址是不同的,因此在最终寻址上会区别开。

.LC0:
        .string "non-const %p, %d\n"
Moo::Printf(int):
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     edx, DWORD PTR [rbp-12]
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC0
        mov     eax, 0
        call    printf
        nop
        leave
        ret
.LC1:
        .string "const %p, %d\n"
Moo::Printf(int) const:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], rdi
        mov     DWORD PTR [rbp-12], esi
        mov     edx, DWORD PTR [rbp-12]
        mov     rax, QWORD PTR [rbp-8]
        mov     rsi, rax
        mov     edi, OFFSET FLAT:.LC1
        mov     eax, 0
        call    printf
        nop
        leave
        ret

额外结论1

是故得出一个额外的结论:const 修饰成员函数时,根本上是修饰了 "this"

拓展2

之前有提到静态成员函数。静态成员函数不属于任何类的实例化对象,而是属于类。既然不属于某个特定的对象,所以就不需要"this",于是无法访问类的其他成员变量,只能访问静态变量了。

// 此处不含"this", 编译器会报错
static void static_print() { this; cout << "const static_print" << endl; };

在这里插入图片描述

额外结论2

因此C++11以前,使用静态成员函数作为回调函数注册给调用者。普通成员函数不能做到这一点,因为在注册时由于隐含的"this"使得函数参数个数不匹配,导致回调函数注册失败。
静态成员函数因为没有"this",所以参数是匹配的。

C++11 引入的类模板std::function跟可变参函数模板std::bind(_Fx&& _Func, _Types&&... _Args)搭配可以做到普通成员函数作为回调函数设置,原理也就是将this绑定到生成的调用对象上,生成的调用对象编译成汇编指令后,不会去拷贝this到寄存器中。

拓展3

最开始的示例中提到一个nullptr去调用成员函数,却没造成段错误的原因是:
Moo*nullptr去调用成员函数时,并没有比对着Moo的内存模型去修改对应地址的值,也就是说并没有去修改一个权限为只读的段空间,也就是说CPU数据总线,没有把数据写回用户空间的只读区域,所以不会造成段错误。
比如下面代码依旧不会出错:

#include 

class Moo {
 private:
  int m = 10;
  
 public:
  void Printf() {
    this;
    this->m;
  }
};

int main() {
  Moo *p1 = nullptr;
  p1->Printf();
}

它对应的汇编:

Moo::Printf():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        nop
        pop     rbp
        ret
main:
        push    rbp
        mov     rbp, rsp
        sub     rsp, 16
        mov     QWORD PTR [rbp-8], 0
        mov     rax, QWORD PTR [rbp-8]
        mov     rdi, rax
        call    Moo::Printf()
        mov     eax, 0
        leave
        ret

从汇编代码来看,根本没有去获取地址中的m的内容。

如果修改成下面,当空指针访问时,就会出现段错误:

class Moo {
 public:
  int m = 10;
  void Printf() {
    this->m++;;
  }
};

因为对应的汇编变成了下面的情况:

Moo::Printf():
        push    rbp
        mov     rbp, rsp
        mov     QWORD PTR [rbp-8], rdi
        mov     rax, QWORD PTR [rbp-8]
        mov     eax, DWORD PTR [rax]
        lea     edx, [rax+1]
        mov     rax, QWORD PTR [rbp-8]
        mov     DWORD PTR [rax], edx
        nop
        pop     rbp
        ret

你可能感兴趣的:(this指针,C++11,c++,指针)