Alright, let’s get our hands dirty with the two operators that make pointers both powerful and infuriating: & (the address-of operator) and * (the dereference operator). If you don’t get these, you’ll be lost at sea without a paddle, and the sea is full of segmentation faults.

Think of a variable in your code, say int score = 975;. It lives somewhere in your computer’s memory. That ‘somewhere’ is its address. The & operator is your way of asking, “Hey, variable, where do you live?” It gives you the memory address of the variable.

The * operator is the opposite. You use it on a variable that already holds a memory address. It’s you saying, “Okay, you claim to hold an address. Let’s go to that address and get the actual value that’s stored there.” This is called dereferencing.

Getting the Address with &

Let’s see & in action. It’s straightforward.

#include <stdio.h>

int main() {
    int number = 42;
    char letter = 'A';

    printf("Value of number: %d\n", number);
    printf("Address of number: %p\n", (void*)&number); // Note the &

    printf("Value of letter: %c\n", letter);
    printf("Address of letter: %p\n", (void*)&letter); // Note the &
    
    return 0;
}

Run this. You’ll see two values and two hideous-looking numbers in hexadecimal (like 0x7ffd947a3b4c). Those hex numbers are the memory addresses. The %p format specifier is specifically for printing addresses. Why the (void*) cast? It’s good practice. %p expects a void*, and this explicitly converts our pointer type to avoid any potential warnings. The addresses will be different every time you run it because the operating system loads your program into a different location in memory each time. Neat, huh?

The Magic of Dereferencing with *

Now, the * operator. This is where the magic and the crashes happen. To use it, you need a pointer variable. A pointer is just a variable that specializes in holding memory addresses. You declare one by specifying the type of data it points to, followed by an asterisk.

#include <stdio.h>

int main() {
    int number = 42;
    int *number_pointer; // Declares a pointer to an integer

    number_pointer = &number; // number_pointer now holds the address of 'number'

    printf("Value of number: %d\n", number);
    printf("Address stored in number_pointer: %p\n", (void*)number_pointer);
    printf("Value AT the address stored in number_pointer: %d\n", *number_pointer); // Dereference!

    return 0;
}

The key line is *number_pointer. The * here is the dereference operator. It follows the address stored in number_pointer and fetches the integer value from that location. It’s essentially an alias for number. If you change *number_pointer, you change number.

*number_pointer = 100; // This is the same as writing 'number = 100'
printf("Now the value of number is: %d\n", number); // This will print 100

The Most Common Beginner Pitfall

This is the big one, the rite of passage for every C programmer: the uninitialized pointer. What’s in a pointer variable when you first declare it?

int *bad_pointer; // What address is in here? Who knows!
printf("%d\n", *bad_pointer); // This is CATASTROPHIC.

You’re asking the computer to go to some random, unknown memory address and read an integer. The operating system hates this. It’s like trying to use a GPS coordinate you found scribbled on a napkin—you’re just as likely to end up in the middle of the ocean as you are in your own driveway. At best, your program crashes immediately with a Segmentation Fault (core dumped). At worst, it corrupts some other part of your program’s memory and causes a cryptic error hours later. Always, always initialize your pointers. Set them to NULL if you have nothing else to assign yet (int *ptr = NULL;), so you can check before dereferencing.

Why the Type Matters

You might wonder, “An address is just a number, why does a pointer need a type (like int* vs char*)?” It’s because of two things: 1) Dereferencing and 2) Pointer Arithmetic (which we’ll get to next).

When you write *number_pointer, the compiler needs to know how many bytes to read starting from that address. An int is typically 4 bytes, a char is 1 byte. If number_pointer were a char*, *number_pointer would only read the first single byte of the integer 42, which would give you a completely different (and incorrect) value. The type is the compiler’s instruction manual for how to handle the memory at that address.