开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第7天,点击查看活动详情
成员函数
Virtual Member Functions
每个声明了虚函数或者继承了有虚函数的类,都会有一个自己的vtbl。同时该类的每个对象都会包含一个vptr去指向该vtbl。虚函数按照其声明顺序放于 vtbl 表中, vtbl 数组中的每一个元素对应一个函数指针。如果子类覆盖了父类的虚函数,将被放到了虚表中原来父类虚函数的位置。
如果 normalize()是一个 virtual member function,那么调用:ptr->normalize();
实际上会被编译器转化为: (*ptr->vptr[1])(ptr);
vptr是指向虚函数表的指针1是表中该函数的索引,ptr表示的是this指针
显示地调用“虚函数”可以压制机制,比如说,point3d::normalized()就不会触发虚拟机制,其行为和非静态成员函数行为一致,直接可以在编译时确定调用类的normalized()。
注意:类的对象调用虚函数是不具有多态性质,只能调用对象本身的虚拟函数版本。
Point3d obj;
obj.normalize();
// 会被编译器转换为: *obj.vptr[1](&obj); 虽然正确,但是没有必要,相当于point3d::normalized(),
// 决议方式类似于非静态成员函数: normalize_7Point3dFv(&obj);
静态成员函数
静态成员函数被转换为非成员函数的方式:
obj.normalized(); // normalize 7Point3dSFv();
ptr->normalized(); // normalize 7Point3dSFv();
// 实际上应该是由类直接调用静态函数,而不是类对象或者类指针
对象调用和指针调用可见完全一样,而且转换之后,不带有任何关乎调用这个静态函数的类信息,这也就说明了为什么不能在静态函数中直接调用“普通类成员”。
静态成员函数的主要特性就是它没有this指针,次要特性:
- 不能够直接存取所属类中的非静态成员函数
- 不能被声明为
const、virtual、volatile。 - 不需要通过类对象来调用
如果获取一个静态成员函数的地址,获得的是这个静态成员函数在内存中的位置,也就是其地址。因为其没有this指针,所以地址类型是“指向非成员函数指针”,而不是“指向类成员函数指针”。即:
&Point3d::object_count();
// 得到一个数值,其类型是 unsigned int (*)();
// 而不是 unsigned int (point3d::* )();
因此,缺乏this指针的静态成员函数,很大程度上等同于非成员函数。
虚函数的继承
对于一个虚函数调用ptr->z();,其中z()是一个虚函数,对于类而言需要什么信息才能让我们在执行期调用正确的z()实例:
ptr所指向对象的真实类型。这可使得我们选择正确的z()实例·z()实例的位置,使得我们可以正确的找到并且调用它
基于上面的要求,因此在每个多态类上增加两个数据成员:
-
一个字符串或者数字,表示
class的类型, -
一个指针,指向虚函数表,表格中持有程序的
virtual function执行期地址虚函数地址,在编译时期就可以确定。
因为类中如果有虚函数,这在编译时期就可以确定,并且获得其地址。这个地址是固定的,在执行期不能改变。因此虚函数表的大小和内容都不会改变,其构建和存取都可以在编译期完成,不需要执行期的参与。
为了能在执行期找到这些地址,分为编译器部分和执行期任务:
-
编译器还做了两个任务:
- 为了找到表格,每个具有虚函数的类对象都被放置了一个由编译器内部产生的指针
vptr,指向这个虚函数表格 - 为了找到地址,每个
virtual function都被指派了一个表格索引值。
- 为了找到表格,每个具有虚函数的类对象都被放置了一个由编译器内部产生的指针
-
执行期任务:传入调用虚函数的对象,在特定的
virtual table slot中激活virtual function,也就是调用这个虚函数。
一个类只有一个虚函数表,每个表中可能含有三种类型的虚函数:
- 此类定义的函数实例。包括重写了父类中的虚函数
- 从父类中继承的且没有重写的虚函数
- 纯虚函数