一、多重继承:
#include <iostream>
using namespace std;
class Horse
{
public:
Horse(){cout<<"Horse constructor...";}
virtual ~Horse(){cout<<"Horse destructor...";}
virtual void whinny()const{cout<<"Whinny!..";}
private:
int itsAge;
};
class Bird
{
public:
Bird(){cout<<"Bird constructor...";}
virtual ~Bird(){cout<<"Bird destructor...";}
virtual void chirp()const{cout<<"Chirp!...";}
virtual void fly(){cout<<"Fly!!!";}
private:
int itsWeight;
};
class Pegasus:public Horse,public Bird
{
public:
Pegasus(){cout<<"Pegasus constructor...";}
//飞马也以鸟鸣的方式叫,但叫出来的声音是马的声音
//注意const不能少,否则就不能覆盖父类中的虚方法
//void chirp()const,从而多态调用失败
//掉了const导致的后果是隐藏,将来通过子类
//对象的指针、引用或者子类对象本身,都无法调用父类的
//同名方法,另一方面,通过指向子类对象的父类类型的指针
//也不能达到动态绑定从而多态调用,调用的永远是父类的
//方法
virtual void chirp()const{whinny();}
virtual ~Pegasus(){cout<<"Pegasu destructor...";}
};
int main()
{
Horse * ranch[2];
Bird * aviary[2];//鸟笼和农场的容量都是2
//观察打印的每一行可以看出每个对象的创建过程
ranch[0]=new Horse;//初始化农场
cout<<endl;
ranch[1]=new Pegasus;
cout<<endl;
aviary[0]=new Bird;//初始化鸟笼
cout<<endl;
aviary[1]=new Pegasus;
cout<<endl;
cout<<"*********************"<<endl;
ranch[0]->whinny();cout<<endl;
ranch[1]->whinny();cout<<endl;
aviary[0]->chirp();aviary[0]->fly();cout<<endl;
aviary[1]->chirp();aviary[1]->fly();cout<<endl;
cout<<"*********************"<<endl;
//观察打印的每一行可以看出每个对象的销毁过程
delete ranch[0];//清理农场
cout<<endl;
delete ranch[1];
cout<<endl;
delete aviary[0];//清理鸟笼
cout<<endl;
delete aviary[1];
cout<<endl;
//上面的子类对象能得以正确析构,要归功于虚析构函数
return 0;
}
打印结果:
避免歧义:
上面的代码中,若Horse和Bird类中都有getColor方法(可能是虚方法,也可能不是),且该方法在两个类中的签名完全一致,对子类来说,这两个方法都继承了,问题也随之而来:
1、当我们在客户端使用子类对象来调用该方法时:
Pegasus pgs;
pgs.getColor();
会告诉错误,因为编译器不知道该调用哪个getColor。解决办法,要么让子类有自己的getColor方法(覆盖父类的getColor),我们可以在子类自己的getColor方法指定调用哪个父类的getColor,或者都不调用,子类的getColor有自己全新的逻辑;要么在调用的时候指定调谁的:pgs.Bird::getColor()。
2、如果我们在客户端用这样调用:
Pegasus pgs;
Horse h=pgs;
h.getColor();
则不会有任何歧义,这种调用方式不会引入多态机制,永远调用父类的方法——与java不同。
3、如果这样调用:
Pegasus pgs;
Horse* ph=&pgs;//或者通过引用
ph->getColor();
也不会有任何歧义,这也不会引入多态机制,仅仅是在调用父类方法。
注意,上面所说的歧义,与getColor是不是虚方法没有关系,即,仅仅靠虚方法不能避免歧义,解决办法与1类似。
另一种歧义:
现在的情况是:马和鸟都继承自动物类Animal,由于飞马从马和鸟多继承,导致飞马对象的内存结构是这样的:
这五块内容,构成一个完整的飞马Pegasu对象,其中有两块一模一样的数据:Animal。这是飞马的两个直接父类从共同的基类Animal继承而来的。代码如下:
#include <iostream>
using namespace std;
enum COLOR{BLACK,RED,BLUE};
class Animal
{
public:
void baseFunc(){cout<<"Animal func..."<<endl;}
};
class Horse:public Animal
{
};
class Bird:public Animal
{
};
class Pegasus:public Horse,public Bird
{
};
int main()
{
Pegasus pgs;
Horse* ph=&pgs;
ph->baseFunc();//无歧义
pgs.baseFunc();//有歧义
return 0;
}
二、虚继承解决歧义:
虚继承得到的最终子类对象的内存模型:
从而能消除上面引起的歧义。代码:
#include <iostream>
using namespace std;
enum COLOR{BLACK,RED,BLUE};
class Animal
{
public:
void baseFunc(){cout<<"Animal func..."<<endl;}
};
class Horse:virtual public Animal
{
};
class Bird:virtual public Animal
{
};
class Pegasus:public Horse,public Bird
{
};
int main()
{
Pegasus pgs;
Horse* ph=&pgs;
ph->baseFunc();//无歧义
pgs.baseFunc();//无歧义,虚继承解决歧义
return 0;
}
虚继承引进的一个注意事项是:共同基类(这里指Animal)的初始化任务,由最终子类完成。——上述代码没有体现这一点,但要注意。所谓的“类的初始化”这种说法,严格来说,并不确切。对于上面的内存模型,它是一个最终子类对象的内存模型,即是一个Pegasu对象,我们给其所占内存里的内容编上号,从上到下从左至右,分别为0,1,2,3:
可以这样理解:整个(0,1,2,3)构成的是一个完整的飞马对象,而0块内容是一个Animal对象,1和0是一个Horse对象,2和0是一个Bird对象。上面所说的“基类的初始化”,意指0块内容的初始化。不涉及虚继承的情况下,父类的初始化,都在直接子类完成——通过在子类构造的参数列表后面手动调用父类相应的构造来完成。
经验总结:在单继承可行的情况,不要使用多继承,避免带来的开销及额外的风险。
三、纯虚函数(抽象类):
有纯虚函数的类是抽象类。抽象类不能实例化,它存在的意义类似java中的接口和抽象类,将后代共同的操作统一声明,至于具体如何实现,后代自己决定。一旦在父类中定义一个纯虚函数,就对子类提出这样的要求:要想使子类不是抽象类,就必须要覆盖实现这个纯虚函数,要覆盖父类中的每个纯虚函数。
虚函数可以有自己的实现。往往它完成的是通用的、基本的工作,通常的做法是:在子类具体的覆盖实现中,调用父类纯虚函数的实现来完成共同的基本操作。其实虚函数也能做到这一点,不过父类必须给出虚函数实现,而不能仅仅是声明。从抽象的角度来讲,具有纯虚函数的类抽象程度更大一些,它仅有通用方法的声明即可,且不能被实例化。