单继承
#include <iostream>
#define P(x) std::cout<<x<<std::endl;
class A
{
public:
A(){}
~A(){}
virtual void func(){ P("A func call"); }
virtual void funcA(){ P("A funcA call"); }
int a = 10;
};
class B : public A
{
public:
B(){}
~B(){}
virtual void func(){ P("B func call"); }
virtual void funcB(){ P("B funcB call"); }
int b = 20;
};
void main()
{
A* t1 = new A();
A* t2 = new B();
t1->func();
t2->func();
}
根据多态的性质我们知道此时输出是:
A func call
B func call
先看看A对象的内存布局(本例中的测试程序都是32位的)
根据这个内存布局和指针t1来调用到func和funcA以及如何访问成员变量a(这里强调的是不是通过指针的->操作符来调用,而是通过对象的基地址来获取到对应函数和成员变量的地址来访问)
先看一下获取成员变量a。a的地址是对象的基地址下移了一个虚函数表地址的大小,也就是4个字节。改一下main函数,看下面的代码:
void main()
{
A* t1 = new A();
A* t2 = new B();
t1->func();
t2->func();
int A_a = *((int*)t1 + 1);
P(A_a);
}
上面的A_a就是t1的成员变量a的值,大家看一下输出就知道了。将指针t1强转成int*类型,加一后相当于偏移了4个字节,也就是偏移一个虚表地址的大小,这时候指向的是成员变量a的地址,取地址内容就是成员变量a的值。
接下来看一下通过t1指针调用func和funcA两个虚函数。我们先上代码
void main()
{
A* t1 = new A();
A* t2 = new B();
typedef void(*FunPtr)();
auto A_func = FunPtr(*((int*)(*(int*)t1)));
A_func();
auto A_funcA = FunPtr(*((int*)(*(int*)t1) + 1));
A_funcA();
}
A_func和A_funcA分别调用的是A::func和A::funcA。这里具体说一下怎么获取到的:
auto A_func = FunPtr(*((int*)(*(int*)t1)));
(*(int*)t1) //获取虚函数表的地址值。
((int*)(*(int*)t1)) //取虚函数表中第一个元素的地址
*((int*)(*(int*)t1)) //获取虚函数表中第一个元素的值,也就是A::func的函数地址,并强转成函数地址并
调用
auto A_funcA = FunPtr(*((int*)(*(int*)t1) + 1));
//跟上面相比只是这里是获取虚函数表第二个元素的地址,并去第二个元素的值也就是A::funcA的函数地址,并
最终调用到A::funcA。结合着内存分布应该很容易理解
下面继续讲B内存分布。我们先看B对象的内存布局
B的对象的虚函数表中,其中func函数是被重载了的,所以地址被替换成的B::func的地址,其他的两个虚函数位置不变。根据前面的将的规则很容易通过t2指针访问对象的虚函数和成员变量,下面给出代码,分析如上所示:
void main()
{
A* t1 = new A();
A* t2 = new B();
typedef void(*FunPtr)();
auto B_func = FunPtr(*((int*)(*(int*)t2)));
B_func();
auto B_funcA = FunPtr(*((int*)(*(int*)t2) + 1));
B_funcA();
auto B_funcB = FunPtr(*((int*)(*(int*)t2) + 2));
B_funcB();
auto B_a = *(((int*)t2) + 1);
P(B_a);
auto B_b = *(((int*)t2) + 2); //严格意义上说这里的加2是特殊处理的。改成auto B_b = *((int*)((char*)t2 + sizeof(A)))会更严谨
P(B_b);
}
//输出
B func call
A funcA call
B funcB call
10
20
这里额外说一个知识点。在类的构造函数中调用虚函数是不会有多态的效果。具体的分析请看这篇博客
多重继承
其实单继承是多重继承的一个特例。我们看下面多重继承的代码:
#include <iostream>
#define P(x) std::cout<<x<<std::endl;
class A
{
public:
A(){}
~A(){}
virtual void func(){ P("A func call"); }
virtual void funcA(){ P("A funcA call"); }
int a = 10;
};
class B
{
public:
B(){}
~B(){}
virtual void func(){ P("B func call"); }
virtual void funcB(){ P("B funcB call"); }
int b = 20;
};
class C : public A, public B
{
public:
C(){}
~C(){}
virtual void func(){ P("C func call"); }
virtual void funcC(){ P("C funcC call"); }
int c = 30;
};
void main()
{
C* t1 = new C();
A* t2 = t1;
B* t3 = t1;
}
下面我们直接来说一下C的内存布局:
看看内存布局就知道怎么获取对象的虚函数和成员变量,下面给出具体的代码:
void main()
{
C* t1 = new C();
typedef void(*FunPtr)();
auto C_func = FunPtr(*((int*)(*(int*)t1)));
C_func();
auto C_funcA = FunPtr(*((int*)(*(int*)t1) + 1));
C_funcA();
auto C_funcC = FunPtr(*((int*)(*(int*)t1) + 2));
C_funcC();
int* b_addr = (int*)((char*)t1 + sizeof(A));
auto BC_func = FunPtr(*((int*)(*b_addr)));
BC_func();
auto BC_funcB = FunPtr(*((int*)(*b_addr) + 1));
BC_funcB();
int C_a = *((int*)t1 + 1);
P(C_a);
int C_b = *((int*)((char*)t1 + sizeof(A) + 4));
P(C_b);
int C_c = *((int*)((char*)t1 + sizeof(A) + sizeof(B)));
P(C_c);
}
//输出
C func call
A funcA call
C funcC call
C func call
B funcB call
10
20
30
多重继承第一个父类的虚函数表的地址就成了整个对象的虚函数表的地址(同样适用于单继承),后续的每个父类都有自己的虚函数表地址,子类重载的虚函数会冲掉父类中对应的虚函数地址。
这里需要注意的第一个虚函数表中的包含哪些虚函数的地址。它包含A类中的所有虚函数地址(当然被C重载的虚函数地址要替换掉C类对应的虚函数地址)加上C类中未重载任何父类的虚函数地址之和。针对这个例子就是包含A中的A::func和A::funcA两个虚函数和C中C::funcC这个未重载任何父类的虚函数。而A::func是被C重载的,所以需要替换成C::func。这样就形成了第一个虚函数表。
如果我们把C::funcC换成C::funcB。如下代码:
#include <iostream>
#define P(x) std::cout<<x<<std::endl;
class A
{
public:
A(){}
~A(){}
virtual void func(){ P("A func call"); }
virtual void funcA(){ P("A funcA call"); }
int a = 10;
};
class B
{
public:
B(){}
~B(){}
virtual void func(){ P("B func call"); }
virtual void funcB(){ P("B funcB call"); }
int b = 20;
};
class C : public A, public B
{
public:
C(){}
~C(){}
virtual void func(){ P("C func call"); }
virtual void funcB(){ P("C funcB call"); }
int c = 30;
};
这时候的第一个虚函数表中就不再包含C::funcB,因为它重载了父类B中funcB,会出现B类对应的虚函数表中。而不会出现在第一个虚函数中。
整个对象模型如下所示