理解父/基类的c ++指针

时间:2016-04-06 15:43:34

标签: c++ pointers inheritance

我被问到这个面试问题而且我弄错了。 “输出是什么”:我的答案是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;
}

2 个答案:

答案 0 :(得分:8)

也许如果你打印出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

,因此会被绕过

答案 1 :(得分: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或Release / DO_PACKSTRUCTURES&amp; 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已定义&amp; 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或Release / DO_PACKSTRUCTURES&amp; 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(类)计算大小 - 我们将获得正确的大小,但不是sizeof(成员类型) - 因为'virtual'关键字 - 虚拟关键字本身是分配额外的虚拟表指针大小。

 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编程。