C++之多继承

本文介绍了C++中的多继承,特别是菱形继承带来的数据冗余和二义性问题。虚拟继承作为解决方案,通过虚基类指针和虚基类表避免了冗余并解决了二义性。此外,还探讨了虚继承在构造函数中的应用及其注意事项。

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

什么叫多继承
多继承就是一个子类有两个及以上的父类。

多继承的方式
一般形式:
在这里插入图片描述
菱形继承:
在这里插入图片描述
第一种继承方式:派生类会同时拥有两个基类的特性(属性和方法,也就是两个类的所有成员)。
第二种继承方式:这个结构中类C同时拥有类B1和类B2的特性,但是这就会产生一个问题,上述两个类都继承自类A,那么类C的对象会同时包含两个A的对象。这样就会带来数据冗余和二义性问题。
菱形继承:

class Person
{
public:
	string _name; // 姓名
};
class Student : public Person
{
protected:
	int _num; //学号
};
class Teacher : public Person
{
protected:
	int _id; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected:
	string _majorCourse; // 主修课程
};
int main()
{
	
	Assistant a;
	// 这样会有二义性无法明确知道访问的是哪一个,Student和Teacher中都有_name
	a._name = "peter";
	// 需要显示指定访问哪个父类的成员可以解决二义性问题,但是数据冗余问题无法解决
	//在类Assistant对象中有两个_name 
	a.Student::_name = "xxx";
	a.Teacher::_name = "yyy";
	return 0;
}

所以这时候需要想一个办法来解决菱形继承中的二义性和数据冗余问题。

虚拟继承
先举个例子:

class Person
{
public :
    string _name ; // 姓名
};
class Student : virtual public Person
{
protected :
    int _num ; //学号
};
class Teacher : virtual public Person
{
protected :
    int _id ; // 职工编号
};
class Assistant : public Student, public Teacher
{
protected :
    string _majorCourse ; // 主修课程
};
void Test ()
{
    Assistant a ; 
    a._name = "peter";
}

在这里插入图片描述
通过上述代码可以看到虚拟继承就是在继承的限定符前加上virtual关键字,最后将两个虚拟继承的类当做最后一个派生类的基类。
综合上述代码和监视图中可以看到,已经不用担心二义性和数据冗余了,因为虚基类(最初要继承的类)中的成员只有一份了,而不存在两个直接派生类中了。

虚拟继承解决数据冗余和二义性的原理
虚继承底层实现原理与编译器相关,一般通过虚基类指针和虚基类表实现,每个虚继承的子类都有一个虚基类指针(占用一个指针的存储空间,4字节)和虚基类表(不占用类对象的存储空间)(需要强调的是,虚基类依旧会在子类里面存在拷贝,只是仅仅最多存在一份而已,并不是不在子类里面了);当虚继承的子类被当做父类继承时,虚基类指针也会被继承。

实际上,有一个虚基类表指针(virtual base table pointer),该指针指向了一个虚基类表(virtual table),虚表中记录了虚基类与本类的偏移地址;通过偏移地址,这样就找到了虚基类成员,而虚继承也不用像普通多继承那样维持着公共基类(虚基类)的两份同样的拷贝,节省了存储空间。

我们借助菱形继承和内存窗口来讲解:

class A
{
public:
	int _a;
};

//class B:public A
class B : virtual public A
{
public:
	int _b;
};
//class C:public A
class C : virtual public A
{
public:
	int _c;
};

class D : public B, public C
{
public:
	int _d;
};

int main()
{
	D d;
	d.B::_a = 1;
	d.C::_a = 2;
	d._b = 3;
	d._c = 4;
	d._d = 5;

	return 0;
}

下图是菱形继承的内存对象成员模型:
在这里插入图片描述
下图是菱形虚拟继承的内存对象成员模型:
在这里插入图片描述
从上图可以分析出D对象中将A放到的了对象组成的最下面,这个A 同时属于B和C,那么B和C如何去找到公共的A呢?这里是通过了B和C的两个指针,指向的一张表。这两个指 针叫虚基表指针,被指向两个表叫虚基表。虚基表中存的偏移量(所指地址的下一个地址存的就是偏移量)。通过偏移量可以找到下面的A。
菱形虚拟继承的原理模型:
在这里插入图片描述
关于菱形虚拟继承和构造函数的问题:

 class A {
 protected:
     int a;
 public:
     A(int a) : a(a) {}
 };
 
 class B : virtual public A {
 protected:
     int b;
 public:
    B(int a, int b) : A(a), b(b) {}
 };
 
 class C : virtual public A {
 protected:
     int c;
 public:
     C(int a, int c) : A(a), c(c) {}
};
 
 class D : public B,  public C {
 private:
     int d;
 public:
     D(int a, int b, int c, int d) : A(a), B(a, b), C(a, c), d(d) {}
     void display();
 };
 
 void D::display()
 {
     cout << "a = " << a << endl;
     cout << "b = " << b << endl;
     cout << "c = " << c << endl;
    cout << "d = " << d << endl;
 }
 
 int main()
 {
    (new D(1, 2, 3, 4))->display(); 
    return 0;                                                                                                           
 }

有两个注意事项:
1.请注意派生类D的构造函数,与以往的用法有所不同。以往,在派生类的构造函数中只需负责对其直接基类初始化,再由其直接基类负责对间接基类初始化。现在,由于虚基类在派生类中只有一份成员变量,所以对这份成员变量的初始化必须由派生类直接给出。如果不由最后的派生类直接对虚基类初始化,而由虚基类的直接派生类(如类B和类C)对虚基类初始化,就有可能由于在类B和类C的构造函数中对虚基类给出不同的初始化参数而产生矛盾。所以规定:在最后的派生类中不仅要负责对其直接基类进行初始化,还要负责对虚基类初始化。
2.类D的构造函数通过初始化表调了虚基类的构造函数A,而类B和类C的构造函数也通过初始化表调用了虚基类的构造函数A,这样虚基类的构造函数岂非被调用了3次?因为在C++编译系统只执行最后的派生类对虚基类的构造函数的调用,而忽略虚基类的其他派生类(如类B和类C)对虚基类的构造函数的调用,这就保证了虚基类的数据成员不会被多次初始化。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值