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.
- 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
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.
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).ptr + n = address + n sizeof(ptr), not address + n.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.
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.Legal vs. Illegal Pointer Arithmetic — The Rules That Prevent Chaos
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.
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.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.
(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.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.
-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.The $50,000 Dereference: One Off-by-One Crashed a Trading System
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.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.- Never dereference the one-past-the-end pointer — it's guaranteed undefined behaviour even if the address appears valid.
- Always use
ptr < endnotptr <= endwhen walking a buffer with a sentinel pointer. - Invest in AddressSanitizer in your CI pipeline — it catches this exact bug on the first run.
<= instead of <. Verify that the end pointer is one past the last valid element, not pointing at it.(int32_t )(uint8_t_ptr) violates strict aliasing. Use memcpy instead.ptrdiff_t, not int.free() or realloc() — heap corruption.Key takeaways
ptr + n always means 'skip n elements', not 'skip n bytes'array[i] and *(array + i) are 100% identical after compilation3[array] is valid (if terrible) C.ptrdiff_t, never int, to survive on 64-bit platforms with large arrays.for (ptr = start; ptr < end; ptr++) loop in the C standard library.Common mistakes to avoid
3 patternsTreating pointer arithmetic as byte arithmetic
ptr = (int)((char)ptr + 1) to advance by 'one'.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
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`
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 Questions on This Topic
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?
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.Frequently Asked Questions
That's C Basics. Mark it forged?
5 min read · try the examples if you haven't