探索C++对象模型

1.多态的实现-虚表

2.多继承的对象模型

3.菱形继承&菱形虚拟继承的对象模型

————————————————————————————————————————————————————

1.多态:

多态(Polymorphism)按字面的意思就是“多种状态”。在面向对象语言中,接口的多种不同的实现方式即为多态。允许将子类类型的指针赋值给父类类型的指针。根据对象不同,找到相应对象的实现方式。多态性在Object Pascal和C++中都是通过虚函数实现的。

多态的实现原理以及多态的理解
多态的实现效果 
- 多态:同样的调用语句有多种不同的表现形态;

多态实现的三个条件 
- 有继承、有virtual重写、有父类指针(引用)指向子类对象。缺一不可

多态的C++实现 
- virtual关键字,告诉编译器这个函数要支持多态;不要根据指针类型判断如何调用;而是要根据指针所指向的实际对象类型来判断如何调用

多态的重要意义 
- 设计模式的基础。

实现多态的理论基础 
- 函数指针做函数参数,动态联编PK静态联编。根据实际的对象类型来判断重写函数的调用。

C++中多态的实现原理 
当类中声明虚函数时,编译器会在类中生成一个虚函数表虚函数表是一个存储类成员函数指针的数据结构虚函数表是由编译器自动生成与维护的virtual成员函数会被编译器放入虚函数表中存在虚函数时,每个对象中都有一个指向虚函数表的指针(vptr指针)

例:

#include <iostream>
using namespace std;

class Base
{
public:
	virtual void fun1()
	{
		cout<<"Base::fun1 is called"<<endl;
	}
};
class A : public Base
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1 is called"<<endl;
	}
};
int main()
{
	Base b;
	A a;
	Base* p = &b;
	p->fun1();
	p = &a;
	p->fun1();
	return 0;
}

当基类指针p指向基类对象时,调用基类的fun1,指向子类时,调用子类的fun1,这就是简单的多态。

当构成多态的3个条件不成立的话,则构成不了多态,则构成静态联编,多态是动态联编,C++中的多态性具体体现在运行和编译两个方面。运行时多态是动态多态,其具体引用的对象在运行时才能确定。编译时多态是静态多态,在编译时就可以确定对象使用的形式。

我们可以看一下指针p调用fun1时的反汇编来理解静态多态和动态多态

当不构成多态时(静态)


可以看到这里在编译时就确定了 调用的是Base::fun1();

构成多态时(动态)


这里p运行时是直接去调指向对象的虚表。

当指针p指向不同的对象时,就调用对应的虚表,虚表内的存的时函数指针

这是虚表就相当于是一个函数指针数组


———————————————————————————————————————————————————

2.多继承的对象模型

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base1::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base1::fun2 is called"<<endl;
	}
protected:
	int _b1;
};
class Base2
{
public:
	virtual void fun1()
	{
		cout<<"Base2::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base2::fun2 is called"<<endl;
	}
protected:
	int _b2;
};
class A : public Base1,public Base2
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1 is called"<<endl;
	}
	virtual void fun3()
	{
		cout<<"A::fun2 is called"<<endl;
	}
protected:
	int _a;
};
typedef void(*VTFUNC)();	//定义一个返回值为void 无参数的函数指针类型VTFUNC
void VTable(void* vt)
{
	cout<<"虚表地址"<<vt<<endl;
	VTFUNC* arr = (VTFUNC*)vt;	//将此时虚表转化为一个虚函数指针数组arr
	for(int i=0; arr[i]!=0; ++i)//虚表内虚函数地址以0作为结束标志
	{
		printf("第%d个虚函数地址:0x%x\n",i,arr[i]);
		arr[i]();
	}
	cout<<"***************"<<endl;
}
int main()
{
	Base1 b1;
	Base2 b2;
	A a;
	Base1* p = &b1;
	VTable(*((int**)&a));
	VTable(*((int**)((char*)&a+sizeof(Base1))));//找到对象a中Base2的虚表地址
	return 0;
}
A类对象a多继承了Base1,Base2,看一下监视窗口 

监视窗口中并没有显示a的虚函数fun3,我们可以通过调用a中继承的两个虚表来看一下a中的fun3放在哪里了


可以看到fun3被放在了第一个继承的Base1的虚表中。

所以子类A的对象模型可以用下图表示



———————————————————————————————————————————————————

菱形继承和虚拟继承详细可参考此链接内容点击打开链接

3.菱形继承&菱形虚拟继承的对象模型

#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base1::fun1 is called"<<endl;
	}
public:
	int _b1;
};
class Base2:public Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base2::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base2::fun2 is called"<<endl;
	}
protected:
	int _b2;
};
class Base3:public Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base3::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base3::fun2 is called"<<endl;
	}
protected:
	int _b3;
};
class A : public Base2,public Base3
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1 is called"<<endl;
	}
	virtual void fun3()
	{
		cout<<"A::fun3 is called"<<endl;
	}
protected:
	int _a;
};
typedef void(*VTFUNC)();	//定义一个返回值为void 无参数的函数指针类型VTFUNC
void VTable(void* vt)
{
	cout<<"虚表地址"<<vt<<endl;
	VTFUNC* arr = (VTFUNC*)vt;	//将此时虚表转化为一个虚函数指针数组arr
	for(int i=0; arr[i]!=0; ++i)//虚表内虚函数地址以0作为结束标志
	{
		printf("第%d个虚函数地址:0x%x\n",i,arr[i]);
		arr[i]();
	}
	cout<<"***************"<<endl;
}
int main()
{
	Base1 b1;
	Base2 b2;
	A a;
	Base2* p = &a;
	p->fun2();
	//a._b1 = 2;	访问不明确,不知道访问的是Base2还是Base3中的_b1;
	VTable(*((int**)&a));
	VTable(*((int**)((char*)&a+sizeof(Base2))));//找到对象a中Base2的虚表地址
	return 0;
}

用监视看一下大致的对象模型


虚函数对应的关系为


将没有虚继承的菱形继承对象模型简化可得


菱形虚拟继承的对象模型


#include <iostream>
using namespace std;

class Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base1::fun1 is called"<<endl;
	}
public:
	int _b1;
};
class Base2:virtual public Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base2::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base2::fun2 is called"<<endl;
	}

	int _b2;
};
class Base3:virtual public Base1
{
public:
	virtual void fun1()
	{
		cout<<"Base3::fun1 is called"<<endl;
	}
	virtual void fun2()
	{
		cout<<"Base3::fun2 is called"<<endl;
	}
	int _b3;
};
class A : public Base2,public Base3
{
public:
	virtual void fun1()
	{
		cout<<"A::fun1 is called"<<endl;
	}
	virtual void fun3()
	{
		cout<<"A::fun3 is called"<<endl;
	}
	int _a;
};
typedef void(*VTFUNC)();	//定义一个返回值为void 无参数的函数指针类型VTFUNC
void VTable(void* vt)
{
	cout<<"虚表地址"<<vt<<endl;
	VTFUNC* arr = (VTFUNC*)vt;	//将此时虚表转化为一个虚函数指针数组arr
	for(int i=0; arr[i]!=0; ++i)//虚表内虚函数地址以0作为结束标志
	{
		printf("第%d个虚函数地址:0x%x\n",i,arr[i]);
		arr[i]();
	}
	cout<<"***************"<<endl;
}
int main()
{
	A a;
	a._b2 = 2;
	a._b3 = 3;
	a._b1 = 1;
	a._a = 4;
	cout<<sizeof(Base2)<<endl;
	Base2* p = &a;
	p->fun2();
	//a._b1 = 2;	访问不明确,不知道访问的是Base2还是Base3中的_b1;
	VTable(*((int**)&a));//找到Base2虚表地址
	VTable(*((int**)((char*)&a+sizeof(Base2)-8)));//找到对象a中Base3的虚表地址
	VTable(*((int**)((char*)&a+sizeof(Base2)-8+sizeof(Base3)-4)));//找到共同继承Base1虚表地址
	return 0;
}



可以先可以单独看一下Base2的对象虚继承Base1后的模型


再看监视对应的内存地址


所以,可以简化Base2虚继承Base1后的对象模型

虚基表的作用:如图


小结:虚继承会将基类的虚表和对象放在子类对象内存的最后面,如果要找继承基类的成员和函数,则通过虚基表中的偏移量可以依次找到基类的虚函数和成员变量。


接下来看菱形继承A的对象模型

内存


再看监视,大概的对象模型


我们发现A对象a中只继承了一次Base1的虚表,红色线可以证明

并且在内存中,Base1的虚表和成员_b1都放在了最后面,Base2和Base3的虚表和虚基表按照继承顺序排列

并且,子类对象a中的自己的虚函数(没有重写父类的),将会放在第一个继承的基类的虚表中。


注:因为编译器问题,需要重新生成解决方案,可能地址会有所改变,但是本质不变




所以,可以得到菱形继承的对象模型为


对象模型的问题大概解决了,但是还有一些小问题。

1.我写的代码中Base2,Base3,同时都有fun2(),那么子类A的对象调用fun2()该调用谁?

2.如果A没有重写fun1(),而Base2,Base3都重写了Base1中的fun1(),那么A的对象中的Base1()虚表中存的是谁重写的 fun1()?

3.如果A没有重写fun1(),而Base2重写了fun1(),Base3没有重写了Base1中的fun1(),那么A的对象中的Base1()虚表中 存的是谁重写的fun1()?


注意:1.如果A没有重写fun2(),则没有构成多态,如果要调用基类中的fun2(),需要加上基类的作用域,

如a.Base2::fun2();  

2.会提示“A”:“void Base1::fun1(void)”的不明确继承,编译器不知道该继承Base1虚表中Base2fun1()的重写还是Base3::fun1()的重写。有二义性,所以呀尽量避免这种写法

3.A的对象会继承Base2中重写了fun1()的虚表。。测试过了的。。可能是编译器自己选择的问题。


小结:内存模型是我们理解多态,继承的重要手段,当把内存模型写出来的时候,你就对其了解更上一层楼了。


  • 3
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值