2

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(考虑开头4bytes的vptr)。然而,在MS Visual Studio 2012中,输出为:

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

问题:MS C++ 编译器是否做了一些优化或阻止这种机制?

4

6 回答 6

4

tl;博士

《C++ 对象模型内部》是一本非常古老的书,无论如何,它的大部分内容都是特定编译器的实现细节。不要担心将您的编译器与一些古老的编译器进行比较。

完整版本

对此问题的评论中链接的问题的答案很好地解决了这个问题。

某物的偏移量是从一开始就有多少个单位。第一件事是在开始,所以它的偏移量为零。

[...]

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


而且,如果你摘录的那本书真的是Inside the C++ Object Model,那它就有点长了。

它来自 96 年并讨论了 C++ 下的内部结构(对知道它在哪里是多么好的抒情vptr,错过了在错误的抽象级别上工作并且你永远不应该关心的全部要点)日期相当多.

[...]

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

于 2013-04-27T16:02:34.773 回答
3

该语言没有指定成员指针的表示方式,因此您在书中读到的任何内容都只是它们可能表示方式的一个示例。

在这种情况下,正如您所说,听起来 vptr 占用了对象的前四个字节;同样,这不是语言指定的内容。如果是这种情况,则没有可访问成员的偏移量为零,因此无需调整偏移量以避免为零;成员指针可以简单地由成员的偏移量表示,零为“null”保留。听起来这就是您的编译器所做的。

您可能会发现非多态类型的偏移量已按照您的描述进行了调整;或者您可能会发现“null”的表示不为零。要么是有效的。

于 2013-04-27T16:02:33.633 回答
2
class Point3d {
public:
     virtual ~Point3d();

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

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

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

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

于 2013-04-27T15:54:49.877 回答
1

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

于 2013-04-27T16:06:06.067 回答
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”。

于 2013-04-27T16:35:15.213 回答
1

那本书(可在此链接获得)应该更清楚地说明它只是描述了 C++ 编译器的特定实现。您提到的细节不是 C++ 语言规范的一部分——这正是 Stanley B. Lippman 和他的同事决定实现特定功能的方式。其他编译器可以自由地以不同的方式做事。

于 2013-04-27T16:38:16.083 回答