How C Pointers Actually Work: Memory Addresses Explained

5

Most developers treat memory like a black box. They assume variables are just names attached to values. That works until you need to write code that manipulates data directly at the machine level. Then you hit pointers.

A normal variable is simple. It’s a chunk of memory with a name you use in your code. Declare an integer i, and the compiler reserves four bytes. You see i. The machine sees an address. The value sits in those four bytes.

Pointers break that simple model.

A pointer doesn’t hold a value. It holds an address. It points to another variable. Specifically, it stores the memory location of that variable. This distinction is critical. If you confuse the two, your program will crash. Hard.

The structure is dual-layered. First, there is the pointer variable itself. It contains a numeric address. Second, there is the target variable. The address in the pointer leads you to the actual data. Once you grasp this separation, you unlock significant power in systems programming.

Consider this standard C example:

The first line declares two standard integers, i and j. The next line, int *p, declares a pointer. The asterisk is the key. It tells the compiler that p is not an integer. It is a pointer to an integer. You can do this with floats, structures, characters, or anything else. The syntax remains the same.

Then comes the part that trips up beginners: p = &i;.

In C, the ampersand & is the address operator. &i does not return the value of i. It returns the memory address where i lives. The assignment p = &i copies that address into the pointer p. Now p points to i.

If you skip this step, p holds garbage. It contains a random, undefined address. Dereferencing it later will likely cause a segmentation fault. The program crashes because you told it to access memory it doesn’t own.

Visualization helps. Imagine memory as a grid.

Before initialization, i, j, and p exist but hold unknown values. The integers are boxes with question marks. They could contain anything. The pointer p is a circle. It has arrows pointing in random directions. It doesn’t know where to go yet.

After you execute p = &i, the arrows change. They all point directly to i. p now knows exactly where i lives.

The relationship is clear. p holds the location. i holds the data. You use p to reach i.

This mechanism is how C manages memory efficiently. It allows functions to modify variables passed by reference. It enables dynamic memory allocation. It lets you build complex data structures like linked lists and trees.

But it also introduces risk. One wrong address, and you’re reading or writing into the void.

The asterisk in *p = 5 does something different than the one in int *p. The second asterisk was part of the type definition. It declared the variable as a pointer. The first asterisk is the dereference operator. It means “go to the address stored in p and change the value there.”

So *p = 5 puts the number 5 into the memory location pointed to by p. Since p points to i, i becomes 5. The variable j is then assigned the value of i. The output prints 5, 5, and 5. All three reflect the same underlying change.

Understanding how C pointers work requires shifting your mindset. Stop thinking of variables as containers. Start thinking of them as locations. The pointer is the map. The variable is the destination. If the map is wrong, you end up lost. And in low-level programming, being lost is expensive.

The Indirection Operator Explained

When you assign the address of i to p, you are essentially giving that single block of memory two different names. The variable i still refers to it directly, but p now serves as a handle, a reference point to reach the same data.

In C, this duality defines how pointer variables function. The pointer itself, p, is simply a container holding an address. It doesn’t hold the actual value you are interested in. Instead, it holds the location where that value lives. This is where the indirection operator * comes in.

Using *p allows you to access the data at the address stored in p. Think of it as dereferencing. If you write *p = 5, you aren’t changing the address inside p. You are going to the location p points to and overwriting whatever value is there with the number 5.

This distinction is critical for understanding memory management. p tells you where to look. *p tells you what to look at. Confusing the two leads to bugs that are hard to track, because you might end up changing an address instead of the data, or trying to write to a random memory location because p wasn’t initialized correctly.

Once you get comfortable with this mental model, pointers stop being abstract syntax and become a direct line to memory. You can modify variables across different scopes, pass large structures by reference without copying them, and build dynamic data structures that grow and shrink as needed. It’s powerful stuff, once you stop fighting the syntax.

The code snippet below illustrates this exact behavior, showing how the value of i changes only because p is being used to reach it.

The value of i updates instantly. The address hasn’t moved. Only the content has.

How Pointers Store Values and Addresses

It seems simple enough, but the mechanics can trip you up. Since the variable i is assigned the value 5, and the pointer p holds the address of i, dereferencing p (*p ) returns 5. When you execute j = i;, you are copying that value. So j becomes 5.

Run the printf statement now. You get 5 5 5.

This happens because of the dual nature of pointers. Think of them as having two distinct roles. First, the pointer variable itself stores a memory address. Second, it points to a specific data type located at that address.

In our example:
p is the pointer. It holds the memory location.
*p is the value. It is the actual data stored at that address.

Confusing the two leads to bugs. If you treat the address as the data, you get garbage values. If you treat the data as the address, you crash your program. Knowing which one you are dealing with changes everything.

Why does this distinction matter for everyday coding? Because understanding the difference between where data lives and what data is allows you to manipulate memory directly. This is useful for dynamic structures, but dangerous if you miss the step. One wrong move and the system tries to read from a non-existent location.

You start to see why C remains powerful and perilous in equal measure.