C ++中的数据成员偏移量

时间:2013-04-27 15:47:43

标签: c++ visual-c++ compiler-construction

在C ++对象模型中”表示类中数据成员的偏移量总是比实际偏移量多1,以便区分指向0的指针和指向第一个数据成员,这是一个例子:

class Point3d {
public:
     virtual ~Point3d();

public:
    static Point3d origin;
    float x, y, z;
};
//to be used after, ignore it for the first question
int main(void) {        
    /*cout << "&Point3d::x = " << &Point3d::x << endl;
    cout << "&Point3d::y = " << &Point3d::y << endl;
    cout << "&Point3d::z = " << &Point3d::z << endl;*/
    printf("&Point3d::x = %p\n", &Point3d::x);
    printf("&Point3d::y = %p\n", &Point3d::y);
    printf("&Point3d::z = %p\n", &Point3d::z);
    getchar();
}

因此,为了区分下面的两个指针,数据成员的偏移总是1个。

float Point3d::*p1 = 0;
float Point3d::*p2 = &Point3d::x;

上面的主要功能是尝试获取成员的偏移量以验证此参数,该参数应该输出:5,9,13(考虑开头的4字节的vptr)。但是,在 MS Visual Studio 2012 中,输出为:

&Point3d::x = 00000004
&Point3d::y = 00000008
&Point3d::z = 0000000C

问题:MS C ++编译器是否进行了一些优化或某些事情来阻止这种机制?

6 个答案:

答案 0 :(得分:4)

TL;博士

在C ++对象模型中是一本非常古老的书,其中大部分内容都是特定编译器的实现细节。不要担心将编译器与某些古老的编译器进行比较。

完整版

An answer to the question linked to in a comment on this question很好地解决了这个问题。

  

某事物的偏移量是从一开始就有多少单位。首先是在开始时它的偏移为零。

     

[...]

     

请注意,ISO标准未指定项目在内存中的布局位置。填充字节以创建正确的对齐当然是可能的。在一个假设的环境中,整数只有两个字节,但它们所需的对齐是256个字节,它们不是0,2和4,而是0,256和512。   

<小时/>   而且,如果那本书摘录的那本书真的是Inside the C++ Object Model,那么它的篇幅就会有点长。

     

事实上它来自'96并且讨论了C ++下的内部结构(对于知道vptr的位置有多么好的抒情,错过了在错误的抽象级别工作的全部观点而你永远不应该关心)约会它。

     

[...]

     

作者显然领导了cfront 2.1和3团队,虽然这本书似乎具有历史意义,但我认为它与现代C ++语言(和实现)无关,至少我读过的那些。 / p>

答案 1 :(得分:3)

该语言未指定成员指针的表示方式,因此您在书中阅读的任何内容都只是它们如何表示的示例。

在这种情况下,正如你所说,听起来vptr占据了对象的前四个字节;再次,这不是语言指定的东西。如果是这种情况,那么没有可访问的成员将具有零偏移,因此不需要调整偏移以避免零;成员指针可以简单地由成员的偏移量表示,零保留为“null”。听起来这就是你的编译器所做的。

您可能会发现非多态类型的偏移量会根据您的描述进行调整;或者您可能会发现“null”的表示不为零。两者都有效。

答案 2 :(得分:2)

class Point3d {
public:
     virtual ~Point3d();

public:
    static Point3d origin;
    float x, y, z;
};

由于您的类包含虚拟析构函数,并且(大多数)编译器通常将指向虚函数表的指针作为对象中的第一个元素,因此您的第一个数据处于偏移量是有意义的4(我猜你的编译器是32位编译器)。

但请注意,C ++标准没有规定数据成员应该如何存储在类中,甚至更少的空间(如果有的话)应该占用虚拟函数表。

[是的,获取一个不是“真正的”成员对象的元素的地址是无效的(未定义的行为),但我不认为这在这个特定的例子中引起了一个问题 - 它可能与不同的编译器或不同的处理器架构等]

答案 3 :(得分:1)

除非你指定一个不同的对齐方式,否则你对偏移量5的期望......,无论如何都是如此。 Normaly比char更大的元素的地址通常在偶数地址上对齐,我想甚至到下一个4字节边界。原因是访问CPU中的内存的效率。 在某些体系结构中,访问奇数地址可能会导致异常(即Motorola 68000),具体取决于成员,或者至少会导致性能下降。

答案 4 :(得分:1)

虽然“指向给定类型成员的指针”类型的空指针必须与该类型的任何非空值不同,但将非空指针偏移一个并不是编译器的唯一方法。可以确保这一点。例如,我的编译器使用空指针到成员的非零表示。

namespace {
struct a {
    int x, y;
};
}

#include <iostream>

int main() {
    int a::*p = &a::x, a::*q = &a::y, a::*r = nullptr;

    std::cout << "sizeof(int a::*) = " << sizeof(int a::*)
              << ", sizeof(unsigned long) = " << sizeof(long);

    std::cout << "\n&a::x = " << *reinterpret_cast<long*>(&p)
              << "\n&a::y = " << *reinterpret_cast<long*>(&q)
              << "\nnullptr = " << *reinterpret_cast<long*>(&r)
              << '\n';
}

产生以下输出:

sizeof(int a::*) = 8, sizeof(unsigned long) = 8
&a::x = 0
&a::y = 4
nullptr = -1

您的编译器可能正在做类似的事情,如果不相同的话。对于实现的大多数“正常”用例,此方案可能更有效,因为每次使用非空指针指向成员时,它不必额外执行“减1”。

答案 5 :(得分:1)

那本书(可用at this link)应该更清楚它只是描述C ++编译器的特定实现。像你提到的那些细节不是C ++语言规范的一部分 - 它就是Stanley B. Lippman和他的同事决定实现一个特定功能的方式。其他编译器可以自由地以不同的方式做事。