Skip to main content

Documentation Index

Fetch the complete documentation index at: https://resources.devweekends.com/llms.txt

Use this file to discover all available pages before exploring further.

Pointers Deep Dive

Pointers are C’s superpower. They enable direct memory manipulation, efficient data structures, and system-level programming. While they have a reputation for being difficult, the core concept is simple: a pointer is a variable that stores a memory address, like a sticky note with a house number on it. The sticky note is not the house — it just tells you where the house is. If you come from Python or JavaScript, you have already used pointers — you just did not know it. Every time you pass an object to a function in Python, you are passing a reference (which is a pointer with guardrails). C gives you the same thing, minus the guardrails.

Pointers in C

At its core, a pointer is just a number—the address of a byte in memory. C pointers and memory model

Why Does C Have Pointers?

The Fundamental Problem

Before learning pointer syntax, understand why they exist: The problem: How do you write efficient, low-level code? In high-level languages, you work with “values”:
  • Pass a 1MB struct to a function? Copy all 1MB
  • Return a large array? Copy it back
  • Modify a parameter? Can’t affect the original
The solution: Pointers give you direct memory addresses:
  • Pass a pointer (8 bytes) instead of copying 1MB
  • Return a pointer instead of copying data
  • Modify data through a pointer to affect the original
Pointer vs Value Memory Layout Real-world impact: Without pointers, C couldn’t:
  • Implement efficient data structures (linked lists, trees)
  • Interface with hardware (memory-mapped I/O)
  • Implement system calls (kernel needs to modify user memory)
  • Build operating systems and drivers
The tradeoff: Power and efficiency vs safety. Pointers give you direct memory access, but with that comes responsibility.

Pointer Fundamentals

Memory Model

#include <stdio.h>

int main(void) {
    int x = 42;
    int *p = &x;  // p stores the ADDRESS of x

    printf("Value of x:          %d\n", x);       // 42
    printf("Address of x:        %p\n", (void*)&x); // 0x7fff...
    printf("Value of p:          %p\n", (void*)p);  // 0x7fff... (same)
    printf("Value at address p:  %d\n", *p);      // 42 (dereference)
    printf("Address of p:        %p\n", (void*)&p); // Different address

    *p = 100;  // Modify x through p
    printf("New value of x:      %d\n", x);       // 100

    return 0;
}
Memory Layout:
┌─────────────────────────────────────────────────────────────┐
│ Address        │ Name  │ Value                              │
├─────────────────────────────────────────────────────────────┤
│ 0x7fff5000     │ x     │ 42                                 │
│ 0x7fff5008     │ p     │ 0x7fff5000 (points to x)          │
└─────────────────────────────────────────────────────────────┘
Pointer vs Value Memory Layout

Pointer Types

int x = 42;
int *p = &x;        // p is a pointer to int

char c = 'A';
char *pc = &c;      // pc is a pointer to char

double d = 3.14;
double *pd = &d;    // pd is a pointer to double

// Type matters for:
// 1. Dereferencing (how many bytes to read/write)
// 2. Pointer arithmetic (how much to increment)

printf("sizeof(int*):    %zu\n", sizeof(int*));    // 8 (on 64-bit)
printf("sizeof(char*):   %zu\n", sizeof(char*));   // 8
printf("sizeof(double*): %zu\n", sizeof(double*)); // 8
// All pointers are same size, but types differ

Pointer Arithmetic

Pointer arithmetic is one of the most confusing yet powerful features of C. It allows you to navigate memory based on the type of data you are pointing to. Pointer Arithmetic
#include <stdio.h>

int main(void) {
    int arr[] = {10, 20, 30, 40, 50};
    int *p = arr;  // Points to first element

    printf("p:       %p, *p = %d\n", (void*)p, *p);       // 10
    printf("p + 1:   %p, *(p+1) = %d\n", (void*)(p+1), *(p+1)); // 20
    printf("p + 2:   %p, *(p+2) = %d\n", (void*)(p+2), *(p+2)); // 30
    ptrdiff_t diff = end - p;  // 4 (number of elements, not bytes)

    // Increment/decrement
    p++;  // Now points to arr[1]
    p--;  // Back to arr[0]

    return 0;
}

What’s Allowed

// Legal pointer operations:
p + n      // Add integer to pointer
p - n      // Subtract integer from pointer
p1 - p2    // Subtract two pointers (same array only!)
p1 == p2   // Compare pointers
p1 < p2    // Relational comparison (same array only!)

// Illegal:
p1 + p2    // Can't add two pointers
p * 2      // Can't multiply pointer
p / 2      // Can't divide pointer

Pointer Arithmetic Visualized

Pointer Arithmetic Key Insight: Pointer arithmetic is scaled by the size of the pointed-to type. When you write p + 1, the compiler doesn’t add 1 byte to the address. Instead, it adds sizeof(*p) bytes. This is why pointer arithmetic “just works” with arrays:
int arr[] = {10, 20, 30, 40};
int *p = arr;

// Memory layout (assuming 4-byte ints):
// Address:  0x1000   0x1004   0x1008   0x100C
// Value:    10       20       30       40
//           ↑
//           p

p + 0;  // 0x1000 (points to arr[0])
p + 1;  // 0x1004 (points to arr[1]) - advanced by 4 bytes!
p + 2;  // 0x1008 (points to arr[2]) - advanced by 8 bytes!
Why This Matters:
  • Enables efficient array traversal
  • Allows treating arrays as pointers
  • Foundation for dynamic data structures
  • Critical for understanding sizeof and alignment
Common Mistake:
int *p = (int*)0x1000;
p + 1;  // 0x1004, NOT 0x1001!

Arrays and Pointers

Array Decay

In C, arrays and pointers are closely related but NOT identical. The most important concept to grasp is “array decay” — the automatic conversion of an array name into a pointer to its first element. Think of it like this: if you give someone your home address, they know where you live but they do not know how many rooms your house has. That “loss of size information” is exactly what happens when an array decays to a pointer. Array Decay Visualization
#include <stdio.h>

void print_array(int *arr, size_t size) {
    // arr is a POINTER here, not an array!
    printf("sizeof(arr) in function: %zu\n", sizeof(arr)); // 8 (pointer size)
    
    for (size_t i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

int main(void) {
    int arr[] = {1, 2, 3, 4, 5};
    
    printf("sizeof(arr) in main: %zu\n", sizeof(arr)); // 20 (5 * 4 bytes)
    
    // When passed to function, array "decays" to pointer
    print_array(arr, 5);
    
    // arr and &arr[0] are the same address, but different types!
    int *p1 = arr;       // OK: int* from int[]
    int *p2 = &arr[0];   // OK: int* from int*
    
    // But:
    int (*p3)[5] = &arr; // &arr is pointer to array of 5 ints!
    
    printf("arr:       %p\n", (void*)arr);
    printf("&arr:      %p\n", (void*)&arr);
    printf("arr + 1:   %p\n", (void*)(arr + 1));   // +4 bytes (next int)
    printf("&arr + 1:  %p\n", (void*)(&arr + 1));  // +20 bytes (past entire array!)
    
    return 0;
}

Multi-dimensional Arrays

#include <stdio.h>

int main(void) {
    int matrix[3][4] = {
        {1, 2, 3, 4},
        {5, 6, 7, 8},
        {9, 10, 11, 12}
    };

    // matrix decays to pointer to first row: int (*)[4]
    int (*row_ptr)[4] = matrix;
    
    // Accessing elements
    printf("%d\n", matrix[1][2]);        // 7
    printf("%d\n", *(*(matrix + 1) + 2)); // 7
    printf("%d\n", row_ptr[1][2]);       // 7

    // Memory is contiguous (row-major)
    int *flat = &matrix[0][0];
    for (int i = 0; i < 12; i++) {
        printf("%d ", flat[i]); // 1 2 3 4 5 6 7 8 9 10 11 12
    }
    
    return 0;
}

// Passing 2D arrays to functions
void process(int mat[][4], size_t rows) {
    // Must specify all dimensions except first
    for (size_t i = 0; i < rows; i++) {
        for (size_t j = 0; j < 4; j++) {
            printf("%d ", mat[i][j]);
        }
    }
}

// Or with pointer syntax
void process2(int (*mat)[4], size_t rows) {
    // Same as above
}

Pointers to Pointers

#include <stdio.h>
#include <stdlib.h>
#include <string.h>

int main(void) {
    int x = 42;
    int *p = &x;
    int **pp = &p;   // Pointer to pointer
    int ***ppp = &pp; // And so on...

    printf("x:      %d\n", x);        // 42
    printf("*p:     %d\n", *p);       // 42
    printf("**pp:   %d\n", **pp);     // 42
    printf("***ppp: %d\n", ***ppp);   // 42

    **pp = 100;  // Modify x through pp
    printf("x:      %d\n", x);        // 100

    return 0;
}

// Real use case: modifying a pointer in a function
void allocate_buffer(char **buf, size_t size) {
    *buf = malloc(size);  // Modify the pointer itself
}
int main(void) {
    char *buffer = NULL;
    allocate_buffer(&buffer, 1024);  // Pass address of pointer
    
    if (buffer) {
        strcpy(buffer, "Hello");
        printf("%s\n", buffer);
        free(buffer);
    }
    
    return 0;
}

Array of Pointers vs Pointer to Array

#include <stdio.h>

int main(void) {
    // Array of pointers (common for strings)
    char *strings[] = {"Hello", "World", "!"};
    // strings[0] is char*, points to "Hello"
    // strings[1] is char*, points to "World"
    // strings[2] is char*, points to "!"
    
    for (int i = 0; i < 3; i++) {
        printf("%s\n", strings[i]);
    }

    // Pointer to array
    int arr[5] = {1, 2, 3, 4, 5};
    int (*ptr_to_arr)[5] = &arr;
    // ptr_to_arr points to entire array
    
    printf("%d\n", (*ptr_to_arr)[2]); // 3

    // Common confusion:
    int *ptr_arr[5];    // Array of 5 pointers to int
    int (*arr_ptr)[5];  // Pointer to array of 5 ints
    
    return 0;
}

Const Correctness

#include <stdio.h>

int main(void) {
    int x = 10;
    int y = 20;

    // 1. Pointer to const int (can't modify *p)
    const int *p1 = &x;
    // *p1 = 5;   // ERROR: can't modify value
    p1 = &y;     // OK: can change what p1 points to

    // 2. Const pointer to int (can't change pointer)
    int *const p2 = &x;
    *p2 = 5;     // OK: can modify value
    // p2 = &y;  // ERROR: can't change pointer

    // 3. Const pointer to const int (can't change either)
    const int *const p3 = &x;
    // *p3 = 5;  // ERROR
    // p3 = &y;  // ERROR

    // Read it right-to-left:
    // const int *p     → "p is a pointer to int that is const"
    // int *const p     → "p is a const pointer to int"
    // const int *const p → "p is a const pointer to int that is const"

    return 0;
}

// Best practice: use const for read-only parameters
size_t strlen_safe(const char *s) {
    // Promises not to modify the string
    size_t len = 0;
    while (*s++) len++;
    return len;
}

Void Pointers

A void* is C’s way of saying “I’m pointing to something, but I’m not going to tell you what type it is.” It is the foundation of generic programming in C — the same pattern that languages like Java use generics and Rust uses trait objects for. The standard library’s qsort, bsearch, and pthread_create all use void* to work with arbitrary data types. The tradeoff: you gain flexibility but lose type safety. The compiler cannot check that you are casting back to the correct type. This is where discipline and documentation matter.
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// Generic swap function -- works for ANY type because it operates on raw bytes.
// This is the pattern used by qsort and other standard library functions.
void swap(void *a, void *b, size_t size) {
    void *temp = malloc(size);  // Allocate space for one element of unknown type
    memcpy(temp, a, size);      // Copy a's bytes to temp
    memcpy(a, b, size);         // Copy b's bytes to a
    memcpy(b, temp, size);      // Copy temp's bytes to b
    free(temp);
    // For performance-critical code, use a stack buffer for small sizes:
    // char temp[256]; if (size <= sizeof(temp)) { ... } else { malloc... }
}

// Generic print (needs type info)
void print_value(const void *ptr, char type) {
    switch (type) {
        case 'i': printf("%d", *(const int*)ptr); break;
        case 'd': printf("%f", *(const double*)ptr); break;
        case 'c': printf("%c", *(const char*)ptr); break;
        case 's': printf("%s", (const char*)ptr); break;
    }
}

int main(void) {
    int a = 5, b = 10;
    printf("Before: a=%d, b=%d\n", a, b);
    swap(&a, &b, sizeof(int));
    printf("After:  a=%d, b=%d\n", a, b);

    double x = 1.5, y = 2.5;
    swap(&x, &y, sizeof(double));

    // void* rules:
    void *vp;
    int i = 42;
    vp = &i;         // Any pointer converts to void*
    int *ip = vp;    // void* converts to any pointer (in C)
    
    // Can't dereference void* directly
    // *vp = 5;      // ERROR
    *(int*)vp = 5;   // OK after cast

    // Can't do arithmetic on void*
    // vp++;         // ERROR (size unknown)
    
    return 0;
}

Generic Container with void*

typedef struct {
    void *data;
    size_t size;
    size_t capacity;
    size_t elem_size;
} Vector;

Vector* vector_create(size_t elem_size, size_t initial_capacity) {
    Vector *v = malloc(sizeof(Vector));
    v->data = malloc(elem_size * initial_capacity);
    v->size = 0;
    v->capacity = initial_capacity;
    v->elem_size = elem_size;
    return v;
}

void vector_push(Vector *v, const void *elem) {
    if (v->size == v->capacity) {
        v->capacity *= 2;
        v->data = realloc(v->data, v->elem_size * v->capacity);
    }
    memcpy((char*)v->data + v->size * v->elem_size, elem, v->elem_size);
    v->size++;
}

void* vector_get(Vector *v, size_t index) {
    return (char*)v->data + index * v->elem_size;
}

// Usage
int main(void) {
    Vector *v = vector_create(sizeof(int), 10);
    
    for (int i = 0; i < 20; i++) {
        vector_push(v, &i);
    }
    
    for (size_t i = 0; i < v->size; i++) {
        int *elem = vector_get(v, i);
        printf("%d ", *elem);
    }
    
    free(v->data);
    free(v);
    return 0;
}

The Restrict Keyword

The restrict keyword is a promise you make to the compiler: “I guarantee that this pointer is the only way to access this memory region.” It is not a runtime check — it is a contract. If you break this contract, the compiler will generate wrong code silently. Why does this matter? Without restrict, the compiler must assume that any two pointers might point to overlapping memory. This forces it to reload values from memory after every store, preventing many powerful optimizations.
// restrict tells compiler: this pointer is the ONLY way to access this memory
// Allows better optimization (no aliasing concerns)

void copy(int *restrict dest, const int *restrict src, size_t n) {
    // Compiler knows dest and src don't overlap
    for (size_t i = 0; i < n; i++) {
        dest[i] = src[i];
    }
}

// Without restrict, compiler must assume they might overlap
// and reload values from memory more often

// memcpy uses restrict (undefined if regions overlap)
void *memcpy(void *restrict dest, const void *restrict src, size_t n);

// memmove does NOT (handles overlap)
void *memmove(void *dest, const void *src, size_t n);

Function Pointers

#include <stdio.h>
#include <stdlib.h>

// Function pointer declaration
int add(int a, int b) { return a + b; }
int sub(int a, int b) { return a - b; }
int mul(int a, int b) { return a * b; }

int main(void) {
    // Declare function pointer
    int (*op)(int, int);
    
    op = add;  // or: op = &add (same thing)
    printf("add: %d\n", op(5, 3));  // or (*op)(5, 3)
    
    op = sub;
    printf("sub: %d\n", op(5, 3));

    // Array of function pointers (jump table)
    int (*ops[])(int, int) = {add, sub, mul};
    
    for (int i = 0; i < 3; i++) {
        printf("ops[%d](10, 3) = %d\n", i, ops[i](10, 3));
    }

    // Typedef makes it cleaner
    typedef int (*BinaryOp)(int, int);
    BinaryOp my_op = mul;
    printf("mul: %d\n", my_op(5, 3));

    return 0;
}

// Callbacks
void process_array(int *arr, size_t n, int (*transform)(int)) {
    for (size_t i = 0; i < n; i++) {
        arr[i] = transform(arr[i]);
    }
}

int double_it(int x) { return x * 2; }
int square_it(int x) { return x * x; }

// qsort comparison function
int compare_ints(const void *a, const void *b) {
    return (*(const int*)a) - (*(const int*)b);
}

int main(void) {
    int arr[] = {3, 1, 4, 1, 5, 9};
    qsort(arr, 6, sizeof(int), compare_ints);
    return 0;
}

Common Pitfalls

Dangling Pointers

A dangling pointer is like a hotel room key for a room that has already been cleaned and reassigned to another guest. The key still “works” — it opens a door — but whatever you find behind that door is unpredictable and definitely not what you left there.
int *dangling(void) {
    int x = 42;
    return &x;  // DANGER! x lives on the stack and is destroyed when function returns.
    // The caller gets a pointer to memory that has already been reclaimed.
    // It might still read 42 by luck, or it might read garbage, or it might
    // read data from a completely unrelated function call.
}

char *also_dangling(void) {
    char buf[100];
    strcpy(buf, "Hello");
    return buf;  // DANGER! buf is a local stack array -- gone after return.
    // This is the single most common pointer bug in beginner C code.
}

// Safe alternatives
int *safe_alloc(void) {
    int *p = malloc(sizeof(int));
    *p = 42;
    return p;  // Caller must free
}

// Or return by value for small structs
typedef struct { int x, y; } Point;
Point make_point(int x, int y) {
    return (Point){x, y};  // Safe, returns copy
}

Null Pointer Dereference

void process(int *p) {
    // Always check for NULL
    if (p == NULL) {
        return;  // or handle error
    }
    *p = 42;
}

// Or use assert for programming errors
#include <assert.h>
void process_required(int *p) {
    assert(p != NULL);  // Crash with message if NULL
    *p = 42;
}

Wild Pointers

A wild pointer is an uninitialized pointer that contains whatever garbage bits happened to be in that stack location before. Dereferencing it is like dialing a random phone number and shouting instructions — you have no idea who is on the other end or what damage you might cause.
int *p;       // Uninitialized! Contains whatever was on the stack before.
*p = 42;      // UNDEFINED BEHAVIOR - writing to a random memory address.
              // Could corrupt your data, crash immediately, or silently break
              // something that only manifests hours later in a completely
              // unrelated part of the program.

// Always initialize pointers at declaration:
int *p = NULL;        // Safe: crash predictably (SIGSEGV) if you dereference by mistake
int *p = malloc(...); // Or allocate immediately and check for NULL
int *p = &some_var;   // Or point to a known-valid location

// Practical tip: compile with -Wuninitialized -Werror to catch many of these at compile time.

Exercises

1

Pointer Puzzle

Given int a = 5, *p = &a, **pp = &p;, trace what each of these expressions evaluates to: *p, **pp, *pp, &p, &a, p[0].
2

Custom memcpy

Implement your own void *my_memcpy(void *dest, const void *src, size_t n) using only pointer operations.
3

Reverse Array In-Place

Write void reverse(int *arr, size_t n) using only pointer arithmetic (no array indexing []).
4

Generic Sorting

Write a sorting function that accepts array, size, element size, and comparison function pointer, like qsort.
5

2D Dynamic Array

Implement a dynamically allocated 2D array with functions to create, access, and free it.

Next Up

Memory Layout & Segments

Understand how programs use memory

Interview Deep-Dive

Strong Answer:
  • The strict aliasing rule (C99 6.5/7) says that an object must only be accessed through a pointer to its declared type (or a compatible type, or char*). Accessing an int through a float* is undefined behavior. The compiler is allowed to assume that pointers of different types never alias the same memory, which enables aggressive optimizations like reordering loads and stores.
  • At -O0, the compiler generates naive code that reads from memory every time, so aliasing violations often “work.” At -O2, the compiler caches values in registers across stores to unrelated pointer types. If you write *(float*)&my_int = 3.14f; and then read my_int, the compiler may return the old value of my_int because it assumes the float* write could not have affected the int variable.
  • The safe alternatives are: use memcpy for type punning (compilers optimize it to zero overhead), use a union (defined behavior in C but not C++), or access through char*/unsigned char* which is explicitly allowed to alias anything.
  • Real-world example: network code that casts a char[] receive buffer to a struct ip_header* technically violates strict aliasing. Using memcpy to copy the bytes into a properly typed struct is both correct and equally fast after optimization.
Follow-up: How does the restrict keyword relate to aliasing, and when should you use it?Follow-up Answer:
  • restrict is a promise you make to the compiler: “This pointer is the only way to access the memory it points to during its lifetime.” This goes beyond strict aliasing — even two int* pointers might alias the same int, and the compiler must account for that. With restrict, the compiler knows they do not overlap and can keep values in registers across stores, vectorize loops, and reorder operations freely. memcpy uses restrict on both parameters (source and destination must not overlap); memmove does not. Misusing restrict (passing overlapping buffers to a restrict-qualified function) is undefined behavior that produces silently wrong results — not a crash, just incorrect output.
Strong Answer:
  • char *s = "hello" makes s a pointer that points to a string literal stored in read-only memory (the .rodata section). The pointer itself lives on the stack (or as a global), but the string data is shared and immutable. Writing s[0] = 'J' is undefined behavior — on most systems it causes a segfault because the page is mapped read-only.
  • char s[] = "hello" creates an array of 6 bytes on the stack (5 characters plus the null terminator) and copies the literal’s contents into it. The array is fully mutable. s[0] = 'J' changes it to “Jello” with no issues.
  • The subtle trap: sizeof(s) is different. For the pointer, sizeof(s) is 8 (pointer size on 64-bit). For the array, sizeof(s) is 6 (the full array including null terminator). This matters when computing buffer sizes.
  • In production, always declare string literal pointers as const char *s = "hello". This way the compiler warns you if you try to modify the literal, and it documents the intent. Code that omits the const is a red flag in code review.
Follow-up: What is array decay, and why does passing an array to a function lose its size information?Follow-up Answer:
  • When an array name appears in most expressions (including as a function argument), it “decays” to a pointer to its first element. Inside the called function, the parameter is a pointer — sizeof returns the pointer size (8 bytes), not the array size. This is a design decision from C’s origins: arrays were designed to be lightweight and interchangeable with pointers for efficiency. The practical consequence is that you must always pass the array size as a separate parameter. Forgetting this is the root cause of countless buffer overflows. Modern C practice is to pair every array parameter with a size_t length parameter, and some coding standards enforce this via naming conventions like void process(const int *data, size_t data_len).
Strong Answer:
  • When a function returns, its stack frame is deallocated — the stack pointer moves back up. The memory where the local variable lived is not zeroed or protected; it simply becomes “available” for the next function call to reuse. The returned pointer now points to this reclaimed memory. Dereferencing it may return the original value (by luck, if no other function has overwritten that stack slot yet), garbage from a subsequent function call, or crash with a segfault if the page has been unmapped (rare for stack memory).
  • This is undefined behavior, and the insidious part is that it often “works” in simple test cases (the stack slot has not been reused yet) and only fails in production when a different call pattern overwrites the memory.
  • Detection: -Wreturn-local-addr (GCC) and -Wreturn-stack-address (Clang) catch the obvious cases at compile time. AddressSanitizer (-fsanitize=address) catches it at runtime by placing red zones around stack variables and detecting access after the function returns. Valgrind’s memcheck also flags this as “use of uninitialized value” in many cases. In code review, any function returning a pointer should be scrutinized: the pointer must refer to heap memory (malloc), static/global memory, or memory owned by the caller (passed in as a parameter).
Follow-up: What are the safe alternatives to returning a pointer to a local?Follow-up Answer:
  • Return a heap-allocated object (caller is responsible for free). Return by value for small structs (C copies the struct into the caller’s stack frame — efficient for structs up to ~64 bytes). Accept a caller-provided buffer as a parameter (void get_name(char *buf, size_t buf_size)). Use a static local variable (safe but not thread-safe and not reentrant — each call overwrites the previous result). Of these, the caller-provided buffer pattern is the most common in production C code because it gives the caller full control over allocation lifetime and strategy.
Strong Answer:
  • qsort takes an array, its length, element size, and a comparison function pointer. It uses the comparison function to order arbitrary data types without knowing their structure. Internally, most implementations use a hybrid algorithm: introsort (quicksort with fallback to heapsort when recursion depth gets too deep) for the general case, with insertion sort for small subarrays. Since qsort operates on void*, it uses memcpy or byte-level swaps to move elements, which is slower than type-aware sorting but fully generic.
  • The classic bug in return *(int*)a - *(int*)b is signed integer overflow. If a is INT_MAX and b is -1, the subtraction produces INT_MAX - (-1) = INT_MAX + 1, which overflows a signed int (undefined behavior). The result wraps to INT_MIN (a negative number), telling qsort that INT_MAX < -1, which is wrong. This corrupts the sort order.
  • The safe comparison pattern is return (a > b) - (a < b), which returns -1, 0, or 1 without any arithmetic that could overflow. This is the pattern you should always use.
Follow-up: Why does qsort use void* instead of being type-specific, and what is the performance cost?Follow-up Answer:
  • C has no templates or generics at the language level, so qsort must work with void* for universality. The cost is twofold: every comparison requires a function pointer call (which the compiler cannot inline, defeating branch prediction and instruction cache locality), and every swap is a byte-by-byte memcpy instead of a single register move. Benchmarks show that a hand-written type-specific sort can be 2-5x faster than qsort on the same data. In performance-critical code, you either write your own sort or use a macro-generated sort (the Linux kernel’s sort() function avoids function pointer overhead by inlining the comparison).