Использование куч в программах на С++
Чтобы в поЛной мере использовать преимущества динамически распределяемой па мяти, следует включить ее поддержку в существующие программы, написанные па С++. В этом языке выделение памяти для объекта класса выполняется вызовом оператора new, а не функцией malloc, как в обычной библиотеке С. Когда необходимость в дан ном объекте класса отпадает, вместо библиотечной С-функции frее следует применять оператор delete. Скажем, у нас есть класс CSomeClasb, и мы хотим создать экземпляр этого класса. Для этого нужно написать что-то вроде.
CSomeClass* pSorneClass = new CSomeClass;
Дойдя до этой строки, компиляюр С++ сначала проверит, содержит ли класс CSomeClass функцию-член, переопределяющую оператор new. Если да, компилятор генерирует код для вызова этой функции Нет — создает код для вызова стандартно го С++-оператора new.
Созданный объект уничтожается обращением к оператору delete
delete pSomeClass;
Переопределяя операторы new и delete для нашегоC++ - класса, мы получаем воз можность использовать преимущества функций, управляющих кучами. Для этого оп ределим класс CSomeClass в заголовочном файле, скажем, так:
class CSomeClass
{
private
static HANDLE s_hHeap;
static UINT s_uNumAllocsInHeap;
// здесь располагаются закрытые данные и функции-члены
public:
void* operator new (size_t size);
void operator delete (void* p);
// здесь располагаются открытые данные и функции-члены
...
};
Я объявил два элемента данных, s_hHeap и s_uNumAllocs!nHeap, как статические переменные А раз так, то компилятор С++ заставит все экземпляры класса CSomeClass использовать одни и те же переменные. Иначе говоря, он не станет выделять отдель ные переменные s_hHeap и s_uNumAllocsInHeap для каждого создаваемого экземпля ра класса. Это очень важно: ведь мы хотим, чтобы все экземпляры класса CSomeClass были созданы в одной куче.
Переменная s_hHeap будет содержать описатель кучи, в которой создаются объек ты CSomeClass. Переменная s_uNumAllocsInHeap — просто счетчик созданных в куче
объектов CSomeClass. Она увеличивается на 1 при создании в куче нового объекта CSomeClass и соответственно уменьшается при его уничтожении. Когда счетчик об нуляется, куча освобождается. Для управления кучей в СРР-файл следует включить примерно такой код:
HANDLE CSomeClass::s_hHeap = NULL;
UINT CSomeClass::s_uNumAllocsInHeap = 0;
void* CSomnClass::operator new (size_t size)
{
if (s_hHeap == NULL)
{
// куча не существует, создаем ее
s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0);
if (s_hHeap == NULL)
return(NULL);
}
// куча для объектов CSomeClass существует
void* p = HeapAlloc(s hHeap, 0, size);
if (p != NULL)
{
// память выделена успешно; увеличиваем счетчик объектов CSomeClass в куче
s_uNumAllocsInHeap++;
}
// возвращаем адрес созданного объекта CSomeClass
return(p);
}
Заметьте, что сначала я объявил два статических элемента данных, s_hHeap и s_uNumAllocsInHeap, а затем инициализировал их значениями NULL и 0 соответственно.
Оператор new принимает один параметр — size, указывающий число байтов, нуж ных для хранения CSomeClass Первым делом он создает кучу, если таковой нет Для проверки анализируется значение переменной s_bHeap: если оно NULL, кучи нет, и тогда она создается функцией HeapCreate, а описатель, возвращаемый функцией, со храняется в переменной s_bHeap, чтобы при следующем вызове оператора new ис пользовать существующую кучу, а не создавать еще одну.
Вызывая HeapCreate, я указал флаг HEAP_NO_SERIALIZE, потому что данная про грамма построена как однопоточная. Остальные параметры, указанные при вызове HeapCreate, определяют начальный и максимальный размер кучи. Я подставил на их место по нулю. Первый нуль означает, что у кучи нет начального размера, второй — что куча должна расширяться по мере необходимости.
Hе исключено, что Вам показалось, будто параметр size оператора new стоит пе редать в HeapCreatc как второй параметр. Вроде бы тогда можно инициализировать кучу так, чтобы она была достаточно большой для размещения одного экземпляра класса. И в таком случае функция HeapAlloc при первом вызове работала бы быстрее, так как не пришлось бы изменять размер кучи под экземпляр класса. Увы, мир устро ен не так, как хотелось бы. Из-за того, что с каждым выделенным внутри кучи блоком памяти связан свой заголовок, при вызове HeapAlloc все равно пришлось бы менять размер кучи, чтобы в нее поместился не только экземпляр класса, но и связанный с ним загловок.
После создания кучи из нее можно выделять память под новые объекты CSomeClass с помощью функции HeapAlloc. Первый параметр — описатель кучи, второй — раз мер объекта CSomeClass. Функция возвращает адрес выделенного блока.
Если выделение прошло успешно, я увеличиваю переменную-счетчик s_uNum AllocsInHeap, чтобы знать число выделенных блоков в куче. Наконец, оператор new возвращает адрес только что созданного объекта CSomeClass.
Вот так происходит создание нового объекта CSomeClasb. Теперь рассмотрим, как этот объект разрушается, — если он больше не нужен программе. Эта задача возлага ется на функцию, переопределяющую оператор delete.
void CSomeClass::operator delete (void* p)
{
if (HeapFrce(s_hHcap, 0, p))
{
// объект удален успешно
s_uNumAllocsInKeap--;
}
if (s_uNumAllocsInHeap == 0)
{
// если в куче больше нет объектов, уничтожаем ее
if (HeapDestroy(s_hHeap))
{
// описатель кучи приравниваем NULL, чтобы оператор new
// мог создать новую кучу при создании нового объекта
CSomeClass s_hHeap = NULL;
}
}
}
Оператор delete принимает только один параметр: адрес удаляемого объекта. Сна чала он вызывает HeapFree и передает ей описатель кучи и адрес высвобождаемого объекта. Если объект освобожден успешно, s_uNumAllocslnHeap уменьшается, показы вая, что одним объектом CSomeClass в куче стало меньше. Далее оператор проверяет: не равна ли эта переменная 0, и, если да, вызывает HeapDestroy, передавая ей описа тель кучи. Если куча уничтожена, s_hHeap присваивается NULL. Это важно: ведь в бу дущем наша программа может попытаться создать другой объект CSomeClass. При этом будет вызван оператор new, который проверит значение s_hHeap, чтобы опре делить, нужно ли использовать существующую кучу или создать новую.
Данный пример иллюстрируеn очень удобную схему работы с несколькими куча ми. Этот код легко подстроить и включить в Ваши классы. Но сначала, может быть, стоит поразмыслить над проблемой наследования. Если при создании нового класса Вы используете класс CSomeClass как базовый, то производный класс унаследует опе раторы new и delete, принадлежащие классу CSomeClass. Новый класс унаследует и его кучу, а это значит, что применение оператора new к производному классу повлечет выделение памяти для объекта этого класса из той же кучи, которую использует и класс CSomeClass. Хорошо это или нет, зависит от конкретной ситуации. Если объек ты сильно различаются размерами, это может привести к фрагментации кучи, что зятруднит выявление таких ошибок в коде, о которых я рассказывал в разделах «За щита компонентов» и «Более эффективное управление памятью».
Если Вы хотите использовать отдельную кучу для производных классов, нужно продублировать все, что я сделал для класса CSomeClass. Л конкретнее - включить еще один набор переменных s_hHeap и s_uNumAllocsInHeap и повторить еще раз код для операторов new и delete. Компилятор увидит, что Вы переопределили в производном классе операторы new и delete, и сформирует обращение именно к ним, а не к тем, которые содержатся в базовом классе.
Если Вы не будете создавать отдельные кучи для каждого класса, то получите един ственное преимущество: Вам не придется выделять память под каждую кучу и соот ветствующие заголовки. Но кучи и заголовки не занимают значительных объемов
памяти, так что даже это преимущество весьма сомнительно Неплохо, конечно, если каждый класс, используя свою кучу, в то же время имеет доступ к куче базового клас са. Но делать так стоит лишь после полной отладки приложения. И, кстати, проблему фрагментации куч это не снимает.