Lesson overview
C++ gives programmers direct access to memory concepts that many languages hide. That power creates risk: a pointer can outlive the object it points at, ownership can be unclear, and manual allocation can leak or be deleted incorrectly. This lesson builds the professional habit of making lifetime and ownership explicit, then using RAII, containers and tools to catch mistakes early.
Learning objectives
- Distinguish stack lifetime, heap lifetime, pointer value and object ownership.
- Explain why returning or storing pointers to dead stack objects is unsafe.
- Identify unclear ownership in raw pointer code, especially
T*&outputs. - Choose safer alternatives: return by value, write into caller-owned references, containers or
std::unique_ptr. - Use warnings, sanitizers and minimal repros to catch leaks, dangling pointers and use-after-free bugs early.
Key vocabulary before you start
Ownership
Responsibility for the lifetime and release of a resource.
RAII
Resource Acquisition Is Initialisation: resource lifetime bound to object lifetime.
Stack lifetime
Automatic lifetime tied to a scope.
Heap lifetime
Dynamic lifetime controlled through ownership mechanisms.
Dangling reference
A reference to an object whose lifetime has ended.
Move-only type
A type such as std::unique_ptr that transfers rather than copies ownership.
Memory ownership map
A pointer is only safe when the pointed-to object's lifetime and owner are both clear.
Lifetime first
Before using a pointer or reference, know whether the target object is still alive.
Ownership must be explicit
The code should reveal who creates, owns and releases heap objects.
Prefer RAII
Containers and smart pointers tie release to object lifetime and remove manual delete paths.
Instrument risky code
Warnings, sanitizers, assertions and small repros catch memory mistakes while they are cheap to fix.
Example: unclear ownership versus RAII
The raw-pointer version relies on the caller remembering the matching delete; the RAII version makes ownership explicit.
void make_pointer(int*& out) {
out = new int(99); // who deletes?
}
int* p = nullptr;
make_pointer(p);
delete p;
std::unique_ptr<int> make_owner() {
return std::make_unique<int>(99);
}
auto owner = make_owner();Trace the state
make_pointerallocates aninton the heap and mutates the caller's pointer.- The raw pointer does not say who owns the allocated object.
- If the caller forgets
delete, the object leaks. make_ownerreturns astd::unique_ptr<int>so ownership is visible in the type.- When the
unique_ptrleaves scope, it releases the heap object automatically.
| 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: Memory Ownership, Pointers and RAII 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: ownership transfer and release
Use this model to decide whether a design owns, borrows or merely observes an object.
Create resourceA value, container or smart pointer establishes ownership.
Borrow safelyReferences and raw pointers may observe without owning.
Transfer explicitlyMove-only objects make ownership transfer visible.
Release automaticallyDestructors release resources at scope exit.
Reject unclear ownersRaw new, output owning pointers and T*& should trigger design review.
Ownership task
For each case lab item, decide whether the API should return by value, borrow by reference, or transfer ownership with a smart pointer.
Stack and heap lifetimes
Stack objects usually live until their scope ends. Heap objects live until they are explicitly released, or until an owning RAII object releases them.
A pointer is only a value that stores an address. It does not, by itself, prove that the object at that address is alive.
Memory-safety review starts by asking what object exists, who owns it and which scope or owner controls its release.
Applied task
Choose one pointer in a program and identify the object it points at, where that object is created and where it stops being valid.
Dangling pointers and stack escape
A pointer to a local stack object becomes invalid when that local object leaves scope.
Returning, storing or assigning such an address beyond the scope is a classic dangling-pointer bug.
The code can look plausible because the pointer contains an address, but the lifetime guarantee has already ended.
Pointer to a dead stack object
The local variable dies when the function returns.
int* bad_pointer() {
int local = 7;
return &local; // dangling pointer
}Applied task
Explain why the pointer value can exist after return even though the pointed-to object is no longer valid.
Safer ownership alternatives
Most pointer-returning designs can be replaced with clearer ownership patterns.
Return by value when the result is just a value. Write into a reference when the caller owns the destination. Return std::unique_ptr when heap ownership really must be transferred.
The goal is not to ban pointers. The goal is to make ownership and lifetime hard to misunderstand.
Safer alternatives
Each signature communicates a different ownership story.
int make_value(); // return by value
void write_into(int& out); // caller keeps ownership
std::unique_ptr<int> make_heap_value(); // explicit heap ownerApplied task
For each signature, state who owns the result and when the result is released.
Diagnostics: catch bugs early
Enable sanitizers such as address and undefined-behaviour sanitizers, plus high warning levels, in debug builds.
Add assertions around risky dereferences, initialise pointers to nullptr, and make invalid states visible.
Prefer containers and algorithms that offer debug-mode bounds checks where practical.
Applied task
Draft a debug profile for one compiler/toolchain that enables high warnings and memory-safety diagnostics.
Worked example: leak and dangle risk to RAII
The problem version mutates a caller's pointer and allocates on the heap. The caller must remember to release the object, and ownership is not visible at the call site.
The RAII version returns an explicit owner. The simplest version avoids heap allocation entirely and returns by value.
Professional C++ design prefers the simplest ownership model that satisfies the requirement.
Problem: leak and dangle risk
The function allocates, but the caller must remember the matching delete.
void make_pointer(int*& out) { // mutates caller's pointer
out = new int(99); // who deletes?
}
int* p = nullptr;
make_pointer(p);
delete p; // caller must remember (or leak)Better: explicit owner
unique_ptr makes ownership visible and releases automatically.
#include <memory>
std::unique_ptr<int> make_owner() {
return std::make_unique<int>(99);
}
auto p = make_owner(); // ownership clear, no manual deleteBest when no heap is needed
Return by value when the result is just a value.
int make_value_simple() { return 99; } // return by value; move elisionApplied task
Explain why the value-return version is better than heap allocation when no shared lifetime is needed.
Summary checklist
Prefer RAII: containers and smart pointers over raw new and delete.
Make ownership explicit: return by value, use references for required inputs or outputs, and use smart pointers for owned heap.
Do not hand out pointers to stack objects beyond scope. Avoid T*& unless you really need to mutate a caller's pointer.
Avoid mixing stack and heap carelessly; assume pointers can dangle unless proven safe.
Encapsulate allocations. Put new and delete in one place, usually constructors, destructors or established RAII wrappers.
Instrument with sanitizers and tests. Let tools and AI-assisted review catch memory mistakes early.
Later lessons can build on this with encapsulation patterns and smart pointers, including shared ownership.
Applied task
Use the checklist to review one pointer-based function and propose the safest replacement.
Applied case lab
Case 1: The pointer output parameter
A function writes a heap allocation into an int*& output parameter. Decide who owns the result, who deletes it and how the signature could make that obvious.
Case 2: The escaping stack address
A function returns a pointer or reference to a local variable. Explain why the address is invalid after return and propose a safe value-return alternative.
Case 3: The raw array
A raw new[] allocation is used for a fixed-size buffer. Decide whether std::array, std::vector or another container communicates the requirement better.
Applied task: spot the bug and fix it
Practise identifying dangling pointer risks, replacing raw ownership and choosing the smallest safe alternative.
Stage 1: read and classify
A. Fix a dangling pointer three ways
The function assigns a pointer to a local object that dies when the function returns.
int*& g(int*& dst) {
int local = 7;
dst = &local; // returns pointer to dead stack object
return dst;
}- Explain the lifetime bug.
- Fix it by returning a value.
- Fix it by writing into a caller-owned reference.
- Fix it with
std::unique_ptronly if heap ownership is genuinely needed.
Reveal one possible refactor
int make_value() {
return 7;
}
void write_into(int& out) {
out = 7;
}
std::unique_ptr<int> make_heap_value() {
return std::make_unique<int>(7);
}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 a raw array
Manual array allocation is easy to leak or delete incorrectly.
int* arr = new int[100];
// ... use arr ...
delete[] arr; // easy to forget- Choose
std::array<int, 100>if the size is fixed. - Choose
std::vector<int>if the size is dynamic. - Explain how the container changes the ownership story.
Reveal one possible refactor
std::array<int, 100> fixed{};
std::vector<int> dynamic(100);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. Audit an allocation boundary
Decide whether this allocation should exist at all.
int* p = new int(99);
use(*p);
delete p;- Rewrite by value if heap lifetime is unnecessary.
- Rewrite with
std::unique_ptrif ownership must cross a boundary. - State which version has the clearest release point.
Reveal one possible refactor
int value = 99;
use(value);
// or, if heap ownership must be explicit:
auto p = std::make_unique<int>(99);
use(*p);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 is wrong with returning &local from a function?
2. What does std::unique_ptr<int> communicate?
3. Which replacement is best when no heap lifetime is needed?
4. What is the main benefit of RAII?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Safety tooling
- Ask AI for compiler flags and a debug profile with high warnings and sanitizers suited to your toolchain.
- Have AI generate minimal repros to confirm a suspected leak or use-after-free.
- Ask AI to explain sanitizer output, then verify the failing line yourself.
Ownership classifier
- Paste a pointer-based function and ask AI who owns each object.
- Ask whether return-by-value, reference output,
std::vector,std::arrayorstd::unique_ptrcommunicates the design better. - Reject suggestions that preserve raw ownership without a clear reason.
RAII refactor
- Ask AI to replace raw
newanddeletewith containers or smart pointers. - Ask it to preserve semantics while making release automatic.
- Review all boundary changes because ownership changes can affect APIs.
Lifetime trace
- Ask AI to trace when each object is created, borrowed, moved and destroyed.
- Ask it to mark dangling-pointer and use-after-free risks.
- Compare the trace against actual scope boundaries and destructor calls.
Assessment tasks
- Find one raw pointer function and write an ownership trace for it.
- Replace one manual
new/deletepair withstd::unique_ptror a standard container. - Rewrite one pointer-output function as return-by-value or reference-output code.
- Create a minimal dangling-pointer repro and describe how a sanitizer reports it.
- Review one raw array and choose
std::arrayorstd::vectorwith justification.
Judgement questions
Why is pointer ownership a design issue, not just a syntax issue?
Discuss creation, borrowing, ownership transfer, release and the caller's responsibilities.
When is heap allocation actually necessary?
Compare value return, stack objects, containers and owned heap lifetime across function boundaries.
How should AI be used in memory-safety work?
Use it to trace, refactor and explain diagnostics, but verify with compiler warnings, sanitizers and tests.
