C++ 理解c++;指向父/基类的指针

C++ 理解c++;指向父/基类的指针,c++,pointers,inheritance,C++,Pointers,Inheritance,有人问我这个面试问题,我弄错了。“输出是什么”:我的答案是135,实际输出是136。这意味着指向两个父类的指针不相等,即使它们通过了先前的测试,即等于子类。 我以为我理解C++指针,但这让我难于解释。虽然我想我知道发生了什么,但我不知道为什么。任何C++专家都能提供技术解释吗? 看起来前两个比较在本质上更符合逻辑,而最后一个比较更符合字面意义 #include <iostream> class A { public: A() : m_i(0) { } pr

有人问我这个面试问题,我弄错了。“输出是什么”:我的答案是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;
}
#包括
甲级
{
公众:
A():m_i(0){}
受保护的:
国际货币基金组织;
};
B类
{
公众:
B():mud(0.0){}
受保护的:
双m_d;
};
C类
:公共A、公共B
{
公众:
C():m_C('a'){}
私人:
charm_c;
};
int main()
{
C C;
A*pa=&c;
B*pb=&c;
常数int x=(pa==&c)?1:2;
常数y=(pb==&c)?3:4;
const int z=(重新解释铸造(pa)=重新解释铸造(pb))?5:6;

std::cout如果您打印出
pa
pb
可能会更清楚发生了什么

std::cout << "A: " << pa << std::endl;
std::cout << "B: " << pb << std::endl;
std::cout << "C: " << &c << std::endl;
(您的输出可能不同,重要的是它们并不都相等)

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


当您将
C*
转换为
a*
时,编译器知道
a
部分从偏移量0开始,因此指针值不会改变。对于
C*
B*
需要偏移
sizeof(int)
(加上填充)。对于计算
x
y
,此偏移处理会自动完成。对于
z
,由于使用
重新解释强制转换

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;
}
指向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 = 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 = 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&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'关键字-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——据我所知,它应该有vtable。这是稍后要解决的问题。
微软Visual C++编译器还具有特殊类型的关键字:这也反映了vtable的生成方式。但这是普通开发人员不需要的,除非你正在处理高级COM编程。

@SethKitchen我想你的意思是他们有相同的动态类型,但不同的静态类型?
pa
是一个
A
(静态)指向一个
C
(动态),而
pb
是指向
C
(动态)的
B
(静态)你可以考虑添加<代码>语言律师< /代码>标签来解释为什么会发生这种情况。故事的寓意是:不要使用多重继承来代替任何混合。实际上我做了所有这些。我困惑的是为什么陈述:const int y=(Pb==& c)。?3:4;计算结果为true,正如您刚才指出的那样,这在技术上是不正确的。指向“b”的指针显然不等于指向“c”的指针。如果运行此程序,您将看到输出为136。请查看定义
b*pb=&c;
现在您看到了吗?如果您不仔细查看。如果您不继续阅读。
y=(pb=&c)?3:4
.pb==&c是正确的,因为它是这样定义的,您将
&c
的地址分配给
bd
。稍后还会进行布尔比较,检查
pb==&c
是否正确。当然正确!希望清除它。注意pb不同于&c。但这没关系,因为&c是c作为
c*
的地址>。当您比较
pb==&c
时,您不能将
pb
向上转换为a
c
,但可以将
c
向下转换为a
B
,当您进行比较时,比较结果为真-它产生与赋值相同的结果,因此比较结果为真。@kfsone-您是说在对pb的比较中存在隐式向下转换吗==&c?如果我要在代码中复制它,我会使用哪种类型的转换-reinterpret_cast?@Gio A reinterpret_cast保持指针值不变,但只是将指针(顾名思义)重新解释为其他内容。正如您所看到的,向下转换可能会更改指针的值。要在代码中手动执行此操作(您不想这样做)您可以执行类似于
reinterpret\u cast(reinterpret\u cast(&c)+sizeof(A))
的操作。这会忽略填充,并且是特定于编译器的。编译器为您执行此类操作的原因之一。要执行显式向下转换,您可以使用
静态\u cast
&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
 It reflects 21 - 8 - 4 - 1 = 8
 8 / sizeof(void*) = 2 - that's two vtables - one from A and another from B class instances.