Lesson overview
Variables are not just names in source code. They have a lifecycle: declaration, instantiation, assignment, and release or deletion. Correct C++ depends on knowing which stage a variable is in and which memory is still alive.
Learning objectives
- Distinguish declaration, instantiation, assignment, release and deletion.
- Explain why automatic variables are released when their scope ends.
- Identify uninitialised reads, shadowing, dangling pointers and mismatched deletion.
- Use RAII and scope to make variable lifetimes easier to reason about.
- Trace construction and destruction order in a small C++ example.
Key vocabulary before you start
Declaration
A source-code introduction of a name and type.
Instantiation
The creation of an object with storage and lifetime.
Assignment
A write to an existing object.
Release
The end of a lifetime or return of a resource.
Shadowing
A nested declaration hiding an outer name.
Dangling pointer
A pointer that refers to an object whose lifetime has ended.
Variable lifecycle map
A variable is safe only while its lifetime, value state and ownership are understood.
Declare deliberately
A declaration binds a name to a type and should be as local as possible.
Instantiate before use
Memory must exist before a value can safely be stored or read.
Assign before reading
An object can exist but still contain no meaningful program value.
Respect release
After scope exit or deletion, the name or object must not be used as if it were still alive.
Example: a variable's basic lifecycle
This small example separates the stages that are often compressed into one line.
int x; // declaration + instantiation
x = 5; // assignment
// At the end of the enclosing scope, x is released and deleted.
int y = 5; // declaration + instantiation + assignmentTrace the state
xis introduced with typeint.- Storage for
xexists, but reading it before assignment would be unsafe. x = 5writes a meaningful value into that storage.- When the block ends,
xceases to exist.
| 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: The Lifecycle of a Variable. |
| 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: lifecycle failure timeline
Most lifecycle bugs occur when the code reads after release, writes through the wrong owner or hides an earlier object.
DeclareA name and type enter a scope.
Create valid stateThe object is initialised or otherwise given a meaningful value.
Use or mutateReads and writes occur while the lifetime and invariant still hold.
ReleaseThe lifetime ends automatically or through an owning object.
Reject stale accessAny later read through a pointer, reference or hidden name is a defect.
Timeline task
For each bug in the mini exercise, identify the exact lifecycle step that is missing or violated.
The four lifecycle actions
Declaration associates an identifier with a type. Instantiation reserves memory for an object. Assignment writes a value into that object. Release and deletion end the object's lifetime.
These actions may appear separately or together. int x = 5; compresses declaration, instantiation and assignment into one source statement.
Release is normally implicit for automatic variables: when the closing brace of the block is reached, the variable's lifetime ends.
Applied task
For a short function, label every declaration, assignment and scope exit.
Explicit vs implicit memory management
For int x;, declaration is explicit because you name x and give its type. Instantiation and release are managed by the compiler for an automatic local object.
This implicit behaviour is valuable because the compiler can enforce scope rules. You cannot refer to a local name after its block has ended.
Heap allocation changes the burden. If you use new, you must ensure there is a matching and correctly timed release, which is why RAII containers and smart pointers are preferred.
Stack and heap lifetimes
The pointer variable and the heap object have different lifetimes.
int n = 3; // automatic object
int* p = new int(n); // p is automatic; *p is heap allocated
*p += 1;
delete p; // releases heap object
p = nullptr;Applied task
Explain what is released by delete p and what is released at the end of the surrounding block.
Common lifecycle errors
Novice errors usually happen at the boundary between lifecycle stages: reading before assignment, reading after release, returning references to dead locals, or deleting with the wrong form.
Shadowing can create a second variable with the same name in an inner scope. That is legal, but it can hide the variable you intended to initialise or update.
A good debugging session watches state transitions, not only final output. Ask which object is alive, which one has a meaningful value and who owns it.
Bad and good initialisation
The first read has undefined behaviour because x has not been assigned.
int x;
std::cout << x; // unsafe: uninitialised read
int y = 42;
std::cout << y; // safeDangling return
Returning the address of a stack local leaks a dead lifetime to the caller.
int* make_bad() {
int x = 42;
return &x; // dangling pointer
}
int make_good() {
return 42;
}Applied task
Find one uninitialised read and one dangling pointer risk in a sample function.
Use scope and RAII to make lifetime visible
RAII ties resource lifetime to object lifetime. The constructor acquires the resource and the destructor releases it.
Containers and smart pointers turn lifetime management into a property of scope rather than a manual checklist.
At Masters level, the goal is to design code where the safe lifetime is the obvious lifetime.
Trace construction and destruction
This pattern is useful for seeing when objects are created and released.
struct Trace {
Trace(const char* name) : name(name) { std::cout << "make " << name << '\n'; }
~Trace() { std::cout << "drop " << name << '\n'; }
const char* name;
};
int main() {
Trace outer("outer");
{
Trace inner("inner");
} // inner destroyed here
} // outer destroyed hereApplied task
Predict the printed construction and destruction order before running the program.
Applied case lab
Case 1: The shadowed variable
An outer n is declared, an inner n is initialised and the outer one is later printed. Decide which object is being read and whether it has a meaningful value.
Case 2: The dangling pointer
A pointer is assigned the address of a local object and used after the block ends. Explain the exact lifecycle violation.
Case 3: The mismatched delete
An array allocated with new[] is released with delete. Explain why the allocation and release forms must match.
Applied task: trace, fix and safeguard variable lifecycles
Practise identifying lifecycle stages and replacing fragile lifetime patterns with safer alternatives.
Stage 1: read and classify
A. Label the lifecycle
Mark declaration, instantiation, assignment and release for each identifier.
#include <iostream>
using namespace std;
int main() {
int n; // outer n
if (true) {
int n = 3; // shadows outer n
int* p = new int(n);
*p += 1;
cout << *p << "\n";
delete p;
}
cout << n << "\n"; // outer n was never assigned
}- Explain what is released by
delete pand what is released at the closing brace. - Rewrite so the outer
nis safely initialised and no shadowing occurs.
Reveal one possible refactor
#include <iostream>
int main() {
int outerCount = 0;
{
int innerCount = 3;
auto value = std::make_unique<int>(innerCount);
*value += 1;
std::cout << *value << "\n";
}
std::cout << outerCount << "\n";
}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. Fix dangling returns
Identify the lifecycle errors and return safe values instead.
int* makeVal() {
int x = 42;
return &x;
}
int& pick(bool b) {
int a = 1, c = 2;
return b ? a : c;
}- Provide a value-return fix.
- Provide an RAII pointer fix where heap ownership is genuinely needed.
Reveal one possible refactor
int makeVal() {
return 42;
}
std::unique_ptr<int> makeValOwner() {
return std::make_unique<int>(42);
}
int pick(bool b) {
return b ? 1 : 2;
}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. Halt use-after-release
Find the invalid use and the mismatched deletion.
int* p = new int{5};
delete p;
std::cout << *p << "\n";
int* q = new int[3]{1, 2, 3};
delete q;- Fix the use-after-free.
- Replace the raw array with a standard container.
Reveal one possible refactor
auto p = std::make_unique<int>(5);
std::cout << *p << "\n";
std::array<int, 3> q{1, 2, 3};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. Which lifecycle action writes a meaningful value into existing storage?
2. What is wrong with returning &x when x is a local variable?
3. Which approach best reduces manual lifecycle mistakes?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Lifecycle Coach
- Ask AI to build a timeline for declaration, assignment, scope exit and destruction.
- Ask for a scope map that shows shadowed variables and dangling references.
- Ask for a RAII refactor and then check ownership yourself.
Sanitiser Hints
- Ask AI which compiler flags expose uninitialised reads and use-after-free.
- Generate minimal examples that reproduce one lifecycle bug at a time.
Assessment tasks
- Annotate one C++ function with lifecycle stages for every local variable.
- Refactor one raw allocation into a container or smart pointer and explain the ownership change.
- Add a short debugger trace showing where one variable becomes invalid.
Judgement questions
Which lifecycle error are you most likely to miss in review?
Choose one: uninitialised read, shadowing, dangling pointer, use-after-release or mismatched delete. Explain the warning sign you will look for.
When should lifetime be made shorter?
Identify one variable that could be declared closer to first use and explain why that reduces risk.
