Home C / C++ Pointers in C — Preventing Use-After-Free Crashes
Intermediate 11 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 & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Production
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
Before you start⏱ 25 min
  • Solid grasp of fundamentals
  • Comfortable reading code examples
  • Basic production concepts
 ● Production Incident 🔎 Debug Guide ⚙ Triage Commands
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).
✦ Definition~90s read
What is Pointers in C?

A pointer is a variable that stores a memory address — literally the numeric location of a byte in RAM. In C, this is the only way to indirectly access data, and it's the foundation for dynamic memory, data structures, and system-level programming. Without pointers, you can't allocate memory at runtime, build linked lists or trees, or pass large structs without copying them.

Imagine your house has a street address.

They exist because C gives you direct control over memory layout and performance, unlike languages with hidden references or garbage collection. Every pointer has a type (e.g., int, char) that tells the compiler how many bytes to read when dereferencing, but the pointer itself is just an address — typically 4 or 8 bytes depending on your architecture.

Pointers solve two core problems: avoiding expensive copies when passing data to functions, and enabling dynamic memory allocation via malloc and free. When you pass a struct to a function by value, the entire struct is copied onto the stack — for a 1KB struct, that's 1KB of memory traffic.

Pass a pointer instead, and you copy only 8 bytes. More critically, pointers let you allocate memory on the heap that persists beyond the function scope, which is how every non-trivial C program manages variable-sized data. But this power comes with risk: dangling pointers (pointers to freed memory) cause use-after-free bugs, which are among the most common and dangerous crashes in C programs.

Pointer arithmetic is where C gets both powerful and treacherous. An array name decays to a pointer to its first element, so arr[i] is identical to (arr + i). The compiler scales the offset by the size of the element type — arr + 3 on an int moves 12 bytes forward (assuming 4-byte ints), not 3 bytes.

This means arrays and pointers are interchangeable in most contexts, but they're not the same: sizeof(arr) on a true array gives the total byte count, while sizeof(ptr) gives just the pointer size. Understanding this distinction is critical for avoiding off-by-one errors and buffer overflows.

Dynamic memory with pointers introduces double pointers (int), which are pointers to pointers. You need them when a function must modify a pointer argument — for example, reallocating a buffer inside a function requires passing char so the function can update the caller's pointer.

The four const-pointer combinations (const int, int const, const int const, and int const — which is identical to const int*) let you enforce read-only access at compile time, preventing accidental mutation of data or the pointer itself. Mastering these patterns is what separates someone who writes C from someone who writes C that doesn't crash.

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.
pointers-c Memory Management Layers From allocation to deallocation and safety Application Code Pointer variable | Dereference operations Dynamic Memory API malloc() | free() | realloc() Heap Manager Free list | Allocation tracking OS Memory Virtual pages | Physical RAM THECODEFORGE.IO
thecodeforge.io
Pointers C

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-c THECODEFORGE.IO Memory Management Layers From stack to heap and pointer safety Application Code Function calls | Pointer variables Stack Local variables | Return addresses Heap Dynamic allocations | malloc/free Pointer Safety Layer NULL checks | Free tracking Operating System Virtual memory | Page protection THECODEFORGE.IO
thecodeforge.io
Pointers C

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.

Changing Value Pointed by Pointers — The Trap That Kills Production Code

You already know that ptr gives you the value at the address a pointer holds. But you can also assign* through that dereference. That's where most junior bugs get born.

When you write *ptr = 42;, you're not modifying the pointer. You're overwriting the memory address it points to with the value 42. If that address is a local variable in another function, you just changed it — permanently. This is how you mutate caller state without a return value.

This works because the dereference operator * on the left side of an assignment becomes a write operation. It's the same mechanism that makes pass-by-reference possible in C, even though the language officially only has pass-by-value. The pointer value is copied, but the data it points to is shared.

Where this bites you: accidentally overwriting read-only buffers, corrupting shared memory in embedded systems, or silently nuking another thread's stack frame. Always ask yourself: "Who else owns this address?"

WriteThroughPointer.cppCPP
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
// io.thecodeforge — c-cpp tutorial

#include <stdio.h>

void corrupt_buffer(char *buf, size_t len) {
    for (size_t i = 0; i < len; i++) {
        buf[i] = 'X';  // Direct memory write via pointer
    }
}

int main() {
    char message[] = "Production secret: 42";
    printf("Before: %s\n", message);
    
    corrupt_buffer(message, 5);
    printf("After:  %s\n", message);
    
    return 0;
}
Output
Before: Production secret: 42
After: XXXXXtion secret: 42
⚠ Production Trap:
Dereferencing an uninitialized pointer and writing to it is a guaranteed segfault. Always check for NULL before the write, especially in long-lived server processes where NULL isn't immediately fatal — it corrupts memory silently first.
🎯 Key Takeaway
*ptr = value writes to the address stored in ptr, not to ptr itself. If you don't own that address, you're breaking encapsulation.

Common Mistakes When Working with Pointers — We've Fixed All of These in Code Review

After a decade of debugging post-mortem dumps, I've seen the same five pointer sins repeated by engineers who "know pointers." Here they are, unvarnished.

Mistake 1: Dereferencing NULL. The most common. You malloc, don't check if it returned NULL, then write through it. On embedded systems, this writes to address 0, which might not crash until it corrupts your interrupt vector table. Always check malloc returns before dereferencing.

Mistake 2: Forgetting & in scanf. C doesn't enforce type safety on varargs. scanf("%d", ptr) without & treats the integer value of ptr as an address. You corrupt memory at a random location. The fix: &ptr for primitives, no & for arrays.

Mistake 3: Array decay confusion. Passing an array to a function decays it to a pointer to the first element. sizeof(array) inside the function gives pointer size, not array size. You write a loop that overruns the buffer.

Mistake 4: Dangling pointers from returning stack addresses. You allocate a local variable, return its address. Caller dereferences it, gets garbage because the stack frame is gone. Fix: allocate on the heap or pass a buffer from the caller.

Mistake 5: Pointer arithmetic going out of bounds. Incrementing past allocated memory. This doesn't error at runtime — it silently reads or writes memory belonging to other variables. Debugging this takes days.

CommonMistakes.cppCPP
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
// io.thecodeforge — c-cpp tutorial
#include <stdio.h>
#include <stdlib.h>

int* buf;  // Global, bad practice but makes the mistake visible

void classic_dangle() {
    int local = 42;
    buf = &local;  // MISTAKE: returning address of stack variable
    // function returns, 'local' is gone
}

int main() {
    classic_dangle();
    printf("Dangling value: %d\n", *buf);  // Undefined behavior
    
    // Now let's do it right
    int *safe = malloc(sizeof(int));
    if (safe) {
        *safe = 99;
        printf("Safe value: %d\n", *safe);
        free(safe);
    }
    return 0;
}
Output
Dangling value: -132456789 // garbage, varies
Safe value: 99
💡Senior Shortcut:
Use valgrind or AddressSanitizer (-fsanitize=address) for every pointer-heavy code path. The tool will catch 95% of these mistakes in seconds, not hours.
🎯 Key Takeaway
Five pointer mistakes: NULL deref, missing & in scanf, array decay sizeof trap, returning stack addresses, and out-of-bounds arithmetic. Know them, check for them in code review.

Declaring and Initializing Pointers in C++

In C++, declaring a pointer starts with the type followed by an asterisk and the variable name, like int ptr. However, initialization is what separates safe code from cryptic crashes. A pointer should always point to a valid memory address or be set to nullptr (C++11 onward). The compiler enforces type safety more strictly than C, so you cannot assign a double to an int without an explicit cast. Use the address-of operator & to initialize from an existing variable: int value = 42; int ptr = &value;. Alternatively, use dynamic allocation with new: int* ptr = new int(10);. Always pair new with delete to prevent memory leaks. Modern C++ discourages raw pointers for ownership; prefer smart pointers like std::unique_ptr instead. The key why: without proper initialization, pointers become wild pointers that point to random memory, causing undefined behavior and security vulnerabilities in production systems.

PointerInit.cppCPP
1
2
3
4
5
6
7
8
9
10
11
// io.thecodeforge — c-cpp tutorial
#include <iostream>
int main() {
    int value = 42;
    int* ptr = &value;       // points to value
    int* safe = nullptr;     // C++11 safe null
    int* dyn = new int(10);  // heap allocation
    std::cout << *ptr;       // prints 42
    delete dyn;
    return 0;
}
Output
42
⚠ Production Trap:
Uninitialized pointers are undefined behavior. Always initialize at declaration or set to nullptr immediately.
🎯 Key Takeaway
Always initialize pointers at declaration; use nullptr for null states.

Advantages, Disadvantages, Best Practices, Conclusion, and FAQ

Advantages: C++ pointers enable direct memory access for performance-critical systems, dynamic data structures (trees, graphs), and polymorphism via virtual functions. They support array-pointer duality for efficient iteration. Disadvantages include manual memory management risks (leaks, dangling pointers), complex debugging, and no bounds checking, leading to buffer overflows. Best practices: Use nullptr over NULL; prefer references for non-nullable parameters; employ smart pointers (std::unique_ptr, std::shared_ptr) for ownership; avoid pointer arithmetic unless working with raw arrays. Conclusion: Pointers are powerful but demand discipline—use them only where necessary and wrap them in RAII containers. FAQ: Q: When should I use raw pointers in modern C++? A: Only for observing (non-owning) references to objects managed elsewhere, like in legacy APIs. Q: Can I mix smart and raw pointers? A: Yes, but never delete a raw pointer from a smart pointer; it causes double-free. Q: Why avoid delete? A: Except in custom allocators, smart pointers automate it and prevent leaks.

BestPractices.cppCPP
1
2
3
4
5
6
7
8
9
// io.thecodeforge — c-cpp tutorial
#include <memory>
int main() {
    auto ptr = std::make_unique<int>(10); // best practice
    int raw = 5;
    auto observer = &raw; // non-owning raw
    // ptr automatically deleted
    return 0;
}
🔥Best Practice:
Owning raw pointers are obsolete; use std::unique_ptr for automatic lifetime management.
🎯 Key Takeaway
Prefer smart pointers for ownership; raw pointers only for non-owning observation.

C23: nullptr Keyword Replacing NULL

The C23 standard introduces the nullptr keyword as a safer and more explicit alternative to the traditional NULL macro. While NULL is typically defined as ((void*)0) or 0, which can lead to ambiguities in overload resolution in C++ or unintended conversions in C, nullptr is a distinct pointer type that implicitly converts to any pointer type but not to integer types. This eliminates common bugs where NULL is mistakenly treated as an integer zero.

In C23, you can use nullptr exactly like NULL but with stronger type safety. For example:

int *ptr = nullptr;
if (ptr == nullptr) {
    // handle null case
}

Unlike NULL, nullptr cannot be assigned to non-pointer types without a cast. This prevents accidental misuse, such as passing NULL to a function expecting an integer.

Migration Tip: Replace NULL with nullptr in new code. For legacy code, consider using a macro like #define nullptr ((void*)0) for pre-C23 compilers, but be aware of potential issues with C++ compatibility.

Important: In C++, nullptr has been available since C++11 and is preferred over NULL. C23 aligns C with C++ in this regard, making cross-language development more consistent.

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

void foo(int *p) {
    if (p == nullptr) {
        printf("Pointer is null\n");
    } else {
        printf("Pointer is not null\n");
    }
}

int main() {
    int *ptr = nullptr;
    foo(ptr);
    
    // This would cause a compilation error in C23:
    // int x = nullptr; // error: cannot convert nullptr to int
    
    return 0;
}
🔥C23 nullptr vs C++ nullptr
📊 Production Insight
Adopt nullptr in new C23 projects to reduce bugs from NULL/integer confusion. For cross-platform code, ensure your compiler supports C23 or use a compatibility macro.
🎯 Key Takeaway
Use nullptr instead of NULL in C23 to avoid type confusion and improve code clarity.

Pointer Provenance and Strict Aliasing Rule

Pointer provenance refers to the origin or history of a pointer value, which affects whether accessing memory through that pointer is valid. The strict aliasing rule (C11 6.5p7) states that you cannot access an object through a pointer of an incompatible type, except via character types. Violating this rule leads to undefined behavior, often manifesting as subtle bugs that compilers exploit for optimization.

Example of violation:

``c int x = 42; float fp = (float)&x; float y = *fp; // UB: accessing int through float pointer ``

Correct approach: Use memcpy or union (in C, unions allow type punning).

``c int x = 42; float y; memcpy(&y, &x, sizeof(x)); // safe ``

Pointer provenance also matters for restrict-qualified pointers and pointer arithmetic. Compilers assume that pointers with different provenance do not alias, enabling optimizations. For example:

``c void copy(int restrict dest, int restrict src, size_t n) { for (size_t i = 0; i < n; i++) { dest[i] = src[i]; // safe because restrict guarantees no overlap } } ``

Practical advice: Always use the correct type for pointers. Avoid casting between unrelated pointer types unless you are certain of the underlying object's effective type. Prefer memcpy for type punning, and use restrict to give the compiler optimization hints.

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

int main() {
    int x = 0x12345678;
    float f;
    
    // Violation: accessing int as float
    // float *fp = (float*)&x;
    // printf("%f\n", *fp); // UB
    
    // Correct: use memcpy
    memcpy(&f, &x, sizeof(x));
    printf("%f\n", f);
    
    // Also correct in C: use union
    union { int i; float f; } u = { .i = x };
    printf("%f\n", u.f);
    
    return 0;
}
⚠ Strict Aliasing Violations Are Common
📊 Production Insight
Enable compiler warnings for strict aliasing and consider using -fno-strict-aliasing only as a last resort. In performance-critical code, use restrict to help the optimizer.
🎯 Key Takeaway
Respect the strict aliasing rule: never access an object through a pointer of an incompatible type. Use memcpy or unions for type punning.
Safe vs Unsafe Pointer Usage Use-after-free prevention strategies Unsafe Pattern Safe Pattern After free() Pointer left dangling Pointer set to NULL Dereference check No NULL check before use Always check ptr != NULL Multiple frees Double free possible Check and set NULL prevents double free Ownership tracking No ownership documentation Clear owner and lifetime rules Tool support Manual debugging only Use valgrind/ASan for detection THECODEFORGE.IO
thecodeforge.io
Pointers C

Function Pointer Type Safety with typedef

Function pointers in C can be error-prone due to complex syntax and the risk of mismatched signatures. Using typedef to define function pointer types improves readability and type safety. For example:

```c // Without typedef: hard to read and maintain typedef int (*Operation)(int, int);

// With typedef: clear and reusable Operation op = &add; int result = op(3, 4); ```

Benefits
  • Type safety: The compiler checks that assigned functions have the correct signature.
  • Readability: Complex pointer syntax is hidden behind a meaningful name.
  • Maintainability: Changing the signature requires updating only the typedef.

Example:

```c #include

typedef int (*MathOp)(int, int);

int add(int a, int b) { return a + b; } int multiply(int a, int b) { return a * b; }

void execute(MathOp op, int x, int y) { printf("Result: %d ", op(x, y)); }

int main() { MathOp ops[] = {add, multiply}; for (int i = 0; i < 2; i++) { execute(ops[i], 5, 3); } return 0; } ```

Best practices
  • Always use typedef for function pointers used in multiple places.
  • Name the typedef to indicate its purpose (e.g., Callback, Comparator).
  • Avoid hiding pointer syntax entirely; sometimes explicit syntax is clearer for simple cases.

Caveat: In C++, prefer std::function or templates over function pointers for greater flexibility.

function_pointer_typedef.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
#include <stdio.h>

typedef int (*Comparator)(int, int);

int ascending(int a, int b) { return a - b; }
int descending(int a, int b) { return b - a; }

void sort(int *arr, size_t n, Comparator cmp) {
    for (size_t i = 0; i < n; i++) {
        for (size_t j = i + 1; j < n; j++) {
            if (cmp(arr[i], arr[j]) > 0) {
                int temp = arr[i];
                arr[i] = arr[j];
                arr[j] = temp;
            }
        }
    }
}

int main() {
    int arr[] = {3, 1, 4, 1, 5};
    size_t n = sizeof(arr)/sizeof(arr[0]);
    
    sort(arr, n, ascending);
    for (size_t i = 0; i < n; i++) printf("%d ", arr[i]);
    printf("\n");
    
    return 0;
}
💡Typedef for Callbacks
📊 Production Insight
In large codebases, typedef function pointers to improve maintainability. For C++ code, consider using std::function or templates for more flexibility.
🎯 Key Takeaway
Use typedef to create readable and type-safe function pointer types, especially in APIs and callback-heavy code.
● 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
⚙ Quick Reference
15 commands from this guide
FileCommand / CodePurpose
pointer_basics.cint main(void) {What a Pointer Actually Is
pass_by_reference.cvoid fahrenheit_to_celsius_broken(double fahrenheit) {Why Pointers Exist
pointer_arithmetic.cint main(void) {Pointer Arithmetic
dynamic_memory.cdouble *create_sensor_buffer(int capacity) {Pointers to Pointers and Dynamic Memory
double_pointer_linkedlist.ctypedef struct Node {Double Pointers
const_pointers.cint main(void) {Const and Pointers
function_pointer.cint add(int a, int b) { return a + b; }Function Pointers and Callbacks in C
pointer_size.cint main(void) {Pointer Size on 32-bit vs 64-bit Architectures
WriteThroughPointer.cppvoid corrupt_buffer(char *buf, size_t len) {Changing Value Pointed by Pointers
CommonMistakes.cppint* buf; // Global, bad practice but makes the mistake visibleCommon Mistakes When Working with Pointers
PointerInit.cppint main() {Declaring and Initializing Pointers in C++
BestPractices.cppint main() {Advantages, Disadvantages, Best Practices, Conclusion, and F
nullptr_example.cvoid foo(int *p) {C23
strict_aliasing.cint main() {Pointer Provenance and Strict Aliasing Rule
function_pointer_typedef.ctypedef int (*Comparator)(int, int);Function Pointer Type Safety with typedef

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.
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]`?
N
Naren Founder & Principal Engineer

20+ years shipping performance-critical C and C++ systems. Lessons pulled from things that broke in production.

Follow
Verified
production tested
July 19, 2026
last updated
2,466
articles · all by Naren
🔥

That's C Basics. Mark it forged?

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

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