А. Александреску - Современное проектирование на C++ (1119444), страница 27
Текст из файла (страница 27)
Это происходит благодаря огромной разинце между скоростью работы основной памяти (большой и медленной) и кэ|л-памяти (маленькой и быстрой). Глава 4. Размещение в памяти нвбольших объектов гз хедл11осасог* рьазсоеа11ос При поступлении запроса на выделение памяти сначала проверяется указатель рьазсл11ос . Если его размер не соответствует ожидаемому, функция веа110Ь)л11осасог: гд1!осасе выполняет бинарный поиск в массиве роо1 .
Освобождение памяти осуществляется примерно так же. Единственное отличие заключается в том, что функция вва1!0Ь)л11осасог::д11осасе может закончить свою работу, вставив в массив роо1 новый объект класса гзхедл1!осасог, Как уже отмечалось при обсужлении класса г!хедд11осасог, эта простая схема кэширования предназначена лля многократного создания и удаления объектов за постоянное время. 4.7. Трюк На послелнем уровне нашей архитектуры расположен класс ваа110Ь)есс, базовый класс, инкапсулирующий функциональные возможности, предоставленные классом вва110Ь)а11осасог. Класс впа110Ь)есс перегружает операторы пеп и де1есе, Таким образом, при создании объекта класса, производного от класса ваа110Ь)есс, в действие вступает перегрузка, направляющая запрос объекту класса гзхедл11осасог.
Определение класса вва110Ь)есс довольно простое, но интересное. с)авв вва110Ь)есс рцЬ)зс: зсасзс чозд* орегасог пеп(зсд::ззхе с ззхе); зсасзс чо)д орегасог де1есе(чозд* р, зсд::ззхе с ззхе); ч(гсиа1 -вва110Ь)ессО () )*' Класс вва110Ь)есс выглядит вполне нормально, за исключением одной маленькой детали. Во многих книгах, посвященных языку С++, например, в учебнике Бипег (2000), говорится, что при перегрузке оператора де1есе его единственным аргументом должен быть указатель типа чозд.
В языке С++ есть одна лазейка, которая нас очень интересует. (Напомним, что мы разработали класс веа110Ь)а11осасог так, чтобы размер освобождаемого блока передавался как аргумент.) В стандартном варианте оператор де1есе можно перегрузить двумя способами. Первый из них выглядит так. чо)д орегасог де1есе(чозд* р); Второй способ таков. чо)д орегасог де1есе(чозд= р, зсд". газге с Мхе) ! Эта тема очень подробно изложена в книге бипег (2000). Если используется первый способ, размер улаляемого блока памяти игнорируется. Однако этот размер нам очень нужен, поскольку его следует передать объекту класса зеа!1оЬ)л11осасог.
Слеловательно, нам полхолит лишь второй способ перегрузки. Как заставить компилятор автоматически определять размер блока! На первый взгляд кажешься, по лля этого понадобится дополнительная память для каждого объекта, хотя именно этого мы и стремились избежать. Однако на самом леле никакой лополнительной памяти не требуется. Рассмотрим следующий код. Часть Е Методы 112 с1аэз вазе ( з пт а [х00]; роЫ(с: чз гтца1 -вазеО () с1азз оегзче6 : роЫ зс вазе ( з пт Ь [200]; рцЫзс: чзгтца1 -оегзче6О () вазе" р = пеп оегзче6; 6е1еге р; Объекты классов вазе и оегзче6 имеют разные размеры.
Для того чтобы избежать лишних затрат памяти, связанных с необходимостью хранить размер фактического объекта, на который ссылается указатель р, компилятор прибегает к следующему трюку: он генерирует код, распознающий размер на лету. Этого можно достичь с ломошью следующих четырех приемов. (Вообразите на несколько минут, что мы перевоплотились в разработчиков компилятора и способны творить чудеса, на которые не способны обычные программисты.) 1. Деструктору передается буяевский признак: "Вызывать/не вызывать оператор бе1ете после разрушения объекта".
Деструктор класса вазе виртуален, поэтому в нашем примере оператор 6е1ете р относится к правильному объекту класса оег(че6. В этом случае размер объекта известен уже на этапе компиляции — он равен э(хео((оегз че6) — и компилятор просто передает эту константу оператору 6е1ете.
2. Деструктор возвращает размер объекта. Мы можем сделать так (ведь мы сами написали компилятор, не правда ли?), чтобы каждый деструктор после разрушения объекта возвращал величину аз гео~(С1аза). Эта схема глюке вполне работоспособна, поскольку деструктор класса вазе виртуален. После его вызова система поддержки выполнения программ вызовет оператор 6е1ете, передавая ему результат работы деструктора.
3. Реализуется скрытая виртуальная функция-член, получающая размер объекта в качестве аргумента. Назовем ее 5згеО, например. В этом случае система полдержки выполнения программ вызовет эту функцию, сохранит результат ее работы, уничтожит объект и вызовет оператор 6е1ете. Такая реализация может показаться неэффективной, но ее преимущество заключается в том, что компилятор может использовать функцию 5зхеО и для других целей.
4. Размер непосрелственно хранится где-то в таблице виртуальных функций каждого класса. Это решение и гибко, и эффективно, но его нелегко реализовать. (Окончен бал, погасли свечи, и мы из разработчиков компилятора разжалованы в рядовые программисты.) Клк видим, для того чтобы передать оператору бе1ете правильный размер блока, компилятор должен выполнить довольно много работы. Зачем же нам терять эту информацию и выполнять при каждом удалении объекта поиск, расхолуя время? Ведь все так хорошо складывается! Объекту класса 5иа11411осатог нужен размер удаляемого блока. Компилятор передает ему этот размер, а объект класса 5ва110Ь) ест переалресовывает его объекту класса язхе6л11осатог.
Глава 4. Размещение в памяти неболыпих обьектов Большинство из перечисленных выше решений принято в предположении, что в классе вазе определен виртуальный деструктор. Это лишний раз доказывает, насколько важно делать деструкторы полиморфных классов виртуальными. Если этим требованием пренебречь„удаление указателя типа вазе, который на самом деле ссылается на объект производного класса, может привести к непредсказуемым результатам.
В этом случае механизм распределения памяти в режиме отладки приведет к срабатыванию макросов аззегт, а в режиме нпявца просто вызовет крах программы. Каждый из нас согласится, что такое поведение можно считать "непредсказуемым". Для того чтобы не заботиться об этом (и не проводить бессонные ночи, отлаживая программу, если вы вдруг забудете о том, что сказано выше), в классе зма110Ь5ест определен виртуальный деструктор. Любой класс, производный от класса вазе, наследует его виртуальный деструктор.
Это приводит нас к реализации класса зма110Ь)ест. Во всем приложении нам нужен один-единственный объект класса вма110Ь)д110- сатог. Этот объект должен быть правильно создан и правильно разрушен, что само по себе трудно. К счастью„библиотека ).о)ц позволяет решить эту проблему с помощью шаблонного класса Ыпд1етопно1дег, описанного в главе б. (Конечно, отсылать вас к следующим главам досадно, но еше досаднее потерять возможность повторного использования када.) Пока можно рассматривать класс ззпд1етопно1дег как механизм, позволяющий осуществлять управление единственным экземпляром класса.
Если этот класс имеет имя х, шаблонный класс конкретизируется как 5)пд1етоп<х>. Затем, для того чтобы получить доступ к единственному экземпляру этого класса, нужно вызвать функцию Ыпд1етоп<х>.":тпэтапсеО. Шаблон проектирования Ыпд1етоп (Одиночка) описан в книге Оапцпа ег ай () 995). Использование класса зтпд1етопно1дег позволяет чрезвычайно просто реализовать класс зма110Ь)ест. туредег 5)пд1етоп<зма110Ь)д11осатог> муд11ос; чО)6* зяа110Ь)ЕСт::ОрЕГатОГ ПЕя(зтд::З)ХЕ т З)КЕ) ( гетцгп муя11ос::тпзтапсеО .я11осате(атее); чо1о заа110Ь)ест::орегатог ое1ете(чозд> р, этд::зчке т збтве) ( муд11ос::тпзтапсе().пеа11осате(р, з)ге); ) 4.8. Просто, сложно и сноеа просто Реализация класса заа110Ь) ест оказалась довольно простой.
Однако на самом деле не все так просто — ведь остались нерешенными проблемы, связанные с многопоточностью. Единственный объект класса Вяа11д11осатог используется всеми экземплярами класса заа110Ь)ест. Если эти экземпляры принадлежат разным потокам, то объект класса вма110Ь)д11осатог придется распределять между ними. Как указано в приложении, в этом случае нужно предпринимать специальные меры.
Кажется, иам придется пройтись по всем уровням нашей архитектуры, выделить критические операции и добавить соответствующую блокировку. Однако многопоточность не является неразрешимой проблемой, поскольку в библиотеке )лк) уже определены механизмы синхронизации объектов высокого уровня, Следуя поговорке "'лучший способ повторного использования кода — его применение", включим заголовочный файл тпгеадз.п из библиотеки Ео)с) и внесем в класс вяа110ь)ест следующие изменения (они выделены полужирным шрифтом).