Allocating memory on the fly is how C programs grow and shrink while they run. You don’t know the exact needs of your application until it’s actually executing. The solution is the heap. You request a block. The operating system reserves it. You use it. Then you return it with free. This cycle lets other programs recycle that same RAM.
Consider this simple example. It’s the most basic interaction with the heap you can get.
How Malloc Actually Works
The malloc function isn’t magic. It’s a mechanic checking the inventory. When you call it, three things happen in sequence.
- It checks the heap’s current capacity. It looks at the parameter you passed—in this case
sizeof(int), which is 4 bytes. It asks if enough contiguous space exists. If the heap is fragmented or full, it fails. - If space exists, it reserves that block. The system marks it as “yours.” No other
malloccall can touch this chunk. It prevents accidental collisions between allocations. - It returns the address of that block. It doesn’t give you the data itself. It gives you a pointer variable. That variable holds the location. You now have a handle to a specific spot in RAM.
In our example, p receives that address. The pointer variable is just a container for a memory location. The actual integer value lives at that location.
Why You Must Check for NULL
You see the check if (p == 0) immediately after allocation. Some beginners skip this. It’s a fatal mistake.
Always check the pointer after any call to malloc to ensure the pointer is valid.
The heap size fluctuates constantly. It depends on what other processes are running. It depends on how much memory they have grabbed. There is no guarantee a call will succeed. If malloc fails, it returns zero. Zero is NULL. If you try to dereference a null pointer, your program crashes. Hard.
You could write if (p == NULL) or if (!p). They mean the same thing. But you must check. Every time.
What Happens If You Forget to Free Memory?
If your program ends and you didn’t call free, the operating system cleans up. It releases the executable space. It clears the stack. It reclaims global memory. It also recycles any pending heap allocations.
So, is it okay to leak memory at the end of a program? Technically, yes. The OS handles the cleanup. There are no long-term consequences for the machine.
But it’s bad form. And more importantly, memory leaks during execution are deadly. If you keep allocating without freeing, you starve your own process. The heap fills up. New allocations fail. The program dies by suffocation, not by a crash.
Pointers vs. The Values They Point To
Pointers can be confusing. They are distinct from the values they reference. Here is how that plays out in practice.
First scenario. You allocate one block. You point two variables at it.
Output:
10
20
Why? p and q are different pointers. But q = p makes them point to the same memory block. Changing the value through p changes it for q. Changing it through q changes it for p. They are two handles on the same door.
Second scenario. You allocate two separate blocks.
Output:
20
Here, p and q point to different locations. *q holds 20. *p = *q copies the value 20 into the location p points to. The pointer p itself doesn’t move. The value at p ‘s address changes.
The distinction matters. One changes the address stored in the variable. The other changes the data at that address. Confusing the two leads to logic errors that are harder to debug than syntax errors.
Pointers, Memory, and the Free Fall
The output is 20. Line 6 says it all.
You might think *p = *q and p = q do the same thing. They don’t. One moves data. The other moves addresses. The compiler lets you assign *p = *q because both dereferenced pointers are integers. It’s a direct swap of values. But p = q? That’s different. It points p to the exact same memory block as q. The address moves. The data stays put.
If types don’t match, the compiler blocks it. A pointer to an integer can’t point to a character string. The types must align.
Four Ways to Initialize
A new pointer variable is a liability until it’s initialized.
int *p; creates a pointer that points nowhere specific. It points to garbage. Dereferencing it is an error. You need a known location. There are four ways to get there.
- malloc : This allocates a block on the heap.
pnow holds a valid address. The pointer is initialized because it points to a specific, reserved chunk of memory. - Assignment :
p = q;ifqis valid,pbecomes valid. It inheritsq’s address. Ifqis garbage,pgets garbage too. - Explicit Address : You can point
pdirectly at a known variable.p = &i;pointspto integeri. Simple. Direct. - Zero/NULL :
p = 0;orp = NULL;. This sets the address to zero. It doesn’t point to a block. It points to the null location.
Why use zero? It’s a flag.
You can check if (p == 0) to see if the pointer is valid. The system recognizes this. If you try to dereference a null pointer, the program crashes. Hard.
p = 0; *p = 5;
The second line fails. p points to nothing. You can’t write to nothing. This behavior is critical for linked lists later on. It’s a safety net.
Freeing the Heap
Allocating memory isn’t enough. You have to give it back.
malloc grabs space. free returns it.
free(p) does two things. First, it unreserves the block. The heap gets that memory back. It can be reused. Second, p is left uninitialized. It doesn’t magically become safe. You must reinitialize p before using it again.
The block is gone from your scope. The pointer is dangling.
Here is the standard pattern for handling a single integer on the heap:
This is mostly a demo. But it shows the lifecycle.
sizeof(int) returns the size in bytes. On most machines, that’s 4. You could hardcode malloc(4). Don’t. sizeof makes code portable. It reads better. It adapts if the architecture changes.
malloc returns a generic pointer. Compilers often warn if you don’t cast it. (int *) converts that generic blob into a pointer to an integer. It matches p.
free(p) hands the block back to the heap.
Structs and Operator Precedence
Structs behave the same way. You allocate a block for the whole structure, not just fields.
Look at (*p).i = 10;.
Why the parentheses?
Because . has higher precedence than *.
Without parentheses, *p.i means *(p.i). It tries to access i on the pointer p, then dereference that. That’s not what you want. You want to dereference p first, then access i.
Precedence rules are strict. * binds tighter than . in this context. Parentheses force the dereference to happen before the member access.
Most people hate typing (*p).i repeatedly. It’s verbose.
C has a shorthand.
p->i
It’s exactly the same as (*p).i. But it’s faster to type. You’ll see -> everywhere in C code. It’s the standard way to access struct members via a pointer.
The heap is a finite resource. Allocate carefully. Deallocate consistently. Or watch your memory leak away.




























