Dataslope logoDataslope

Pointers

Addresses, the * and & operators, and why pointers are the soul of C

This is the chapter people are warned about. "Pointers are hard." "That's where C gets weird." They are right that pointers are where C reveals what is really going on. They are wrong that it has to be confusing.

The trick is to keep the picture in front of you at all times. Every pointer is just a box that holds an address. Once you draw the boxes, everything else falls into place.

What a pointer is

Every byte of RAM has an address — a number. A pointer is a variable whose value is one of those addresses. We write the type of a pointer-to-int as int *:

int   n = 42;     // an ordinary int
int  *p = &n;     // a pointer that holds the address of n

Two new operators appeared:

  • &n — the address-of operator. Read it as "the address of n".
  • int *p — the type "pointer to int". Read the asterisk in declarations as "is a pointer to".

To read or write through the pointer, use * (the dereference operator):

*p          // the value at the address p holds, i.e. n
*p = 100;   // store 100 into the memory p points at, i.e. into n
Code Block
C 17 (201710L)

The exact addresses printed will differ every run. That's fine — we care about the relationships, not the specific numbers.

The two operators, side by side

The most common beginner trap is reading the * in a declaration the same way you read it in an expression. They mean different things:

ContextMeaning
int *p;declaration: p is a pointer to int
*pexpression: the value at the address stored in p
&nexpression: the address of n
int *p = &n;declare p, initialize it with the address of n

Once you internalize that, half of pointer confusion vanishes.

Pictures, always pictures

Let's trace a tiny program in pictures.

int n = 7;
int *p = &n;
*p = *p + 1;

After line 1: a box n exists, containing 7.

+-----+
|  n  |
+-----+
|  7  |
+-----+

After line 2: a new box p exists, containing the address of n:

+-----+        +-----+
|  p  | ---->  |  n  |
+-----+        +-----+
|0x100|        |  7  |
+-----+        +-----+

After line 3: *p reads the value at n (7), adds 1, and writes 8 back into the same address:

+-----+        +-----+
|  p  | ---->  |  n  |
+-----+        +-----+
|0x100|        |  8  |
+-----+        +-----+

The value of p itself never changed. Only the value of the thing it pointed at changed.

Pointers as function parameters

Recall that C passes arguments by value: a function gets a copy of each argument. If you want a function to modify a caller's variable, you pass a pointer to it.

Code Block
C 17 (201710L)

The parameter x is of type int *. *x is the int it points at. (*x)++ increments that int. The parentheses matter: *x++ would parse as *(x++), which is a different (and much more confusing) operation.

This pattern — "pass &variable so the function can change it" — is how every scanf-style API works.

Returning two values from a function

A function can return only one value, but it can also write to several output parameters via pointers:

Code Block
C 17 (201710L)

The null pointer

A pointer that points to nothing valid should be set to NULL:

int *p = NULL;

Dereferencing a NULL pointer is undefined behavior, but on most systems it crashes loudly (a "segmentation fault"), which is much better than silently corrupting memory. Check for NULL before using a pointer that might be unset:

if (p != NULL) {
    *p = 5;
}

A common idiom is if (p) / if (!p), since NULL is zero (false).

What happens if you forget to initialize a pointer

An uninitialized pointer holds whatever bits were in those bytes already — an essentially random address. Dereferencing it could crash, or could silently overwrite some unrelated piece of memory.

int *p;     // p holds garbage
*p = 5;     // writing to a random address — very bad

Always initialize. Either point at something real with &x, or set to NULL to mark "not yet pointing at anything".

Pointer types matter

A char * and an int * are different types — they advance through memory by different amounts when you do arithmetic on them, and they read different numbers of bytes when you dereference. Mixing them without intention is a bug.

int n = 42;
int *pi = &n;
char *pc = (char *)&n;   // a deliberate cast — looking at n one byte at a time

We'll use this kind of cast deliberately in the chapter on memory layout. For now, stick to pointer types that match what you're pointing at.

A walk through a function call

Here is a tiny but complete example that uses pointers to swap two variables:

Code Block
C 17 (201710L)

Trace it with pictures, before and after each line of swap. You should see that tmp (a local variable inside swap) holds x's value, then x is overwritten by y, then y is overwritten from tmp. The pointers a and b give swap access to main's variables without copying them.

Challenge: in-place addition

Challenge
C 17 (201710L)
Increase a variable through a pointer

Write a function void add_to(int *value, int delta) that adds delta to whatever value points at. main uses it like this:

int x = 10;
add_to(&x, 7);
printf("%d\n", x);   // should print 17

The program must print exactly 17 and nothing else.

QuestionSelect one

Given:

int n = 5;
int *p = &n;
*p = 10;

What is the value of n afterwards?

5

The address of n.

10

Undefined, because p was reassigned.

QuestionSelect one

What is the type of the expression &x if x is declared as double x;?

double

int *

void *

double *

On this page