Анимация
JavaScript


Главная  Библионтека 

0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 [ 68 ] 69 70 71 72 73 74 75 76 77 78 79 80 81 82

Уплотнение памяти


Представьте себе обычное управление памятью в С++: вы зажигаете благовония, приносите жертву божествам операционной системы и удаляете объект. Если все идет нормально, объект будет должным образом деинициализирован и уничтожен и никто никогда не попытается им воспользоваться. Ха! Всем известно, что в реальной жизни так не бывает.

Одна из главных достопримечательностей динамических языков - таких как SmallTalk и Lisp - не имеет никакого отношения к самому языку, а лишь к тому, что он удаляет объекты за вас, автоматически и надежно. Выходит потрясающая экономия времени и энергии, не говоря уже о благовониях и жертвах. Можно ли то же самое сделать на С++? Вместо прямолинейного «да» или «нет» я отвечу: «Все зависит от того, сколько труда вы хотите вложить в решение».

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

Поиск указателей

Помимо подсчета ссылок и нестандартных операторов new и delete в большинстве стратегий управления памятью сочетаются две методики: определение момента, когда доступ к объекту становится невозможным, для его автоматического уничтожения (сборка мусора) и перемещение объектов в памяти (уплотнение). В свою очередь, эти стратегии зависят от того, что в других языках делается легко и просто, но оказывается дьявольски сложным в С++ - от задачи поиска всех указателей на объекты.

Поиск указателей в программе на С++ чрезвычайно сложен, поскольку компилятор не оставляет никаких инструкций на этот счет. Более того, в С++ программа может получить адрес части объекта, поэтому некоторые указатели могут ссылаться на середину большого объекта.

Мама, откуда берутся указатели?

В С++ существуют невероятно разнообразные способы получения указателей. Одни связаны с конкретным представлением объектов в памяти, другие - с наследованием, третьи - с переменными классов. Конечно, самый очевидный способ - это нахождение адреса. А теперь давайте рассмотрим другие, не столь тривиальные способы.

Адреса переменных класса

Имея объект, вы можете получить адрес переменной класса, поспользоваться им или передать другому объекту.

class Foo { private:

int x;

String y;



public:

int& X() { return x; } Ссылка на x String* Name() { return &y; } Адрес y

Каждый экземпляр Foo выглядит примерно так, как показано на представленной ниже диаграмме (вообще говоря, все зависит от компилятора, но в большинстве компиляторов дело обстоит именно так):

Foo* -

vtable

x& -

y* -

Как правило, несколько первых байт занимает указатель на v-таблицу для класса данного объекта. За ним следуют переменные класса в порядке их объявления. Если вы получаете адрес переменной класса в виде ссылки или указателя, возникает указатель на середину объекта.

Адреса базовых классов

Наследование также может вызвать массу положительных эмоций. class A {...}; Один базовый класс class B {...}; Другой базовый класс

class C : public A, public B {...}; Множественное наследование

При одиночном наследовании преобразование от derived* к base* (где base - базовый, а derived - производный класс) адрес остается прежним, даже если компилятор полагает, что тип изменился. При множественном наследовании дело обстоит несколько сложнее.

C* c = new C;

A* a = c; Преобразование от производного к первому базовому классу B* b = c; Преобразование от производного ко второму базовому классу

cout << c << endl; cout << a << endl; cout << b << endl;

Вроде бы все просто, но в действительности компилятор проделывает довольно-таки хитрый фокус. При преобразовании C* к A* указатель остается прежним. Однако при преобразовании C* к B* компилятор действительно изменяет адрес. Это связано с тем, как объект хранится в памяти (структура объектов зависит от компилятора, но сказанное относится ко всем компиляторам, с которыми я работал).

C*, A* ->


Компилятор строит объект в порядке появления базовых классов, за которыми следует производный класс. Когда компилятор преобразует C* к A*, он словно набрасывает черное покрывало на составляющие B и C и убеждает клиентский код, что тот имеет дело с самым настоящим A.

A*(c) ->




Размещение v-таблицы в начале объекта приводит к тому, что принадлежащие C реализации виртуальных функций, объявленных в A, останутся доступными, но будут иметь те же смещения, что и для A. Работая с C*, компилятор знает полную структуру всего объекта и может обращаться к членам A, B и C на их законных местах. Но когда компилятор выполняет преобразование ко второму или одному из следующих классов в списке множественного наследования, адрес изменяется - клиентский код будет считать, что он имеет дело с B.


На самом деле v-таблиц две. Одна находится в начале объекта и содержит все виртуальные функции, первоначально объявленные в A или C, а другая - в начале компонента B и содержит виртуальные функции, объявленные в B. Это означает, что преобразование типа от производного к базовому классу в С++ может при некоторых обстоятельствах породить указатель на середину объекта (по аналогии с указателями на переменные класса, о которых говорилось выше). Кроме того, в С++ открывается возможность дурацких фокусов:

C* anotherC = C*(void*(B*(c))); anotherC->MemberOfC();

Видите, в чем проблема? Преобразование B*(c) смещает указатель. Затем он преобразуется к типу void*. Далее следует обратное преобразование к C* - и наша программа будет уверена, что C начинается с неверного адреса. Без преобразования к void* все работает, поскольку компилятор может опеределить смещение B* в C*. В сущности, преобразование от base* к derived* (где base - базовый, а derived - производный класс) выполняется каждый раз, когда клиент вызывает виртуальную функцию B, переопределенную в C. Но когда происходит преобразование от void* к C*, компилятор лишь наивно полагает, что программист действует сознательно.

Запомните: каждый программист на С++ за свою карьеру проводит как минимум одну бессонную ночь, пытаясь понять, почему его объект бредит. Потом приходит какой-нибудь гуру, с ходу ставит диагноз «синдром класс-void-класс» - притом так, чтобы слышали окружающие смехом. Впрочем, я отклонился от темы.

и разражается злорадным

Виртуальные базовые классы

Если вы пользуетесь виртуальными базовыми классами, попрощайтесь со всеми схемами уплотнения и сборки мусора, требующими перемещения объектов в памяти. Ниже приведен фрагмент программы и показано, как объект представлен в памяти.

class Base {...};

class A : virtual public Base {...}; class B : virtual public Base {...}; class Foo : public A, public B {...};

Тьфу. Компилятору так стыдно, что Base приходится реализовывать как виртуальный базовый класс, что он прячет его как можно дальше, под Foo. A и B содержат указатели на экземпляр Base да, все верно, указатели, то есть непосредственные адреса в памяти. Вы не имеете доступа к этим указателям и, следовательно, не сможете обновить их при перемещении объекта в памяти.

Base



0 1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 [ 68 ] 69 70 71 72 73 74 75 76 77 78 79 80 81 82