Masters-level programming lessons for professional software judgement.

Masters Programmes · Masters Software Engineering · Lesson 3

The Lifecycle of a Variable

Understand how variables come into existence, receive values, leave scope and become invalid, then use that lifecycle to prevent common C++ errors.

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.

LevelMasters
ModeSeminar, trace reading and debugger practice
EvidenceA lifecycle trace showing declaration, instantiation, assignment, release and deletion for stack and heap values
SourceConverted from supplied Canvas lesson HTML

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.

L1

Declare deliberately

A declaration binds a name to a type and should be as local as possible.

L2

Instantiate before use

Memory must exist before a value can safely be stored or read.

L3

Assign before reading

An object can exist but still contain no meaningful program value.

L4

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 + assignment

Trace the state

  1. x is introduced with type int.
  2. Storage for x exists, but reading it before assignment would be unsafe.
  3. x = 5 writes a meaningful value into that storage.
  4. When the block ends, x ceases to exist.
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: The Lifecycle of a Variable.
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: 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; // safe

Dangling 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 here

Applied 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 p and what is released at the closing brace.
  • Rewrite so the outer n is 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

  1. Annotate one C++ function with lifecycle stages for every local variable.
  2. Refactor one raw allocation into a container or smart pointer and explain the ownership change.
  3. 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.