Lesson 118 min read

Pointers & References

GPS coordinates vs. nicknames β€” two ways to reach the same data

The GPS and the Nickname

Imagine you live at 42 Elm Street. A pointer is like writing that address on a sticky note β€” it doesn't contain your house, it just tells someone where to find it. A reference is like a nickname β€” if your friends call you "Al" instead of "Alexander," they're still talking about the same person. No address needed, just another name.

In C++, these two ideas β€” pointers and references β€” are the foundation of working with memory directly. Let's dig in.

Raw Pointers: * and &

A pointer is a variable that stores a memory address. You declare one with * and get an address with &.

  • int* p β€” "p is a pointer to an int"
  • &x β€” "the address of x"
  • *p β€” "the value at the address p holds" (dereferencing)

Think of & as asking "where do you live?" and * as "let me visit that address."

Pointer Basics

#include <iostream>
using namespace std;
int main() {
int x = 42;
int* p = &x; // p stores the address of x
cout << "x = " << x << endl;
cout << "&x = " << &x << endl; // address of x
cout << "p = " << p << endl; // same address
cout << "*p = " << *p << endl; // dereference: value at that address
*p = 99; // modify x through the pointer
cout << "x after *p = 99: " << x << endl;
return 0;
}
Output
x  = 42
&x = 0x7ffeeb24a8dc
p  = 0x7ffeeb24a8dc
*p = 42
x after *p = 99: 99

References: Another Name for the Same Thing

A reference is declared with & (yes, the same symbol β€” context matters). Once bound, a reference is the original variable. There's no separate address to manage, no dereferencing needed.

Reference Basics

#include <iostream>
using namespace std;
int main() {
int x = 42;
int& ref = x; // ref is another name for x
cout << "x = " << x << endl;
cout << "ref = " << ref << endl;
ref = 100; // modifying ref modifies x
cout << "x after ref = 100: " << x << endl;
x = 7;
cout << "ref after x = 7: " << ref << endl;
return 0;
}
Output
x   = 42
ref = 42
x after ref = 100: 100
ref after x = 7:   7

Swap: Pointer Version vs. Reference Version

The classic swap function shows the difference beautifully. With pointers, you pass addresses and dereference. With references, the syntax is clean β€” the compiler handles the indirection for you.

Swap β€” Pointers vs. References

#include <iostream>
using namespace std;
// Pointer version: caller must pass addresses
void swapPtr(int* a, int* b) {
int temp = *a;
*a = *b;
*b = temp;
}
// Reference version: cleaner syntax, same effect
void swapRef(int& a, int& b) {
int temp = a;
a = b;
b = temp;
}
int main() {
int x = 1, y = 2;
swapPtr(&x, &y); // must pass &x, &y
cout << x << " " << y << endl;
int a = 10, b = 20;
swapRef(a, b); // just pass a, b
cout << a << " " << b << endl;
return 0;
}
Output
2 1
20 10

nullptr β€” Not NULL

In modern C++, use nullptr instead of NULL or 0. nullptr is type-safe β€” it's specifically a null pointer, while NULL is just the integer 0 wearing a disguise, which can cause ambiguity in function overloads.

nullptr Check

#include <iostream>
using namespace std;
void process(int* data) {
if (data == nullptr) {
cout << "No data β€” pointer is null!" << endl;
return;
}
cout << "Processing: " << *data << endl;
}
int main() {
int value = 42;
process(&value); // valid pointer
process(nullptr); // null pointer
return 0;
}
Output
Processing: 42
No data β€” pointer is null!

const Pointer vs. Pointer to const

This trips up everyone at first. Read the declaration right to left:

  • const int* p β€” pointer to a const int (can't change the value, can change where it points)
  • int* const p β€” const pointer to an int (can change the value, can't change where it points)
  • const int* const p β€” const pointer to a const int (can't change anything)

const Pointer Variations

#include <iostream>
using namespace std;
int main() {
int a = 10, b = 20;
// Pointer to const: can't modify *p1, can reassign p1
const int* p1 = &a;
// *p1 = 50; // ERROR: can't modify value
p1 = &b; // OK: can point somewhere else
cout << "p1 -> " << *p1 << endl;
// Const pointer: can modify *p2, can't reassign p2
int* const p2 = &a;
*p2 = 50; // OK: can modify value
// p2 = &b; // ERROR: can't repoint
cout << "a = " << a << endl;
// Const pointer to const: can't do either
const int* const p3 = &b;
cout << "p3 -> " << *p3 << endl;
// *p3 = 99; // ERROR
// p3 = &a; // ERROR
return 0;
}
Output
p1 -> 20
a = 50
p3 -> 20

Pointer vs. Reference: When to Use Which

Here's the quick guide:

FeaturePointerReference
Can be nullYesNo
Can be reassignedYesNo (bound at creation)
Syntax overheadMore (* and &)Less (automatic)
ArithmeticYes (p++, p+3)No

Rule of thumb: Use references when you can, pointers when you must. You need pointers for optional values (nullable), dynamic memory, and data structures like linked lists.

Note: References cannot be null and cannot be reseated (pointed at something else after initialization). This makes them safer than pointers. Use references when you can, pointers when you must β€” like when you need nullability or pointer arithmetic.
Challenge

Quick check

What does the expression *p do when p is a pointer?
← Inheritance & PolymorphismSmart Pointers β†’