Как реализуется наследование на уровне памяти?
предположим, что я
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++.