Skip to main content

Pointer Arithmetic

Pointer arithmetic navigates contiguous memory with automatic scaling by type size. Essential for arrays but dangerous without bounds checking.

Automatic Scaling
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

ptr + 1; // Moves by sizeof(int) = 4 bytes, not 1 byte!

Compiler handles math: ptr + nptr + (n * sizeof(type))

Memory Layout

Address    Value    Pointer Position
0x1000 10 ptr + 0 (arr[0])
0x1004 20 ptr + 1 (arr[1])
0x1008 30 ptr + 2 (arr[2])
0x100C 40 ptr + 3 (arr[3])
0x1010 50 ptr + 4 (arr[4])
0x1014 ?? ptr + 5 (⚠️ out of bounds!)

Basic Operations

int arr[] = {10, 20, 30, 40, 50};
int* ptr = arr;

// Addition
*(ptr + 0); // 10
*(ptr + 1); // 20
*(ptr + 2); // 30

// Subscript notation (syntactic sugar)
ptr[0]; // 10 (same as *(ptr + 0))
ptr[1]; // 20 (same as *(ptr + 1))
ptr[2]; // 30 (same as *(ptr + 2))

// Increment/Decrement
ptr++; // Move to next element
*ptr; // 20
ptr += 2; // Move forward 2 elements
*ptr; // 40
No Bounds Checking
int arr[5] = {10, 20, 30, 40, 50};
int* ptr = arr;

ptr[5]; // ❌ Undefined behavior
ptr[100]; // ❌ Undefined behavior
*(ptr - 1); // ❌ Before array start

Compiler won't stop you - your responsibility!

Pointer Subtraction

int arr[] = {10, 20, 30, 40, 50};
int* start = arr;
int* end = arr + 5;

// Subtraction gives element count (not bytes)
ptrdiff_t count = end - start; // 5 elements

int* p1 = arr + 2; // Points to arr[2]
int* p2 = arr; // Points to arr[0]
ptrdiff_t diff = p1 - p2; // 2 elements
Only Same Array

Subtraction only valid for pointers into same array!

int arr1[5], arr2[5];
int* p1 = arr1;
int* p2 = arr2;
p1 - p2; // ❌ Undefined behavior

Pointer Comparison

int arr[] = {10, 20, 30, 40, 50};
int* p1 = arr;
int* p2 = arr + 2;
int* end = arr + 5;

if (p1 < p2) { // ✅ True: p1 comes before p2
std::cout << "Earlier in memory\n";
}

if (p2 < end) { // ✅ True: p2 before end
std::cout << "Within bounds\n";
}

// One-past-end is valid for comparison
if (p1 != end) { // ✅ Valid comparison
// Don't dereference end!
}

Relational comparison (<, >, <=, >=) only makes sense for pointers into the same array or object.

Array Iteration

int arr[] = {10, 20, 30, 40, 50};
int* end = arr + 5; // One past last element

// Forward iteration
for (int* ptr = arr; ptr != end; ++ptr) {
std::cout << *ptr << " "; // 10 20 30 40 50
}

// Backward iteration
for (int* ptr = end - 1; ptr >= arr; --ptr) {
std::cout << *ptr << " "; // 50 40 30 20 10
}
Modern Alternative
// ✅ Prefer range-based for
for (int value : arr) {
std::cout << value << " ";
}

// Or iterators
for (auto it = vec.begin(); it != vec.end(); ++it) {
std::cout << *it << " ";
}

Multi-Dimensional Arrays

int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};

int* ptr = &matrix[0][0];

// Row-major storage (rows are contiguous)
*(ptr + 0); // 1 (matrix[0][0])
*(ptr + 4); // 5 (matrix[1][0])
*(ptr + 8); // 9 (matrix[2][0])

// Calculate position: row * cols + col
int row = 1, col = 2;
*(ptr + row * 4 + col); // 7 (matrix[1][2])

Common Dangers

Buffer Overflow

char buffer[10];
char* ptr = buffer;

// ❌ Writes past buffer
for (int i = 0; i < 20; ++i) {
*(ptr + i) = 'X'; // Undefined after i >= 10
}

// ✅ Safe version
for (int i = 0; i < 10; ++i) {
*(ptr + i) = 'X';
}

One-Past-End Dereference

int arr[5] = {10, 20, 30, 40, 50};
int* end = arr + 5;

// ✅ Valid for comparison
if (ptr != end) { }

// ❌ Invalid to dereference
*end; // Undefined behavior

Type Punning

int arr[] = {10, 20, 30};
int* iptr = arr;

// ❌ Treating as different type
char* cptr = reinterpret_cast<char*>(arr);
cptr + 1; // Moves 1 byte, not 1 int!

int x = *(int*)(cptr + 1); // ❌ Undefined behavior

Summary

Core mechanics
  • ptr + n moves by n * sizeof(type) bytes
  • ptr[i] is syntactic sugar for *(ptr + i)
  • Subtraction gives element count, not bytes
  • Comparison works for same-array pointers
Valid operations
  • Addition: ptr + n, ptr++, ptr += n
  • Subtraction: ptr - n, ptr--, ptr1 - ptr2
  • Comparison: <, >, <=, >=, ==, !=
  • Dereference: *ptr, ptr[i]
Dangers
  • No bounds checking (compiler won't stop you)
  • Buffer overflows (security vulnerabilities)
  • One-past-end dereference (valid for comparison only)
  • Type assumptions (arithmetic assumes correct type)
Modern practice
  • Prefer iterators and range-based for
  • Use std::vector/std::array (bounds checking)
  • Pointer arithmetic for C API interop only