Cpp--改造多重继承

来源:互联网 发布:linux备份文件命令bak 编辑:程序博客网 时间:2024/03/28 23:28

当我构建一个继承两个基类的继承类时,考虑到可以将两个基类合并成一个,那么必然涉及到如何将两个基类的构造函数都调用。此时,要用到构造函数的重载来对两个基类的成员变量初始化。

这是未改动之前源代码:

#include<iostream>using namespace std;class CTimeType//定义时间类{int hour,minute,second,month,day,year;            //成员变量,默认为privatepublic:   //全部为公类函数CTimeType(int h=12,int m=0,int s=0)     //构造函数,初始化成员变量{hour=h;minute=m;second=s;}/* CTimeType(int mon=1,int d=1,int double y=2008)//实现构造函数重载{month=mon;day=d;year=y;}*/void display()//公类成员函数,输出时间{cout<<hour<<":"<<minute<<":"<<second<<endl;}void SetTime(int h,int m,int s)//成员函数,设置时间{hour=h;minute=m;second=s;}/*void SetDate(int mon,int d,double y)//成员函数,设置日期{month=mon;day=d;year=y;}void display1()    //成员函数,输出日期{cout<<month<<"/"<<day<<"/"<<year<<endl;}*/};class CDateType//日期类{int month,day,year;           //成员变量public:CDateType(int mon=1,int d=1,int y=2008)//构造函数{month=mon;day=d;year=y;}void display()//成员函数,输出日期{cout<<month<<"/"<<day<<"/"<<year<<endl;}void SetDate(int mon,int d,int y)//成员函数,设置日期{month=mon;day=d;year=y;}};class CDateTimeType:public CTimeType,public CDateType//时间日期类{public:CDateTimeType(int mon=1,int d=1,double y=2000,int h=0,int m=0,int s=0):CTimeType(h,m,s),CDateType(mon,d,y){/*形参列表为空*/};//继承类的构造函数//构造函数void display()//成员函数,显示时间、日期{CDateType::display();//调用CDateType类的display函数   派生类中要使用与基类一样的函数,直接调用即可CTimeType::display();//调用CTimeType类的display函数//CTimeType::display1();}/*void display1()  //创造一样的函数来调用{CTimeType::display1();}*/}; int main(){cout<<"类的多重继承演示"<<endl;CDateTimeType dt(1,1,2008,11,12,12);//直接使用DateTimeType构造函数设置日期时间cout<<"调用CDateTimeType类构造函数设定的初始日期、时间为:"<<endl;dt.display();//显示时间日期//dt.display1();dt.SetDate(8,8,2008);//调用基类的成员函数修改日期dt.SetTime(20,8,8);//调用基类的成员函数修改时间cout<<"调用基类成员函数修改后的日期、时间为:"<<endl;dt.display();//dt.display1();//通过派生类调用基类定义的函数return 0;}
那么测试结果是:


我们在改动之后可以看到程序代码为:

#include<iostream>using namespace std;class CTimeType//定义时间类{int hour,minute,second,month,day;            //成员变量,默认为privatedouble year;public:   //全部为公类函数CTimeType(int mon=1,int d=1,double y=2008,int h=12,int m=0,int s=0)     //构造函数,初始化成员变量{hour=h;minute=m;second=s;month=mon;day=d;year=y;}/* CTimeType(int mon=1,int d=1,double y=2008)//实现构造函数重载{month=mon;day=d;year=y;}*/void display()       //公类成员函数,输出时间{cout<<hour<<":"<<minute<<":"<<second<<endl;}void SetTime(int h,int m,int s)//成员函数,设置时间{hour=h;minute=m;second=s;}void SetDate(int mon,int d,double y)//成员函数,设置日期{month=mon;day=d;year=y;}void display1()    //成员函数,输出日期{cout<<month<<"/"<<day<<"/"<<year<<endl;}};/*class CDateType//日期类{int month,day,year;           //成员变量public:CDateType(int mon=1,int d=1,int y=2008)//构造函数{month=mon;day=d;year=y;}void display()//成员函数,输出日期{cout<<month<<"/"<<day<<"/"<<year<<endl;}void SetDate(int mon,int d,int y)//成员函数,设置日期{month=mon;day=d;year=y;}};*/class CDateTimeType:public CTimeType//,public CDateType//时间日期类{public:CDateTimeType(int mon=1,int d=1,double y=2000,int h=0,int m=0,int s=0):CTimeType(mon,d,y,h,m,s){/*构造函数形参列表为空*/};  //  CDateTimeType(int mon=1,int d=1,double y=2000,int h=0,int m=0,int s=0):CTimeType(mon,d,y){}; //  这里并不能这样来编写                                                                                               //  否则不能构成构造函数void display()//成员函数,显示时间、日期{//CDateType::display();//调用CDateType类的display函数   派生类中要使用与基类一样的函数,直接调用即可CTimeType::display();//调用CTimeType类的display函数//CTimeType::display1();}void display1()  //创造一样的函数来调用{CTimeType::display1();}}; int main(){cout<<"类的多重继承演示"<<endl;CDateTimeType dt(1,1,2008,11,12,12);//直接使用DateTimeType构造函数设置日期时间cout<<"调用CDateTimeType类构造函数设定的初始日期、时间为:"<<endl;dt.display1(); //显示时间日期dt.display();dt.SetDate(8,8,2008);//调用基类的成员函数修改日期dt.SetTime(20,8,8);//调用基类的成员函数修改时间cout<<"调用基类成员函数修改后的日期、时间为:"<<endl;dt.display1();dt.display();//通过派生类调用基类定义的函数return 0;}

测试结果是:


达到同样的效果。这里有个比较棘手的问题:

如何在派生类中同时引用基类的构造函数和重载构造函数?这会导致程序出现巨大的漏洞。



这样,便建立了我们需要的程序。但是实际上我们也需要使用多重继承来简化重载构造函数。下面讲解多重继承的知识。

多重继承

为支持多继承,一个类的派生表:

class Bear : public ZooAnimal { ... };

被扩展成支持逗号分割的基类表。例如:

class Panda : public Bear, public Endangered { ... };

每个被列出的基类还必须指定其访问级别:public、protected 或private 之一。与单继承一样,只有当一个类的定义已经出现后,它才能被列在多继承的基类表中。

对于一个派生类的基类的数目,C++没有限制。实际情况下,两个基类是最常见的,一个基类常常用于表示一个公有抽象接口,另外一个基类提供私有实现。从三个或者更多个直接基类继承而来的派生类遵循mixin-based

设计风格,其中每个基类都表示该派生类完整接口的一个方面。

 

构造的次序

在多继承下,派生类含有每个基类的一个基类子对象。例如,当我们写:

Panda ying_yang;

时,ying_yang 由一个Bear类子对象(它又含有一个ZooAnimal 基类子对象)、一个Endangered 类子对象,以及在Panda 类中声明的非静态数据成员组成:

 

多继承Panda 层次结构

基类构造函数被调用的顺序以类派生表中声明的顺序为准。例如对ying_yang 来说,构造函数被调用的顺序是:Bear 构造函数(因为Bear 是从ZooAnimal 派生的,所以在Bear构造函数执行之前,ZooAnimal 的构造函数先被调用),Endangered 构造函数,然后是Panda构造函数。

构造函数调用顺序不受基类在成员初始化表中是否存在以及被列出的顺序的影响,也即是说,如果Bear 缺省构造函数被隐式调用,没有出现在成员初始化表中,如下所示:

// Bear 缺省构造函数在Endangered 的双参数构造函数之前被调用

Panda::Panda(): Endangered( Endangered::environment,Endangered::critical )

{ ... }

那么Bear 的缺省构造函数仍然在显式列出的双参数Endangered 构造函数之前被调用。类似地,析构函数调用顺序总是与构造函数顺序相反。在我们的例子中,析构函数调用顺序是:~Panda()、~Endangered()、~Bear(), 最后是~ZooAnimal()。

 

同名成员函数的二义性

在单继承下,基类的public 和protected 成员可以直接被访问,就像它们是派生类的成员一样,对多继承这也是正确的。但是在多继承下,派生类可以从两个或者更多个基类中继承同名的成员。然而在这种情况下,直接访问是二义的,将导致编译时刻错误。例如,如果Bear 和Endangered 都定义了一个成员函数print(),则如下语句:

ying_yang.print( cout );

将导致编译时刻错误,即使这两个通过继承得到的成员函数定义了不同的参数类型:

Error: ying_yang.print( cout ) -- ambiguous, one of

Bear::print( ostream& )

ndangered::print( ostream&, int )

原因在于继承得到的成员函数没有构成派生类中的重载函数,因此,对于print()调用,编译器在解析的时候,只是使用了针对print 的名字解析,而不是使用“基于传递给print()的实际实参类型的重载解“。

 

转换与多个基类

在单继承下,如果有必要的话,派生类的指针或引用可以自动被转换成基类的指针或引用。对于多继承,这也是正确的。例如一个Panda指针或引用可以被转换成ZooAnimal、Bear或Endangered类的指针或引用。例如:

extern void display( const Bear& );

extern void highlight( const Endangered& );

Panda ying_yang;

display( ying_yang );    // ok

highlight( ying_yang ); // ok

extern ostream& operator<<( ostream&, const ZooAnimal& );

cout << ying_yang << endl; // ok

但是在多继承下二义转换的可能性非常大。例如,考虑下列两个函数:

extern void display( const Bear& );

extern void display( const Endangered&);

用非限定修饰的Panda 对象调用display():

Panda ying_yang;

display( ying_yang ); // 错误: 二义性

将导致下列一般形式的编译时刻错误:

Error: display( ying_yang ) -- ambiguous, one of

extern void display( const Bear&);

extern void display( const Endangered&);

编译器没有办法区分应该使用哪一个直接基类(编译器不会试图根据派生类转换来区别基类间的转换,转换到每个基类都一样好)。

 

多重继承下的虚函数

为了了解多继承怎样影响虚拟函数机制,让我们为每个Panda 的直接基类定义一组虚拟函数:

class Bear : public ZooAnimal {

public:

virtual ~Bear();

virtual ostream& print( ostream&) const;

virtual string isA() const;

// ...

};

class Endangered {

public:

virtual ~Endangered();

virtual ostream& print( ostream&) const;

virtual void highlight() const;

// ...

};

现在我们来定义Panda,它提供了自己的print()实例、析构函数,并引入了一个新的虚拟函数cuddle():

class Panda : public Bear, public Endangered

{

public:

virtual ~Panda();

virtual ostream& print( ostream&) const;

virtual void cuddle();

// ...

};

可以直接从Panda 对象调用的虚拟函数集如下表所示:

虚拟函数名

活动实例

Destructor(构析函数)

Panda::~Panda()

print(ostream&) const

Panda::print(ostream&)

isA() const

Bear::isA()

highlight() const

Endangered::highlight()

cuddle()

Panda::cuddle()

当用Panda 类对象的地址初始化或赋值Bear 或ZooAnimal 指针或引用时,Panda 接口中“Panda 特有的部分“以及”Endangered 部分“就都不能再被访问。例如:

Bear *pb = new Panda;

pb->print( cout ); // ok: Panda::print(ostream&)

pb->isA();           // ok: Bear::isA()

pb->cuddle();       // 错误: 不是 Bear 接口的部分

pb->highlight();   // 错误: 不是 Bear 接口的部分

delete pb;          // ok: Panda::~Panda()

类似地,当用Panda类对象的地址初始化或赋值Endangered 指针或引用时,Panda 接口中“Panda 特有的部分“以及”Bear 部分“都不能再被访问。例如:

Endangered *pe = new Panda;

pe->print( cout ); // ok: Panda::print(ostream&)

pe->isA();           // 错误: 不是 Endangered 的接口部分

pe->cuddle();       // 错误: 不是 Endangered 的接口部分

pe->highlight();    // ok: Endangered::highlight()

delete pe;           // ok: Panda::~Panda()

无论我们删除对象所使用的指针类型是什么,虚拟析构函数的处理都是一致的。例如:

// ZooAnimal *pz = new Panda;

delete pz;

// Bear *pb = new Panda;

delete pb;

// Panda *pp = new Panda;

delete pp;

// Endangered *pe = new Panda;

delete pe;

在上述例子中,析构函数的调用顺序完全相同。析构函数调用顺序与构造函数的顺序相反:Panda析构函数被通过虚拟机制调用。在Panda 析构函数执行之后依次静态调用Endangered、Bear 和ZooAnimal 析构函数。

多重继承的顺序

对于构建的多重继承,其顺序也是一个值得关注的地方

 class 派生类名:继承方式1 基类名1,继承方式2 基类名2,....

{

   //派生类成员定义

};

对每个基类可以用不同的继承方式,默认继承方式为private。

      在多重继承中,派生类的构造函数与单继承下派生类构造函数相似,它必须负责该派生类所有基类构造函数以及对象成员(如果有的话)构造函数的调用。同时,派生类的参数必须包含完成所有基类、对象成员以及派生类中新增数据成员初始化所需的参数。派生类构造函数执行顺序如下:

    (1)所有基类的构造函数,多个基类构造函数的执行顺序取决于定义派生类时所指定的顺序,与派生类构造函数中所定义的成员初始化列表的参数顺序无关;

     (2)对象成员的构造函数;

     (3)派生类本省的构造函数。


   加上虚基类后,它的初始化在语法上与一般多继承的初始化是相同的,但在调用构造函数的顺序上有点差别。

       (1)先调用虚基类构造函数,然后调用非虚基类的构造函数。

        (2)当同一层有多个虚基类,按照他们的声明顺序调用它们的构造函数;

        (3)当虚基类是由非虚基类派生时,则先调用基类构造函数,再调用派生类构造函数。

例1:

class X:public Y,virtual public Z
{
}
X one;
将产生如下调用顺序:
Z()
Y()
X()

这里Z是X的虚基类,故先调用Z的构造函数,再调用Y的构造函数,最后才调用派生类X自己的构造函数.


例2:

class base1
{
  ....
};
class base2
{
  ....
};
class level1:public base2,virtual public base1
{
   ....
};
class level2:public base2,virtual public base1
{
   ....
};
class toplevel1:public level1,virtual public level2
{
   ....
};
toplevel1 view;

当产生对象view时,将产生如下调用次序:

base1()

base2()

level2()

base2()

level1()

toplevel1()

toplevel1有两个基类:一个是虚基类level2,l另一个是非虚基类level1.根据规定:应先执行levevl2的构造函数;level2也有两个基类,一个实虚基类base1,另一个是非虚基类base2,应先执行base1的构造函数,再执行base2的构造函数,最后执行level2的构造函数。toplevel1然后执行level1的构造函数,而level1又有两个基类,base1是虚基类,无需再执行其构造函数,base2是非虚基类,因此要先执行base2的构造函数,然后执行level1的构造函数。最后执行toplevel1的构造函数。上例中,对于toplevel1的对象而言,base1是level1的虚基类。




0 0