探索C++对象模型
前两篇博客主要了解了多态和继承的基础,可是当我们在学习多态和继承的时候,难免会碰到很多关于C++对象模型的问题,例如菱形继承中的数据冗余如何解决,虚基表是如何解决菱形继承中数据冗余问题等,这一篇博客我们以C++中多态与继承为基础,探索C++对象模型。
首先我们先来看看虚函数,虚函数就是在函数名前面加virtual的函数,虚函数可以解决继承的很多问题,而对于多态而言,没有虚函数就没有多态,虚函数存在虚函数表里供对象调用实现多态。
纯虚函数
纯虚函数就是在成员函数的形参后面加上=0,成员函数被声明为纯虚函数
包含纯虚函数的类被叫做**抽象类(接口类)**抽象类不能实例化出对象,只有在派生类中重新定义后,派生类才能实例化出对象。
我们用代码看看纯虚函数
class Person
{
virtual void Display () = 0;
// 纯虚函数
protected :
string _name ;
// 姓名
};
class Student : public Person{};
抽象类是强制重写的一种机制
虚函数与虚函数表
接下来我们来看看虚函数与虚函数表,虚函数是C++实现多态的方式,C++用在父类中实现虚函数,子类完成对虚函数的重写从而完成了类型的灵活调用。
下面是一个含有虚函数的类
class A
{
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
protected:
int num;
};
我们可以看出当我们将一个类中的成员函数声明为虚函数后,会在编译的时候生成一张虚函数表,在内存的表现形式是在A中有一个vfptr指针,这个指针指向虚函数表,而虚函数表中存着这个类中的虚函数。
我们来总结下虚函数的使用规则
- 派生类重写基类的虚函数实现多态,要求函数名、参数列表、返回值完全相同。(协变除外)
- 基类中定义了虚函数,在派生类中该函数始终保持虚函数的特性。
- 只有类的成员函数才能定义为虚函数。
- 静态成员函数不能定义为虚函数。
- 如果在类外定义虚函数,只能在声明函数时加virtual,类外定义函数时不能加virtual。
- 不要在构造函数和析构函数里面调用虚函数,在构造函数和析构函数中,对象是不完整的,可能会发生未定义的行为。
- 最好把基类的析构函数声明为虚函数。(why?另外析构函数比较特殊,因为派生类的析构函数跟基类的析构函数名称不一样,但是构成覆盖,这里是因为编译器做了特殊处理)
- 构造函数不能为虚函数,虽然可以将operator=定义为虚函数,但是最好不要将operator=定义为虚函数,因为容易使用时容易引起混淆.
在看完虚函数后,我们了解虚函数可以在C++中完成重写,然而我们经常将重写,重载,重定义互相混淆,这里做出区分
单继承中的对象模型
接下来我们来看看单继承中的对象模型,在单继承中虚函数表在子类中是如何完成多态调用的
我们先来一段代码看看单继承中的对象模型
class A
{
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
protected:
int num;
};
class C :public A
{
virtual void fun1()
{
cout << "C::fun1()" << endl;
}
virtual void fun3()
{
cout << "C::fun3()" << endl;
}
virtual void fun4()
{
cout << "C::fun4()" << endl;
}
};
这里我们可以预测下,A中实现了虚函数fun2与fun1,C中重写了A的fun1,自己实现了虚函数fun3,fun4我们调用监视窗口看看
可是我们发现在C的对象模型中虚表里面只有两个虚函数,这里其实是编译器的问题
那么我们尝试自己实现一个打印虚表的函数
void PrintVTable(int* VTable)
{
cout << " 虚表地址" << VTable << endl;
for (int i = 0;VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i+1, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
这里实现这个函数的原理是访问数组的方式去访问函数指针,取出虚表首地址后解引用进入虚表打印出每一个虚函数的地址
PrintVTable((int*)(*(int*)(&c)));
这个传参很值得解释下
首先我们拿到b的首地址,将他强转成int类型,让他读取到虚表的地址再然后对那个地址解引用,我们已经拿到虚表的首地址的内容(虚表里面存储的第一个函数的地址)了,但是此时这个变量的类型解引用后是int,不能够传入函数,所以我们再对他进行一个int的强制类型转换,这样我们就传入参数了,开始函数执行了,我们一切都是在可控的情况下使用强转,使用强转你必须要特别清楚的知道内存的分布结构。
多继承的对象模型
接下来我们剖析下多继承的对象模型,这个就没有单继承这个简单了
首先我们实现两个父类,两个父类中都有fun1与fun2,子类继承两个父类,看看子类的对象模型
#include<iostream>
#include<windows.h>
using namespace std;
class A
{
virtual void fun1()
{
cout << "A::fun1()" << endl;
}
virtual void fun2()
{
cout << "A::fun2()" << endl;
}
protected:
int num;
};
class B
{
virtual void fun1()
{
cout << "B::fun1()" << endl;
}
virtual void fun2()
{
cout << "B::fun2()" << endl;
}
protected:
int num;
};
class C :public A,public B
{
virtual void fun1()
{
cout << "C::fun1()" << endl;
}
virtual void fun3()
{
cout << "C::fun3()" << endl;
}
virtual void fun4()
{
cout << "C::fun4()" << endl;
}
};
typedef void(*FUNC)(void);
void PrintVTable(int* VTable)
{
cout << " 虚表地址" << VTable << endl;
for (int i = 0;VTable[i] != 0; ++i)
{
printf(" 第%d个虚函数地址 :0X%x,->", i+1, VTable[i]);
FUNC f = (FUNC)VTable[i];
f();
}
cout << endl;
}
int main()
{
A a;
B b;
C c;
PrintVTable((int*)(*(int*)(&c)));
system("pause");
return 0;
}
这里子类继承了A与B两张虚表,我们依旧是将虚表打印出来看看子类的对象模型
这里我们看到C中的fun1完成了函数的重写,虚表中继承的是A的fun2,这里很明显是继承了A的虚表,所以我们得出结论子类的虚表继承先继承的类的虚表