废话不多说,直接进入主题,大家把下面的代码调试一番,相信对基类的内存布局会有所了解
#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <memory>
class CBase
{
public:
CBase(){
printf("Init Base\r\n");
strcpy(m_szText, "null");
}
~CBase(){
printf("Uninit Base\r\n");
}
virtual void SetText(const char* strText){
strcpy(m_szText, strText);
}
virtual void __stdcall SetText2(const char* strText){
strcpy(m_szText, strText);
}
void ShowText(){
printf("%s\n", m_szText);
}
void ShowMe() {
printf("%p\n", this);
}
private:
char m_szText[64];
};
struct TBase;
typedef void(__thiscall* set_text_ptr)(TBase* _this_ptr, const char* strText);
typedef void(__stdcall* set_text_ptr2)(TBase* _this_ptr, const char* strText);
typedef void(__thiscall* show_text_ptr)(TBase* _this_ptr);
struct TBaseVtbl{
set_text_ptr pfnSetText;
set_text_ptr2 pfnSetText2;
};
struct TBase{
struct TBaseVtbl* pVtbl;
char szText[64];
};
int main()
{
CBase* pBase = new CBase();
struct TBase* pBase2 = (struct TBase*)pBase;
pBase->ShowText();
pBase2->pVtbl->pfnSetText(pBase2, "hello");
pBase->ShowText();
pBase2->pVtbl->pfnSetText2(pBase2, "hello world");
pBase->ShowText();
strcpy(pBase2->szText, "strcpy");
void(CBase:: * cfnShowText)(void) = &CBase::ShowText;
show_text_ptr pfnShowText = (show_text_ptr) * (void**)&cfnShowText;
pfnShowText(pBase2);
struct TBase __Base3 = { 0, "text" };
CBase* pBase4 = (CBase *)&__Base3;
//由于没有对虚函数赋值,下面这段代码会崩溃
//pBase4->SetText("set text");
//普通成员函数独立于class对象的内存之外,所以不需要对函数指针赋值也是正常访问
pBase4->ShowText();
delete pBase;
getchar();
return 0;
}
上面的代码有两个知识点,
-
基类的虚函数,其实是一个指针数组或者说队列也可以,这里我们用 struct TBaseVtbl来展示了它的内存结构,而且它的位置是放在class内存布局的第一位,基类 class 内存布局都是
class{
虚函数列表指针;
其他成员变量;
};
即时你把成员变量定义在虚函数前面,布局也是不会变的。
如果没有虚函数则布局如下:
class{
其他成员变量;
}; -
类成员函数,都隐藏了一个 this 指针的参数,不管何种调用约定,它都是放在函数的第一个参数。而成员函数的调用约定,默认为__thiscall , 当然你也可以声明为其他调用约定。不过所谓的调用约定,只有在 x86平台下有区别,现在x64平台只有 __stdcall ,而且 x64的__stdcall 跟 x86 的__stdcall 不是同一个东西,有兴趣的同学自己网上看看资料。
-
普通成员函数指针是不包含在class对象的内存里面的,它独立于对象之外,注意class包含的只是函数指针,不是函数的代码段!不管是虚函数还是普通成员函数,它的代码段,都是独立于class 对象之外的!
大家一定要理解class里面的虚函数和普通成员函数是何如被调用的, 假设有如下代码:
int main()
{
CBase* pBase = NULL;
pBase->ShowMe(); //会崩溃吗?改成虚函数呢?
pBase->ShowText(); //会崩溃吗?
getchar();
return 0;
}
我用C语言描绘一下它们是如何被调用的:
void CBase_ShowMe(struct TBase* pBase)
{
printf("%p\n", pBase);
}
void CBase_ShowText(struct TBase* pBase)
{
printf("%s\n", pBase->szText);
}
int main()
{
CBase* pBase = NULL;
pBase->ShowMe(); //普通成员函数不会崩,因为没有对this指针的成员进行访问!
//重复一下,没有崩是因为没有this指针的成员进行访问!
CBase_ShowMe((struct TBase* )pBase); //pBase->ShowMe() 编译后的访问形式
//可以看到,相当于 CBase_ShowMe 的参数为空,又怎么会崩呢?
//如果pBase->ShowMe() 是虚函数,则调用的伪代码如下,
//struct TBase* pBase2 = NULL;
//pBase2->pVtbl->pfnShowMe(pBase2); //在pBase2->pVtbl 这个动作的时候就已经崩了
//因为虚表函数也是this指针的成员,所以它会崩
pBase->ShowText();
//CBase_ShowText函数被调用不会崩,因为ShowText函数不是this指针的内部成员!
//它是在函数内部崩的,因为它对this指针的成员进行了访问!
//因为里面有 pBase->szText 这句代码,相当于 (NULL)->szText
CBase_ShowText((struct TBase*)pBase);//pBase->ShowText() 编译后的访问形式
getchar();
return 0;
}
上面的代码虽然没有百分百还原编译后的成员函数和虚函数的调用过程,但思想上跟编译后的函数调用过程是一样的!其实C++ 的成员函数和虚函数调用跟C语言的普通函数调用区别仅在于语法之上,编译后的调用本质是没有区别的!
基类的内存布局和函数调用大家都了解了吧,我们再来看看单继承的内存布局。
#define _CRT_SECURE_NO_WARNINGS
#define _CRT_SECURE_NO_WARNINGS
#include <stdlib.h>
#include <stdio.h>
#include <string>
#include <memory>
#include <windows.h>
class CBase
{
public:
CBase(){
printf("Init Base\r\n");
strcpy(m_szText, "null");
}
~CBase(){
printf("Uninit Base\r\n");
}
virtual void SetText(const char* strText){
strcpy(m_szText, strText);
}
void SetText2(const char* strText)
{
sprintf(m_szText, "msg=%s", strText);
}
void ShowText(){
printf("%s\n", m_szText);
}
protected:
char m_szText[64];
};
class CDerive : public CBase
{
public:
CDerive():CBase() {
printf("Init CDerive\r\n");
memset(m_szTitle, 0, sizeof(m_szTitle));
}
~CDerive() {
printf("Uninit CDerive\r\n");
}
virtual void SetText(const char* strText) {
sprintf(m_szText, "text=%s", strText);
}
virtual void __stdcall SetTitle(const char* strText) {
sprintf(m_szTitle, "%s", strText);
}
void SetTitle2(const char* strText)
{
sprintf(m_szTitle, "title=%s", strText);
}
void ShowTitle() {
printf("%s\n", m_szTitle);
}
private:
char m_szTitle[64];
};
struct __TDerive;
typedef void(__thiscall* set_text_ptr)(struct __TDerive* _this_ptr, const char* strText);
typedef void(__stdcall* set_title_ptr)(struct __TDerive* _this_ptr, const char* strText);
typedef struct __TDeriveVtbl{
set_text_ptr pfnSetText;
set_title_ptr pfnSetTitle;
}TDeriveVtbl;
typedef struct __TDerive {
TDeriveVtbl* pVtbl;
char szText[64];
char szTitle[64];
}TDerive;
typedef struct __TTemp {
void * pNot;
char szText[64];
char szTitle[64];
}TTemp;
void __stdcall UpdateText(struct __TDerive* _this_ptr, const char* strText)
{
sprintf(_this_ptr->szText, "%s", strText);
}
int main()
{
CDerive* pDerive = new CDerive();
TDerive* pDerive2 = (TDerive* )pDerive;
//原来继承的时候,新的虚函数,覆盖了基类的虚函数指针
pDerive2->pVtbl->pfnSetText(pDerive2, "showtext");
pDerive->ShowText();
//而新增的虚函数位置,跟基类共用一个虚表,位置在所有基类虚函数的后面
//struct 虚表 {
// 基类虚函数;
// 派生类虚函数;
//};
pDerive2->pVtbl->pfnSetTitle(pDerive2, "showtitle");
pDerive->ShowTitle();
//下面做一些骚操作
TTemp Temp = { 0 };
CDerive* pTemp = (CDerive*)&Temp;
//我用 pDerive2对象的虚函数修改了 pTemp对象的信息,哈哈哈
pDerive2->pVtbl->pfnSetText((TDerive*)&Temp, "骚操作");
//Temp只是一个结构体,类型转换后可以调用成员函数,
pTemp->ShowText();
pTemp->SetText2("第二次骚操作");
pTemp->ShowText();
//让它具备调用虚函数的能力
Temp.pNot = pDerive2->pVtbl;
pTemp->SetTitle("第三次骚操作");
pTemp->ShowTitle();
TDerive* pDerive4 = (TDerive*)new CDerive();
if (pDerive4->pVtbl == pDerive2->pVtbl)
{
printf("相同的类,使用同一个虚表\n");
}
//由于权限不足,下面的代码会崩,说明系统帮我们申请的虚表是不可随意修改的
//void(CBase:: * cfnSetText)(const char* strText) = &CBase::SetText2;
//pDerive2->pVtbl->pfnSetText = (set_text_ptr)*(void**)&cfnSetText;
//但是我们可以换个思路,把整个虚表替换掉
TDeriveVtbl Vtbl = { 0 };
Temp.pNot = &Vtbl;
//void(CDerive:: * cfnSetText)(const char* strText) = &CBase::SetText; //这个替换写法有问题
//下面两句能够正确替换虚函数,虽然代码写的不规范
//TDerive* pDerive5 = (TDerive*)new CBase();
//Vtbl.pfnSetText = pDerive5->pVtbl->pfnSetText;
void(CDerive:: * cfnSetText)(const char* strText) = &CBase::SetText2; //替换成普通成员函数
Vtbl.pfnSetText = (set_text_ptr) * (void**)&cfnSetText;
Vtbl.pfnSetTitle = UpdateText;
//由于虚表是我们自己构造的,所以pTemp对象的虚函数,跑起来十分有“个性”
Vtbl.pfnSetText((TDerive*)pTemp, "set_text_ptr");
pTemp->ShowText();
pTemp->SetText("SetText");
pTemp->ShowText();
pTemp->SetTitle("SetTitle");
pTemp->ShowText();
delete pDerive;
delete pDerive4;
getchar();
return 0;
}
上面不仅展示了单继承的内存布局,还展示了相同的对象,总是使用使用同一个虚表。可是这份代码有一个尚未搞清楚的问题。就是那段被屏蔽的代码:
//void(CDerive:: * cfnSetText)(const char* strText) = &CBase::SetText; //这个替换写法有问题
//下面两句能够正确替换虚函数,虽然代码写的不规范
//TDerive* pDerive5 = (TDerive*)new CBase();
//Vtbl.pfnSetText = pDerive5->pVtbl->pfnSetText;
void(CDerive:: * cfnSetText)(const char* strText) = &CBase::SetText2; //替换成普通成员函数
Vtbl.pfnSetText = (set_text_ptr) * (void**)&cfnSetText;
大家可以把CBase::SetText这句开出来,把 CBase::SetText2 这句代码注释掉,会发生很奇怪的事情,反正只要虚表成员指向的是虚函数,就会有问题。准确的原因暂时不清楚,知道答案的同学可以留言解答一下原因。
由于篇幅原因,多继承和虚基类的内存布局放到其他章节中。