A heap is a memory pool for a specific process. When your program needs a block of memory, it calls a heap allocation function, and it calls a companion function to free the memory. There's no assumption about 4-KB page boundaries; the heap manager uses space in existing pages or calls VirtualAlloc to get more pages. First we'll look at Windows heaps. Next we'll consider heaps managed by the CRT library for functions like malloc and new.
Windows provides each process with a default heap, and the process can create any number of additional Windows heaps. The HeapAlloc function allocates memory in a Windows heap, and HeapFree releases it.
You might never need to call HeapAlloc yourself, but it will be called for you by the GlobalAlloc function that's left over from Win16. In the ideal 32-bit world, you wouldn't have to use GlobalAlloc, but in this real world, we're stuck with a lot of code ported from Win16 that uses "memory handle" (HGLOBAL) parameters instead of 32-bit memory addresses.
GlobalAlloc uses the default Windows heap. It does two different things, depending on its attribute parameter. If you specify GMEM_FIXED, GlobalAlloc simply calls HeapAlloc and returns the address cast as a 32-bit HGLOBAL value. If you specify GMEM_MOVEABLE, the returned HGLOBAL value is a pointer to a handle table entry in your process. That entry contains a pointer to the actual memory, which is allocated with HeapAlloc.
Why bother with "moveable" memory if it adds an extra level of indirection? You're looking at an artifact from Win16, in which, once upon a time, the operating system actually moved memory blocks around. In Win32, moveable blocks exist only to support the GlobalReAlloc function, which allocates a new memory block, copies bytes from the old block to the new, frees the old block, and assigns the new block address to the existing handle table entry. If nobody called GlobalReAlloc, we could always use HeapAlloc instead of GlobalAlloc.
Unfortunately, many library functions use HGLOBAL return values and parameters instead of memory addresses. If such a function returns an HGLOBAL value, you should assume that memory was allocated with the GMEM_MOVEABLE attribute, and that means you must call the GlobalLock function to get the memory address. (If the memory was fixed, the GlobalLock call just returns the handle as an address.) Call GlobalUnlock when you're finished accessing the memory. If you're required to supply an HGLOBAL parameter, to be absolutely safe you should generate it with a GlobalAlloc(GMEM_MOVEABLE, ) call in case the called function decides to call GlobalReAlloc and expects the handle value to be unchanged.