C Programming: Low-Level Mastery
Pointers

Pointer Arithmetic

Master pointer arithmetic - adding, subtracting, comparing pointers. Learn how C automatically scales by element size, traversing arrays efficiently, and the relationship between pointers and array indexing.

Understanding Pointer Arithmetic

Pointer arithmetic allows adding and subtracting integers from pointers to navigate through memory. The key insight: C automatically scales arithmetic by the pointed-to type's size. Adding 1 to an int pointer moves it forward by sizeof(int) bytes (typically 4), not 1 byte. This makes traversing arrays natural and efficient.

C
#include <stdio.h>

int main(void) {
    int arr[5] = {10, 20, 30, 40, 50};
    int *ptr = arr;  // Points to first element
    
    printf("ptr points to: %d\n", *ptr);  // 10
    
    /* Add 1: Moves to next element */
    ptr = ptr + 1;  // Same as: ptr++
    printf("ptr points to: %d\n", *ptr);  // 20
    
    /* Add 2 more: Moves 2 elements forward */
    ptr = ptr + 2;
    printf("ptr points to: %d\n", *ptr);  // 40
    
    /* Subtract 1: Moves back one element */
    ptr = ptr - 1;
    printf("ptr points to: %d\n", *ptr);  // 30
    
    /* Address arithmetic (showing byte offsets) */
    int *p = arr;
    printf("Address of arr[0]: %p\n", (void*)p);
    printf("Address of arr[1]: %p\n", (void*)(p + 1));
    printf("Address of arr[2]: %p\n", (void*)(p + 2));
    // Each address differs by sizeof(int) bytes (typically 4)
    
    return 0;
}

/* Key principle: ptr + n moves by n * sizeof(*ptr) bytes */

Pointer Arithmetic Operations

Pointers support limited arithmetic: addition/subtraction of integers, subtraction of pointers (giving element count), and comparison. These operations are designed specifically for navigating arrays and checking relationships between memory locations.

C
/* Valid pointer arithmetic operations */

int arr[10];
int *ptr = arr;

/* 1. Add integer to pointer */
int *p1 = ptr + 5;  // Points 5 elements ahead
int *p2 = 3 + ptr;  // Same (commutative)

/* 2. Subtract integer from pointer */
int *p3 = ptr - 2;  // Points 2 elements back (if valid)

/* 3. Increment/decrement */
ptr++;     // Move to next element
ptr--;     // Move to previous element
ptr += 3;  // Move 3 elements forward
ptr -= 2;  // Move 2 elements back

/* 4. Subtract two pointers (gives element count) */
int arr2[10];
int *start = arr2;
int *end = arr2 + 10;
ptrdiff_t distance = end - start;  // 10 elements
printf("Distance: %td elements\n", distance);

/* 5. Compare pointers */
int *p4 = arr2;
int *p5 = arr2 + 5;

if (p5 &gt; p4) {
    printf("p5 is ahead of p4\n");
}

if (p4 == arr2) {
    printf("p4 points to start of array\n");
}

/* INVALID operations */
// int *bad1 = ptr + ptr;  // Can't add pointers
// int *bad2 = ptr * 2;    // Can't multiply pointer
// int *bad3 = ptr / 2;    // Can't divide pointer

/* Different types - scaling differs */
char carr[10];
int iarr[10];
double darr[10];

char *cp = carr;
int *ip = iarr;
double *dp = darr;

printf("char ptr: %p -&gt; %p (+1)\n", (void*)cp, (void*)(cp + 1));   // +1 byte
printf("int ptr: %p -&gt; %p (+1)\n", (void*)ip, (void*)(ip + 1));    // +4 bytes
printf("double ptr: %p -&gt; %p (+1)\n", (void*)dp, (void*)(dp + 1)); // +8 bytes

Traversing Arrays with Pointers

Pointer arithmetic provides an alternative to array indexing for traversing arrays. Both approaches are equivalent - arr[i] is converted to *(arr + i) by the compiler. Pointer traversal can be more efficient and is common in performance-critical code.

C
/* Method 1: Array indexing */
void print_array_indexed(int arr[], int size) {
    for (int i = 0; i < size; i++) {
        printf("%d ", arr[i]);
    }
    printf("\n");
}

/* Method 2: Pointer arithmetic with indexing */
void print_array_pointer_indexed(int arr[], int size) {
    int *ptr = arr;
    for (int i = 0; i < size; i++) {
        printf("%d ", *(ptr + i));
    }
    printf("\n");
}

/* Method 3: Pointer incrementation */
void print_array_pointer_increment(int arr[], int size) {
    int *ptr = arr;
    int *end = arr + size;
    
    while (ptr < end) {
        printf("%d ", *ptr);
        ptr++;
    }
    printf("\n");
}

/* Method 4: Pointer with explicit end check */
void print_array_explicit_end(int arr[], int size) {
    for (int *p = arr; p < arr + size; p++) {
        printf("%d ", *p);
    }
    printf("\n");
}

/* Comparison: All are equivalent */
int arr[] = {1, 2, 3, 4, 5};
int size = 5;

print_array_indexed(arr, size);
print_array_pointer_indexed(arr, size);
print_array_pointer_increment(arr, size);
print_array_explicit_end(arr, size);

/* Reverse traversal */
void print_reverse(int arr[], int size) {
    int *ptr = arr + size - 1;  // Point to last element
    
    while (ptr >= arr) {
        printf("%d ", *ptr);
        ptr--;
    }
    printf("\n");
}

/* Process every other element */
void print_every_other(int arr[], int size) {
    for (int *p = arr; p < arr + size; p += 2) {
        printf("%d ", *p);
    }
    printf("\n");
}

Pointer Comparison

Pointers can be compared using relational operators (==, !=, <, >, <=, >=). Comparisons are only meaningful for pointers to elements of the same array (or one past the last element). Comparing pointers to different objects produces undefined behavior.

C
/* Valid pointer comparisons */
int arr[10];
int *start = arr;
int *middle = arr + 5;
int *end = arr + 10;  // One past last (valid for comparison)

/* Equality */
if (start == arr) {
    printf("start points to beginning\n");
}

/* Inequality */
if (middle != start) {
    printf("middle and start are different\n");
}

/* Less than / greater than */
if (middle &gt; start) {
    printf("middle is ahead of start\n");
}

if (end &gt; middle) {
    printf("end is ahead of middle\n");
}

/* Common pattern: Checking if pointer in range */
int *ptr = arr + 3;
if (ptr >= arr && ptr < arr + 10) {
    printf("ptr is within array bounds\n");
}

/* Loop until end */
for (int *p = arr; p < end; p++) {
    *p = 0;  // Zero all elements
}

/* Find element */
int target = 42;
int *found = NULL;

for (int *p = arr; p < arr + 10; p++) {
    if (*p == target) {
        found = p;
        break;
    }
}

if (found != NULL) {
    printf("Found at index: %ld\n", found - arr);
}

/* INVALID: Comparing pointers to different objects */
int x, y;
int *px = &x;
int *py = &y;

// if (px < py) {"{ }"}  // UNDEFINED BEHAVIOR
// Pointers must be to same object/array

/* One-past-end pointer (valid for comparison only) */
int arr2[5];
int *past_end = arr2 + 5;  // Valid to create
// *past_end = 0;  // INVALID: Can't dereference
if (arr2 < past_end) {"{ }"}  // Valid: Can compare

Multi-Dimensional Arrays and Pointer Arithmetic

Pointer arithmetic with multidimensional arrays requires understanding memory layout. C stores multidimensional arrays in row-major order - all elements of a row are contiguous, then the next row. This affects how pointer arithmetic navigates the array.

C
/* 2D array layout in memory */
int matrix[3][4] = {
    {1,  2,  3,  4},
    {5,  6,  7,  8},
    {9, 10, 11, 12}
};

/* Memory: [1][2][3][4][5][6][7][8][9][10][11][12] */

/* Pointer to first element */
int *ptr = &matrix[0][0];

/* Traverse as 1D array */
for (int i = 0; i < 12; i++) {
    printf("%d ", *(ptr + i));
}
printf("\n");

/* Calculate 2D index from 1D pointer */
int rows = 3, cols = 4;
for (int i = 0; i < rows * cols; i++) {
    int row = i / cols;
    int col = i % cols;
    printf("matrix[%d][%d] = %d\n", row, col, *(ptr + i));
}

/* Pointer to row (array of 4 ints) */
int (*row_ptr)[4] = matrix;  // Pointer to array of 4 ints

/* Access elements through row pointer */
printf("%d\n", row_ptr[0][0]);  // 1
printf("%d\n", row_ptr[1][2]);  // 7
printf("%d\n", (*(row_ptr + 1))[2]);  // 7 (pointer notation)

/* Navigate by rows */
for (int i = 0; i < 3; i++) {
    printf("Row %d: ", i);
    for (int j = 0; j < 4; j++) {
        printf("%d ", (*(row_ptr + i))[j]);
    }
    printf("\n");
}

/* Flatten 2D to 1D pointer arithmetic */
int *flat = &matrix[0][0];
int value = matrix[1][2];  // Row 1, column 2

/* Calculate offset: row * num_cols + col */
int offset = 1 * 4 + 2;  // = 6
printf("Value: %d\n", *(flat + offset));  // 7 (same as matrix[1][2])

Practical Applications

Pointer arithmetic is essential for efficient C programming. Understanding these patterns helps you write faster, more idiomatic C code.

C
/* String length using pointer arithmetic */
size_t my_strlen(const char *str) {
    const char *start = str;
    while (*str != '\0') {
        str++;
    }
    return str - start;  // Pointer subtraction gives length
}

/* String copy */
void my_strcpy(char *dest, const char *src) {
    while (*src != '\0') {
        *dest++ = *src++;  // Copy and increment both
    }
    *dest = '\0';  // Null terminate
}

/* Alternative (more compact) */
void my_strcpy_compact(char *dest, const char *src) {
    while ((*dest++ = *src++));  // Copy until null
}

/* Array reversal in-place */
void reverse_array(int arr[], int size) {
    int *left = arr;
    int *right = arr + size - 1;
    
    while (left < right) {
        /* Swap */
        int temp = *left;
        *left = *right;
        *right = temp;
        
        left++;
        right--;
    }
}

/* Find in array */
int* find_element(int *arr, int size, int value) {
    int *end = arr + size;
    
    for (int *p = arr; p < end; p++) {
        if (*p == value) {
            return p;
        }
    }
    
    return NULL;  // Not found
}

/* Remove duplicates from sorted array */
int remove_duplicates(int *arr, int size) {
    if (size == 0) return 0;
    
    int *write = arr + 1;
    int *read = arr + 1;
    int *end = arr + size;
    
    while (read < end) {
        if (*read != *(read - 1)) {
            *write = *read;
            write++;
        }
        read++;
    }
    
    return write - arr;  // New size
}

/* Buffer operations */
void fill_buffer(int *buffer, int size, int value) {
    int *end = buffer + size;
    for (int *p = buffer; p < end; p++) {
        *p = value;
    }
}

/* Pointer-based binary search */
int* binary_search(int *arr, int size, int target) {
    int *left = arr;
    int *right = arr + size - 1;
    
    while (left <= right) {
        int *mid = left + (right - left) / 2;
        
        if (*mid == target) {
            return mid;
        }
        
        if (*mid < target) {
            left = mid + 1;
        } else {
            right = mid - 1;
        }
    }
    
    return NULL;
}

Common Pitfalls

Pointer arithmetic errors are dangerous and often cause crashes or security vulnerabilities. Understanding these pitfalls helps you write safer code.

C
/* Pitfall 1: Out of bounds arithmetic */
int arr[5];
int *ptr = arr + 10;  // Points outside array!
// *ptr = 42;  // UNDEFINED BEHAVIOR

/* Pitfall 2: Arithmetic on void pointer */
void *vptr;
// vptr++;  // ERROR: Can't do arithmetic on void*
// Must cast first:
// int *iptr = (int*)vptr;
// iptr++;

/* Pitfall 3: Comparing pointers to different arrays */
int arr1[10];
int arr2[10];
int *p1 = arr1;
int *p2 = arr2;
// if (p1 < p2) {"{ }"}  // UNDEFINED BEHAVIOR

/* Pitfall 4: Integer overflow in pointer arithmetic */
int *ptr = arr;
size_t huge = SIZE_MAX;
// ptr = ptr + huge;  // Overflow!

/* Pitfall 5: Forgetting pointer scales by type size */
// Incorrect mental model:
int arr[5];
int *ptr = arr;
// Adding 1 moves by 1 byte? NO!
// Adding 1 moves by sizeof(int) bytes

/* Pitfall 6: One-past-end dereference */
int arr[5];
int *end = arr + 5;  // Valid to create
// printf("%d\n", *end);  // INVALID: Can't dereference

/* Pitfall 7: Subtracting pointers of different types */
int iarr[10];
double darr[10];
int *ip = iarr;
double *dp = darr;
// ptrdiff_t diff = ip - dp;  // ERROR: Different types

/* Best practices */

// 1. Always check bounds
int *ptr = arr;
if (ptr >= arr && ptr < arr + size) {
    *ptr = value;  // Safe
}

// 2. Use size_t for offsets
size_t offset = 5;
int *p = arr + offset;

// 3. Store array end pointer
int *start = arr;
int *end = arr + size;
for (int *p = start; p < end; p++) {
    // Process
}

// 4. Be consistent with pointer vs array notation
// Choose one style and stick to it
for (int i = 0; i < size; i++) {
    arr[i] = 0;  // Array style
}
// OR
for (int *p = arr; p < arr + size; p++) {
    *p = 0;  // Pointer style
}

Summary & What's Next

Key Takeaways:

  • ✅ ptr + n moves n * sizeof(*ptr) bytes
  • ✅ Pointer arithmetic scales automatically by type size
  • ✅ Can add/subtract integers, subtract pointers, compare
  • ✅ ptr[i] equivalent to *(ptr + i)
  • ✅ Efficient for array traversal
  • ✅ Subtracting pointers gives element count
  • ✅ Only compare pointers to same array
  • ✅ One-past-end valid for comparison, not dereferencing

What's Next?

Let's learn about pointers to pointers and multi-level indirection!