鸡啄米在编程入门系列的上一讲中讲了作用域分辨符。今天主要讲解虚基类及其派生类的构造函数。
1.虚基类的概念及用法
上一讲中说过,如果派生类的全部或者部分基类有共同的基类,那么派生类的这些直接基类从上一级基类继承的成员都具有相同的名称,定义了派生类的对象后,同名数据成员就会在内存中有多份拷贝,同名函数也会有多个映射。访问这些同名成员时,为了唯一标识它们可以使用上一讲中的作用域分辨符,也可以使用虚基类技术。我们将派生类直接基类的共同基类声明为虚基类后,派生类从不同的直接基类继承来的同名数据成员在内存中就会只有一份拷贝,同名函数也会只有一个映射,这样不仅实现了唯一标识同名成员,而且也节省了内存空间,可见虚基类技术是很实用的。
在派生类声明时除继承方式外还使用关键字virtual限定基类的话,此基类就是虚基类。虚基类声明的语法形式为:
class 派生类名:virtual 继承方式 基类名
这里关键字virtual跟继承方式一样,只限定紧跟在它后面的基类。比如,声明了类A为虚基类,类B为A的派生类,类C也是A的派生类,类D是由类B和C共同继承而来,则类B和类C从A继承的同名数据成员在类D的对象中只有一份拷贝,同名函数成员也只有一个函数体。
鸡啄米讲上一讲中的第二个例子做下修改,将Base0声明为虚基类来说明下虚基类的用法:我们先声明一个基类Base0,Base0中有数据成员x和函数成员show,再声明类Base1和Base2,它们都由Base0公有继承而来,与上一讲中不同的是派生时声明Base0为虚基类,最后从Base1和Base2共同派生出类Child。这时Base0的成员经过到Base1和Base2再到Child的两次派生过程,出现在Child类中时,数据成员x在内存中也只有一份拷贝,函数成员show也只有一个映射。
#include <iostream> using namespace std; class Base0 // 基类Base0的声明 { public: int x; void show() { cout<<"x of Base0: "<<x<<endl; } }; class Base1 : virtual public Base0 // Base0为虚基类,公有派生Base1类 { }; class Base2 : virtual public Base0 // Base0为虚基类,公有派生Base2类 { }; class Child : public Base1, public Base2 { }; int main() { Child child; child.x = 5; child.show(); return 0; }
程序运行结果为:
x of Base0: 5
大家可以看到,声明虚基类只需要在它的派生类声明时使用关键字virtual修饰。
我们对作用域分辨符和虚基类技术进行对比分析可知,使用作用域分辨符唯一标识同名成员时,派生类中有同名成员的多个拷贝,可以存放不同的数据,进行不同的操作,而使用虚基类时派生类的同名成员只有一份拷贝,更节省内存。我们在软件开发中可以根据实际情况自己做出选择。
2.虚基类派生类的构造函数
上面例子中各个类都没有定义构造函数,而是使用的默认构造函数。如果虚基类定义了带参数表的非默认构造函数,没有定义默认形式的构造函数,那么情况会有些复杂。因为由虚基类直接或间接继承的所有派生类,都必须在构造函数的成员初始化列表中给出对虚基类成员的初始化。鸡啄米这里再讲上面的例子做进一步修改,为虚基类添加带参数表的构造函数,那么整个程序就要改成以下形式:
#include <iostream> using namespace std; class Base0 // 基类Base0的声明 { public: Base0(int y) { x=y; } int x; void show() { cout<<"x of Base0: "<<x<<endl; } }; class Base1 : virtual public Base0 // Base0为虚基类,公有派生Base1类 { public: Base1(int y):Base0(y) { } }; class Base2 : virtual public Base0 // Base0为虚基类,公有派生Base2类 { public: Base2(int y):Base0(y) { } }; class Child : public Base1, public Base2 { public: Child(int y):Base0(y),Base1(y),Base2(y) { } }; int main() { Child child(3); child.show(); return 0; }
程序运行结果为:
x of Base0: 3
主函数中定义了派生类Child的对象child,在构造对象child时调用了child的构造函数,其初始化列表中不只调用了虚基类Base0的构造函数对从它继承的成员x进行初始化,而且还调用了基类Base1和Base2的构造函数Base1()和Base2(),而Base1()和Base2()的初始化列表中又有对虚基类Base0成员x的初始化。这么说,从虚基类Base0继承来的成员x初始化了三次,其实不然,因为编译器在遇到这种情况时会进行特殊处理:如果构造的对象中有从虚基类继承来的成员,那么虚基类成员的初始化由而且只由最远派生类的构造函数调用虚基类的构造函数来完成。最远派生类就是声明对象时指定的类,上面例子中构造对象child时,类Child就是最远派生类。除了最远派生类,它的其他基类对虚基类构造函数的调用会被忽略。上例中就只会由Child类的构造函数调用虚基类Base0的构造函数完成成员x的初始化,而Child类的基类Base1和Base2对虚基类Base0构造函数的调用会被忽略。
鸡啄米对虚基类及其构造函数的内容就讲到这里了。有任何编程入门问题欢迎到鸡啄米博客留言讨论。