目录
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、在使用虚基类的情况下,构造父类的顺序问题
在派生类对象的创建中
- 首先是虚基类的构造函数并按它们声明的顺序构造。
- 第二批是非虚基类的构造函数按它们声明的顺序调用。
- 第三批是成员对象的构造函数。
- 最后是派生类自己的构造函数被调用.
例如:
#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对象的内存布局
虚基类表