    КУЧИ 

    глава 9 

    Третий, и последний, механизм управления памятью в Win32 — динамически 
распределяемые области памяти, или кучи (heaps). Они весьма удобны при 
создании множества небольших блоков данных. Например, связанными списками и 
деревьями проще манипулировать, используя именно кучи, а не виртуальную память 
(глава 7) или файлы, проецируемые в память (глава 8). 
    Есди^ы^трограммировали для 16-разрядной Windows, Вам знакомы два типа куч: 
локальные и глобальные. У каждого процесса и DLL-модуля в ней собственная 
локальная куча, а единственная глобальная — доступна всем процессам. 
    В Win32 схема управления динамически распределяемой памятью принципиально 
иная. Вот лишь некоторые отличия: 
    • существует только один тип кучи (названия вроде «глобальная» или 
«локальная», естественно, не применяются); 
    • кучи всегда являются локальными для процесса; к содержимому его кучи не 
может обратиться поток из другого процесса. Во многих приложениях 16-разрядной 
Windows глобальная куча служила для совместного использования данных 
несколькими процессами, и это вызывает проблемы при переносе программ с 
16-разрядной Windows на платформу Win32; 
    • процессу разрешается создавать несколько куч, являющихся частью его 
адресного пространства; 
    • DLL-модулю собственная куча не выделяется; он использует кучи, 
являющиеся частью адресного пространства процесса. Но DLL может создать кучу в 
адресном пространстве процесса и использовать ее для себя. Поскольку многие 
16-разрядные DLL обменивались данными с процессами через специальную локальную 
кучу, выделяемую DLL-модулям, то и здесь часто возникают проблемы при переносе программ. 
    В этой главе мы разберемся с кучами в Win32 и функциями, позволяющими 
оперировать с ними: создавать, изменять, удалять и т. д. (Кстати, в новых 
Win32-пpилoжeнияx надо применять именно эти функции.) В конце главы я 
расскажу, как в Win32 реализованы функции 16-разрядной Windows для управления 
динамически распределяемой памятью. Надеюсь, Вы понимаете, что они существуют 
в Win32 API исключительно для совместимости; это как бы надстройка над новыми 
функциями, и поэтому они исполняются медленно и требуют дополнительной памяти. 
Используйте их только в крайнем случае. 

    201 

    202 _____________________________ Windows для профессионалов 


    Win32-Ky4H 

    Это регион зарезервированного адресного пространства. Первоначально 
большей его части физическая память не передается. По мере того, как программа 
занимает эту область под данные, специальный, диспетчер, управляющий кучами 
(heap manager), передает ей физическую память постранично. А при освобождении 
страниц в куче диспетчер возвращает физическую память системе. 
    Меня часто спрашивают, по каким правилам диспетчер передает или отбирает 
физическую память. Правила зависят от конкретной реализации и версии Win32 
API. Microsoft постоянно тестирует свои операционные системы и прогоняет 
разные сценарии, чтобы определить, какие правила в большинстве случаев 
работают лучше. Их приходится менять и по мере появления нового программного 
обеспечения и оборудования. Если эти правила необходимы для Ваших программ, не 
стоит использовать динамически распределяемую память — работайте с функциями 
виртуальной памяти (т. е. VirtualAllocn VirtualFree), и Вы сможете 
устанавливать свои правила. 

    Стандартная куча процесса 

    При инициализации Win32-nponecca система создает в его адресном 
пространстве стандартную кучу процесса (process's default heap). Ее размер — 1 
Мб. Но система позволяет увеличивать размер, предлагаемый по умолчанию, для 
чего надо указать компоновщику при сборке программы параметр /HEAP: 

    /HEAP: reserve]., commit] 

    (Только не забудьте: DLL-модулям куча по умолчанию не предоставляется, и 
поэтому при их компоновке нельзя применять параметр /HEAP.) 
    Стандартная куча процесса необходима многим \Уш32-функциям. Например, 
функции ядра Windows NT выполняют все операции с использованием 
Unicode-символов и строк. Если вызвать ANSI-версию какой-нибудь Win32^yHKm™, 
ей придется, преобразовав строки из ANSI в Unicode, вызывать свою 
Unicode-версию. Для преобразования строк ANSI-функции нужно выделить блок 
памяти, в котором она размещает Unicode-версию строки. Этот блок памяти 
заимствуется из стандартной кучи процесса. Есть и другие функции, использующие 
временные блоки памяти, — они тоже выделяются из стандартной кучи процесса. Из 
нее же «черпают» себе память и функции 16-разрядной Windows, управляющие кучами. 
    Поскольку стандартную кучу процесса используют многие 'WmH-функции, а 
потоки Вашего приложения могут одновременно вызвать массу таких функций, то 
доступ к этой куче разрешается только по очереди. Иными словами, система 
гарантирует, что в каждый момент времени только один поток сможет выделить или 
освободить блок памяти в этой куче. Если же два потока попытаются выделить в 
ней блоки памяти одновременно, второй поток будет ждать, пока первый не 
выделит свой блок, — только после этого ему будет разрешено выделить себе в 
куче блок памяти. Принцип последовательного доступа потоков к куче немного 
снижает производительность многопоточной программы. Если в программе всего 
один поток, для быстрейшего доступа к куче нужно создать отдельную кучу и не 
использовать стандартную. Но Win32-фyнкциям этого, увы, не прикажешь — они 
используют кучу только последнего типа. 
    Как'я уже говорил, у одного процесса может быть несколько куч. Они 
создаются и разрушаются в период его существования. Но стандартная куча 
процесса создается в начале его исполнения и автоматически уничтожается по его 
завершении — сами уничтожить ее Вы не можете. Каждую кучу идентифицирует свой 
описатель, и все Win32^yHкции, выделяющие и освобождающие блоки в ее пределах, 
требуют передавать им этот описатель как параметр. 

    ________________________________________Глава 9. Кучи 203 

    Описатель стандартной кучи процесса возвращает функция GetProcessHeap: 

    HANDLE GetProcessHeap(VOID); 


    Дополнительные кучи в Win32-npouecce 

    В адресном пространстве процесса допускается создание дополнительных куч. 
Для чего они нужны? Тому может быть три причины: 
    • защита компонентов; 
    • более эффективное управление памятью; 
    • локальный доступ. 
    Рассмотрим эти причины подробнее. 

    Защита компонентов 

    Допустим, программа должна обрабатывать два компонента: связанный список 
структур NODE и двоичное дерево структур BRANCH. Представим также, что у Вас 
есть два С-файла: 
    LNKLST.C, содержащий функции для обработки связанного списка, и BINTREE.C 
с функциями для обработки двоичного дерева. 
    Если структуры NODE и BRANCH хранятся в одной куче, она может выглядеть 
примерно так (см. рис. 9-1): 

    NODE3 
    BRANCH I 
   
    NODE 2 
    BRANCH 2 
    BRANCH 3 
    NODE 1 

    Рис. 9-1. Единая куча, в которой размещены структуры. NODE и BRANCH 

    Теперь предположим, что в коде, обрабатывающем связанный список, «сидит 
жучок», который приводит к случайной перезаписи 8 байтов после NODE I. А это в 
свою очередь влечет порчу данных в BRANCH 3. Впоследствии, когда код из файла 
BINTREE.C пытается «пройти» по двоичному дереву, происходит сбой из-за того, 
что часть данных в памяти испорчена. Можно подумать, ошибка возникает из-за 
«жучка» в коде двоичного дерева, тогда как на самом деле он — в коде 
связанного списка. Ну а поскольку разные типы объектов смешаны в одну кучу (в 
прямом и переносном смысле), то отловить «жучков» в коде становится гораздо труднее. 
    Создав же две отдельные кучи — одну для NODE, другую для BRANCH, — Вы 
локализуете место возникновения ошибки. И тогда «жучок» в коде связанного 
списка не испортит целостности двоичного дерева, и наоборот. Конечно, всегда 
остается вероятность такой фатальной ошибки в коде, которая приведет к записи 
данных в постороннюю кучу, но это случается значительно реже. 

    204 ______________________________Windows для профессионалов 


    Эффективное управление памятью 

    Кучами можно управлять гораздо эффективнее, выделяя в них объекты 
одинакового размера. Допустим, каждая структура NODE занимает 24 байта, а 
каждая структура BRANCH — 32. Память для всех этих объектов выделяется из 
одной кучи. На рис. 9-2 показано, как выглядит полностью занятая куча с 
несколькими объектами NODE и BRANCH. Если объекты NODE 2 и NODE 4 удаляются, 
то память в куче становится фрагментированной. И если после этого попытаться 
выделить в ней память для структуры BRANCH, ничего не выйдет — даже несмотря 
на то, что в куче свободно 48 байтов, а структура BRANCH требует всего 32. 

    NODE3 
    NODE 5 
    NODE 4 
    
    NODE 6 
    BRANCH 5 
    BRANCH 1 
    
    NODE 2 
    BRANCH 2 
    BRANCH 3 
    
    NODE 1 

    Рис. 9-2. Фрагментированная куча, содержащая несколько объектов NODE и BRANCH 

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

    Локальный доступ 

    И последняя причина, по которой имеет смысл использовать в программе 
раздельные кучи, — локальный доступ. Выделение 4 Гб адресного пространства 
программе на компьютере с куда меньшим объемом реальной памяти требует 
согласованной работы операционной системы и процессора. Перекачка страницы из 
оперативной памяти в страничный файл занимает какое-то время. Та же задержка 
происходит и в момент подгрузки страницы данных обратно в оперативную память. 
Обращаясь в основном к памяти, локализованной в небольшом диапазоне адресов, 
Вы снизите вероятность перекачки страниц между оперативной памятью и 
страничным файлом. 
    Поэтому при разработке приложения старайтесь размещать объекты, к которым 
необходим частый доступ, как можно _плотнее друг к другу. Возвращаясь к 
примеру со связанным списком и двоичным деревом, отмечу, что просмотр списка 
не связан с просмотром двоичного дерева. Поместив все структуры NODE друг за 
другом в одной куче, Вы, возможно добьетесь того, что по крайней мере 
несколько структур NODE уместятся впредедах одной страницы физической памяти. 
И тогда просмотр связанного списка не потребует от процессора при каждом 
обращении к какой-нибудь структуре NODE переключаться с одной страницы на другую. 
    Если же «свалить» оба типа структур в одну кучу, объекты NODE не 
обязательно будут размещены строго друг за другом. При самом неблагоприятном 
стечении обстоятельств на странице окажется всего одна структура NODE, а 
остальное место займут структуры BRANCH. В этом случае просмотр связанного 
списка будет приводить к ошибке стра

    ________________________________________Глава 9. Кучи 205 

    ницы (page fault) при обращении к каждой структуре NODE, что в результате 
может чрезвычайно замедлить скорость выполнения Вашего процесса. 

    Создание Win32-K/4M 

    Дополнительные кучи в процессе создаются вызовом HeapCreate: 

    HANDLE HeapCreate(DWORD flOptions, DWORD dwInitialSize, DWORD cbMaximumSize): 

    Параметр flOptions модифицирует характер операций, выполняемых над кучей. 
В нем можно указать О, HEAP_NO_SERIALIZE, HEAP_GENERATE_EXCEPTIONS или 
комбинацию последних двух флагов. 
    По умолчанию действует принцип последовательного доступа к куче, что 
позволяет не опасаться одновременного обращения к ней сразу нескольких 
потоков. При попытке • выделения из кучи блока памяти функция НеарАНос (ее 
параметры мы обсудим чуть позже): 
    1. Просматривает связанный список выделенных и свободных блоков памяти. 
    2. Находит адрес свободного блока. 
    3. Выделяет новый блок, помечая свободный как занятый. 
    4. Добавляет новый элемент в связанный список блоков памяти. 
    Чтобы наглядно представить смысл флага HEAP_NO_SERIALIZE, допустим, что 
два потока одновременно пытаются выделить блоки памяти из одной кучи. Первый 
поток выполняет операции по пп. 1 и 2 и получает адрес свободного блока 
памяти. Но только он соберется перейти к третьему этапу, как его вытеснит 
второй поток и тоже выполнит операции по пп. 1 и 2. Поскольку первый поток не 
успел дойти до этапа 3, второй обнаружит тот же свободный блок памяти. 
    Итак, оба потока «считают», что нашли свободный блок памяти в куче. 
Поэтому поток 1 обновляет связанный список, помечая новый блок как занятый. 
После этого и поток 2 обновляет связанный список, помечая тот же-бдо-к как. 
занятыЙ.._Ни„один из потоков пока ничего_«не подозревает», хотя оба получили 
адреса, указывающие на один и тот же блок памяти. 
    Ошибку такого рода обнаружить очень трудно, поскольку она проявляется не 
сразу. Но в конце концов сбой произойдет и, будьте уверены, — в самый 
неподходящий момент. Вот какие проблемы это может вызвать: 
    • поврежден связанный список блоков памяти. Эта проблема не проявится до 
попытки выделения или освобождения блока; 
    • оба потока делят один и тот же блок памяти. Оба записывают в него свою 
информацию. Когда поток 1 начнет просматривать содержимое блока, он не 
«поймет» данные, записанные потоком 2; 
    • один из потоков, закончив работу с блоком, освобождает его. Это приводит 
к тому, что другой поток записывает в невыделенную память. Происходит 
повреждение кучи. 
    Решение этих проблем — предоставить одному из потоков монопольный доступ к 
куче и ее связанному списку (пока он не закончит все необходимые операции с 
кучей). Именно так и происходит в отсутствие флага HEAP_NO_SERIALIZE. Этот 
флаг можно использовать без опаски только при выполнении следующих условий: 
    • в процессе существует лишь один поток; 
    • в процессе несколько потоков, но с кучей работает лишь один из них; 
    • в процессе несколько потоков, но он сам регулирует доступ потоков к 
куче, применяя различные формы взаимоисключения (mutual exclusion), например 
объекты-мьютексы или семафоры (см. главу 10). 

    206 ______________________________Windows для профессионалов 

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

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

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

    LPVOID HeapAlloc(HANDLE hHeap, DWORD dwFlags, DWORD dwBytes); 

    Параметр hHeap идентифицирует описатель кучи, из которой выделяется 
память. Описатель надо получить предварительным вызовом HeapCreate или 
GetProcessHeap. Параметр dwBytes определяет число выделяемых в куче байтов, а 
dwFlags позволяет указывать флаги, влияющие на характер выделения памяти. В 
настоящее время поддерживаются только три флага: 

    HEAP_ZERO_MEMORY, HEAP_GENERATE_EXCEPTIONS и HEAP_NO_SERIALIZE. 

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

    Идентификатор                  Описание 

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

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

    ________________________________________Глава 9. Кучи 207 

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

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

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

    LPVOID HeapReAlloc(HANDLE hHeap, DWORD dwFlags. LPVOID IpMem, DWORD dwBytes); 

    Как всегда, параметр hHeap идентифицирует кучу, содержащую изменяемый 
блок. Параметр dwFlags указывает флаги, используемые при изменении размера 
блока. Допустимы четыре флага: HEAP_GENERATE_EXCEPTIONS, HEAP_NO_SERIALIZE, 
HEAP_ZERO_ MEMORY и HEAP_REALLOC_IN_PLACE_ONLY. 
    Первые два имеют тот же смысл, что и при использовании с HeapAlloc. Флаг 
НЕАР_ZERO_MEMORY полезен только при увеличении размера блока памяти. В этом 
случае дополнительные байты, включаемые в блок, предварительно обнуляются. При 
уменьшении размера блока этот флаг не действует. 
    Флаг HEAP_REALLOC_IN_PLACE_ONLY сообщает HeapReAlloc, что данный блок 
памяти перемещать внутри кучи не разрешается; а именно это и пытается сделать 
функция при расширении блока. Если функция сможет расширить блок без его 
перемещения, она расширит его и вернет исходный адрес блока. С другой стороны, 
если для расширения блока его надо переместить, она возвращает адрес нового, 
большего по размеру блока. Если блок затем снова уменьшается, функция вновь 
возвращает исходный адрес первоначального блока. Флаг 
HEAP_REALLOC_IN_PLACE_ONLY имеет смысл указывать, если блок является составной 
частью связанного списка или дерева. В этом случае в других узлах списка или 
дерева могут содержаться указатели на данный узел, и его перемещение в куче 
непременно приведет к нарушению целостности связанного списка. 
    Остальные два параметра (IpMem и dwBytes) определяют текущий адрес 
изменяемого блока и его новый размер (в байтах). Функция HeapReAlloc 
возвращает либо адрес нового, измененного блока, либо NULL, если размер блока 
изменить не удалось. 

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

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

    DWORD HeapSize(HANDLE hHeap, DWORD dwFlags, LPCVOID IpMem): 

    Параметр hHeap (возвращенный в предыдущем вызове HeapCreate или 
GetProcessHeap) идентифицирует кучу, а IpMem (возвращенный в предыдущем вызове 
HeapAlloc или HeapReAlloc) сообщает адрес блока. Параметр dwFlags принимает 
два значения: 0 или НЕАР_NO_SERIALIZE. 

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

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

    BOOL HeapFree(HANDLE hHeap, DWORD dwFlags, LPVOID IpMem): 

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

    208 ______________________________Windows для профессионалов 

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

    Уничтожение Win32-Ky4H 

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

    BOOL HeapDestroy(HANDLE hHeap); 

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

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

    Чтобы в полной мере задействовать преимущества Win32-Ky4, включите их 
поддержку в существующие программы, написанные на C++. В этом языке выделение 
памяти для объекта класса выполняется вызовом оператора new, а не функцией 
malloc, как в обычной библиотеке С. Когда необходимость в данном объекте 
класса отпадает, вместо библиотечной Сфункции free следует вызвать оператор 
delete. Скажем, у нас есть класс CSomeClass, и мы хотим создать экземпляр 
этого класса. Для этого нужно написать что-то вроде: 

    CSomeClass* pCSomeClass = new CSomeClass; 

    Дойдя до этой строки, компилятор C++ сначала проверит, содержит ли класс 
CSomeClass функцию-член, переопределяющую оператор new. Если да, компилятор 
генерирует код для вызова этой функции. Нет — генерирует код для вызова 
стандартного С++-оператора new. Созданный объект уничтожается обращением к 
оператору delete: 

    delete pCSomeClass; 

    Переопределяя операторы new и delete для нашего С++-класса, мы получаем 
возможность использовать преимущества \Уш32-функций, управляющих кучами. Для 
этого определим класс CSomeClass в заголовочном файле, скажем, так: 

    class CSomeClass { 
    private; 
    static HHEAP s_hHeap; 
    static UINT s_uNumAllocsInHeap; 
    // здесь располагаются закрытые данные и функции-члены 
    
    public:                                                                    
                    ' 
    void* operator new (size_t size): 
    void operator delete (void* p); 
    // здесь располагаются открытые данные и функции-члены 

    ________________________________________Глава 9. Кучи 209 

    Я объявил два элемента данных — s_hHeap и s_uNumAllocsInHeap — как 
статические переменные. А раз так, то компилятор C++ заставит все экземпляры 
класса CSomeClass использовать одни и те же переменные. Иначе говоря, он не 
станет выделять отдельные переменные s_hHeap и s_uNumAllocsInHeap для каждого 
создаваемого экземпляра класса. Это очень важно: ведь мы хотим, чтобы все 
экземпляры класса CSomeClass были выделены в одной куче. ~~~' Переменная 
s_hHeap будет содержать описатель кучи, в которой выделяются объекты 
CSomeClass. Переменная s_uNumAllocsInHeap — простой счетчик выделенных в куче 
объектов CSomeClass. Она увеличивается на единицу при выделении в куче нового 
объекта CSomeClass и соответственно уменьшается при его уничтожении. Когда 
счетчик обнуляется, куча становится ненужной и освобождается. Для управления 
кучей в СРР-файл следует включить примерно такой код: 

    ННЕАР CSomeClass::s_hHeap = NULL; 
    UINT CSomeClass::s_uNumAllocsInHeap = 0; 
    
    void* CSomeClass::operator new (size_t size) { 
    if (s_hHeap == NULL) { 
    
    // куча не существует: создадим ее 
    s_hHeap = HeapCreate(HEAP_NO_SERIALIZE, 0, 0); 
    
    if (s_hHeap == NULL) 
    return(NULL); 
    
    // куча для объектов CSomeGlass существует 
    void* p: 
    
    while ((p = (void *) HeapAlloc(s_hHeap. 0, size)) == NULL) { 
    // объект CSomeClass нельзя выделить из кучи 
    if (_new_handler != NULL) { 
    
    // вызываем определенный в приложении обработчик 
    (*_new_handler)(); 
    } else { 
    
    // обработчик в приложении не определен: просто возвращаем управление 
    break: 
    } 
    if (p != NULL) { 
    
    // память выделена успешно: увеличиваем счетчик 
    // объектов CSomeClass в куче 
    s_uNumAllocsInHeap++; 
    
    // возвращаем адрес созданного объекта GSomeClass 
    return(p): 
    } 

    Заметьте, что сначала я объявил два статических элемента данных — s_hHeap 
и s_uNumAllocsInHeap, а затем инициализировал их значениями NULL и 0 
соответственно. Оператор new принимает один параметр — size, указывающий число 
байтов, нужных для хранения CSomeClass. Оператор new первым делом создает 
кучу, если таковой нет. Для проверки посмотрите значение переменной s_hHeap: 
если оно NULL, то кучи нет, и тогда она создается функцией HeapCreate, а 
описатель, возвращаемый функцией, сохраняется в 

    210 _____________________________Windows для профессионалов 

    переменной s_hHeap, чтобы при следующем вызове оператора new использовать 
существующую кучу, а не создавать еще одну. 
    Вызывая HeapCreate, я указал флаг HEAP_NO_SERIALIZE, потому что данная 
программа построена как однопоточная. В главе 10 мы рассмотрим некоторые 
особенности Win32, учет которых в приведенном выше коде позволит не опасаться 
работы и с несколькими потоками. Остальные параметры, указанные при вызове 
HeapCreate, определяют начальный и максимальный размер кучи. Я подставил на их 
место по нулю. Первый 0 означает, что у кучи нет начального размера, второй — 
что куча должна расширяться по мере необходимости. 
    Не исключено, что Вам показалось, будто параметр size оператора new стоит 
передать в HeapCreate как второй параметр. Вроде бы тогда можно 
инициализировать кучу так, чтобы она была достаточно большой для размещения 
одного экземпляра класса. И тогда функция HeapAlloc при первом вызове работала 
бы быстрее, так как не пришлось бы изменять размер кучи под экземпляр класса. 
Увы, мир устроен не так, как хотелось бы. Из-за того, что с каждым выделенным 
внутри кучи блоком памяти связан свой заголовок, при вызове HeapAlloc все 
равно пришлось бы менять размер кучи, чтобы в нее поместился не только 
экземпляр класса, но и связанный с ним заголовок.                              
    После создания кучи из нее можно выделять новые объекты CSomeClass с 
помощью HeapAlloc. Первый параметр — описатель кучи, второй — размер объекта 
CSomeClass. Функция возвращает адрес выделенного блока. 
    Если выделение прошло успешно, я увеличиваю переменную-счетчик 
s_uNumAllocsInHeap, чтобы знать число выделенных блоков в куче. Наконец, 
оператор new возвращает адрес только что созданного объекта CSomeClass.        
    Вот так происходит создание нового объекта CSomeClass. Теперь рассмотрим, 
как этот объект разрушается, если он больше не нужен программе. Эта задача 
возлагается на оператор delete:                                                
    
    void CSomeClass::operator delete (void* p) {                               
    if (HeapFree(s_hHeap, 0, p)) { 
    // объект удален успешно 
    s_uNumAlIocs In Heap--; 
    if (s_uNumAllocsInHeap == 0) { 
    
    // если в куче больше нет объектов, уничтожаем ее 
    if (HeapDestroy(s_hHeap)) { 
    // описатель кучи приравниваем NULL, чтобы оператор new 
    // мог создать новую кучу при создании нового объекта CSomeClass 
    s_hHeap = NULL; 
    } 
    } 
    } 

    Оператор delete принимает только один параметр: адрес удаляемого объекта. 
Сначала он вызывает HeapFree и передает ей описатель кучи и адрес 
высвобождаемого объекта. Если объект освобожден успешно, s_uNumAllocsInHeap 
уменьшается, показывая, что одним объектом CSomeClass в куче стало меньше. 
Далее оператор проверяет: не равна ли эта переменная 0, и, если да, вызывает 
HeapDestroy, передавая ей описатель кучи. Если куча уничтожена, s_hHeap 
присваивается NULL. Это важно: ведь в будущем наша программа может попытаться 
создать другой объект CSomeClass. При этом будет вызван оператор new, который, 
проверив значение s_hHeap, определит, нужно ли использовать существующую кучу 
или создать новую. 

    ________________________________________Глава 9. Кучи 211 

    Данный пример иллюстрирует очень удобную схему работы с несколькими 
кучами. Этот код легко подстроить и включить в несколько Ваших классов. Но 
сначала, может быть, стоит поразмыслить над проблемой наследования. Если при 
создании нового класса Вы используете класс CSomeClass как базовый, то 
производный класс (derived class) унаследует операторы new и delete, 
принадлежащие классу CSomeClass. Новый класс унаследует и его кучу, а это 
значит, что применение оператора new к производному классу повлечет выделение 
памяти для объекта этого класса из той же кучи, которую использует и класс 
CSomeClass. Хорошо это или нет, зависит от конкретной ситуации. Если объекты 
сильно различаются размерами, это может привести к фрагментации кучи, что 
затруднит выявление таких ошибок в коде, о которых я рассказывал в разделах 
«Защита компонентов» и «Эффективное управление памятью». 
    Если Вы хотите использовать отдельную кучу для производных классов, нужно 
продублировать все, что я сделал для класса CSomeClass. А конкретнее — 
включить еще один набор переменных s_hHeap и s_uNumAllocsInHeap и повторить 
еще раз код для операторов new и delete. Компилятор увидит, что Вы 
переопределили в производном _классе опегшторы new и delete и сформирует 
обращение именно к ним, а не к тем, что содержатся в базовом классе. 
    "Если не создавать отдельные кучи для каждого класса, то единственное 
преимущество здесь — не нужно выделять память под каждую кучу и 
соответствующий ей заголовок. Но кучи и заголовки не занимают значительных 
объемов памяти, так что даже это преимущество весьма сомнительно. Неплохо, 
конечно, если каждый класс, используя свою кучу, в то же время имеет доступ к 
куче базового класса. Но делать так стоит лишь после полной отладки 
приложения. И, кстати, проблему фрагментации куч это не снимает. 

    Другие функции управления кучами 

    Кроме уже упомянутых, в Windows NT есть еще несколько функций, 
предназначенных для управления кучами. Я расскажу Вам о них, но не забудьте, 
что в Windows 95 этих функций нет. 
    В адресном пространстве процесса может быть несколько куч, и функция 
GetProcessHeaps позволяет получить их описатели «одним махом»: 

    DWORD GetProcessHeaps(DWORD NumberOfHeaps, PHANDLE ProcessHeaps): 

    Предварительно Вы должны создать массив описателей, а затем вызвать 
функцию так: 

    HANDLE hHeaps[25]: 
    DWORD dwHeaps = GetProcessHeaps(25, hHeaps); 
    if (dwHeaps > 25) { 
    // у процесса больше куч, чем мы ожидали 
    } else { 
    // элементы от hHeaps[0] до hHeaps[dwHeaps-1] 
    // идентифицируют существующие кучи 
    } 

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

    BOOL HeapValidate(HANDLE hHeap, DWORD dwFlags, LPCVOID ipMem); 

    проверяет целостность кучи. Обычно ее вызывают, передавая в hHeap 
описатель кучи, в dwFlags — 0 (этот параметр допускает еще флаг 
HEAP_NO_SERIALIZE), а в IpMem — NULL. Функция просматривает все блоки в куче, 
чтобы убедиться в отсутствии поврежденных блоков. 

    212 ______________________________Windows для профессионалов 

    Чтобы функция работала быстрее, в параметре IpMem можно передать адрес 
конкретного блока. Тогда функция проверит только этот блок. 
    В отличие от 16-разрядной Windows, блоки памяти в кучах, принадлежащих 
Win32 процессам, не сжимаются (compact) и не отбрасываются (discard). Тем не 
менее функция HeapCompact существует и в Win32: 

    UINT HeapCompact(HANDLE hHeap. DWORD dwFlags); 

    Несмотря на определенное сходство в названии, она не делает того, что 
делают функции 16разрядной Windows GlobalCompact и LocalCompact. Функция 
HeapCompact просматривав указанную кучу и просто объединяет свободные блоки, а 
также возвращает системе страни цы, не занятые блоками. Обычно в параметре 
dwFlags передают 0, но можно передать и HEAP_NO_SERIALIZE. 
    Следующие две функции — HeapLock и HeapUnlock — используются парно: 

    BOOL HeapLock(HANDLE hHeap); 
    BOOL HeapUnlock(HANDLE hHeap); 

    Как и предыдущая, они не имеет ничего общего с функциями 16-разрядной 
Windows GlobalLock, LocalLock, GlobalUnlock и LocalUnlock. Функции HeapLock и 
HeapUnlock предназначены для синхронизации потоков. После успешного вызова 
HeapLock поток, который вызывал ее, становится владельцем кучи. Если любой 
другой поток обращается к куче, указывая тот же описатель кучи, система 
приостанавливает его выполнение до тех пор, пока куча не будет разблокирована 
вызовом HeapUnlock. 
    Функции HeapAlloc, HeapSize, HeapFree и другие — все обращаются к HeapLock 
и HeapUnlock, чтобы обеспечить последовательный доступ к куче. Самостоятельно 
вызывать эти функции не рекомендуется. 
    Последняя функция, предназначенная для работы с кучами, — HeapWalk: 

    BOOL HeapWalk(HANOLE hHeap, LPPROCESS_HEAP_ENTRY IpEntry); 

    Она служит только для отладки и позволяет просматривать содержимое кучи. 
Обычно ее вызывают по нескольку раз, передавая адрес структуры 
PROCESS_HEAP_ENTRY (Вы долж ны сами создать ее и инициализировать): 

    typedef struct _PROCESS_HEAP_ENTRY { 
    PVOID IpData; 
    DWORD cbData; 
    BYTE cbOverhead; 
    BYTE iRegionIndex: 
    WORD wFlags; 
    uhioh { 
    struct { 
    HANDLE hMem; 
    DWORD dwReserved[ 3 ]; 
    } Block; 
    struct { 
    DWORD dwCommittedSize; 
    DWORD dwUhCommittedSize; 
    LPVOID IpFirstBlock; 
    LPVOID IpLastBlock; 
    } Region; 
    }; 
    } PROCESS_HEAP_ENTRY, *LPPROCESS_HEAP_ENTRY, *PPROCESS_HEAP_ENTRY; 

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

    ________________________________________Глава 9. Кучи 213 

    следующему блоку, вызовите HeapWalk еще раз, передав ей тот же описатель 
кучи и адрес тойГже структуры PROCESS_HEAP_ENTRY. Если HeapWalk вернет FALSE, 
значит, блоков в куче больше нет. Подробное описание элементов структуры 
PROCESS_HEAP_ENTRY см. в Win32 SDK. 
    Обычно вызовы функции HeapWalk «обрамляют» вызовами HeapLock и HeapUnlock, 
чтобы посторонние потоки не портили картины, создавая или удаляя блоки в 
просматриваемой куче. 

    Управление кучами функциями 16-разрядной Windows 

    Теперь о том, как в Win32 реализованы функции 16-разрядной Windows, 
предназначенные для управления кучами. Хотя речь здесь пойдет о функциях, 
манипулирующих как глобальной, так и локальной кучами, о приемах их 
использования я умолчу, поскольку предполагаю, что Вам уже известны принципы 
программирования для 16-разрядной Windows, и поскольку этих функций лучше 
избегать при разработке новых Win32-пpилoжeний. Win32 поддерживает функции, 
управляющие локальной и глобальной кучами, только для упрощения переноса 
программ. Если Вы разрабатываете новое Win32-пpилoжeниe и не собираетесь 
перекомпилировать программу, разработанную для 16-разрядной Windows, лучше не 
обращаться к функциям управления глобальной и локальной памятью — они работают 
хуже и медленнее \^тЭ2-фун.ки,ий, управляющих кучами. 
    Для поддержки 16-разрядных функций, управляющих глобальной и локальной 
кучами, каждому процессу при инициализации предоставляется своя таблица 
описателей и своя стандартная куча, в которой выделяются участки для 
глобальной и локальной памяти. 
    Таблица описателей создается для того, чтобы Win32 мог управлять 
выделением глобальной и локальной памяти. Эта таблица представляет собой 
массив структур; каждый элемент массива указывает на блок памяти, выделенной 
из стандартной кучи процесса. При вызове GlobalAlloc система выделяет блок из 
этой кучи и отыскивает свободный элемент в таблице описателей, принадлежащей 
процессу. Затем сохраняет адрес выделенного блока в таблице описателей и 
возвращает адрес элемента этой таблицы. Последнее значение, по сути, — 
описатель блока памяти. И когда Вы вызовете GlobalLock, система просмотрит 
таблицу описателей и просто вернет адрес блока памяти, выделенного в 
стандартной куче процесса. 
    Первоначально Win32 отводит небольшой объем памяти, достаточный для 
хранения буквально нескольких элементов таблицы описателей. По мере 
необходимости в нее добавляются новые элементы; при этом Win32 может 
увеличивать объем памяти, занимаемый таблицей описателей. 
    Только из-за одних дополнительных операций с таблицей описателей старые 
16-разрядные функции работают медленнее, чем \^т32-функи,ии, управляющие 
кучами. Однако, если Вам нужно написать код, компилируемый как для Win32, так 
и для 16-разрядной Windows, или если Вы хотите быстро перекроить программу под 
Win32, используйте эти функции. Но знайте: не все они ведут себя точно так же, 
как функции 16-разрядной Windows. Поэтому мы и рассмотрим, что делают в 
Win32-cpeдe 16-разрядные функции, управляющие кучами. 

    Функции 16-разрядной Windows, перенесенные в Win32 

    В таблице на рис. 9-3 показано, чем занимаются в Win32 функции 
16-разрядной Windows, управляющие памятью. В каждой строке приведено по две 
функции, выполняющих идентичные операции с кучами. Заметьте, что в Win32 типы 
HGLOBAL и HLOCAL определены как HANDLE. 

    214 ______________________________Windows для профессионалов 

    Функции управления памятью из 16-разрядной Windows                 Смысл в Win32 

    HGLOBAL GlobalAlloc(UINT fuAlloc,          Выделяют блок памяти. 
    DWORD cbAlloc); 
    HLOCAL LocalAlloc(UINT fuAlloc, 
    UINT cbAlloc); 
    HGLOBAL GlobalDiscard(HGLOBAL hgib);      Удаляют блок памяти. Макросы 
определяются как: 
    HLOCAL LocalDiscard(HLOCAL hid);          GlobalReAlloc((hglb), 0, GMEM_MOVEABLE); 
    LocalReAlloc((hlcl), 0, LMEM_MOVEABLE); 
    
    UINT GlobalFlags(HGLOBAL hgib);            Возвращают информацию о флагах 
блока памяти. 
    UINT LocalFlags(HLOCAL hid); 
    
    HGLOBAL GlobalFree(HGLOBAL hgib);        Освобождают блок памяти. 
    HLOCAL LocalFree(HLOCAL hid); 
    
    LPVOID GlobalLock(HGLOBAL hgib);          Блокируют блок памяти. 
    LPVOID LocalLock(HLOCAL hid); 
    
    BOOL GlobalUnlock(HGLOBAL hgib);          Разблокируют блок памяти. 
    BOOL LocalUnlock(HLOCAL hid); 
    
    HGLOBAL GlobalReAlloc(HGLOBAL hgib,      Изменяют размер и/или флаги блока памяти. 
    DWORD cbNewSize, UINT fuAlloc); 
    HLOCAL LocalReAlloc(HLOCAL hid, 
    UINT cbAlloc, UINT fuAlloc); 
    
    DWORD GlobalSize(HGLOBAL hgib);           Возвращают размер блока памяти. 
    UINT LocalSize(HLOCAL hid); 
    
    HGLOBAL GlobalHandle(LPVOID IpvMem);     Возвращают описатель блока памяти, 
    HLOCAL LocalHandle(LPVOID IpvMem);       содержащего переданный адрес. 
    
    Рис. 9-3. Функции-управления памятью, перенесенные из 16-разрядной Windows 
в Win32 


    Функции с семантическими изменениями 

    Когда программа вызывает GlobalAlloc или LocalAlloc для выделения 
нефиксированной памяти, Win32-CHCTeMa всегда отводит память для описателя 
данных — как и для самих данных. Эти функции возвращают описатель — адрес 
элемента в таблице описателей. Допустим, исполняются строки кода: 

    HGLOBAL hgib = GlobalAlloc(GMEM_MOVEABLE, 10); 
    LPVOID Ipv = GlobalLock(hglb); 

    Переменная hgib — адрес структуры в таблице описателей. При вызове 
GlobalLock система, просматривая элемент в таблице описателей, определяет 
адрес блока памяти. Впоследствии GlobalLock возвращает именно этот адрес. 
    Обе функции возвращают адрес выделенного блока памяти. Непосредственно 
перед этим блоком располагается внутренняя структура данных с управляющей 
информацией (о размере выделенного блока и его описателе). При выделении 
фиксированных блоков система не создает описатель в таблице описателей — 
просто выделяет память и возвращает адрес блока через GlobalAlloc или LocalAlloc. 
    В таблице на рис. 9-4 приведен список флагов, допустимых в Win32. (Имейте 
в виду, что назначение некоторых флагов GlobalAlloc и LocalAlloc изменилось.) 

    _______________________________________Глава 9. Кучи 215 


    Флаг                  Смысл в Win32 
    
    GHND                  Определен как (GMEM_MOVEABLE : GMEM_ZEROINIT). 
    LHND                  Определен как (LMEMJMOVEABLE ! LMEM_ZEROINIT). 
    GPTR                   Определен как (GMEM_FIXED ! GMEM_ZEROINIT). 
    LPTR                   Определен как (LMEM_FIXED : LMEM_ZEROINIT). 
    GMEM_DDESHARE,        Совместное использование памяти таким способом в 
Win32 не 
    GMEM_SHARE            допускается. Однако этот флаг можно включить, чтобы 
подсказать системе, как совместно использовать память в будущем. 
    
    GMEM_DISCARDABEE,    Выделенный блок может быть отобран системой 
(discardable memory 
    LMEM_DISCARDABLE     block). Win32 игнорирует эти флаги. 
    GMEM_FIXED,            Выделенный блок считается фиксированным. 
    LMEM_FIXED 
    GMEM_LOWER,          Игнорируются. 
    GMEM_NOT_BANKED, 
    GMEM_NOCOMPACT, 
    LMEM_NOCOMPACT, 
    GMEM_NODISCARD, 
    LMEM_NODISCARD, 
    GMEM_NOTIFY, 
    LMEM_NOTIFY 
    GMEM_MOVEABLE,       Выделенный блок считается перемещаемым. 
    EMEM_MOVEABLE 
    GMEM_ZEROINIT,         Содержимое только что выделенного блока обнуляется. 
    LMEM_ZEROINIT 
    NONZEROLHND          Определен как (LMEM_MOVEABLE). 
    NONZEROEPTR          Определен как (LMEM_FIXED). 

    Рис. 9-4. Флаги памяти и их смысл в 'Win32 

    Функцию GlobalReAlloc или LocalReAlloc нельзя вызывать с одним только 
флагом GMEM_DISCARDABLE или LMEMJ3ISCARDABLE, не включив флаг GMEM_MODIFY или LMEM_MODIFY'. 

    Функции 16-разрядной Windows, которых следует избегать в Win32 

    В таблице на рис. 9-5 перечислены функции 16-разрядной Windows, 
сохраненные для упрощения переноса приложений на платформы Win32, но они 
устарели, и применять их не стоит. Каждая из этих функций существует по одной 
или нескольким причинам: 
    • чтобы программы могли управлять общей глобальной кучей. В Win32 у 
каждого приложения свое адресное пространство, а глобальной кучи нет; 
    • чтобы помочь в управлении памятью, которая может быть отобрана системой 
(discardable memory). В Win32 блоки памяти не отбираются самой системой. Такое 
может произойти, только если приложение явным образом вызывает GlobalDiscard 
или LocalDiscard. Да и то они лишь обнуляют размер указанных блоков; 
    1. Флаги GMEM_MODIFY и LMEM_MODIFY не приведены в таблице на рис. 9-4 
из-за того, что они используются исключительно в функциях GlobalReAlloc и LocalReAlloc. 

    216 _____________________________Windows для профессионалов 

    • чтобы помочь в управлении перемещаемой памятью (moveable memory). В 
Win32 система не перемещает и не сжимает блоки памяти самостоятельно. 
    Функции управления памятью из 16-разрядной Windows              Смысл в Win32 

    BOOL DefineHandleTable(WORD w);        Макрос, определенный как ((w), TRUE) 
    DWORD GetFreeSpace(UINT u);            Макрос, определенный как (OxIOOOOOL) 
    DWORD GlobalCompact(DWORD);         Всегда возвращает 0х100000 
    void GlobalFix(HGLOBAL);                 To же, что GlobalLock 
    HGLOBAL GlobalLRUNewest(HGLOBAL h); Макрос, определенный как (HANDLE) (h) 
    HGLOBAL GlobalLRU01dest(HGLOBAL h);  Макрос, определенный как (HANDLE) (h) 
    void GlobalUnfix(HGLOBAL);              To же, что GlobalUnlock 
    BOOL GlobalUnWire(HGLOBAL);          To же, что GlobalUnlock 
    void *GlobalWire(HGLOBAL);              To же, что GlobalLock 
    void LimitEmsPages(DWORD);              Макрос определен как пустой 
    UINT LocalCompact(UINT);               Всегда возвращает 0х100000 
    UINT LocalShrink(HLOCAL, UINT);         Всегда возвращает 0х100000 
    HGLOBAL LockSegment(UINT w);          Макрос, определенный как 
GlobalFix((HANDLE) (w)) 
    LONG SetSwapAreaSize(UINT w);           Макрос, определенный как (w) 
    void UnlockSegment(UINT w);              Макрос, определенный как 
GlobalUnfix((HANDLE) (w)) 

    Рис. 9-5. Этих функций управления памятью из 16-разрядной Windows следует 
избегать в Win32 


    Функции 16-разрядной Windows, убранные из Win32 

    Вот список функций 16-разрядной Windows, удаленных из Win32 API главным 
образом потому, что они специфичны для процессора Intel. Ведь Win32 API 
разработан специально, чтобы все его функции работали на любых процессорных 
платформах. Поэтому вызов следующих функций даст при компиляции ошибку: 

    AllocDStoCSAlias      GlobalDOSAlhc       Locallnit 
    AllocSekctor           GlobalDOSFree        SwitchStackBack 
    ChangeS elector        GlobalNotify          SwitchStackTo 
    FreeSelector            GlobalPageLock 
    GetCodeInfo          GlobalPageUnlock 
