Skip to main content

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, and free correctly in C, and new/delete in 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 of age.
  • int *ptr — declares ptr as a pointer to an int.
  • *ptr — the dereference operator, giving the value stored at the address ptr holds.

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:

FunctionPurpose
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/new should have exactly one matching free/delete.
  • Set pointers to NULL/nullptr immediately after freeing them.
  • Never use a pointer after it has been freed — check for NULL before 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

TermDefinitionRelated Concept
PointerA variable that stores the memory address of another variableDereference, Address-of
Dereference (*)The operation of accessing the value stored at a pointer's addressPointer
Address-of (&)The operator that returns a variable's memory addressPointer
StackMemory region for automatic, fixed-size, function-scoped variablesStatic Allocation
HeapMemory region for dynamically allocated memory, managed manually in C/C++Dynamic Allocation
malloc/freeC functions to allocate and release heap memoryDynamic Allocation
new/deleteC++ operators to allocate and release heap memoryDynamic Allocation
Memory LeakAllocated memory that is never freedfree(), Garbage Collection
Dangling PointerA pointer referencing memory that has already been freedUse-After-Free
Double FreeCalling free() twice on the same memory addressUndefined 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

FeatureC (manual)C++ (manual/smart pointers)Python / Java (managed)
Allocationmalloc/calloc/reallocnew / smart pointersImplicit (x = SomeClass())
Deallocationfree() — must be explicitdelete or automatic (smart pointers)Automatic garbage collection
Common bugsLeaks, dangling pointers, double freeSame as C if not using smart pointersRare; mostly reference cycles or unclosed resources
PerformanceFast, predictable, no GC pauseFast, predictableSlight overhead from garbage collector
Developer responsibilityHigh — must track every allocationMedium — smart pointers reduce burdenLow — runtime manages memory

Practice Questions

Recall

  1. What does the * symbol mean when it appears (a) in a declaration like int *ptr, and (b) in an expression like *ptr = 5? Look for: (a) declares ptr as a pointer to an int; (b) dereferences ptr, meaning "the value at the address ptr holds."

  2. 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

  1. Explain why numbers[i] and *(ptr + i) produce the same result when ptr points to the start of the array numbers. Look for: array indexing is defined in terms of pointer arithmetic; ptr + i advances by i * sizeof(element type) bytes, landing exactly on the address of numbers[i].

  2. Why is a dangling pointer more dangerous than a NULL pointer? Look for: dereferencing NULL typically 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

  1. Write C code that dynamically allocates an array of n doubles, 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 a free() call at the end.

  2. 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

  1. 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.

  2. 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/free manage heap memory in C; new/delete do 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/nullptr immediately 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 raw new/delete in new code.
  • Segmentation faults happen when a program accesses memory it isn't allowed to touch, often from a bad pointer.

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