类的内容有点多,分章节研究
结构体跟类
1.结构体跟类
结构体跟类本质上没有太大区别,只是结构体成员默认为public的,而类则是private。这里直接省去了结构体的反汇编。直接看类。
#include<iostream>
using namespace std;
class A
{
public:
/*
以下数据成员探讨 一个类的大小由什么决定
以及数据排列 内存对齐等特性
*/
const static int sos = 654;
int old;
int old2;
char ui;
char po[14];
/*
以下来探讨函数在类中是如何表现的
*/
int getNumber()
{
return old + old2;
}
//这个函数来看看this指针到底是什么
void seeThis()
{
this->old = 555;
}
};
int main()
{
A a;
a.getNumber();
a.seeThis();
A b;
b.getNumber();
b.seeThis();
return 0;
}
选择了静态反汇编工具IDAPro跟动态反汇编OD(便于调试),配合进行 。附上了反汇编代码
main函数部分:
这里并没有发现实例对象的代码.(构造函数的问题?这里先放放),看其他部分
.text:004013B0 ; int main()
.text:004013B0 public _main
.text:004013B0 _main proc near ; CODE XREF: ___mingw_CRTStartup+F8p
.text:004013B0 push ebp
.text:004013B1 mov ebp, esp
.text:004013B3 and esp, 0FFFFFFF0h
.text:004013B6 sub esp, 30h
.text:004013B9 call ___main
//这里开始
//把a的首地址赋值给eax,然后赋值给ecx
.text:004013BE lea eax, [esp+18h]
.text:004013C2 mov ecx, eax
//调用a的成员函数getNumber()
.text:004013C4 call __ZN1A9getNumberEv ; A::getNumber(void)
//把a的首地址赋值给eax,然后赋值给ecx
.text:004013C9 lea eax, [esp+18h]
.text:004013CD mov ecx, eax
//调用a的成员函数seeThis()
.text:004013CF call __ZN1A7seeThisEv ; A::seeThis(void)
//把b的首地址赋值给eax,然后赋值给ecx
.text:004013D4 mov eax, esp
.text:004013D6 mov ecx, eax
//调用b的getNumber()
.text:004013D8 call __ZN1A9getNumberEv ; A::getNumber(void)
//把b的首地址放到ecx中
.text:004013DD mov eax, esp
.text:004013DF mov ecx, eax
//调用b的seeThis()函数
.text:004013E1 call __ZN1A7seeThisEv ; A::seeThis(void)
.text:004013E6 mov eax, 0
.text:004013EB leave
.text:004013EC retn
.text:004013EC _main endp
这里产生的问题:
1.类的在内存中的排列由什么决定?
看到 .text:004013BE 这行 与 .text:004013D4 这行。esp栈顶寄存器相隔18h,也就是24字节大小。反观类A,两个int类型,
一个char型,一个14字节的char数组,一个静态成员。除去静态成员4字节的大小(静态数据成员不算大小),一个char 加上 char数组 (这里有内存对齐,对齐的部分留空),一共算16字节,两个int 8字节,相加为24字节。
这里可以初步总结:不算内存对齐的留空内存,类在内存中的排列方式大致跟数组相同。静态数据成员其实跟全局变量相同。只不过编译器做了作用域限制。这里不再详细给出,但是可以肯定是,静态数据成员的位置不在栈空间中。那么类在内存中的排列其实还是看编译器怎么处理,内存对齐也复杂,这里以看到的为准,就轻描淡写的过了。
2.在调用成员函数时,为什么要把a,b的首地址放到ecx寄存器中?
在看一段反汇编代码:
.text:0041B6B0 ; void __cdecl A::seeThis(A *const this)
.text:0041B6B0 __ZN1A7seeThisEv proc near ; CODE XREF: _main+1Fp
.text:0041B6B0 ; _main+31p
.text:0041B6B0
.text:0041B6B0 this = dword ptr -4
.text:0041B6B0
.text:0041B6B0 push ebp
.text:0041B6B1 mov ebp, esp
.text:0041B6B3 sub esp, 4
//ecx即为this
.text:0041B6B6 mov [ebp+this], ecx
//this放入eax
.text:0041B6B9 mov eax, [ebp+this]、
//this->old = 555;
.text:0041B6BC mov dword ptr [eax], 22Bh
.text:0041B6C2 leave
.text:0041B6C3 retn
.text:0041B6C3 __ZN1A7seeThisEv endp
.text:0041B6C3
.text:0041B6C4
.text:0041B6C4 ; =============== S U B R O U T I N E =======================================
.text:0041B6C4
.text:0041B6C4 ; Attributes: static bp-based frame
.text:0041B6C4
.text:0041B6C4 ; int __cdecl A::getNumber(A *const this)
.text:0041B6C4 public __ZN1A9getNumberEv
.text:0041B6C4 __ZN1A9getNumberEv proc near ; CODE XREF: _main+14p
.text:0041B6C4 ; _main+28p
.text:0041B6C4
.text:0041B6C4 this = dword ptr -4
.text:0041B6C4
.text:0041B6C4 push ebp
.text:0041B6C5 mov ebp, esp
.text:0041B6C7 sub esp, 4
//把首地址放入 ebp+this(此this非彼this,不要弄混淆)
.text:0041B6CA mov [ebp+this], ecx
//return old + old2;
.text:0041B6CD mov eax, [ebp+this]
.text:0041B6D0 mov edx, [eax]
.text:0041B6D2 mov eax, [ebp+this]
.text:0041B6D5 mov eax, [eax+4]
.text:0041B6D8 add eax, edx
.text:0041B6DA leave
.text:0041B6DB retn
.text:0041B6DB __ZN1A9getNumberEv endp
以下分别为getNumber()跟seeThis()的反汇编代码,部分解释在注释里
1.这里发现神奇的事情,a跟b这两个对象,共用一段函数代码。也就是说,在实例类对象时,不会重复定义函数,函数只会存在一份。那么此时又产生一个问题,如何识别当前调用的是a的函数还是b的函数?承接上面的问题,关键在ecx寄存器。ecx即为this。函数执行的时候会以这个this当做基地址+上成员变量的偏移地址。这有点像多线程中的可重入函数。当然,具体以什么寄存器作为this的保存者,具体看编译器怎么决定,大致做法都是一样的。可能这里ecx为esi,edi都有可能。
2.这里可以看到类成员函数区别于普通函数的是,编译器隐藏了一个参数传递.为以下:
.text:0041B6B0 ; void __cdecl A::seeThis(A *const this)
.text:0041B6C4 ; int __cdecl A::getNumber(A *const this)
A*const this。这是编译器通过ecx传递过去的参数。具体怎么传递,看编译器的传递方式,这里为c的调用方式。也可以用stdcall的调用方式。
由此,初步结论。类变得不再那么神秘,它是编译器抽象化的产物,数据排列方式大致如数组,成员函数不在类中定义(但同类的对象公用一个函数代码块,这就跟调用普通函数没有任何区别),静态成员跟全局变量相同,只不过编译器做了作用域限制。this指针即为对象的首地址。一般存入某个寄存器。