1.类对象模型的分析
1.1具有虚函数的类对象模型
请问下面这个类的对象占多少字节的内存空间?
class A
{
public:
virtual void Func1()
{
cout << "Func1()" << endl;
}
private:
int _a = 1;
};
答案: 8 字节
这是因为一个类一旦有虚函数,那它的对象中就会多一个指针类型的成员变量,这个指针叫虚函数表指针,这个指针指向了一张表,这张表叫虚函数表,虚函数表中存的是虚函数的地址,所以,虚函数表的本质就是一个函数指针数组;如下图所示:
1.2继承具有虚函数的类 的类对象模型
我们可以借助内存窗口分析类对象模型,如下图所示:
从图中可以看出,当一个类继承一个具有虚函数的类,这个类中也会有一个虚函数指针;但是,这个虚函数指针和父类中的不是同一个虚函数指针;说明,父类和子类各自具有一张虚函数表。并且,在VS环境下,虚函数表是以空结尾的。
因为该测试样例中子类中并没有重写父类中的虚函数,所以父子类的虚函数表中的虚函数地址是一样的,但是,当子类中重写父类的虚函数的时候,重写的虚函数的地址就会被覆盖,所以重写也叫覆盖,重写是语法层的概念,覆盖是原理层的概念。当子类添加新的虚函数的时候,虚函数的地址会添加在虚函数表的后面。但始终是以空结尾。
2.多态的原理
对于普通的类(没有虚函数的类),类对象当中只存储成员变量,成员方法是存储在公共的代码区的,调用的时候是通过函数名修饰规则,在符号表中找到调用的函数的地址进行调用;这种调用是通的调用。
对于具有虚函数的类,类对象中不仅存储了成员变量,还存储了一个虚函数表指针,通过这个指针可以找到独属于这个类的虚函数表;子类重写虚函数,会将父类中被重写的虚函数的地址覆盖;运行时,如果符合多态调用的条件,编译器就会去 所指向对象的虚函数表中 找到对应的虚函数进行调用,这就是多态的原理。
反思一下多态的条件:多态需要两个条件:1.子类中需要完成虚函数的重写。2.通过父类的指针or引用去调用对应的虚函数。
- 通过父类的指针or引用去调用是因为 子类对象的指针or应用可以通过赋值兼容转换,转换成父类对象的指针or引用;通过这个父类的指针or引用去调用对应的虚函数的时候,这个指针or引用只能看到父类的那一部分。
- 完成虚函数的重写是因为 要保证去不同的类的虚函数表中找对应的虚函数。
总结一下:成员函数的调用分为两种调用,一种是多态调用,一种是普通调用。
多态调用:当调用函数的时候,满足多态的条件,就是多态调用;多态调用是运行时去虚函数表中找到函数的地址,进行调用;所以如果指向的对象是父类的对象,调用的是父类对象中的虚函数;指向的是子类,调用的是子类对象中的虚函数。这样就实现了多态。
- 多态调用关注的是所指向对象的类型,指向谁调用谁里面的成员方法。
普通调用:当调用函数的时候,如果不满足多态调用,就是普通调用;普通调用是编译时,通过调用者类型确定函数地址,也就是通过函数名修饰规则,在符号表中找到函数的地址进行调用。
- 普通调用关注的是调用者本身的类型,调用的是该类型中的成员方法。
3.几个疑问
虚函数存在哪?虚函数也是函数,函数编译完之后是一串指令,所以虚函数和普通函数一样是存在代码段的;
虚函数表存在哪?虚函数表的存放位置我们可以验证一下,如下图所示:
可以看出虚函数表的地址离常量区是最近的,所以虚函数表应该是存在常量区的。 虚函数表是在编译时生成的,是存在常量区的。
虚函数表指针存在哪?虚函数表指针是存在对象中的,作为对象的成员变量,在构造函数的初始化列表阶段初始化,并且是第一个初始化的。