【问题标题】:understanding c++ pointers to parent/base classes理解 C++ 指向父类/基类的指针
【发布时间】:2016-04-06 15:43:34
【问题描述】:

有人问我这个面试问题,但我弄错了。 “输出是什么”:我的答案是 135,实际输出是 136。这意味着指向两个父类的指针不相等,即使它们通过了与子类相等的先前测试。 我以为我理解 c++ 指针,但这让我很难解释。虽然我想我知道发生了什么,但我不知道为什么。任何可以提供技术解释的c ++专家吗? 看起来前两个比较在本质上更合乎逻辑,而最后一个比较更文字......

#include <iostream>

class A
{
    public:
    A() : m_i(0) { }

    protected:
    int m_i;
};

class B
{
    public:
    B() : m_d(0.0) { }

    protected:
    double m_d;
};

class C
    : public A, public B
{
    public:
    C() : m_c('a') { }

    private:
    char m_c;
};

int main()
{
    C c;
    A *pa = &c;
    B *pb = &c;

    const int x = (pa == &c) ? 1 : 2;
    const int y = (pb == &c) ? 3 : 4;
    const int z = (reinterpret_cast<char*>(pa) == reinterpret_cast<char*>(pb)) ? 5 : 6;

    std::cout << x << y << z << std::endl;
    return 0;
}

【问题讨论】:

  • @SethKitchen 我认为您的意思是它们具有相同的动态类型但不同的静态类型? pa 是一个A(静态)指向一个C(动态),而pb 是一个B(静态)指向一个C(动态)
  • 您可能会考虑添加language-lawyer 标签以获得基于标准的解释为什么会发生这种情况。
  • 故事的寓意:除了 mixins 之外,不要对任何东西使用多重继承。

标签: c++ pointers inheritance


【解决方案1】:

也许如果你打印出papb 会更清楚发生了什么。

std::cout << "A: " << pa << std::endl;
std::cout << "B: " << pb << std::endl;
std::cout << "C: " << &c << std::endl;

main 的末尾运行这个给我

A: 0xbfef54e0
B: 0xbfef54e4
C: 0xbfef54e0

(您的输出可能不同,重要的是它们并不完全相同)

这是因为C 对象在内存中的表示方式。由于C 既是A 又是B,因此它需要具有来自每个部分的数据成员。 C 的真实布局是这样的(忽略填充):

int    A::m_i;
double B::m_d;
char   C::m_c;

当您将C* 转换为A* 时,编译器知道A 部分从偏移量0 开始,因此指针值不会改变。对于C*B*,它需要偏移sizeof(int)(加上填充)。此偏移量处理会自动为您完成计算xy。对于z,它被绕过,因为您使用reinterpret_cast

【讨论】:

  • 其实我都做到了。我对 abpout 感到困惑的是为什么声明: const int y = (pb == &c) ? 3:4;评估为真,正如您刚刚指出的那样,在技术上不正确。指向“b”的指针显然不等于指向“c”的指针。如果你运行这个程序,你会看到输出是 136。
  • 看定义B *pb = &amp;c; 现在你明白了吗?如果不仔细看。如果你不继续阅读。 y = (pb == &amp;c) ? 3 : 4。 pb == &c 是真的,因为它是这样定义的,您将&amp;c 的地址分配给bd。稍后也会进行布尔比较,检查是否pb == &amp;c。当然可以!希望清除它。
  • ideone.com/Vxeju4 请注意,pb 与 &c 不同。但这没关系,因为 &c 是 c 作为C* 的地址。当您比较pb == &amp;c 时,您不能将pb 向上转换为C,但您可以将C 向下转换为B,并且当您进行比较时,比较结果为真 - 它产生与分配相同的结果,所以比较结果为真。
  • @kfsone - 你是说在 pb == &c 的比较中有一个隐含的沮丧吗?如果我要在代码中复制它,我会使用哪个演员 - reinterpret_cast?
  • @Gio reinterpret_cast 使指针值保持不变,但只是将指针重新解释(顾名思义)作为其他东西。正如您所看到的,向下转换可能会更改指针的值。要在代码中手动执行此操作(您不想这样做),您需要执行reinterpret_cast&lt;B*&gt;(reinterpret_cast&lt;char*&gt;(&amp;c)+sizeof(A)) 之类的操作。这会忽略填充并且是特定于编译器的。编译器为您做这种事情是有原因的。要进行明确的向下转换,您可以使用static_cast
【解决方案2】:

C++ 通常一个接一个地布置类结构。让我们检查以下代码示例:

#include <iostream>
#include <stdio.h>

//#define DO_PACKSTRUCTURES
//#define DEFINE_VFUNC

#ifdef DO_PACKSTRUCTURES
#pragma pack(push, 1)
#endif


class A
{
public:
    A() : m_i(0) { }

#ifdef DEFINE_VFUNC
    virtual 
#endif    
    void myfunc()
    {
    }

protected:
    int m_i;
};

class B
{
public:
    B() : m_d(0.0) { }

#ifdef DEFINE_VFUNC
    virtual 
#endif    
    void myfunc2()
    {
    }

protected:
    double m_d;
};

class C
    : public A, public B
{
public:
    C() : m_c('a') { }

#ifdef DEFINE_VFUNC
    virtual
#endif    
    void myfunc3()
    {
    }

    char m_c;
};

#ifdef DO_PACKSTRUCTURES
#pragma pack(pop)
#endif

void pprint(char* prefix, void* p)
{
    printf("%s = %p\r\n", prefix, p);
}

int main()
{
    C c;
    A *pa = &c;
    B *pb = &c;

    pprint("&c", &c);
    pprint("pa", pa);
    printf("\r\n");

    pprint("pb", pb);
    pprint("pa + sizeof(A)", ((char*)pa) + sizeof(A));
    printf("\r\n");

    pprint("&c.m_c", &c.m_c);
    pprint("pb + sizeof(B)", ((char*)pb) + sizeof(B));
    printf("\r\n");
    printf("sizeof(A)=%d\r\n", sizeof(A));
    printf("sizeof(B)=%d\r\n", sizeof(B));
    printf("sizeof(C)=%d\r\n", sizeof(C));
    printf("sizeof(int)=%d\r\n", sizeof(int));
    printf("sizeof(double)=%d\r\n", sizeof(double));
    printf("sizeof(char)=%d\r\n", sizeof(char));
    printf("sizeof(void*)=%d\r\n", sizeof(void*));

    pa->myfunc();
    c.myfunc2();
    c.myfunc3();

    return 0;
}

Win32 / Debug or Release / DO_PACKSTRUCTURES & DEFINE_VFUNC 未定义:

&c = 00BBF7A4
pa = 00BBF7A4

pb = 00BBF7AC
pa + sizeof(A) = 00BBF7A8

& c.m_c = 00BBF7B4
pb + sizeof(B) = 00BBF7B4

sizeof(A) = 4
sizeof(B) = 8
sizeof(C) = 24
sizeof(int) = 4
sizeof(double) = 8
sizeof(char) = 1
sizeof(void*) = 4

所以指向 C* 的指针与 A* 的指针相同 - 因为 A 是内存布局开始的第一个基类。所以内存布局看起来像这样:

C*:
   A
   B
   C members (m_c)

pb 不等于 ( pa + sizeof(A) ) - 因为编译器在 A 和 B 之间添加了一些对齐字节以加快对 B 的访问。不确定这些优化有多重要 - 使同一类的数百万个实例可能将会对性能产生影响。

Win32 / Debug 或 Release / DO_PACKSTRUCTURES 已定义而 DEFINE_VFUNC 未定义:

&c = 00B9F770
pa = 00B9F770

pb = 00B9F774
pa + sizeof(A) = 00B9F774

&c.m_c = 00B9F77C
pb + sizeof(B) = 00B9F77C

sizeof(A)=4
sizeof(B)=8
sizeof(C)=13
sizeof(int)=4
sizeof(double)=8
sizeof(char)=1
sizeof(void*)=4

现在我们不添加任何对齐或填充字节(因为 #pragma pack(push, 1) ) - 我们使 C 类更小,现在 pb == (pa + sizeof(A))。现在我们还可以看到什么是“分配”C - 类 sizeof(int) / sizeof(double) + sizeof(char) = 4 + 8 + 1 = 13。

Win32 / Debug or Release / DO_PACKSTRUCTURES & DEFINE_VFUNC 定义:

&c = 007EFCF4
pa = 007EFCF4

pb = 007EFCFC
pa + sizeof(A) = 007EFCFC

&c.m_c = 007EFD08
pb + sizeof(B) = 007EFD08

sizeof(A)=8
sizeof(B)=12
sizeof(C)=21
sizeof(int)=4
sizeof(double)=8
sizeof(char)=1
sizeof(void*)=4

我们仍然有指针计算匹配,但如果我们使用 sizeof(class) 计算大小 - 我们将得到正确的大小,但不是 sizeof(member type) - 因为“虚拟”关键字 - 虚拟关键字本身是分配额外的虚拟表指针大小。

 It reflects 21 - 8 - 4 - 1 = 8
 8 / sizeof(void*) = 2 - that's two vtables - one from A and another from B class instances.

我不确定为什么 C 类本身没有它自己的 vtable——据我所知,它应该。这是以后要弄清楚的事情。 Microsoft Visual C++ 编译器还有一种特殊的关键字 __declspec (novtable),它也反映了 vtable 的生成方式。但这是普通开发人员不需要的东西,除非您正在处理高级 COM 编程。

【讨论】:

    猜你喜欢
    • 2021-07-25
    • 2015-08-12
    • 1970-01-01
    • 2018-03-24
    • 2011-05-03
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    相关资源
    最近更新 更多