程序员C++的知识分享C++

12. C++多继承 二义性 虚基类

2019-04-06  本文已影响5人  飞扬code

12.1 继承

派生类都只有一个基类,称为单继承。
同时C++也支持多继承,即一个派生类可以有两个或多个基类。

注意:多继承容易让代码逻辑复杂、思路混乱,具有一定争议,中小型项目中较少使用,一些编程语言甚至已经抛弃这种多继承。

多继承的语法也很简单,将多个基类用逗号隔开即可。例如已声明了类A、类B和类C,那么可以这样来声明派生类D:

class D: public A, private B, protected C{
    //类D新增加的成员
}

D 是多继承形式的派生类,
它以公有的方式继承 A 类,
以私有的方式继承 B 类,
以保护的方式继承 C 类。
D 根据不同的继承方式获取 A、B、C 中的成员,确定它们在派生类中的访问权限。

12.2 多继承下的构造函数

多继承形式下的构造函数和单继承形式基本相同,只是要在派生类的构造函数中调用多个基类的构造函数。
以 A、B、C、D 类为例,D 类构造函数的写法为:

D(形参列表): A(实参列表), B(实参列表), C(实参列表){
    //其他操作
}

基类构造函数的调用顺序和和它们在派生类构造函数中出现的顺序无关,
而是和声明派生类时基类出现的顺序相同。

12.3 多继承例程

#include <iostream>
using namespace std;

//基类
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
protected:
    int a;
    int b;
};

BaseA::BaseA(int a, int b): a(a), b(b){
    cout<<"基类A类构造调用"<<endl;
}
BaseA::~BaseA(){
    cout<<"基类A类构造调用"<<endl;
}

//基类
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
protected:
    int c;
    int d;
};

BaseB::BaseB(int c, int d): c(c), d(d){
    cout<<"基类B类构造调用"<<endl;
}
BaseB::~BaseB(){
    cout<<"基类B类构造调用"<<endl;
}

//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void show();
private:
    int e;
};

Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), e(e){
    cout<<"派生类构造调用"<<endl;
}

Derived::~Derived(){
    cout<<"派生类析构调用"<<endl;
}

void Derived::show(){
    cout<<this->a<<", "<<this->b<<", "<<this->c<<", "<<this->d<<", "<<this->e<<endl;
}

int main(){
    Derived obj(1, 2, 3, 4, 5);
    obj.show();
    return 0;
}
image.png

多继承形式下析构函数的执行顺序和构造函数的执行顺序相反


12.3 二义性

1.在继承时,基类之间、或基类与派生类之间发生成员同名时,将出现对成员访问的不确定性——同名二义性。
2.当派生类从多个基类派生,而这些基类又从同一个基类派生,则在访问此共同基类中的成员时,将产生另一种不确定性——路径二义性。

12.3.1 同名二义性
image.png

当两个或多个基类中有同名的成员时,如果直接访问该成员,就会产生命名冲突,编译器不知道使用哪个基类的成员。
这个时候需要在成员名字前面加上类名和域解析符::,以显式地指明到底使用哪个类的成员,消除同名二义性。
添加基类A和B一个新的成员函数show,派生类原有show改为diaplay函数

#include <iostream>
using namespace std;

//基类
class BaseA{
public:
    BaseA(int a, int b);
    ~BaseA();
    void show();
protected:
    int a;
    int b;
};

BaseA::BaseA(int a, int b): a(a), b(b){
    cout<<"基类A类构造调用"<<endl;
}

BaseA::~BaseA(){
    cout<<"基类A类构造调用"<<endl;
}

void BaseA::show(){
    cout<<"a = "<<this->a<<endl;
    cout<<"b = "<<this->b<<endl;
}

//基类
class BaseB{
public:
    BaseB(int c, int d);
    ~BaseB();
    void show();
protected:
    int c;
    int d;
};

BaseB::BaseB(int c, int d): c(c), d(d){
    cout<<"基类B类构造调用"<<endl;
}

BaseB::~BaseB(){
    cout<<"基类B类构造调用"<<endl;
}

void BaseB::show(){
    cout<<"c = "<<this->c<<endl;
    cout<<"d = "<<this->d<<endl;
}

//派生类
class Derived: public BaseA, public BaseB{
public:
    Derived(int a, int b, int c, int d, int e);
    ~Derived();
public:
    void display();
private:
    int e;
};

Derived::Derived(int a, int b, int c, int d, int e): BaseA(a, b), BaseB(c, d), e(e){
    cout<<"派生类构造调用"<<endl;
}

Derived::~Derived(){
    cout<<"派生类析构调用"<<endl;
}

void Derived::display(){
    BaseA::show(); //调用BaseA类的show()函数
    BaseB::show(); //调用BaseB类的show()函数
    cout<<"e = "<<this->e<<endl;
}

int main(){
    Derived obj(1, 2, 3, 4, 5);
    obj.display();
    return 0;
}
image.png

同名隐藏规则——解决同名二义的方法
1、当派生类与基类有同名成员时,派生类中的成员将屏蔽基类中的同名成员。
2、若未特别指明,则通过派生类对象使用的都是派生类中的同名成员;
3、如要通过派生类对象访问基类中被屏蔽的同名成员,应使用基类名限定(::)。

同名二义性的解决方法
解决方法一:用类名限定(::)
解决方法二:同名覆盖,派生类中再声明一个同名成员函数show(),该函数根据需要调用A::show() 或B::show()


12.3.2 路径二义性
image.png

为了解决路径二义性问题,引入虚基类。

用于有共同基类的多继承场合(多层共祖)

以virtual修饰说明共同的直接基类

例:class B1: virtual public B{...};

作用
1、用来解决多继承时可能发生的对同一基类继承多次和多层而产生的二义性问题.
2、为最远的派生类提供唯一的基类成员,而不重复产生个副本。
注意:
在第一级继承时就要将共同基类设计为虚基类。

虚基类举例

class B { public: int b;};
class B1 : virtual public B { private: int b1;};
class B2 : virtualpublic B { private: int b2;};
class C: public B1, public B2{ private: float d;};

在子类对象中,最远基类成分是唯一的。于是下面的访问是正确的:

C cobj;
cobj.b;

使用最远基类成员原则

//使用最远基类成员原则
#include <iostream>
using namespace std;
class B0//声明基类B0
{ 
public://外部接口
    int nV;
    void fun(){cout<<"Member of B0"<<endl;}
};
class B1: virtual public B0 //B0为虚基类,派生B1类
{ 
    public://新增外部接口
    int nV1;
};
class B2: virtual public B0 //B0为虚基类,派生B2类
{ 
    public://新增外部接口
    int nV2;
};
 
class D1: public B1, public B2//派生类D1声明
{
    public://新增外部接口
    int nVd;
    void fund(){cout<<"Member of D1"<<endl;}
};
int main()//程序主函数
{
    D1 d1;//声明D1类对象d1
    d1.nV=2;//使用最远基类成员
    d1.fun();
        return 0;
}
image.png

有虚基类时的构造函数的调用次序:
1、无论虚基类与产生对象的派生类相隔多远,首先调用虚基类的构造函数;
2、然后按继承次序调用直接基类的构造函数;
3、如果有包含的对象,再按声明次序调用所包含对象类的构造函数;
4、最后才是普通类型数据成员的初始化。

//有虚基类时的构造函数举例
#include <iostream>
using namespace std;
class B0//声明基类B0
{ 
    public://外部接口
    B0(int n){ nV=n;cout<<"B0's constructor called \n";}
    int nV;
    void fun(){cout<<"Member of B0"<<endl;}
};
class B1: virtual public B0
{ 
public:
    B1(int a) : B0(a) {cout<<"B1's constructor called \n";}
    int nV1;
};
class B2: virtual public B0
{ 
public:
    B2(int b) : B0(b) {cout<<"B2's constructor called \n";}
    int nV2;
};
class D1: public B1, public B2
{
public:
    D1(int c) : B0(c), B1(c), B2(c),b1(c),b2(c) {cout<<"D1's constructor called \n"; }
    int nVd;
    void fund(){cout<<"Member of D1"<<endl;}
private:
    B1 b1;
    B2 b2;
};
int main()
{
    D1 d1(1);
    d1.nV=2;
    d1.fun();
        return 0;
}
image.png
12.3.3 二义性的发生场合:

1.带默认形参值的函数与同名的重载函数相遇时;
2.继承时的同名二义;
3.多层共祖的路径二义;
4.形实结合时的类型兼容;

二义性的解决方式:
1、类名限定
2、同名覆盖
3、虚基类

上一篇 下一篇

猜你喜欢

热点阅读