Pointer Arithmetic in C++: A Comprehensive Guide

Pointer Arithmetic in C++: Understanding the Fundamentals

Pointer arithmetic is one of C++’s most powerful low-level features, providing direct manipulation of memory addresses. While it can be dangerous when misused, understanding pointer arithmetic is essential for systems programming, implementing custom memory allocators, and writing performance-critical code.

What is Pointer Arithmetic?

Pointer arithmetic allows you to perform mathematical operations on pointers to traverse memory. Unlike regular integer arithmetic, pointer arithmetic is automatically scaled by the size of the type being pointed to.

int* IntPointer = new int[10];
IntPointer++;  // Advances by sizeof(int) bytes (typically 4 bytes)

double* DoublePointer = new double[10];
DoublePointer++;  // Advances by sizeof(double) bytes (typically 8 bytes)

When you increment a pointer, it doesn’t simply add 1 to the address, it advances by the size of one complete element. This automatic scaling makes array traversal intuitive and type-safe.

Valid Pointer Arithmetic Operations

C++ supports several operations on pointers:

  • Addition/Subtraction with integers: Pointer + 5, Pointer - 3
  • Increment/Decrement: Pointer++, --Pointer
  • Pointer differencing: PointerA - PointerB (yields number of elements between them)
  • Comparison: PointerA < PointerB, PointerA == PointerB
int Array[10] = {0, 1, 2, 3, 4, 5, 6, 7, 8, 9};
int* Start = Array;
int* End = Array + 10;

// Traverse the array
for (int* Current = Start; Current != End; ++Current) 
{
    std::cout << *Current << " ";
}

// Calculate array size
size_t ElementCount = End - Start;  // Returns 10

The Void Pointer Problem

A critical limitation to understand: pointer arithmetic is undefined for void* pointers. The compiler cannot perform arithmetic on void* because it doesn’t know the size of the pointed-to object.

void* VoidBuffer = malloc(100);
// VoidBuffer++;  // Error: invalid use of void expression!

This is why you’ll often see void* cast to char* or unsigned char* before any arithmetic operations are performed.

Why Use char* for Raw Memory Operations?

When working with raw memory buffers, unsigned char* (or char*) is the standard choice for several important reasons:

1. Guaranteed Size: sizeof(char) is always exactly 1 byte by definition, giving you precise byte-level control.

2. Type Aliasing: The C++ standard allows char* to alias any object type, making it legal for inspecting the byte representation of any object.

3. Portability: Every platform supports byte-level access through char*.

void ZeroMemory(void* Buffer, size_t ByteCount) 
{
    unsigned char* BytePointer = static_cast<unsigned char*>(Buffer);
    for (size_t Index = 0; Index < ByteCount; ++Index)
    {
        BytePointer[Index] = 0;
    }
}

Using Word-Sized Types for Performance

While char* provides byte-level control, operating systems and processors are optimized for word-aligned operations. A “word” is the natural data size for a processor, typically 4 bytes on 32-bit systems or 8 bytes on 64-bit systems.

When you need to move large blocks of memory efficiently, using word-sized types like size_t* or uint64_t* can significantly improve performance:

void FastMemoryCopy(void* Dest, const void* Source, size_t ByteCount) 
{
    // Copy word-by-word for bulk of data
    size_t* DestWords = static_cast<size_t*>(Dest);
    const size_t* SourceWords = static_cast<const size_t*>(Source);
    size_t WordCount = ByteCount / sizeof(size_t);
    
    for (size_t Index = 0; Index < WordCount; ++Index) 
    {
        DestWords[Index] = SourceWords[Index];
    }
    
    // Copy remaining bytes
    size_t BytesRemaining = ByteCount % sizeof(size_t);
    if (BytesRemaining > 0) 
    {
        unsigned char* DestBytes = reinterpret_cast<unsigned char*>(DestWords + WordCount);
        const unsigned char* SourceBytes = reinterpret_cast<const unsigned char*>(SourceWords + WordCount);
        
        for (size_t Index = 0; Index < BytesRemaining; ++Index) 
        {
            DestBytes[Index] = SourceBytes[Index];
        }
    }
}

This approach leverages the processor’s ability to move 4 or 8 bytes in a single instruction instead of copying byte-by-byte.

Memory Alignment Matters

Memory alignment is critical when using pointer arithmetic with word-sized types. Most processors require or strongly prefer that data be stored at addresses that are multiples of the data’s size:

  • int (4 bytes) should be at addresses divisible by 4
  • double (8 bytes) should be at addresses divisible by 8
  • size_t (4 or 8 bytes) should be at addresses divisible by 4 or 8

Accessing misaligned data can cause:

  • Significant performance penalties (multiple memory accesses required)
  • Hardware exceptions on some architectures (ARM, SPARC)
  • Undefined behavior according to the C++ standard
bool IsAligned(const void* Pointer, size_t Alignment) 
{
    return (reinterpret_cast<uintptr_t>(Pointer) % Alignment) == 0;
}

// Example: Ensure pointer is 8-byte aligned before treating as size_t*
void* AlignPointer(void* Pointer, size_t Alignment) 
{
    uintptr_t Address = reinterpret_cast<uintptr_t>(Pointer);
    size_t Misalignment = Address % Alignment;
    
    if (Misalignment != 0) 
    {
        size_t Padding = Alignment - Misalignment;
        return static_cast<unsigned char*>(Pointer) + Padding;
    }
    
    return Pointer;
}

This is why you must be careful when casting between pointer types, ensure the address is properly aligned for the target type.

Common Use Cases

Pointer arithmetic is essential in several domains:

Custom Memory Allocators: Games and real-time systems often use memory pools where pointer arithmetic manages allocation within a pre-allocated buffer.

Serialization: Converting data structures to byte streams for network transmission or file storage requires byte-level pointer manipulation.

Hardware Interfaces: Embedded systems use pointer arithmetic to access memory-mapped hardware registers at specific addresses.

Performance Optimization: High-performance libraries use pointer arithmetic to implement fast memory operations that outperform generic alternatives.

// Simple example: traversing a buffer
void ProcessBuffer(const unsigned char* Buffer, size_t Length)
{
    const unsigned char* Current = Buffer;
    const unsigned char* End = Buffer + Length;
    
    while (Current < End) 
    {
        ProcessByte(*Current);
        ++Current;
    }
}

Understanding pointer arithmetic opens the door to low-level optimization and systems programming, but it requires careful attention to type sizes, alignment, and memory safety. When used correctly, it provides unmatched control over memory operations that higher-level abstractions cannot match.