1 of 4
Tell an address from the value at it, and know which addresses you are allowed to read through.
Every one of these appears in ordinary C. Sort each by what it evaluates to, and put the two that evaluate to nothing defensible in the last bucket.
Throughout, arr is an int array of length 4, p and q are int pointers into it, len is 4, and null_ptr is NULL.
- An address
- Something you could store in a pointer variable.
- A value in memory
- Something you could store in an int.
- Undefined behaviour
- The compiler is entitled to assume this never happens, which is worse than a crash.
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
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.
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
Implement your solution in the editor on the right or work in your local IDE. All unit tests run against your code in real-time.
Exercise
pointer-arithmetic
19 tests, graded on your machine in about a second. Stuck for 25 minutes? The desktop app will offer you the reference solution.