C++多重继承的二义性问题怎么解决?虚继承详解

 更新时间:2026年08月31日 09:58:31   作者:凉夏465  
本文详细讲解C++多重继承的构造函数调用顺序、二义性问题的成因与解决方法,重点介绍虚继承如何消除菱形继承中的数据冗余和命名冲突,并通过iostream案例说明虚继承的实际应用,无论是初学者还是进阶开发者,都能从中掌握多重继承的注意事项与最佳实践

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

语法:

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等,都不支持多继承。

以上为个人经验,希望能给大家一个参考,也希望大家多多支持脚本之家。

相关文章

  • C语言实现Fibonacci数列递归

    C语言实现Fibonacci数列递归

    这篇文章主要介绍了C语言实现Fibonacci数列递归,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价值,需要的朋友们下面随着小编来一起学习学习吧
    2020-02-02
  • C++ Boost EnableIf函数使用介绍

    C++ Boost EnableIf函数使用介绍

    Boost是为C++语言标准库提供扩展的一些C++程序库的总称。Boost库是一个可移植、提供源代码的C++库,作为标准库的后备,是C++标准化进程的开发引擎之一,是为C++语言标准库提供扩展的一些C++程序库的总称
    2022-11-11
  • Qt私有信号实现(private signal)

    Qt私有信号实现(private signal)

    在使用Qt信号槽机制的时候,有时候我们需要一个信号只能由类内发出,而不允许使用该类对象的用户发出,此时就需要私有信号的支持,本文主要介绍了Qt私有信号实现(private signal),感兴趣的可以了解一下
    2023-10-10
  • C语言中数组作为函数的参数以及返回值的使用简单入门

    C语言中数组作为函数的参数以及返回值的使用简单入门

    这篇文章主要介绍了C语言中数组作为函数的参数以及返回值的使用简单入门,这里以一维数组作为基本条件进行例子讲解,需要的朋友可以参考下
    2015-12-12
  • C++的get()函数与getline()函数使用详解

    C++的get()函数与getline()函数使用详解

    这篇文章主要介绍了C++的get()函数与getline()函数使用详解,是C++入门学习中的基础知识,需要的朋友可以参考下
    2015-09-09
  • VSCode下.json文件的编写之(1) linux/g++ (2).json中参数与预定义变量的意义解释

    VSCode下.json文件的编写之(1) linux/g++ (2).json中参数与预定义变量的意义解释

    这篇文章主要介绍了VSCode下.json文件的编写之(1) linux/g++ (2).json中参数与预定义变量的意义解释,本文给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友可以参考下
    2020-03-03
  • C++初阶教程之类和对象

    C++初阶教程之类和对象

    C++是面向对象编程的,这也是C++与C语言的最大区别,而类和对象就是C++面向对象的基础,下面这篇文章主要给大家介绍了关于C++初阶教程之类和对象的相关资料,需要的朋友可以参考下
    2022-02-02
  • C语言轮转数组的三种实现

    C语言轮转数组的三种实现

    轮转数组是一种将数组元素循环移动的处理方式,它通常用于解决一些需要对固定长度的数组进行循环滚动处理的问题,本文就介绍了C语言轮转数组的三种实现,感兴趣的可以了解一下
    2023-08-08
  • C++实现图的邻接矩阵存储和广度、深度优先遍历实例分析

    C++实现图的邻接矩阵存储和广度、深度优先遍历实例分析

    这篇文章主要介绍了C++实现图的邻接矩阵存储和广度、深度优先遍历,实例分析了C++实现图的遍历技巧,非常具有实用价值,需要的朋友可以参考下
    2015-04-04
  • C语言 动态内存分配的详解及实例

    C语言 动态内存分配的详解及实例

    这篇文章主要介绍了C语言 动态内存分配的详解及实例的相关资料,需要的朋友可以参考下
    2016-09-09

最新评论