Lesson overview
C++ allows direct manipulation of memory through pointers. That power matters for systems work, but it must be handled with discipline: initialise pointers, respect lifetimes, avoid raw ownership and prefer references or containers when direct pointer semantics are unnecessary.
Learning objectives
- Explain virtual address space and why a pointer value is an address in a process.
- Use
&and*to take addresses and dereference pointers safely. - Distinguish pointers from references and choose the clearer API type.
- Read const-correct pointer declarations.
- Identify pointer bugs such as wild pointers, dangling pointers, double delete and out-of-bounds arithmetic.
Key vocabulary before you start
Virtual address
An address in the process view of memory rather than a physical hardware location.
Pointer value
A value that may designate an object, be null or be invalid/dangling.
Dereference
Accessing the object a pointer points to with *.
Null pointer
A pointer value that deliberately points to no object.
Dangling pointer
A pointer whose target object has ended its lifetime.
Ownership
Responsibility for releasing a resource exactly once.
Pointer safety map
A pointer is useful only when the pointed-to object is alive, the pointer is valid and the ownership story is clear.
Initialise pointers
Use nullptr until a pointer has a real target.
Check lifetime
Never use a pointer after the pointed-to object has gone out of scope or been deleted.
Prefer references for required objects
Use a reference when the object must exist and null is not meaningful.
Avoid raw ownership
Use containers and smart pointers to own dynamic memory.
Example: address-of and dereference
This example mirrors the supplied pointer lesson and shows how a pointer can observe and mutate another object.
int y = 5;
int* x = nullptr;
x = &y; // x stores the address of y
int v = *x; // dereference: read y through x
*x = 42; // dereference: write y through xTrace the state
yis anintobject with value 5.xis a pointer variable that can store the address of anint.x = &ymakesxpoint aty.*x = 42writes through the pointer, soybecomes 42.
| Line or operation | Variable | Value | State role | Invariant or check |
|---|---|---|---|---|
| Read the example before running it | Key names and calls | Known from source | Reasoning setup | Each named element should support the lesson focus: Direct Memory Manipulation and Pointers in C++. |
| Trace the first meaningful operation | Primary state or boundary | Established by initialisation, call or condition | Valid-state checkpoint | Later reasoning must not use the value before this checkpoint. |
| Identify the first decision or dereference | Control or access point | Depends on current state | Failure-mode checkpoint | The condition, pointer, argument or dependency must be valid before use. |
| Record the observable result | Return value, output or mutation | Produced by the example | Evidence checkpoint | The result should match the contract explained in the lesson. |
Visual model: pointer validity states
Before dereferencing a pointer, classify its state rather than trusting the syntax.
NullThe pointer intentionally points to no object and must not be dereferenced.
Valid non-owningThe pointer refers to a live object owned elsewhere.
Owning raw pointerThe pointer is responsible for deletion, which is fragile in modern C++.
DanglingThe target lifetime has ended; dereference is invalid.
Prefer safer boundaryUse references, values, containers or smart pointers when they express intent better.
Validity task
For each pointer in the mini exercise, mark its state before and after every assignment or delete.
Virtual vs physical memory
A process works with a virtual address space. The operating system and hardware memory-management unit map those virtual addresses onto physical memory as needed.
A pointer value is meaningful inside the process that owns that address space. It is not a portable, universal physical location.
This abstraction lets programs treat memory as a large addressable space while the system manages protection, mapping and paging.
Applied task
Explain why printing an address is useful for debugging but not a stable program result.
The unary operators & and *
&object gives the address of an object. *pointer dereferences a pointer to access the object it points to.
Dereferencing is only valid if the pointer points to a live object of the right type.
Use parentheses and naming when pointer expressions become hard to read. A confusing pointer expression is a maintenance risk.
Read and write through a pointer
The pointer does not own y; it aliases it.
int y = 5;
int* p = &y;
std::cout << *p << '\n';
*p = 99;
std::cout << y << '\n';Applied task
Trace which object changes when *p = 99 executes.
Initialisation matters: nullptr, dangling and wild pointers
An uninitialised pointer contains an indeterminate value. Dereferencing it is unsafe.
Use nullptr to represent no target. It is explicit, comparable and safer than leaving a pointer wild.
A dangling pointer once pointed to a valid object, but that object's lifetime has ended.
Wild vs null pointer
The first pointer is dangerous because it has no known state.
int* p; // wild: do not use
int* q = nullptr; // safe empty stateDangling pointer
The local object dies at the closing brace.
int* d = nullptr;
{
int local = 7;
d = &local;
}
// d is now dangling; do not dereference itApplied task
Identify the line where a pointer becomes dangling.
Pointers vs references
A reference is usually the clearer API when an object is required. It cannot be reseated and does not represent null.
A pointer is appropriate when null is a meaningful state, when reseating is needed, or when interfacing with APIs that use pointer semantics.
Use the type to communicate intent. int& says required object; int* says optional or low-level address semantics.
Required vs optional object
The reference version does not need a null check.
void set_to_42(int& r) {
r = 42;
}
void maybe_set(int* p) {
if (p) *p = 42;
}Applied task
Rewrite one pointer parameter as a reference where null is not meaningful.
Const-correctness with pointers
const int* means pointer to const int: you cannot modify the int through that pointer.
int* const means const pointer to int: the pointer cannot be reseated, but the pointed-to int can be modified.
const int* const means both the pointer and pointed-to int are const through that name.
Read pointer declarations right-to-left
Small examples make the rule easier to remember.
int value = 1;
const int* readOnly = &value; // cannot write *readOnly
int* const fixedTarget = &value; // cannot reseat fixedTarget
const int* const fixedReadOnly = &value; // neither through this nameApplied task
For each declaration, say whether the pointer can be reseated and whether the pointed-to value can be modified.
Arrays and pointer arithmetic
Array names can decay to pointers in many expressions, and pointer arithmetic advances by elements, not bytes.
This is powerful but easy to misuse. Indexing past the valid range is undefined behaviour.
Prefer std::array, std::vector, iterators and ranges unless raw pointer arithmetic is required for a clear systems-level reason.
Pointer arithmetic
Incrementing the pointer moves to the next element.
int values[3] = {10, 20, 30};
int* p = values;
std::cout << *p << '\n';
++p;
std::cout << *p << '\n';Applied task
Explain why p + 1 moves by one int, not one byte.
Dynamic memory: avoid raw new and delete in modern C++
Raw new and delete create ownership obligations that are easy to break across returns, exceptions and multiple code paths.
Use containers for sequences and smart pointers when heap ownership is genuinely required.
The best heap allocation is often the one you do not write at all.
Prefer containers
The vector owns and releases its memory automatically.
std::vector<int> values(100);
values[0] = 42;Prefer smart ownership
unique_ptr makes sole ownership explicit.
auto p = std::make_unique<int>(42);
std::cout << *p << '\n';Applied task
Replace one raw allocation with a container or smart pointer.
Printing addresses
Printing addresses can help trace aliasing and lifetime in a debugging exercise.
Cast object pointers to const void* for a clear address display.
Do not build program logic around printed addresses. Address values vary between runs and environments.
Address display
This shows the address of value without implying ownership.
int value = 5;
std::cout << static_cast<const void*>(&value) << '\n';Applied task
Use address printing to prove two pointers alias the same object.
Common pointer bugs
The classic pointer bugs are uninitialised pointers, dangling pointers, use-after-free, double delete, out-of-bounds access and invalidated pointers into containers.
Many bugs are not visible immediately. They corrupt state and fail later, which is why sanitizers and small repros matter.
The professional default is to remove raw pointer ownership from ordinary application code.
Invalidated vector pointer
A reallocation can invalidate a pointer into a vector.
std::vector<int> v{1, 2, 3};
int* p = &v[0];
v.push_back(4); // may reallocate
// p may now be invalidApplied task
For each pointer in a sample, identify owner, target lifetime and invalidation risk.
Applied case lab
Case 1: The optional output
A function takes int* out and checks for null. Decide whether a pointer is appropriate or whether a reference would be clearer.
Case 2: The raw array
A dynamically allocated array is manually deleted after several early returns. Replace it with std::vector.
Case 3: The container pointer
A pointer into a vector is saved before push_back. Explain how reallocation can invalidate it.
Applied task: explain the pointer
Practise tracing pointer state and replacing unsafe pointer APIs.
Stage 1: read and classify
A. Trace address and dereference
Explain every state change.
int y = 5;
int* x = nullptr;
x = &y;
int a = *x;
*x = 99;- State the value of
yafter each line. - State whether
xis null, valid or dangling after each line.
Reveal one possible refactor
After `x = &y`, `x` validly points to `y`. `a` becomes 5. `*x = 99` mutates `y`, so `y` becomes 99.Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 2: trace or repair
B. Replace an output pointer
Null is not meaningful here, so use a reference.
void write_value(int* out, int v) {
if (out) *out = v;
}- Rewrite using
int&. - Explain how the signature changes caller expectations.
Reveal one possible refactor
void write_value(int& out, int v) {
out = v;
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Stage 3: justify the design
C. Make ownership explicit
This function returns owned heap memory through a raw pointer.
int* make_value(int v) {
return new int(v);
}- Rewrite using
std::unique_ptr<int>. - Also provide a version that returns by value when no heap is needed.
Reveal one possible refactor
std::unique_ptr<int> make_value(int v) {
return std::make_unique<int>(v);
}
int make_value_simple(int v) {
return v;
}Model reasoning
A strong answer explains why the change preserves behaviour, what failure mode it removes, and which trade-off remains acceptable.
Quick checks
1. What does &y produce?
2. When is a reference usually clearer than a pointer?
3. Which pointer state is safest before a target is known?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Safety First
- Ask AI to classify each pointer as owner, observer, optional output or legacy API boundary.
- Ask AI to identify where each pointer can become null, dangling or invalidated.
- Ask for a replacement using references, containers, iterators or smart pointers.
Memory Debug Toolkit
- Ask for compiler flags enabling address and undefined-behaviour sanitizers.
- Generate a minimal reproduction for one pointer bug before fixing the production code.
Assessment tasks
- Annotate a pointer-heavy function with owner, target lifetime and nullability for every pointer.
- Refactor one pointer output parameter to a reference or return value.
- Replace one raw dynamic array with
std::vectororstd::array.
Judgement questions
When is a raw pointer justified?
Give one legitimate systems-level or interoperability case and one case where a reference/container is better.
Which pointer bug is hardest to see by inspection?
Pick dangling, invalidated, double delete or out-of-bounds and explain the evidence you would gather.
