虚函数、虚继承——C++

C++中多态的实现机制——虚表

关于虚表,我们就要用到一个关键字:virtual,可以修饰函数,也可以修饰类。类的成员函数被virtual修饰之后,就成为了虚函数;修饰类,主要是虚继承。
在此之前,我们首先要了解一个概念:对象模型,也就是说,一个基类形成之后,里面的成员是怎么存放的,当派生类继承基类之后,派生类的成员是怎么存放的。
我们首先看下面一段代码,普通的public继承:
class Base{
public:
	int _b;
};
class Derive:public Base
{
public:
	int _d;
};

void test()
{
	Derive d;
	d._b = 1;
	d._d = 2;
}
当我们调出内存,查看时对象d,并依次给_b,_d赋值,
就可以得到派生类对象d的对象模型:


虚继承:
继承中有public,private,protected等继承方式,虚继承就是用关键字virtual 继承的方式
class Base{
private:
	int _b;
public:
	void funtest1()
	{
		cout<<"Base::funtest1()"<<endl;
	}
};
class Derive :virtual Base
{
private:
	int _d;
public:
	void funtest2()
	{
		cout<<"Derive::funtest2()"<<endl;
	}
};
但是我们在使用:cout<<"sizeof(Derive)"<<endl;测试Derive的大小的时候,发现Derive类的大小是12,但是基类中的成员变量和派生类的成员变量总共8字节,那么多出来的4个字节是什么?
我们在内存中,查看派生类的对象:


发现在派生类的对象的前面4个字节已经存放了一些数据,这个数据就是偏移量表格的地址,我们用这个地址在打开一块内存窗口,里面的数据如下:

其中,0x00855800存放的是相对于自己的偏移量,也就是0,而0x00855804里面存放的是相对于基类的偏移量,此时,我们也给出虚继承的对象模型:

Tip:在虚继承的前提下,创建派生类对象的时候,编译器就会在前4个字节写入偏移量表格的地址,所以:在没有构造函数的情况的下,编译器一定给派生类合成一个构造函数。
虚继承能够解决菱形继承产生的二义性问题,这个等下面虚函数讲解完成之后,在做介绍。
虚函数:
我们先来看下面的代码:
class Base{
private:
	int _b;
public:
	void fun()
	{
		cout<<"Base::fun()"<<endl;
	}
};

void testclassvirtual()
{
	cout<<sizeof(Base)<<endl;
}
此时输出为:4,也就是说,类的大小就是成员变量_b的大小,
但是当我们在类成员函数的前面加上:virtual时
class Base{
private:
	int _b;
public:
	virtual void fun()
	{
		cout<<"Base::fun()"<<endl;
	}
};

void testclassvirtual()
{
	cout<<sizeof(Base)<<endl;
}
此时输出为:8,那么此时多出来的4个字节是什么呢?是一个指向虚函数表的指针,虚函数表里面存放着什么呢?存放的就是类里面虚函数的地址,有了虚表的指针,我们就可以得到虚表,得到了虚表,我们就得到了里面存放着的虚函数。
如上图:创建对象b之后,对象的地址就是0x00cff788,里面的前4四个字节已经存放内容,我们在第二个内存窗口打开,发现地址里面存放的是也是地址,此时我们在监视窗口查看Base类里fun函数的地址,和内存2里面的地址相同。
总结:在类中如果有虚函数,那么在对象创建的时候,就会在对象的前4个字节存放虚函数表的地址,而虚函数表里面按照在类中虚函数的声明顺序,存放每一个虚函数的地址。 



那么在继承的关系中,虚函数又有着怎么样的储存呢?
我们先看一段单继承的代码
class B
{
public:
	virtual void fun1()
	{
		cout<<"B::fun1"<<endl;
	}
	virtual void fun2()
	{
		cout<<"B::fun2"<<endl;
	}
	virtual void fun3()
	{
		cout<<"B:fun3"<<endl;
	}
};
class D:public B
{
public:
	virtual void  fun1()
	{
		cout<<"D::fun1()"<<endl;
	}
	virtual void fun4()
	{
		cout<<"D::fun4()"<<endl;
	}
	virtual void fun5()
	{
		cout<<"D::fun5"<<endl;
	}
};


void test()
{
	B b;
	D d;
}
我们使用上面的代码,用同样的方式,查看内存中虚表所存放的函数地址是否一致,得到如下:

我们在创建基类对象和派生类对象之后,使用基类的对象,查看其虚表中存放的地址,再在监视窗口,查看对应函数的地址,就可以知道虚表里面存放的对应的函数是哪一个。
也就很容易得到函数的对象模型:

派生类的虚表生成:
①现将基类的虚函数拷贝一份
②如果派生类中对基类中的虚函数进行了重写,那么派生类的虚函数将替代相同偏移量位置的基类的虚函数
③如果派生类中新增加自己的新函数,那么就按照在派生类中声明的次序,依次存放到虚表中。


但是在多继承中,虚表就又有一些小的变化,我们先给出多继承的实现:
class B1{
	virtual void  funtest1()
	{
		cout<<"B1::funtest1()"<<endl;
	}
	virtual void funtest2()
	{
		cout<<"B1::funtest2()"<<endl;
	}
	virtual void funtest3()
	{
		cout<<"B1::funtest3()"<<endl;
	}
};
class B2{
	virtual void  funtest4()
	{
		cout<<"B2::funtest4()"<<endl;
	}
	virtual void funtest5()
	{
		cout<<"B2::funtest5()"<<endl;
	}
	virtual void funtest6()
	{
		cout<<"B2::funtest6()"<<endl;
	}
};

class  C :public B1,public B2
{
public:
	virtual void funtest2()
	{
		cout<<"C::funtest2()"<<endl;
	}
	virtual void funtest5()
	{
		cout<<"C::funtest5()"<<endl;
	}
	virtual void funtest7()
	{
		cout<<"C::funtest7()"<<endl;
	}
};

void test()
{
	B1 b1;
	B2 b2;
	C c;
}
我们使用之前的通过内存窗口和监视窗口来查看对象c的模型:
对象c里面存放着两个地址,打开第一个地址里面就是内存2的,对比监视窗口,得到其模型:
b1::funtest1()
c::funtest2()
b1::funtest3()
c::funtest7()

我们在打开对象c中第二个地址,得到:
其对象模型:
b2::funtest4()
c::funtest5()
b2::funtest6()

总结起来对象c的对象模型如下:


此时派生类的虚表的产生和单继承的时候产生规则一样,派生类自己新产生的虚函数,存放在按照继承顺序中的第一个。



Tip:限于编者水平,文章有很多不足的地方,欢迎改进
如需转载,请注明出处~!


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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值