之前讲解了 class基类和单继承的内存布局,本章将一下多继承和虚基类的内存布局是怎样的。
先上多继承的代码:
#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 ShowText() {
printf("%s\n", m_szText);
}
protected:
char m_szText[64];
};
class CName
{
public:
CName() {
printf("Init CName\r\n");
strcpy(m_szName, "null");
}
~CName() {
printf("Uninit CName\r\n");
}
virtual void SetName(const char* strText) {
strcpy(m_szName, strText);
}
virtual void ShowName() {
printf("name=%s\n", m_szName);
}
protected:
char m_szName[64];
};
class CAge
{
public:
CAge() {
printf("Init CAge\r\n");
m_nAge = 0;
}
~CAge() {
printf("Uninit CAge\r\n");
}
virtual void SetAge(int nAge) {
m_nAge = nAge;
}
virtual void ShowAge() {
printf("age=%d\n", m_nAge);
}
protected:
int m_nAge;
};
class CDerive : public CBase, public CName, public CAge
{
public:
CDerive() :CBase(), CName(), CAge() {
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 SetAge(int nAge) {
m_nAge = nAge + 3;
}
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];
};
typedef void(__thiscall* set_text_ptr)(void* _this_ptr, const char* strText);
typedef void(__stdcall* set_title_ptr)(void* _this_ptr, const char* strText);
typedef void(__thiscall* set_age_ptr)(void* _this_ptr, int nAge);
typedef void(__thiscall* show_ptr)(void* _this_ptr);
typedef struct __TBaseVtbl {
set_text_ptr pfnSetText;
}TBaseVtbl;
typedef struct __TNameVtbl {
set_text_ptr pfnSetName;
show_ptr pfnShow;
}TNameVtbl;
typedef struct __TAgeVtbl {
set_age_ptr pfnSetAge;
show_ptr pfnShow;
}TAgeVtbl;
typedef struct __TDeriveVtbl {
set_text_ptr pfnSetText;
set_title_ptr pfnSetTitle;
}TDeriveVtbl;
typedef struct __TBase {
TBaseVtbl* pVtbl;
char szText[64];
}TBase;
typedef struct __TName {
TNameVtbl* pVtbl;
char szName[64];
}TName;
typedef struct __TAge {
TAgeVtbl* pVtbl;
int nAge;
}TAge;
typedef struct __TDerive {
union //CDerive 和 第一个继承的类的虚表位置是重合的
{
TDeriveVtbl* pVtbl;
TBase Base;
};
TName Name; //第二个继承的类
TAge Age; //第三个继承的类
char szTitle[64];//最后才是CDerive的成员变量
}TDerive;
int main()
{
printf("先进行常规的调用看看结果\n\n");
CDerive* pDerive = new CDerive();
pDerive->SetText("hello");
pDerive->ShowText();
pDerive->SetTitle("测试");
pDerive->ShowTitle();
pDerive->SetName("lili");
pDerive->ShowName();
pDerive->SetAge(5);
pDerive->ShowAge();
delete pDerive;
printf("\n\n再测试一下结构体的内存布局是否正确");
CDerive* pTemp = new CDerive();
TDerive* pDerive2 = (TDerive*)pTemp;
pDerive2->pVtbl->pfnSetText(pDerive2, "hello");
pTemp->ShowText();
//与单继承不同,多继承的this指针是需要偏移的,所以不能够直接传pDerive2
//必须从派生类中偏移到对应的class指针进行传递
pDerive2->Base.pVtbl->pfnSetText(&pDerive2->Base, "ShowText");
pTemp->ShowText();
pDerive2->pVtbl->pfnSetTitle(pDerive2, "测试");
pTemp->ShowTitle();
strcpy(pDerive2->szTitle, "测试2");
pTemp->ShowTitle();
pDerive2->Name.pVtbl->pfnSetName(&pDerive2->Name, "lili");
pDerive2->Name.pVtbl->pfnShow(&pDerive2->Name);
pTemp->ShowName();
pDerive2->Age.pVtbl->pfnSetAge(&pDerive2->Age, 3);
pDerive2->Age.pVtbl->pfnShow(&pDerive2->Age);
pTemp->ShowAge();
delete pTemp;
//TDerive* pDerive2 = (TDerive* )pDerive;
getchar();
return 0;
}
有了<<class 关键字内存布局详解(一)>> 的基础,相信上面的代码不用解释大家都能够看懂了。当中比较需要留意的是this指针的偏移,在C++的class对象中,它会自己进行隐式转换,但是struct却不会,所以我们需要手动偏移到正确的 class对象的this指针,然后进行参数传递。其实单继承中也需要注意这个问题的,但单继承的派生类和基类的地址是覆盖和追加的,也就是说 this 指针偏移都是零,所以一般不额外进行偏移也没有关系,但是多继承除了第一个继承对象外,其他对象的this指针都不是零,必须进行额外的this指针转换。
然后我们再看看虚基类的内存布局:
#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, "CBase");
}
~CBase() {
printf("Uninit Base\r\n");
}
virtual void SetText(const char* strText) {
strcpy(m_szText, strText);
}
void ShowText() {
printf("%s\n", m_szText);
}
protected:
char m_szText[64];
};
class CName : virtual public CBase
{
public:
CName() {
printf("Init CName\r\n");
strcpy(m_szName, "CName");
}
~CName() {
printf("Uninit CName\r\n");
}
virtual void SetName(const char* strText) {
strcpy(m_szName, strText);
}
virtual void ShowName() {
printf("name=%s\n", m_szName);
}
protected:
char m_szName[64];
};
class CAge : virtual public CBase
{
public:
CAge() {
printf("Init CAge\r\n");
m_nAge = -1;
}
~CAge() {
printf("Uninit CAge\r\n");
}
virtual void SetAge(int nAge) {
m_nAge = nAge;
}
virtual void ShowAge() {
printf("Age=%d\n", m_nAge);
}
protected:
int m_nAge;
};
class CSex
{
public:
CSex() {
printf("Init CSex\r\n");
strcpy(m_szSex, "CSex");
};
~CSex() {
printf("Uninit CSex\r\n");
}
virtual void SetSex(const char* strText) {
strcpy(m_szSex, strText);
}
virtual void ShowSex() {
printf("sex=%s\n", m_szSex);
}
protected:
char m_szSex[64];
};
class CDerive : public CName, public CAge, public CSex
{
public:
CDerive() {
printf("Init CDerive\r\n");
strcpy(m_szTitle, "Title");
}
~CDerive() {
printf("Uninit CDerive\r\n");
}
virtual void __stdcall SetTitle(const char* strText) {
sprintf(m_szTitle, "%s", strText);
}
void ShowTitle() {
printf("%s\n", m_szTitle);
}
private:
char m_szTitle[64];
};
typedef void(__thiscall* set_text_ptr)(void* _this_ptr, const char* strText);
typedef void(__stdcall* set_title_ptr)(void* _this_ptr, const char* strText);
typedef void(__thiscall* set_age_ptr)(void* _this_ptr, int nAge);
typedef void(__thiscall* show_ptr)(void* _this_ptr);
typedef struct __TBtbl {
int nOffset;
}TBtbl;
typedef struct __TBaseVtbl {
set_text_ptr pfnSetText;
}TBaseVtbl;
typedef struct __TNameVtbl {
set_text_ptr pfnSetName;
show_ptr pfnShow;
}TNameVtbl;
typedef struct __TAgeVtbl {
set_age_ptr pfnSetAge;
show_ptr pfnShow;
}TAgeVtbl;
typedef struct __TSexVtbl {
set_text_ptr pfnSetSex;
show_ptr pfnShow;
}TSexVtbl;
typedef struct __TDeriveVtbl {
set_text_ptr pfnSetName;
show_ptr pfnShowName;
set_title_ptr pfnSetTitle;
}TDeriveVtbl;
typedef struct __TBase {
TBaseVtbl* pVtbl;
char szText[64];
}TBase;
typedef struct __TName {
TNameVtbl* pVtbl;
TBtbl* pBtbl;
char szName[64];
}TName;
typedef struct __TName2 {
TNameVtbl* pVtbl;
TBtbl* pBtbl;
char szName[64];
TBase Base;
}TName2;
typedef struct __TAge {
TAgeVtbl* pVtbl;
TBtbl* pBtbl;
int nAge;
}TAge;
typedef struct __TSex {
TSexVtbl* pVtbl;
char szSex[64];
}TSex;
//class CDerive : public CName, public CAge, public CSex
typedef struct __TDerive {
union //CDerive 和 第一个继承的类的虚表位置是重合的
{
TDeriveVtbl* pVtbl;
TName Name;
};
TAge Age; //第二个继承的类
TSex Sex; //第三个继承的类
char szTitle[64];//CDerive的成员变量
//最后才是虚继承的基类数据
//虽然两个相同类型的union看起来怪怪的,用一个变量的写法也可以
//但我认为这种写法,更好地表达了虚继承的思想
union
{
TBase NameBase;
TBase AgeBase;
};
}TDerive;
int main()
{
printf("先进行常规的调用看看结果\n\n");
CDerive* pDerive = new CDerive();
pDerive->SetText("hello");
pDerive->ShowText();
pDerive->SetTitle("测试");
pDerive->ShowTitle();
pDerive->SetName("lili");
pDerive->ShowName();
pDerive->SetAge(5);
pDerive->ShowAge();
pDerive->SetSex("女");
pDerive->ShowSex();
delete pDerive;
printf("\n\n再测试一下结构体的内存布局是否正确");
CDerive* pTemp = new CDerive();
TDerive* pDerive2 = (TDerive*)pTemp;
int a = sizeof(CBase);
int b = sizeof(CName);
int c = sizeof(CAge);
int d = sizeof(CSex);
int e = sizeof(CDerive);
int a1 = sizeof(TBase);
int b1 = sizeof(TName);
int c1 = sizeof(TAge);
int d1 = sizeof(TSex);
int e1 = sizeof(TDerive);
pDerive2->pVtbl->pfnSetTitle(pDerive2, "测试");
pTemp->ShowTitle();
printf("%s\n", pDerive2->szTitle);
pDerive2->Name.pVtbl->pfnSetName(pDerive2, "lili");
pDerive2->Name.pVtbl->pfnShow(pDerive2);
pTemp->ShowName();
printf("name=%s\n", pDerive2->Name.szName);
pDerive2->Age.pVtbl->pfnSetAge(&pDerive2->Age, 5);
pDerive2->Age.pVtbl->pfnShow(&pDerive2->Age);
pTemp->ShowAge();
printf("age=%d\n", pDerive2->Age.nAge);
pDerive2->Sex.pVtbl->pfnSetSex(&pDerive2->Sex, "女");
pDerive2->Sex.pVtbl->pfnShow(&pDerive2->Sex);
pTemp->ShowSex();
printf("sex=%s\n", pDerive2->Sex.szSex);
//测试虚基类
pDerive2->NameBase.pVtbl->pfnSetText(&pDerive2->NameBase, "pfnSetText");
pTemp->ShowText();
strcpy(pDerive2->AgeBase.szText, "pDerive2->Base2.szText");
pTemp->ShowText();
printf("Name=%p; age=%p; \n", &pDerive2->Name, &pDerive2->Age);
//pBtbl->nOffset 在x86中一般为 -4, 就是当前指针位置往后偏移4位,就是派生类的 this指针
//比如CName的 &pBtbl->nOffset + pBtbl->nOffset = CName
printf("src=%p; base=%p; name=%d; age=%d\n",
pDerive2, &pDerive2->NameBase,
pDerive2->Name.pBtbl->nOffset,
pDerive2->Age.pBtbl->nOffset);
if (pDerive2->Name.pBtbl->nOffset + (char*)&pDerive2->Name.pBtbl == (char*)&pDerive2->Name)
{
printf("找到了派生类的 this 指针\n");
}
delete pTemp;
getchar();
return 0;
}
以上为 VC 中测试到的虚继承内存布局,可以看到虚基类的内容被放到了对象的尾部。要注意的一点是,C++中并没有规定说虚基类一定要放在对象的尾部,而且派生类的虚基类指针,也没必要是偏移量。也就是说,当你在非VC环境,比如 gcc 环境下,这个虚继承测试可能不通过(其他多继承这些是可以的)。由于我这边暂时缺乏 gcc 环境,有兴趣的同学可以自己测试一下。