电子商务网站计划书网站下载app免费安全
课程总目录
文章目录
- 一、虚基类和虚继承
- 二、菱形继承的问题
一、虚基类和虚继承
虚基类:被虚继承的类,就称为虚基类
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) = 1
class A
{virtual void fun() {}
};
sizeof(A)=1class B : public A {};
sizeof(B) = 4 //B的内存里有vfptr
class 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与ma,B中还有自己的mb
此时A被虚继承,从A中继承来的所有的东西都移动到派生类的最后面,然后在最前面补一个vbptr,vbptr指向vbtable,vfptr指向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的是A8,delete的时候也是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();,有两个基类,两个基类指针都可以指向派生类对象
