指向数据成员地址的指针

时间:2010-08-13 14:51:01

标签: c++ pointers

我读过(Inside 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,则layout分别为0,4和8   如果vptr放在开头,则结束或4,8和12   类。但是,从会员的地址返回的值,   总是增加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。“

7 个答案:

答案 0 :(得分:11)

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

根据您在内存位置100的结构来考虑:

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

如您所见,a的地址与整个结构的地址相同,因此其偏移量(您必须添加到结构地址以获取项目地址)为0。 / p>

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


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

事实上它来自'96并且讨论了C ++下的内部结构(对于知道vptr的位置有多么好的抒情,错过了在错误的抽象级别工作的全部观点而你永远不应该关心)约会它。实际上,引言甚至还说明了“解释面向对象特性的基本实现”(我的斜体)。

事实上,没有人能够在ISO标准中找到任何说明这种行为的事实,以及MSVC和gcc都没有采取这种行为这一事实让我相信,即使这对于一个特定的实现来说也是如此。过去,并非真实(或必须是真实的)。

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

答案 1 :(得分:8)

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

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

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

其次,我不明白您在p1p2示例中想要说些什么。假设指针包含相同的值是完全错误的。他们不会。

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

如果我们遵循现代GCC和MSVC ++编译器使用的方法,那么0x1将获得p1(空指针)的物理值,而0xFFFF....将被分配一个物理{ {1}}。 p20x0也是不同的值。

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

答案 2 :(得分:3)

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

答案 3 :(得分:2)

补充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)之类的内容或<<流操作符执行的转换实际上是由编译器编写的,作为对0xffffffff位模式的测试(而不是我们通常喜欢将指针测试视为原始的测试地址0x0)。

答案 4 :(得分:1)

  

我已经读过指针的地址   C ++中的数据成员是偏移量   数据成员加1?

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

答案 5 :(得分:1)

$ 9.2 / 12很有意思

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

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

答案 6 :(得分:0)

您可能需要查看更详细地讨论此问题的How are objects stored in memory in C++?