第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)(); |