C++多态对象模型
本文转自https://blog.csdn.net/ling_hun_pang_zi/article/details/81276898
单继承&多继承
前面介绍的是单继承,即一个类是从一个父类派生而来的。但实际上,常常有这样的情况,一个子类有两个或多个父类,子类从两个或多个父类中继承所需的属性。
- 单继承:一个子类只有一个直接父类。
- 多继承:一个子类有两个或两个以上的父类。
探索虚函数表
每一个具有虚函数的类都叫做多态类。这个虚函数或者是从基类继承来的,或者是自己新增加的。C++编译器必须为每一个多态类至少创建一个虚函数表,它其实就是一个函数指针数组,其中存放着这个类所有的虚函数的地址及该类的类型信息,其中也包括那些继承但未被改写的虚函数。
每一个多态对象都有一个隐含的指针成员,它指向所属类型的vtable,这就是vptr。
单继承的多态对象模型
class Base
{
public:
virtual void f1()
{
cout << "Base::f1" << endl;
}
virtual void f2()
{
cout << "Base::f2" << endl;
}
private:
int _a;
};
class Derived : public Base
{
public:
virtual void f1()
{
cout << "Derived::f1" << endl;
}
virtual void f3()
{
cout << "Derived::f3" << endl;
}
void f4()
{
cout << "Derive::f4" << endl;
}
private:
int _b;
};
打印虚函数的地址
typedef void(*FUNC) ();
void PrintVTable(int* VTable)
{
cout << " 虚表地址>" << VTable << endl;
for (int i = 0; VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
多继承的多态对象模型
class Base1
{
public:
virtual void f1()
{
cout << "Base1::f1" << endl;
}
virtual void f2()
{
cout << "Base1::f2" << endl;
}
private:
int _b1;
};
class Base2
{
public:
virtual void f1()
{
cout << "Base2::f1" << endl;
}
virtual void f2()
{
cout << "Base2::f2" << endl;
}
private:
int _b2;
};
class Derived : public Base1, public Base2
{
public:
virtual void f1()
{
cout << "Derived::f1" << endl;
}
virtual void f3()
{
cout << "Derived::f3" << endl;
}
private:
int _d1;
};
我们可以看出:子类的成员函数被放到了第一个父类的表中。
菱形继承
class A
{
public:
int _a;
};
class B : public A
{
public:
int _b;
};
class C : public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
菱形虚拟继承—— 解决菱形继承的二义性和数据冗余的问题
在类B和类C继承类A的时候加virtual关键字,就是虚继承。
class A
{
public:
int _a;
};
class B : virtual public A
{
public:
int _b;
};
class C : virtual public A
{
public:
int _c;
};
class D : public B, public C
{
public:
int _d;
};
对上面的代码我们求D和B的大小
我们肯定会奇怪为什么是这样,和我们想象的不太一样啊。。。
因为在上面的虚继承图示中,我们可以看到内存中显示的B和C里不仅存储了各自的变量,还各存放了一个地址,这个地址是虚基表的地址,指向虚基表,虚基表里面的后四个字节处存储的是距离A的偏移量(在vs中)。表示向下多少字节处为_a成员。( 虚基表为什么首先不存偏移量,而是在存偏移量之前预留了一个0x0000 0000的位置呢? )所以计算B的大小时,里面会除了B里面自身变量的大小4字节、继承的A里的变量的大小4字节、还有存储的虚基表地址的大小4字节,所以是12字节。D的大小也是这样计算的。
菱形虚拟继承(有虚函数覆盖)
class A
{
public:
virtual void f1()
{}
public:
int _a;
};
class B : virtual public A
{
public:
virtual void f1()
{}
virtual void f2()
{
cout << "B::f2()" << endl;
}
public:
int _b;
};
class C : virtual public A
{
public:
virtual void f1()
{}
virtual void f2()
{
cout << "C::f2()" << endl;
}
public:
int _c;
};
class D : public B, public C
{
public:
virtual void f1()
{
cout << "D::f1()" << endl;
}
virtual void f3()
{
cout << "D::f3()" << endl;
}
public:
int _d;
};
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 1;
d._b = 3;
d._c = 4;
d._d = 5;
PrintVTable((int*)((*(int*)&d)));
PrintVTable((int*)(*((int*)((char*)&d + sizeof(B) - sizeof(A)))));
PrintVTable((int*)(*((int*)((char*)&d + sizeof(D) - sizeof(A)))));
return 0;
}
- 1
- 2
- 3
- 4
- 5
- 6
- 7
- 8
- 9
- 10
- 11
- 12
- 13
- 14
我们可以看出:
1) 每个父类都有自己的虚表。
2) 子类的成员函数被放到了第一个父类的表中。
3) 内存布局中,其父类布局依次按声明顺序排列。
4) 每个父类的虚表中的f1()函数都被覆盖成了子类的f1()。这样做就是为了解决不同的父类类型的指针指向同一个子类实例,而能够调用到实际的函数。
int main()
{
D d;
d.B::_a = 1;
d.C::_a = 1;
d._b = 3;
d._c = 4;
d._d = 5;
cout << sizeof(D) << endl;//36
cout << sizeof(B) << endl;//20
return 0;
}
计算D的大小:有变量的大小,还有虚表指针和虚基表指针的大小,从上图可以看出,共36字节。
总结:
虚基表——偏移量 ——虚继承解决菱形继承的二义性和数据冗余
虚表——虚函数重写——虚函数表——实现多态