C++类的多继承问题较为复杂,本文对类的继承问题进行深入探讨。在实际编程过程中应该避免这种难以理解的编程方式。
一个子类可以从父类继承所有的成员变量和方法,但是不能继承父类的构造函数,子类要初始化父类的数据成员必须显示或者隐式的调用父类的构造函数。对于带有参数的构造函数,必须显式调用,不谈只能调用默认构造函数。
类的派生方式有三种:public,protected,private。类的派生不是控制子类对父类的访问权限,而是控制类的使用者对父类的访问权限。如【代码块1】所示的基类Base,其子类Base4从Base类private派生,但在该类里可以访问到基类的公有成员函数。而MyClass2从Base4公有派生,却不能访问print_hello函数,原因是print_hello函数相当于是Base4的private成员,所以MyClass2不能访问。总结来说,类的派生方式控制的是类的使用者的权限,而不是类本身对父类的访问权限。
在C++中,对于class关键字声明的类从父类的默认继承方式是private,类的默认属性也是private的;对于struct关键字声明的类从父类的默认继承方式是public,类的默认属性也是public的。
代码块1
class Base {
/*基类*/
public:
//构造函数
Base(int i) {
cout << i;
}
//公有成员函数
void print_hello() {
cout << "hello" << endl;
}
};
如【代码块2】所示,MyClass类定义了初始化列表,那么初始化列表的执行顺序是如何的呢?在C++中没有规定初始化列表的执行顺序,可能与编译器有关。这里可以讨论下在vs上的测试结果:在初始化列表中从最顶级的基类开始逐级调用构造函数,对于直接父类,按照派生列表的顺序调用构造函数;构造函数调用完成后,再执行类的成员变量的初始化,成员变量的执行顺序按照成员变量的定义顺序执行。按照这个规则,【代码块2】中的初始化列表的执行顺序是:
Base->Base1->Base2->m2->m1
代码块2
class MyClass :Base1, Base2 {
public:
MyClass(int a, int b, int c, int d) :
m2(b),
Base1(c),
Base2(d),
Base(a),
m1(a) {
cout << b;
}
private:
Base2 m2;
Base1 m1;
};
在多继承中可能有如下图所示的菱形继承关系,假设A有一个成员变量int a,那么D类要初始化成员变量是调用B的构造函数还是调用C的构造函数呢?这个构造函数调用由于需要显式传参,所以要显示指明,不会产生二义性。那么D中到底有几份成员变量a呢?答案是两份,通常我们并不需要有两份这样的成员变量,这会导致二义性,我并不知道对a进行操作时是操作的从B继承而来的a还是从C继承而来的a。虽然可以通过“::”显式声明,但是我们真正的需求是我们并不需要有两份a。
如下代码块3所示,Base1和Base2都从Base基类虚继承而来。像第一节中所阐述的那样,类的派生方式控制的是类的使用者的权限,而不是类本身对父类的访问权限。这里使用virtual关键字控制的也是类的使用者的权限,在这里就是控制的MyClass的行为。在MyClass中只会保留Base类中成员变量和成员函数的一份。在初始化列表中,分别调用了Base1和Base2,而Base1和Base2中都调用了Base。如果不是虚继承,那么Base1和Base2中都会执行Base;如果是虚继承,在调用Base1和Base2时只会执行1次Base的构造函数。
代码块3
class Base1:public virtual Base {
public:
Base1(int i, int j = 0) :
Base(j) {
cout << i;
}
};
class Base2 :public virtual Base {
public:
Base2(int i, int j = 0) :Base(j) {
cout << i;
}
};
class MyClass :Base1, Base2 {
public:
MyClass(int a, int b, int c, int d) :
m2(b),
Base1(c),
Base2(d),
Base(a),
m1(a) {
cout << b;
}
private:
Base2 m2;
Base1 m1;
};