在县城做团购网站,甘肃新闻最新消息今天,免费logo设计生成器免费试用,wordpress自动清缓存课程总目录 文章目录 一、虚基类和虚继承二、菱形继承的问题 一、虚基类和虚继承
虚基类#xff1a;被虚继承的类#xff0c;就称为虚基类
virtual作用#xff1a;
virtual修饰成员方法是虚函数可以修饰继承方式#xff0c;是虚继承#xff0c;被虚继承的类就称为虚基类…
课程总目录 文章目录 一、虚基类和虚继承二、菱形继承的问题 一、虚基类和虚继承
虚基类被虚继承的类就称为虚基类
virtual作用
virtual修饰成员方法是虚函数可以修饰继承方式是虚继承被虚继承的类就称为虚基类
注意与抽象类有纯虚函数的类区分开来
来看这段代码
class A
{
public:
private:int ma;
};class B : public A
{
public:
private:int mb;
};
//A a; 4个字节
//B b; 8个字节使用指令cl xxx.cpp -d1reportSingleClassLayoutA和cl xxx.cpp -d1reportSingleClassLayoutB看一下 如果采用虚继承
class B : virtual public A再来看一下B从8字节变为了12字节了 分析当我们遇到虚继承时候要考虑派生类B的内存布局时首先我们先不考虑虚继承。类B继承了基类A的ma还有自己的mb当我们基类A被虚继承后基类A变为虚基类虚基类的数据一定要被挪到派生类数据的最后面再在最前面添加一个vbptr 来看一些例题
class A {};
sizeof(A)1 //空类大小是1class B : public A {};
sizeof(B) 1class A
{virtual void fun() {}
};
sizeof(A)1class B : public A {};
sizeof(B) 4 //B的内存里有vfptrclass A
{virtual void fun() {}
};
sizeof(A)1class B : virtual public A {};
sizeof(B) 8 //B的内存里有vfptr和vbptr总结
vfptr一个类有虚函数这个类生成的对象就有vfptr指向vftablevbptr派生类中虚继承基类会有vbptrvftable存放RTTI指针指向运行时RTTI信息、虚函数地址。vbtable第一行为向上偏移量第二行为vbptr离虚基类数据在派生类内存中的偏移量。
接下来再来看当虚基类指针与虚函数指针在一起出现的时候会发生什么呢
class A
{
public:virtual void func() { cout call A::func endl; }
private:int ma;
};class B : virtual public A
{
public:void func() { cout call B::func() endl; }
private:int mb;
};int main()
{// 基类指针指向派生类对象永远指向的是派生类中基类部分数据的起始地址A* p new B();p-func();delete p;return 0;
}可以看到调用是没有被影响到的但是delete会出错
分析 B的内存布局B首先从A中获取vfptr与maB中还有自己的mb
此时A被虚继承从A中继承来的所有的东西都移动到派生类的最后面然后在最前面补一个vbptrvbptr指向vbtablevfptr指向vftable
基类指针指向派生类对象永远指向的是派生类基类部分数据的起始地址
普通情况下派生类内存布局先是基类数据再是派生类自己的数据基类指针指向派生类对象时基类指针指向的就是派生类内存的起始部分。
但是在虚继承下基类为虚基类虚基类的数据被挪到派生类最后面最前面补上vbptr此时再用基类指针指向派生类对象时候基类指针还是指向派生类基类部分数据的起始地址也即指向vfptr这也是能正常调用p-func();的原因
那么在释放内存的时候呢现在p指向的是vfptr从vfptr开始释放内存而对象内存现在是从vbptr开始这就出错了
验证一下
class A
{
public:virtual void func() { cout call A::func endl; }void operator delete(void* p){cout operator delete p: p endl;free(p);}
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();cout main p: p endl;p-func();delete p;return 0;
}operator new p:00D316A0
main p:00D316A8
call B::func()
operator delete p:00D316A8可以看到从A0开始new的返回给p的是A8delete的时候也是A8也就是从vfptr开始释放的这是不对的
但是这段代码也能说是错的这和编译器有关在Windows的vs中是从vfptr开始释放的但是在linux的g下会自动偏移到new出来的内存的起始部分来进行释放
如果在栈上开辟内存基类指针指向派生类对象出了作用域自己进行析构不涉及内存的释放这样是没有问题的正常运行不会报错
B b;
A *p b;
cout main p: p endl;
p-func();运行结果
main p:010FFE04
call B::func()使用命令cl xxx.cpp -d1reportSingleClassLayoutB查看一下
再来看这时有人会问了派生类为啥不像下面这样画呢 如果是这样画也就是vfptr属于B的作用域这是不对的因为A中有虚函数vfptr是从A中继承而来的
如果真的这样画的话那就是基类中没有虚函数从派生类中才有的虚函数
二、菱形继承的问题
多重继承可以复用多个基类的代码到派生类中
但是多重继承中也会出现问题菱形继承、半圆形继承等 这些都会导致派生类有多份间接基类的数据此时可以采用虚继承来解决
菱形继承代码
class A
{
public:A(int data) : ma(data) { cout A() endl; }~A() { cout ~A() endl; }
protected:int ma;
};
//
class B : public A
{
public:B(int data) : A(data), mb(data) { cout B() endl; }~B() { cout ~B() endl; }
protected:int mb;
};class C : public A
{
public:C(int data) : A(data), mc(data) { cout C() endl; }~C() { cout ~C() endl; }
protected:int mc;
};
//
class D : public B, public C
{
public:D(int data) : B(data), C(data), md(data) { cout D() endl; }~D() { cout ~D() endl; }
protected:int md;
};int main()
{D d(10);return 0;
}运行结果
A()
B()
A()
C()
D()
~D()
~C()
~A()
~B()
~A()来看一下D的内存布局
用指令cl xxx.cpp -d1reportSingleClassLayoutD看看 可以看到调用了两次A的构造同时数据重复了
怎么解决呢虚继承
class A { ... };
//
class B : virtual public A { ... };
class C : virtual public A { ... };
//
class D : public B, public C { ... };此时内存布局变了解决了多份数据的问题
用指令cl xxx.cpp -d1reportSingleClassLayoutD看看 但是注意此时编译会报错因为现在A::ma靠在了D的作用域上面我们要在D里面给A初始化
class D : public B, public C
{
public:D(int data) : A(data), B(data), C(data), md(data) { cout D() endl; }~D() { cout ~D() endl; }
protected:int md;
};再运行看一看结果
A()
B()
C()
D()
~D()
~C()
~B()
~A()多重继承的好处可以做更多代码的复用比如上面的例子D继承自B和C那么就可以B* p new D();或C* p new D();有两个基类两个基类指针都可以指向派生类对象