Skip to content
Atlas

Memory, bits, and pointers in C

Memory addresses and pointer arithmetic

The linear address space, pointer dereferencing, pointer arithmetic across typed strides, null pointers, and buffer boundaries.

25 minute read


⚡
For the best experience: Download the native Atlas Desktop app for instant test running, native IDE sync, and 100% offline practice.
Download Desktop App→

Von Neumann computer architectures structure main memory as a uniform, contiguous sequence of bytes. Every byte cell has a unique numeric address.

A pointer is a variable that stores a memory address rather than a raw data value.

Linear byte-addressable memory and pointer types

In a 64-bit architecture, the virtual address space spans theoretical addresses from $0x0$ to $2^{64}-1$. Each individual address references exactly 8 bits (one byte) of physical or virtual storage.

A pointer type in C combines two concepts: the raw numeric memory address, and the data type of the value stored at that address.

int x = 42;
int *p = &x;     // p stores the address of x
double *d_ptr;   // d_ptr stores an address where a double is located
void *raw_ptr;   // raw_ptr stores an un-typed memory address

On a 64-bit machine, every pointer occupies 8 bytes (64 bits) of storage, regardless of whether it points to a 1-byte char, a 4-byte int, or an 8-byte double.

Pointer dereferencing and address operations

The address-of operator & extracts the memory location of a variable. The dereference operator * accesses or modifies the value at the address held by a pointer:

#include <stdio.h>

int main(void) {
    int value = 100;
    int *ptr = &value;

    // Read through pointer (rvalue context)
    int read_val = *ptr;

    // Write through pointer (lvalue context)
    *ptr = 250;

    printf("value is now: %d\n", value); // prints 250
    return 0;
}

A pointer initialized to NULL (typically address 0x0) represents an invalid, unassigned reference. Attempting to dereference a null pointer triggers a CPU memory fault (a segmentation fault on POSIX systems, SIGSEGV), terminating the process:

int *bad_ptr = NULL;
*bad_ptr = 10; // CPU fault: Page fault at address 0x0

Pointer arithmetic and typed strides

Arithmetic on pointers is automatically scaled by the size of the referenced type (sizeof(*ptr)).

When adding an integer $n$ to a pointer ptr of type T*, the hardware address advances by $n \times \text{sizeof}(T)$ bytes:

$$\text{address}(\text{ptr} + n) = \text{address}(\text{ptr}) + n \cdot \text{sizeof}(T)$$

Consider an array of 32-bit integers (sizeof(int) == 4):

int numbers[4] = {10, 20, 30, 40};
int *p = numbers; // points to numbers[0] at address 0x1000

int *p_next = p + 1; // points to numbers[1] at address 0x1004
int *p_third = p + 2; // points to numbers[2] at address 0x1008

Subtracting two pointers of the same type (p2 - p1) computes the number of elements separating them, rather than the raw byte distance:

ptrdiff_t count = p_third - p; // evaluates to 2, not 8

Relational comparisons (<, <=, >, >=) between pointers are only well-defined in C when both pointers target elements of the same array or struct object (or one element past its end).

Array decay and string buffers

In C, the name of an array expression implicitly converts (decays) into a pointer to its first element in most expression contexts:

int arr[5] = {1, 2, 3, 4, 5};
int *ptr = arr; // Equivalent to: int *ptr = &arr[0];

Array indexing arr[i] is syntactic sugar for *(arr + i):

arr[2] = 99;
*(arr + 2) = 99; // Identical assembly instruction

Strings in C are arrays of characters terminated by a null byte (�, numeric value 0x00). Standard string functions traverse memory sequentially until reaching this sentinel:

size_t custom_strlen(const char *s) {
    const char *p = s;
    while (*p != '\0') {
        p++;
    }
    return (size_t)(p - s);
}

Buffer overruns and boundary violations

C does not perform runtime array boundary validation. If an index or pointer arithmetic advances past the allocated buffer bounds, operations read or write adjacent memory:

void dangerous_copy(void) {
    char source[8] = "1234567";
    char dest[4];
    
    // Writes 8 bytes into a 4-byte destination buffer
    for (int i = 0; i < 8; i++) {
        dest[i] = source[i];
    }
}

A buffer overrun overwrites whatever memory follows the target buffer:

  • Stack variables located adjacent to the buffer
  • Saved frame pointers and return addresses
  • Dynamic heap metadata structures

Boundary violations lead to memory corruption, security vulnerabilities, or immediate hardware segmentation faults when accessing unmapped virtual pages.