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

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

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

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

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

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

Ответ 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.

Ответ 2

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

Ответ 3

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

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

Ответ 4

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

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

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

инструкция псевдо-сборки будет выглядеть ниже

00B5A2C3   call        print(006de180)

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

Ответ 5

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

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

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