Engineering/Backend

What Does a C Pointer Store, and What Does Dereferencing Actually Change?

Isaac S. Lee 2026. 7. 28. 01:34

The most confusing part of learning pointers is rarely the amount of code involved. The example above contains only two lines, yet x, &x, p, *p, and &p all mean different things.

Trying to memorize each expression as an isolated syntax rule tends to work only until pointers appear alongside arrays, functions, or structures. A more reliable approach is to begin with the objects that exist in memory and the relationships between them.

int x = 10;
int *p = &x;

The central question is simple:

What is stored in p, and which object do we access when we evaluate *p?

Once that relationship is clear, several other ideas follow naturally: modifying an object through a pointer, allowing multiple pointers to refer to the same object, and passing an address to a function so that it can modify caller-owned state.


A variable has both a value and an address

The following declaration creates an integer object named x and stores the value 10 in it.

int x = 10;

When a program evaluates x, it normally reads the value stored in that object.

printf("%d\n", x);
10

However, x does not exist only as an abstract name. During program execution, it occupies storage, and that storage has an address.

Suppose, purely for illustration, that x is located at address 0x1000. The actual address may vary between executions and environments, so the number itself is not important. The relationship is.

ExpressionMeaningExample

x The value stored in x 10
&x The address of x 0x1000

The & operator is the address-of operator. Applied to an object, it produces a pointer to that object.

printf("%p\n", (void *)&x);

The %p conversion expects a void *, so casting the pointer to void * is the appropriate way to print it with printf.

The first distinction to make is therefore:

x   → the stored data
&x  → the location of that data

Pointers begin to make sense once values and addresses are treated as different kinds of information.


A pointer is an object too

We can store the address of x in another object.

int x = 10;
int *p = &x;

Here, p is a pointer to int. Its current value points to x.

Using hypothetical addresses, the memory state might look like this:

x
address: 0x1000
value:   10

p
address: 0x2000
value:   0x1000

The pointer p is itself an object that occupies storage. It has its own address, separate from the address it stores.

ExpressionMeaningExample

x The value stored in x 10
&x The address of x 0x1000
p The pointer value stored in p 0x1000
&p The address of the pointer object p 0x2000

At this point, the following comparison is true:

p == &x

The pointer does not contain a copy of x's value. It contains a pointer value that identifies the location of x.

A useful way to read the declaration

int *p;

is:

p is an object that can point to an int.

That is more informative than simply remembering that p is “a variable with an asterisk.”


Dereferencing accesses the pointed-to object

Reading a pointer value does not automatically read the object it points to. To access that object, C provides the unary *operator.

printf("%d\n", *p);

Since p currently points to x, evaluating *p accesses x and reads the value stored there.

p
↓
x
↓
10

The current relationship can be summarized as follows:

p == &x
*p == x

The second comparison does not mean that *p and x are separate copies that happen to contain the same value. Both expressions access the same underlying object.

Conceptually, evaluating *p involves the following steps:

  1. Read the pointer value stored in p.
  2. Find the object identified by that pointer.
  3. Treat the pointed-to object as an int.
  4. Access that object.

The distinction is straightforward:

p   → read the pointer value
*p  → access the object designated by that pointer

Assigning through a pointer modifies the original object

Dereferencing is not limited to reading. If a pointer designates a modifiable object, dereferencing it can also be used to update that object.

#include <stdio.h>

int main(void) {
    int x = 10;
    int *p = &x;

    *p = 25;

    printf("%d\n", x);
    return 0;
}

The output is:

25

This statement does not store 25 in the pointer object p.

*p = 25;

It means:

Store 25 in the object pointed to by p.

Because p currently points to x, the assignment changes x.

Under the current pointer relationship, these two statements modify the same object:

x = 25;
*p = 25;

The first accesses the object directly by name. The second reaches it indirectly through its address.

That indirect access is the defining purpose of a pointer. Code does not need to know the original variable name. If it has a valid pointer to the object, it can access that object.


The * in a declaration is not the same operation as dereferencing

The same symbol appears in two different contexts, which is one reason pointer syntax initially feels inconsistent.

In a declaration:

int *p;

the * is part of the declarator. It tells us that p is a pointer to int.

In an expression:

*p = 20;

the * is the dereference operator. It accesses the object designated by p.

The following statement shows both ideas together:

int *p = &x;

It can be read in three parts:

  • int *p: declare p as a pointer to int
  • &x: produce a pointer to x
  • =: store that pointer value in p

Keeping declarations and expressions conceptually separate makes more complex pointer code easier to read.


A pointer’s type determines how the target is interpreted

Pointers store pointer values, but the pointed-to type still matters.

int *int_pointer;
char *char_pointer;
double *double_pointer;

The type tells the compiler what kind of object should be accessed when the pointer is dereferenced.

int *p;

Dereferencing p accesses an int.

char *p;

Dereferencing this p accesses a char.

The type also affects pointer arithmetic.

p + 1

If p is an int *, adding one does not generally mean moving one byte forward. It means advancing to the next intelement.

On an implementation where sizeof(int) is 4, advancing an int * by one typically changes the underlying address by four bytes.

Pointer arithmetic is not permission to move through arbitrary memory. In C, arithmetic on a pointer is defined primarily within the bounds of the same array object, including the special one-past-the-end position. Creating a pointer one past the final element is allowed, but dereferencing it is not.

This restriction becomes especially important when working with arrays.


Multiple pointers can refer to the same object

Two distinct pointer objects may contain pointers to the same target object.

#include <stdio.h>

int main(void) {
    int x = 4;

    int *p = &x;
    int *q = p;

    *q = 9;

    printf("%d\n", x);
    printf("%d\n", *p);
    printf("%d\n", *q);

    return 0;
}

The output is:

9
9
9

This declaration does not copy the value 4 into q.

int *q = p;

It copies the pointer value stored in p. Afterward, both pointers designate x.

p ─┐
   ├──→ x
q ─┘

When the program executes

*q = 9;

it modifies x.

Because p still points to the same object, reading *p also produces 9.

When multiple expressions or pointers refer to the same object, they are often said to alias that object. Aliasing is useful because it allows data to be shared and modified indirectly, but it can also make data flow harder to reason about.

If a modification in one part of a program unexpectedly affects another part, one of the first questions to ask is whether two pointers refer to the same object.


Passing a pointer does not change C’s calling convention

A common use of pointers is allowing a function to modify an object owned by its caller.

Consider a function that accepts an ordinary integer parameter:

#include <stdio.h>

void increment(int value) {
    value++;
}

int main(void) {
    int count = 5;

    increment(count);

    printf("%d\n", count);
    return 0;
}

The output remains:

5

The function receives a copy of the value stored in count.

count in main       = 5
value in increment  = 5

Changing value does not change count because they are separate objects.

To modify the caller’s object, the function can receive its address instead.

#include <stdio.h>

void increment(int *value) {
    (*value)++;
}

int main(void) {
    int count = 5;

    increment(&count);

    printf("%d\n", count);
    return 0;
}

The output is now:

6

The relationship is:

&count
   │
   ▼
value ───→ count

The pointer parameter receives a copy of the pointer to count. Dereferencing that pointer accesses the original object.

This does not mean that C suddenly switches to pass-by-reference.

C always passes arguments by value. In this example, the copied value happens to be a pointer value rather than an integer value.

increment(&count);

The function receives a copy of &count. That copied pointer still identifies the caller’s object, which is why indirect modification is possible.

A precise description is therefore:

C passes the pointer by value, and the function uses the copied pointer to access the caller’s object.


How pointers appear in real function interfaces

A function that swaps two values demonstrates the purpose more clearly than a simple increment function.

#include <stdio.h>

void swap(int *left, int *right) {
    int temporary = *left;
    *left = *right;
    *right = temporary;
}

int main(void) {
    int first = 10;
    int second = 20;

    swap(&first, &second);

    printf("%d %d\n", first, second);
    return 0;
}

The output is:

20 10

The function does not know the names first or second. It receives pointers to the two objects.

left  ───→ first
right ───→ second

By assigning through *left and *right, the function modifies the caller-owned objects directly.

This pattern appears throughout C APIs:

  • filling a caller-provided buffer
  • returning multiple results through output parameters
  • modifying the state of a structure
  • processing arrays and linked data structures
  • managing dynamically allocated resources

Pointers are not merely a syntax for displaying memory addresses. They are part of how C functions express ownership, mutation, and access to external objects.


(*p)++ and *p++ do not mean the same thing

Pointers become particularly error-prone when combined with increment operators.

Consider these two expressions:

(*p)++;
*p++;

The first expression increments the value of the object pointed to by p.

The second is parsed as:

*(p++);

because postfix ++ has higher precedence than unary *.

The pointer itself is incremented, not the pointed-to value.

ExpressionMeaning

(*p)++ Increment the value of the pointed-to object
p++ Advance the pointer to the next element
*p++ Form the dereferenced expression using the current pointer, then increment the pointer

A common array-processing pattern is:

value = *p++;

This reads the current element into value, then advances p to the next element.

The same behavior can be written more explicitly:

value = *p;
p++;

The shorter form is not automatically better. When pointer mutation is involved, explicit code is often easier to review and less likely to be misread.


A pointer needs more than a numeric address to be valid

A pointer containing an address-like bit pattern is not necessarily safe to dereference. It must designate a live object that the program is allowed to access.

Uninitialized pointers

int *p;
*p = 10;

The local pointer p has not been initialized. There is no guarantee that it points to a valid object. Dereferencing it produces undefined behavior.

Null pointers

int *p = NULL;

A null pointer represents the absence of a pointer to an object.

It must not be dereferenced.

*p = 10;

NULL is not a special address that can safely be read or written. It is a sentinel value indicating that no valid target object is present.

Pointers to objects whose lifetime has ended

int *create_pointer(void) {
    int value = 10;
    return &value;
}

The local object value ceases to exist when the function returns. The returned pointer no longer designates a live object.

The same problem occurs after dynamically allocated memory has been released.

free(p);
printf("%d\n", *p);

Once the allocation has been freed, dereferencing the old pointer is invalid.

Safe pointer use therefore requires more than checking whether the pointer is non-null. The programmer must also consider whether:

  • the pointer has been initialized
  • the pointer is non-null
  • the pointed-to object is still alive
  • the program is allowed to access that object
  • the access uses a compatible type
  • alignment requirements are satisfied
  • array bounds are respected

Good pointer code does not merely preserve addresses. It preserves clear relationships between objects, ownership, and lifetime.


Trace relationships before calculating values

Consider the following program:

#include <stdio.h>

int main(void) {
    int first = 3;
    int second = 7;
    int *current = &first;

    *current += 2;
    current = &second;
    (*current)++;

    printf("%d %d\n", first, second);
    return 0;
}

Instead of immediately calculating the output, begin by recording the pointer relationship.

first = 3
second = 7
current → first

The first update is:

*current += 2;

Since current points to first, the program updates first.

first = 5
second = 7
current → first

The next assignment changes the pointer itself.

current = &second;
first = 5
second = 7
current → second

Finally:

(*current)++;

Now current points to second, so second is incremented.

first = 5
second = 8
current → second

The output is:

5 8

The most useful question during pointer tracing is not merely:

What value is stored in current?

A better question is:

Which object does current designate at this point in the program?

Once that relationship is known, the meaning of *current follows directly.


Copying a pointer is different from copying pointed-to data

The following program contains two assignments that look similar but have very different effects.

#include <stdio.h>

int main(void) {
    int x = 10;
    int y = 20;

    int *p = &x;
    int *q = p;

    *q = 30;
    q = &y;
    *q = *p;

    printf("%d %d\n", x, y);
    return 0;
}

Initially:

p → x
q → x

This declaration copies a pointer value:

int *q = p;

Both pointers now designate the same object.

The next statement modifies x:

*q = 30;
x = 30
y = 20
p → x
q → x

Then the pointer stored in q is replaced:

q = &y;
x = 30
y = 20
p → x
q → y

Finally:

*q = *p;

The right-hand side reads the value of x through p. The left-hand side writes that value into y through q.

x = 30
y = 30

The final output is:

30 30

The difference can be summarized clearly:

q = p;

Copies the pointer value. Afterward, the two pointers designate the same object.

*q = *p;

Copies data between the two pointed-to objects.

Confusing these two operations is one of the most common mistakes in pointer-heavy code.


Practice problems

Before running each example, write down:

  1. the current value of each ordinary variable
  2. the object designated by each pointer
  3. whether each assignment changes a pointer or a pointed-to object

Problem 1

int x = 5;
int *p = &x;

*p = 12;

printf("%d %d\n", x, *p);

Problem 2

int first = 1;
int second = 2;
int *p = &first;

p = &second;
*p += 3;

printf("%d %d\n", first, second);

Problem 3

int value = 4;
int *p = &value;
int *q = p;

(*p)++;
*q += 2;

printf("%d %d %d\n", value, *p, *q);

Problem 4

void double_value(int *p) {
    *p *= 2;
}

int main(void) {
    int value = 6;

    double_value(&value);

    printf("%d\n", value);
    return 0;
}

Problem 5

int first = 10;
int second = 20;

int *p = &first;
int *q = &second;

*p = *q;
*q = 30;

printf("%d %d\n", first, second);

Solutions

Problem 1

12 12

p points to x, so assigning 12 through *p modifies x. Both x and *p access the same object.


Problem 2

1 5

The pointer initially designates first, but this assignment changes the relationship:

p = &second;

The later expression

*p += 3;

therefore modifies second. The value of first remains unchanged.


Problem 3

7 7 7

Both p and q point to value.

(*p)++;

changes value from 4 to 5.

*q += 2;

uses another pointer to the same object, changing the value from 5 to 7.

All three output expressions observe the same object.


Problem 4

12

The call passes a pointer to value.

double_value(&value);

Inside the function, p designates the caller’s object. Multiplying *p by two therefore changes value from 6 to 12.


Problem 5

20 30

The initial relationships are:

p → first
q → second

This statement copies the value of second into first:

*p = *q;

The state becomes:

first = 20
second = 20

The next statement modifies second through q:

*q = 30;

The final state is:

first = 20
second = 30

Understanding pointers means understanding relationships

Return to the original example:

int x = 10;
int *p = &x;

Each expression has a distinct role.

ExpressionMeaning

x The value stored in the object x
&x A pointer to x
p The pointer value stored in p
*p The object designated by p
&p A pointer to the pointer object p

While p points to x, this comparison is true:

p == &x

Dereferencing p accesses x.

From this small model, most fundamental pointer behavior follows:

  • storing an object’s address in a pointer
  • reading an object through a pointer
  • modifying an object through a pointer
  • copying addresses so multiple pointers refer to the same object
  • passing object addresses into functions
  • tracking the validity and lifetime of pointed-to objects

When reading pointer-based code, the first task should not be to calculate the final output.

Ask this instead:

Which object does each pointer designate at this exact point in the program?

Once that relationship is clear, it becomes much easier to determine what *p reads, what *p = value modifies, and which state changes survive a function call.

C pointers are not merely a collection of unusual symbols. They allow object locations to be represented as values and used for indirect access.

To understand pointers well is not simply to recognize an address. It is to reason accurately about the relationship between objects, pointer values, ownership, and lifetime.

References