1. 数据成员指针
对于普通指针变量来说,其值是它所指向的地址,0表示空指针。
而对于数据成员指针变量来说,其值是数据成员所在地址相对于对象起始地址的偏移值,空指针用-1表示。例:
struct X {
int a;
int b;
};
#define VALUE_OF_PTR(p) (*(long*)&p)
int main() {
int X::*p = 0; // VALUE_OF_PTR(p) == -1
p = &X::a; // VALUE_OF_PTR(p) == 0
p = &X::b; // VALUE_OF_PTR(p) == 4
return 0;
}
2. 函数成员指针
函数成员指针与普通函数指针相比,其size为普通函数指针的两倍(x64下为16字节),分为:ptr和adj两部分。因为在简单函数指针的后面还需要保存怎样调整 “this" 指针(总是隐式地传递给非静态成员函数)的信息。这是 C++ 语言的特性,这里成员函数的指针不是直接映射到硬件上的,它由运行时(编译器)来实现,会带来一些额外的开销,通常会导致性能的损失。C++ 语言规范中并没有提到实现的细节,也没有解释这种类型指针。看下面类继承的例子:
#include <iostream>
struct A {
void foo() const {
std::cout << "A's this:\t" << this << std::endl;
}
char pad0[32];
};
struct B {
void bar() const {
std::cout << "B's this:\t" << this << std::endl;
}
char pad2[64];
};
struct C : A, B
{ };
void call_by_ptr(const C &obj, void (C::*mem_func)() const)
{
void *data[2];
std::memcpy(data, &mem_func, sizeof(mem_func));
std::cout << "------------------------------\n"
"Object ptr:\t" << &obj <<
"\nFunction ptr:\t" << data[0] <<
"\nPointer adj:\t" << data[1] << std::endl;
(obj.*mem_func)();
}
int main()
{
C obj;
call_by_ptr(obj, &C::foo);
call_by_ptr(obj, &C::bar);
}
A 和 B 都有一个非静态成员函数以及一个数据成员。这两个方法可以通过隐式传递给它们的 “this" 指针来访问到它们类中的数据成员。为了访问到任意的数据成员,需要在 "this" 指针上加上一个偏移,偏移是数据成员到类对象基址的偏移,可以由 ptrdiff_t 来表示。然而事情在多重继承时将会变得更复杂。我们有一个类 C 继承了 A 和 B,将会发生什么呢?编译器将 A 和 B 同时放到内存中,B 在 A 之下,因此,通过类C的对象实例分别调用A 类的方法和 B 类的方法,看到的 this 指针的值是不一样的。这可以通过实践来简单验证。
“this” 指针的值传给 B 的方法要比 A 的方法要大 32 字节——一个类 A 对象的实际大小。但是,当我们用下面的函数通过指针来调用类 C 的方法时,会发生什么呢?
void call_by_ptr(const C &obj, void (C::*mem_func)() const) {
(obj.*mem_func)();
}
与调用什么函数有关,不同的 "this" 指针值会被传递到这些函数中。但是 call_by_ptr
函数并不知道它的参数是 foo()
的指针还是 bar()
的指针,能知道该信息的唯一时机是这些方法使用时。这就是为什么成员函数的指针在调用之前需要知道如何调整 this
指针。