539-C++的虚基类和虚继承

多重继承
好处是代码的复用
相当于一个派生类有多个基类
在这里插入图片描述
既从A继承又从B继承,C是从A,B多继承而来,C有2个基类:A和B,C可以把A和B的成员都继承而来,复用起来

虚基类

被虚继承的类称作虚基类
vbptr和vbtable

在这里插入图片描述
virtual
1.修饰成员方法是虚函数
2.可以修饰继承方式,是虚继承。被虚继承的类,称作虚基类

在这里插入图片描述

A被B虚继承,所以A是虚基类
我们打开命令提示符来看看
在这里插入图片描述
A被虚继承,A本身内存布局没有变化
我们看看B的内存布局:

在这里插入图片描述
原本应该最上面放ma,然后放mb,但是现在多了一个vbptr,那么A被B虚继承的ma跑哪里去了?跑到派生类最后面来了。vbptr指向的是vbtable在这里插入图片描述
vbtable第一行是0,第二行是这个虚基类指针vbptr到虚基类数据的偏移量,偏移量是按字节算的,从vbptr偏移4个字节到mb,然后偏移4个字节到ma,所以vbtable的第二行存放的是8
在这里插入图片描述
当我们在分析时,看到是虚继承,考虑派生类的内存空间。
我们可以先不考虑虚继承,
在这里插入图片描述
当我们这个基类被虚继承以后,基类就成为虚基类了,虚基类的数据一定要搬到我们派生类内存的最后面,然后在刚才的地方添加:vbptr,在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
vbptr指向是vbtable,一个类对应一个虚基类表
vbtable有2行,第一行存的是向上的偏移量,因为vbptr在派生类对象内存的起始部分,所以向上的偏移量是0
第二行存的是向下的偏移量,原来的基类数据在最上面放着,现在搬到了最下面。ma被挪地方了,vbtable通过虚函数指针告诉,ma移到了哪个位置,偏移了多少字节,存放的就是vbptr离虚基类数据的偏移量。
在这里插入图片描述

在这里插入图片描述
这个vbtable也是编译时生成,运行时期被放在数据段
一个类型定义的多个对象的vbptr指向的是同一个类型的vbtable
在这里插入图片描述

我们看下面一道题
在这里插入图片描述
在这里插入图片描述

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
C++继承和多态,有2个指针和2个表
在这里插入图片描述

应用拓展

当我们的虚基类指针和虚函数指针在一块的时候,并不影响多态的使用
在这里插入图片描述
在这里插入图片描述
在这里插入图片描述
最终调用到了B的func,但是系统提示堆操作有问题。
为什么会出现这样的问题???
当我们的虚函数指针,虚函数表,虚基类指针, 虚基类表在一起结合的时候,调用起来没问题,但是delete的时候出错了。

我们先分析一下:
B的内存布局:
在这里插入图片描述
vbptr指向一个vbtable,vfptr指向一个vftable
当我们在主函数new了一个B对象
它的内存布局就是上图所示。
在这里插入图片描述
但是当我们把B的内存地址给指针p的时候,是把vbptr给p还是把vfptr给p呢?
在这里插入图片描述
在这里插入图片描述
普通情况下,派生类内存的布局肯定先是基类再是派生类,基类指针指向派生类的时候,基类指针指向的就是派生类的起始地址。
但是,虚继承就不一样了。基类被虚继承以后,基类成为一个虚基类了,虚基类的数据要被搬到派生类的最后面,然后在原来的地方补一个vbptr。
在这里插入图片描述
现在,再用基类指针指向派生类对象的时候,永远指向的是派生类中基类部分的起始地址(vfptr)。
在这里插入图片描述
但是释放内存的时候出错了,因为派生类对象是从vbptr的地方开始开辟的,但是释放内存的时候,p持有的地址是虚基类的起始地址,在这里插入图片描述
delete p就是从虚基类部分的起始地址开始delete的,这就是有问题了!
释放内存的时候出错了。
如果是用基类的指针指向栈上的或者全局的对象,就没有关系,因为比如在栈上出作用域,栈内存自动回收。

class A
{
public:
	virtual void func() { cout << "call A::func" << endl; }
	void operator delete(void *ptr)
	{
		cout << "operator delete p:" << ptr << endl;
		free(ptr);
	}
private:
	int ma;
};
class B : virtual public A
{
public:
	void func() { cout << "call B::func" << endl; }

	void* operator new(size_t size)
	{
		void *p = malloc(size);
		cout << "operator new p:" << p << endl;
		return p;
	}
private:
	int mb;
};

int main()
{
	//基类指针指向派生类对象,永远指向的是派生类基类部分数据的起始地址
	A *p = new B(); //B::vftable
	cout << "main p:" << p << endl;
	p->func();
	delete p;

	return 0;
}

在这里插入图片描述
在这里插入图片描述
对于编译器来说,在delete的时候,应该自己知道应该把这个内存从vbptr开始free。
和编译器产生的指令有关。
我们Windows的VS编译器都是有问题的,比较呆。
对于linux的g++来说,在delete的时候自动偏移到new内存的起始部分开始free。

如果我们不考虑堆,弄成是栈上的对象。
在这里插入图片描述
在这里插入图片描述
栈上的对象,出作用域自动释放。

有一疑问:派生类为什么不这样画?
在这里插入图片描述
vfptr不是派生类的吗?为什么要画到基类里面?
在这里插入图片描述

上图那种情况是派生类自己有虚函数,基类里面没有虚函数,vfptr就是归派生类自己的。
但是如果派生类是从基类继承而来,而且基类有虚函数,那么vfptr相当于就是从基类继承下来的,因为基类里已经有一个虚函数指针了,派生类用1个vfptr就可以了,提供内存的利用率。

在这里插入图片描述
在这里插入图片描述

  • 2
    点赞
  • 4
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

林林林ZEYU

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值