Pointer Arithmetic

Introduction

C is a fairly simple language, but when it comes to pointer arithmetic, the standard committee went a bit overboard. There’s a lot you need to know to use it effectively, yet the standard itself says surprisingly little on the topic. So, without further ado, I want to introduce you to POINTER ARITHMETIC RULES:

  1. Subtracting pointers is allowed only within the same object or array. Two pointers from different objects cannot be subtracted.
  2. When adding or subtracting a pointer and a value, the value must be of type int.
  3. Pointer arithmetic is not allowed on void * types.
  4. You can take the address just past the end of an object (e.g., arr + n where arr is a pointer to an array and n is its size). However, dereferencing such a pointer is undefined behavior (UB).
  5. Any pointer can be cast to unsigned char * and treated as raw bytes.
  6. When casting between pointers of different types, ensure they have compatible alignment. For example, a 6-byte array cannot be properly aligned for an 8-byte type without copying the data.

Pointer and Integer: Adding and Subtracting

Let’s say we have an array arr:

int arr[] = {1, 2, 3};

We can traverse arr using pointer addition and subtraction:

int *a = arr + 1;
int *b = a - 1;
printf("%d\n", *arr);
printf("%d\n", *a);
printf("%d\n", *b); 

Pointer Differences: Subtracting Two Pointers

Pointer subtraction always produces a ptrdiff_t. This is especially useful because it represents the number of elements, not bytes. If it returned a byte count, we would have to manually divide by sizeof(type) every time.

For example, suppose we want to measure how many elements there are from the start of the array arr to elem, where elem is known to reside somewhere inside arr:

int *elem = arr + 2;
ptrdiff_t amount_of_elems = elem - arr;
printf("Numbers: %d\n", amount_of_elems);

We can do the same in terms of raw bytes:

int *elem = arr + 2;
ptrdiff_t amount_of_bytes = (unsigned char *)elem - (unsigned char *)arr;
printf("Bytes: %d\n", amount_of_bytes);

Why void * Can’t Be Used in Arithmetic

We cannot perform pointer arithmetic on void * types. Before adding or subtracting, we must cast them to an appropriate type.

The issue with void * is that it has no defined element size, so pointer increments cannot be computed meaningfully.

long longrr[] = {1, 2, 3};
printf("sizeof(longrr): %d\n", sizeof(longrr));
printf("%ld vs %ld\n", *(longrr + 1), *(long *)((void *)longrr + 1));

Which prints:

sizeof(longrr): 24
2 vs 144115188075855872

One-Past-the-End Rule

We can take the address of one element past the end of an array, but we cannot dereference it:

int *end = arr + 3;
while (b < end) {
    printf("%d\n", *b);
    b++;
}

Byte-Wise Arithmetic with unsigned char *

Sometimes you need to treat objects as raw bytes. For example, when comparing a UTF-16 string with a regular C string, it can be useful to count bytes instead of elements:

typedef uint16_t utf16_t;
utf16_t wide_str[] = u"Hello";
char str[] = "Hello";

printf("sizeof(wide_str): %d\n", sizeof(wide_str));
printf("sizeof(str): %d\n", sizeof(str));

printf("wide_str: %s\n", wide_str); // Prints only 'H' because 0000 is seen as a null terminator
printf("str: %s\n", str);

Mixing Pointer Types Safely

When mixing pointer types, you must ensure the alignment of the object is compatible:

// 3-byte array
char a[] = "ab";
uint16_t *ap = (uint16_t *)a;

printf("First element: %d\n", ap + 1); // Memory error: ap + 1 points beyond `a`'s bounds

Undefined Behavior: The Silent Danger

Be especially careful with pointer arithmetic. For example, promoting an int * to a float * is undefined behavior in C. The only safe promotion is to unsigned char * to access raw bytes:

int a[1] = {42, 33};
float *pf = (float *)a;
printf("%f\n", *pf); // On Linux, this prints `0`

Conclusions

Pointer arithmetic is tricky, but if you get it right, it can make your code much more powerful and flexible.

Published:

Category: C

Comments

Leave a comment