虚继承
在菱形继承中出现的数据二义性问题,使得数据访问时变得复杂,并且导致了数据冗存。虚继承则解决了从不同途径继承来的同名的数据成员在内存中有不同的拷贝造成数据不一致问题。
关键字:virtual
用法:将共同基类声明设置为虚基类,这时从不同路径继承过来的同名数据成员在内存中只有一份,同一个函数名也只有一个映射。
语法:
class 派生类: virtual 基类1,virtual 基类2,…,virtual 基类n
{
…//派生类成员声明
};
构造函数与析构函数的执行次序:
先执行虚基类的构造函数,多个虚基类按照被继承的次序顺序构造。
执行基类的构造函数,如果有多个基类则按照被声明次序依次构造。
执行成员对象的构造函数,多个成员对象按照声明次序构造。
执行派生类自己的构造函数。
析构函数与构造次序相反。
注意:
只有用于建立对象的最派生类的构造函数调用虚基类的构造函数,而该派生类的所有基类中列出的对虚基类的构造函数的调用在执行中被忽略,从而保证对虚基类子对象只初始化一次。
在一个成员初始化列表中同时出现对虚基类和非虚基类构造函数的调用时,虚基类的构造函数先于非虚基类的构造函数执行。
class A
{
public:
A()
:_a(1)
{}
private:
int _a;
};
class B :virtual public A
{
public:
B()
:A()
,_b1(2)
,_b2(3)
{}
private:
int _b1;
int _b2;
};
void test() {
B date;
}
打开监视窗口可以发现:
对象模型变为:
可以发现虚拟继承比普通继承多了4个字节。
而这4个字节保存的是一个地址,再调用一个内存监视跳转。
这个表格被称为偏移量表格,记载虚基类成员相对于函数内存起始地址的偏移量。(因为虚继承中对象模型改变,虚基类的成员调到最底部,所以需要记录虚基类成员相对于对象)
这时虚继承与普通继承之间的一处区别。
菱形虚拟继承
class A
{
public:
A()
:_a(0)
{}
int _a;
};
class B1 :virtual public A //虚继承
{
public:
B1()
:_b1(1)
{}
int _b1;
};
class B2 : virtual public A //虚继承
{
public:
B2()
:_b2(2)
{}
int _b2;
};
class C :public B1,public B2
{
public:
C()
:_c(3)
{}
int _c;
};
void test()
{
C c1;
c._a;
}
运行后调出监视窗口:
这时数据模型变为:
可以看到在对象C中只有一份 _a 数据,并且在直接访问时候,不再需要声明作用域。
虚函数
C++中,虚函数用来实现多态。
关键字:virtual
如果一个类包含了虚函数,那么在创建对象时会额外增加一张表,表中的每一项都是虚函数的入口地址。这张表就是虚函数表,也称为 vtable。 可以认为虚函数表是一个数组。 为了把对象和虚函数表关联起来,编译器会在对象中安插一个指针,指向虚函数表的起始位置。
虚表:
class A
{
public:
virtual void fun1() {}
virtual void fun2() {}
};
void test() {
A a1;
}
打开监视窗口:
可以发现,虚表指针被放在了对象的首部。
各种情况下的虚表:
class A {
protected:
int a1;
int a2;
public:
virtual void display() {
cout << "A::display()" << endl;
}
virtual void clone() {
cout << "A::clone()" << endl;
}
};
class B : public A {
protected:
int b;
public:
virtual void display() {
cout << "B::display()" << endl;
}
virtual void init() {
cout << "B::init()" << endl;
}
};
class C : public B {
protected:
int c;
public:
virtual void display() {
cout << "C::display()" << endl;
}
virtual void execute() {
cout << "C::execute()" << endl;
}
};
各个类的内存分布如下:
调用:
对于虚函数display(),它在虚表中的索引为0,发生调用时,
p->display()编译器内部发生转换,转换为:( *( p->vptr )[0] ) (p),完成函数调用。
对于不同的虚函数,只需要改变索引值即可。