菱形继承对象模型
菱形继承对于C++的继承这里来讲算是说一个漏洞吧,这里会牵扯出很多的问题,而我们为什么要研究它呢?
我们就是要看编译器如何巧妙地解决掉这些问题,以及加深我们继承和多态概念的理解。
现在我们从零开始,什么是菱形继承呢?
现在开始我们来看下面代码:
class Person
{
public:
string _name; // 姓名
};
class Student : public Person
{
protected:
int _num; //学号
};
class Teacher : public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 显示指定访问哪个父类的成员
Assistant a;
a.Student::_name = "听风";
a.Teacher::_name = "听檒";
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
这个编译可以通过的所以讲,所以编译器认可一个人拥有两个名字,这个就是菱形继承 的诟病。
也就是我们即将要说的
二义性
和
数据冗余
。
遇到问题解决问题,那么C++如何解决这个问题的呢?
这也就引出虚继承的概念:
何为虚继承,虚继承--解决菱形继承的二义性和数据冗余的问题。
1.虚继承解决了在菱形继承体系里面子类对象包含多份父类对象的数据冗余&浪费空间的问题。
2.虚继承体系看起来好复杂,在实际应用我们通常不会定义如此复杂的继承体系。一般不到万
不得已都不要定义菱形结构的虚继承体系结构,因为使用虚继承解决数据冗余问题也带来了性能
上的损耗。
对于虚继承如何定义呢?
class Person
{
public:
string _name; // 姓名
};
class Student : virtual public Person
{
protected:
int _num; //学号
};
class Teacher : virtual public Person
{
protected:
int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
string _majorCourse; // 主修课程
};
void Test()
{
// 显示指定访问哪个父类的成员
Assistant a;
a.Student::_name = "听风";
a.Teacher::_name = "听檒";
cout << a.Student::_name << endl;
cout << a.Teacher::_name << endl;
}
int main()
{
Test();
system("pause");
return 0;
}
其实我们只需要对父类加上一个virtual关键字,就可以完美的解决上面的问题,搞定虚继承~
下面就是虚拟继承的对象模型图:
现在我们还没有说虚函数,所以也没有虚函数表这个概念,但是我们知道了虚函数表以后,我们应该
知道这里还会有一种情况,他不但有了成员变量的二义性,如果他也有了成员函数的二义性呢?
先回顾一下虚函数表是什么东西?
何为虚函数表,我们写一个程序,调一个监视窗口就知道了。
下面是一个有虚函数的类:
#include<iostream>
#include<windows.h>
using namespacestd;
class Base
{
public:
virtual void func1()
{}
virtual void func2()
{}
private:
inta;
};
void Test1()
{
Base b1;
}
int main()
{
Test1();
system("pause");
return0;
}
我们现在点开b1的监视窗口
这里面有一个_vfptr,而这个_vfptr指向的东西就是我们的主角,虚函数表。
一会大家就知道了,无论是单继承还是多继承甚至于我们的菱形继承虚函数表
都会有不同的形态,虚函数表是一个很有趣的东西。
现在我们已经知道了必备的知识点,我们开始探究菱形继承最复杂的地方,那就是当一个菱形继承中有常量成员和虚函
数成员有的
二义性时,C++是
如何解决这些东西的呢?
class A
{
public:
virtual void f1()
{}
int _a;
};
class B : virtual public A
{
public:
virtual void f1()
{
cout << "B::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}
int _b;
};
class C : virtual public A
{
public:
virtual void f1()
{
cout << "C::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}
int _c;
};
class D : public B, public C
{
public:
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f2()
{
cout << "D::f2()" << endl;
}
int _d;
};
int main()
{
D dd;
dd.B::_a = 1;
dd.C::_a = 2;
dd._b = 3;
dd._c = 4;
dd._d = 5;
system("pause");
return 0;
}
我们先对类的内部的情况解释一下,首先B和C都重写了A类的fun1(),而他们各自都有一个独立的fun2(),然后D
菱形继承B和C类,D重写了fun1()和fun2(),好就是这样,剩下的让下面的2个图告诉你吧。
我们现在调用一下它的内存窗口看一看,我对这里进行一点标记。
这里的首地址就是&dd,我们可以看到从头到尾他们的内存分布,注意那两条红线,
并不是直接指向哪里,他们是通过他们存储的地址哪里找到地址偏移量而访问的,
这里这是为了直观一点。
我们现在仔仔细细的画一个对象模型,让我们知道这3个虚函数表里面装的是什么?
对于菱形继承来说,这些了解我们已经足够了,我们只是学学编译器如何解决这些问题,毕竟编译器都是计算机的
高手写出来的,学习这些思想,万一我们
以后遇到了类似的问题,那我们就能多一丝从容。