【C++面向对象程序设计】CH5 继承与派生(续)——虚基类

2024-02-17 22:20

本文主要是介绍【C++面向对象程序设计】CH5 继承与派生(续)——虚基类,希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

目录

前言

一、虚基类的作用

二、虚基类的初始化

三、例【5.9】在【例5.8】中在teacher类和student类之上增加一个共同的基类person,人员的一些基本数据放在person中

四、多层多重继承用虚基类

五、虚基类的构造函数

六、多重继承如何工作

七、虚拟继承

八、虚拟继承沙发床

九、多继承的构造顺序 

十、多重继承的构造函数举例

十一、多重继承综合举例

1.方法一

(1)代码一 

(2)结果一 

2.方法二

(1)代码二 

(2)结果二 

3.方法三

(1)代码三

(2)结果三 


前言

        从上面的例子可知,如果一个派生类有多个直接基类,而这些直接基类有有一个共同的基类,在派生类中会保留这个间接共同基类数据成员的多个同名成员。图5.19和图5.20描述了这种情况。在引用这些同名成员时,为避免二义性,必须在派生类对象名后增加直接基类名。如:

c1.A::a=3; c1.A::display();

一、虚基类的作用

        如果不希望在派生类中保留间接共同基类的多个同名成员,C++提供了虚基类的方法,使派生类在继承间接共同基类时只保留一份成员。

        虚基类:用于有共同基类的场合。

        声明:以virtual修饰说明基类。(class B1:virtual public B)

        作用:主要用来解决多层继承时可能发生的对同一基类继承多次而产生的二义性问题;为最底层的派生类提供唯一的基类成员,而不重复产生多次拷贝。

        注意:在第一级继承时就要将共同基类设计为虚基类。

        声明虚基类的格式:

class  派生类名:virtual 继承方式  基类名

        当基类通过多条派生路径被一个派生类继承时,派生类只继承该基类一次。

        现将A类声明为虚基类:

class  A
{… … };
class  B: virtual public A
{… … };
class  C: virtual public A
{… … };

        虚基类是在声明派生类时,指定继承方式时声明的。因为一个基类可以作为一个派生类的虚基类,同时也可以作为另一个派生类的非虚基类。

        派生类B和C声明虚基类后,派生类D的成员如图5.23所示。

二、虚基类的初始化

        如果在虚基类中定义了带参数的构造函数,而且没定义默认构造函数,要求在它的所有派生类(直接和间接)中,通过构造函数的初始化表对虚基类进行初始化。 

class  A
{ A (int k) { } … … };class  B: virtual public A
{B (int n ):A(n){ }… … };class  C: virtual public A
{C (int n ):A(n){ } … … };class  D: public B,public C
{D (int n ):A(n),B(n),C(n) { } … … };

【注】

        在定义类D的构造函数时,与以往的方法不同。虚基类在派生类中只有一份数据成员,所以这份数据成员的初始化必须由派生类直接给出。如果不由最后的派生类(如图5.21的类D)直接对虚基类初始化,而由虚基类的直接派生类(如图5.21的类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数。所以规定:最后的派生类不仅要负责对其直接基类初始化,还要负责对虚基类初始化。

        可能有人提出:类D的构造函数用初始化表调用了虚基类的构造函数A,类B和类C的构造函数也用初始化表调用了虚基类的构造函数A,这岂不是调用了三次虚基类构造函数?其实C++编译系统只执行最后的派生类调用虚基类的构造函数,忽略虚基类其他派生类(如类B和类C)调用虚基类构造函数,保证对虚基类的数据成员只作一次初始化。

三、例【5.9】在【例5.8】中在teacher类和student类之上增加一个共同的基类person,人员的一些基本数据放在person中

#include <iostream>
#include <string.h>
using namespace std;class Person 
{public:Person(char *nam, char s, int a) { //    构造函数strcpy(name, nam);sex = s;age = a;}protected:                                    //    保护成员char name[20];char sex;int age;
};class Teacher: virtual public Person 
{ //声明Person为公用继承的虚基类public:Teacher(char *nam, char s, int a, char *t): Person(nam, s, a) { //构造函数strcpy(title, t);}protected:                                       //    保护成员char title[10];                                //    职称
};class Student: virtual public Person
//声明Person为公用继承的虚基类
{public:Student(char *nam, char s, int a, float sco):Person(nam, s, a), score(sco) { } //    初始化表protected:                                      //    保护成员float score;                                  //    成绩
};class Graduate: public Teacher, public Student
//    声明Teacher和Student类为公用继承的直接基类
{public:Graduate(char *nam, char s, int a, char *t, float sco, float w): //  构造函数Person(nam, s, a), Teacher(nam, s, a, t), Student(nam, s, a, sco),wage(w) {} void show( ) {                               //    输出研究生的有关数据cout << "name:" << name << endl;cout << "age:" << age << endl;cout << "sex:" << sex << endl;cout << "score:" << score << endl;cout << "title:" << title << endl;cout << "wages:" << wage << endl;}private:float wage;                     //工资
};int main( ) 
{Graduate grad1("Wang-li", 'f', 24, "assistant", 89.5, 1234.5);grad1.show( );return 0;
}

四、多层多重继承用虚基类

#include <iostream>
#include <string.h>
using namespace std;class B 
{public:B(int i ) {b = i;}int b;
};class B1 : virtual public B 
{ //  在第一层声明虚基类public:B1(int b, int bx): B(b) { //  在每层派生中都调用基类b1 = bx;}private:int b1;
};class B2 : virtual public B 
{    //  在第一层声明虚基类public:B2(int b, int bx): B(b) { //  在每层派生中都调用基类b2 = bx;}private:int b2;
};class C: public B1, public B2 
{public: //  在每层派生中都调用基类构造函数C(int x1, int x2, int x3, int x4): B(x1), B1(x1, x2), B2(x1, x3) {d = x4;}private:int d;
};int main(int argc, char *argv[]) 
{C cc(1, 2, 3, 4);cout << cc.b << endl;return 0;
}

五、虚基类的构造函数

        调用顺序的规则:

  • 若同一层次中只包含多个虚基类,按他们声明的先后次序调用,再调用派生类的构造函数
  • 若虚基类由非虚基类派生而来,先调用基类构造函数;再掉用派生类的构造函数
  • 若同一层次中同时包含虚基类和非虚基类,先调用虚基类的构造函数;再掉用非虚基类的构造函数;最后调用派生类构造函数

六、多重继承如何工作

        两用沙发是一张沙发,也是一张床,两用沙发允许同时继承沙发和床的特征,即SleepSofa继承Bed和Sofa两个类。

        在上节中,sofa和bed都有一个weight成员这是合理的,因为两者都是实体,都有重量。问题是SleepSofa继承哪个重量?回答是两者都继承,由于两者有相同的个名字weight,使得对weight的使用变得稍微复杂一点。

        假如按照下面的使用:

int  main()
{    SleeperSofa ss;ss.Setweight(20); … 
}

        结果导致名称冲突(name collision),编译时出错。解决的方法是在成员名前指定其基类名:

int  main()
{    SleeperSofa ss;ss.sofa.Setweight(20);  … 
}

         在编写应用程序时,还要程序员知道类的层次信息,增加了编程的复杂度,在单继承中不会出现这样的问题。

七、虚拟继承

        客观上讲,一个SleepSofa没有沙发和床两种重量,如此继承不是真实世界的反应。其实沙发和床都是家居的一种,凡是家具都有重量,所以通过分解考虑它们的关系。

        因为SleepSofa不是直接继承Furniture,而是bed和sofa各自继承Furniture,所以完整的SleepSofa对象的内存布局如前面的图,它包括一个完整的bed,还有一个完整的sofa和SleepSofa自己的成员。它包括了两个weight成员,C++不知道SetWeight()属于哪个Furniture成员,指向Furniture的指针也不知道究竟指向哪个Furniture,这就是程序多重继承2无法通过编译的原因。

        SleepSofa只需要一个Furniture,所以希望它只包含一个Furniture拷贝,同时又要共享bed和sofa的成员函数和数据成员,C++提供了虚拟继承方法实现这种继承结构。

        在定义bed和sofa继承Furniture时,在冒号和继承方式之间增加关键字virtual。这相当说,如果还没有Furniture,则加入一个Furniture拷贝,否则就用已有的那下一个。此时一个SleepSofa对象在内存中只保留一个Furniture拷贝。

【注】虚拟继承的虚拟和虚拟函数的虚拟没有任何关系

八、虚拟继承沙发床

#include <iostream>
#include <string.h>
using namespace std;class Furniture 
{protected:int weight ;public:Furniture() {}void Setweight( int i ) {weight = i ;}int getweight() {return weight;}
};class bed: virtual public Furniture 
{public:bed() {}void Sleep() {cout << "  睡眠 " << endl;}};class sofa: virtual public Furniture 
{public:sofa() {}void WatchTV() {cout << "  看电视  " << endl;}};class SleeperSofa : public bed, public sofa 
{public:SleeperSofa() {}void Foldout() {cout << "  打开沙发" << endl;}
};int main(int argc, char *argv[]) 
{SleeperSofa ss;ss.Setweight(20);Furniture *pf;pf = &ss;cout << pf-> getweight() << endl;cout << ss.getweight() << endl;return 0;
}

九、多继承的构造顺序 

        构造函数按下列顺序被调用:

  • 按继承虚基类的顺序调用调用虚基类的构造函数
  • 按继承非虚基类的顺序调用非虚基类的构造函数
  • 按声明成员对象的顺序调用其构造函数
  • 调用派生类自己的构造函数

十、多重继承的构造函数举例

#include <iostream>
#include <string.h>
using namespace std;class OBJ1 
{public:OBJ1() {cout << "调用OBJ1类构造函数" << endl;}
};class OBJ2 
{public:OBJ2() {cout << "调用OBJ2类构造函数" << endl;}
};class Base1 
{public:Base1() {cout << "调用Base1类构造函数" << endl;}
};class Base2 
{public:Base2() {cout << "调用Base2类构造函数" << endl;}
};class Base3 
{public:Base3() {cout << "调用Base3类构造函数" << endl;}
};class Base4 
{public:Base4() {cout << "调用Base4类构造函数" << endl;}
};class Derived: public Base1, virtual public Base2,public Base3, virtual public Base4 
{public:Derived(): Base4(), Base3(), Base2(), Base1(), obj1(), obj2() {cout << "调用派生类构造函数成功!" << endl;}protected:OBJ1 obj1;OBJ2 obj2;
};int main(int argc, char *argv[]) 
{Derived aa;cout << "派生类对象 aa 构造成功,谢谢! " << endl;return 0;
}

十一、多重继承综合举例

        分别定义Teacher类和Cadre类,用多重继承方式由这两个类派生出新类Teacher_Cadre。

  • 在这两个基类中都包含数据成员:姓名、年龄、性别、地址、电话
  • 在Teacher类中还有职称,在Cadre类中还有职务,在派生类中有工资数据成员
  • 两个基类中的数据成员用相同的名字,在引用时指定作用域
  • 在类体中声明成员函数,在类体外定义成员函数
  • 在派生类的成员函数show中调用Teache类的display函数,输出姓名、年龄性别、职称、地址、电话,然后再用cout语句输出职务和工资

1.方法一

(1)代码一 

#include <iostream>
#include <string.h>
using namespace std;class Teacher 
{protected:string name;int age;char sex;string title;string addr;string tel;public:Teacher(string nam, int a, char s, string tit, string ad, string t);void display();
};Teacher::Teacher(string nam, int a, char s, string tit, string ad, string t): name(nam), age(a),  sex(s), title(tit),addr(ad), tel(t) { }void Teacher::display() 
{cout << "name:" << name << endl;cout << "age" << age << endl;cout << "sex:" << sex << endl;cout << "title:" << title << endl;cout << "address:" << addr << endl;cout << "tel:" << tel << endl;
}class Cadre 
{protected:string name;int age;char sex;string post;string addr;string tel;public:Cadre(string nam, int a, char s, string p, string ad, string t);void display();
};Cadre::Cadre(string nam, int a, char s, string p, string ad, string t):name(nam), age(a), sex(s), post(p), addr(ad), tel(t) {}void Cadre::display() 
{cout << "name:" << name << endl;cout << "age:" << age << endl;cout << "sex:" << sex << endl;cout << "post:" << post << endl;cout << "address:" << addr << endl;cout << "tel:" << tel << endl;
}class Teacher_Cadre: public Teacher, public Cadre 
{private:float wage;public:Teacher_Cadre(string nam, int a, char s, string tit, string p, string ad, string t, float w);void show( );
};//派生类构造函数
Teacher_Cadre::Teacher_Cadre(string nam, int a, char s, string t, string p, string ad, string tel,float w):  Teacher(nam, a, s, t, ad, tel), Cadre(nam, a, s, p, ad, tel), wage(w) {}void Teacher_Cadre::show( ) 
{Teacher::display();cout << "post:" << Cadre::post << endl;cout << "wages:" << wage << endl;
}int main(int argc, char *argv[]) 
{Teacher_Cadre t1("Wang-li", 50, 'f', "prof.", "president", "135 Beijing Road,Shanghai", "(021)61234567", 1534.5);t1.show( );cout << "基类Teacher长度是:" << sizeof( Teacher) << "字节" << endl;cout << "基类Cadre长度是:" << sizeof( Cadre) << "字节" << endl;cout << "派生类对象长度是:" << sizeof( t1) << "字节" << endl;return 0;
}

(2)结果一 

        从程序运行结果得知,不采用虚基类,派生类的对象长度是各个基类长度之和。这对提高内存利用率不利。我们用虚基类的方法求解上面的题目,看派生类对象的长度怎样变化。

2.方法二

(1)代码二 

#include <iostream>
#include <string.h>
using namespace std;class Teacher 
{protected:string name;int age;char sex;string title;string addr;string tel;public:Teacher(string nam, int a, char s, string tit, string ad, string t);void display();
};Teacher::Teacher(string nam, int a, char s,string tit, string ad, string t): name(nam),age(a),  sex(s), title(tit), addr(ad), tel(t) { }void Teacher::display() 
{cout << "name:" << name << endl;cout << "age" << age << endl;cout << "sex:" << sex << endl;cout << "title:" << title << endl;cout << "address:" << addr << endl;cout << "tel:" << tel << endl;
}class Cadre 
{protected:string name;int age;char sex;string post;string addr;string tel;public:Cadre(string nam, int a, char s, string p, string ad, string t);void display1();
};Cadre::Cadre(string nam, int a, char s, string p, string ad, string t):name(nam), age(a), sex(s), post(p), addr(ad), tel(t) {}void Cadre::display1() 
{cout << "name:" << name << endl;cout << "age:" << age << endl;cout << "sex:" << sex << endl;cout << "post:" << post << endl;cout << "address:" << addr << endl;cout << "tel:" << tel << endl;
}class Teacher_Cadre: virtual public Teacher, virtual public Cadre 
{public:Teacher_Cadre(string nam, int a, char s, string tit, string p, string ad,string t, float w);void show( );private:float wage;
};Teacher_Cadre::Teacher_Cadre(string nam, int a, char s, string t, string p, string ad, string tel, float w):Teacher(nam, a, s, t, ad, tel), Cadre(nam, a, s, p, ad, tel), wage(w) {}void Teacher_Cadre::show( ) 
{//    Teacher::display();display();//    cout<<"post:"<<Cadre::post<<endl;cout << "post:" << post << endl;cout << "wages:" << wage << endl;
}int main(int argc, char *argv[]) 
{Teacher_Cadre t1("Wang-li", 50, 'f', "prof.", "president", "135 Beijing Road,Shanghai", "(021)61234567", 1534.5);t1.show( );cout << "基类Teacher长度是:" << sizeof( Teacher) << "字节" << endl;cout << "基类Cadre长度是:" << sizeof( Cadre) << "字节" << endl;cout << "派生类对象长度是:" << sizeof( t1) << "字节" << endl;return 0;
}

(2)结果二 

        从程序运行结果得知,派生类对象的长度没有变小,而是变大。
        从教师类和干部类看到两个类有许多相同的数据成员,可以对两个类再抽象,建立一个基类staff,包括姓名、年龄、性别、地址、电话等数据成员。教师类从它派生,增加职称数据成员;干部类也从它派生,增加职务数据成员,教师干部类从教师类和干部类多重派生,增加工资成员。

3.方法三

(1)代码三

#include <iostream>
#include <string.h>
using namespace std;class Staff 
{protected:string name;int age;char sex;string addr;string tel;public:Staff(string nam, int a, char s, string ad, string t);void display();};Staff::Staff(string nam, int a, char s, string ad, string t):name(nam), age(a), sex(s), addr(ad), tel(t) { }void Staff::display() 
{cout << "name:" << name << endl;cout << "age" << age << endl;cout << "sex:" << sex << endl;cout << "address:" << addr << endl;cout << "tel:" << tel << endl;
}class Teacher: virtual public Staff 
{protected:string title;public:Teacher(string nam, int a, char s, string ad, string t, string tit);void display();};Teacher::Teacher(string nam, int a, char s, string ad, string t, string tit):Staff(nam, a, s, ad, t), title(tit) { }void Teacher::display() 
{Staff::display();cout << "title:" << title << endl;}class Cadre: virtual public Staff 
{protected:string post;public:Cadre(string nam, int a, char s, string ad, string t, string pos);void display1();};Cadre::Cadre(string nam, int a, char s, string ad, string t, string pos):Staff(nam, a, s, ad, t), post(pos) { }void Cadre::display1() 
{Staff::display();cout << "post:" << post << endl;
}class Teacher_Cadre:  public Teacher, public Cadre 
{public:Teacher_Cadre(string nam, int a, char s, string tit, string p, string ad, string t, float w);void show( );private:float wage;
};Teacher_Cadre::Teacher_Cadre(string nam, int a, char s, string t, string p, string ad, string tel, float w):Staff( nam, a, s, ad, tel), Teacher(nam, a, s, ad, tel, t), Cadre(nam, a, s, ad, tel, p), wage(w) {}void Teacher_Cadre::show( ) 
{display();cout << "post:" << post << endl;cout << "wages:" << wage << endl;
}int main(int argc, char *argv[]) 
{Teacher_Cadre t1("Wang-li", 50, 'f', "prof.", "president", "135 Beijing Road,Shanghai", "(021)61234567", 1534.5);t1.show( );cout << "基类Staff长度是:" << sizeof( Staff) << "字节" << endl;cout << "基类Teacher长度是:" << sizeof( Teacher) << "字节" << endl;cout << "基类Cadre长度是:" << sizeof( Cadre) << "字节" << endl;cout << "派生类教师干部对象t1长度是:" << sizeof( t1) << "字节" << endl;return 0;
}

(2)结果三 


这篇关于【C++面向对象程序设计】CH5 继承与派生(续)——虚基类的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/719154

相关文章

C#如何调用C++库

《C#如何调用C++库》:本文主要介绍C#如何调用C++库方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录方法一:使用P/Invoke1. 导出C++函数2. 定义P/Invoke签名3. 调用C++函数方法二:使用C++/CLI作为桥接1. 创建C++/CL

C++如何通过Qt反射机制实现数据类序列化

《C++如何通过Qt反射机制实现数据类序列化》在C++工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作,所以本文就来聊聊C++如何通过Qt反射机制实现数据类序列化吧... 目录设计预期设计思路代码实现使用方法在 C++ 工程中经常需要使用数据类,并对数据类进行存储、打印、调试等操作。由于数据类

Linux下如何使用C++获取硬件信息

《Linux下如何使用C++获取硬件信息》这篇文章主要为大家详细介绍了如何使用C++实现获取CPU,主板,磁盘,BIOS信息等硬件信息,文中的示例代码讲解详细,感兴趣的小伙伴可以了解下... 目录方法获取CPU信息:读取"/proc/cpuinfo"文件获取磁盘信息:读取"/proc/diskstats"文

C++使用printf语句实现进制转换的示例代码

《C++使用printf语句实现进制转换的示例代码》在C语言中,printf函数可以直接实现部分进制转换功能,通过格式说明符(formatspecifier)快速输出不同进制的数值,下面给大家分享C+... 目录一、printf 原生支持的进制转换1. 十进制、八进制、十六进制转换2. 显示进制前缀3. 指

C++中初始化二维数组的几种常见方法

《C++中初始化二维数组的几种常见方法》本文详细介绍了在C++中初始化二维数组的不同方式,包括静态初始化、循环、全部为零、部分初始化、std::array和std::vector,以及std::vec... 目录1. 静态初始化2. 使用循环初始化3. 全部初始化为零4. 部分初始化5. 使用 std::a

C++ vector的常见用法超详细讲解

《C++vector的常见用法超详细讲解》:本文主要介绍C++vector的常见用法,包括C++中vector容器的定义、初始化方法、访问元素、常用函数及其时间复杂度,通过代码介绍的非常详细,... 目录1、vector的定义2、vector常用初始化方法1、使编程用花括号直接赋值2、使用圆括号赋值3、ve

如何高效移除C++关联容器中的元素

《如何高效移除C++关联容器中的元素》关联容器和顺序容器有着很大不同,关联容器中的元素是按照关键字来保存和访问的,而顺序容器中的元素是按它们在容器中的位置来顺序保存和访问的,本文介绍了如何高效移除C+... 目录一、简介二、移除给定位置的元素三、移除与特定键值等价的元素四、移除满足特android定条件的元

Python获取C++中返回的char*字段的两种思路

《Python获取C++中返回的char*字段的两种思路》有时候需要获取C++函数中返回来的不定长的char*字符串,本文小编为大家找到了两种解决问题的思路,感兴趣的小伙伴可以跟随小编一起学习一下... 有时候需要获取C++函数中返回来的不定长的char*字符串,目前我找到两种解决问题的思路,具体实现如下:

C++ Sort函数使用场景分析

《C++Sort函数使用场景分析》sort函数是algorithm库下的一个函数,sort函数是不稳定的,即大小相同的元素在排序后相对顺序可能发生改变,如果某些场景需要保持相同元素间的相对顺序,可使... 目录C++ Sort函数详解一、sort函数调用的两种方式二、sort函数使用场景三、sort函数排序

Java调用C++动态库超详细步骤讲解(附源码)

《Java调用C++动态库超详细步骤讲解(附源码)》C语言因其高效和接近硬件的特性,时常会被用在性能要求较高或者需要直接操作硬件的场合,:本文主要介绍Java调用C++动态库的相关资料,文中通过代... 目录一、直接调用C++库第一步:动态库生成(vs2017+qt5.12.10)第二步:Java调用C++