class A{
public:
void func();
};
class B{
private:
bool func() const;
};
class C: public A, public B{ ... };
C c;
c.func(); // 歧义!
解决冲突的时候就必须
c.A::func();
class File{};
class InputFile: public File{};
class OutputFile: public File{};
class IOFile: public InputFile, public OutputFile{};
这样的层级在C++标准库中也存在,例如basic_ios, basic_istream, basic_ostream, basic_iostream。
IOFile的两个父类都继承自File,那么File的属性(比如filename)应该在IOFile中保存一份还是两份呢? 这是取决于应用场景的,就File::filename来讲显然我们希望它只保存一份,但在其他情形下可能需要保存两份数据。 C++还是一贯的采取了自己的风格:都支持!默认是保存两份数据的方式。如果你希望只存储一份,可以用virtual继承:
class File{};
class InputFile: virtual public File{};
class OutputFile: virtual public File{};
class IOFile: public InputFile, public OutputFile{};
代价:
对于这些复杂性,建议
这样一个不包含数据的虚基类和Java或者C#提供的interface有很多共同之处,这样的类在C++中称为接口类,一个Person的接口类是这样的:
class IPerson {
public:
virtual ~IPerson();
virtual std::string name() const = 0;
virtual std::string birthDate() const = 0;
};
class Base
{
public:
Base(int i) :baseI(i){};
virtual ~Base(){}
int getI(){ return baseI; }
static void countI(){};
virtual void print(void){ cout << "Base::print()"; }
private:
int baseI;
static int baseS;
};
class Base_2
{
public:
Base_2(int i) :base2I(i){};
virtual ~Base_2(){}
int getI(){ return base2I; }
static void countI(){};
virtual void print(void){ cout << "Base_2::print()"; }
private:
int base2I;
static int base2S;
};
class Drive_multyBase :public Base, public Base_2
{
public:
Drive_multyBase(int d) :Base(1000), Base_2(2000) ,Drive_multyBaseI(d){};
virtual void print(void){ cout << "Drive_multyBase::print" ; }
virtual void Drive_print(){ cout << "Drive_multyBase::Drive_print" ; }
private:
int Drive_multyBaseI;
};
可见内存布局为,Base,Base_2, 然后再是自己的变量。
Base和Base_2中都有自己的虚函数指针,都有的print方法被重写。两个虚函数表都被重写,继承类的虚函数指针跟在Base类的后面,析构函数也是虚函数。
注意x64架构下指针为8个字节,而x86架构下指针为4个字节。
using Fun = void(*)();
int main()
{
Drive_multyBase d(3000);
cout << sizeof(Base) << "\n";
cout << sizeof(d) << "\n";
//[0]
cout << "[0]Base::vptr";
cout << "\t地址:" << (int *)(&d) << endl;
//vprt[0]析构函数无法通过地址调用,故手动输出
cout << " [0]" << "Derive::~Derive" << endl;
//vprt[1]
cout << " [1]";
Fun fun1 = (Fun)*((int *)*((int *)(&d)) + 1);
fun1();
cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 1) << endl;
//vprt[2]
cout << " [2]";
Fun fun2 = (Fun)*((int *)*((int *)(&d)) + 2);
fun2();
cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 2) << endl;
//[1]
cout << "[1]Base::baseI=" << *(int*)((int *)(&d) + 1);
cout << "\t地址:" << (int *)(&d) + 1;
cout << endl;
//[2]
cout << "[2]Base_::vptr";
cout << "\t地址:" << (int *)(&d) + 2 << endl;
//vprt[0]析构函数无法通过地址调用,故手动输出
cout << " [0]" << "Drive_multyBase::~Derive" << endl;
//vprt[1]
cout << " [1]";
Fun fun4 = (Fun)*((int *)*((int *)(&d)) + 1);
fun4();
cout << "\t地址:\t" << *((int *)*((int *)(&d)) + 1) << endl;
//[3]
cout << "[3]Base_2::base2I=" << *(int*)((int *)(&d) + 3);
cout << "\t地址:" << (int *)(&d) + 3;
cout << endl;
//[4]
cout << "[4]Drive_multyBase::Drive_multyBaseI=" << *(int*)((int *)(&d) + 4);
cout << "\t地址:" << (int *)(&d) + 4;
cout << endl;
getchar();
}
在x86下可以执行,因为指针和int占的内存相同,但在x64下不可以执行。
x86下sizeof(d)为20字节,x64下sizeof(d)为40字节,因为虚函数指针为8字节,然后内存对齐。
这时候D中有两个ib, 有两个ib2,在调用的时候,要加上类::来消除二义性
D d;
d.ib =1 ; //二义性错误,调用的是B1的ib还是B2的ib?
d.B1::ib = 1; //正确
d.B2::ib = 1; //正确
内存布局如图所示,无法访问内存是因为存在二义性?VS日常抽风。
x86下sizeof是28 x64下sizeof是56.
在C++对象模型中,虚继承而来的子类会生成一个隐藏的虚基类指针(vbptr), **在VS中,虚基类指针总是在虚函数表指针之后,**因而,对某个类实例来说,如果它有虚基类指针,那么虚基类指针可能在实例的0字节偏移处(该类没有vfptr时,vbptr就处于类实例内存布局的最前面,否则vptr处于类实例布局的最前面),也可能在类实例的4字节偏移处。
一个类的虚基类指针指向的虚基类表,与虚函数表一样,虚基类表也由多个条目组成,条目中存放的偏移值,第一个条目存放虚基类所在地址到该类内存首地址的偏移值,由第一段的分析我们知道,这个偏移值为0(类没有vptr)或者-4(类有虚函数,此时有vptr)。
虚基类表的第二、第三…个条目依次为该类的最左虚继承父类、次左虚继承父类…的内存地址相对于虚基类表指针的偏移值,这点我们在下面会验证。
虚继承的精彩在于引入虚基类指针,虚基类指针记录了虚基类表指针(vbptr)所在地址到该类内存首地址的偏移值,这样保证了虚基类在子类中只出现一次。这样做,可以解决二义性,但时间和空间效率上都会受到较大的影响。
将上面的改为虚继承
其中虚基类指针跟在b1的__Vfptr后面。
#include
using namespace std;
class A //大小为4
{
public:
int a;
};
class B :virtual public A //大小为12,变量a,b共8字节,虚基类表指针4
{
public:
int b;
};
class C :virtual public A //与B一样12
{
public:
int c;
};
class D : virtual public B, public C //24,变量a,b,c,d共16,B的虚基类指针4,C的虚基类指针
{
public:
int d;
};
int main()
{
A a;
B b;
C c;
D d;
d.a = 1;
cout << sizeof(a) << endl;
cout << sizeof(b) << endl;
cout << sizeof(c) << endl;
cout << sizeof(d) << endl;
system("pause");
return 0;
}
x64下
貌似就是B,C会有虚基类指针,来记录偏移量,但D不会有,所以才会出现这个结果,虚基类指针对debug是隐藏的,也看不到,这样看上去A似乎存在D上,B,C上的A只是指针。
但在b中,却又是实实在在的实体,这时指针被忽略了
在这个例子中,貌似又没有那个补充的0了。
也许是因为没有虚函数指针吧。
在虚基类继承中,有虚函数的条件下会多加一个0,没有虚函数的条件下不会多加一个0.
而且内存布局是倒置的,先有B1,再有B基类在最下面,呵呵哒~
所幸调用顺序还是对的哈