Q1:非静态成员函数的地址
• 非静态成员函数地址值:
○ 如果该函数是非虚函数,则得到的结果是其在内存中真正的地址
○ 如果该函数是虚函数,则得到的结果是其在虚函数表中的索引值
• 地址不完全性(数据成员的地址与函数成员的地址):
○ 非静态数据成员的地址是其在类的布局中的位置(加1),是一个不完整的值,需要将其绑定在一个类对象上,才能进行存取
○ 非静态函数成员(非虚)的地址是其在内存中的真正地址,但仍是一个不完全地址,需要被绑定在一个类对象上,才能进行调用(因为类成员函数需要使用 this 指针作为传递通道)
• 成员函数指针的定义与使用:
Eg:
class X
{
public:
void func(){};
};
int main()
{
void (X::*pfunc)() = &X::func; //定义并初始化指针
X x;
(x.*pfunc)(); //绑定对象并调用函数指针所指函数。指向“member selection”运算符(.* ->*)
(&x->*pfunc)();
return 0;
}
可以看出,指向成员函数的指针的声明语法,以及指向 “member selection运算符”的指针,其作用均是作为 this 指针的空间保留者。因此,可以理解静态成员的类类型是函数指针,而不是指向成员函数的指针(因为静态成员函数不能绑定某个对象,且其调用不需要 this 指针)
Q2:支持“指向虚拟成员函数”的指针
• 虚拟成员函数的地址值是该函数在虚函数表中的索引值,而普通的成员函数的地址值是该函数在内存中的真正的地址值,当虚拟成员函数与普通成员函数有相同的函数原型时,将带来复杂性,即成员函数指针需要满足以下两个条件:
1. 能持有两种数据(索引值与地址值)
2. 数值可以被区分出是内存地址函数还是索引值
Eg:
class X
{
public:
int normfunc();
virtual int virfunc(); //普通成员函数 normfunc() 与虚拟成员函数 virfunc() 有相同的函数原型
};
//函数指针 pfunc 既可以指向普通成员函数(内存地址),也可以指向虚拟成员函数(索引值)
int(X::*pfunc)() = &X::normfunc;
int(X::*pfunc)() = &X::virfunc;
• 带有限制的解决方法:这两种值被内含在一个普通的指针内,其识别使用以下技巧:(对于调用方式 (ptr->pfunc())(); 进行讨论;其中X x; X ptr = &x; )
(((int)pfunc) & ~127) ? (*pfunc)(ptr) : (*ptr->vptr[(int)pfunc])(ptr);
这种区别方法含有缺陷:继承体系中最多只有128个虚函数,对于 pfunc 中值小于128 的认为是索引值,否则认为是地址值
Q3:在多重继承下, 支持成员函数的指针
• 定义以下结构体来实现让多重继承与虚拟继承支持成员函数的指针:
struct __mptr
{
int delta; // this 指针的 offset 值
int index; // 虚函数表的索引(当 index 不指向虚函数时,会被设为-1)
union
{
ptrtofunc faddr; //非虚成员函数的内存地址
int v_offset; //一个虚基类的 vptr 的位置
};
};
此时调用操作会发生如下变化:
(ptr->*pfunc)();
//转换为如下形式的调用:
(pfunc.index < 0) ? (*pfunc.faddr)(ptr) : (*ptr->vptr[pfunc.index](ptr));
此方法缺点:每一个调用操作都要付出成本,检查其是否为虚函数或非虚函数。
• 微软导入 vcall thunk,在此策略下,避免了上述检查操作,faddr 被指定的要不就是真正的成员函数地址,要不就是 vcall thunk 地址。
• 每个编译器在自身内部根据不同的类特性提供多种指向成员函数的指针形式。如微软:
1. 一个单一继承实例(其中持有 vcall thunk 地址或是函数地址,仅需要 faddr 成员)
2. 一个多重继承实例(其中持有 faddr, delta 两个成员) // delta 提供 this 指针的偏移值
3. 一个虚拟继承实例(其中持有四个成员)