Course outline · 0% complete

0/27 lessons0%

Course overview →

Addresses and Pointers

lesson 5-1 · ~13 min · 13/27

The machinery under a reference

Lesson 4-2 used void doubleIt(int& x) to modify the caller's variable, and the & there made the parameter a reference, an alias bound to the caller's variable. Assignments inside the function therefore write to the original, since x was never a separate box at all.

That explanation was deliberately incomplete. This unit supplies the machinery underneath the trick, namely memory addresses and the pointer type that stores them, which is also what makes linked structures possible.

Memory is addressed, and C++ lets you use the addresses

This unit exists because the structures interviews love, meaning linked lists, trees, and graphs, are built from values that refer to other values, and in C++ that referring mechanism is explicit rather than hidden.

It is also the layer that finally explains crashes. Segmentation faults, dangling references, and even Python's object model all become concrete once you can see addresses.

The hardware rule comes first. RAM is byte-addressable, meaning every byte of memory has a unique number, its address, and the CPU reads and writes data by supplying that number. While your program runs, every variable therefore lives at some address. If a mental image helps, picture a gigantic row of numbered one-byte boxes. An int occupies 4 consecutive bytes, and the address of x means the address of its first byte.

C++ lets you work with addresses directly through three pieces of syntax:

  • &x is the address-of operator, giving the address where x lives. It is the same symbol as a reference declaration, with a different meaning decided by context.
  • A pointer is a variable whose value is an address. int* p declares p as a pointer to int.
  • *p is the dereference operator, meaning go to the address stored in p and use the int that lives there.
int x = 42;
int* p = &x;   // p now holds x's address

std::cout << *p;   // 42, read x through p
*p = 99;           // write through p
std::cout << x;    // 99, x itself changed

Python hides all of this, since every Python name is secretly a pointer-like handle managed for you. C++ hands you the raw mechanism, including the ability to misuse it.

int x = 99address 0x5000int* p = 0x5000a pointer stores an address&x gives 0x5000*p follows the arrow to x
A pointer is just a box whose contents are the address of another box. Dereferencing (*p) follows the stored address back to the pointed-at value.

One variable, two ways to reach it

The variable x changes even though some of the assignments never mention it by name.

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;         // p holds the address of x

    std::cout << "read through p: " << *p << "\n";

    *p = 99;             // write through p
    std::cout << "x is now: " << x << "\n";

    x = 7;               // write x directly
    std::cout << "read through p again: " << *p << "\n";
    return 0;
}

Output

read through p: 42
x is now: 99
read through p again: 7

The traffic runs in both directions, and that is the point of the third print. Writing through p changes what x reads as, and writing x directly changes what *p reads as, because there is only ever one int involved.

Two distinct assignment forms are easy to confuse. *p = 99 writes to the pointed-at int, while p = &y would repoint p at a different variable and leave both ints unchanged. The presence of the * is the whole difference between changing the target and changing the pointer.

nullptr, and why pointers exist at all

A pointer that points at nothing should be set to nullptr:

int* p = nullptr;
if (p != nullptr) { std::cout << *p; }   // guard before dereferencing

Dereferencing a null (or otherwise invalid) pointer is undefined behavior, usually a crash. This is the sharpest edge in C++.

Why does the language keep such a dangerous tool?

  • Dynamic memory: memory created at run time (unit 6) is reachable only through a pointer.
  • Data structures: linked lists, trees, and graphs are nodes that point at other nodes.
  • Cheap sharing: passing an 8-byte address instead of copying a large object (references, from lesson 4-2, are a tamer packaging of the same idea).
  • Arrays: an array's name is essentially the address of its first element, as you will see next lesson.

Modifying a caller's variable through a pointer

reset_to_zero takes a pointer and sets the pointed-at int to 0, which is why main prints before: 55 and then after: 0.

#include <iostream>

void reset_to_zero(int* p) {
    *p = 0;
}

int main() {
    int lives = 55;
    std::cout << "before: " << lives << "\n";
    reset_to_zero(&lives);
    std::cout << "after: " << lives << "\n";
    return 0;
}

Output

before: 55
after: 0

The parameter is a pointer, so the body has to dereference it. *p = 0 reaches through the address and overwrites the int living there, and that single line is the whole function.

Writing p = 0 instead would compile and do nothing useful. It would set the local copy of the address to null and leave lives untouched, since the pointer itself was passed by value like any other argument. The pointer is a copy, and the thing it points at is not.

Note the & at the call site, in reset_to_zero(&lives). A pointer parameter forces the caller to write that ampersand, which makes the modification visible at the call, unlike the reference version in lesson 4-2 where doubleIt(a) looked identical to a read-only call.

Swapping two variables through pointers

The same swap as lesson 4-2, rewritten with pointers instead of references, so every access needs an explicit dereference.

#include <iostream>

void swapValues(int* a, int* b) {
    int tmp = *a;
    *a = *b;
    *b = tmp;
}

int main() {
    int x = 3, y = 7;
    swapValues(&x, &y);
    std::cout << x << " " << y << "\n";
    return 0;
}

Output

7 3

Every line in the body carries a *, because reading a value through a pointer is *a and writing through it is *a = .... The temporary is needed for the same reason as before, since the first assignment destroys the old value of *a.

Swapping the pointers themselves, as int* t = a; a = b; b = t;, would compile and accomplish nothing. It exchanges the local copies of the two addresses, and both copies are discarded when the function returns.

VersionParametersCall siteBody
reference (4-2)int& a, int& bswap_ints(x, y)tmp = a; a = b; b = tmp;
pointer (here)int* a, int* bswapValues(&x, &y)tmp = *a; *a = *b; *b = tmp;

Both do the identical work, and the reference version is what modern C++ prefers, since it cannot be null and needs no dereference noise. Pointers earn their place when the target is genuinely optional or when it has to be repointed, which references cannot do.