首页 > 编程笔记

C++多继承的定义和使用

我们知道继承方式都是单继承,即派生类的基类只有一个。但是在实际开发应用中,一个派生类往往会有多个基类,派生类从多个基类中获取所需要的属性,这种继承方式称为多继承。

例如水鸟,既具有鸟的特性,能在天空飞翔,又具有鱼的特性,能在水里游泳。本文将针对多继承进行详细讲解。

多继承方式

多继承是单继承的扩展,在多继承中,派生类的定义与单继承类似,其语法格式如下所示:

class 派生类名:继承方式  基类1名称,继承方式  基类2名称,…,继承方式  基类n名称
{
新增成员;
};

通过多继承,派生类会从多个基类中继承成员。

在定义派生类对象时,派生类对象中成员变量的排列规则是:按照基类的继承顺序,将基类成员依次排列,然后再存放派生类中的新增成员。

多继承的示例代码如下所示:

class Base1        //基类Base1
{
protected:
    int base1;       //成员变量base1
};
class Base2        //基类Base2
{
protected:
    int base2;       //成员变量base2
};
class Derive:public Base1,public Base2  //Derive类公有继承Base1类和Base2类
{
private:
    int derive;       //派生类新增成员变量
};

在上述代码中,派生类 Derive 公有继承 Base1 类和 Base2 类,如果定义 Derive 类对象,则 Derive 类对象中成员变量的排列方式如图 1 所示。

图1 Derive类对象中成员变量的排列方式
图1 Derive类对象中成员变量的排列方式

多继承派生类的构造函数与析构函数

与单继承中派生类构造函数类似,多继承中派生类的构造函数除了要初始化派生类中新增的成员变量,还要初始化基类的成员变量。

在多继承中,由于派生类继承了多个基类,因此派生类构造函数要负责调用多个基类的构造函数。

在多继承中,派生类构造函数的定义格式如下所示:

派生类构造函数名(参数列表):基类1构造函数名(参数列表), 基类2构造函数名(参数列表), …
{
派生类新增成员的初始化语句
}

在上述格式中,派生类构造函数的参数列表包含了新增成员变量和各个基类成员变量需要的所有参数。

定义派生类对象时,构造函数的调用顺序是:首先按照基类继承顺序,依次调用基类构造函数,然后调用派生类构造函数。

如果派生类中有成员对象,构造函数的调用顺序是:首先按照继承顺序依次调用基类构造函数,然后调用成员对象的构造函数,最后调用派生类构造函数。

除了构造函数,在派生类中还需要定义析构函数以完成派生类中新增成员的资源释放。析构函数的调用顺序与构造函数的调用顺序相反。如果派生类中没有定义析构函数,编译器会提供一个默认的析构函数。

【示例1】下面通过案例演示多继承派生类构造函数与析构函数的定义与调用,C++ 代码如下:
#include<iostream>
using namespace std;
class Wood        //木材类Wood
{
public:
    Wood(){cout<<"木材构造函数"<<endl; } 
    ~Wood(){cout<<"木材析构函数"<<endl; }
};
class Sofa        //沙发类Sofa
{
public:
    Sofa(){cout<<"沙发构造函数"<<endl; }
    ~Sofa(){cout<<"沙发析构函数"<<endl; }
    void sit(){cout<<"Sofa用来坐..."<<endl; }
};
class Bed         //床类Bed
{
public:
    Bed(){cout<<"床的构造函数"<<endl; }
    ~Bed(){cout<<"床的析构函数"<<endl; }
    void sleep(){cout<<"Bed用来睡觉..."<<endl; }
};
class Sofabed:public Sofa,public Bed    //Sofabed类,公有继承Sofa类和Bed类
{
public:
    Sofabed(){cout<<"沙发床构造函数"<<endl; }
    ~Sofabed(){cout<<"沙发床析构函数"<<endl; }
    Wood pearwood;      //Wood对象pearwood
};
int main()
{
    Sofabed sbed;       //创建沙发床对象sbed
    sbed.sit();      //通过sbed调用基类Sofa的sit()函数
    sbed.sleep();       //通过sbed调用基类Bed的sleep()函数
    return 0;
}
运行结果:

沙发构造函数
床的构造函数
木材构造函数
沙发床构造函数
Sofa用来坐...
Bed用来睡觉...
沙发床析构函数
木材析构函数
床的析构函数
沙发析构函数

示例分析:
在对象 sbed 创建和析构的过程中,构造函数的调用顺序如下:按照基类的继承顺序,先调用 Sofa 类构造函数,再调用 Bed 类构造函数。

调用完基类构造函数之后,调用派生类 Sofabed 中的成员对象(Wood类)的构造函数,最后调用派生类 Sofabed 的构造函数。在析构时,析构函数的调用顺序与构造函数相反。

多继承二义性问题

相比单继承,多继承能够有效地处理一些比较复杂的问题,更好地实现代码复用,提高编程效率,但是多继承增加了程序的复杂度,使程序的编写容易出错,维护变得困难。

最常见的就是继承过程中,由于多个基类成员同名而产生的二义性问题。多继承的二义性问题包括两种情况,下面分别进行介绍。

1) 不同基类有同名成员函数

在多继承中,如果多个基类中出现同名成员函数,通过派生类对象访问基类中的同名成员函数时就会出现二义性,导致程序运行错误。

【示例2】下面通过案例演示派生类对象访问基类同名成员函数时产生的二义性问题,C++ 代码如下:
#include<iostream>
using namespace std;
class Sofa       //沙发类Sofa
{
public:
    void rest(){cout<<"沙发可以坐着休息"<<endl; }
};
class Bed        //床类Bed
{
public:
    void rest(){cout<<"床可以躺着休息"<<endl; }
};
class Sofabed:public Sofa,public Bed   //Sofabed类,公有继承Sofa类和Bed类
{
public:
    void function(){cout<<"沙发床综合了沙发和床的功能"<<endl; }
};
int main()
{
    Sofabed sbed;       //创建沙发床对象sbed
    sbed.rest();       //通过sbed调用rest()函数
    return 0;
}
运行时编译器报错,如下所示:

“Sofabed::rest”不明确

示例分析:
本例 Sofabed 类与 Sofa 类、Bed 类的继承关系如图 2 所示。

图2 Sofabed类与Sofa类、Bed类的继承关系
图2 Sofabed类与Sofa类、Bed类的继承关系

由图 2 可知,在派生类 Sofabed 中有两个 rest() 函数,因此在调用时产生了歧义。

多继承的这种二义性可以通过作用域限定符::指定调用的是哪个基类的函数,可以将【示例2】中第 21 行代码替换为如下两行代码:

sbed.Sofa::rest();       //调用基类Sofa的rest()函数
sbed.Bed::rest();       //调用基类Bed的rest()函数

通过上述方式明确了所调用的函数,即可消除二义性。这需要程序设计者了解类的继承层次结构,相应增加了开发难度。

2) 间接基类成员变量在派生类中有多份拷贝

在多继承中,派生类有多个基类,这些基类可能由同一个基类派生。例如,派生类 Derive 继承自 Base1 类和 Base2 类,而 Base1 类和 Base2 类又继承自 Base 类。

在这种继承方式中,间接基类的成员变量在底层的派生类中会存在多份拷贝,通过底层派生类对象访问间接基类的成员变量时,会出现访问二义性。

【示例3】下面通过案例演示多重继承中成员变量产生的访问二义性问题,C++ 代码如下:
#include<iostream>
using namespace std;
class Furniture      //家具类Furniture
{
public:
    Furniture(string wood);     //Furniture类构造函数
protected:
    string _wood;       //成员变量_wood,表示材质
};
Furniture::Furniture(string wood)   //类外实现构造函数
{
    _wood=wood;
}
class Sofa:public Furniture    //沙发类Sofa,公有继承Furniture类
{
public:
    Sofa(float length,string wood);  //Sofa类构造函数
protected:
    float _length;      //成员变量_length,表示沙发长度
};
//类外实现Sofa类构造函数
Sofa::Sofa(float length,string wood):Furniture(wood)
{
    _length=length;
};
class Bed:public Furniture    //床类Bed,公有继承Furniture类
{
public:
    Bed(float width, string wood);  //Bed类构造函数
protected:
    float _width;       //成员变量_width,表示床的宽度
};
//类外实现Bed类构造函数
Bed::Bed(float width, string wood):Furniture(wood)
{
    _width=width;
}
class Sofabed:public Sofa,public Bed   //Sofabed类,公有继承Sofa类和Bed类
{
public:
    //构造函数
    Sofabed(float length,string wood1, float width,string wood2);
    void getSize();      //成员函数getSize(),获取沙发床大小
};
//类外实现Sofabed类构造函数
Sofabed::Sofabed(float length, string wood1, float width, string wood2):
    Sofa(length,wood1),Bed(width,wood2)
{
}
void Sofabed::getSize()     //类外实现getSize()函数
{
    cout<<"沙发床长"<<_length<<"米"<<endl;
    cout<<"沙发床宽"<<_width<<"米"<<endl;
    cout<<"沙发床材质为"<< _wood<<endl;
}
int main()
{
    Sofabed sbed(1.8,"梨木",1.5,"檀木"); //创建Sofabed类对象sbed
    sbed.getSize();      //调用getSize()函数获取沙发床信息
    return 0;
}
运行时编译器报错,如下所示:

"参数" :从“double”到“float”截断
对“_wood”的访问不明确
"Sodabed::_wood"不明确

示例分析:
Sofabed 类、Sofa 类、Bed 类和 Furniture 类之间的继承关系如图 3 所示。

图4-20 Sofabed类、Sofa类、Bed类和Furniture类之间的继承关系
图3 Sofabed类、Sofa类、Bed类和Furniture类之间的继承关系

由图 3 可知,基类 Furniture 的成员变量 _wood 在 Sofabed 类中有两份拷贝,分别通过继承 Sofa 类和 Bed 类获得。创建 Sofabed 类对象时,两份拷贝都获得数据。

本例第 58~59 行代码,创建 Sofabed 类对象 sbed,并通过对象 sbed 调用 getSize() 函数获取沙发床信息。

在 getSize() 函数中,第 54 行代码通过 cout 输出 _wood 成员值,由于 sbed 对象中有两个 _wood 成员值,在访问时出现了二义性,因此编译器报错。

为了避免访问 _wood 成员产生的二义性,必须通过作用域限定符::指定访问的是哪个基类的 _wood 成员。

可以将【示例3】中的第 54 行代码替换为如下两行代码:

cout<<"沙发床材质为"<<Sofa::_wood<<endl;
cout<<"沙发床材质为"<<Bed::_wood<<endl; 

优秀文章