Как наследование реализовано на уровне памяти? - PullRequest
11 голосов
/ 21 апреля 2010

Предположим, у меня есть

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 ]

7 голосов
/ 21 апреля 2010

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

3 голосов
/ 21 апреля 2010

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

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

3 голосов
/ 21 апреля 2010

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

1 голос
/ 21 апреля 2010

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

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

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

1 голос
/ 21 апреля 2010

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

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

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

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

00B5A2C3   call        print(006de180)

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

...