C Pointers & Memory Management
Understand C pointers: declaration, dereferencing, pointer arithmetic, and dynamic memory with malloc/free. The most important topic in C.
C Pointers & Memory Management
Pointers are what make C powerful — and what makes it dangerous. Understanding them deeply is the difference between writing correct C and producing crashes.
What is a Pointer?
A pointer is a variable that stores a memory address.
int x = 42;
int* ptr = &x; // ptr holds the address of x
printf("Value of x: %d\n", x); // 42
printf("Address of x: %p\n", &x); // 0x7fff... (some address)
printf("Value of ptr: %p\n", ptr); // same address as &x
printf("Dereferenced ptr: %d\n", *ptr); // 42 — read the value at ptr
// Modify through pointer
*ptr = 100;
printf("x is now: %d\n", x); // 100Passing by Pointer (Modify Arguments)
// Pass by value — can't modify original
void double_value_wrong(int n) {
n *= 2; // only changes the local copy
}
// Pass by pointer — CAN modify original
void double_value(int* n) {
*n *= 2; // modifies what n points to
}
int x = 5;
double_value(&x); // pass address of x
printf("%d\n", x); // 10Returning multiple values
// min and max in one call
void min_max(const int* arr, int n, int* out_min, int* out_max) {
if (n == 0) return;
*out_min = arr[0];
*out_max = arr[0];
for (int i = 1; i < n; i++) {
if (arr[i] < *out_min) *out_min = arr[i];
if (arr[i] > *out_max) *out_max = arr[i];
}
}
int nums[] = {3, 1, 4, 1, 5, 9, 2};
int mn, mx;
min_max(nums, 7, &mn, &mx);
printf("Min: %d, Max: %d\n", mn, mx); // Min: 1, Max: 9Pointers and Arrays
Arrays and pointers are closely related in C.
int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr; // ptr points to arr[0] (no & needed for arrays)
// Equivalent ways to access elements
printf("%d\n", arr[2]); // 30
printf("%d\n", *(ptr+2)); // 30 — pointer arithmetic
printf("%d\n", ptr[2]); // 30 — pointer used like array
// Pointer arithmetic
ptr++; // ptr now points to arr[1]
printf("%d\n", *ptr); // 20Passing arrays to functions
// These two declarations are equivalent:
void print_array(int* arr, int n);
void print_array(int arr[], int n);
void print_array(int* arr, int n) {
for (int i = 0; i < n; i++) {
printf("%d ", arr[i]);
}
printf("\n");
}Stack vs Heap
| | Stack | Heap |
|---|---|---|
| Allocation | Automatic | Manual (malloc/free) |
| Size | Limited (~1-8 MB) | Large (limited by RAM) |
| Lifetime | Until function returns | Until free() is called |
| Speed | Fast | Slower |
void stack_example(void) {
int x = 10; // stack — freed when function returns
int arr[100]; // stack — 400 bytes on stack
// Do NOT return a pointer to a local variable!
}
// Heap allocation
int* heap_example(int n) {
int* arr = malloc(n * sizeof(int)); // allocate on heap
if (arr == NULL) {
fprintf(stderr, "malloc failed\n");
return NULL;
}
// ... use arr ...
return arr; // caller must free this!
}Dynamic Memory — malloc, calloc, realloc, free
#include <stdlib.h>
// malloc — allocate uninitialized memory
int* arr = malloc(10 * sizeof(int));
if (arr == NULL) { /* handle error */ }
// calloc — allocate zero-initialized memory
int* zeros = calloc(10, sizeof(int)); // all elements are 0
// realloc — resize existing allocation
arr = realloc(arr, 20 * sizeof(int));
if (arr == NULL) { /* original arr may be leaked! use temp ptr */ }
// free — release memory
free(arr);
arr = NULL; // set to NULL after free — prevents use-after-freeDynamic array pattern
typedef struct {
int* data;
int size;
int capacity;
} IntArray;
IntArray* create_array(int initial_capacity) {
IntArray* arr = malloc(sizeof(IntArray));
if (!arr) return NULL;
arr->data = malloc(initial_capacity * sizeof(int));
if (!arr->data) { free(arr); return NULL; }
arr->size = 0;
arr->capacity = initial_capacity;
return arr;
}
int push(IntArray* arr, int value) {
if (arr->size >= arr->capacity) {
int new_cap = arr->capacity * 2;
int* new_data = realloc(arr->data, new_cap * sizeof(int));
if (!new_data) return -1; // allocation failed
arr->data = new_data;
arr->capacity = new_cap;
}
arr->data[arr->size++] = value;
return 0; // success
}
void free_array(IntArray* arr) {
if (arr) {
free(arr->data);
free(arr);
}
}Pointers to Structs
typedef struct {
char name[50];
int age;
} Person;
Person* create_person(const char* name, int age) {
Person* p = malloc(sizeof(Person));
if (!p) return NULL;
strncpy(p->name, name, sizeof(p->name) - 1);
p->name[sizeof(p->name) - 1] = '\0';
p->age = age;
return p;
}
Person* alice = create_person("Alice", 30);
// Two ways to access struct members through a pointer
printf("%s\n", (*alice).name); // dereference then access
printf("%s\n", alice->name); // arrow operator — equivalent, cleaner
free(alice);
alice = NULL;Common Pointer Bugs
// 1. NULL pointer dereference
int* p = NULL;
// *p = 5; // CRASH — segmentation fault
// 2. Dangling pointer
int* p2 = malloc(sizeof(int));
free(p2);
// *p2 = 5; // undefined behavior — memory was freed
// 3. Memory leak
int* p3 = malloc(100 * sizeof(int));
// function returns without free(p3) — leak!
// 4. Double free
int* p4 = malloc(sizeof(int));
free(p4);
// free(p4); // undefined behavior
// 5. Buffer overflow
int arr[5];
// arr[10] = 99; // writes past array — corrupts memoryKey Takeaways
- A pointer stores an address — dereference with
*to get the value - Use
&to get the address of a variable,*to dereference a pointer - The
->operator is shorthand for(*ptr).member - Always check
mallocreturn value — it returns NULL on failure - Every
mallocneeds exactly onefree— set pointer to NULL after freeing
Enjoyed this article?
Explore the Backend Systems learning path for more.
Found this helpful?
Leave a comment
Have a question, correction, or just found this helpful? Leave a note below.