|
Программирование >> Синтаксис инициирования исключений
внутренние из кучи. Как будет показано в последних главах, это играет важную роль в схемах уплотнения и сборки мусора. В двух следующих главах эта методика будет применяться довольно часто. Перемещаемые объекты отделяются от объектов, остающихся на одном месте. Ведущие указатели, доступные из стека, находятся в одном пространстве памяти, а доступные из других процессов - в другом. Пространства стека и кучи Наконец, сам стек тоже можно считать разновидностью пространства памяти, а выделяемые в стеке пулы - их частным случаем. Этот подход применялся ранее в этой главе для пулов, локальных по отношению к области действия конкретной функции. Эта особая интерпретация стека упоминается и в двух последующих главах. Уплотнение памяти Представьте себе обычное управление памятью в С++: вы зажигаете благовония, приносите жертву божествам операционной системы и удаляете объект. Если все идет нормально, объект будет должным образом деинициализирован и уничтожен и никто никогда не попытается им воспользоваться. Ха! Всем известно, что в реальной жизни так не бывает. Одна из главных достопримечательностей динамических языков - таких как SmallTalk и Lisp - не имеет никакого отношения к самому языку, а лишь к тому, что он удаляет объекты за вас, автоматически и надежно. Выходит потрясающая экономия времени и энергии, не говоря уже о благовониях и жертвах. Можно ли то же самое сделать на С++? Вместо прямолинейного да или нет я отвечу: Все зависит от того, сколько труда вы хотите вложить в решение . В этой главе начинается настоящее веселье - вы увидите, как перемещать объекты в памяти, чтобы свести к минимуму фрагментацию свободной памяти. Надеюсь, при виде моих алгоритмов никто не будет кататься по полу от смеха. Заодно мы подготовим сцену для полноценных алгоритмов сборки мусора, о которых речь пойдет в следующей главе. Поиск указателей Помимо подсчета ссылок и нестандартных операторов new и delete в большинстве стратегий управления памятью сочетаются две методики: определение момента, когда доступ к объекту становится невозможным, для его автоматического уничтожения (сборка мусора) и перемещение объектов в памяти (уплотнение). В свою очередь, эти стратегии зависят от того, что в других языках делается легко и просто, но оказывается дьявольски сложным в С++ - от задачи поиска всех указателей на объекты. Поиск указателей в программе на С++ чрезвычайно сложен, поскольку компилятор не оставляет никаких инструкций на этот счет. Более того, в С++ программа может получить адрес части объекта, поэтому некоторые указатели могут ссылаться на середину большого объекта. Мама, откуда берутся указатели? В С++ существуют невероятно разнообразные способы получения указателей. Одни связаны с конкретным представлением объектов в памяти, другие - с наследованием, третьи - с переменными классов. Конечно, самый очевидный способ - это нахождение адреса. А теперь давайте рассмотрим другие, не столь тривиальные способы. Адреса переменных класса Имея объект, вы можете получить адрес переменной класса, поспользоваться им или передать другому объекту. class Foo { private: int x; String y; public: int& X() { return x; } Ссылка на x String* Name() { return &y; } Адрес y Каждый экземпляр Foo выглядит примерно так, как показано на представленной ниже диаграмме (вообще говоря, все зависит от компилятора, но в большинстве компиляторов дело обстоит именно так):
Как правило, несколько первых байт занимает указатель на 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) ->
|
© 2006 - 2024 pmbk.ru. Генерация страницы: 0
При копировании материалов приветствуются ссылки. |