C++中子类的对象是如何分配的?

3

我对C++中的继承概念感到困惑,假设我们有一个名为computer的类,并从computer类公开继承了一个名为laptop的类。现在,当我们在主函数中创建一个laptop类的对象时,内存中会发生什么?请解释。


3
最好让笔记本电脑类继承自计算机类。公共继承是一个“is-a”关系。笔记本电脑“是一个”计算机。 - luke
2
代码。输入一些代码。 - bmargulies
1
@JonH,公平地说,那个问题被关闭是因为他没有使用文本,现在他已经用文本格式创建了这个问题。 - James McMahon
2
Zia——我建议你找一个好的调试器,写一些带有继承类的代码,并查看实际发生在内存中的情况。 - Chip Uni
1
有人可以把问题标题修改得更具体一些吗,比如改成“当我们创建一个派生的C++对象时,在内存中会发生什么?”谢谢。 - Emile Cormier
显示剩余4条评论
7个回答

11
class Computer {
  public:
   Computer() { /* whatever */}
   /* whatever */
};

class Laptop : public Computer {
   public:
    Laptop() { /* whatever */ }
  /* whatever */
};

然后...

x = new Laptop();

编译器的实现如下:

  1. ptr = ::operator new(sizeof(Laptop))
  2. Computer::Computer(ptr)
  3. Laptop::Laptop(ptr)
  4. x = ptr

栈的等效实现留给读者作为练习。


完全不相关于问题并且没有帮助的答案。 - Zia ur Rahman
3
那么您需要重新编写问题。这是对所提出的问题的直接回答,正如投票所证明的那样。如果您澄清问题,我会补充回答。 - bmargulies
@bmargulies:问题明确询问了内存布局,没有提到函数调用或构造。 - Mooing Duck
如果你认为这个问题明显地在询问任何事情,那么你比我更好的千里眼。而内存布局是实现相关的。 - bmargulies

4

我假设您想了解多态对象的内存布局。我将尝试通过图示来展示它。考虑:

class Base()
{
public:
    virtual void foo();
    virtual void bar();
    void hello();

private:
    int variable1;
};

class Derived : public Base
{
public:
    virtual void foo();
    virtual void bar();
    void bye();

private:
    float variable2;
};

(注:为了清晰起见,故意省略虚析构函数。)
内存布局将类似于此:
/*
Base object layout:
[vftable pointer]   (points to the 1st row in the virtual function table)
[int variable1  ]   (from Base)

Derived object layout:
[vftable pointer]   (points to the 2nd row in the virtual function table)
[int variable1  ]   (inherited from Base)
[float variable2]   (from Derived)

virtual function table layout:
[&Base::foo   ] [&Base::bar   ]
[&Derived::foo] [&Derived::bar]
*/

注意整个程序中只有一个虚函数表用于Base和Derived类,该表不会为Derived的每个实例重复复制。相反,每个Derived实例都持有指向虚函数表“行”的隐藏指针。
另请注意,hello()和bye()不出现在虚函数表中,因为它们不是虚函数。在这种情况下,正确的函数指针可以在编译时确定。
维基百科的文章还展示了一个内存布局的例子,但由于该例使用多重继承,因此比较复杂。
Chip Uni使用调试器查看内存中发生的情况的想法是一个很好的练习。
对于更高级的程序员,另一个好的练习是尝试使用结构体和函数指针在C语言中实现多态性。

4

笔记本电脑类包含自身和计算机类中定义的属性。派生类包含基类。这就是为什么Stroustrup选择术语“基类”而不是“超类”来指代被继承的类。超级一词意味着被继承的类更大,但事实上相反,继承类扩展了它继承的类。编译器分配足够容纳派生类的内存块。


3
我假设Laptop继承自Computer,并对一般情况下发生的事情进行解释;由于C++的实现细节(出于优化原因)可能与这个一般解释不同。
从逻辑上讲,Laptop类的定义具有指向Computer类定义的指针。Laptop类的实例具有指向Laptop类定义的指针(在C++中,最可能只是对类方法的函数指针数组的引用)。
当笔记本电脑对象接收到消息时,它首先在自己的方法表中查找相应的函数。如果没有找到,它会跟随继承指针并在Computer类的方法表中查找。
现在,在C++中,很多事情都发生在编译阶段,特别是我认为方法表被压平,任何可以静态绑定的调用都被快捷处理。

非常感谢,这正是我所期望的。您回答的精髓是什么呢?第一个是“逻辑上,笔记本电脑类定义具有指向计算机类定义的指针”,第二个是“当笔记本电脑对象接收到消息时,它首先在自己的方法表中查找相应的函数。如果没有找到,则遵循继承指针并查找计算机类的方法表。”这两点确实帮了我很多,再次非常感谢。 - Zia ur Rahman
1
@Steven A. Lowe:不能保证有任何指针指向任何类定义。可能没有虚函数,而且vtbl可能被优化掉了。 - bmargulies
@[bmargulies]:正确,因此有关“任何可以静态绑定的调用都会被快捷方式处理”的说明。 - Steven A. Lowe
@Steven:在重新阅读您的答案后,我发现您谈论的是非虚拟方法调用,并进一步澄清了“其中很多都发生在编译阶段”。 - Emile Cormier
@emile:如果笔记本电脑是虚拟派生的,那么它确实会沿着类层次结构向上走。 - Mooing Duck
显示剩余2条评论

2
一个非常简单的例子可以是:
class Computer {
    char manufacturer[20];
    char type[10];
}

class Laptop : Computer {
    int runningTime;
}

如果您现在创建一个类型为computer的对象,将分配20+10 = 30字节的内存。假设在您的系统上一个整数需要4字节,由于制造商和类型的继承到Laptop,Laptop的实例将需要额外的4字节= 34字节。分配发生的地址取决于堆的当前状态。(在现实中,还有内存管理等方面的考虑。)新创建的对象需要分配给引用变量: 例如:
Laptop lap = new Laptop();

1

-1

内存被分配以容纳theObject的大小。 这包括可能继承的任何原语,例如int、char等。 可能在堆/栈上。


不,它可能完全存在于堆栈中。 - Matthieu M.

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接