Память наследованных классов
А вы когда-нибудь задумывались, как бы выглядел рентген снимок матрёшки? Скорее всего, что нет, но, если в двух словах, это именно то, о чем сегодня мы будем говорить. Мне бы хотелось рассмотреть наследование классов в C++, с точки зрения представления данных в памяти.
Как мы уже знаем, при создании объекта какого-либо класса всегда выделяется память. Размер, преимущественно, зависит от количества полей и их типов, выравнивания, а так же наследованных классов. Наглядно продемонстрировать структуру памяти объектов нам поможет следующий набор флагов компиляции для Clang:
В качестве результата мы будем видеть разметку сырой памяти в классах.
Начнем с тривиальных примеров наследования, чтобы вам потом мысленно было легче декомпозировать более сложные. Рассмотрим живой пример 1 дампа памяти для класса
Структура
Компилятор знает «из чего состоит» дочерний класс
Можно сказать, что в иерархии классов с единственным родителем образуется «матрёшка», где каждый класс включает в себя предшествующий. Вот живой пример 2. Однако эта матрёшка на самом деле немного сложнее устроена, чем мы привыкли думать. Она должна уметь описывать логику для множественного наследования, когда родителей может быть больше одного!
Рассмотрим живой пример 3 для множественного наследования:
Порядок следования областей памяти зависит от порядка наследования классов:
При вычислении адреса класса
Вышеописанный пример можно усложнить - пусть
В первую очередь хочется отметить, что классы
В данном примере мы увидели, что родительские классы
#cppcore #compiler
А вы когда-нибудь задумывались, как бы выглядел рентген снимок матрёшки? Скорее всего, что нет, но, если в двух словах, это именно то, о чем сегодня мы будем говорить. Мне бы хотелось рассмотреть наследование классов в C++, с точки зрения представления данных в памяти.
Как мы уже знаем, при создании объекта какого-либо класса всегда выделяется память. Размер, преимущественно, зависит от количества полей и их типов, выравнивания, а так же наследованных классов. Наглядно продемонстрировать структуру памяти объектов нам поможет следующий набор флагов компиляции для Clang:
-Xclang -fdump-record-layouts
В качестве результата мы будем видеть разметку сырой памяти в классах.
Начнем с тривиальных примеров наследования, чтобы вам потом мысленно было легче декомпозировать более сложные. Рассмотрим живой пример 1 дампа памяти для класса
B
:struct A {..};
struct B: A {..};
Layout of B:
0: [ Memory of struct A ] <- A*, B*
8: [ Memory of struct B ]
Структура
B
включает в себя родительский класс A
, память родителя предшествует дочернему классу. Указатель на объект класса B
смотрит на начало всей области памяти и совпадает с приведенным указателем на родительский класс:// Выполняется
assert(address_of_B == address_of_A);
Компилятор знает «из чего состоит» дочерний класс
B
. Следовательно, ему известно смещение от начала выделенной области памяти до полей родительского класса A
, а далее и B
. Это достаточно удобное представление.Можно сказать, что в иерархии классов с единственным родителем образуется «матрёшка», где каждый класс включает в себя предшествующий. Вот живой пример 2. Однако эта матрёшка на самом деле немного сложнее устроена, чем мы привыкли думать. Она должна уметь описывать логику для множественного наследования, когда родителей может быть больше одного!
Рассмотрим живой пример 3 для множественного наследования:
struct P1 {..};
struct P2 {..};
struct Child: P2, P1 {..};
Layout of Child:
0: [ Memory of struct P2 ] <- P2*, Child*
8: [ Memory of struct P1 ] <- P1*
16: [ Memory of struct Child ]
Порядок следования областей памяти зависит от порядка наследования классов:
P2, P1
. Картина всё еще кажется нам похожей, только вот нюанс заключается в следующем:// Выполняется
assert(address_of_Child != address_of_P1);
assert(address_of_Child == address_of_P2);
При вычислении адреса класса
P1
мы получаем другое значение указателя. При работе с классом P1
, абстрагировано от Child
, компилятор не знает о каких-либо смещениях, известных для Child
. Следовательно, чтобы сохранить корректность дальнейшей работы, необходимо вернуть указатель, смещенный до начала сырой памяти P1
.Вышеописанный пример можно усложнить - пусть
P1
и P2
станут наследниками класса Base
. Теперь мы получим ромбовидное наследование в живом примере 4:struct Base {..};
struct P1 : Base {..};
struct P2 : Base {..};
struct Child: P2, P1 {..};
Layout of Child:
0: [ Memory of struct P2::Base ] <- P2*, P2::Base*, Child*
8: [ Memory of struct P2 ]
16: [ Memory of struct P1::Base ] <- P1*, P1::Base*
24: [ Memory of struct P1 ]
32: [ Memory of struct Child ]
В первую очередь хочется отметить, что классы
P1
и P2
имеют индивидуальные области памяти для своих родителей Base
. Примерные дети! Просто так эти области памяти не могут быть объединены, т.к. в общем случае класс P1
никак не зависит от класса P2
. Следовательно, ему нужен свой собственный независимый кусочек памяти для Base
, куда можно писать и читать всё что угодно без оглядки на P2
и наоборот.В данном примере мы увидели, что родительские классы
P1
и P2
имеют независимые области памяти для своих родителей Base
. В некоторых случаях таким образом удобно именно раздельно представлять данные в памяти, но порой этот родительский класс должен быть один и использован совместно несколькими наследниками с помощью виртуального наследования. Разберем эту тему в следующем посте!#cppcore #compiler