Добавил:
Опубликованный материал нарушает ваши авторские права? Сообщите нам.
Вуз: Предмет: Файл:
Создание эффективных приложений для Windows Джеффри Рихтер 2004 (Книга).pdf
Скачиваний:
377
Добавлен:
15.06.2014
Размер:
8.44 Mб
Скачать

ничего не подозревает, хотя оба получили адреса, указывающие на один и тот же блок памяти.

Ошибку такого рода обнаружить очень трудно, поскольку она проявляется не сра зу. Но в конце концов сбой произойдет и, будьте уверены, это случится в самый не подходящий момент, Вот какие проблемы это может вызвать.

Повреждение связанного списка блоков памяти. Эта проблема не проявится до попытки выделения или освобождения блока.

Оба потока делят один и тот же блок памяти. Оба записывают в него свою информацию Когда поток 1 начнет просматривать содержимое блока, он не поймет данные, записанные потоком 2.

Один из потоков, закончив работу с блоком, освобождает его, и это приводит к тому, что другой поток записывает данные в невыделенную память Проис ходит повреждение кучи.

Решение этих проблем — предоставить одному из потоков монопольный доступ к куче и ее связанному списку (пока он не закончит все необходимые операции с кучей) Именно так и происходит в отсутствие флага HEAP_NO_SERIALIZE Этот флаг можно использовать без опаски только при выполнении следующих условий'

в процессе существует лишь один поток; в процессе несколько потоков, но с кучей работает лишь один из них;

в процессе несколько потоков, но он сам регулирует доступ потоков к кучс, применяя различные формы взаимоисключения, например критические сек ции, объекты-мьютексы или семафоры (см. главы 8 и 9).

Если Вы не уверены, нужен ли Вам флаг HEAP_NO_SERIALIZE, лучше не пользуй тесь им. В его отсутствие скорость работы многопоточной программы может чуть снизиться из-за задержек при вызовах функций, управляющих кучами, но зато Вы избежите риска повреждения кучи и ее данных.

Другой флаг, HEAP_GENERATE_EXCEPTIONS, заставляет систему генерировать ис ключение при любом провале попытки выделения блока в куче Исключение (см. гла вы 23, 24 и 25) — еще один способ уведомления программы об ошибке. Иногда при ложение удобнее разрабатывать, полагаясь на перехват исключений, а не на провер ку значений, возвращаемых функциями.

Второй параметр функции HeapCreate — dwlnilialSize — определяет количество байтов, первоначально передаваемых куче. При необходимости функция округляет это значение до ближайшей большей величины, кратной размеру страниц И после дний параметр, clwMaximumSize, указывает максимальный объем, до которого может расширяться куча (предельный объем адресного пространства, резервируемого под кучу). Если он больше 0, Вы создадите кучу именно такого размера и не сможете его увеличить. А если этот параметр равен 0, система резервирует регион и, если надо, расширяет его до максимально возможного объема При успешном создании кучи HeapCreate возвращает описатель, идентифицирующий новую кучу Он используется и другими функциями, работающими с кучами.

Выделение блока памяти из кучи

Для этого достаточно вызвать функцию HeapAlloc

PVOID HeapAlloc( HANDLE hHeap, DWORD fdwFlags, SIZE_T dwBytes);

Параметр hHeap идентифицирует описатель кучи, из которой выделяется память. Параметр dwBytes определяет число выделяемых в куче байтов, а параметр fdwFlags позволяет указывать флаги, влияющие на характер выделения памяти В настоящее время поддерживается только три флага. HEAP_ZERO_MEMORY, HEAP_GENERATE_EX CEPTIONS и HEAP_NO_SERIALIZE.

Назначение флага HEAP_ZERO_MEMORY очевидно Он приводит к заполнению содержимого блока нулями перед вовратом из HeapAlloc. Второй флаг заставляет эту функцию генерировать программное исключение, если в куче не хватает памяти для удовлетворения запроса. Вспомните, этот флаг можно указывать и при создании кучи функцией HeapCreate, он сообщает диспетчеру, управляющему кучами, что при невоз можности выделения блока в куче надо генерировать соответствующее исключение. Если Вы включили данный флаг при вызове HeapCreate, то при вызове HeapAlloc ука зывать его ужс не нужно С другой стороны, Вы могли создать кучу без флага HEAP_GE NERATE_EXCEPTIONS. В таком случае, если Вы укажете его при вызове HeapAlloc, он повлияет лишь на данный ее вызов.

Если функция HeapAlloc завершилась неудачно и при этом разрешено генериро вать исключения, она может вызвать одно из двух исключений, перечисленных в сле дующей таблице

Идентификатор

Описание

 

 

STATUS_NO_MEMORY

Попытка выделения памяти не удалась из-за ее нехватки

STATUS_ACCESS_VIOLATION Попытка выделения памяти не удалась из-зa повреждения кучи или неверных параметров функции

При успешном выделении блока HeapAlloc возвращает егo адрес Если памяти не достаточно и флаг HEAP_GENERATE_EXCEPTIONS не указан, функция возвращает

NULL.

Флаг HEAP_NO_SERIALIZE заставляет HeapAlloc при данном вызове нс применять принцип последовательного доступа к куче Этим флагом нужно пользоваться с ве личайшей осторожностью, так как куча (особенно стандартная куча процесса) может быть повреждена при одновременном доступе к ней нескольких потоков

WINDOWS 98

Вызов HeapAlloc с требованием выделить блок размером более 256 Мб Win dows 98 считает ошибкой. Заметьте, что в этом случае функция всегда возвра щает NULL, a исключение никогда не генерируется, даже если при создании кучи или попытке выделить блок Вы указали флаг HEAP_GENERATE_EXCEPTIONS,

NOTE:

Для выделения больших блоков памяти (от 1 Мб) рекомендуется использовать функцию VirtualAlloc, а не функции, оперирующие с кучами.

Изменение размера блока

Часто бывает необходимо изменить размер блока памяти. Некоторые приложения изначально выделяют больший, чем нужно, блок, а затем, разместив в нем данные, уменьшают его Но некоторые, наоборот, сначала выделяют небольшой блок памяти и потом увеличивают его по мере записи новых данных. Для изменения размера бло ка памяти вызывается функция HeapReAlloc:

PVOID HeapReAlloc( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem,

SIZE_Т dwBytes);

Как всегда, параметр hHeap идентифицирует кучу, в которой содержится изменя емый блок. Параметр fdwFlags указывает флаги, используемые при изменении разме

pa блока HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, HEAP_ZEROMEMORY или HEAP_REALLOC_IN_PLACE_ONLY.

Первые два флага имеют тот же смысл, что и при использовании с HeapAlloc. Флаг HEAPZEROMEMORY полезен только при увеличении размера блока памяти. В этом случае дополнительные байты, включаемые в блок, предварительно обнуляются. При уменьшении размера блока этот флаг не действует.

Флаг HEAP_REALLOC_IN_PLACE_ONLY сообщает HeapReAlloc, что данный блок памяти перемещать внутри кучи не разрешается (а именно это и может попытаться сделать функция при расширении блока). Если функция сможет расширить блок без его перемещения, она расширит его и вернет исходный адрес блока. С другой сторо ны, ссли для расширения блока его надо переместить, оня возвращает адрес нового, большего по размеру блока. Если блок затем снова уменьшается, функция вновь воз вращает исходный адрес первоначального блока. Флаг HEAP_REALLOC_IN_PLACE_ ONLY имеет смысл указывать, когдя блок является частью связанного списка или де рева. В этом случае в других узлах списка или дерева могут содержаться указатели на данный узел, и его перемещение в куче непременно приведет к нарушению целост ности связанного списка

Остальные два параметра (pvMem и dwBytes) определяют текущий адрес изменяе мого блока и сго новый размер (в байтах). Функция HeapReAlloc возвращает либо ад рес нового, измененного блока, либо NULL, ссли размер блока изменить не удалось.

Определение размера блока

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

SIZE_T HeapSize( HANDLE hHeap, DWORD fdwFlags, LPCVOlD pvMem);

Параметр hHeap идентифицирует кучу, а параметр pvMem сообщает адрес блока. Параметр dfwFlags принимает два значения: 0 или HEAP_NO_SERIALIZE

Освобождение блока

Для этого служит функция HeapFree.

BOOL HeapFree( HANDLE hHeap, DWORD fdwFlags, PVOID pvMem);

Она освобождает блок памяти и при успешном вызове возвращает TRUE. Параметр fdwFlags принимает два значения 0 или HEAP_NO_SFRIALIZE Обращение к этой фун

кции может привести к тому, что диспетчер, управляющий кучами, вернет часть фи зической памяти системе, но это не обязательно.

Уничтожение кучи

Кучу можно уничтожить вызовом HeapDestroy-.

BOOL HeapDestroy(HANDLE hHeap);

Обращение к этой функции приводит к освобождению всех блоков памяти внут ри кучи и возврату системе физической памяти и зарезервированного региона адрес

ного пространства, занятых кучей При успешном выполнении функция возвращает TRUE. Если при завершении процесса Вы не уничтожаете кучу, это делает система, но — подчеркну еще раз — только в момент завершения процесса. Если куча создана потоком, она будет уничтожена лишь при завершении всего процесса

Система не позволит уничтожить стандартную кучу процесса — она разрушается только при завершении процесса. Если Вы передадите описатель этой кучи функции HeapDestroy, система просто проигнорирует Ваш вызов.

Использование куч в программах на С++

Чтобы в поЛной мере использовать преимущества динамически распределяемой па мяти, следует включить ее поддержку в существующие программы, написанные па С++. В этом языке выделение памяти для объекта класса выполняется вызовом оператора 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, и сформирует обращение именно к ним, а не к тем, которые содержатся в базовом классе.

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