继承是面向对象中实现复用的一个机制,通过继承定义的一个类,可以共享公共的东西,而各自实现本质不同的东西。
在单继承下,这种由继承机制支持的、特殊形式的按值组合提供了最有效、最紧凑的对象表示。在多继承下,当一个基类在派生类中多次出现时就会出现问题。最主要的一个例子是 iostream
类的层次结构模型。如下图:
(图片来源:http://blog.csdn.net/lostspeed/article/details/50431402)
如上图所示:istream
和 ostream
都从抽象 ios
基类派生而来, 而 iostream
类又继承了 istream
和 ostream
两个类,这种情况下,每个 iostream
类的对象都包含两个 ios
类的子对象:在 istream
和 ostream
中子对象的实例,保存两个相同的副本,对内存来说是一种浪费,更严重的地方在于:两个实例所造成的二义性问题。
这是一种典型的菱形继承所造成的二义性问题。如下:
当在 Assistant
中访问成员 _name
时,到底该访问哪份实例。下面这份代码会出现编译时错误:
class Person{
public:
//...
private:
string _name;
};
class Student: public Person{//Student继承了Person类
public:
//...
protected:
int _num;
};
class Teacher : public Person{ //Teacher也继承了Person类
public:
//...
protected:
int _id;
};
class Assistant : public Student, public Teacher{ //Assistan继承了Student和Teacher
public:
//...
void Show()
{
cout << "name" << _name << endl;//编译时报错
cout << "num" << _num << endl;
cout << "id" << _id << endl;
}
private:
//...
};
任何未加作用域限定符的去访问 _name
都会出现编译时错误,要想让上述代码通过,我们必须修改它,像下面这样:
cout << "name" << Student::_name << endl;
这样写法不失为一种好的办法,但仍然无法解决 _name
在内存中有两份实例的缺陷。C++ 有一种更好的解决方案是:提供另一种可替代“按引用组合”的继承机制——虚拟继承。
在虚拟继承下,无论一个公共的基类子对象在派生类层次中出现了多少次,只会有一个共享的基类子对象被继承。共享的基类子对象被称为虚基类,在虚拟继承机制下,基类子对象的多份复制产生的二义性问题被消除了。
通过 virtual
关键字的修饰可以实现虚拟继承,如下:
class Person{
public:
//...
private:
string _name;
};
class Student: virtual public Person{//虚拟继承,Person是Student的虚基类
public:
//...
protected:
int _num;
};
class Teacher : public virtual Person{ //虚拟继承,Person是Teacher的虚基类,public和virtual的顺序无关紧要
public:
//...
protected:
int _id;
};
class Assistant : public Student, public Teacher{ //Assistan继承了Student和Teacher
public:
//...
void Show()
{
cout << "name" << _name << endl;//OK!
cout << "num" << _num << endl;
cout << "id" << _id << endl;
}
private:
//...
};
下面我们用另一个简单的例子来深入分析虚拟继承的机制。
先看下面这段代码,没有使用虚拟继承:
class A{
public:
int _a;
};
class B: public A{
public:
int _b;
};
class C : public A{
public:
int _c;
};
class D: public B, public C{
public:
int _d;
};
如我们所料,在正常的继承层次关系中,对于基类 A
没什么可说的,B
、C
、D
的大小分别为 8、8、20,对于 D
,它在内存中的布局是:最上面是继承的 B
和 C
,其次是自己的成员。
上面是没有用虚拟继承时的继承层次,下面看看在使用虚拟继承时的内存布局。
先看这段代码,和上面不同的是使用了虚拟继承,注意使用虚拟继承的地方,B
和 C
虚拟继承了 A
:
class A{
public:
int _a;
};
class B: virtual public A{
public:
int _b;
};
class C : virtual public A{
public:
int _c;
};
class D: public B, public C{
public:
int _d;
};
我们看到,菱形继承中,使用了虚拟继承后,B
、C
、D
的大小都发生了变化。而且 D
中继承自基类的两个 _a
的地址相同,这说明了什么?说明在派生类 d
里面只有一份 _a
,此时当然不会出现我们上述没有虚拟继承时的公共子对象成员的二义性问题了。
那么虚拟继承实现的底层原理是什么呢?下面我们来讨论一下这个问题。
虚拟继承的虚基类并不是其本身的一个显式特性,而是它与派生类的一个关系,虚拟继承采用“按引用组合”的方式,也就是说,对于子对象的成员是间接访问的。这样内存中只有一份公共子对象,而其他派生类中都只需保存该子对象相对于其本身的偏移量,这样做的有点是——消除了菱形继承中二义性问题(因为公共的成员在内存中只有一份实例),但这样带来的坏处是:每次对于虚拟类成员的访问都是间接的,这在效率上的表现并不是很好。
实际上虚拟继承的底层实现原理与编译器相关,但大多都采用虚基类指针和虚基表实现,其中虚基类指针指向虚基表,而该表中记录的虚基类与本类的偏移量,我们可以在VS的内存窗口中看出这一点
先看一段代码,这段代码用于给 d
的各个成员赋值:
d._a = 10;
d._b = 20;
d._c = 30;
d._d = 40;
在对象 d
的内存布局中, 先是继承自 B
的内容,包括一个指向虚基类偏移量表的指针,该表中存储的是公共的子类成员 _a
与派生类 B
之间的偏移量,还包括 B
自己的成员;接着是继承自 C
的内容,也包括一个虚基类偏移量表指针和自己的成员;完了是 D
的成员;而公共的子类成员则放在最后。
下面通过到虚表中找偏移量,然后通过偏移量再访问,为什么要加 4,请看上图:vbptr所指向的位置的下一个位置才是偏移量:
虚拟继承解决的菱形继承中公共子成员出现的二义性问题,这不失为一种好的办法,但它仍然有很大的缺点:就比如上面的例子,使用虚拟继承后节省了一份公共成员的拷贝,但多了两个虚基类偏移量表的指针,而且也增加了访问数据的成本。
——完!
【作者:果冻 http://blog.csdn.net/jelly_9】