这篇博客开始介绍《深度探索C++对象模型》第四章的剩余部分,包括成员函数指针和内联函数。
1. 成员函数指针
对于静态成员函数,其和常规的函数是一样的,故这里不做介绍。下面主要介绍非静态的成员函数指针,包括普通的非virtual成员函数指针和virtual成员函数指针。
注意,这篇是按照《深度探索C++对象模型》的内容写的,最后讲到支持多继承的成员函数指针时才会给出真正的成员函数指针的实现!
1.1 非virtual成员函数指针
对于一个非virtual的成员函数取址,得到的就是该成员函数在内存中的地址,但是它不能单独调用,需要使用其绑定的对象/指针/引用调用。
// test26.cpp
class Test {
public:
Test(int i)
: m_i(i)
{}
int getInt() const {
return m_i;
}
void setInt(int i) {
m_i = i;
}
private:
int m_i;
};
int main() {
Test t(1);
int i = t.getInt();
void (Test::*pMemberFunc)(int) = nullptr; // 成员函数指针
pMemberFunc = &Test::setInt;
(t.*pMemberFunc)(2);
i = t.getInt();
}
1.2 支持“指向虚成员函数”的指针
对于非虚成员函数我们可以直接拿到其地址,因为其没有多态性。但对于虚函数,其地址要在运行时确定,因此对于虚成员函数我们取的应该是其相对虚表指针的偏移index。
所以如果有如下类:
class Point {
public:
Point(int x, int y);
virtual
~Point();
int x() const {return m_x;}
int y() const {return m_y;}
virtual
int z() const { return 0; }
private:
int m_x;
int m_y;
};
对于析构函数取值&Point::~Point
取得的是0。
对于x()和y()取址&Point::x, &Point::y
得到的是其地址,因为他们不是虚函数。
对于z()取址&Point::z
得到的是1。通过pMemberFunc调用z(),其会是类似下面的形式:
(*ptr->vptr[(int)pMemberFunc])(ptr)
1.3 支持多继承的成员函数的指针
在多继承的情况下还要考虑虚函数表的位置问题,因为在多重继承下可能有多个虚函数表;还有this指针可能需要进行偏移,如果派生类没有覆盖第二个或后面的基类的虚函数的话。
为了要支持以上种种特性:如果是非虚函数,指针中要包括其地址;如果是虚函数,要包括其相对虚表指针的偏移;如果是多重继承,还要找到虚函数在哪个虚表中和对this指针进行偏移。
在《深度探索C++对象模型》中提出的是这样的结构:
struct _mptr{
int delta;
int index;
union {
PtrToFunc faddr;
int v_offset;
};
};
其中delta是this指针要进行的偏移,index是虚函数在虚表指针指向空间中的下标,faddr是非虚函数的地址,v_offset是虚表指针的的位置。
所以下面的操作:
(ptr->*pmf)();
会变成:
// 我觉得这个可能是有问题
pmf.index < 0
? // 非虚函数调用
(*pmf.faddr)(paddr)
: // 虚函数调用
(*ptr->vptr[pmf.index])(ptr)
《深度探索C++对象模型》中是这么写的,但按照作者的说法,实际的代码应该是:
pmf.index < 0
?
(pmf.faddr)(pmf + delta)
:
(((vptr*)(ptr+pmf.v_offset))[pmf.index])(ptr+delta)
// (ptr+pmf.v_offset) 是虚表地址
// ((vptr*)(ptr+pmf.v_offset))[pmf.index] 是虚表的第pmf.index项
// ptr+delta是对this指针进行偏移
让我们来看看g++中是怎么实现的:
// test27.cpp
class Point {
public:
Point(int x, int y);
virtual
~Point();
int x() const {return m_x;}
int y() const {return m_y;}
virtual
int z() const { return 0; }
private:
int m_x;
int m_y;
};
Point::Point(int x, int y)
: m_x(x), m_y(y)
{}
Point::~Point() {
m_x = m_y = 0;
}
int main() {
Point p(1, 2);
using MemberFunction_t = int (Point::*)() const ;
MemberFunction_t pVirtualMemberFunc = nullptr;
MemberFunction_t pMemberFunc = nullptr;
pMemberFunc = &Point::x;
pVirtualMemberFunc = &Point::z;
int x = (p.*pMemberFunc)();
int z = (p.*pVirtualMemberFunc)();
++z;
}
我们使用gdb看一下这个成员函数指针的size:
(gdb) p sizeof(MemberFunction_t)
$1 = 16
在赋值之后,查看pMemberFunc和pVirtualMemberFunc的二进制是什么:
(gdb) x/2ag &pMemberFunc
0x7ffffffee0d0: 0x8000a86 <Point::x() const> 0x0
(gdb) x/2ag &pVirtualMemberFunc
0x7ffffffee0c0: 0x11 0x0
可以看到g++实现的成员函数指针有两个QWORD(QWORD是size为8字节的【有符号或无符号】整型值)。如果函数指针指向的是非虚函数,第一个QWORD里面是该函数的地址;如果是的话,看上去是该虚函数相对于虚表的偏移+1,因为Point::z在vptr[2]
的地方(vptr[0]
是Point::~Point,但不调用::operator delete
;vptr[1]
也是Point::~Point,会随后调用::operator delete
),那偏移就是0x10,但内容是0x11,可能就是加了1。
让我们看一下汇编代码是怎么操作的:
上面的汇编是即将执行int x = (p.*pMemberFunc)();
这一语句。
总结如下:
- 如果不是虚函数,低8个字节是函数的地址,高8个字节是this指针的偏移;
- 如果是虚函数,低8个字节是虚表指针相对于this指针的偏移&1(位与操作),而高8个字节同样是this指针的偏移;
这两种情况就按低8个字节的QWORD的最低位是不是