Senior 7 min · March 06, 2026

Pointers in C — Preventing Use-After-Free Crashes

Intermittent SIGSEGV after hours of operation in embedded sensor firmware traced to a dangling pointer after free() — fix by NULL assignment.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Pointers store memory addresses, not values – & gets an address, * dereferences to access the value.
  • Pointer types encode byte-width (e.g., int reads 4 bytes, char reads 1), ensuring correct memory access.
  • Passing a pointer to a function avoids copying large data – it's C's pass-by-reference mechanism.
  • Dynamic memory (malloc/free) returns a pointer to heap memory; forgetting free leaks memory, double‑free corrupts the allocator.
  • Array names decay to pointers when passed to functions, losing size information – always pass a separate length parameter.
  • The most common pointer bug: dereferencing an uninitialized or freed pointer leads to undefined behavior (crash or silent corruption).
Plain-English First

Imagine your house has a street address. The house is your data — the actual stuff you care about. A pointer is just that street address written on a piece of paper. You can hand that piece of paper to anyone, they can find your house and change what's inside — without you having to move the house itself. That's all a pointer is: a variable that holds an address, not the data directly.

Every time you run a C program, the operating system hands it a chunk of memory. Variables live in that memory, each one sitting at a specific address — like houses on a street. Pointers let you work with those addresses directly, and that capability is the single reason C can do things most high-level languages can't: build operating systems, write device drivers, manage memory with surgical precision, and pass huge data structures around without copying a single byte.

What a Pointer Actually Is — Memory Addresses Made Concrete

When you declare int temperature = 72;, C reserves 4 bytes somewhere in memory and stores the value 72 there. That location has an address — think of it as a number like 0x7ffd3a2c. A pointer is a variable whose job is to hold that address.

The & operator gives you the address of any variable. The * operator — called the dereference operator — goes the other way: given an address, it gives you the value sitting at that address. These two operators are inverses of each other, and once that clicks, the rest of pointers makes sense.

Pointer types matter because the compiler needs to know how many bytes to read starting from an address. A char reads 1 byte, an int reads 4, a double* reads 8. The type encodes that byte‑width contract.

pointer_basics.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
#include <stdio.h>

int main(void) {
    int temperature = 72;          // a normal int living somewhere in memory
    int *temp_ptr = &temperature;  // temp_ptr holds the ADDRESS of temperature

    printf("Value of temperature    : %d\n", temperature);
    printf("Address of temperature  : %p\n", (void *)&temperature);
    printf("What temp_ptr holds     : %p\n", (void *)temp_ptr);   // same address
    printf("Value via dereference   : %d\n", *temp_ptr);          // goes to that address, reads value

    // Changing the value THROUGH the pointer
    *temp_ptr = 98;  // we went to the address and wrote a new value
    printf("temperature after *temp_ptr = 98 : %d\n", temperature); // temperature itself changed!

    return 0;
}
Output
Value of temperature : 72
Address of temperature : 0x7ffd3a2c4b1c
What temp_ptr holds : 0x7ffd3a2c4b1c
Value via dereference : 72
temperature after *temp_ptr = 98 : 98
Why the address changes each run:
Modern operating systems use ASLR (Address Space Layout Randomization) to place your program's memory at a random location each run as a security measure. The exact address you see will differ from the output above — but the relationship between the pointer's value and &temperature will always match.
Production Insight
Mismatch between pointer type and actual data type leads to reading incorrect bytes – a classic bug when casting void*.
Always use the exact type or cast carefully to avoid undefined behavior.
Production rule: if you see corrupted values but no crash, suspect type mismatch in dereference.
Key Takeaway
Pointer type encodes byte-width.
& and * are inverses.
A pointer is just an address – the type tells the compiler how many bytes to read.

Why Pointers Exist — Pass by Reference and Avoiding Costly Copies

C is a pass-by-value language. When you call a function with a variable, C copies that variable. For a single integer, that's fine. But imagine passing a 10,000-element array or a large struct by value on every function call — you'd be copying megabytes of data just to read a few fields. Pointers solve this completely.

By passing a pointer instead of the data, you pass just 8 bytes (on a 64-bit machine) regardless of how big the actual data is. The function then operates on the original data in place. This is what 'pass by reference' means in C — you're passing the address of the original, not a clone.

This pattern shows up everywhere: scanf needs a pointer so it can write back to your variable. qsort takes a comparator that receives pointers so it can compare without copying. String functions like strcpy operate on char* for exactly this reason.

pass_by_reference.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
#include <stdio.h>

// WITHOUT a pointer — this does NOT work as intended
void fahrenheit_to_celsius_broken(double fahrenheit) {
    fahrenheit = (fahrenheit - 32.0) * 5.0 / 9.0; // modifies a LOCAL COPY, caller never sees this
}

// WITH a pointer — this writes back to the caller's variable
void fahrenheit_to_celsius(double *fahrenheit_ptr) {
    // dereference to read the value, compute, then write result back to the same address
    *fahrenheit_ptr = (*fahrenheit_ptr - 32.0) * 5.0 / 9.0;
}

int main(void) {
    double boiling_point = 212.0;

    fahrenheit_to_celsius_broken(boiling_point);
    printf("After broken function   : %.2f F\n", boiling_point); // still 212 — unchanged

    fahrenheit_to_celsius(&boiling_point);  // pass the ADDRESS so the function can write back
    printf("After pointer function  : %.2f C\n", boiling_point); // now correctly 100

    return 0;
}
Output
After broken function : 212.00 F
After pointer function : 100.00 C
Pro Tip — The scanf Pattern:
Every time you write scanf("%d", &age), you're using a pointer. scanf needs to write into your variable, so you hand it the address. If you forget the &, scanf receives the value of age (garbage or zero), tries to treat it as an address, and your program crashes or corrupts memory — one of the most common beginner bugs in C.
Production Insight
Forgetting & in scanf causes a segmentation fault – every beginner hits this.
Passing large structs by value instead of pointer can cause stack overflow or performance degradation.
Rule: if a function needs to modify the caller's variable, pass a pointer.
Key Takeaway
C is pass-by-value.
Pointers are the only way to achieve pass-by-reference.
Use & to give functions write access to your variables.

Pointer Arithmetic — How Arrays and Pointers Are the Same Thing

Here's something that surprises most newcomers: in C, an array name is essentially a pointer to its first element. When you write scores[2], the compiler translates that to *(scores + 2) — 'start at the base address, move forward by 2 elements, then dereference.' These are literally identical operations.

Pointer arithmetic is scaled by the type. If int_ptr points at an int (4 bytes), then int_ptr + 1 doesn't add 1 to the address — it adds 4, landing exactly on the next integer. The compiler handles the byte math for you. This is why you must get the pointer type right.

Understanding this relationship is the key to writing efficient string processing, building your own data structures, and reading any C library source code. It's also why C arrays don't carry their own length — they're just a starting address, and the programmer is responsible for knowing where they end.

pointer_arithmetic.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
#include <stdio.h>

int main(void) {
    int daily_steps[5] = {8200, 11500, 6700, 9300, 10100};

    int *cursor = daily_steps;  // array name decays to pointer to first element

    printf("--- Iterating with pointer arithmetic ---\n");
    for (int day = 0; day < 5; day++) {
        // (cursor + day) computes the address of element [day]
        // The +day actually adds (day * sizeof(int)) bytes under the hood
        printf("Day %d: %d steps  (address: %p)\n",
               day + 1,
               *(cursor + day),
               (void *)(cursor + day));
    }

    printf("\n--- Proof that arr[i] and *(arr+i) are identical ---\n");
    printf("daily_steps[3]      = %d\n", daily_steps[3]);
    printf("*(daily_steps + 3)  = %d\n", *(daily_steps + 3)); // exact same result
    printf("*(cursor + 3)       = %d\n", *(cursor + 3));       // and again

    printf("\nAddress gap between elements: %ld bytes\n",
           (long)((cursor + 1) - cursor) * (long)sizeof(int)); // always sizeof(int) = 4

    return 0;
}
Output
--- Iterating with pointer arithmetic ---
Day 1: 8200 steps (address: 0x7ffd1a3c5020)
Day 2: 11500 steps (address: 0x7ffd1a3c5024)
Day 3: 6700 steps (address: 0x7ffd1a3c5028)
Day 4: 9300 steps (address: 0x7ffd1a3c502c)
Day 5: 10100 steps (address: 0x7ffd1a3c5030)
--- Proof that arr[i] and *(arr+i) are identical ---
daily_steps[3] = 9300
*(daily_steps + 3) = 9300
*(cursor + 3) = 9300
Address gap between elements: 4 bytes
Interview Gold — Arrays Decay to Pointers:
When you pass an array to a function in C, it silently becomes a pointer to its first element. The function loses all knowledge of the array's size — that's why every C function that takes an array also takes a separate size_t length parameter. sizeof(arr) inside the function gives you the pointer size (8 bytes), not the array size. This catches even experienced developers off guard.
Production Insight
Pointer arithmetic on void* is illegal – compiler doesn't know byte size.
Using arr + 1 steps by sizeof(element) bytes, not 1 byte – off-by-one errors if you forget scaling.
Rule: always pair array with length when passing to functions.
Key Takeaway
arr[i] equals *(arr + i).
Arrays decay to pointers in function calls – size information is lost.
Always pass length separately.

Pointers to Pointers and Dynamic Memory — Where Real Power Lives

A pointer is just a variable, and like any variable, it has its own address. A pointer-to-pointer (int **) stores the address of another pointer. This sounds abstract, but it has two very concrete uses: passing a pointer itself by reference so a function can change what it points to, and managing 2D dynamically allocated arrays.

Dynamic memory is where pointers truly shine. malloc returns a pointer to a freshly allocated block on the heap — memory that persists until you explicitly free it with free. This lets you build data structures whose size you don't know at compile time: linked lists, trees, dynamic arrays.

The golden rule of dynamic memory: every malloc must have exactly one free. Miss the free and you leak memory. Free twice and you corrupt the allocator. Free then use and you get undefined behavior. Pointers make this power possible — and make these bugs your responsibility.

dynamic_memory.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
#include <stdio.h>
#include <stdlib.h>  // malloc, free, realloc
#include <string.h>  // memset

// Allocates a sensor reading buffer of requested size on the HEAP
// Returns a pointer to the buffer — caller is responsible for freeing it
double *create_sensor_buffer(int capacity) {
    double *buffer = malloc(capacity * sizeof(double)); // ask OS for capacity*8 bytes

    if (buffer == NULL) {  // ALWAYS check — malloc can fail if memory is exhausted
        fprintf(stderr, "ERROR: malloc failed for sensor buffer of size %d\n", capacity);
        return NULL;
    }

    // Initialize all bytes to 0 — malloc does NOT zero memory for you
    memset(buffer, 0, capacity * sizeof(double));
    return buffer;
}

// Demonstrates a double pointer — function needs to CHANGE what the pointer points to
void reassign_buffer(double **buffer_ptr, int new_capacity) {
    free(*buffer_ptr);                              // release the old block
    *buffer_ptr = malloc(new_capacity * sizeof(double)); // point to a new block
    if (*buffer_ptr) memset(*buffer_ptr, 0, new_capacity * sizeof(double));
}

int main(void) {
    int initial_capacity = 3;
    double *readings = create_sensor_buffer(initial_capacity);
    if (readings == NULL) return 1;

    readings[0] = 23.5;  // array indexing works on heap memory exactly like stack arrays
    readings[1] = 24.1;
    readings[2] = 22.8;

    printf("Initial readings: %.1f, %.1f, %.1f\n",
           readings[0], readings[1], readings[2]);
    printf("Buffer lives on heap at: %p\n", (void *)readings);

    // Grow the buffer — reassign_buffer needs **double to change what readings points to
    reassign_buffer(&readings, 6);
    readings[0] = 100.0;  // fresh buffer, old data is gone
    printf("After reassign, readings[0]: %.1f\n", readings[0]);
    printf("Buffer now lives at        : %p\n", (void *)readings); // different address!

    free(readings);      // ALWAYS free heap memory when done
    readings = NULL;     // NULL the pointer immediately — prevents accidental use-after-free

    printf("Buffer freed and pointer nulled.\n");
    return 0;
}
Output
Initial readings: 23.5, 24.1, 22.8
Buffer lives on heap at: 0x55a3f2e01260
After reassign, readings[0]: 100.0
Buffer now lives at : 0x55a3f2e01690
Buffer freed and pointer nulled.
Watch Out — Dangling Pointers:
After calling free(readings), the pointer variable still holds the old address — it doesn't become NULL automatically. Reading or writing through it is undefined behavior: your program might crash immediately, silently corrupt data, or appear to work fine today and fail in production. Always set the pointer to NULL right after freeing. Tools like Valgrind and AddressSanitizer (-fsanitize=address) catch these bugs during development.
Production Insight
Forgetting to free causes memory leaks – monitored with valgrind.
Double‑free corrupts heap metadata – often crashes later in unrelated code (hard to debug).
Rule: every malloc must pair with exactly one free, and set pointer to NULL afterwards.
Key Takeaway
malloc/free are your responsibility.
Always check malloc return for NULL.
NULL after free prevents use-after-free.

Double Pointers — Using Pointers to Pointers

A pointer to a pointer (int **) stores the address of another pointer. This extra level of indirection is necessary when a function needs to modify the pointer variable in the caller — not just the data it points to, but the pointer itself. Common use cases include:

  • Linked list insertion at head: Pass &head (a Node**) so the insertion can update the head pointer to the new node. Without double pointers, the function would only modify a local copy.
  • 2D dynamic arrays: int *matrix = malloc(rows sizeof(int*)); then allocate each row. Double pointer is the base.
  • Reallocating buffers inside functions: As shown in the previous section, reassign_buffer uses double ** to change the caller's pointer.

Understanding double pointers makes you ready to implement dynamic data structures and to understand function signatures like main(int argc, char **argv) where argv is a pointer to an array of strings.

double_pointer_linkedlist.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
#include <stdio.h>
#include <stdlib.h>

typedef struct Node {
    int data;
    struct Node *next;
} Node;

// Insert at head – needs Node** to update the head pointer
void insert_at_head(Node **head_ptr, int value) {
    Node *new_node = malloc(sizeof(Node));
    if (new_node == NULL) return;
    new_node->data = value;
    new_node->next = *head_ptr;  // old head becomes second node
    *head_ptr = new_node;        // head now points to new node
}

void print_list(Node *head) {
    for (Node *cur = head; cur != NULL; cur = cur->next)
        printf("%d ", cur->data);
    printf("\n");
}

int main(void) {
    Node *head = NULL;
    insert_at_head(&head, 10);
    insert_at_head(&head, 20);
    insert_at_head(&head, 30);
    printf("List: ");
    print_list(head);

    // Cleanup
    while (head) {
        Node *tmp = head;
        head = head->next;
        free(tmp);
    }
    return 0;
}
Output
List: 30 20 10
Common Double Pointer Pattern:
Whenever you see a function with a parameter like Node **head_ptr, it means the function can change the pointer variable passed from the caller. Always pass the address of the pointer (&head) when the function needs to allocate or reassign it.
Production Insight
Forgetting to use double pointer in linked list insertion leads to memory leak – the new node is allocated but head remains NULL.
Always initialize int ** to NULL and check before dereferencing.
Rule: if a function must modify the caller's pointer (allocate/free/reassign), pass the address of the pointer.
Key Takeaway
int * stores address of an int.
Use double pointers when the function needs to change the pointer itself.
Pass &ptr to a function expecting T**.

Const and Pointers: The 4 Combinations

The const keyword interacts with pointers in four distinct ways. Reading the declaration from right to left is the trick: const int ptr means ptr is a pointer to a constant integer – you can change where ptr points, but you cannot modify the integer through it. int const ptr means ptr is a constant pointer to a integer – you cannot change the address stored in ptr, but you can modify the integer. const int const ptr locks both: the pointer is constant and the data is constant. int ptr is fully mutable.

In APIs, const int signals that a function will not modify the data (read‑only access). int const is rare but useful for hardware registers where you must always point to the same location. Getting these wrong produces compilation errors – a good thing, as the compiler enforces your intent.

const_pointers.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
#include <stdio.h>

int main(void) {
    int a = 10, b = 20;

    // pointer to const int: can change pointer, cannot change value via pointer
    const int *ptr_to_const = &a;
    // *ptr_to_const = 30;  // ERROR: assignment of read-only location
    ptr_to_const = &b;       // OK – pointer can move

    // const pointer to int: cannot change pointer, can change value
    int *const const_ptr_to_int = &a;
    *const_ptr_to_int = 40;  // OK – value modifiable
    // const_ptr_to_int = &b;  // ERROR: assignment of read-only location

    // const pointer to const int: neither can be changed
    const int *const const_ptr_to_const = &a;
    // *const_ptr_to_const = 50;  // ERROR
    // const_ptr_to_const = &b;   // ERROR

    printf("a = %d, b = %d\n", a, b);
    return 0;
}
Output
a = 40, b = 20
Reading Declarations:
Read pointer declarations right‑to‑left. const int ptr: ptr is a pointer to const int. int const ptr: ptr is a const pointer to int. This rule never fails.
Production Insight
Mixing up const int p and int const p leads to compilation errors or unintended writes.
In APIs, using const int * for input parameters signals that the function won't modify data – good practice.
Rule: use const to enforce read‑only intent on pointer parameters.
Key Takeaway
Read const declarations from right to left.
const int *p – pointer to constant int.
int *const p – constant pointer to int.

Function Pointers and Callbacks in C

A function pointer stores the address of a function – just as a data pointer stores the address of a variable. They are heavily used for callbacks, plugin architectures, and event‑driven systems. The syntax is unusual: int (func_ptr)(int, int) declares a pointer to a function that takes two ints and returns an int. Assign it with func_ptr = &add; (or func_ptr = add; – the & is optional). Call it via (func_ptr)(3, 4) or func_ptr(3, 4).

The standard library qsort uses a function pointer for its comparator callback. This lets you sort any data type without copy‑pasting the sorting algorithm. Function pointers are also the backbone of interrupt vector tables in embedded systems and dynamic dispatch in low‑level OOP implementations.

function_pointer.cC
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
#include <stdio.h>
#include <stdlib.h>

// Two simple operations
int add(int a, int b) { return a + b; }
int multiply(int a, int b) { return a * b; }

// Comparator for qsort (ascending order)
int compare_int(const void *a, const void *b) {
    int ia = *(const int *)a;
    int ib = *(const int *)b;
    return (ia > ib) - (ia < ib);
}

int main(void) {
    // Function pointer: points to any function with (int, int) → int
    int (*operation)(int, int) = NULL;

    operation = &add;
    printf("Add via function pointer: %d\n", operation(10, 5));

    operation = &multiply;
    printf("Multiply via function pointer: %d\n", operation(10, 5));

    // qsort example
    int values[] = {8, 3, 5, 1, 9};
    size_t n = sizeof(values) / sizeof(values[0]);
    qsort(values, n, sizeof(int), compare_int);

    printf("Sorted array: ");
    for (size_t i = 0; i < n; i++) printf("%d ", values[i]);
    printf("\n");
    return 0;
}
Output
Add via function pointer: 15
Multiply via function pointer: 50
Sorted array: 1 3 5 8 9
Qt Signal‑Slots & Embedded ISRs:
Function pointers are the mechanism behind callback frameworks like Qt's signals/slots and microcontroller interrupt service routines (ISRs). In these contexts, the address of the handler function is stored in a vector table, and the CPU jumps to it when the event fires.
Production Insight
Wrong callback signature leads to undefined behavior – compiler may not warn if cast to void*.
In embedded systems, function pointers are used for interrupt vectors – care with volatile.
Rule: always use typedef for function pointer types to improve readability.
Key Takeaway
Function pointers store the address of a function.
They enable callbacks and late binding.
Use typedef to simplify syntax.

Comprehensive Guide to Special Pointer Types — NULL, Void, Wild, and Dangling

Beyond typed pointers, C has several special pointer categories that every developer must understand to avoid bugs and write robust code.

TypeWhat It IsHow to CreateSafe to Dereference?How to Fix/Escalate
NULL pointerPoints to address 0, intentionally invalidint *p = NULL;No — immediate crash (SIGSEGV)Check before use: if (p != NULL) { ... }
*void pointer**Generic pointer without type informationvoid *p = malloc(100);No — must cast to a type firstCast to appropriate type before dereference
Wild pointerUninitialized pointer, contains garbageint *p; (no assignment)No — writes to random memoryAlways initialize: int *p = NULL; or to a valid address
Dangling pointerPreviously pointed to valid memory that has been freedfree(p); without NULLing pNo — undefined behavior; may crash or corruptSet to NULL after free: free(p); p = NULL;

What to do when each appears in production: - NULL: Add defensive checks — the pointer is intentionally invalid, and a check prevents crash. - *void: Ensure the code that dereferences it knows the actual type. Use static_cast in C++ or explicit cast in C. Prefer typed pointers when possible. - Wild: Run with AddressSanitizer to detect use of uninitialized memory. Always initialize pointers at declaration. - Dangling**: Use tools like Valgrind or ASan to identify use-after-free. Adopt the pattern of setting pointer to NULL immediately after free and check for NULL before use.

Dangling Pointers Are the Stealth Bombers of C Bugs:
Unlike NULL pointer dereferences which crash immediately, dangling pointers may silently corrupt memory. The bug might manifest hours later in a completely unrelated part of the code. Always pair free() with immediate NULL assignment, and compile with -fsanitize=address during development to catch these early.
Production Insight
NULL pointers are intentionally harmless if checked – always check before dereference.
Void pointers require type tracking elsewhere – misuse causes data corruption.
Wild and dangling pointers are the primary source of undefined behavior in C production code.
Rule: initialize all pointers; NULL after free; use sanitizers.
Key Takeaway
NULL = safe crash, dangling = silent corruption.
Initialize pointers to NULL or valid address.
Always check for NULL before dereference.

Pointer Size on 32-bit vs 64-bit Architectures — What You Need to Know

The size of a pointer on a given platform depends on the address bus width. On 32-bit systems, pointers are 4 bytes (32 bits), limiting addressable memory to 4 GiB. On 64-bit systems, pointers are 8 bytes (64 bits), allowing a vastly larger address space. This matters for memory consumption, struct packing, and portability.

Aspect32-bit System64-bit System
Pointer size4 bytes8 bytes
sizeof(int*)48
sizeof(char*)48
sizeof(void*)48
sizeof(int*) vs sizeof(int)Both 4Pointer larger than int (8 vs 4)
Maximum addressable memory~4 GB16 EB (theoretical)
Struct alignment impactPointers pack as 4-byte entitiesPointers often cause padding to 8-byte boundaries
Typical use casesLegacy embedded, older microcontrollersModern desktop, server, mobile, embedded (ARM64)

Key implication for structs: On 64-bit, fields after a pointer may be padded to align the pointer to an 8-byte boundary. This can increase struct size unexpectedly. Use #pragma pack or reorder fields to minimize padding.

pointer_size.cC
1
2
3
4
5
6
7
8
9
10
11
#include <stdio.h>

int main(void) {
    printf("Pointer sizes (compile with -m32 or -m64 to change):\n");
    printf("sizeof(int*)   = %zu bytes\n", sizeof(int*));
    printf("sizeof(char*)  = %zu bytes\n", sizeof(char*));
    printf("sizeof(void*)  = %zu bytes\n", sizeof(void*));
    printf("sizeof(double*) = %zu bytes\n", sizeof(double*));
    printf("sizeof(int)    = %zu bytes\n", sizeof(int));
    return 0;
}
Output
=== On 64-bit system ===
Pointer sizes (compile with -m32 or -m64 to change):
sizeof(int*) = 8 bytes
sizeof(char*) = 8 bytes
sizeof(void*) = 8 bytes
sizeof(double*) = 8 bytes
sizeof(int) = 4 bytes
=== On 32-bit system ===
Pointer sizes:
sizeof(int*) = 4 bytes
sizeof(char*) = 4 bytes
sizeof(void*) = 4 bytes
sizeof(double*) = 4 bytes
sizeof(int) = 4 bytes
Portability Note:
When writing code that must run on both 32-bit and 64-bit platforms, avoid assuming pointer and integer sizes are equal. Use uintptr_t (from <stdint.h>) if you need to store a pointer as an integer — it's always the correct size for the platform.
Production Insight
On 64-bit, passing many pointers as function arguments uses more stack space than on 32-bit.
Serializing data structures with pointers across platforms (e.g., over network) fails because pointer sizes differ.
Rule: never hardcode pointer sizes; use sizeof() and uintptr_t for portability.
Key Takeaway
Pointers are 4 bytes on 32-bit, 8 bytes on 64-bit.
Pointer size affects struct alignment and memory usage.
Use uintptr_t to store pointer as integer portably.
● Production incidentPOST-MORTEMseverity: high

Use-After-Free in Embedded Sensor Firmware

Symptom
System crashes after hours of operation with SIGSEGV at random addresses. Intermittent and difficult to reproduce. Health‑check logs show corrupted sensor readings before crash.
Assumption
The firmware team assumed that freeing a pointer in the cleanup routine was sufficient – they forgot to NULL it, and a different task continued to use the pointer, leading to use‑after‑free.
Root cause
After free() was called on a sensor buffer pointer, the pointer variable still held the old address. A periodic sensor logging task dereferenced that pointer, reading freed heap memory. Over time, the allocator reused that memory, and reading it caused a segmentation fault.
Fix
Set the pointer to NULL immediately after freeing, and add a check if (ptr != NULL) before dereferencing in all tasks that use it. Add AddressSanitizer to the CI build pipeline.
Key lesson
  • After every free, always set pointer to NULL – it turns use‑after‑free into a clean crash at the point of null dereference.
  • Never trust that only one part of the code owns a pointer – cross‑reference all usages after free.
  • Use sanitizers (ASan, Valgrind) in development builds to catch these bugs before they reach production.
Production debug guideSymptom → Action – Debugging pointer problems in production5 entries
Symptom · 01
Segmentation fault when dereferencing a pointer
Fix
Check if pointer is NULL before dereferencing; if not, run under Valgrind or AddressSanitizer to identify the source.
Symptom · 02
Memory leak (heap grows unbounded over time)
Fix
Inspect malloc/free pairs; use valgrind --leak-check=full to find unreachable memory.
Symptom · 03
Double free or invalid free error
Fix
Ensure freed pointers are set to NULL immediately; check if same pointer is freed more than once.
Symptom · 04
Program crashes after free on a global pointer
Fix
Locate all code paths that might still use the pointer after free; set pointer to NULL and add guards.
Symptom · 05
Corruption of data structures (e.g., linked list nodes become garbage)
Fix
Check for buffer overflows using AddressSanitizer; verify pointer arithmetic doesn't exceed allocation bounds.
★ Use-After-Free Debugging Cheat SheetQuick steps to catch use‑after‑free bugs during development.
Intermittent crash when accessing a pointer that was previously freed
Immediate action
Check if the pointer was set to NULL after `free()` – if not, set it and reproduce the crash.
Commands
gcc -fsanitize=address -g program.c -o program && ./program
valgrind --tool=memcheck --leak-check=full ./program
Fix now
Set pointer to NULL after free() and add a null check before every dereference of that pointer.
Stack vs Heap Pointer Usage
AspectStack Variable (int x)Heap Pointer (int *x = malloc(...))
Where memory livesStack — managed automaticallyHeap — managed by you
LifetimeUntil enclosing function returnsUntil you call free()
Size known at compile time?Yes — requiredNo — decided at runtime
Risk of memory leakNone — auto-cleanedYes — if you forget free()
Can outlive the function?No — dangerous if you tryYes — designed for this
Access syntaxx = 5;*x = 5; or x[0] = 5;
Typical use caseLocal counters, loop varsDynamic arrays, linked lists, trees

Key takeaways

1
A pointer holds a memory address, not a value
the * operator dereferences it to reach the value, and & gets the address of any variable. These two are inverses.
2
Pass a pointer to a function when you need the function to modify the caller's variable or when the data is large enough that copying it would be wasteful
this is C's only mechanism for pass-by-reference.
3
Array indexing arr[i] and pointer arithmetic *(arr + i) are identical operations
arrays decay to pointers when passed to functions, which is why you always need a separate length parameter.
4
Every malloc must be paired with exactly one free, and you should NULL the pointer immediately after freeing it
use Valgrind or -fsanitize=address in your build pipeline to catch leaks and use-after-free bugs before they reach production.
5
Use const liberally with pointer parameters to enforce read/write contracts
read declarations right-to-left to understand the binding.
6
Function pointers enable callbacks and polymorphic behavior in C; always use a typedef to keep syntax manageable.

Common mistakes to avoid

4 patterns
×

Using an uninitialized pointer

Symptom
Writing to an uninitialized pointer writes to a random memory location, causing segmentation faults or silent data corruption.
Fix
Always initialize pointers – either to the address of an existing variable (int *ptr = &var) or to the result of malloc(). Set unused pointers to NULL.
×

Forgetting to check malloc return value

Symptom
malloc returns NULL when memory is exhausted. Dereferencing a NULL pointer causes an immediate segmentation fault.
Fix
Always guard malloc with if (ptr == NULL) { handle_error(); } before touching the returned pointer. This cheap check prevents a whole class of crashes.
×

Off-by-one errors in pointer arithmetic

Symptom
Iterating one element past the end of an array (using <= instead of <) reads or writes out of bounds – undefined behavior, may crash or corrupt adjacent memory.
Fix
Use strict less-than (<) in loops over arrays. Compile with -fsanitize=address to catch out-of-bounds access immediately.
×

Dereferencing a pointer after free (use-after-free)

Symptom
After calling free(), the pointer variable still holds the old address. Accessing it later (reading or writing) is undefined behavior – may crash, corrupt data, or appear to work.
Fix
Set the pointer to NULL immediately after free(). Regularly use tools like Valgrind or AddressSanitizer during development.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01SENIOR
What is the difference between `int *const ptr` and `const int *ptr` in ...
Q02JUNIOR
If you have a function `void update(int *value)` and you call it with `u...
Q03SENIOR
What is a dangling pointer and how is it different from a null pointer? ...
Q04SENIOR
Explain the purpose of the `void*` pointer. What can and cannot you do w...
Q01 of 04SENIOR

What is the difference between `int *const ptr` and `const int *ptr` in C? What does each guarantee, and when would you use one over the other?

ANSWER
int const ptr is a constant pointer to a non‑constant integer – you cannot change the address stored in ptr, but you can modify the integer it points to. const int ptr is a pointer to a constant integer – you cannot modify the integer via ptr, but you can change ptr to point elsewhere. Use int const when you want to ensure the pointer always points to a specific variable (e.g., a hardware register). Use const int when you want to prevent modification of the pointed‑to data (e.g., reading from a read‑only buffer).
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
What is the difference between a pointer and a reference in C?
02
Why does my pointer print a different address every time I run the program?
03
What actually happens when you dereference a NULL pointer?
04
Can I use pointer arithmetic on `void*` in standard C?
05
What does `int *p[10]` mean compared to `int (*p)[10]`?
🔥

That's C Basics. Mark it forged?

7 min read · try the examples if you haven't

Previous
Strings in C
8 / 17 · C Basics
Next
Pointer Arithmetic in C