Following the arrow: the * operator
In guide 1 you built the core picture: a pointer is just a variable whose value happens to be an address — the house number of some other variable on memory's long numbered street. We took an address with the address-of operator, writing `p = &x` to mean "let p hold the address where x lives". That is half of the story. A house number you can only write down but never visit is useless; the whole point of holding an address is to be able to go there. That second half — actually following the arrow to reach the value it points at — is called dereferencing, and it is what makes pointers earn their keep.
The operator that dereferences is a single asterisk written in front of a pointer: `*p`. Read `*p` out loud as "the value at the address held in p", or more snappily "the thing p points to". If `p` holds `&x`, then `*p` is x — not a copy, the very same storage. This is why dereferencing works in both directions. On the right of an assignment, `int y = *p;` reads x's value through the pointer. On the left, `*p = 42;` writes 42 into x itself, exactly as if you had typed `x = 42;`. The pointer is a remote control for a box you may not even know the name of.
The pointer that points nowhere
Sometimes a pointer should mean "I am not pointing at anything right now". You cannot leave it uninitialised — an uninitialised pointer holds whatever leftover bits were in that memory, a so-called wild pointer aimed at a random house, and dereferencing it is a catastrophe waiting to happen. So C reserves one special value for "deliberately nowhere": the null pointer, written `NULL` (or just the constant `0` in a pointer context). A null pointer is guaranteed never to equal the address of any real object, so it is the universal flag for "this points at nothing".
The iron rule is short: you may store, copy and compare a null pointer all you like, but you must never dereference one. Writing `*p` when `p` is `NULL` asks the machine to read or write at address 0x0, which the operating system has deliberately left unmapped so that exactly this mistake gets caught. The result is the famous segmentation fault — your program is killed on the spot. That sounds harsh, but a crash here is a gift: the alternative, silently reading garbage from address zero, would corrupt your data and surface as a baffling bug far away.
This is exactly why the libc allocators hand back a pointer you are expected to check. When malloc() fails it returns `NULL` rather than a usable block, and a program that charges ahead and dereferences that `NULL` segfaults instead of reporting the real problem (out of memory). The honest pattern is to test before you trust, every single time — and the same three-step discipline applies to open(), fork(), and every call that can fail, because learners copy what they see:
- Call the allocator and keep its result, e.g. `int *p = malloc(n * sizeof(int));` — note we size by sizeof(int), never a guessed byte count.
- Immediately test for failure: if `p == NULL`, report it (perror("malloc")) and bail out — return an error, do NOT touch p.
- Only past that check is it safe to dereference: now `p[0] = 42;` writes into real, owned memory because p is known non-null.
Pointer arithmetic counts in elements, not bytes
Here is where pointers get genuinely surprising. You can add an integer to a pointer, and the result is another pointer — this is pointer arithmetic. But `p + 1` does not mean "the next byte". It means "the next element of whatever type p points to". If `p` is an `int *` and one `int` is 4 bytes wide, then `p + 1` is the address `&(*p) + 4` — the compiler silently scales the `1` by `sizeof(int)`. Add 1 to a `char *` and you move 1 byte; add 1 to a `double *` (8 bytes) and you leap 8 bytes. The pointer's type is what sets the stride.
Once you see that, the rest follows like clockwork. Subtracting two pointers into the same block gives the number of elements between them, not bytes: if `q = p + 3`, then `q - p` is 3, whatever the element size. You can compare pointers with `<` and `==` to ask which comes first or whether they coincide. And incrementing — `p++` — simply walks to the next element, which is precisely how you march through a row of values one at a time. The arithmetic is element-aware so that you almost never have to multiply by `sizeof` yourself.
int a[4] = {10, 20, 30, 40};
int *p = &a[0]; /* p -> the first int */
*p == 10 /* the value at p */
*(p + 1) == 20 /* one element on: address moved 4 bytes */
*(p + 3) == 40 /* three elements on: 12 bytes from start */
(p + 3) - p == 3 /* pointer difference is in ELEMENTS, not bytes */Staying inside the lines
Pointer arithmetic is powerful precisely because it trusts you, and that trust is also its trap. C only defines the arithmetic when you stay within a single array (or one slot just past its end, a position you may form but must never dereference). Compute a pointer that lands two houses before the array starts, or far past its end, and you have stepped into undefined behaviour — not merely a wrong number, but a program the compiler is now allowed to treat as if the bad case can never happen. Do not let the slogan fool you: this is not "platform-dependent"; the optimizer may assume your out-of-range pointer is impossible and quietly delete the very check that would have saved you.
A second, quieter constraint is alignment. Most CPUs expect a 4-byte `int` to sit at an address that is a multiple of 4, an 8-byte `double` at a multiple of 8, and so on. The compiler arranges your variables to satisfy this automatically, which is why honestly typed pointer arithmetic stays aligned. The danger appears only when you do something exotic — casting a `char *` to an `int *` at an arbitrary offset, say — where you can manufacture a misaligned address. On some machines that merely runs slowly; on others it faults outright. Respect the type and the alignment takes care of itself.
Where this leaves you
Tally up what you can now do. You can follow a pointer to its value and write back through it with a single `*`; you can mark "nothing here" with `NULL` and you know — viscerally — never to dereference it; and you can step a pointer through memory one typed element at a time, with the compiler scaling the stride for you. Crucially, you also met the two ways this goes wrong honestly: the null dereference that segfaults, and the out-of-bounds pointer that wanders into undefined behaviour the optimizer is free to exploit.
Notice that the worked example above leaned on an array, and that `&a[0]` and walking with `p + 1` felt almost interchangeable with indexing `a[1]`. That was no accident — and it is also not the whole truth. The relationship between an array and a pointer in C is one of the most quietly confusing corners of the language, full of cases where they behave identically and a few where they sharply do not. Guide 3, "Arrays and Pointers: The Truth", takes that tangle apart strand by strand. You now hold every tool it needs.