【问题标题】:Pointer to data member address指向数据成员地址的指针
【发布时间】:2010-08-13 14:51:00
【问题描述】:

我已经读过(在 C++ 对象模型中)C++ 中指向数据成员的指针的地址是数据成员的偏移量加 1?
我正在 VC++ 2005 上尝试这个,但我没有得到准确的偏移值。
例如:

Class X{  
  public:  
    int a;  
    int b;  
    int c;
}

void x(){  
  printf("Offsets of a=%d, b=%d, c=%d",&X::a,&X::b,&X::c);
}  

应打印 a=1、b=5、c=9 的偏移量。但在 VC++ 2005 中,它变成了 a=0,b=4,c=8。
我无法理解这种行为。
书中摘录:

“然而,这种期望偏离了一个——有点传统的错误 适用于 C 和 C++ 程序员。

类内三个坐标成员的物理偏移量 如果 vptr 位于,则布局分别为 0、4 和 8 结尾或 4、8 和 12(如果 vptr 位于开头) 班级。但是,从获取成员地址返回的值, 总是增加 1。因此实际值为 1、5 和 9,并且 很快。问题是区分指向无数据的指针 成员和指向第一个数据成员的指针。举个例子:

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

// oops: how to distinguish?   
if ( p1 == p2 ) {   
   cout << " p1 & p2 contain the same value — ";   
   cout << " they must address the same member!" << endl;   
}

为了区分p1和p2,每个实际的成员偏移值是 增加了 1。因此,编译器(和用户)都必须记住 在实际使用该值来寻址成员之前减去 1。”

【问题讨论】:

  • pointer to data member in C++ is the offset of data member plus 1 - 你从哪里得到这些信息的?
  • 看尺子,它也是从0开始的。
  • 您引用的那段摘录缺少 CONTEXT .. 它在说什么?无论如何,您的示例代码结果完全正确,并且比文档更能说真话(这是编码的真理)。
  • 书名是什么?把书扔掉。获取good one
  • 这本书貌似是《Inside the C++ Object model》,作者带领早期的cfront C++实现团队,自96年以来一直没有更新。像许多书籍一样,它可能更多是描述特定实现的历史好奇心,与今天的 C++ 无关。

标签: c++ pointers


【解决方案1】:

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

想想你的结构位于内存位置 100:

100: class X { int a;
104:           int b;
108:           int c;

如您所见,a 的地址与整个结构的地址相同,因此它的偏移量(必须添加到结构地址才能获得项目地址)为 0。

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


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

它来自 96 年并讨论了 C++ 下的内部结构(对知道vptr 的位置有多好表示抒情,错过了在错误的抽象级别上工作的全部要点,而你永远不应该关心)日期相当多。事实上,引言甚至声明“解释了面向对象特性的基本实现......”(我的斜体字)。

事实上,没有人能在 ISO 标准中找到任何东西说这种行为是必需的,以及 MSVC 和 gcc 都不是这样的事实,这让我相信,即使这是一个特定的实现,远在过去,它不是真的(或必须是真的)。​​

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

【讨论】:

    【解决方案2】:

    首先,指向数据成员类型的指针值的内部表示是一个实现细节。它可以通过许多不同的方式来完成。您遇到了一种可能实现的描述,其中指针包含成员加 1 的偏移量。很明显,“加 1”的来源:特定实现要为 空指针 保留物理零值 (0x0),因此第一个数据成员的偏移量(可以很容易为 0) 必须转换为其他东西以使其与空指针不同。将所有此类指针加 1 即可解决问题。

    但是,应该注意这是一种相当麻烦的方法(即编译器在执行访问之前总是必须从物理值中减去 1)。该实现显然非常努力地确保所有空指针都由物理零位模式表示。说实话,这些天我在实践中还没有遇到过遵循这种方法的实现。

    今天,大多数流行的实现(如 GCC 或 MSVC++)仅使用普通偏移(不向其添加任何内容)作为指向数据成员的指针的内部表示。当然,物理零将不再用于表示空指针,因此它们使用其他一些物理值来表示空指针,例如 0xFFFF...(这是 GCC 和 MSVC++ 使用的)。

    其次,我不明白你想用 p1p2 的例子说什么。假设指针将包含相同的值是绝对错误的。他们不会。

    如果我们按照您帖子中描述的方法(“offset + 1”),那么p1 将接收空指针的物理值(显然是物理0x0),而p2 将接收物理值0x1(假设 x 的偏移量为 0)。 0x00x1 是两个不同的值。

    如果我们遵循现代 GCC 和 MSVC++ 编译器使用的方法,那么 p1 将收到 0xFFFF....(空指针)的物理值,而 p2 将被分配一个物理 0x00xFFFF...0x0 又是不同的值。

    P.S. 我刚刚意识到 p1p2 的例子实际上不是你的,而是一本书的引述。好吧,这本书再一次描述了我上面提到的同样的问题——0 偏移量与空指针的0x0 表示的冲突,并提供了一种可能的可行方法来解决该冲突。但是,再一次,有其他方法可以做到这一点,今天许多编译器使用完全不同的方法。

    【讨论】:

    • MSVC 实际上有三个或四个不同的指向成员表示的指针,具体取决于为前向声明类型假定的继承模型。
    • @MSN:这通常适用于指向成员函数的指针。指向 data 成员的指针明显更简单。它们对继承模型的敏感度明显较低(或根本不敏感)。通常,可以将它们实现为 any 继承模型中的普通偏移。如果 MSVC++ 正在做一些更复杂的事情,我不知道是什么原因。
    • @AndreyT:你观察到了正确的问题。这个问题与对齐问题无关。它是关于将指向数据成员的空指针与已初始化的空指针区分开来。谢谢。
    • @AndreyT,您忘记了指向虚拟基类成员的指针。那个人也不太宽容。
    • @MSN:不,我没有忘记任何事情。指向成员 functions 的指针的问题是,在一般情况下,正确的 this 指针值的非平凡计算必须在 dereference 的时刻执行。这就是为什么指向成员函数的指针必须携带相当多的额外信息。这就是它们如此复杂的原因。
    【解决方案3】:

    你得到的行为对我来说看起来很合理。听起来不对的是你读到的。

    【讨论】:

    • 更不用说在不均匀的地址上使用成员变量会非常低效。
    • 添加了本书的摘录。请看一下
    • 我看过了。我仍然认为我上面所说的非常准确 - 充其量,他描述的是某个特定编译器使用的方法,而不是一般要求。顺便说一句,我不确定我是否见过以这种方式工作的编译器,但即使确实如此,我也没有看到太大的相关性。
    【解决方案4】:

    补充 AndreyT 的回答:尝试在您的编译器上运行此代码。

    void test()
    {  
        using namespace std;
    
        int X::* pm = NULL;
        cout << "NULL pointer to member: "
            << " value = " << pm 
            << ", raw byte value = 0x" << hex << *(unsigned int*)&pm << endl;
    
        pm = &X::a;
        cout << "pointer to member a: "
            << " value = " << pm 
            << ", raw byte value = 0x" << hex << *(unsigned int*)&pm << endl;
    
        pm = &X::b;
        cout << "pointer to member b: "
            << " value = " << pm 
            << ", raw byte value = 0x" << hex << *(unsigned int*)&pm << endl;
    }
    

    在 Visual Studio 2008 上,我得到:

    NULL pointer to member:  value = 0, raw byte value = 0xffffffff
    pointer to member a:  value = 1, raw byte value = 0x0
    pointer to member b:  value = 1, raw byte value = 0x4
    

    确实,这个特定的编译器使用特殊的位模式来表示 NULL 指针,因此留下 0x0 位模式来表示指向对象第一个成员的指针。

    这也意味着,无论编译器在何处生成代码以将此类指针转换为整数或布尔值,都必须注意查找该特殊位模式。因此,像if(pm)&lt;&lt; 流运算符执行的转换实际上是由编译器编写的,作为针对 0xffffffff 位模式的测试(而不是我们通常喜欢将指针测试视为针对地址 0x0 的原始测试) )。

    【讨论】:

      【解决方案5】:

      我已经读取了指针的地址 C++ 中的数据成员是 数据成员加1?

      我从未听说过,而您自己的经验证据表明情况并非如此。我认为您误解了 C++ 中结构和类的一个奇怪属性。如果它们完全为空,它们的大小仍然为 1(因此它们的数组的每个元素都有一个唯一的地址)

      【讨论】:

      • 我添加了本书的摘录。请看一看。
      【解决方案6】:

      $9.2/12 很有趣

      在没有中间访问说明符的情况下声明的(非联合)类的非静态数据成员被分配,以便后面的成员在类对象中具有更高的地址。由访问说明符分隔的非静态数据成员的分配顺序未指定(11.1)。实现对齐要求可能会导致两个相邻的成员不能立即分配;管理虚拟功能 (10.3) 和虚拟基类 (10.1) 的空间要求也是如此。

      这解释了这种行为是由实现定义的。然而,'a'、'b' 和 'c' 处于递增地址的事实符合标准。

      【讨论】:

      • 但是这些地址是否不连续?
      猜你喜欢
      • 1970-01-01
      • 1970-01-01
      • 2023-03-27
      • 1970-01-01
      • 1970-01-01
      • 2018-03-24
      • 1970-01-01
      • 1970-01-01
      • 2016-07-28
      相关资源
      最近更新 更多