Ready to practice?
Sign up to access interactive coding exercises and track your progress.
Navigating Memory with Pointer Arithmetic
Calculate memory addresses and access elements using pointer offsets.
Pointer arithmetic and subscripting
Arrays occupy contiguous memory locations. This lesson examines how array indexing works at the memory level.
While you won't use pointer arithmetic math daily, understanding these concepts provides insight into how range-based for loops function and prepares you for working with iterators.
What is pointer arithmetic?
Pointer arithmetic enables applying certain integer operations (addition, subtraction, increment, or decrement) to a pointer to calculate a new memory address.
Given pointer ptr, ptr + 1 yields the address of the next object in memory (scaled by the pointed-to type's size). For a double* where doubles are 8 bytes, ptr + 1 returns an address 8 bytes beyond ptr, and ptr + 2 returns an address 16 bytes beyond.
#include <iostream>
int main()
{
double score{};
const double* ptr{ &score }; // assume 8 byte doubles
std::cout << ptr << ' ' << (ptr + 1) << ' ' << (ptr + 2) << '\n';
return 0;
}
Sample output:
0x7ffeeb4ca8b0 0x7ffeeb4ca8b8 0x7ffeeb4ca8c0
Each address is 8 bytes larger than the previous.
Subtraction works similarly. Given ptr, ptr - 1 yields the address of the previous object in memory.
#include <iostream>
int main()
{
double score{};
const double* ptr{ &score }; // assume 8 byte doubles
std::cout << ptr << ' ' << (ptr - 1) << ' ' << (ptr - 2) << '\n';
return 0;
}
Sample output:
0x7ffeeb4ca8b0 0x7ffeeb4ca8a8 0x7ffeeb4ca8a0
Each address is 8 bytes smaller than the previous.
Pointer arithmetic returns the address of the next/previous object (scaled by the pointed-to type's size), not just the next/previous byte address.
The increment (++) and decrement (--) operators perform the same calculations but modify the pointer itself.
Given integer num, ++num is shorthand for num = num + 1. Similarly, for pointer ptr, ++ptr is shorthand for ptr = ptr + 1, performing pointer arithmetic and assigning the result back.
#include <iostream>
int main()
{
double score{};
const double* ptr{ &score }; // assume 8 byte doubles
std::cout << ptr << '\n';
++ptr; // ptr = ptr + 1
std::cout << ptr << '\n';
--ptr; // ptr = ptr - 1
std::cout << ptr << '\n';
return 0;
}
Sample output:
0x7ffeeb4ca8b0
0x7ffeeb4ca8b8
0x7ffeeb4ca8b0
Pointer arithmetic outside array bounds is undefined behavior. The C++ standard defines pointer arithmetic only when both the pointer and result are within the same array (or one-past-the-end). Modern compilers generally don't prevent this, but relying on it is risky.
Subscripting uses pointer arithmetic
In previous lessons, we applied operator[] to pointers:
#include <iostream>
int main()
{
const int levels[]{ 5, 12, 8, 15, 3 };
const int* ptr{ levels }; // pointer holding address of element 0
std::cout << ptr[3]; // subscript ptr to get element 3, prints 15
return 0;
}
Here's how this actually works.
The subscript operation ptr[n] is syntactic sugar for *((ptr) + (n)). This is pointer arithmetic with parentheses for evaluation order and implicit dereferencing to access the object at that address.
First, ptr is initialized with levels. When used as an initializer, levels decays to a pointer holding element 0's address. So ptr holds element 0's address.
Next, we print ptr[3]. This is equivalent to *((ptr) + (3)), which simplifies to *(ptr + 3). The expression ptr + 3 yields the address three objects past ptr—the element at index 3. Dereferencing returns that object.
Another example:
#include <iostream>
int main()
{
const int damage[]{ 25, 50, 75 };
// Get addresses and values using subscripting
std::cout << &damage[0] << ' ' << &damage[1] << ' ' << &damage[2] << '\n';
std::cout << damage[0] << ' ' << damage[1] << ' ' << damage[2] << '\n';
// Same results using pointer arithmetic
std::cout << damage << ' ' << (damage + 1) << ' ' << (damage + 2) << '\n';
std::cout << *damage << ' ' << *(damage + 1) << ' ' << *(damage + 2) << '\n';
return 0;
}
Sample output:
0x7ffee3ca4a00 0x7ffee3ca4a04 0x7ffee3ca4a08
25 50 75
0x7ffee3ca4a00 0x7ffee3ca4a04 0x7ffee3ca4a08
25 50 75
The address damage holds differs from (damage + 1) by 4 bytes, and (damage + 2) differs by 8 bytes. Dereferencing retrieves the elements at those addresses.
Because array elements are sequential, when arr points to element 0, *(arr + n) accesses the n-th element.
This is why arrays are 0-based: the compiler doesn't need to subtract 1 during subscripting, making indexing more efficient.
An interesting consequence: since ptr[n] becomes *((ptr) + (n)), you can also write n[ptr]. The compiler converts this to *((n) + (ptr)), which is behaviorally identical. Don't do this—it's confusing.
Pointer arithmetic and subscripting are relative
When learning subscripting, it's natural to assume indices represent fixed array positions: index 0 is always the first element, index 1 is always the second, etc.
This is an illusion. Array indices are relative positions. They only appear fixed because we typically index from the array's start (element 0).
Remember, *(ptr + 1) and ptr[1] both return the next object in memory. "Next" is relative. If ptr points to element 0, both expressions return element 1. But if ptr points to element 4, both return element 5.
#include <iostream>
int main()
{
const int scores[]{ 100, 85, 90, 75, 60 };
const int* ptr{ scores }; // ptr points to element 0
// Verify ptr points to element 0
std::cout << *ptr << ptr[0] << '\n'; // prints 100100
// ptr[1] is element 1
std::cout << *(ptr + 1) << ptr[1] << '\n'; // prints 8585
// Now point to element 4
ptr = &scores[4];
// Verify ptr points to element 4
std::cout << *ptr << ptr[0] << '\n'; // prints 6060
// ptr[1] is element 5 (past the array!)
std::cout << *(ptr + 1) << ptr[1] << '\n'; // undefined behavior
return 0;
}
Code becomes confusing when ptr[1] doesn't mean "element 1." Therefore, use subscripting only when indexing from element 0. Use pointer arithmetic for relative positioning.
Use subscripting when indexing from element 0 (indices match element positions). Use pointer arithmetic for relative positioning from a given element.
Negative indices
Previously, we mentioned that C-style array indices can be signed integers. This isn't just convenience—you can actually use negative subscripts.
We covered that *(ptr - 1) returns the previous object in memory. The subscript equivalent is ptr[-1].
#include <iostream>
int main()
{
const int health[]{ 100, 85, 70, 55, 40 };
// Point to element 4
const int* ptr{ &health[4] };
// Verify ptr points to element 4
std::cout << *ptr << ptr[0] << '\n'; // prints 4040
// ptr[-1] is element 3
std::cout << *(ptr - 1) << ptr[-1] << '\n'; // prints 5555
return 0;
}
Traversing arrays with pointer arithmetic
A common use of pointer arithmetic is iterating through C-style arrays without explicit indices:
#include <iostream>
#include <iterator>
int main()
{
constexpr int xpGains[]{ 150, 275, 100, 50, 325 };
const int* begin{ xpGains }; // begin points to first element
const int* end{ xpGains + std::size(xpGains) }; // end points to one-past-the-end
for (; begin != end; ++begin) // iterate until begin reaches end
{
std::cout << *begin << ' '; // dereference to access current element
}
return 0;
}
Output:
150 275 100 50 325
We start at the element begin points to (element 0). Since begin != end, the loop body executes. Inside, *begin dereferences to access the current element. After each iteration, ++begin advances to the next element using pointer arithmetic. This continues until begin == end.
Note that end holds the address one-past-the-end. Holding this address is valid (as long as we don't dereference it). This simplifies our comparisons—no off-by-one adjustments needed.
Pointer arithmetic on C-style array elements is valid when the result is within array bounds or one-past-the-end. Going beyond these bounds is undefined behavior, even without dereferencing.
This pattern can be extracted into a function that works with decayed arrays:
#include <iostream>
#include <iterator>
void showStats(const int* begin, const int* end)
{
for (; begin != end; ++begin)
{
std::cout << *begin << ' ';
}
std::cout << '\n';
}
int main()
{
constexpr int xpGains[]{ 150, 275, 100, 50, 325 };
const int* begin{ xpGains };
const int* end{ xpGains + std::size(xpGains) };
showStats(begin, end);
return 0;
}
This works even though we never pass the array directly. The begin/end pointers contain all information needed to traverse the elements.
The standard library uses this begin/end pattern extensively in functions and algorithms.
Range-based for loops use pointer arithmetic
Consider this range-based for loop:
#include <iostream>
int main()
{
constexpr int xpGains[]{ 150, 275, 100, 50, 325 };
for (auto gain : xpGains)
{
std::cout << gain << ' ';
}
return 0;
}
Range-based for loops are typically implemented like this:
{
auto __begin = begin-expr;
auto __end = end-expr;
for ( ; __begin != __end; ++__begin)
{
range-declaration = *__begin;
loop-statement;
}
}
Expanding the previous example:
#include <iostream>
#include <iterator>
int main()
{
constexpr int xpGains[]{ 150, 275, 100, 50, 325 };
auto __begin = xpGains; // begin expression
auto __end = xpGains + std::size(xpGains); // end expression
for (; __begin != __end; ++__begin)
{
auto gain = *__begin; // range declaration
std::cout << gain << ' '; // loop statement
}
return 0;
}
This is nearly identical to our manual pointer traversal. The only difference is assigning *__begin to gain rather than using *__begin directly.
Summary
Pointer arithmetic: Adding, subtracting, incrementing, or decrementing pointers produces new memory addresses. ptr + 1 returns the address of the next object (scaled by object size), not just the next byte.
Subscripting via pointer arithmetic: ptr[n] is syntactic sugar for *((ptr) + (n)). This explains 0-based arrays: no subtraction needed, making indexing efficient.
Relative vs absolute indices: Array indices are relative positions. ptr[1] means "the element after what ptr points to," not always "element 1." Indices appear fixed only because we usually index from element 0.
Negative indices: C-style arrays support negative subscripts because ptr[-1] equals *(ptr - 1), accessing the previous element. Only meaningful when ptr points beyond the first element.
Traversing with pointers: Use begin and end pointers (end pointing one-past-the-end) to iterate through arrays. Increment the pointer each iteration and compare against end. Works with decayed arrays.
Begin/end pattern: Standard library algorithms use begin/end pointer pairs to specify which elements to operate on. This avoids passing arrays directly.
Range-based for loops: Implemented using pointer arithmetic internally. The compiler creates begin and end pointers, iterates until they're equal, and dereferences in each iteration.
Pointer arithmetic bounds: Valid only when results are within array bounds or one-past-the-end. Going beyond is undefined behavior, even without dereferencing.
Understanding pointer arithmetic reveals how array indexing actually works and prepares you for iterator and algorithm design patterns throughout the standard library.
Navigating Memory with Pointer Arithmetic - Quiz
Test your understanding of the lesson.
Practice Exercises
Pointer Arithmetic and Subscripting
Practice pointer arithmetic to navigate through arrays. Understand the relationship between pointers and array subscripting.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!