8 Allocating Memory
Every variable in your program lives somewhere in memory, but not all memory is created equal. Understanding where variables live — and how long they last — is important for writing correct C programs. Managing lifetimes is challenging enough that newer languages like Java and Rust were designed to do it for you.
8.1 Global Variables
A global variable is declared outside of any function. It is created when the program starts and exists until the program exits:
#include <stdio.h>
int high_score = 0; // global — lives for the entire program
void update_score(int points) {
if (points > high_score) {
high_score = points;
}
}
int main(void) {
update_score(1000);
update_score(500);
printf("High score: %d\n", high_score); // 1000
return 0;
}Global variables are visible to every function defined after them in the file. They are convenient but can make programs harder to reason about, since any function can change them.
Tip: Use global variables sparingly. When every function can read and write the same variable, bugs become harder to track down. Prefer passing data through function parameters.
8.2 Static Global Variables
Marking a global variable static keeps its lifetime — the entire program — but limits its visibility to the file it is declared in:
static int cache_hits = 0; // only this .c file can see itWithout static, any other .c file can reach a global by declaring it extern. With static, the variable is invisible outside its own file, so two files can each have their own static int cache_hits without clashing. C++ gets the same effect with an anonymous namespace. If a global variable is only used inside one file, make it static.
Wut: static means two different things in C. On a global variable it does not change lifetime (globals already live for the whole program) — it hides the variable from other files. On a local variable it does not change visibility — it extends the lifetime, as you will see in Static Local Variables below.
8.3 Local Variables
A local variable is declared inside a function (or block). It is created when the function is called and destroyed when the function returns:
void greet(void) {
char message[] = "Hola, amigo"; // local — only during greet()
printf("%s\n", message);
}
// message is gone once greet() returnsLocal variables live on the stack — a region of memory that grows and shrinks automatically as functions are called and return. You do not need to free stack memory; it is reclaimed automatically.
Trap: Never return a pointer to a local variable. The memory is reclaimed when the function returns, and the pointer becomes a dangling pointer — it points to memory that no longer belongs to you:
int *bad(void) {
int x = 42;
return &x; // BUG: x is destroyed when bad() returns
}8.4 Static Local Variables
A static local variable has the scope of a local variable but the lifetime of a global. It is declared inside a function with the static keyword, created once when the program starts, and retains its value between calls:
#include <stdio.h>
void count_calls(void) {
static int count = 0; // initialized once, persists between calls
count++;
printf("Called %d time(s)\n", count);
}
int main(void) {
count_calls(); // Called 1 time(s)
count_calls(); // Called 2 time(s)
count_calls(); // Called 3 time(s)
return 0;
}Without static, count would be reset to 0 on every call. With static, it lives in the data segment (like a global) but is only accessible inside count_calls.
8.5 Variable-Length Arrays
Sometimes you do not know how big an array needs to be until the program is running — the size comes from user input, a function argument, or a file you just read. C has variable-length arrays (VLAs) for exactly this situation: local arrays whose length is computed at runtime. VLAs are worth learning because they are the natural thing to write — just put a variable between the brackets — and you will see them in other people’s code. They are also, as you will see, usually a bad idea: the convenience comes with dangers that keep them out of most style guides.
void print_squares(int n) {
int squares[n]; // length decided at runtime
for (int i = 0; i < n; i++) {
squares[i] = i * i;
}
for (int i = 0; i < n; i++) {
printf("%d ", squares[i]);
}
printf("\n");
}A VLA lives on the stack like any other local variable and is reclaimed automatically when it goes out of scope — no cleanup required.
VLAs are the one exception to the rule from the Variables chapter that sizeof costs nothing at runtime: the compiler cannot know sizeof squares in advance, so it computes n * sizeof(int) when the line executes.
Wut: VLAs never made it into C++. g++ accepts them as an extension, but -pedantic warns ISO C++ forbids variable length array, and standard C++ uses std::vector instead. Even in C they are an optional feature — a compiler can conform without supporting them — so very portable code avoids them.
Trap: There is no way to check whether a VLA allocation succeeded. If n is large, int squares[n] silently overflows the stack and crashes. Only use a VLA when you can bound the size to something small; for anything large or user-controlled, use malloc.
8.6 Dynamic Allocation: malloc and free
void *malloc(size_t size);
void free(void *ptr);Sometimes you need memory that outlives the function that created it, or memory whose size you do not know at compile time. For this, C provides malloc and free from <stdlib.h>.
malloc allocates a block of memory on the heap and returns a pointer to it. A heap allocation persists until you explicitly release it:
#include <stdio.h>
#include <stdlib.h>
int main(void) {
int *nums = malloc(5 * sizeof(int));
if (nums == NULL) {
printf("Allocation failed!\n");
return 1;
}
for (int i = 0; i < 5; i++) {
nums[i] = (i + 1) * 10;
}
for (int i = 0; i < 5; i++) {
printf("nums[%d] = %d\n", i, nums[i]);
}
free(nums); // release the memory back to the system
return 0;
}malloc returns a void * — a generic pointer that can be assigned to any pointer type without a cast in C. It returns NULL if the allocation fails. Always check for NULL after calling malloc. Having said that, there is a school of thought held by some very good engineers that NULL checks just clutter the code. To handle NULL gracefully, there will be a check and logic every place where it could be NULL, so you end up with checks and logic that are rarely used and almost never tested. If you are out of memory, you’ll probably need to shut down the program, so the reasoning goes that if you try to use a NULL pointer, the CPU will do the check for you. For safety-critical systems, the above argument does not hold, although those systems often forbid the use of dynamically allocated memory entirely.
Tip: There are no smart pointers in C. There is no RAII. There is no garbage collector. If you call malloc, you must call free when you are done. If you forget, you leak memory. If you call free twice on the same pointer, you get undefined behavior. If you use a pointer after freeing it, you get undefined behavior. Memory management in C is entirely your responsibility.
calloc is a variant that allocates memory and initializes it to zero:
void *calloc(size_t count, size_t size);int *nums = calloc(5, sizeof(int)); // 5 ints, all initialized to 0You could achieve the same result with malloc and a follow-up zero fill (using memset, covered later in this chapter), but calloc has two advantages beyond the convenience of a single call. First, calloc checks whether count * size overflows before allocating — malloc(count * size) silently wraps around if the multiplication overflows, and you get a buffer far smaller than you asked for. Second, on Linux and other systems with demand-paged virtual memory, calloc can skip the zeroing for large allocations entirely. The kernel hands out fresh pages that are already zero-filled, so calloc simply returns them without touching a byte. Writing zeroes by hand forces every page to be faulted in and dirtied, costing real time and real memory.
Tip: Reach for calloc whenever you want zero-initialized memory. For large buffers it is often measurably faster than allocating and then zeroing by hand, because the kernel has already done the zeroing.
And realloc lets you resize a previously allocated block:
void *realloc(void *ptr, size_t size);nums = realloc(nums, 10 * sizeof(int)); // grow to 10 ints (see Trap)
Trap: Never assign the result of realloc directly back to the same pointer. If realloc fails, it returns NULL and the original memory is not freed — so nums = realloc(nums, ...) loses your only pointer to the original block, causing a memory leak. Use a temporary pointer instead:
int *tmp = realloc(nums, 10 * sizeof(int));
if (tmp == NULL) {
// handle error — nums is still valid
} else {
nums = tmp;
} 8.7 Working with Raw Memory: memcpy and memset
Two functions from <string.h> (yeah, it’s a counterintuitive name…) operate on raw bytes rather than strings. You will see them constantly in C code:
memset fills a block of memory with a byte value. It is commonly used to zero out a buffer:
void *memset(void *s, int c, size_t n);int nums[10];
memset(nums, 0, sizeof(nums)); // set all bytes to 0memcpy copies a block of bytes from one location to another. Unlike strcpy, it does not stop at a '\0' — you tell it exactly how many bytes to copy:
void *memcpy(void *dest, const void *src, size_t n);int src[] = {10, 20, 30};
int dest[3];
memcpy(dest, src, sizeof(src)); // all 12 bytes (3 ints × 4 bytes)
Trap: memcpy requires that the source and destination do not overlap. If they might overlap (e.g., shifting elements within the same array), use memmove instead, which handles overlapping regions correctly.
void *memmove(void *dest, const void *src, size_t n);8.8 Where Variables Live: A Summary
| Kind | Where | Lifetime | Example |
|---|---|---|---|
| Global | Data segment | Entire program | int count = 0; (outside functions) |
| Static global | Data segment | Entire program | static int n = 0; (outside functions) |
| Local | Stack | Until function returns | int x = 5; (inside a function) |
| Static local | Data segment | Entire program | static int n = 0; (inside a function) |
| VLA | Stack | Until scope exits | int a[n]; (runtime n) |
| Dynamic | Heap | Until you call free | int *p = malloc(...) |
8.9 Try It: Memory Lifetimes
#include <stdio.h>
#include <stdlib.h>
int total = 0; // global — lives for the whole program
void add_to_total(int n) {
int local = n; // local — gone when add_to_total returns
total += local;
}
int main(void) {
add_to_total(10);
add_to_total(20);
printf("Total: %d\n", total); // 30
// dynamic — lives until we free it
int *data = malloc(3 * sizeof(int));
if (data == NULL) return 1;
data[0] = 1985;
data[1] = 1986;
data[2] = 1987;
for (int i = 0; i < 3; i++) {
printf("data[%d] = %d\n", i, data[i]);
}
free(data);
return 0;
}8.10 Key Points
- Global variables live for the entire program; local variables live only until the function returns.
- A
staticglobal keeps the same lifetime but is visible only in its own file. - Static local variables have the scope of a local but the lifetime of a global.
- A VLA is a local array sized at runtime. It lives on the stack, so keep it small — use
mallocfor large or unbounded sizes. mallocallocates memory on the heap. You must callfreewhen done.callocallocates and zeroes memory.reallocresizes an allocation.memcpycopies bytes between non-overlapping regions. Usememmovefor overlapping regions.memsetfills a block of memory with a byte value.
8.11 Exercises
Think about it: Why would you choose
callocovermallocfollowed bymemsetto zero?What does this print?
#include <stdio.h> void counter(void) { static int n = 0; n++; printf("%d ", n); } int main(void) { counter(); counter(); counter(); return 0; }Calculation: On a system where
intis 32 bits, how many bytes doesmalloc(5 * sizeof(int))allocate?Where is the bug?
int *p = malloc(10 * sizeof(int)); for (int i = 0; i < 10; i++) { p[i] = i; } free(p); printf("%d\n", p[0]);Where is the bug?
int *a = malloc(5 * sizeof(int)); int *b = a; free(a); free(b);Write a program that uses
mallocto allocate an array ofnintegers (wherenis provided by the user viascanf), fills the array with squares (0, 1, 4, 9, …), prints them, and frees the memory.