C++中类的成员函数是如何调用this指针
起因
最近工作中遇到一个有趣的闪退问题,原因很简单,就是因为调用了一个空对象的成员函数。但是在dump的堆栈信息里发现程序终止的地方并不是函数刚开始,而是在函数内部执行很多语句后才终止,类似于:
class Example
{
// 不重要的乱七八糟
void DumpFunction(//稀奇古怪的参数)
{
代码甲
代码乙
代码丙
...
代码癸 <- 对,就在癸(gui)这里终止了
...
}
// 也不重要的乱七八糟
}
Example* shit = new Example();
shit = NULL;
shit->DumpFunction(//稀奇古怪的参数)
按照自己之前的理解,既然是空指针调用成员函数,那应该在函数最开始的地方就直接终止了(因为对空指针进行了访问),不应该执行之后的语句,但是事实却是执行了一堆代码之后才终止,当时因为着急功能开发,所以忽略了这个问题,但是后续和潘大沟通的时候,潘大提供了一些信息并建议看看反汇编里进行了哪些操作,于是就有了这篇文章。
潘大的信息
C++对类的成员函数的实现相当于在C语言的函数功能上加了this指针,并且调用时默认传入,这样就变成了类的成员函数。这也就解释了为什么成员函数不占用类对象的字节空间。
分析
既然成员函数和类对象没关系,那如果我在函数中做各种操作,但是就是不调用this指针的话,那即使用空对象调用这个成员函数,也就应该不会有任何问题。测试了以下代码:
class Example
{
int CanYouBoom(int fire,int yao)
{
int boom = fire + yao;
return boom
}
}
void main()
{
Example* shit = NULL;
shit->CanYouBoom(6,9);
}
果然没有崩溃。
探究
现在确定了从潘大处获取的信息是事实,但是究竟是咋实现的呢,this是到底咋传进去的,带着这个疑问,进行了以下的探究
测试代码
class Example
{
private:
int m_smell = 0;
public:
int LiuLianBoom(int ni_cai,int hao_wen_bu)
{
int xiang = ni_cai + hao_wen_bu;
return xiang;
}
int ShitBoom(int ni_cai,int hao_wen_bu)
{
this->m_smell = 0;
int xiang = ni_cai + hao_wen_bu;
return xiang;
}
};
int main()
{
Example* shit = nullptr;
shit->LiuLianBoom(1,2);
shit->ShitBoom(3,4);
}
代码中Example的LiuLianBoom和ShitBoom成员函数只有“this->m_smell = 0;”的差异,其余都一样,那么编译后有什么区别呢,咱们可以通过反汇编来看下。
编译后进行反汇编
编译器这里用的g++编译器。在通过objdump命令对.o文件进行反汇编后得到如下信息(截取关键内容)
Disassembly of section .text:
0000000000000000 <main>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 30 sub $0x30,%rsp
8: e8 00 00 00 00 call d <main+0xd>
d: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp)
14: 00
15: 48 8b 45 f8 mov -0x8(%rbp),%rax
19: 41 b8 02 00 00 00 mov $0x2,%r8d
1f: ba 01 00 00 00 mov $0x1,%edx
24: 48 89 c1 mov %rax,%rcx
27: e8 00 00 00 00 call 2c <main+0x2c>
2c: 48 8b 45 f8 mov -0x8(%rbp),%rax
30: 41 b8 04 00 00 00 mov $0x4,%r8d
36: ba 03 00 00 00 mov $0x3,%edx
3b: 48 89 c1 mov %rax,%rcx
3e: e8 00 00 00 00 call 43 <main+0x43>
43: b8 00 00 00 00 mov $0x0,%eax
48: 48 83 c4 30 add $0x30,%rsp
4c: 5d pop %rbp
4d: c3 ret
Disassembly of section .text$_ZN7Example11LiuLianBoomEii:
0000000000000000 <_ZN7Example11LiuLianBoomEii>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 48 89 4d 10 mov %rcx,0x10(%rbp)
c: 89 55 18 mov %edx,0x18(%rbp)
f: 44 89 45 20 mov %r8d,0x20(%rbp)
13: 8b 55 18 mov 0x18(%rbp),%edx
16: 8b 45 20 mov 0x20(%rbp),%eax
19: 01 d0 add %edx,%eax
1b: 89 45 fc mov %eax,-0x4(%rbp)
1e: 8b 45 fc mov -0x4(%rbp),%eax
21: 48 83 c4 10 add $0x10,%rsp
25: 5d pop %rbp
26: c3 ret
Disassembly of section .text$_ZN7Example8ShitBoomEii:
0000000000000000 <_ZN7Example8ShitBoomEii>:
0: 55 push %rbp
1: 48 89 e5 mov %rsp,%rbp
4: 48 83 ec 10 sub $0x10,%rsp
8: 48 89 4d 10 mov %rcx,0x10(%rbp)
c: 89 55 18 mov %edx,0x18(%rbp)
f: 44 89 45 20 mov %r8d,0x20(%rbp)
13: 48 8b 45 10 mov 0x10(%rbp),%rax
17: c7 00 00 00 00 00 movl $0x0,(%rax)
1d: 8b 55 18 mov 0x18(%rbp),%edx
20: 8b 45 20 mov 0x20(%rbp),%eax
23: 01 d0 add %edx,%eax
25: 89 45 fc mov %eax,-0x4(%rbp)
28: 8b 45 fc mov -0x4(%rbp),%eax
2b: 48 83 c4 10 add $0x10,%rsp
2f: 5d pop %rbp
30: c3 ret
通过对比,发现两个函数只有:13: 48 8b 45 10 mov 0x10(%rbp),%rax
17: c7 00 00 00 00 00 movl $0x0,(%rax)
这两行有区别,对这两行解释下。13行是将rbp寄存器中保存的地址偏移0x10(10进制为16)个字节后的地址写入rax寄存器,而17行的操作是将0值写入了rax寄存器保存的地址中去。所以,从反汇编结果看的话,0x10(%rbp)里保存的是this->m_smell地址。
但是this->m_smell的地址是如何保存到0x10(%rbp)这里的呢,这就需要看下8~f的代码,分别将rcx、edx、r8d三个寄存器的内容写入了0x10(%rbp)、0x18(%rbp)、0x20(%rbp),那这三个寄存器的数据从哪里来的,可以参看
d: 48 c7 45 f8 00 00 00 movq $0x0,-0x8(%rbp) 对应了代码bitch = nullptr。
14: 00
15: 48 8b 45 f8 mov -0x8(%rbp),%rax
19: 41 b8 02 00 00 00 mov $0x2,%r8d 将形参2传入r8d寄存器中。
1f: ba 01 00 00 00 mov $0x1,%edx 传形参1传入edx寄存器中。
24: 48 89 c1 mov %rax,%rcx 将bitch指针本身的地址传入rcx中。
这样寄存器的存和取正好对应上了。
结论
·调用空对象的成员函数不一定宕机,主要看成员函数里有没有对this指针进行访问地址操作。不过成员函数如果对类成员变量没有任何操作的话完全可以写成静态函数或者全局函数之类的。
·设计者真的是一堆大神,函数加个this指针就完美实现了类成员函数的功能。
·这次是真的理解了为什么类成员函数并不占用类对象的字节大小。
原创文章,作者:Maggie-Hunter,如若转载,请注明出处:https://blog.ytso.com/273362.html