三、多继承情况下:钻石继承、虚拟继承与虚基类

目录

 

1、来自不同父类的相同成员的访问问题

2、多继承中的钻石继承问题

3、钻石继承问题的解决方法

4、在使用虚基类的情况下,构造父类的顺序问题


1、来自不同父类的相同成员的访问问题

多继承情况下,来自不同父类的相同成员的访问问题 :假设子类继承自2个父类,这两个父类中包含有同名的(数据)成员n,这时通过对象直接来访问该(数据)成员n时会出现什么问题?

从继承的角度讲,该数据成员将分别拷贝到子类中,那么子类中将有两个同名但内存地址不同的n。这样,在访问该成员时,系统将不知对象要访问的是哪个基类的成员n(二义性,标识名n不唯一)。
解决问题的方法:通常采用作用域分辨符“::”进行唯一性标识。格式:

基类名::成员名;   //数据成员
基类名::成员名(参数表); //函数成员

例如:

#include<iostream>
using namespace std;

class B1
{
public:
	B1():n(0)
	{
	}
private:
	int n;
};

class B2
{
public:
	B2():n(0)
	{
	}
public:
	int n;  //也有成员n 
};
class D:public B1,public B2
{
public:
	D():x(0)
	{
	}
public:
	int x;
};
int main()
{
	D d;
//	d.n=100  //这种情况下,系统会不知对象要访问哪个基类的成员n 
//    d.B1::n=100; //正确写法,确保标识符的唯一性 
//    d.B2::n =200; //两个n的地址不同
	return 0;
}

2、多继承中的钻石继承问题

钻石继承:形如子类D继承自B1和B2,而B1和B2分别继承自B0,从继承的图示上看是个菱形结构,类似钻石,故称之钻石继承。

                                                   

这种情况下,若B0的成员身份证号(ID)将分别拷贝给B1和B2,那么D也会继承这2个同名的ID成员,当然,通过作用域限定符可以访问B1和B2中的ID,但是,ID号本身应该唯一,没有必要分别给B1和B2中各拷贝一份,ID完全可以共享使用,要实现这样的目的,需要用到虚拟继承的方法。

3、钻石继承问题的解决方法

使用虚拟继承(virtual)和虚基类,从不同路径继承来的同名数据成员在内存中就只有一个拷贝,同名函数也只有一种映射,使得子类都去共享基类中的成员。这种继承方式称为虚拟继承,被继承的类称为虚基类。
格式:

    class 派生类名:virtual  访问限定符  基类类名{...};
或
    class 派生类名:访问限定符  virtual  基类类名{...};

例如:

#include<iostream>
using namespace std;
class B0
{
public:
	B0():ID(0)
	{
	}
public:
	int ID;
};
class B1:virtual public B0 //虚基类
{
public:
	B1():n(0)
	{
	}
private:
	int n;
};

class B2:virtual public B0 //虚基类
{
public:
	B2():n(0)
	{
	}
public:
	int n;  //也有成员n 
};
class D: public B1, public B2
{
public:
	D():x(0)
	{
	}
public:
	int x;
};
int main()
{
	D d;
    d.B1::ID=100;  //ID的位置上放的是指针,两个指针都指向ID成员存储的内存
    d.B2::ID =200;
    d.ID=300; //此时,内存中只有1个ID的拷贝,具有唯一性,不需再使用:: 
	return 0;
}

4、在使用虚基类的情况下,构造父类的顺序问题

在派生类对象的创建中

  1. 首先是虚基类的构造函数并按它们声明的顺序构造。
  2. 第二批是非虚基类的构造函数按它们声明的顺序调用。
  3. 第三批是成员对象的构造函数。
  4. 最后是派生类自己的构造函数被调用.

例如:

#include<iostream>
using namespace std;


class Base1
{
public:
	Base1(int d1):x1(d1)
	{
		cout<<"构造Base1"<<endl;
	}
	~Base1()
	{
		cout<<"析构Base1"<<endl;
	}
private:
	int x1;
};
class Base2
{
public:
	Base2(int d2):x2(d2)
	{
		cout<<"构造Base2"<<endl;
	}
	~Base2()
	{
		cout<<"析构Base2"<<endl;
	}
private:
	int x2;
};
class Base3
{
public:
	Base3(int d3):x3(d3)
	{
		cout<<"构造Base3"<<endl;
	}
	~Base3()
	{
		cout<<"析构Base3"<<endl;
	}
private:
	int x3;
};
class D:public Base1,public virtual Base2,public virtual Base3  //virtual
{
public:
	D(int data):Base1(data),Base2(data),Base3(data),d1(data),d2(data),d3(data)/*因为基类中没有缺省构造函数,
所以此时的data相当于3个基类中的d1,d2,d3*/
	{
		cout<<"构造D"<<endl;
	}
	~D()
	{
		cout<<"析构D"<<endl;
	}
private://按它们在类定义中声明的先后顺序,顺序调用父类的构造函数 
	
	Base3 d3;
	Base2 d2;
	Base1 d1;
 } ;
 int main()
 {
 	D d(10);//实例化d对象,将先调动父类的构造函数构造3个父类对象,则,d对象的数据成员包含了x1,x2,x3,y 
 	return 0;
  } 

运行结果:

派生类D的对象的内存布局

为了方便我画成下图

vbptr指的是虚基类表指针(virtual base table pointer),该指针指向了一个虚表(virtual table),虚表中第一项记录了vbptr相对于当前作用域偏移地址;第二项是vbptr相对于共有基类元素之间的偏移量

vbtable 中:
0 当前最近作用域的偏移 - vbptr的偏移
1 虚继类数据的起始偏移 - vbptr的偏移

此时vbtable指向的虚基类表如下

这里的-4指的是vbptr相对于当前作用域偏移地址,即0-4=-4,16指的是共有基类元素x2与vbptr之间的偏移量16=20-4,20指的是共有基类元素x3与vbptr之间的偏移量20=24-4

例子:

#include <iostream>  

class A
{
public:
	int dataA;
};

class B : virtual public A
{
public:
	int dataB;
};

class C : virtual public A
{
public:
	int dataC;
};

class DD : public B, public C
{
public:
	int dataD;
};

int main()
{
	DD d;
	return 0;
}

d对象的内存布局

虚基类表

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值