Pointer arithmetic is a powerful feature in the C++ programming language that allows developers to manipulate memory address directly. Understanding pointer arithmetic is crucial for tasks involving dynamic memory allocation, arrays, and data structures.
Basics of Pointers:
Before delving into pointer arithmetic, let's quickly review the basics of pointers. A pointer is a variable that stores the memory address of another variable. The declaration of a pointer involves specifying the type it points to, and the address it holds is obtained using the address-of operator (&
). For example:
int main() {
int number = 42;
int *ptr = &number; // Declaring and initializing a pointer
// Accessing the value using the pointer
std::cout << "Value at the memory location pointed by ptr: " << *ptr << std::endl;
return 0;
}
Pointer Arithmetic:
Pointer arithmetic involves manipulating the memory addresses stored in pointers. This feature allows us to apply certain integer arithmetic operators (addition, subtraction, increment, decrement) to a pointer to produce a new memory address.
Given some pointer ptr
, ptr + 1
returns the address of the next object in memory (based on the type being pointed to). So if ptr
is an int*
, and an int
is 4 bytes, ptr + 1
will return the memory address that is 4 bytes after ptr
, and ptr + 2
will return the memory address that is 8 bytes after ptr
.
#include <iostream>
int main()
{
int x {};
const int* ptr{ &x }; // assume 4 byte ints
std::cout << ptr << ' ' << (ptr + 1) << ' ' << (ptr + 2) << '\n';
return 0;
}
On author's machine this printed:
0x7ffe63102f34 0x7ffe63102f38 0x7ffe63102f3c
Note that each memory address is 4 bytes greater than the previous.
Similarly, pointer arithmetic also works with subtraction. Given some pointer ptr
, ptr - 1
returns the address of the previous object in memory (based on the type being pointed to).
#include <iostream>
int main()
{
int x {};
const int* ptr{ &x }; // assume 4 byte ints
std::cout << ptr << ' ' << (ptr - 1) << ' ' << (ptr - 2) << '\n';
return 0;
}
On the author's machine, this printed:
0x7ffdb96e5cb4 0x7ffdb96e5cb0 0x7ffdb96e5cac
In this case, each memory address is 4 bytes less than the previous.
Pointer arithmetic returns the address of the next/previous object (based on the type being pointed to).
Applying the increment (++
) and decrement (--
)operators to a pointer do the same thing as pointer addition and pointer subtraction respectively, but actually modify the address held by the pointer.
Given some int value x
, ++x
is shorthand for x = x +1
. Similarly, given some pointer ptr
, ++ptr
is shorthand for ptr = ptr + 1
, which does pointer arithmetic and assigns the results back to ptr
.
#include <iostream>
int main()
{
int x {};
const int* ptr{ &x }; // assume 4 byte ints
std::cout << ptr << '\n';
++ptr; // ptr = ptr + 1
std::cout << ptr << '\n';
--ptr; // ptr = ptr - 1
std::cout << ptr << '\n';
return 0;
}
On the author's machine, this printed:
0x7fff72f2b434
0x7fff72f2b438
0x7fff72f2b434
Subscripting is implemented via pointer arithmetic
#include <iostream>
int main()
{
const int arr[] { 9, 7, 5, 3, 1 };
int* ptr{ arr }; // a normal pointer holding the address of element 0
std::cout << ptr[2]; // subscript ptr to get element 2, prints 5
return 0;
}
// Output = 5
Let's take a deeper look at what's happening here.
It turns out that subscript operation ptr[n]
is a concise syntax equivalent to the more verbose expression *((ptr) + (n))
. You will note that this is just pointer arithmetic, with some additional parenthesis to ensure things evaluate in the correct order, and an implicit dereference to get the object at that address.
First, we initialize ptr
with arr
. When arr
is used as an initializer, it decays into a pointer holding the address of the element with index 0. So ptr
now holds the address of element 0.
Next, we print ptr[2]
. ptr[2]
is equivalent to *((ptr) + (2))
, which is equivalent to (*ptr + 2)
. ptr + 2
returns the address of the object that is two objects past ptr
, which is the element with index 2. The object at that address is then returned to the caller.
Let's take a look at another example:
#include <iostream>
int main()
{
const int arr[] { 3, 2, 1 };
// First, let's use subscripting to get the address and values of our array elements
std::cout << &arr[0] << ' ' << &arr[1] << ' ' << &arr[2] << '\n';
std::cout << arr[0] << ' ' << arr[1] << ' ' << arr[2] << '\n';
// Now let's do the equivalent using pointer arithmetic
std::cout << arr<< ' ' << (arr+ 1) << ' ' << (arr+ 2) << '\n';
std::cout << *arr<< ' ' << *(arr+ 1) << ' ' << *(arr+ 2) << '\n';
return 0;
}
On the author's machine, this printed:
0x7ffeafbe1434 0x7ffeafbe1438 0x7ffeafbe143c
3 2 1
0x7ffeafbe1434 0x7ffeafbe1438 0x7ffeafbe143c
3 2 1
You will note ptr
is holding address, (ptr + 1)
returns an address 4 bytes later, and (ptr + 2)
returns an address 8 bytes later. We can dereference these addresses to get the elements at those addresses.
Because array elements are always sequential in memory, if ptr
is a pointer to element 0 of an array, *(ptr + n)
will return the nth element in the array.
This could be the reason arrays are 0-based rather than 1-based. It makes the math more efficient (because the compiler doesn't have to subtract 1 whenever subscripting).
The compiler converts
ptr[n]
into*((ptr) + (n))
when subscripting a pointer, this means we can also subscript a pointer asn[ptr]
, the compiler convers this into*((n) + (ptr))
, which is behaviorally identical to*((ptr) + (n))
.
ptr[n] == *((ptr) + (n)) == n[ptr] == *((n) + (ptr))
Negative Indices
We just covered that *(ptr + 1)
returns the next object in memory. And ptr[1]
is just a convenient syntax to do the same.
Similarly, *(ptr - 1)
returns the previous object in memory which is equivalent to ptr[-1]
.
#include <array>
#include <iostream>
int main()
{
const int arr[] { 9, 8, 7, 6, 5 };
// Set ptr to point at element 3
const int* ptr { &arr[3] };
// Prove that we're pointing at element 3
std::cout << *ptr << ptr[0] << '\n'; // prints 66
// Prove that ptr[-1] is element 2!
std::cout << *(ptr-1) << ptr[-1] << '\n'; // prints 77
return 0;
}
Pointer arithmetic can be used to traverse an array
One of the most common use of pointer arithmetic is to iterate through C-style array without explicit indexing. The following example illustrates how this is done:
#include <iostream>
int main()
{
constexpr int arr[]{ 9, 7, 5, 3, 1 };
const int* begin{ arr }; // begin points to start element
const int* end{ arr + std::size(arr) }; // end points to one-past-the-end element
for (; begin != end; ++begin) // iterate from begin up to (but excluding) end
{
std::cout << *begin << ' '; // dereference our loop variable to get the current element
}
return 0;
}
// Output
9
7
5
3
1
In the above example, we start out traversal at the element pointed to by begin
(which in this case is element 0 of the array). Since begin ! = end
yet, the loop body executes. Inside the loop, we access the current element via *begin
, which is just a pointer dereference. After the loop body, we do ++begin
, which uses pointer arithmetic to increment begin
to point at the next element. Since begin != end
, the loop body executes again. This continues until begin != end
is false
, which happens when begin == end
.
Consider the following example:
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int *ptr = numbers; // Pointing to the first element of the array
// Accessing array elements using pointer arithmetic
std::cout << "Value at index 2: " << *(ptr + 2) << std::endl;
return 0;
}
In this example, ptr + 2
calculates the memory address of the third element of the array, and *(ptr + 2)
dereferences the pointer to obtain the value at that address.
Increment and Decrement Operations:
Pointer arithmetic is often used to iterate over arrays or dynamically allocate memory. The increment (++
) and decrement (--
) operation are handy for moving the pointer to the next or previous memory location.
Example:
int main() {
int numbers[] = {1, 2, 3, 4, 5};
int *ptr = numbers; // Pointing to the first element of the array
// Using increment to traverse the array
for (int i = 0; i < 5; ++i) {
std::cout << *ptr << " ";
++ptr; // Move to the next element
}
return 0;
}