【问题标题】:How is inheritance implemented at the memory level?如何在内存级别实现继承?
【发布时间】:2010-04-21 04:41:56
【问题描述】:

假设我有

class A           { public: void print(){cout<<"A"; }};
class B: public A { public: void print(){cout<<"B"; }};
class C: public A {                                  };

如何在内存级别实现继承?

C 是否将print() 代码复制到自身,或者它是否有一个指向它的指针,该指针指向代码的A 部分?

当我们覆盖之前的定义时,同样的事情是如何发生的,例如在B(在内存级别)?

【问题讨论】:

  • 您的意思是“C 类”从 C 类继承吗?
  • 正确答案中有很多细节。如果您愿意购买/阅读有关该主题的书籍,可以阅读 Stan Lippman 的《深入了解 C++ 对象模型》。
  • 你不是说“打印”是虚拟的吗?
  • @squelart:不,我没有。我想先了解正常继承的工作原理。

标签: c++ memory inheritance implementation


【解决方案1】:

编译器可以根据自己的选择来实现这一点。但它们通常遵循 CFront 的旧实现。

对于没有继承的类/对象

考虑:

#include <iostream>

class A {
    void foo()
    {
        std::cout << "foo\n";
    }

    static int bar()
    {
        return 42;
    }
};

A a;
a.foo();
A::bar();

编译器将最后三行更改为类似于:

struct A a = <compiler-generated constructor>;
A_foo(a); // the "a" parameter is the "this" pointer, there are not objects as far as
          // assembly code is concerned, instead member functions (i.e., methods) are
          // simply functions that take a hidden this pointer

A_bar();  // since bar() is static, there is no need to pass the this pointer

曾几何时,我猜想这是在每个创建的A 对象中使用指向函数的指针来处理的。但是,这种方法意味着每个A 对象都将包含相同的信息(指向相同函数的指针),这会浪费大量空间。编译器很容易处理这些细节。

对于具有非虚拟继承的类/对象

当然,你问的不是这个。但我们可以将其扩展到继承,这正是您所期望的:

class B : public A {
    void blarg()
    {
        // who knows, something goes here
    }

    int bar()
    {
        return 5;
    }
};

B b;
b.blarg();
b.foo();
b.bar();

编译器将最后四行变成如下内容:

struct B b = <compiler-generated constructor>
B_blarg(b);
A_foo(b.A_portion_of_object);
B_bar(b);

虚方法注意事项

当您谈论 virtual 方法时,事情会变得有些棘手。在这种情况下,每个类都会获得一个特定于类的函数指针数组,每个virtual 函数都有一个这样的指针。这个数组称为 vtable(“虚拟表”),每个创建的对象都有一个指向相关 vtable 的指针。对 virtual 函数的调用通过在 vtable 中查找要调用的正确函数来解决。

【讨论】:

  • +1 用于将名称修改和编译过程带入讨论。除了使用虚拟方法变得更复杂(不一定通过 vtable)之外,多重继承和虚拟继承变得更加混乱......可能超出了问题的范围。
  • 是的,我不想讨论多重继承或虚拟继承的细节。
  • "每个创建的对象都有一个额外的隐藏表" 不,每个创建的 object 都会得到一个指向 vtable 的指针 (虚拟表),称为 vptr。每个动态类都有一个或多个 vtable。 “vtable,它是一个指向函数的指针数组”不,它更像是一个具有不同信息的结构(许多指向函数的指针(有时带有偏移量),一个指向 char 的指针数组,有时还有很多偏移量,指向其他数据结构的指针...)
【解决方案2】:

查看the C++ ABI,了解有关事物内存布局的任何问题。它被标记为“Itanium C++ ABI”,但它已成为大多数编译器实现的 C++ 标准 ABI。

【讨论】:

    【解决方案3】:

    我不认为该标准做出任何保证。编译器可以选择制作函数的多个副本,将碰巧访问完全不同类型的相同内存偏移量的副本组合起来,等等。内联只是其中最明显的例子之一。

    但大多数编译器不会为 A::print 生成代码副本,以便在通过 C 实例调用时使用。在 C 的编译器内部符号表中可能有一个指向 A 的指针,但在运行时您很可能会看到:

    A a; C c; a.print(); c.print();
    

    已经变成了类似的东西:

    A a;
    C c;
    ECX = &a; /* set up 'this' pointer */
    call A::print; 
    ECX = up_cast<A*>(&c); /* set up 'this' pointer */
    call A::print;
    

    两个调用指令都跳转到代码内存中完全相同的地址。

    当然,既然您已经要求编译器内联 A::print,那么代码很可能会被复制到每个调用站点(但由于它替换了 call A::print,它实际上并没有增加程序大小) .

    【讨论】:

    • +1 在原始示例中,BC 不相关,但将其解释为 AC 参数成立。
    • 是的...我只检查了继承是公共的而不是虚拟的,而不是继承了哪个类。哎呀。固定。
    【解决方案4】:

    对象中不会存储任何信息来描述成员函数。

    aobject.print();
    bobject.print();
    cobject.print();
    

    编译器只会将上述语句转换为直接调用 print 函数,本质上没有任何内容存储在对象中。

    伪汇编指令如下所示

    00B5A2C3   call        print(006de180)
    

    由于 print 是成员函数,因此您将有一个附加参数;这个指针。这将作为函数的所有其他参数传递。

    【讨论】:

    • @yesraaj:我指的是类代码。 C class 是否将 print() 定义复制到自身或仅在 A class 中使用指向 print() 的指针?
    • @cambr 我认为类不会被存储在任何地方。任何方式让我们等待更多的答案
    【解决方案5】:

    在您的示例中,没有任何内容的复制。通常一个对象在运行时不知道它在哪个类中——发生的情况是,当程序被编译时,编译器会说“嘿,这个变量是 C 类型的,让我们看看是否有一个C::print()。不,好的,A::print() 怎么样?是的?好的,调用它!"

    虚拟方法的工作方式不同,因为指向正确函数的指针存储在对象引用的“vtable”* 中。如果您直接使用 C,那仍然没关系,因为它仍然遵循上述步骤。但是对于指针,它可能会说“哦,C::print()?地址是 vtable 中的第一个条目。”并且编译器会插入指令以在运行时获取该地址并调用它。

    * 从技术上讲,这不是必须的。我很确定您不会在“vtables”标准中找到任何提及;根据定义,它是特定于实现的。它恰好是第一个 C++ 编译器使用的方法,并且比其他方法更全面地工作,因此几乎每个现有的 C++ 编译器都使用它。

    【讨论】:

    • 通常一个对象在运行时不知道它所在的类”动态类的实例在运行时当然知道它的类型!
    • @curiousguy:在 C++ 中,类在运行时甚至不存在。这只是字节、指针和烟雾。你说的这些“动态类”到底是什么?
    • 类在运行时甚至不存在”不正确,typeid(*this).name() 在运行时确实存在“你说的这些“动态类”是什么的,无论如何?" 具有 vptr 的类(用于虚拟函数或虚拟基类)
    • @curiousguy:是的,typeid 可以在运行时存在。然而,类不会。第一个 C++ 编译器最初是作为将 C++ 转换为 C 的翻译器——当然,它不包含任何内置的“类”概念。对象变成了结构体,成员函数变成了名称略有不同的全局函数,而虚函数变成了 vtable 条目。在很大程度上,尽管它们现在直接生成机器代码,但它们仍然以这种方式工作——CPU 也很少知道或关心“类”。就像我说的,这都是字节和指针……加上一点花招和误导。
    • 至于当类不再存在时 RTTI 如何工作,我认为 vtable 中有一个指向预先存在的(编译入)type_info 的指针(可能是第一个条目)。似乎是最简单的方法。
    猜你喜欢
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 1970-01-01
    • 2019-03-24
    • 1970-01-01
    • 2016-06-20
    • 2013-07-25
    相关资源
    最近更新 更多