Как реализуется наследование на уровне памяти?

предположим, что я

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

как реализуется наследование на уровне памяти?

тут C скопировать print() код для себя или у него есть указатель на него, который указывает где-то в A часть кода?

как происходит то же самое, когда мы переопределяем предыдущее определение, например в B (на уровне памяти)?

5 ответов


компиляторы могут реализовать это, однако они выбирают. Но они обычно следуют старой реализации 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 объект будет содержать идентичную информацию (указатель на ту же функцию), которая будет тратить много места. Компилятору достаточно легко позаботиться об этих деталях.

для классов / объектов с не виртуальным наследованием

конечно, это не совсем то, о чем вы спрашивали. Но мы можем распространить это на наследование, и это то, что вы ожидаете:

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 методы. В этом случае каждый класс получает специфичный для класса массив указателей на функции, по одному такому указателю для каждого


проверить C++ ABI для любых вопросов, касающихся компоновки в памяти вещей. Он называется "Itanium C++ ABI", но он стал стандартным ABI для C++, реализованным большинством компиляторов.


Я не думаю, что стандарт дает никаких гарантий. Компиляторы могут создавать несколько копий функций, комбинировать копии, которые имеют доступ к одним и тем же смещениям памяти на совершенно разных типах и т. д. Инлайнинг-лишь один из наиболее очевидных случаев этого.

но большинство компиляторов не будут генерировать копию кода для A::print для использования при вызове через экземпляр C. Может быть указатель на A во внутренней таблице символов компилятора для C, но во время выполнения вы скорее всего, увидим, что:

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, на самом деле это не добавляет много к размеру программы).


в объекте не будет храниться никакой информации для описания функции-члена.

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

компилятор просто преобразует вышеуказанные операторы в прямой вызов функции print, по существу ничего не хранится в объекте.

псевдо инструкция по сборке будет как ниже

00B5A2C3   call        print(006de180)

поскольку print является функцией-членом, у вас будет дополнительный параметр; этот указатель. Это будет, как и любой другой аргумент в пользу функция.


в вашем примере, без копирования чего-либо. Обычно объект не знает, в каком классе он находится во время выполнения - что происходит, когда программа составлен, компилятор говорит: "Эй, эта переменная имеет тип C, давайте посмотрим, есть ли C::print(). Нет, хорошо, как насчет A:: print()? Да? Хорошо, называй это!"

виртуальные методы работают по-разному, в том, что указатели на правильные функции хранятся в"vtable"* ссылка на объект. Это все равно не имеет значения, если вы работаете непосредственно с C, потому что он по-прежнему следует инструкциям выше. Но для указателей он может сказать: "о, C:: print()? Адрес является первой записью в vtable."и компилятор вставляет инструкции, чтобы захватить этот адрес во время выполнения и вызов.

* технически это не обязательно должно быть правдой. Я уверен, что вы не найдете упоминания в стандарте "vtables"; это по определению специфично для реализации. Это просто оказывается, это метод, используемый первыми компиляторами C++, и он работает лучше, чем другие методы, поэтому его использует почти каждый существующий компилятор C++.