1. 什么是虚基类?
在C++继承中,一个派生类可以从多个基类派生,而这些基类可能会有下层类所继承,形成类似于树形结构的关系,如下图所示:
class A {};
class B1: public A {};
class B2: public A {};
class C: public B1, public B2 {};
在这个示例中,类C继承了类B1和类B2,而这两个类都继承了类A。这样,类C就间接继承了类A。但是,如果类B1和类B2都含有像下面这样的成员变量:
class B1: public A {
public:
int i;
};
class B2: public A {
public:
int i;
};
在这种情况下,类C就会存在两个由类A派生而来的int i变量。这样会导致代码中的一些问题。为了解决这个问题,C++引入了虚基类的概念。
虚基类是指在多层继承关系中,派生类的基类在整个继承体系中只有一个实例。回到上面的例子,如果我们将类A定义为虚基类,则类B1和B2中的int i变量只会被保留一份,避免了冗余:
class A {};
class B1: virtual public A {};
class B2: virtual public A {};
class C: public B1, public B2 {};
2. 虚基类的初始化
在使用虚基类的时候,需要在继承列表中指定虚拟基类的初始化顺序,并且虚基类只能在最派生类中初始化,不能在其他派生类中再次初始化。同时,虚基类的初始化必须是在构造函数中进行的,而且只会被初始化一次。如果在非最下层的派生类中使用虚成员函数访问虚基类的成员变量,需要使用虚基类名来进行访问,例如:
class A {
public:
int i;
};
class B1: virtual public A {
public:
void set(int x) { i = x; }
};
class B2: virtual public A {};
class C: public B1, public B2 {
public:
void print() { std::cout << B1::i << " " << B2::i << std::endl; }
};
int main() {
C c;
c.set(1);
c.print(); // 输出1 0
return 0;
}
上述代码中,类C中的print()函数访问了类B1和B2中的i变量。由于虚基类A只有一个实例,因此在继承体系中只剩下了一个i变量,它的值被设为了1。而访问B2中的i变量时,由于它并不是虚基类,因此需要使用B2::i来访问。
3. 错误分析
在使用虚基类的时候,如果没有正确的指定虚基类的初始化顺序,就会出现编译错误。例如,下面的代码就会出现“虚基类必须在同一层次结构中以唯一的方式被初始化”的错误:
class A {};
class B1: virtual public A {};
class B2: virtual public A {};
class C: public B1, public B2 {};
class D: public C, public B2, public A {};
在上述代码中,类D继承了类C、B2和A,而类C中含有虚基类A。在类D的构造函数中需要指定A的初始化顺序。然而,由于B1也是虚基类A的派生类,因此在类D的构造函数中必须先调用B1的构造函数,再调用B2的构造函数,最后调用C和A的构造函数。但是,在类D的构造函数中,由于代码的书写顺序问题,B2和A的构造函数先于B1的构造函数被调用了,因此就会出现错误。
4. 解决方案
要解决这个问题,可以使用初始化列表来显式地指定虚基类的构造函数调用顺序。例如,修正后的代码如下:
class A {};
class B1: virtual public A {};
class B2: virtual public A {};
class C: public B1, public B2 {};
class D: public C, public B2, public A {
public:
D(): B1(), B2(), A(), C() {}
};
在类D的构造函数中,通过调用B1、B2、A和C的构造函数,确保了虚基类A在同一层次结构中只被初始化一次,并且没有顺序问题。
5. 总结
使用虚基类可以避免类派生中的一些问题,如成员变量的冗余。但是,在使用虚基类时需要特别注意初始化顺序的问题,否则就会出现“虚基类必须在同一层次结构中以唯一的方式被初始化”的错误。可以使用初始化列表来解决这个问题。