0%

深度探索C++对象模型之function语意学

4章:Function语意学

4.1 静态成员函数

function的调用方式

nonmember functionstatic member functionnonstatic member function的调用效率完全一样,因为,在内部都被编译器处理成相同的形式。而virtual member function的调用需要通过vptr所指向的virtual table,因此,效率有所降低。

static member function的主要特性就是它没有this指针。以下的次要特性统统根源于其主要特性:

  • 它不能够直接存取其class中的nonstatic members
  • 它不能够被声明为constvolatilevirtual
  • 它不需要经由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类型的pointerreference,寻址出一个derived class object的意思。多态机能体现在通过pointerreference对虚函数的调用身上。因此,识别一个class是否支持多态,唯一适当的方法就是看看它是否有任何virtual function

为了支持多态,需要在执行期决议出正确的virtual function实例,这需要如下执行期信息的支持:

  1. 它所引用的对象的地址,也就是当前它自身的值;
  2. 所引用对象的真实类型。这可使我们选择正确的虚函数所在的实体;
  3. virtual function实体位置,也就是函数地址,以便我能够调用它。

在实现上,在每一个多态的class object身上增加两个member

  1. 一个字符串或数字,表示class的类型;
  2. 一个指针,指向某表格,表格中带有程序的virtual function的执行期地址。

virtual function的地址是固定不变的,执行期不可能新增或替换,而表格的大小和内容在执行期不会改变,因此其建构和存取皆在编译期就可以完成。

为了找到virtual function的地址,需要:

  1. 为了找到表格,每一个class object被安插上一个由编译器内部产生的指针,指向该表格;
  2. 为了找到函数地址,每一个virtual function被指派一个表格索引值。

这些工作都由编译器完成。执行期要做的,只是在特定的virtual table slot(记录着virtual function的地址)中调用virtual function。这些virtual function可以是:

  1. 这个class所定义的函数实体。它override了一个base class virtual function函数实体;
  2. 继承自base class的函数实体。这是在derived class中决定不overridevirtual function时的情况;
  3. 一个pure_virtual_called()函数实体。它既可以扮演pure virtual function的空间保卫者角色,也可以当做执行期异常处理函数(有时候会用到)。

单一继承

例如,对于如下的单一继承体系

image-20210312160159297

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

image-20210312160324194

image-20210312160340269

此时,一共有三种可能性:

  1. 它可以继承base class所声明的virtual function的函数实体。正确地说,是该函数实体的地址会被拷贝到derived classvirtual table相对应的slot之中;
  2. 它可以使用自己的函数实体。这表示它自己的函数实体地址必须放在对应的slot之中;
  3. 它可以加人一个新的virtual function。这时候virtual table的尺寸会增大一个slot,而新的函数实体地址会被放进该slot之中。

Point2dvirtual tableslot 1中指出destructor,而在slot 2中指出mult()取代pure virtual function。它自己的y()函数实体地址放在slot 3,继承自Pointz()函数实体地址则放在slot 4

类似的情况:

image-20210312160431103

Point3dvirtual table中的slot 1放置Point3ddestructorslot 2放置Point3d::mult()函数地址。slot 3放置继承自Point2dy()函数地址,slot 4放置自己的z()函数地址。

这个继承体系中的三个类的virtual table布局如下所示:

image-20210312161401843

现在,如果我们有这样的式子:

1
ptr->z();

那么,我如何有足够的知识在编译时期设定virtual function的调用呢?

  1. 一般而言,我并不知道ptr所指对象的真正类型。然而我知道,经由ptr可以存取到该对象的virtual table
  2. 虽然我不知道哪一个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
2
3
(origin.*coord)();
// 或
(ptr->*coord)();

这些操作会被编译器转化为:

1
2
3
(coord)(&origin);
// 和
(coord)(ptr);

获得该函数在内存中的地址。然而面对一个virtual function,其地址在编译时期是未知的,所能知道的仅是virtual function在其相关之virtual table中的索引值,也就是说,对一个virtual member function取其地址,所能获得的只是一个索引值。

那么问题来了,假设我们有以下的Point声明:

1
2
3
4
5
6
7
class Point
{
public:
// ...
virtual ~Point();
virtual float z();
};

z()函数的地址得到的索引值是2,而不是函数地址。

1
2
float (Point::*pmf)() = &Point::z;
Point *ptr = new Point3d;

那么如果通过pmf来间接调用z()函数的话:

1
2
3
4
5
(ptr->*pmf)();

// 转化为

(*ptr->vptr[(int)pmf]5(ptr);

那么如何知道pmf指向的是virtual function还是nonvirtual function,毕竟pmf如果对nonvirtual function取地址的话得到的是在内存中的地址。也就是说,pmf的内部定义需要允许该函数能够寻址出nonvirtualvirtual两个member function

同时为了让执行member function的指针也能支持多重继承和虚拟继承,实现方法为使用一个结构体:

1
2
3
4
5
6
7
8
9
10
11
// 用以支持在多重继承之下指向 member function 的指针
struct __mptr
{
int delta;
int index; // virtual 函数 在 virtual table 的索引, 当 index 不指向 virtual table 时值为 -1
union
{
ptrtofunc faddr; // nonvirtual 函数的地址
int voffset;
};
};

在这样的模型下:

1
2
3
4
5
(ptr->*pmf)();

// 转化为

(pmf.inidex < 0) ? (*pmf.faddr)(ptr) : (*ptr->vptr[pmf.index](ptr));
4.4 inline函数