Как наследование реализуется на уровне памяти?
Предположим, что
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"; он по определению специфичен для реализации. Это просто метод, который используют первые компиляторы С++, и он работает лучше, чем другие методы, поэтому он использует почти каждый используемый С++ компилятор.