白话C++系列(21) -- 虚继承
2016-05-29 19:07 Keiven_LY 阅读(901) 评论(0) 编辑 收藏 举报虚继承
多继承+多重继承的烦扰
先来看一个例子,在这个例子中,类A是父类,类B和类C继承了类A,而类D既继承类B又继承类C(如下图所示),我们称这种继承关系又称之为菱形继承。在菱形继承中我们发现,既有多继承,也有多重继承
那么,现在问题来了……
当我们要实例化D的对象的时候呢,我们发现,D是从B继承来的,B又是从A继承来的;而D也是从C继承来的,C又是从A继承来的,这样一来,D中就含有两个一样的A的数据,而这种情况是我们不能容忍的。因为在一个对象当中有两份完全相同的数据,这种冗余数据我们没有办法来承担它的系统开销,所以我们必须要解决。也许,你会说在实际的工作中会遇到这么复杂的情况吗?我们来看一看,不用实际工作,我们只需要回顾一下前面举过的例子:之前我们讲过人这个类,也讲过工人,农民和农民工这三个类,那么,工人和农民可以继承人这个类,而农民工则既可以继承农民,也可以继承工人这两个类(如下)
那么,这种典型的菱形继承关系用什么办法刚才我们所说的数据冗余的问题呢?那就要用到接下来要讲到的虚继承相关知识。
虚继承
虚继承是继承的一种方式,其关键字是virtual,我们来看一看如何使用,请看下面这个例子。
在这个例子中,我们定义了一个工人的类,工人的类要去继承人这个类,而工人这个类将来也会被农民工这个类所继承,我们就把工人这个类就称之为虚基类。那么,在工人这个类去继承人这个类的时候,就需要加上关键字virtual。对于农民这个类写法上与工人这个类一样。(如下所示)。
在使用的时候,我们的农民工这个类,就可以继承工人和农民这两个类了。于是使用农民工这个类去实例化一个农民工的对象,那么,它当中就只含有一份Person的数据。
虚继承编码实践
题目描述:
/* **************************************************************************** */
/*
虚继承:
说明:通过这个例子学习虚继承的使用方法,及虚继承存在的必要性
要求:
1.Farmer类,数据成员: m_strName【农民姓名】,成员函数:构造函数,析构函数,sow()【播种函数】
2.Worker类,数据成员:m_strCode【工人工号】,成员函数:构造函数,析构函数,carry()【搬运函数】
3.MigrantWorker类,数据成员:无,成员函数:构造函数,析构函数
4.Person类,数据成员:m_strColor【人的肤色】,成员函数:构造函数,析构函数,printColor()
*/
/* *************************************************************************** */
程序框架:
头文件(Person.h)
#include<string> usingnamespace std; classPerson { public: Person(string color = "blue"); virtual ~Person(); void printColor(); protected: string m_strColor; };
源程序(Person.cpp)
#include<iostream> #include"Person.h" usingnamespace std; Person::Person(stringcolor) { m_strColor = color; cout <<"Person()"<< endl; } Person::~Person() { cout <<"~Person()"<< endl; } voidPerson::printColor() { cout << m_strColor << endl; cout <<"Person---printColor()"<< endl; }
头文件(Worker.h)
#include<string> #include"Person.h" usingnamespace std; classWorker:publicPerson { public: Worker(string code = "001", string coloe = "blue");//我们希望Worker可以传入参数“肤色”给Person类 virtual ~Worker(); //虚析构函数 void carry(); protected: string m_strCode; };
源程序(Worker.cpp)
#include"Worker.h" #include<iostream> usingnamespace std; Worker::Worker(stringcode, stringcolor):Person(color) { m_strCode = code; cout <<"Worker()"<< endl; } Worker::~Worker() { cout <<"~Worker()"<< endl; } voidWorker::carry() { cout << m_strCode << endl; cout <<"Worker---carry()"<< endl; }
头文件(Farmer.h)
#include<string> #include"Person.h" usingnamespace std; classFarmer:publicPerson { public: Farmer(string name = "Jack", string color = "blue"); virtual ~Farmer(); void sow(); protected: string m_strName; };
源程序(Farmer.cpp)
#include"Farmer.h" #include<iostream> usingnamespace std; Farmer::Farmer(stringname, stringcolor):Person(color) { m_strName = name; cout <<"Farmer()"<< endl; } Farmer::~Farmer() { cout <<"~Farmer()"<< endl; } voidFarmer::sow() { cout << m_strName << endl; cout <<"Farmer---sow()"<< endl; }
头文件(MigrantWorker.h)
#include"Worker.h" #include"Farmer.h" classMigrantWorker:publicFarmer, publicWorker//农民工类继承农民类和工人类 { public: MigrantWorker(string name,string code, string color); ~MigrantWorker(); };
源程序(MigrantWorker.cpp)
#include"MigrantWorker.h" #include<iostream> usingnamespace std; //采用初始化列表的方式将农民工的姓名和工号分别传递给农民的m_strName和工人的m_strCode MigrantWorker::MigrantWorker(stringname, stringcode, stringcolor):Farmer(name, color),Worker(code, color) { cout <<"MigrantWorker()"<< endl; } MigrantWorker::~MigrantWorker() { cout <<"~MigrantWorker()"<< endl; }
主调程序(demo.cpp)
#include<iostream> #include<stdlib.h> #include"MigrantWorker.h" usingnamespace std; int main() { system("pause"); return 0; }
如果我们现在在主调函数中什么都不写,直接按F5运行程序,我们会看见如下错误:
1>d:\vs_program\farmer_worker_migrantworker\farmer_worker_migrantworker\person.h(5): error C2011: “Person”:“class”类型重定义
为什么会这样呢?
我们看到,在Farmer.h和Worker.h中,我们都引用了”Person.h”,这样就对Person这个类进行了重定义,所以会报这个错误。那么,怎么解决这个错误呢?
下面介绍一种方法----宏定义,来解决重定义的问题。
首先,我们在公共的被继承的这个类的.h文件当中,加上如下几行代码:
#ifndef PERSON_H //如果没有定义 PERSON_H #definePERSON_H//那么就定义 PESON_H #endif//结束定义
加上这三行后,Person.h文件如下:
#ifndef PERSON_H //如果没有定义 PERSON_H #definePERSON_H//那么就定义 PESON_H #include<string> usingnamespace std; classPerson { public: Person(string color = "blue"); virtual ~Person(); void printColor(); protected: string m_strColor; }; #endif//结束定义
这个时候,我们再按F5,运行程序,就不会再报错了
注意:用宏定义解决重定义的技巧,需要大家掌握,因为在工作中,大家肯定会遇到相同的问题。
下面,我们跳到demo.cpp文件中,在其中,我们先实例化一个农民工的对象(从堆中实例化),传入的参数有三个(姓名=”Merry”,工号=”200”,肤色=”Yellow”),然后直接删除这个对象,我们想要看的是:菱形继承过程当中,构造函数和析构函数的执行顺序。
int main() { MigrantWorker *p = newMigrantWorker("Merry", "200", "Yellow"); delete p; p = NULL; system("pause"); return 0; }
我们来看一下运行结果:
我们来分析一下这个执行结果:
因为作为MigrantWorker这个类来说,它在实例化一个农民工对象的时候,必然要先实例化它的两个父类(Farmer类和Worker类),而在实例化Farmer类和Worker类的时候呢,由于这两个类的父类都是Person类,所以在实例化Farmer类之前必定先实例化Person类,同理,在实例化Worker类的时候也必定先实例化Person类,所以Person这个类就实例化了两次。在销毁的时候,析构函数的执行顺序与构造函数的执行顺序相反。
做完这样的实验之后,我们来想一想,其实在农民工这个类当中,已经存在了两份Person对象中的数据。那么,我们也可以通过调用的方式来向大家证明。为了证明这一点,我们需要对现在的程序做一些简单改造。
首先打开Worker.cpp文件,我们先来看一看Worker的构造函数是如何实现的.zai Worker的构造函数中,如果传入参数color,那么color就会直接传递给Person。我们希望在这个过程中,能够打上Worker的印记,所以我们这样来改造一下Worker的构造函数:将Worker作为字符串的一部分也传递给Person(字符串之间是可以进行拼接的)
Worker::Worker(stringcode, stringcolor):Person("Worker" + color) { m_strCode = code; cout <<"Worker()"<< endl; }
使用同样的方法来改造一下Farmer的构造函数,如下:
Farmer::Farmer(stringname, stringcolor):Person("Farmer" + color) { m_strName = name; cout <<"Farmer()"<< endl; }
最后,我们返回demo.cpp文件中,配合前面我们写的代码,要想证明Person在农民工这个对象中存在两份相同的数据成员,那么我们就要通过农民工这个对象指针来分别打印一下这两份数据成员的值。这两份数据成员呢,一份来自于Worker,一份来自于Farmer。
int main() { MigrantWorker *p = newMigrantWorker("Merry", "200", "Yellow"); p->Farmer::printColor(); p->Worker::printColor(); delete p; p = NULL; system("pause"); return 0; }
看看运行效果如何:
从运行结果,我们看到了“FarmerYellow”和“WorkerYellow”,可见,在农民工这个对象当中,实际上是存在着两个m_strColor这样的数据成员的。这种情况是我们不能容忍的,我们希望有一份就可以了。那么,怎么样才能保证使它只有一份数据成员呢?这个时候,就需要用到我们之前讲到的虚继承相关知识。写法上,将Worker类和Farmer类分别采用虚继承的方式继承Person类
classWorker:virtualpublicPerson
{
……
}
classFarmer:virtualpublicPerson
{
……
}
然后,我们返回到demo.cpp中,重新运行一下程序如下:
我们首先来观察一下它的构造函数和析构函数的运行情况:当我们加了virtual这个关键字以后,我们发现:Person这个构造函数和析构函数就执行了一次,这就说明,当我们采用了virtual关键字后就形成了虚继承,虚继承的好处就使得农民工这个类所实例化的对象当中只有一份Person的数据。
我们再来看一看中间部分所打印出来的数据。我们可以看到,原来是“FarmerYellow”和“WorkerYellow”的地方,现在变成了只是“blue”,这就说明,在虚继承的情况下,作为菱形继承最顶层的父类并没有进行参数的传递,也就是说,参数只使用了顶层父类的默认参数,而无法从子类当中获得传入的参数。