Pointer Arithmetic and Data Type Scaling in C

Pointer arithmetic in C allows developers to navigate and manipulate memory addresses using standard mathematical operators. Rather than shifting raw byte addresses directly, pointer arithmetic depends entirely on data type scaling, meaning the compiler automatically adjusts the address offset based on the byte size of the referenced type. This guide covers how pointer arithmetic works, the mathematics behind type scaling, and the fundamental rules governing safe memory navigation in C.

What Defines Pointer Arithmetic?

Pointer arithmetic refers to performing mathematical operations on pointer variables. Unlike standard integer math, operations on pointers calculate memory addresses relative to elements in an array or continuous block of memory.

In C, valid pointer arithmetic operations include:

  • Adding an integer to a pointer (ptr + n): Moves the pointer forward to point to an element \(n\) positions ahead.
  • Subtracting an integer from a pointer (ptr - n): Moves the pointer backward to point to an element \(n\) positions behind.
  • Subtracting two pointers (ptr2 - ptr1): Calculates the number of elements between two pointers of the same type.
  • Incrementing and decrementing (ptr++, --ptr): Advances or retracts the pointer by one element.
  • Comparison operators (<, >, ==, !=): Compares relative positions in memory, provided both pointers point to elements within the same memory block.

Operations such as adding two pointers together, multiplying pointers, or dividing a pointer are strictly prohibited by the C standard.

How Data Type Scaling Works

When an integer \(n\) is added to or subtracted from a pointer, the compiler does not advance the address by \(n\) bytes. Instead, it advances the address by \(n \times \text{sizeof}(T)\), where \(T\) is the data type of the object being pointed to. This automated calculation is known as data type scaling.

The formula for calculating the target address is:

\[\text{New Address} = \text{Current Address} \pm (n \times \text{sizeof}(*ptr))\]

Scaling Examples Across Different Types

Consider an environment where char is 1 byte, int is 4 bytes, and double is 8 bytes. If three pointers all initially reference memory address 0x1000:

  • char *c_ptr = 0x1000;
    c_ptr + 1 evaluates to 0x1000 + (1 * 1) = 0x1001
  • int *i_ptr = 0x1000;
    i_ptr + 1 evaluates to 0x1000 + (1 * 4) = 0x1004
  • double *d_ptr = 0x1000;
    d_ptr + 1 evaluates to 0x1000 + (1 * 8) = 0x1008

Because the compiler knows the size of the underlying type, incrementing a pointer consistently moves it to the next valid element of that type rather than an arbitrary byte.

Pointer Subtraction and ptrdiff_t

When subtracting one pointer from another (ptr2 - ptr1), data type scaling operates in reverse. The compiler calculates the difference in raw byte addresses and divides that number by the size of the base type:

\[\text{Elements Between} = \frac{\text{Address}(ptr2) - \text{Address}(ptr1)}{\text{sizeof}(*ptr1)}\]

For example, if an integer pointer ptr1 is at 0x2000 and ptr2 is at 0x2010, the byte difference is 16 bytes. Assuming an integer size of 4 bytes, ptr2 - ptr1 returns 4, representing the number of int elements between the two addresses. The result of this operation has the standard signed integer type ptrdiff_t (defined in <stddef.h>).

Structs and User-Defined Types

Scaling applies identically to complex data types, including structs and arrays of structs. If a struct occupies 64 bytes in memory:

struct Record {
    char data[60];
    int id;
}; // sizeof(struct Record) == 64

struct Record list[5];
struct Record *r_ptr = list; // Points to list[0]
r_ptr++; // Points to list[1], address increases by 64 bytes

The pointer increases by the total byte footprint of the struct, including any internal padding applied by the compiler for hardware alignment.

Restrictions and Undefined Behavior

  1. Void Pointers (void *): Standard C does not support pointer arithmetic on void * because the size of an incomplete type is undefined. While some compilers (such as GCC) treat sizeof(void) as 1 as an extension, portable C code must cast a void * to a concrete type like char * or uint8_t * before performing arithmetic.
  2. Array Boundaries: Pointer arithmetic is strictly defined only within the boundaries of an allocated array (or a single object treated as an array of length 1), plus an imaginary element one position past the end of the array. Dereferencing that "one-past-the-end" address or moving past it invokes undefined behavior.
  3. Cross-Object Arithmetic: Comparing or subtracting pointers pointing to entirely separate, unrelated objects in memory produces undefined behavior according to the C standard.