首页
学习
活动
专区
圈层
工具
发布
社区首页 >专栏 >【C++】菱形继承为何会引发二义性?虚继承如何破解?

【C++】菱形继承为何会引发二义性?虚继承如何破解?

作者头像
用户11960591
发布2025-12-23 15:55:15
发布2025-12-23 15:55:15
1500
举报

一、多继承及菱形继承问题

1.1单继承

单继承:⼀个派⽣类只有⼀个直接基类时称这个继承关系为单继承。

第一种情形:

代码语言:javascript
复制
#include<iostream>
#include<string>
using namespace std;

class Person
{
public:
	void print()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
	}
	string _name="张三";
	int _age=18;
};

class Teacher :public Person
{
public:
	string _subject;//科目
	int _id;//职工编号

	void printTeacher()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
		cout << "职工编号:" << _id << endl;

	}
};

class Student :public Teacher
{
public:
	int _num;//学号

	void printStudent()
	{
		cout << "姓名:" << _name << endl;
		cout << "年龄:" << _age << endl;
		cout << "学号:" << _num << endl;
		cout << "科目:" << _subject << endl;

	}
};

int main()
{
	Student s;
	s._name = "李四";  //继承自person
	s._age = 19;      //继承自person
	s._num = 241601;  //继承自person
	s._subject = "数学"; //继承自Teacher
	s.printStudent();
	
	return 0;
}

1.2多继承

多继承: ⼀个派⽣类两个或以上直接基类时称这个继承关系为多继承,多继承对象在内存中的模型是,先继承的基类在前⾯后⾯继承的基类在后⾯派⽣类成员在放到最后⾯。如下图所示:

在这里插入图片描述
在这里插入图片描述

当我们在为学生和教师设计一个共同的基类"Person"时,这种继承结构就会形成菱形继承关系,因其类关系图呈现菱形所以叫菱形继承。 如下图:

代码示例:

代码语言:javascript
复制
class Person
{
public:
	string _name; // 姓名
};

class Student : public Person
{
protected:
	int _num; //学号
};

class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
//Assistant继承了两个类
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};

int main()
{
	// 编译报错:error C2385: 对“_name”的访问不明确
	Assistant a;
	//a._name = "peter";
	
	// 需要显示指定访问哪个基类的成员可以解决二义性问题,但是数据冗余问题无法解决
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";

	return 0;
}
在这里插入图片描述
在这里插入图片描述

通过上面的运行结果我们发现,菱形继承存在两个问题:(1)二义性问题;(2)数据冗余问题。 为什么会产生二义性问题? 其实是因为 Anssistant继承了两个类 ,而这两个类各自都包含了一份Person类, 所以在访问Anssistant_name时编译器不知道是访问TeacherPerson类中的_name还是StudentPerson类中的_name。这就导致了_name的访问不明确。 解决二义性的办法:指定类域(是Teacher类的Person还是Student类的Person) 数据冗余问题很好理解,就是Assistant中包含了两个Person类的信息,正常情况下应该只包含一份,但是这里包含两份所以冗余。 解决冗余的办法:虚继承!

1.3虚继承

有了多继承就可能有菱形继承,而菱形继承又存在二义性和数据冗余等问题。所以C++就引入了虚继承来解决数据的冗余问题。但是菱形虚拟继承它的底层实现比较复杂,性能也会有损失,所以最好不要设计菱形继承。

菱形虚拟继承通过在中间类声明中添加virtual关键字实现,下面来看代码

代码语言:javascript
复制
class Person
{
public:
	string _name; // 姓名
};

class Student :vritual public Person
{
protected:
	int _num; //学号
};

class Teacher :virtual public Person
{
protected:
	int _id; // 职工编号
};
//Assistant继承了两个类
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{

	Assistant a;
	a._name = "peter";//这时候再去访问就不会报错了

	return 0;
}
在这里插入图片描述
在这里插入图片描述

采用虚继承的方式相当于将TeacherStudent类中的Person部分提 取到一个公共空间。因此,通过Assistant对象a访问的_name成员,能够明确指向Person类中的_name。这也解决了数据的冗余性问题

在这里插入图片描述
在这里插入图片描述
1.3.1为什么通过虚继承可以将Person部分成员提取出来?

菱形虚拟继承的原理:

虚继承通过共享基类实例实现成员提取。当使用虚继承时,派生类会包含一个指向共享基类实例的指针(虚基类指针),而非直接嵌入基类成员。这使得不同路径继承的虚基类在最终派生类中指向同一内存地址

代码语言:javascript
复制
         Person
        /      \
       vptr     vptr
      /          \
  Teacher      Student
      \          /
       vptr    vptr
         \    /
       Anssistant

虚继承通过虚基类表(vbtable)实现偏移量管理。 每个包含虚基类的派生类都会生成虚基类表,记录虚基类相对于该派生类起始地址的偏移量。这使得无论通过哪条继承路径访问,都能定位到同一个基类实例。

内存布局:

代码语言:javascript
复制
Anssistant对象内存布局:
+-------------------+
| Teacher成员数据    |
+-------------------+
| Student成员数据    |
+-------------------+
| Anssistant新数据   |
+-------------------+
| Person共享数据     | <-- 所有vptr指向此处
+-------------------+

注意:

  • Person只存在一份
  • 每个虚继承的中间类(Teacher、Student)内部不直接包含Person成员(被提取出来了)而是包含一个指向最后Person共享数据处的指针。
  • Anssistant被实例化时,最终在最后生成一个Person共享数据。
  • 所有通过虚继承的类访问Person时,都会通过指针+偏移量访问到Person的共享数据区,从而访问Person的成员。
1.3.2虚继承的构造初始化顺序
代码语言:javascript
复制
class Person
{
public:
	Person(const char* name)
		:_name(name)
	{
	}
	string _name; // 姓名
};
class Student : virtual public Person
{
public:
	Student(const char* name, int num)
		:Person(name)
		, _num(num)
	{
	}
protected:
	int _num; //学号
};
class Teacher : virtual public Person
{
public:
	Teacher(const char* name, int id)
		:Person(name)
		,_id(id)
	{}
protected:
	int _id; // 职⼯编号
};

class Assistant : public Student, public Teacher
{
public:
	Assistant(const char* name1, const char* name2, const char* name3)
		:Person(name3)
		, Student(name1, 1)
		, Teacher(name2, 2)
	{
	}
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	//这⾥a对象中_name是"张三", "李四", "王五"中的哪⼀个?
	Assistant a("张三", "李四", "王五");
	return 0;
}

虚继承的构造初始化顺序:

  • 虚基类优先初始化Assistant的构造函数中,Person(name3)显式调用了虚基类的构造函数,传入name3("王五")。此时_name被初始化为"王五"
  • 非虚基类初始化 Student(name1, 1)和Teacher(name2, 2)的构造函数中也会调用Person的构造函数,但由于Person是虚基类且已初始化,这些调用会被忽略。

注意: 虚继承场景下,最派生类必须直接初始化虚基类,且虚基类的初始化优先于其他基类。因此_name的值由Assistant构造函数中显式指定的Person(name3)决定。

二、总结

共享基类子对象

  • 虚继承确保在多重继承层次结构中,派生类只包含一个共享的基类子对象。

虚基类指针或引用

  • 编译器会为虚继承的类生成额外的信息(如虚基类表或偏移量),用于在运行时定位共享基类子对象。这通常通过虚基类指针(vptr)或间接寻址实现。

初始化责任转移

  • 虚基类的初始化由最派生类(Most Derived Class)直接完成,而非中间派生类。这与普通继承不同,普通继承中每个中间派生类都需要初始化其直接基类。

内存布局调整

  • 虚继承可能导致类的内存布局发生变化,通常会增加额外的间接层或偏移量信息,以支持共享基类的访问。这会带来一定的运行时开销。

构造函数调用顺序

  • 虚基类的构造函数会在任何非虚基类之前被调用,确保共享基类子对象优先初始化。这一顺序由语言标准严格规定。
本文参与 腾讯云自媒体同步曝光计划,分享自作者个人站点/博客。
原始发表:2025-12-23,如有侵权请联系 cloudcommunity@tencent.com 删除

本文分享自 作者个人站点/博客 前往查看

如有侵权,请联系 cloudcommunity@tencent.com 删除。

本文参与 腾讯云自媒体同步曝光计划  ,欢迎热爱写作的你一起参与!

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 一、多继承及菱形继承问题
    • 1.1单继承
    • 1.2多继承
    • 1.3虚继承
      • 1.3.1为什么通过虚继承可以将Person部分成员提取出来?
      • 1.3.2虚继承的构造初始化顺序
  • 二、总结
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档