C++类中如何获取成员变量的偏移值

有Animal类定义如下,我们现在想获得成员变量 y 在类 Animal 中的偏移值。在这个例子中,假设编译器是4字节对齐的,那么 y 的偏移值为8。

class Animal 
{
public:
    int x;
    char tag; 
    int y;
};

那么,我们怎么样在程序中获得任意成员变量的偏移值呢?下面是几种方式。

方式一:指针变换

我们先来看下面这段代码:

    Animal a;
    Animal * pa = (Animal *)&a;
    void * py = &(pa->y);
    size_t offset_y = (size_t)py - (size_t)pa;

通过把成员变量 y 的指针地址和 a 的指针地址相减,就可以得到 y 的偏移值。
更进一步,如何把上面的代码封装的更通用一点呢?
可以看到,上面的代码依赖于实例对象 a ,我们首先考虑把这个依赖因素去除掉。类中成员变量的偏移值是只和类的声明有关的,和具体的类实例对象没有关系。在上面的例子中,void * py = &(pa->y);,我们并没有访问某一个类成员变量的值,而只是访问了它的地址。所以,我们可以把任意的数值强转为 Animal * 类型,只要不去访问成员变量的值,都不会有问题。比如,我们把 1 强转为 Animal *

    Animal * pa = (Animal *)1;
    void * py = &(pa->y);
    size_t offset_y = (size_t)py - (size_t)pa;

更特殊一点,当我们把 0 强转为 Animal * 时,(size_t)pa 恒等于 0,所以

    size_t offset_y = (size_t)py - (size_t)pa;

可以简化为

    size_t offset_y = (size_t)py;

更进一步,我们可以封装成下面的宏函数:

    #define offsetof(s,m) ((size_t)&(((s*)0)->m))

中已经定义了上述 offsetof 宏,可以直接使用,上面的过程只是推演其原理。

方式二:通过类成员指针

类成员指针如下使用,和普通指针是有区别的:

    int Animal::* py = &Animal::y; // py = 0x00000008
    char Animal::* ptag = &Animal::tag; // ptag = 0x00000004

我们可以看到,不光是类成员指针的定义有区别,它的地址值也不是普通的内存地址值,而是一个偏移值,只是以地址值得形式展示出来而已。
我们下面要做的事情就是如何把这个 0x00000008 的指针值转换为 size_t 类型。
对于一个普通指针,要获得指针地址对应的数值,直接可以强转:

    Animal a;
    Animal * pa = &a;
    size_t value_pa = (size_t)pa;

对于类成员指针这样会直接编译错误,

    int Animal::* py = &Animal::y; // py = 0x00000008
   
 //Error    C2440    'type cast': cannot convert from 'int Animal::* ' to 'size_t'        
    size_t offset_y = (size_t)(py);

我们要通过指针的指针来解决这个问题。虽然 py 是一个特殊的类成员指针,但是 &py 就是一个普通指针了,对这个普通指针就可以进行强转操作了。

    int Animal::* py = &Animal::y; // py = 0x00000008
    size_t offset_y = (size_t)(*(void **)(&py));

这样就可以达到我们的目的了。