C++ Pointer Arithmetic and Object Size Boundaries
Pointer arithmetic in C++ allows developers to navigate memory by
stepping through elements of a specific type rather than manipulating
raw byte addresses. The compiler relies strictly on the size of the
pointed-to type (sizeof(T)) to calculate offsets,
automatically scaling increment and decrement operations to align with
contiguous object boundaries. This article explains the underlying
mechanics of scaled address calculations, the role of compiler-enforced
memory boundaries, the constraints imposed by arrays, and the risks of
undefined behavior when these boundaries are violated.
The Scaling Mechanism of Pointer Arithmetic
In C++, pointers are strictly typed (with the exception of
void*). When an integer offset is added to or subtracted
from a pointer, the compiler does not advance the address by that number
of bytes. Instead, it multiplies the integer offset by the size of the
underlying type:
\[\text{Target Address} = \text{Base Address} + (n \times \text{sizeof}(T))\]
Consider the following operation:
int* int_ptr = reinterpret_cast<int*>(0x1000);
int_ptr += 2;Assuming sizeof(int) is 4 bytes on a typical 64-bit
architecture, int_ptr + 2 evaluates to address
0x1008 (\(0x1000 + (2 \times
4)\)), not 0x1002.
Because sizeof(T) is fundamental to this
calculation:
- A
char*increments by 1 byte. - A
double*typically increments by 8 bytes. - A pointer to a custom structure
struct Node { int id; double value; };increments bysizeof(Node), which includes internal structure padding added by the compiler to satisfy hardware alignment requirements.
Because a void* has no associated type and therefore no
determinable sizeof(void), standard C++ explicitly forbids
pointer arithmetic on void*.
Object Boundaries and Contiguous Memory
Pointer arithmetic is strictly defined only within the boundaries of
an array or a single object treated as an array of size one. In
contiguous memory allocations, such as a C-style array or
std::array, objects of type T are packed
sequentially:
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr; // Points to arr[0]When moving ptr forward:
ptr + 0designates the object boundary ofarr[0].ptr + 1designates the object boundary ofarr[1].ptr + 4designates the object boundary ofarr[4].
The C++ standard permits a pointer to point to any element within the
array, as well as to one position past the last element
(arr + 5). The one-past-the-end pointer serves as a valid
sentinel for loop termination (similar to end() in standard
containers). However, dereferencing this sentinel pointer constitutes
undefined behavior because no valid object boundary exists at that
location.
Pointer Difference
(ptrdiff_t)
When two pointers pointing to elements of the same array are
subtracted, the result is not the number of raw bytes between them. The
compiler divides the byte distance by sizeof(T):
\[\text{Index Difference} = \frac{\text{Address}_2 - \text{Address}_1}{\text{sizeof}(T)}\]
The resulting value is of the signed integral type
std::ptrdiff_t. Subtracting pointers that do not point into
the same array (or one-past-the-end) violates language constraints and
results in undefined behavior.
Pitfalls: Object Slicing and Polymorphic Arrays
A common architectural trap occurs when combining pointer arithmetic with inheritance. If a derived class is larger than its base class, navigating an array of derived objects using a base-class pointer breaks object size boundary calculations:
struct Base {
int b;
};
struct Derived : Base {
int d;
};
Derived derived_array[3];
Base* base_ptr = derived_array; // Implicit upcasting
base_ptr++; // Advances by sizeof(Base), not sizeof(Derived)Because sizeof(Derived) (typically 8 bytes) is larger
than sizeof(Base) (typically 4 bytes), incrementing
base_ptr advances the pointer directly into the middle of
the first Derived object rather than landing at the
boundary of the second Derived object. Accessing members
through base_ptr[1] reads misaligned data and causes
undefined behavior.
Summary of Rules
- Automatic Scaling: Every arithmetic operation
(
+,-,++,--) scales bysizeof(T). - Valid Range: Arithmetic must remain within
[0, N], whereNis the size of the array. The element atNis valid for comparison only, never dereferencing. - Homogeneous Types: Pointers being subtracted must point to elements within the same memory block.
- Polymorphic Caution: Never apply pointer arithmetic to base pointers referencing arrays of derived objects.