Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 14

Direct Memory Manipulation and Pointers in C++

Understand raw pointers as direct access to memory addresses, then use that understanding to avoid dangling, invalid and unsafe pointer behaviour.

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.

LevelMasters
ModeSeminar, pointer-tracing lab and safety review
EvidenceA pointer trace that explains address-of, dereference, const pointer forms, pointer arithmetic and safer API replacements
SourceConverted from supplied Canvas lesson HTML

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.

P1

Initialise pointers

Use nullptr until a pointer has a real target.

P2

Check lifetime

Never use a pointer after the pointed-to object has gone out of scope or been deleted.

P3

Prefer references for required objects

Use a reference when the object must exist and null is not meaningful.

P4

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 x

Trace the state

  1. y is an int object with value 5.
  2. x is a pointer variable that can store the address of an int.
  3. x = &y makes x point at y.
  4. *x = 42 writes through the pointer, so y becomes 42.
Line or operationVariableValueState roleInvariant or check
Read the example before running itKey names and callsKnown from sourceReasoning setupEach named element should support the lesson focus: Direct Memory Manipulation and Pointers in C++.
Trace the first meaningful operationPrimary state or boundaryEstablished by initialisation, call or conditionValid-state checkpointLater reasoning must not use the value before this checkpoint.
Identify the first decision or dereferenceControl or access pointDepends on current stateFailure-mode checkpointThe condition, pointer, argument or dependency must be valid before use.
Record the observable resultReturn value, output or mutationProduced by the exampleEvidence checkpointThe 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 state

Dangling pointer

The local object dies at the closing brace.

int* d = nullptr;
{
    int local = 7;
    d = &local;
}
// d is now dangling; do not dereference it

Applied 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 name

Applied 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 invalid

Applied 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 y after each line.
  • State whether x is 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

  1. Annotate a pointer-heavy function with owner, target lifetime and nullability for every pointer.
  2. Refactor one pointer output parameter to a reference or return value.
  3. Replace one raw dynamic array with std::vector or std::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.