Senior 5 min · March 06, 2026

Pointer Arithmetic — One-Past-the-End Bug in Trading System

The while (cursor <= end) bug that crashed a $50,000 real-world trading system — get the pointer arithmetic rule and how AddressSanitizer catches it.

N
Naren · Founder
Plain-English first. Then code. Then the interview question.
About
 ● Production Incident 🔎 Debug Guide
Quick Answer
  • Pointer arithmetic moves by element size, not bytes — the compiler scales n by sizeof(*ptr)
  • array[i] is syntactic sugar for *(array + i) — identical after compilation
  • Pointer subtraction gives the element count between two pointers; store in ptrdiff_t
  • One-past-end address is valid to compute but never to dereference — loop with < not <=
  • memcpy through uint8_t* to bypass strict aliasing when writing non-char data
Plain-English First

Imagine a hotel with numbered rooms. A pointer is like a keycard programmed to a specific room number. Pointer arithmetic is like telling a staff member 'go three rooms down from Room 101' — you don't say Room 104, you say 'plus three', and the building layout handles the rest. In C, the compiler is that building: it knows each room is a different size depending on what's stored there, so 'plus one' on an int pointer skips 4 bytes, not 1.

Most C bugs that end careers — buffer overflows, segfaults, corrupted data — trace back to one misunderstood concept: pointer arithmetic. It's not obscure wizardry reserved for OS kernels and embedded systems. It's the engine under every array you've ever used in C, every string you've ever printed, every memcpy that ever moved bytes across memory. If you've ever wondered why arrays and pointers feel interchangeable in C, pointer arithmetic is the answer.

The problem pointer arithmetic solves is elegant: how do you navigate raw memory without knowing the exact address of every element ahead of time? Arrays give you contiguous memory, but they're just a starting address. Pointer arithmetic lets you move through that memory systematically, letting the compiler handle the messy byte-offset calculations based on the data type's size. Without it, you'd be manually computing byte offsets for every element access — error-prone and unreadable.

By the end of this article you'll understand exactly why ptr + 1 doesn't mean 'add 1 byte', how to traverse arrays and strings without index variables, what makes pointer subtraction meaningful, and which patterns experienced C developers actually reach for in production code. You'll also know the two arithmetic operations that look valid but silently corrupt memory — and how to spot them before they bite you.

Why the Compiler Scales Pointer Steps by the Type Size

Here's the core insight that unlocks everything: when you write ptr + 1, C doesn't add the integer 1 to the address. It adds 1 sizeof(ptr). That scaling is automatic, silent, and non-negotiable.

Why does this exist? Because memory is byte-addressable, but your data isn't byte-sized. An int on most modern systems occupies 4 bytes. If you have an array of ints starting at address 1000, the second element is at address 1004, the third at 1008. Writing ptr + 2 to mean 'skip 8 bytes' would force every programmer to manually multiply by sizeof(int) everywhere — a nightmare that would produce subtly broken code whenever you changed the data type.

C's designers baked the scaling in so that ptr + n always means 'the address of the nth element after ptr', regardless of whether ptr points to a char, an int, a double, or a 200-byte struct. This is also why ptr++ on a double* moves 8 bytes forward, not 1.

This design decision is what makes array[i] syntactic sugar. The compiler literally rewrites it as *(array + i) — pointer arithmetic followed by a dereference. They are exactly the same operation.

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

int main(void) {
    int   scores[5]  = {10, 20, 30, 40, 50};
    char  letters[5] = {'A', 'B', 'C', 'D', 'E'};
    double temps[5]  = {36.6, 37.1, 38.0, 36.9, 37.5};

    int    *score_ptr  = scores;   /* points to scores[0]  */
    char   *letter_ptr = letters;  /* points to letters[0] */
    double *temp_ptr   = temps;    /* points to temps[0]   */

    /* Adding 1 to each pointer — watch how far each jumps */
    printf("=== Type sizes on this machine ===\n");
    printf("sizeof(int)    = %zu bytes\n", sizeof(int));
    printf("sizeof(char)   = %zu bytes\n", sizeof(char));
    printf("sizeof(double) = %zu bytes\n", sizeof(double));

    printf("\n=== Address jump when pointer is incremented by 1 ===\n");
    printf("score_ptr  before: %p\n", (void *)score_ptr);
    printf("score_ptr  after:  %p  (jumped %zu bytes)\n",
           (void *)(score_ptr + 1), sizeof(int));

    printf("letter_ptr before: %p\n", (void *)letter_ptr);
    printf("letter_ptr after:  %p  (jumped %zu bytes)\n",
           (void *)(letter_ptr + 1), sizeof(char));

    printf("temp_ptr   before: %p\n", (void *)temp_ptr);
    printf("temp_ptr   after:  %p  (jumped %zu bytes)\n",
           (void *)(temp_ptr + 1), sizeof(double));

    /* Prove array[i] == *(array + i) */
    printf("\n=== array[2] vs *(array + 2) ===\n");
    printf("scores[2]        = %d\n", scores[2]);          /* subscript form   */
    printf("*(scores + 2)    = %d\n", *(scores + 2));      /* pointer form     */
    printf("*(score_ptr + 2) = %d\n", *(score_ptr + 2));   /* via pointer var  */

    return 0;
}
Output
=== Type sizes on this machine ===
sizeof(int) = 4 bytes
sizeof(char) = 1 bytes
sizeof(double) = 8 bytes
=== Address jump when pointer is incremented by 1 ===
score_ptr before: 0x7ffd2a3b1010
score_ptr after: 0x7ffd2a3b1014 (jumped 4 bytes)
letter_ptr before: 0x7ffd2a3b1024
letter_ptr after: 0x7ffd2a3b1025 (jumped 1 bytes)
temp_ptr before: 0x7ffd2a3b1030
temp_ptr after: 0x7ffd2a3b1038 (jumped 8 bytes)
=== array[2] vs *(array + 2) ===
scores[2] = 30
*(scores + 2) = 30
*(score_ptr + 2) = 30
Interview Gold:
When an interviewer asks 'what does array[i] compile to?', the answer is (array + i). They're not just equivalent — they're identical after the preprocessor. This is also why 3[scores] is valid C: it expands to (3 + scores), which is the same as *(scores + 3).
Production Insight
Mismatched pointer types cause silent data corruption — an int* stepping over a char array reads garbage bytes.
The compiler won't warn you because it trusts your type annotations.
Rule: always double-check the pointed-to type when adding integers to pointers.
Key Takeaway
The compiler scales all pointer arithmetic by sizeof(*ptr).
ptr + n = address + n sizeof(ptr), not address + n.
This is why array[i] is just syntactic sugar for *(array + i).

Traversing Arrays and Strings the Way the Standard Library Does It

Now that scaling makes sense, let's look at why experienced C programmers sometimes prefer pointer traversal over index-based loops — and when it actually matters.

The standard library functions like strlen, strcpy, and memcpy are all implemented internally using pointer arithmetic. There's no index counter ticking up — there's a pointer walking forward until a condition is met. This pattern is worth knowing not because it's faster on modern CPUs (it often isn't; compilers are clever), but because it teaches you to think in terms of memory, not arrays.

Pointer traversal also shines when you're working with a section of an array — a slice. Instead of passing both an array and an index, you pass a pointer to where you want to start. The function doesn't need to know the original array at all. This is exactly how C string functions handle substrings: strstr returns a pointer into the original string, not a copy.

The key habit: always keep a sentinel — either a count of elements or a terminator value like '\0' — so your pointer knows when to stop.

pointer_traversal.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
52
53
54
55
56
57
58
#include <stdio.h>

/* Count characters in a string — reimplementing strlen with visible pointer steps */
size_t count_chars(const char *text) {
    const char *cursor = text;   /* cursor walks forward; text stays at the start */
    while (*cursor != '\0') {    /* dereference: read the byte cursor is pointing at */
        cursor++;                /* move cursor one char (1 byte) forward           */
    }
    /* Subtracting two pointers gives element count between them, not byte count */
    return (size_t)(cursor - text);
}

/* Sum a slice of an integer array — no index needed, just start + end pointers */
int sum_range(const int *start, const int *end) {
    int total = 0;
    /* 'end' is one-past-the-last valid element — a standard C idiom */
    for (const int *ptr = start; ptr < end; ptr++) {
        total += *ptr;           /* dereference ptr to read the current element */
    }
    return total;
}

/* Find the first negative number in an array; return pointer to it or NULL */
int *find_first_negative(int *data, size_t count) {
    int *ptr = data;
    int *sentinel = data + count;   /* one past the end — do NOT dereference this */
    while (ptr < sentinel) {
        if (*ptr < 0) return ptr;   /* return the actual address inside the array */
        ptr++;
    }
    return NULL;   /* no negative found */
}

int main(void) {
    /* --- String traversal --- */
    const char *message = "TheCodeForge";
    printf("String: \"%s\"\n", message);
    printf("Length via count_chars: %zu\n", count_chars(message));

    /* --- Slice summing --- */
    int readings[8] = {12, 7, 34, 5, 89, 23, 61, 4};
    /* Sum only elements at index 2, 3, 4 (readings[2] through readings[4]) */
    int slice_sum = sum_range(readings + 2, readings + 5);
    printf("\nReadings array: {12, 7, 34, 5, 89, 23, 61, 4}\n");
    printf("Sum of slice [2..4]: %d  (expected: 34+5+89 = 128)\n", slice_sum);

    /* --- Finding negative values --- */
    int temperatures[6] = {22, 18, -3, 25, -1, 30};
    int *first_negative = find_first_negative(temperatures, 6);

    if (first_negative != NULL) {
        /* Pointer subtraction tells us which index it landed on */
        ptrdiff_t index = first_negative - temperatures;
        printf("\nFirst negative: %d at index %td\n", *first_negative, index);
    }

    return 0;
}
Output
String: "TheCodeForge"
Length via count_chars: 12
Readings array: {12, 7, 34, 5, 89, 23, 61, 4}
Sum of slice [2..4]: 128 (expected: 34+5+89 = 128)
First negative: -3 at index 2
Pro Tip:
Use ptrdiff_t (from <stddef.h>) to store the result of pointer subtraction — never int. On 64-bit systems an array can be large enough that the difference between two pointers overflows a 32-bit int. ptrdiff_t is guaranteed to be large enough for any valid pointer difference on the platform.
Production Insight
Pointer subtraction can overflow silently if stored in int — on 64-bit systems, the difference may exceed 2^31.
This bug hides in testing because arrays are small.
Always store pointer differences in ptrdiff_t, never int.
Key Takeaway
Pointer subtraction yields element count, not byte count.
Use ptrdiff_t for storage: it's type-safe and portable.
One-past-end addresses are valid sentinels — never dereference them.

Not all pointer arithmetic is created equal. C's standard is precise about what's defined behaviour and what silently explodes.

Legal operations: You can add or subtract an integer to/from a pointer. You can subtract two pointers that point into the same array (including the one-past-the-end position). You can compare two pointers from the same array with <, >, <=, >=. That's it.

Illegal operations: Adding two pointers together is a compile error — it has no geometric meaning. Subtracting pointers from different arrays is undefined behaviour — the result might 'work' on your machine today and crash on the build server tomorrow. Dereferencing the one-past-the-end pointer is undefined behaviour, even though computing its address is fine.

The 'one past the end' rule is subtle but critical. C explicitly allows you to form the address array + N (where N is the array length) as a sentinel for loops. This is valid address arithmetic. What's undefined is actually reading or writing through that address — *(array + N) — because that memory isn't yours.

Pointer comparison with == and != is safe between any two pointers, even from different objects, but < and > between pointers from different objects is undefined. This matters when you're implementing a memory allocator or anything that reasons about relative positions.

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

int main(void) {
    int buffer[5] = {100, 200, 300, 400, 500};
    int *start = buffer;          /* points to buffer[0] */
    int *end   = buffer + 5;      /* one-past-end: valid ADDRESS, never dereference */

    /* === LEGAL: integer + pointer === */
    int *third_element = start + 2;       /* buffer[2], address is buffer_base + 8  */
    printf("Legal: start + 2 = %d\n", *third_element);

    /* === LEGAL: pointer - pointer (same array) === */
    ptrdiff_t distance = end - start;     /* gives 5 — element count, not byte count */
    printf("Legal: end - start = %td elements\n", distance);

    /* === LEGAL: pointer comparison within same array === */
    int *cursor = start;
    int count   = 0;
    while (cursor < end) {   /* cursor < end is legal — both point into buffer */
        count++;
        cursor++;
    }
    printf("Legal: counted %d elements via pointer comparison\n", count);

    /* === LEGAL: one-past-end address computation (no dereference) === */
    printf("Legal: end address computed = %p (not dereferenced)\n", (void *)end);

    /* === ILLEGAL (commented out to keep this code safe to run) ===

    // 1. Adding two pointers — compile error:
    //    int *bad = start + end;   // error: invalid operands to binary +

    // 2. Subtracting pointers from DIFFERENT arrays — undefined behaviour:
    //    int other[5] = {1,2,3,4,5};
    //    ptrdiff_t garbage = buffer - other;  // UB: might crash, might return wrong value

    // 3. Dereferencing one-past-end — undefined behaviour:
    //    int forbidden = *end;   // UB: reading memory that isn't yours

    // 4. Pointer arithmetic past the bounds — undefined behaviour:
    //    int *too_far = start + 10;  // UB even before dereferencing on some platforms
    */

    printf("\nAll legal operations completed safely.\n");
    return 0;
}
Output
Legal: start + 2 = 300
Legal: end - start = 5 elements
Legal: counted 5 elements via pointer comparison
Legal: end address computed = 0x7ffd1a2c3024 (not dereferenced)
All legal operations completed safely.
Watch Out:
Computing an out-of-bounds pointer address (not just dereferencing it) is already undefined behaviour if it goes more than one element past the end. start + 6 on a 5-element array is UB even if you never read from it. Many developers assume you only get UB when you dereference — that's wrong, and it matters when compilers use UB for optimisation.
Production Insight
Subtracting pointers from different objects is undefined — even if the addresses are valid.
In production, this can produce correct results until a compiler upgrade changes the optimisation.
Only subtract pointers known to be from the same array (or one past its end).
Key Takeaway
Legal: +/– integer to pointer, – between same array, comparison with < > within same array.
Illegal: adding two pointers, subtracting from different arrays, dereferencing one-past-end.
The one-past-end address is a valid sentinel — never read or write through it.

Real-World Pattern: Using Pointer Arithmetic in a Custom Memory Buffer

Let's put it all together with a pattern you'll actually encounter: a simple write buffer that tracks its own current position using pointer arithmetic. This is the core idea behind arena allocators, packet serializers, and file format writers.

The idea is straightforward: you allocate a fixed block of memory, keep a write_ptr that starts at the beginning, and advance it each time you write data. Pointer arithmetic tells you how much space you've used (write_ptr - buffer_start) and how much you have left (buffer_end - write_ptr).

This pattern is used in real systems because it avoids repeated malloc calls and gives you cache-friendly contiguous storage. Game engines, network stacks, and embedded firmware all use variations of this. Understanding it requires being comfortable with pointer arithmetic: you need to write to the current position, advance past what you just wrote, and check bounds without ever losing track of where you started.

The code below shows a minimal version you can actually run — a byte buffer that serializes integers and strings one after another, with boundary checking at each step.

write_buffer.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
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
#include <stdio.h>
#include <string.h>   /* memcpy  */
#include <stdint.h>   /* uint8_t */
#include <stddef.h>   /* ptrdiff_t, size_t */

#define BUFFER_CAPACITY 64

typedef struct {
    uint8_t  storage[BUFFER_CAPACITY]; /* raw byte storage                        */
    uint8_t *write_ptr;                /* where the next byte will be written     */
    uint8_t *end_ptr;                  /* one-past-end sentinel for bounds checks */
} WriteBuffer;

void buffer_init(WriteBuffer *buf) {
    buf->write_ptr = buf->storage;                  /* start at the beginning     */
    buf->end_ptr   = buf->storage + BUFFER_CAPACITY;/* one past the last byte     */
}

/* Returns bytes used so far */
size_t buffer_used(const WriteBuffer *buf) {
    return (size_t)(buf->write_ptr - buf->storage); /* pointer subtraction = count */
}

/* Returns bytes remaining */
size_t buffer_remaining(const WriteBuffer *buf) {
    return (size_t)(buf->end_ptr - buf->write_ptr); /* space left to write into   */
}

/* Write a 32-bit integer into the buffer (big-endian) */
int buffer_write_int32(WriteBuffer *buf, int32_t value) {
    if (buffer_remaining(buf) < sizeof(int32_t)) {
        return -1;  /* not enough space */
    }
    /* memcpy is the safe way to write non-char data through a byte pointer */
    memcpy(buf->write_ptr, &value, sizeof(int32_t));
    buf->write_ptr += sizeof(int32_t); /* advance past the 4 bytes we just wrote */
    return 0;
}

/* Write a null-terminated string (without the null terminator) into the buffer */
int buffer_write_string(WriteBuffer *buf, const char *text) {
    size_t text_length = strlen(text);
    if (buffer_remaining(buf) < text_length) {
        return -1;  /* not enough space */
    }
    memcpy(buf->write_ptr, text, text_length);
    buf->write_ptr += text_length;     /* advance past the string bytes we wrote  */
    return 0;
}

int main(void) {
    WriteBuffer packet;
    buffer_init(&packet);

    printf("Buffer capacity: %d bytes\n", BUFFER_CAPACITY);
    printf("Initial — used: %zu, remaining: %zu\n",
           buffer_used(&packet), buffer_remaining(&packet));

    /* Write a 32-bit sensor ID */
    int32_t sensor_id = 4029;
    buffer_write_int32(&packet, sensor_id);
    printf("After writing int32 (%d) — used: %zu, remaining: %zu\n",
           sensor_id, buffer_used(&packet), buffer_remaining(&packet));

    /* Write a status string */
    const char *status = "SENSOR_OK";
    buffer_write_string(&packet, status);
    printf("After writing string (\"%s\") — used: %zu, remaining: %zu\n",
           status, buffer_used(&packet), buffer_remaining(&packet));

    /* Write another int */
    int32_t timestamp = 1718000000;
    buffer_write_int32(&packet, timestamp);
    printf("After writing timestamp (%d) — used: %zu, remaining: %zu\n",
           timestamp, buffer_used(&packet), buffer_remaining(&packet));

    /* Show the raw bytes written */
    printf("\nRaw bytes in buffer (%zu total):\n", buffer_used(&packet));
    for (uint8_t *p = packet.storage; p < packet.write_ptr; p++) {
        /* Print hex for non-printable bytes, char for printable ones */
        if (*p >= 32 && *p < 127)
            printf(" '%c'", *p);
        else
            printf(" %02X", *p);
    }
    printf("\n");

    return 0;
}
Output
Buffer capacity: 64 bytes
Initial — used: 0, remaining: 64
After writing int32 (4029) — used: 4, remaining: 60
After writing string ("SENSOR_OK") — used: 13, remaining: 51
After writing timestamp (1718000000) — used: 17, remaining: 47
Raw bytes in buffer (17 total):
BD 0F 00 00 'S' 'E' 'N' 'S' 'O' 'R' '_' 'O' 'K' 00 49 47 66
Pro Tip:
Never use pointer arithmetic to write non-char types directly via a cast like (int32_t )write_ptr = value; write_ptr += 4. This violates C's strict aliasing rules and can produce subtly wrong code at higher optimisation levels. Always use memcpy to write non-byte data through a uint8_t* — it's what the standard intends, and compilers optimise it away to a single register store anyway.
Production Insight
Strict aliasing violations are the #1 cause of mysterious production bugs after compiler optimization changes.
Using direct pointer cast + assignment looks faster but produces incorrect assembly under -O2.
memcpy with uint8_t* is the standard-conforming pattern that compilers optimize to single stores.
Key Takeaway
Use memcpy to write non-char data through a byte buffer pointer.
Pointer arithmetic on uint8_t* with memcpy is safe, portable, and optimizable.
Never cast a uint8_t to int32_t and dereference — strict aliasing will break you.

Debugging Pointer Arithmetic: How Sanitizers Catch the Bugs You Miss

Pointer arithmetic bugs are notoriously hard to reproduce. A buffer overflow might only crash when the next heap chunk is allocated. An off-by-one might corrupt data silently for weeks before manifesting as a production outage.

The tools that find these bugs don't rely on luck. AddressSanitizer (ASan) instruments every memory access at compile time, checking each pointer operation against the known bounds of the allocation. It catches overflows, use-after-free, and garbage pointer dereferences. It's not a debugger — it's a runtime safety net that terminates the program with a precise report at the first violation.

UndefinedBehaviorSanitizer (UBSan) catches the subtler errors: pointer arithmetic past the allowed bound, misaligned pointer operations, and strict aliasing violations. Together, ASan and UBSan should be part of your standard debug build for any C project that uses pointer arithmetic.

Valgrind (Memcheck) is the heavy artillery for heap corruption. It intercepts every malloc and free, tracking the validity and origin of every byte. When pointer arithmetic causes you to read uninitialised memory, Valgrind tells you exactly where the value came from.

The critical habit: always run with these tools during development and in CI. A single pointer arithmetic mistake that survives code review will eventually cause a production incident. Sanitizers make that mistake visible in the first test run.

sanitize_pointer_arithmetic.shBASH
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
#!/bin/bash
# Build with AddressSanitizer and UndefinedBehaviorSanitizer
# compile
clang -fsanitize=address,undefined -g -O1 -o program program.c
# run — ASan checks every pointer operation
./program
# If there's a bug, ASan will print a detailed report with:
#   - The exact line of the violation
#   - The allocation context
#   - The access direction (read/write) and offset
#
# Example ASan output for a one-past-end dereference:
# ==12345==ERROR: AddressSanitizer: heap-buffer-overflow on address 0x...
# READ of size 4 at 0x... thread T0
#     #0 in main at pointer_test.c:15
#     #1 in ... libc_start_main ...
# 0x... is located 0 bytes after 20-byte region [0x...,0x...)
# allocated by thread T0 here:
#     #0 in malloc ...
#     #1 in main at pointer_test.c:10
Output
No output if the program is clean.
If a bug exists, ASan exits with a non-zero status and prints a trace.
Don't Ship Without This:
Add -fsanitize=address,undefined to your debug build flags and run your test suite with it. A single pointer arithmetic bug that passes code review will eventually cost you a PagerDuty alert. ASan catches it on the first access, not after weeks of silent corruption.
Production Insight
Pointer arithmetic bugs are time bombs — they often don't crash during development because heap layout is deterministic.
Production uses different allocators, thread interleavings, and optimizations — that's when they explode.
Use ASan and UBSan in CI to catch them before they reach production.
Key Takeaway
Run AddressSanitizer and UndefinedBehaviorSanitizer in development and CI.
They catch out-of-bounds, misaligned, and invalid pointer arithmetic on first access.
Valgrind is for heap corruption; sanitizers are faster and easier to automate.
● Production incidentPOST-MORTEMseverity: high

The $50,000 Dereference: One Off-by-One Crashed a Trading System

Symptom
Production crashes occurred every few hours with SIGSEGV in the packet decoder. The crash was non-deterministic — sometimes it ran for hours, sometimes minutes. The stack trace pointed deep inside a memcpy call.
Assumption
The team assumed the crash was a hardware issue or a kernel bug because it only happened under full load. They spent two days rebuilding servers before looking at the code.
Root cause
The decoder loop used while (cursor <= end) on a pointer into a packet buffer. The end pointer pointed to one-past-the-last valid byte. On the last iteration, cursor pointed to the one-past-the-end address, and the loop body dereferenced it. The read was from unmapped memory.
Fix
Change the loop condition from cursor <= end to cursor < end. The one-past-end address is valid to compare against but never to read. The fix took 30 seconds to implement and test.
Key lesson
  • Never dereference the one-past-the-end pointer — it's guaranteed undefined behaviour even if the address appears valid.
  • Always use ptr < end not ptr <= end when walking a buffer with a sentinel pointer.
  • Invest in AddressSanitizer in your CI pipeline — it catches this exact bug on the first run.
Production debug guideWhen your program crashes or produces wrong data, use this guide to narrow down the pointer arithmetic root cause.4 entries
Symptom · 01
SIGSEGV on pointer increment in a loop — crash happens after many iterations, not the first one.
Fix
Check the loop sentinel condition. Look for <= instead of <. Verify that the end pointer is one past the last valid element, not pointing at it.
Symptom · 02
Data read from array is garbage but no crash — values appear swapped or partially overwritten.
Fix
Check if you wrote through a pointer casted to a different type. Example: (int32_t )(uint8_t_ptr) violates strict aliasing. Use memcpy instead.
Symptom · 03
Pointer subtraction result is huge or negative even on small arrays.
Fix
Ensure both pointers point into the same array object. Subtracting pointers from different arrays is undefined. Also verify the result is stored in ptrdiff_t, not int.
Symptom · 04
Stack trace shows crash during free() or realloc() — heap corruption.
Fix
Check if you wrote past the allocated buffer using pointer arithmetic. Use AddressSanitizer or Valgrind to detect buffer overflow.
★ Quick Debug: Pointer Arithmetic ErrorsWhen you suspect a pointer arithmetic bug, run these commands before diving into the code.
Intermittent crash with no clear pattern
Immediate action
Recompile with AddressSanitizer enabled
Commands
gcc -fsanitize=address -g -O1 -o app app.c
./app 2>&1 | head -50
Fix now
AddressSanitizer will point to the exact line where the out-of-bounds access occurs. Fix the loop boundary or allocation size.
Data corruption after pointer arithmetic but no crash+
Immediate action
Compile with -Wall -Wextra -Wstrict-aliasing and run under Valgrind
Commands
gcc -Wall -Wextra -Wstrict-aliasing -g -O0 -o app app.c
valgrind --tool=memcheck --track-origins=yes ./app
Fix now
Valgrind reports invalid reads/writes. If none, suspect strict aliasing violation — use memcpy instead of pointer casting.
Pointer arithmetic involving structs with padding+
Immediate action
Print the struct size and offset of each member
Commands
printf("sizeof(struct) = %zu\n", sizeof(struct my_struct));
printf("offsetof member2 = %zu\n", offsetof(struct my_struct, member2));
Fix now
Adjust your pointer step size to match the actual struct size. If using offsetof, ensure manual calculation accounts for padding.
AspectIndex-Based Access (array[i])Pointer Arithmetic (*ptr)
ReadabilityHigh — intent is obvious to all readersModerate — requires knowing the pattern
What compiler generatesIdentical machine code after optimisationIdentical machine code after optimisation
Bounds checkingNo automatic checking in C either wayNo automatic checking in C either way
Slice / subrange passingMust pass both array + start indexPass a single pointer to the start element
String walking (no length)Awkward — need a manual index counterNatural — cursor++ until sentinel hit
Pointer subtraction (distance)Not directly applicableptr_b - ptr_a gives element count between them
Risk of going out of boundsEasy to catch via off-by-one in loop conditionSlightly harder — no obvious upper bound without sentinel
Used in standard libraryRarely in implementationUniversally — strlen, memcpy, strchr all use it

Key takeaways

1
ptr + n always means 'skip n elements', not 'skip n bytes'
the compiler multiplies n by sizeof(*ptr) automatically, which is why changing a pointer's type changes how far it moves.
2
array[i] and *(array + i) are 100% identical after compilation
the subscript operator is defined as pointer arithmetic plus a dereference, which is why 3[array] is valid (if terrible) C.
3
Pointer subtraction gives you the element count between two pointers, not the byte count
store it in ptrdiff_t, never int, to survive on 64-bit platforms with large arrays.
4
The one-past-end address is legal to compute and compare against but undefined to dereference
this is the foundation of every for (ptr = start; ptr < end; ptr++) loop in the C standard library.
5
Always compile with AddressSanitizer and UndefinedBehaviorSanitizer in debug builds
pointer arithmetic bugs that pass code review will eventually cause a production incident.

Common mistakes to avoid

3 patterns
×

Treating pointer arithmetic as byte arithmetic

Symptom
Reading corrupted or wrong values when stepping through an array, e.g. getting garbage after writing ptr = (int)((char)ptr + 1) to advance by 'one'.
Fix
Let C's type system do the scaling. Write ptr + 1 or ptr++ on a correctly typed pointer and the compiler multiplies by sizeof automatically. Only use explicit byte offsets when you genuinely need to work at the byte level, and use char or uint8_t for that.
×

Dereferencing the one-past-the-end pointer

Symptom
Intermittent crashes or silent data corruption, often reading a valid-looking value that belongs to the next variable on the stack.
Fix
Your loop sentinel should always be ptr < end_ptr, never ptr <= end_ptr. The one-past-end address is legal to compute and compare against, but the instant you write *end_ptr you have undefined behaviour regardless of what happens to be sitting at that address.
×

Storing pointer subtraction results in an `int`

Symptom
Correct results on small arrays in testing, then mysterious negative numbers or truncated counts on large arrays in production.
Fix
Always use ptrdiff_t (from <stddef.h>) to store the result of subtracting two pointers. On 64-bit platforms, arrays can be large enough that the difference overflows a 32-bit int. ptrdiff_t is guaranteed by the standard to hold any valid pointer difference for the target platform.
INTERVIEW PREP · PRACTICE MODE

Interview Questions on This Topic

Q01JUNIOR
If `int *ptr` points to the first element of an `int` array, what is the...
Q02SENIOR
What is the difference between `ptr++` and `++ptr` when used in an expre...
Q03SENIOR
Why is subtracting two pointers that point into different arrays undefin...
Q01 of 03JUNIOR

If `int *ptr` points to the first element of an `int` array, what is the numeric difference between `ptr+3` and `ptr` on a system where sizeof(int) is 4? And what does that tell you about how pointer arithmetic scales?

ANSWER
The difference in address is 12 bytes (3 4), because pointer arithmetic is scaled by the element size. The numeric value of ptr+3 is ptr + 3 sizeof(int) = ptr + 12. This shows that ptr + n means 'skip n elements', not 'skip n bytes'. The compiler automatically multiplies the integer offset by the size of the pointed-to type.
FAQ · 5 QUESTIONS

Frequently Asked Questions

01
Why does adding 1 to a pointer not move it by 1 byte in C?
02
Is pointer arithmetic faster than array indexing in C?
03
Can you do pointer arithmetic on a void pointer in C?
04
What happens to pointer arithmetic when the pointed-to type is a struct with padding?
05
Is it safe to use pointer arithmetic with `void*` in C++ where I've enabled compiler extensions?
🔥

That's C Basics. Mark it forged?

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

Previous
Pointers in C
9 / 17 · C Basics
Next
Structures and Unions in C