Navigating Memory: A Complete Guide to Pointer Arithmetic in C

Pointer arithmetic is one of C's most powerful and distinctive features. Unlike regular arithmetic, adding to or subtracting from a pointer moves it by multiples of the pointed-to type's size, not by individual bytes. This allows for elegant array traversal, efficient memory manipulation, and direct access to data structures. Understanding pointer arithmetic is essential for systems programming, embedded development, and writing high-performance C code.

What Is Pointer Arithmetic?

Pointer arithmetic is the set of operations that can be performed on pointers. When you add an integer to a pointer, the pointer advances by that many elements of the type it points to, not by that many bytes.

type *ptr;  // Pointer to type
ptr + n;    // Advances by n * sizeof(type) bytes
ptr - n;    // Moves back by n * sizeof(type) bytes

Basic Pointer Arithmetic

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *ptr = arr;  // Points to first element (arr[0])
printf("arr[0] address: %p, value: %d\n", (void*)ptr, *ptr);
ptr++;  // Move to next integer (sizeof(int) bytes forward)
printf("After ptr++: %p, value: %d (arr[1])\n", (void*)ptr, *ptr);
ptr += 2;  // Move 2 integers forward
printf("After ptr += 2: %p, value: %d (arr[3])\n", (void*)ptr, *ptr);
ptr--;  // Move back one integer
printf("After ptr--: %p, value: %d (arr[2])\n", (void*)ptr, *ptr);
// Difference between pointers
int *start = arr;
int *end = &arr[4];
printf("Elements between start and end: %ld\n", end - start);
return 0;
}

Output (addresses will vary):

arr[0] address: 0x7ffc12345670, value: 10
After ptr++: 0x7ffc12345674, value: 20 (arr[1])
After ptr += 2: 0x7ffc1234567c, value: 40 (arr[3])
After ptr--: 0x7ffc12345678, value: 30 (arr[2])
Elements between start and end: 4

How Pointer Arithmetic Works

The magic of pointer arithmetic is that it automatically accounts for the size of the pointed-to type:

#include <stdio.h>
int main() {
char *cptr = (char*)1000;
int *iptr = (int*)1000;
double *dptr = (double*)1000;
printf("char pointer:  %p + 1 = %p (offset %ld bytes)\n",
(void*)cptr, (void*)(cptr + 1), (cptr + 1) - cptr);
printf("int pointer:   %p + 1 = %p (offset %ld bytes)\n",
(void*)iptr, (void*)(iptr + 1), (iptr + 1) - iptr);
printf("double pointer: %p + 1 = %p (offset %ld bytes)\n",
(void*)dptr, (void*)(dptr + 1), (dptr + 1) - dptr);
printf("\nPointer arithmetic with different increments:\n");
printf("iptr + 0 = %p\n", (void*)(iptr + 0));
printf("iptr + 1 = %p\n", (void*)(iptr + 1));
printf("iptr + 2 = %p\n", (void*)(iptr + 2));
printf("iptr + 5 = %p\n", (void*)(iptr + 5));
return 0;
}

Output:

char pointer:  0x3e8 + 1 = 0x3e9 (offset 1 bytes)
int pointer:   0x3e8 + 1 = 0x3ec (offset 4 bytes)
double pointer: 0x3e8 + 1 = 0x3f0 (offset 8 bytes)
Pointer arithmetic with different increments:
iptr + 0 = 0x3e8
iptr + 1 = 0x3ec
iptr + 2 = 0x3f0
iptr + 5 = 0x3fc

Pointer Arithmetic with Arrays

Arrays and pointers are closely related in C. In fact, array indexing is defined in terms of pointer arithmetic:

#include <stdio.h>
int main() {
int arr[5] = {2, 4, 6, 8, 10};
int *ptr = arr;
printf("Array access methods:\n");
for (int i = 0; i < 5; i++) {
// All these are equivalent:
printf("arr[%d] = %d\n", i, arr[i]);                 // Array indexing
printf("*(arr + %d) = %d\n", i, *(arr + i));        // Pointer arithmetic
printf("ptr[%d] = %d\n", i, ptr[i]);                 // Pointer as array
printf("*(ptr + %d) = %d\n\n", i, *(ptr + i));      // Pointer arithmetic
}
// Demonstrating the relationship
printf("arr == ptr: %d\n", arr == ptr);
printf("&arr[2] == arr + 2: %d\n", &arr[2] == arr + 2);
printf("arr[2] == *(arr + 2): %d\n", arr[2] == *(arr + 2));
return 0;
}

Pointer Subtraction

Subtracting two pointers of the same type gives the number of elements between them:

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50, 60, 70, 80};
int *start = &arr[2];  // Points to 30
int *end = &arr[6];    // Points to 70
printf("start points to: %d\n", *start);
printf("end points to: %d\n", *end);
printf("Elements between start and end: %ld\n", end - start);
printf("Bytes between: %ld\n", (char*)end - (char*)start);
// Iterating with pointer difference
printf("Elements from start to end: ");
for (int *p = start; p <= end; p++) {
printf("%d ", *p);
}
printf("\n");
// Finding array length using pointer arithmetic
int *first = arr;
int *last = &arr[7];
int length = last - first + 1;
printf("Array length: %d\n", length);
return 0;
}

Output:

start points to: 30
end points to: 70
Elements between start and end: 4
Bytes between: 16
Elements from start to end: 30 40 50 60 70 
Array length: 8

Pointer Comparison

Pointers can be compared using relational operators:

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *p1 = &arr[1];  // 20
int *p2 = &arr[3];  // 40
printf("p1 points to: %d\n", *p1);
printf("p2 points to: %d\n", *p2);
if (p1 < p2) {
printf("p1 is before p2 in memory\n");
}
if (p2 > p1) {
printf("p2 is after p1 in memory\n");
}
// Check if pointer is within array bounds
int *start = arr;
int *end = &arr[4];
if (p1 >= start && p1 <= end) {
printf("p1 points to an element within the array\n");
}
// Pointer comparison in loop
printf("\nTraversing with pointer comparison:\n");
for (int *p = arr; p <= &arr[4]; p++) {
printf("%d ", *p);
}
printf("\n");
return 0;
}

Pointer Arithmetic with Strings

Pointer arithmetic is particularly useful for string manipulation:

#include <stdio.h>
int stringLength(char *str) {
char *start = str;
while (*str != '\0') {
str++;
}
return str - start;
}
void reverseString(char *str) {
char *start = str;
char *end = str;
// Find end of string
while (*end != '\0') {
end++;
}
end--;  // Move back from null terminator
// Reverse using pointer arithmetic
while (start < end) {
char temp = *start;
*start = *end;
*end = temp;
start++;
end--;
}
}
int main() {
char text[] = "Hello, World!";
printf("Original: %s\n", text);
printf("Length: %d\n", stringLength(text));
reverseString(text);
printf("Reversed: %s\n", text);
// String traversal with pointer arithmetic
printf("\nCharacters: ");
for (char *p = text; *p != '\0'; p++) {
printf("%c ", *p);
}
printf("\n");
return 0;
}

Output:

Original: Hello, World!
Length: 13
Reversed: !dlroW ,olleH
Characters: ! d l r o W   , o l l e H

Pointer Arithmetic with Structures

#include <stdio.h>
typedef struct {
int id;
char name[20];
double salary;
} Employee;
int main() {
Employee employees[3] = {
{101, "Alice", 75000.50},
{102, "Bob", 82000.75},
{103, "Charlie", 68000.25}
};
Employee *ptr = employees;
printf("Employee array size: %lu bytes\n", sizeof(employees));
printf("Employee struct size: %lu bytes\n\n", sizeof(Employee));
// Accessing using pointer arithmetic
printf("First employee: ID=%d, Name=%s, Salary=%.2f\n",
ptr->id, ptr->name, ptr->salary);
ptr++;  // Move to next employee (sizeof(Employee) bytes)
printf("Second employee: ID=%d, Name=%s, Salary=%.2f\n",
ptr->id, ptr->name, ptr->salary);
ptr++;  // Move to next employee
printf("Third employee: ID=%d, Name=%s, Salary=%.2f\n",
ptr->id, ptr->name, ptr->salary);
// Iterate using pointer arithmetic
printf("\nAll employees:\n");
for (ptr = employees; ptr < employees + 3; ptr++) {
printf("  %d: %s (%.2f)\n", ptr->id, ptr->name, ptr->salary);
}
return 0;
}

Pointer Arithmetic with Multidimensional Arrays

#include <stdio.h>
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
// Pointer to first element of first row
int *p1 = &matrix[0][0];
// Pointer to first element of second row
int *p2 = &matrix[1][0];
printf("p1 (matrix[0][0]) = %d\n", *p1);
printf("p2 (matrix[1][0]) = %d\n", *p2);
printf("Elements between rows: %ld\n", p2 - p1);
printf("Bytes between rows: %ld\n", (char*)p2 - (char*)p1);
// Accessing entire array as flat memory
printf("\nFlat access:\n");
int *flat = &matrix[0][0];
for (int i = 0; i < 12; i++) {
printf("%4d ", flat[i]);
if ((i + 1) % 4 == 0) printf("\n");
}
// Pointer to array (row pointer)
int (*rowPtr)[4] = matrix;  // Points to first row
printf("\nRow pointer arithmetic:\n");
printf("First row: %d %d %d %d\n", (*rowPtr)[0], (*rowPtr)[1], 
(*rowPtr)[2], (*rowPtr)[3]);
rowPtr++;  // Move to next row
printf("Second row: %d %d %d %d\n", (*rowPtr)[0], (*rowPtr)[1], 
(*rowPtr)[2], (*rowPtr)[3]);
return 0;
}

Output:

p1 (matrix[0][0]) = 1
p2 (matrix[1][0]) = 5
Elements between rows: 4
Bytes between rows: 16
Flat access:
1    2    3    4 
5    6    7    8 
9   10   11   12 
Row pointer arithmetic:
First row: 1 2 3 4
Second row: 5 6 7 8

Advanced Pointer Arithmetic Examples

Example 1: Generic Array Functions

#include <stdio.h>
void* findMax(void *base, size_t num, size_t size, 
int (*compare)(void*, void*)) {
char *ptr = (char*)base;
char *max = ptr;
for (size_t i = 1; i < num; i++) {
char *current = ptr + i * size;
if (compare(current, max) > 0) {
max = current;
}
}
return max;
}
int compareInt(void *a, void *b) {
return *(int*)a - *(int*)b;
}
int compareDouble(void *a, void *b) {
double diff = *(double*)a - *(double*)b;
return diff > 0 ? 1 : (diff < 0 ? -1 : 0);
}
int main() {
int intArray[] = {45, 23, 89, 12, 67, 34, 90, 21};
double doubleArray[] = {3.14, 2.71, 1.618, 2.718, 0.577, 1.414};
int *maxInt = (int*)findMax(intArray, 8, sizeof(int), compareInt);
double *maxDouble = (double*)findMax(doubleArray, 6, sizeof(double), compareDouble);
printf("Max int: %d\n", *maxInt);
printf("Max double: %f\n", *maxDouble);
return 0;
}

Example 2: Circular Buffer

#include <stdio.h>
#include <stdlib.h>
typedef struct {
int *buffer;
int *head;
int *tail;
int *end;
int capacity;
int count;
} CircularBuffer;
CircularBuffer* createBuffer(int capacity) {
CircularBuffer *cb = malloc(sizeof(CircularBuffer));
cb->buffer = malloc(capacity * sizeof(int));
cb->head = cb->buffer;
cb->tail = cb->buffer;
cb->end = cb->buffer + capacity;
cb->capacity = capacity;
cb->count = 0;
return cb;
}
void enqueue(CircularBuffer *cb, int value) {
*cb->tail = value;
cb->tail++;
cb->count++;
// Wrap around if necessary
if (cb->tail == cb->end) {
cb->tail = cb->buffer;
}
}
int dequeue(CircularBuffer *cb) {
int value = *cb->head;
cb->head++;
cb->count--;
// Wrap around if necessary
if (cb->head == cb->end) {
cb->head = cb->buffer;
}
return value;
}
void printBuffer(CircularBuffer *cb) {
printf("Buffer: ");
int *ptr = cb->head;
for (int i = 0; i < cb->count; i++) {
printf("%d ", *ptr);
ptr++;
if (ptr == cb->end) {
ptr = cb->buffer;
}
}
printf("\n");
}
int main() {
CircularBuffer *cb = createBuffer(5);
enqueue(cb, 10);
enqueue(cb, 20);
enqueue(cb, 30);
enqueue(cb, 40);
enqueue(cb, 50);
printBuffer(cb);
printf("Dequeued: %d\n", dequeue(cb));
printf("Dequeued: %d\n", dequeue(cb));
printBuffer(cb);
enqueue(cb, 60);
enqueue(cb, 70);
printBuffer(cb);
return 0;
}

Example 3: Matrix Transpose with Pointers

#include <stdio.h>
#include <stdlib.h>
void transpose(int *src, int *dest, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
// src[i][j] -> dest[j][i]
*(dest + j * rows + i) = *(src + i * cols + j);
}
}
}
void printMatrix(int *matrix, int rows, int cols) {
for (int i = 0; i < rows; i++) {
for (int j = 0; j < cols; j++) {
printf("%4d ", *(matrix + i * cols + j));
}
printf("\n");
}
}
int main() {
int matrix[3][4] = {
{1, 2, 3, 4},
{5, 6, 7, 8},
{9, 10, 11, 12}
};
int transposed[4][3];
printf("Original matrix:\n");
printMatrix(&matrix[0][0], 3, 4);
transpose(&matrix[0][0], &transposed[0][0], 3, 4);
printf("\nTransposed matrix:\n");
printMatrix(&transposed[0][0], 4, 3);
return 0;
}

Example 4: Memory Pool Allocator

#include <stdio.h>
#include <stdlib.h>
#include <stdint.h>
typedef struct {
char *pool;
char *freePtr;
size_t totalSize;
size_t usedSize;
} MemoryPool;
MemoryPool* createPool(size_t size) {
MemoryPool *pool = malloc(sizeof(MemoryPool));
pool->pool = malloc(size);
pool->freePtr = pool->pool;
pool->totalSize = size;
pool->usedSize = 0;
return pool;
}
void* poolAlloc(MemoryPool *pool, size_t size) {
// Align to 8-byte boundary
size_t aligned = (size + 7) & ~7;
if (pool->usedSize + aligned > pool->totalSize) {
return NULL;  // Out of memory
}
void *ptr = pool->freePtr;
pool->freePtr += aligned;
pool->usedSize += aligned;
return ptr;
}
void poolReset(MemoryPool *pool) {
pool->freePtr = pool->pool;
pool->usedSize = 0;
}
void destroyPool(MemoryPool *pool) {
free(pool->pool);
free(pool);
}
int main() {
MemoryPool *pool = createPool(1024);
int *p1 = (int*)poolAlloc(pool, 5 * sizeof(int));
double *p2 = (double*)poolAlloc(pool, 3 * sizeof(double));
char *p3 = (char*)poolAlloc(pool, 20);
printf("Pool total: %zu bytes\n", pool->totalSize);
printf("Pool used: %zu bytes\n", pool->usedSize);
printf("p1 offset: %ld\n", (char*)p1 - pool->pool);
printf("p2 offset: %ld\n", (char*)p2 - pool->pool);
printf("p3 offset: %ld\n", p3 - pool->pool);
poolReset(pool);
printf("\nAfter reset - used: %zu bytes\n", pool->usedSize);
int *p4 = (int*)poolAlloc(pool, 100 * sizeof(int));
printf("p4 offset: %ld\n", (char*)p4 - pool->pool);
destroyPool(pool);
return 0;
}

Pointer Arithmetic Rules and Limitations

Allowed Operations:

  • Adding an integer to a pointer
  • Subtracting an integer from a pointer
  • Subtracting two pointers (same type)
  • Comparing two pointers (same type)

Not Allowed Operations:

  • Adding two pointers
  • Multiplying or dividing pointers
  • Bitwise operations on pointers
  • Pointer arithmetic on void* (GCC extension allows it)
#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
int *p1 = arr;
int *p2 = &arr[3];
// Valid operations
p1++;           // OK: increment
p1 += 2;        // OK: add integer
int diff = p2 - p1;  // OK: pointer difference
if (p1 < p2) {}      // OK: comparison
// Invalid operations (commented out)
// p1 + p2;      // ERROR: can't add pointers
// p1 * 2;       // ERROR: can't multiply pointer
// p1 & p2;      // ERROR: can't bitwise AND pointers
// void *vp; vp++;  // GNU extension, not standard
return 0;
}

Common Pitfalls

1. Going Out of Bounds

#include <stdio.h>
int main() {
int arr[5] = {10, 20, 30, 40, 50};
int *ptr = arr;
// Safe
for (int i = 0; i < 5; i++) {
printf("%d ", *ptr);
ptr++;
}
// DANGER: Going beyond array bounds
ptr = arr + 10;  // Points to invalid memory
*ptr = 999;      // Undefined behavior - may crash
// DANGER: Dereferencing before array
ptr = arr - 1;   // Points before array
*ptr = 888;      // Undefined behavior
return 0;
}

2. Assuming Size with void*

#include <stdio.h>
int main() {
int arr[5] = {1, 2, 3, 4, 5};
void *vp = arr;
// Can't do pointer arithmetic on void* in standard C
// vp++;  // ERROR in standard C
// Must cast first
int *ip = (int*)vp;
ip++;
printf("Second element: %d\n", *ip);
return 0;
}

3. Pointer Arithmetic with Different Types

#include <stdio.h>
int main() {
int arr[] = {10, 20, 30, 40, 50};
int *iptr = arr;
char *cptr = (char*)arr;
printf("int pointer + 1: %p -> %p (offset %d)\n",
(void*)iptr, (void*)(iptr + 1), (int)((char*)(iptr+1) - (char*)iptr));
printf("char pointer + 1: %p -> %p (offset %d)\n",
(void*)cptr, (void*)(cptr + 1), (int)((cptr+1) - cptr));
// These point to different elements!
iptr++;  // Points to next int (arr[1])
cptr++;  // Points to next byte (part of arr[0])
return 0;
}

Performance Considerations

Pointer arithmetic is often more efficient than array indexing:

#include <stdio.h>
#include <time.h>
#define SIZE 10000000
int main() {
static int arr[SIZE];
clock_t start, end;
// Initialize
for (int i = 0; i < SIZE; i++) {
arr[i] = i;
}
// Array indexing
start = clock();
int sum1 = 0;
for (int i = 0; i < SIZE; i++) {
sum1 += arr[i];
}
end = clock();
printf("Array indexing: %f seconds\n", 
(double)(end - start) / CLOCKS_PER_SEC);
// Pointer arithmetic
start = clock();
int sum2 = 0;
int *ptr = arr;
int *endPtr = arr + SIZE;
while (ptr < endPtr) {
sum2 += *ptr++;
}
end = clock();
printf("Pointer arithmetic: %f seconds\n", 
(double)(end - start) / CLOCKS_PER_SEC);
return 0;
}

Best Practices

  1. Stay within bounds - Always ensure pointers point to valid memory
  2. Use const when appropriate - Protect data that shouldn't be modified
  3. Initialize pointers - Never use uninitialized pointers
  4. Check for NULL - Before dereferencing pointers
  5. Use meaningful variable names - Makes pointer intent clear
  6. Prefer array syntax for simple access - More readable
  7. Use pointer arithmetic for iteration - Often more efficient
  8. Be careful with type casting - Can lead to alignment issues

Common Mistakes Checklist

  • [ ] Going beyond array bounds
  • [ ] Forgetting that ptr++ moves by sizeof(type) bytes
  • [ ] Using pointer arithmetic on void* (non-standard)
  • [ ] Subtracting pointers of different types
  • [ ] Comparing pointers from different arrays
  • [ ] Dereferencing past-the-end pointers
  • [ ] Assuming pointer size equals data size
  • [ ] Not accounting for structure padding

Conclusion

Pointer arithmetic is what makes C uniquely powerful for systems programming. It provides direct, efficient access to memory and enables elegant implementations of data structures and algorithms. Understanding pointer arithmetic allows you to:

  • Traverse arrays efficiently
  • Implement dynamic data structures
  • Manipulate memory directly
  • Write generic functions
  • Optimize performance-critical code

Key principles to remember:

  • Adding n to a pointer moves it by n * sizeof(type) bytes
  • Pointers to different types advance by different amounts
  • Array indexing arr[i] is syntactic sugar for *(arr + i)
  • Pointer subtraction gives the number of elements between them
  • Pointers can be compared to determine relative positions in memory

Mastering pointer arithmetic separates novice C programmers from experts. It opens up possibilities for writing systems-level code, embedded software, and high-performance applications that simply aren't possible in higher-level languages.

Leave a Reply

Your email address will not be published. Required fields are marked *


Macro Nepal Helper