5. Pointers and Memory Management
Learning Objectives
- Define a pointer and explain how it differs from an ordinary variable
- Declare and dereference pointers, and perform pointer arithmetic on arrays
- Distinguish static memory allocation from dynamic memory allocation
- Use
malloc,calloc,realloc, andfreecorrectly in C, andnew/deletein C++ - Identify memory leaks, dangling pointers, and double-free errors from code
- Apply best practices that prevent common memory management bugs
Quick Answer
A pointer is a variable that stores a memory address instead of an ordinary value — it "points to" the location where another variable actually lives. Pointers matter because they let programs manipulate data indirectly and efficiently: passing a large array by pointer avoids copying it, and dynamic memory allocation (malloc/new) lets a program request memory at runtime instead of guessing sizes at compile time. In languages like C and C++, this power comes with responsibility — you must free memory you allocate, and using memory after freeing it or freeing it twice causes undefined behavior. Managed languages like Python and Java hide this complexity behind automatic garbage collection, which is why pointer bugs are almost exclusively a C/C++ concern.
What Are Pointers?
A pointer is a variable whose value is the memory address of another variable. Instead of holding data directly, it holds a reference to where that data lives.
#include <stdio.h>
int main() {
int age = 25;
int *ptr = &age; // ptr stores the ADDRESS of age
printf("Value of age: %d\n", age);
printf("Address of age: %p\n", (void*)&age);
printf("Value stored in ptr: %p\n", (void*)ptr);
printf("Value pointed to by ptr: %d\n", *ptr); // dereference
return 0;
}
Three symbols matter here:
&age— the address-of operator, giving the memory address ofage.int *ptr— declaresptras a pointer to anint.*ptr— the dereference operator, giving the value stored at the addressptrholds.
A helpful mental model: if a variable is a labeled box holding a value, a pointer is a sticky note that has the box's street address written on it, not the value itself. Following the address (*ptr) gets you to the actual box.
Pointer Arithmetic
Because arrays are stored as contiguous blocks of memory, pointers can "walk" across them using arithmetic.
#include <stdio.h>
int main() {
int numbers[] = {10, 20, 30, 40, 50};
int *ptr = numbers; // array name decays to a pointer to its first element
for (int i = 0; i < 5; i++) {
printf("numbers[%d] = %d (via *(ptr + %d))\n", i, *(ptr + i), i);
}
return 0;
}
ptr + i doesn't add i bytes — it adds i * sizeof(int) bytes, because the compiler knows ptr points to ints. This is why *(ptr + i) is exactly equivalent to numbers[i]; array indexing is pointer arithmetic in disguise.
Why It Matters
Pointers are the mechanism behind passing large data structures efficiently (by reference instead of by value), building dynamic data structures like linked lists and trees (each node holds a pointer to the next), and implementing dynamic memory allocation. Without pointers, C would have no way to build a structure whose size isn't known until runtime.
Memory Management
Memory management is how a program acquires, uses, and releases memory during execution. Doing it well prevents crashes, memory leaks, and security vulnerabilities; doing it poorly is one of the most common sources of bugs in C and C++ programs.
Static vs. Dynamic Allocation
Static allocation happens at compile time — the size is fixed and known in advance.
int num = 10; // stack-allocated, fixed size, freed automatically
int scores[100]; // fixed-size array, also automatic
Dynamic allocation happens at runtime, using the heap, when you don't know the size until the program is running (e.g., reading a file of unknown length).
#include <stdio.h>
#include <stdlib.h>
int main() {
int size;
printf("How many scores? ");
scanf("%d", &size);
int *scores = (int *)malloc(size * sizeof(int)); // allocate at runtime
if (scores == NULL) {
printf("Memory allocation failed\n");
return 1;
}
for (int i = 0; i < size; i++) {
scores[i] = i * 10;
}
for (int i = 0; i < size; i++) {
printf("scores[%d] = %d\n", i, scores[i]);
}
free(scores); // release the memory back to the system
scores = NULL; // avoid a dangling pointer
return 0;
}
The four core C functions for dynamic memory:
| Function | Purpose |
|---|---|
malloc(size) | Allocates size bytes; contents are uninitialized (garbage) |
calloc(n, size) | Allocates n * size bytes, initialized to zero |
realloc(ptr, newSize) | Resizes a previously allocated block |
free(ptr) | Releases the memory back to the heap |
In C++, the equivalent operators are new and delete:
#include <iostream>
int main() {
int *ptr = new int(100); // allocate and initialize
std::cout << "Value: " << *ptr << std::endl;
delete ptr; // deallocate
ptr = nullptr; // avoid dangling pointer
return 0;
}
The Three Classic Bugs
1. Memory leak — allocated memory is never freed, so it stays reserved for the life of the program.
int *ptr = (int *)malloc(sizeof(int));
*ptr = 5;
// function returns without calling free(ptr) — leaked!
2. Dangling pointer — a pointer still refers to memory that has already been freed.
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
*ptr = 10; // undefined behavior: writing through a freed pointer
3. Double free — calling free() on the same pointer twice, which corrupts the heap's internal bookkeeping.
int *ptr = (int *)malloc(sizeof(int));
free(ptr);
free(ptr); // undefined behavior
Why It Matters in Practice
Memory leaks in a long-running server (like a web backend) accumulate until the process runs out of memory and crashes — this is why production C/C++ services are routinely checked with tools like Valgrind or AddressSanitizer. Dangling pointers and double frees are also a major source of security vulnerabilities (use-after-free bugs are a common exploit vector in browsers and operating systems), which is a big reason the industry has been moving toward memory-safe languages like Rust for systems programming.
Best Practices
- Every
malloc/newshould have exactly one matchingfree/delete. - Set pointers to
NULL/nullptrimmediately after freeing them. - Never use a pointer after it has been freed — check for
NULLbefore dereferencing. - Use tools like Valgrind, AddressSanitizer, or a modern C++ smart pointer (
std::unique_ptr,std::shared_ptr) to avoid manual bookkeeping entirely.
Key Terms
| Term | Definition | Related Concept |
|---|---|---|
| Pointer | A variable that stores the memory address of another variable | Dereference, Address-of |
Dereference (*) | The operation of accessing the value stored at a pointer's address | Pointer |
Address-of (&) | The operator that returns a variable's memory address | Pointer |
| Stack | Memory region for automatic, fixed-size, function-scoped variables | Static Allocation |
| Heap | Memory region for dynamically allocated memory, managed manually in C/C++ | Dynamic Allocation |
| malloc/free | C functions to allocate and release heap memory | Dynamic Allocation |
| new/delete | C++ operators to allocate and release heap memory | Dynamic Allocation |
| Memory Leak | Allocated memory that is never freed | free(), Garbage Collection |
| Dangling Pointer | A pointer referencing memory that has already been freed | Use-After-Free |
| Double Free | Calling free() twice on the same memory address | Undefined Behavior |
Common Mistakes
Misconception: Once you call free(ptr), the pointer itself becomes empty or NULL automatically.
Why it's wrong: free() releases the memory back to the heap allocator, but the pointer variable still holds the old (now invalid) address unless you explicitly reset it. This is exactly how dangling pointers happen.
Correct understanding: Always manually set the pointer to NULL (C) or nullptr (C++) right after freeing it.
Misconception: Pointers and arrays are exactly the same thing.
Why it's wrong: An array name decays into a pointer to its first element in most expressions, which makes them behave similarly, but sizeof(array) gives the total array size while sizeof(pointer) gives only the size of the pointer itself (typically 8 bytes on a 64-bit system). Arrays also can't be reassigned to point elsewhere; pointers can.
Correct understanding: Arrays and pointers are related but distinct — pointer arithmetic works on both, but memory layout and reassignability differ.
Misconception: Garbage-collected languages like Python and Java don't need you to think about memory at all.
Why it's wrong: Garbage collection automates freeing memory, but you can still cause "leaks" by keeping unnecessary references alive (e.g., growing a global list forever, or forgetting to close a file/database connection), which prevents the garbage collector from reclaiming that memory.
Correct understanding: Managed languages remove manual free() calls, but mindful resource management (closing files, breaking unneeded references) is still required for healthy long-running programs.
Comparison and Connections
| Feature | C (manual) | C++ (manual/smart pointers) | Python / Java (managed) |
|---|---|---|---|
| Allocation | malloc/calloc/realloc | new / smart pointers | Implicit (x = SomeClass()) |
| Deallocation | free() — must be explicit | delete or automatic (smart pointers) | Automatic garbage collection |
| Common bugs | Leaks, dangling pointers, double free | Same as C if not using smart pointers | Rare; mostly reference cycles or unclosed resources |
| Performance | Fast, predictable, no GC pause | Fast, predictable | Slight overhead from garbage collector |
| Developer responsibility | High — must track every allocation | Medium — smart pointers reduce burden | Low — runtime manages memory |
Practice Questions
Recall
-
What does the
*symbol mean when it appears (a) in a declaration likeint *ptr, and (b) in an expression like*ptr = 5? Look for: (a) declaresptras a pointer to an int; (b) dereferencesptr, meaning "the value at the addressptrholds." -
Name the four standard C functions used for dynamic memory management and state what each does. Look for:
malloc(allocate, uninitialized),calloc(allocate, zeroed),realloc(resize),free(release).
Understanding
-
Explain why
numbers[i]and*(ptr + i)produce the same result whenptrpoints to the start of the arraynumbers. Look for: array indexing is defined in terms of pointer arithmetic;ptr + iadvances byi * sizeof(element type)bytes, landing exactly on the address ofnumbers[i]. -
Why is a dangling pointer more dangerous than a
NULLpointer? Look for: dereferencingNULLtypically crashes immediately and predictably (segfault), which is easy to catch; dereferencing a dangling pointer may silently "work" by accessing memory that's been reused by something else, corrupting data unpredictably and making the bug much harder to trace.
Application
-
Write C code that dynamically allocates an array of
ndoubles, fills it with their squares (1, 4, 9, ...), prints them, and frees the memory correctly. Look for:malloc(n * sizeof(double)), a NULL check, a loop assigning(i+1)*(i+1), a print loop, and afree()call at the end. -
Identify the bug in this code and fix it:
int *create_array(int size) {
int arr[size];
return arr;
}
Look for: arr is a local stack array that is destroyed when the function returns, so the returned pointer is dangling. Fix: allocate with malloc(size * sizeof(int)) on the heap instead, so it survives after the function returns (caller must free() it later).
Analysis
-
Compare manual memory management (C) with automatic garbage collection (Java/Python) in terms of performance and safety trade-offs. Look for: manual management gives predictable performance and no GC pauses but risks leaks/dangling pointers/double frees if done incorrectly; garbage collection removes those specific bug classes but adds runtime overhead and occasional pause times, and can still "leak" via lingering references.
-
A long-running server written in C slowly consumes more and more memory over days of operation, even though it never crashes. What is likely happening, and how would you diagnose it? Look for: a memory leak — some allocation path is missing its matching
free(), likely on an error-handling branch that returns early. Diagnose with Valgrind or AddressSanitizer to find allocations that are never freed, and check every early-return/error path in allocation-heavy functions.
FAQ
Q: What's the difference between a "null pointer" and a "dangling pointer"?
A null pointer explicitly points to nothing (NULL or nullptr) and dereferencing it fails predictably and immediately. A dangling pointer still holds an address that used to be valid but has since been freed or gone out of scope — dereferencing it is undefined behavior that might work by accident, corrupt unrelated memory, or crash, making it much harder to debug.
Q: Why does C require manual memory management instead of just doing it automatically like Python? C was designed in the 1970s for systems programming (operating systems, embedded devices) where predictable, minimal-overhead performance mattered more than programmer convenience. Automatic garbage collection adds runtime overhead and occasional unpredictable pauses, which is unacceptable for real-time systems, device drivers, or performance-critical code — so C leaves the decision (and responsibility) to the programmer.
Q: Are smart pointers in C++ a complete replacement for manual new/delete?
In modern C++ (C++11 and later), std::unique_ptr and std::shared_ptr handle deallocation automatically when the pointer goes out of scope, eliminating most manual memory bugs. They're strongly recommended for new code. However, understanding raw pointers is still essential for reading legacy code, working with C libraries, and understanding what the smart pointers are doing underneath.
Q: Can a pointer point to another pointer?
Yes — this is called a pointer to a pointer, or a double pointer, written int **ptr. It's commonly used when a function needs to modify a pointer's value itself (not just the data it points to), such as functions that allocate memory and need to hand the new pointer back through a parameter.
Q: Why do we get a segmentation fault, and what does it actually mean?
A segmentation fault occurs when a program tries to access memory it doesn't have permission to use — for example, dereferencing a NULL pointer, a dangling pointer, or writing past the end of an array. The operating system's memory protection detects the illegal access and immediately terminates the program rather than letting it corrupt other processes' memory.
Quick Revision
- A pointer stores a memory address;
&gets an address,*dereferences a pointer to get the value there. - Pointer arithmetic on an array pointer advances by
sizeof(element type)bytes per unit, matching array indexing exactly. - Static allocation happens at compile time on the stack with a fixed size; dynamic allocation happens at runtime on the heap with a size chosen while running.
malloc/calloc/realloc/freemanage heap memory in C;new/deletedo the same in C++.- Memory leak = allocated memory never freed; dangling pointer = pointer to already-freed memory; double free = freeing the same block twice.
- Always set a pointer to
NULL/nullptrimmediately after freeing it to prevent accidental dangling-pointer use. - Valgrind and AddressSanitizer are standard tools for detecting leaks and invalid memory access in C/C++.
- Garbage-collected languages (Python, Java) automate freeing memory but can't prevent leaks caused by unnecessary lingering references.
- Modern C++ smart pointers (
unique_ptr,shared_ptr) automate deallocation and are preferred over rawnew/deletein new code. - Segmentation faults happen when a program accesses memory it isn't allowed to touch, often from a bad pointer.
Related Topics
Prerequisites: Introduction to Programming, Variables and Data Types, Functions and Recursion
Related Topics: Data Structures (linked lists and trees rely on pointers), Debugging and Testing (Valgrind, sanitizers), Operating Systems (virtual memory, stack vs. heap)
Next Topics: File Handling, Basics of OOP, Data Structures and Algorithms