malloc, free, and the HeapUntil now, every variable you've declared lived on the stack β its size fixed at compile time, its lifetime tied to a function scope. Now you get to ask the OS for memory at runtime. This is how real programs work.
static locals, globals, string literals. Exist for the entire program. String literals are read-only.
malloc, you release it with free. You choose the size at runtime. You own the lifetime.
malloc to freeYou've been using the stack this whole time. char buf[100]; β 100 bytes on the stack. int n = 5; β 4 bytes on the stack. Clean, automatic, invisible.
But what if you don't know the size at compile time? What if the user types a string of unknown length and you need to store it? That's when you need the heap.
malloc β ask the OS for memory#include <stdlib.h> // always include this for malloc/free
char *p = malloc(100);
One line. Here's what it does:
void * β a generic pointer to the first byte.NULL.malloc(n) returns either a pointer to n bytes you own, or NULL if it failed. Those are the only two outcomes. The memory is uninitialized β whatever bytes were there before are still there. If you want zeros, use calloc.
NULLchar *buf = malloc(100 * sizeof(char));
if (buf == NULL) {
printf("Out of memory!\n");
return 1; // or handle the error
}
// now buf is 100 bytes of heap memory, all yours
Why check? Because malloc can fail β especially with very large allocations, or on memory-constrained systems. Dereferencing NULL is undefined behavior β the same class of bug as your bare-pointer scanf from lesson 1.
malloc(100 * sizeof(char)) instead of just malloc(100)?
sizeof(char) is always 1, so for char arrays the multiplication is redundant. But the habit malloc(n * sizeof(type)) is load-bearing for other types. sizeof(int) is 4 on most machines, so malloc(100 * sizeof(int)) gets you space for 100 ints β 400 bytes. Write the sizeof every time and you'll never miscalculate on int, struct, or any multi-byte type.
free β give it backchar *buf = malloc(100 * sizeof(char));
if (!buf) { return 1; }
/* use buf */
free(buf); // return the memory to the OS
buf = NULL; // optional but smart: prevents use-after-free
free(buf) tells the OS "I'm done with this memory." The bytes are returned to the heap for reuse. After free, buf is a dangling pointer β it holds an address that no longer belongs to you.
free. UB. The memory may have been given to someone else.free twice on the same pointer. UB. The heap's bookkeeping is corrupted.malloc without a matching free. The memory stays claimed until the program exits. Small leaks are tolerable in short-lived programs; in long-running ones they accumulate and crash.
malloc + strings β the real "dynamic string"Here's a concrete example. You have two strings and you want to concatenate them into a new string β on the heap, sized exactly right:
#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *join(const char *a, const char *b) {
// Count the exact size we need
size_t len = strlen(a) + strlen(b) + 1;
// Ask the heap for exactly that many bytes
char *result = malloc(len * sizeof(char));
if (result == NULL) {
return NULL;
}
// Copy both parts in β using your lesson 2/5 skills
char *p = result;
while (*a != '\0') { *p++ = *a++; }
while (*b != '\0') { *p++ = *b++; }
*p = '\0';
return result; // caller now owns this memory
}
int main(void) {
char *s = join("Hello", "World");
if (s) {
printf("%s\n", s); // β HelloWorld
free(s); // must free what we malloc'd
}
return 0;
}
Notice the discipline: join mallocs, the caller frees. Whoever allocates is responsible for documenting who frees. In library code, this is the convention: the function that returns heap memory transfers ownership to the caller.
Stack (char buf[100]) | Heap (malloc(100)) | |
|---|---|---|
| Size | Fixed at compile time | Chosen at runtime |
| Lifetime | Automatic β gone when function returns | Manual β lives until you free |
| Speed | Very fast (just move a stack pointer) | Slower (OS call, bookkeeping) |
| Size limit | Small (typically 1β8 MB) | Large (limited by available RAM) |
| Errors | Stack overflow if too deep | Leaks, use-after-free, double-free |
| Best for | Small, short-lived buffers | Large or unknown-size data |
calloc β malloc that zeroes// These two are equivalent:
int *a = malloc(100 * sizeof(int)); // 100 ints, uninitialized garbage
int *b = calloc(100, sizeof(int)); // 100 ints, all set to 0
calloc(count, size) takes two arguments: how many elements, and how big each one is. It zeroes the memory. Slightly slower, but safer β uninitialized heap memory is a common source of unpredictable bugs.
realloc β resize a heap allocationchar *buf = malloc(50);
/* ... use buf ... */
/* Need more space? */
char *bigger = realloc(buf, 200);
if (bigger == NULL) {
// realloc failed β original buf is still valid!
free(buf);
return 1;
}
buf = bigger; // now buf points at 200 bytes
realloc tries to grow (or shrink) the allocation in place. If it can't, it allocates new memory, copies the old data over, frees the old block, and returns the new pointer. If it fails, it returns NULL β and the old pointer is still valid. This is the one trap: don't do buf = realloc(buf, 200); without checking β if it returns NULL, you've lost the original pointer (leak + can't free).
buf = realloc(buf, 200); // if NULL β you leaked the original buf!
Always use a temporary variable, check for NULL, then reassign. As shown above.
| Function | Does | Notes |
|---|---|---|
malloc(n) | Allocates n bytes. Uninitialized. | Returns void * or NULL. |
calloc(count, size) | Allocates count Γ size bytes. Zeroed. | Safer default for arrays/structs. |
realloc(ptr, n) | Resizes allocation to n bytes. Preserves data. | Check for NULL β old pointer stays valid on failure. |
free(ptr) | Returns memory to the heap. | Only free what you malloc'd. One time only. |
char *s = malloc(6); strcpy(s, "Hello"); printf("%s\n", s);Show answer
Two things. First: no#include <stdlib.h>β the compiler may not seemalloc's declaration, defaulting to assuming it returnsint, which will corrupt the pointer on 64-bit systems. Second: noNULLcheck β ifmallocfails,strcpywrites to address 0. Both are real bugs, even if they don't crash on your machine today.
free(p), is the memory zeroed?free returns the memory to the heap's internal bookkeeping. The bytes are still there, unchanged. The data may or may not persist β it's undefined. Some implementations overwrite it with debug patterns in debug mode, but you cannot rely on that. The only guarantee is that you no longer own those bytes.
malloc'd but never free'd. The pointer was lost (went out of scope, was overwritten) before free was called. The OS can't reclaim it until the program exits. Example: char *s = malloc(100); s = "hello"; β the malloc'd 100 bytes are now unreachable (the pointer was overwritten by a pointer to a string literal). The original 100 bytes are leaked.
malloc(10 * sizeof(int)) β how many bytes, and how many ints?sizeof(int) = 4: 40 bytes, which is room for 10 ints. The sizeof multiplication is what converts "I want 10 ints" into "I need 40 bytes." If you wrote malloc(10) you'd only get 10 bytes β room for 2 ints and 2 bytes left over. sizeof is the conversion factor.
Write a function mystrdup(const char *src) that returns a heap-allocated copy of src:
strlen to find the length. Allocate len + 1 bytes with malloc.NULL. If allocation fails, return NULL.strcpy, or your mystrcpy from lesson 5).free it.main: call mystrdup("Hello, world!"), print the copy, then free it.const). But confirm the copy is independent by printing its address and the literal's address β they'll differ.mystrdup(NULL) β does it crash? Add a guard: if src == NULL, return NULL early.#include <stdio.h>
#include <stdlib.h>
#include <string.h>
char *mystrdup(const char *src) {
if (src == NULL) return NULL;
size_t len = strlen(src);
char *copy = malloc(len + 1);
if (copy == NULL) return NULL;
// Copy using lesson 5's compact form
char *dst = copy;
while (*src != '\0') {
*dst++ = *src++;
}
*dst = '\0';
return copy;
}
int main(void) {
char *s = mystrdup("Hello, world!");
if (s) {
printf("Copy: %s\n", s);
printf("Address of copy: %p\n", (void *)s);
free(s);
}
return 0;
}
malloc/calloc, resize it with realloc, and release it with free. You know the three heap sins (use-after-free, double-free, leak) and how to avoid them. You can write a function that returns heap memory and document the ownership contract. And you understand when to use the stack vs the heap β the decision is driven by whether size is known at compile time.
realloc in practiceLesson 6 covers the basics. A future lesson will revisit realloc in depth β building a dynamically-growing string or array that starts small and grows as needed. That's where realloc becomes essential. For now, the pattern to remember is: malloc β use β free, always in pairs, always check for NULL.
malloc and new? (C++ question β skip for now)", "what happens if I free a stack pointer?", "how do I see memory leaks with tools?" β type it and we'll dig in. You now have the full picture of C's three memory regions.
malloc β exact semantics, return type, and the full family.realloc β the return-value trap is documented here.