C++:多重继承带来的问题及解决方法

本文详细讨论了多重继承在C++中的构造函数和析构函数调用顺序,以及它带来的二义性问题,包括继承同名成员和菱形继承。介绍了虚继承作为解决这些问题的方法,以及其在避免数据冗余和消除二义性上的作用。作者强调了多继承的复杂性和不推荐在编程中的广泛使用。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

在继承的过程中,如果一个派生类由多个基类派生,则称为多重继承(实际开发中会引入困难,不建议使用)

目录

多重继承的构造函数、析构函数调用顺序:

多重继承引发的二义性问题:

问题1:继承同名成员:

问题2:菱形继承(钻石继承)

多重继承的解决方法:

1.虚继承(virtual)

2.虚继承的二义性

语法:

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

关系图:

多重继承的构造函数、析构函数调用顺序:

构造函数:顺序与单继承一致,先基类、组合类、派生类新增加成员;

强调:

1、 同一层基类的构造函数顺序取决于派生类声明继承基类时的基类先后顺序,与构造函数定义成员初始化列表顺序无关

class A :public B,public C,public D
//基类的构造函数执行顺序为:B C D
{
public:

A (int d,int c,int b,int a):D(d),C(c),B(b),m_a(a)
{cout << "A(int)构造函数" << endl;}
//与构造函数初始化列表顺序无关

~A()
    {
        cout << "~A()析构函数" <<  endl;
    }
}

输出:

B(int)构造函数

C(int)构造函数

D(int)构造函数

A(int)构造函数

~A()析构函数

~D()析构函数

~C()析构函数

~B()析构函数

2、组合类的成员对象的构造函数执行顺序取决于成员对象的声明顺序,与构造函数的初始化列表排列顺序无关

class A:public B
{
private:
    C c1;
    D d1;
    int a1;
    //取决于成员对象的声明顺序
public:
    //与初始化列表成员顺序无关
    A(int a,int b,int d,int c):a1(a),B(b),c1(c),d1(d)
    {cout << "A(int)构造函数" << endl;}

    ~A()
    {
        cout << "~A()析构函数" <<  endl;
    }
//...
}

输出:

B(int)构造函数

C(int)构造函数

D(int)构造函数

A(int)构造函数

~A()析构函数

~D()析构函数

~C()析构函数

~B()析构函数

析构函数:顺序与构造函数相反,先构造的后继承,形成对称

多重继承引发的二义性问题:

问题1:继承同名成员:

派生类继承多基类的同名成员

eg:

class A
{
public :
void show()
{
    cout<<"A show()"<<endl;}
}

class B
{
public :
void show()
{
    cout<<"B show()"<<endl;}
}

class C
{
public :
void showc()
{
    cout<<"C showc()"<<endl;}
}

引入问题:派生类中出现继承而来的同名成员,导致无法直接引用

int main()
{
    C c;
    //error:
    //c.show()  
    //二义性:不知道此时c 该执行A类的show还是B类的show函数
    return 0;
}

解决方法:添加作用域标识符,消除成员限定的二义性

int main()
{
    C c;
    //解决方法:
    c.A::show();
    c.B::show();
    c.showc();
    return 0;
}

作用域格式:

对象名.基类名::成员变量名;
对象名.基类名::成员函数名(参数列表);

问题2:菱形继承(钻石继承)

两次继承三代成员:由一基类派生了两个第一代派生,而一类又共同继承了这两个一代派生形成二代派生

关系图:

class A //间接基类,第一层的类
{
protected:
    int m_a;
};

class B :public A //直接基类B
{
protected:
    int m_b;
//继承m_a;
};

class C :public A //直接基类C
{
protected:
    int m_c;
//继承m_a;
};

问题引入:

class D:public C,public B
{
public:
void set(int a)
{
 //继承m_a;
    //error:  m_a=a;
    //不知道赋值给B 类的还是C类的m_a;
}
...
    

解决方法:

void set(int a)
{
    B::m_a=a;
    C::m_a=a;
}
protected:
    int m_d;
//继承 m_a 源于B;
//继承 m_b 源于B;
//继承 m_a 源于C;
//继承 m_c 源于C;

};

int main()
{
    D d;

    return 0;
}

注意:

作用域标识符虽然可以解决二义性问题,但是在派生类中产生两个同名成员,增加了内存开销,解决方法间下方虚继承

多重继承的解决方法:

解决多重继承的命名冲突及数据冗余问题,C++中虚继承:使派生类只保留一份间接基类成员

1.虚继承(virtual)

格式:

class      A   : virtual  public    B
class 派生类类名:virtual 继承方式 基类类名

使用引例:

//多重继承 虚继承
#include <iostream>
#include <string>
using namespace std; 

class A
{
public :
	int m_a;
};

class B:virtual public A
{
public:
	int m_b;
	//虚继承 m_a
};

class C :virtual public A
{
public:
	int m_c;
	//虚继承 m_a
};

class D:virtual public A
{
public:
	int m_d;
	//虚继承 m_a
};


class E :public C, public B
{
public :
	int m_e;
	//虚继承 m_a
	//继承 m_b
	//继承 m_c
	void seta(int a) { m_a = a; }//正确
	void setb(int b) { m_b = b; }//正确
	void setc(int c) { m_c = c; }//正确
	void setd(int d) { m_d = d; }//正确
	void setd(int e) { m_e = e; }//正确
};

int main()
{
	E e;
	return 0;
}

代码关系梳理:

虚继承(在继承的基类前加入virtual关键字),可以让声明虚继承的类作出声明,共享其基类(虚基类:Virtual Base Class),使得不论虚基类 class A 被继承了多少次,派生类中只含有一份虚基类成员 m_a,节省内存空间,解决了成员名的歧义问题。

局限:

派生类只影响从指定的虚基类的派生类进一步派生出的后代类,而不会影响一代派生类本身

在实际开发中,位于中间层次的基类将其继承声明为虚继承一般不会带来什么问题。通常情况下,使用虚继承的类层次是由一个人或者一个项目组一次性设计完成的。对于一个独立开发的类来说,很少需要基类中的某一个类是虚基类,况且新类的开发者也无法改变已经存在的类体系。

C++标准库中的 iostream 类就是一个虚继承的实际应用案例。iostream 从 istream 和 ostream 直接继承而来,而 istream 和 ostream 又都继承自一个共同的名为 base_ios 的类,是典型的菱形继承。此时 istream 和 ostream 必须采用虚继承,否则将导致 iostream 类中保留两份 base_ios 类的成员。

using istream  = basic_istream<char, char_traits<char>>;
using ostream  = basic_ostream<char, char_traits<char>>;
using iostream = basic_iostream<char, char_traits<char>>;
class basic_istream  : virtual public basic_ios;
class basic_ostream  : virtual public basic_ios;
class basic_iostream : public basic_istream,
public basic_ostream;

2.虚继承的二义性

虚继承要求在派生类中只包含一份虚基类数据,该数据在直接访问时无二义性问题

以菱形继承为例,判断是否存在其他二义性问题?

  1. 仅A 虚基类中定义成员变量 x, 派生类D 直接访问 A类的x 成员(无二义性问题)
  2. A、B类或 A、C 类中均含有成员变量x ,派生类访问 x 成员时,派生类x 优先级高于虚基类成员 x (无二义性)
  3. A、B、C 中三个类均含有变量x ,派生类D访问时,B、C 类成员优先级一致(存在二义性问题)作用域标识符区分

class A //间接基类
{
protected:
    int x;
};

class B :virtual public A //直接基类B
{
protected:
    int x;
};

class C :virtual public A //直接基类C
{
protected:
    int x;
};

class D :public B, public C //派生类D
{
public:
    void setx(int a) 
    {
        //x = a;//错误,有二义性
        //添加作用域标识符
        B::x = a;//正确
        C::x = a;//正确
    }
};

总结:

可以看到,使用多继承经常会出现二义性问题,必须十分小心。上面的例子是简单的,如果继承的层次再多一些,关系更复杂一些,程序员就很容易陷人迷魂阵,程序的编写、调试和维护工作都会变得更加困难,因此不提倡在程序中使用多继承,只有在比较简单和不易出现二义性的情况或实在必要时才使用多继承,能用单一继承解决的问题就不要使用多继承。也正是由于这个原因,C++ 之后的很多面向对象的编程语言,例如Java、C#、PHP等,都不支持多继承。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值