第4章:Function语意学
4.1 静态成员函数
function的调用方式
nonmember function、static member function、nonstatic member function的调用效率完全一样,因为,在内部都被编译器处理成相同的形式。而virtual member function的调用需要通过vptr所指向的virtual table,因此,效率有所降低。
static member function的主要特性就是它没有this指针。以下的次要特性统统根源于其主要特性:
- 它不能够直接存取其
class中的nonstatic members; - 它不能够被声明为
const、volatile或virtual; - 它不需要经由
class object才被调用——虽然大部分时候它是这样被调用的。
若取一个static member function的地址不会得到指向其class member function类型的指针(不是return_type (class_type::*)(parameter_types))。而是一个non-member函数指针(类型为return_type (*)(parameter_types))。
4.2 虚拟成员函数
在C++中,多态表示以一个指向public base class类型的pointer或reference,寻址出一个derived class object的意思。多态机能体现在通过pointer或reference对虚函数的调用身上。因此,识别一个class是否支持多态,唯一适当的方法就是看看它是否有任何virtual function。
为了支持多态,需要在执行期决议出正确的virtual function实例,这需要如下执行期信息的支持:
- 它所引用的对象的地址,也就是当前它自身的值;
- 所引用对象的真实类型。这可使我们选择正确的虚函数所在的实体;
virtual function实体位置,也就是函数地址,以便我能够调用它。
在实现上,在每一个多态的class object身上增加两个member:
- 一个字符串或数字,表示
class的类型; - 一个指针,指向某表格,表格中带有程序的
virtual function的执行期地址。
virtual function的地址是固定不变的,执行期不可能新增或替换,而表格的大小和内容在执行期不会改变,因此其建构和存取皆在编译期就可以完成。
为了找到virtual function的地址,需要:
- 为了找到表格,每一个
class object被安插上一个由编译器内部产生的指针,指向该表格; - 为了找到函数地址,每一个
virtual function被指派一个表格索引值。
这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot(记录着virtual function的地址)中调用virtual function。这些virtual function可以是:
- 这个
class所定义的函数实体。它override了一个base class virtual function函数实体; - 继承自
base class的函数实体。这是在derived class中决定不override的virtual function时的情况; - 一个
pure_virtual_called()函数实体。它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)。
单一继承
例如,对于如下的单一继承体系:

virtual destriucior被赋值slot 1,而mult()被赋值slot 2。此例并没有mult()的函数定义,因为它是一个pure virtual function,所以pure _virtual_called()的函数地址会被放在slot 2中。如果该函数意外地被调用,通常的操作是结束掉这个程序。y()被赋值slot 3而z()被赋值slot 4。x()没有slot,因为x()并非virtual function。


此时,一共有三种可能性:
- 它可以继承
base class所声明的virtual function的函数实体。正确地说,是该函数实体的地址会被拷贝到derived class的virtual table相对应的slot之中; - 它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的
slot之中; - 它可以加人一个新的
virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中。
Point2d的virtual table在slot 1中指出destructor,而在slot 2中指出mult()取代pure virtual function。它自己的y()函数实体地址放在slot 3,继承自Point的z()函数实体地址则放在slot 4。
类似的情况:

Point3d的virtual table中的slot 1放置Point3d的destructor,slot 2放置Point3d::mult()函数地址。slot 3放置继承自Point2d的y()函数地址,slot 4放置自己的z()函数地址。
这个继承体系中的三个类的virtual table布局如下所示:

现在,如果我们有这样的式子:
1 | ptr->z(); |
那么,我如何有足够的知识在编译时期设定virtual function的调用呢?
- 一般而言,我并不知道
ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtual table; - 虽然我不知道哪一个
z()函数实体会被调用,但我知道每一个z()函数地址都被放在slot 4。
这些信息使得编译器可以将该调用转化为:
1 | (*ptr->vptr[4])(ptr); |
在一个单一继承体系中,virtual function机制的行为十分良好,不但有效率而且很容易塑造出模型。但是在多重继承和虚拟继承之中,就呵呵了。
多重继承和虚拟继承
懒得总结了,看是看懂了,乱七八糟的!
4.3 指向Member Function的指针
取一个nonstatic member function的地址,如果该函数是nonvirtual,则得到的结果是它在内存中真正的地址。然而这个值也是不完全的,它也需要被绑定于某个class object的地址上,才能够通过它调用该函数(以参数this指出)。
回顾一下,一个指向member function的指针,其声明语法如下:
1 | double (Point::*pmf)(); |
然后我们可以这样定义并初始化该指针:
1 | double (Point::*coord)() = &Point::x; |
也可以这样指定其值:
1 | coord = &Point::y; |
想调用它,可以这么做:
1 | (origin.*coord)(); |
这些操作会被编译器转化为:
1 | (coord)(&origin); |
获得该函数在内存中的地址。然而面对一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtual function在其相关之virtual table中的索引值,也就是说,对一个virtual member function取其地址,所能获得的只是一个索引值。
那么问题来了,假设我们有以下的Point声明:
1 | class Point |
取z()函数的地址得到的索引值是2,而不是函数地址。
1 | float (Point::*pmf)() = &Point::z; |
那么如果通过pmf来间接调用z()函数的话:
1 | (ptr->*pmf)(); |
那么如何知道pmf指向的是virtual function还是nonvirtual function,毕竟pmf如果对nonvirtual function取地址的话得到的是在内存中的地址。也就是说,pmf的内部定义需要允许该函数能够寻址出nonvirtual和virtual两个member function。
同时为了让执行member function的指针也能支持多重继承和虚拟继承,实现方法为使用一个结构体:
1 | // 用以支持在多重继承之下指向 member function 的指针 |
在这样的模型下:
1 | (ptr->*pmf)(); |