Lesson overview
This lesson develops variables as part of a program's state model. A variable is not merely a typed box: it is a source-code name with a role, a lifetime, a possible value history and a relationship to correctness. At Masters level, the task is to justify why each value exists, whether it should be mutable, where it should live, what invariant it must preserve and how a reader or debugger can verify that state has remained valid.
Learning objectives
- Distinguish declaration, initialisation, assignment, mutation, scope and lifetime in small C++ examples.
- Trace variable values through a sequence of assignments and identify the first invalid state transition.
- Explain why reading an uninitialised automatic variable in C++ is undefined behaviour, not merely an unpredictable result.
- Justify whether a value should be mutable, const, local, parameterised, renamed, removed or retained as a meaningful intermediate.
- Evaluate competing refactors using evidence about readability, correctness, invariants, scope and failure modes.
Key vocabulary before you start
Declaration
Introduces a source-code name and type, such as int count;.
Initialisation
Gives an object its first valid value, such as int count = 0;.
Assignment
Changes the value of an existing object, such as count = 3;.
Mutation
Any intended change to program state after a value already exists.
Scope
The region of source code where a name can be used.
Lifetime
The period during execution when the object or value exists.
Invariant
A condition that must remain true for later code to be valid.
Undefined behaviour
A C++ program state outside the language guarantees, such as reading some uninitialised automatic variables.
Variable discipline map
A variable introduces a name, a type, a lifetime, a value history and a reasoning obligation. The professional question is whether that obligation buys enough clarity or correctness.
Create for meaning, not habit
A variable is justified when it names a real concept, protects correctness, avoids harmful repetition or makes state easier to inspect.
Control mutation
Use const and narrow scope when a value should not change; allow mutation only where the changing state is part of the algorithm.
Respect compiled reality
Source identifiers guide human reasoning, while optimised machine code may use registers, memory, constants or no stored variable at all.
Protect invariants
A variable is safe only while its value satisfies the assumptions that later code relies on.
Worked example: variables as a state model
The supplied Canvas lesson began a C++ average example. This version keeps the same intent but makes the state model explicit: inputs, derived values, invariant and output.
// Calculate the average of three exam scores
#include <iostream>
int main() {
const int score1 = 78;
const int score2 = 85;
const int score3 = 92;
const int totalMarks = score1 + score2 + score3;
const double averageScore = totalMarks / 3.0;
const bool hasPassed = averageScore >= 50.0;
std::cout << "Average score: " << averageScore
<< " passed? " << (hasPassed ? "yes" : "no") << '\n';
return 0;
}Trace the state
score1,score2andscore3are immutable input state for this calculation.totalMarksis a meaningful derived value: it names the aggregation used to calculate the average.averageScoreis adoublebecause3.0forces floating-point division and preserves fractional information.hasPassedcaptures a domain decision, not just a storage convenience: the pass invariant isaverageScore >= 50.0.- The variables are
constbecause none of these values should change after initialisation in this version.
| Line or operation | Variable | Value | State role | Invariant or check |
|---|---|---|---|---|
const int score1 = 78; | score1 | 78 | Input state | Score is fixed for this calculation. |
const int score2 = 85; | score2 | 85 | Input state | Score is fixed for this calculation. |
const int score3 = 92; | score3 | 92 | Input state | Score is fixed for this calculation. |
const int totalMarks = score1 + score2 + score3; | totalMarks | 255 | Derived state | Must equal the sum of all included scores. |
const double averageScore = totalMarks / 3.0; | averageScore | 85.0 | Derived numerical state | Must preserve fractional information if the total does not divide exactly. |
const bool hasPassed = averageScore >= 50.0; | hasPassed | true | Decision state | Must match the current pass rule. |
Visual model: variable lifecycle and failure points
Use this model to ask where a value first becomes valid, where it is allowed to change, and where it stops being relevant.
DeclareA name and type enter the source-code model. Example: int maxVal;.
Establish valid stateThe first meaningful value is created. Example: std::cin >> maxVal; after checking there is at least one input.
Read or mutateLater code reads the value or deliberately changes it. Reads are safe only if the current invariant still holds.
Detect invalid stateA fault appears when code reads before initialisation, keeps stale state, breaks a domain rule, or allows too wide a scope.
End scopeThe name is no longer available. Narrow scope reduces the places where invalid state can be created or read.
Use the model
Apply the five steps to the maxVal example. Identify the missing step, then explain how first-element initialisation repairs the lifecycle.
Variables as program state
A variable is a source-code entity that names a value or object within a particular scope and lifetime. Some variables are deliberately mutable; others are fixed after initialisation with const. Both forms matter because both shape the program's state model.
Variables help computation progress, but they also create failure modes: assignment to the wrong value, reading before initialisation, reuse for a different meaning, a lifetime that is too long, or a scope that allows too many writes.
At Masters level, the question is not simply what type a variable has. The stronger question is what role it plays in the model of the program and what invariant makes its value valid.
State-classification task
Classify score1, totalMarks, averageScore and hasPassed from the worked example as input state, derived state, decision state or output-supporting state. For each one, state whether mutation would improve or weaken the design, and justify your answer with one sentence of evidence.
Use fewer variables only when fewer variables preserve meaning
A redundant variable adds state without adding meaning. It may become stale, be read by mistake or hide the real relationship between values.
However, the rule is not 'fewer variables are always better'. A single-use variable may be worth keeping if it names a domain concept, prevents a repeated expression from drifting, clarifies a boundary condition or gives the debugger a useful checkpoint.
Professional judgement is the ability to defend the trade-off: does this name reduce the reader's reasoning burden more than it increases the program's state surface?
Design judgement task
Compare const double averageScore = totalMarks / 3.0; with printing totalMarks / 3.0 directly. Give one argument for keeping the variable and one argument for removing it. Conclude which choice is stronger in this lesson's exam-score domain.
Declaration, initialisation and assignment are different operations
A declaration introduces a name and type. Initialisation gives the object its first value. Assignment changes the value of an existing object. Confusing these operations leads to weak explanations and, in C++, real defects.
For an automatic local variable such as int maxVal;, no useful integer value is created by default. Reading it before initialisation is undefined behaviour. That is stronger than saying the program reads 'garbage': the C++ program has stepped outside the language's guarantees.
Good variable design therefore starts by asking: where is the first valid value established, and what later operations are allowed to change it?
Code-reading artefact
Identify which line declares, initialises, assigns and first reads each variable.
int maxVal; // declaration only
int count = 0; // declaration with initialisation
std::cin >> count; // assignment through input
if (count > maxVal) // first read of maxVal: unsafe
{
maxVal = count;
}Invalid-state task
Complete a two-column table: line of code, state change or state read. Mark the first operation that is not justified by a valid prior state, then rewrite the snippet using first-element initialisation or an explicit sentinel with a stated precondition.
Meaningful identifiers reduce reasoning risk
An identifier is a source-code name chosen by the programmer. The compiler does not need averageScore to understand the calculation, but a maintainer does.
Clear names reduce the distance between the program and the problem. They make code easier to review because they carry intent at the point where state is read, created or changed.
Poor names are not merely style defects. They can misrepresent whether state is current, validated, temporary, derived, user-supplied or safe to reuse.
Misleading-name task
A variable called average stores a total for six lines before division happens later. Explain the failure mode this creates, then propose two better designs: one using totalMarks followed by averageScore, and one that calculates the average directly.
Variables are source names, not guaranteed runtime boxes
Variables exist as named entities in source code. After compilation, the generated program may represent their values in registers, memory, immediate constants or optimised expressions. With debug symbols, names may remain available to tooling; without them, the running machine code does not need the source identifier.
This matters because source-level reasoning and runtime behaviour are connected but not identical. Optimisation can remove a variable that was useful to the programmer, while low-level memory errors can corrupt state that looked separate in source code.
A professional programmer therefore learns to reason at two levels: the readable source-code model and the lower-level state changes created by compilation, execution and optimisation.
Representation task
Explain why averageScore is useful in the source code even if an optimising compiler could keep the value in a register or fold part of the calculation. Your answer must separate human reasoning value from runtime representation.
Variables are one pressure point for correctness
Not every software defect is a variable defect: requirements, algorithms, I/O, concurrency and control flow can also be wrong. But many defects become visible as invalid state.
A variable with broad scope can be changed from too many places. A variable with an unclear name can be misunderstood. A variable with a stale value can make code appear logical while silently carrying an old assumption.
Debugging often means finding the earliest point where expected state and actual state diverge, then explaining why the program allowed that transition.
State divergence task
For the worked average example, assume the printed pass/fail decision is wrong. List the minimal watch list you would use, the expected value or invariant for each variable, and the first transition you would inspect.
Scope and lifetime should match the variable's responsibility
Scope controls where a name can be used. Lifetime controls how long the object or value exists. A variable declared earlier or wider than necessary invites accidental reads and writes.
A narrow scope is not a decoration; it is a correctness tool. It makes illegal states harder to express and helps reviewers see when a variable is no longer relevant.
The strongest C++ habit is to initialise at the point of declaration wherever possible, mark non-changing values as const, and delay declarations until the program can provide a valid first value.
Scope-tightening task
Given double average; int n; int sum = 0; declared before input validation, decide which declarations should move later. State the invariant that must hold before average can be initialised.
Debugging by watching state transitions
Watching variables is useful only when the watch list is tied to a claim about the program. Watching everything creates noise; watching the variables that define the current invariant creates evidence.
The useful debugging question is not merely where the program crashed. It is which state transition first departed from the expected model, and what operation made that transition possible.
A good debugger explanation names the variable, the expected value or range, the actual value, and the line where the invalid transition first appears.
Debugger evidence task
For the maximum-value program in the exercise below, propose a watch list of no more than four variables. For each variable, give the expected value before the first loop iteration and the evidence that would confirm or reject the program's invariant.
Applied case lab
Case 1: The redundant temporary
A student writes int result = score1 + score2 + score3; int sum = result;. Decide whether both variables are needed. If not, explain which name should survive and why.
Model reasoning
A strong answer removes one of the names. sum or totalMarks is defensible because it describes the domain role. Keeping both adds an avoidable stale-state risk unless the two values are about to diverge for a clearly explained reason.
Case 2: The misleading name
A variable named average stores the sum of three marks for several lines before being divided later. Explain why this is a correctness risk even if the program eventually prints the right answer.
Model reasoning
The name makes a false claim about the current state. A maintainer may read or reuse it before the division and believe it already satisfies the average invariant. A safer design uses totalMarks until division, then averageScore.
Case 3: The scope leak
A loop counter, total and temporary input variable are declared at function level even though each is only needed inside one block. Apply V2 and V4 to propose a safer scope.
Model reasoning
The temporary input belongs inside the loop; the loop counter belongs in the for statement; the total belongs outside the loop only if it is accumulated across iterations. The answer should connect each placement to reduced write access and clearer lifetime.
Case 4: The useful single-use variable
A reviewer suggests replacing const bool hasPassed = averageScore >= passThreshold; with the expression directly in the output statement because hasPassed is used once. Decide whether the variable should stay.
Model reasoning
Either answer can be defensible. Keeping hasPassed is stronger if the pass/fail decision is a domain concept that may later be tested, logged or changed. Removing it is reasonable if the expression remains local and obvious. The mark is for the trade-off, not for always minimising names.
Applied task: audit and refactor variable state
Work through the stages in order: first classify and name state, then tighten scope and lifetime, then debug invalid state, then defend a trade-off.
Stage 1: state and naming
A. Spot redundant or misleading state
Which variables are unnecessary, risky or poorly named? Rewrite the program so each variable either names a meaningful concept or supports a necessary state transition.
// Compute average and pass/fail (threshold 50)
#include <iostream>
int main() {
int a = 78; // exam 1
int b = 85; // exam 2
int c = 92; // exam 3
int tmp = a; // suspicious
int total = 0;
total = a + b + c;
double avg; // declared before a valid value exists
avg = total / 3.0;
bool t = true; // defaulted, then conditionally overwritten
if (avg < 50.0) { t = false; }
std::cout << "Average: " << avg << " passed? " << (t ? "yes" : "no") << '\n';
return 0;
}- List each variable's role: input, redundant temporary, accumulator, derived value or decision state.
- Remove or rename variables only when you can justify the effect on readability and correctness.
- Mark values
constwhen mutation would violate the intended state model.
Reveal one possible refactor
// Compute average and pass/fail (threshold 50)
#include <iostream>
int main() {
const int examScore1 = 78;
const int examScore2 = 85;
const int examScore3 = 92;
const int passThreshold = 50;
const int totalMarks = examScore1 + examScore2 + examScore3;
const double averageScore = totalMarks / 3.0;
const bool hasPassed = averageScore >= passThreshold;
std::cout << "Average: " << averageScore << " passed? " << (hasPassed ? "yes" : "no") << '\n';
return 0;
}Model reasoning
A strong answer removes tmp, renames a, b, c, avg and t, and explains that hasPassed is a meaningful decision state even though it is used once. It should not claim that every single-use variable is wrong.
B. Trace declaration, initialisation and assignment
Complete a state table for this snippet. Mark each line as declaration, initialisation, assignment, read, or invalid read.
int count;
std::cin >> count;
int total = 0;
double average;
average = total / static_cast<double>(count);
std::cout << average << '\n';- Identify the precondition required before calculating
average. - Explain why
count == 0breaks the calculation even though every variable has been initialised. - Rewrite the snippet with a guard that establishes a valid invariant before
averageexists.
Reveal one possible refactor
int count;
std::cin >> count;
if (count <= 0) {
std::cout << "No scores supplied.\n";
return 0;
}
const int total = 0;
const double average = total / static_cast<double>(count);
std::cout << average << '\n';Model reasoning
The invalid design point is not an uninitialised variable; it is calculating an average before the domain invariant count > 0 has been established. A good answer separates initialisation safety from domain validity.
Stage 2: scope and lifetime
C. Tighten scope and add const
Move declarations as close as possible to first valid use, mark read-only variables as const, and guard the edge case.
double average;
int n; std::cin >> n;
int sum = 0;
for (int i = 0; i < n; ++i) {
int v; std::cin >> v;
sum += v;
}
average = sum / static_cast<double>(n);
std::cout << "Average: " << average << '\n';- Which variables can be
const, and which must remain mutable? - Where can you narrow scope without hiding useful state?
- What invariant must hold before
averageis declared?
Reveal one possible refactor
int count;
std::cin >> count;
if (count <= 0) {
std::cout << "No scores supplied.\n";
return 0;
}
int totalScore = 0;
for (int index = 0; index < count; ++index) {
int score;
std::cin >> score;
totalScore += score;
}
const double averageScore = totalScore / static_cast<double>(count);
std::cout << "Average: " << averageScore << '\n';Model reasoning
totalScore must remain mutable because it accumulates. score should live inside the loop. averageScore should be declared only after count > 0 is known and can be const.
Stage 3: undefined behaviour and debugging
D. Debug undefined behaviour
The following program has undefined behaviour. Identify the first invalid state transition, then fix it.
#include <iostream>
int main() {
int maxVal; // declaration only: no valid value yet
int count;
std::cin >> count;
for (int i = 0; i < count; ++i) {
int val;
std::cin >> val;
if (val > maxVal) { // reads maxVal before initialisation
maxVal = val;
}
}
std::cout << "Max: " << maxVal << '\n';
return 0;
}- Explain why the first comparison is undefined behaviour rather than a comparison against a random but valid integer.
- Propose two safe initialisation strategies for
maxVal: first element and sentinel. - List a minimal debugger watch list with the expected state before the first comparison.
Reveal one possible refactor
#include <iostream>
int main() {
int count;
std::cin >> count;
if (count <= 0) {
std::cout << "No values supplied.\n";
return 0;
}
int maxVal;
std::cin >> maxVal; // first-element initialisation establishes valid state
for (int i = 1; i < count; ++i) {
int val;
std::cin >> val;
if (val > maxVal) {
maxVal = val;
}
}
std::cout << "Max: " << maxVal << '\n';
return 0;
}Model reasoning
The first invalid transition is the read of maxVal in val > maxVal before maxVal has been initialised. A first-element strategy is usually clearer than a sentinel unless the sentinel is valid for the full input domain.
Stage 4: trade-off judgement
E. Compare two defensible refactors
Both versions can be acceptable. Decide which is better for maintainability and explain your criteria.
Version 1:
std::cout << (totalMarks / 3.0 >= 50.0 ? "pass" : "fail") << '\n';
Version 2:
const double averageScore = totalMarks / 3.0;
const bool hasPassed = averageScore >= passThreshold;
std::cout << (hasPassed ? "pass" : "fail") << '\n';- Give one reason Version 1 may be preferable.
- Give one reason Version 2 may be preferable.
- Choose one version for a codebase where pass rules change often, and justify the design decision.
Model reasoning
Version 1 is concise and avoids names that may not be reused. Version 2 is stronger when the calculation and decision are domain concepts likely to be tested, logged or changed. The best answer depends on the maintenance context.
Quick checks
1. Which statement best distinguishes declaration from initialisation in C++?
2. Why can a single-use variable still be worth keeping?
3. What is the main problem with int maxVal; if (value > maxVal) before assigning maxVal?
4. Why is double averageScore = totalMarks / 3.0; different from int averageScore = totalMarks / 3;?
5. What is the strongest reason to narrow a variable's scope?
AI-augmented practice notes
Use these as prompts for disciplined support, not as permission to outsource judgement.
Variable audit with AI
- Ask AI to classify each variable as input, accumulator, derived value, decision state, temporary or guard.
- Ask for clearer identifiers, then reject suggestions that hide the domain invariant or over-compress meaningful state.
- Ask AI to produce a state table, then verify every row against the actual C++ execution order.
- Challenge AI to give a counterargument: when would this extra variable be worth keeping?
Debugging with variables
- Ask AI to generate a watch list tied to a specific invariant, not a list of every local variable.
- Ask it to identify reads before initialisation, division by zero risks and stale-state risks.
- Ask AI to narrate expected state transitions step by step, then compare expected versus actual in the debugger.
- Treat AI's explanation as a hypothesis: confirm it with code, compiler diagnostics, tests or debugger evidence.
Variable Coach
- Redundancy scan:
List variables assigned once or never read; for each, argue both for removal and for retention. - Naming pass:
Propose clearer identifiers and identify the invariant each name should communicate. - Scope/const pass:
Move declarations to first valid use, mark immutable values const and explain each exception. - Debug plan:
Generate a watch list with expected values and the first invalid transition to inspect.
Assessment tasks
- Produce a state table for the worked average example, showing each variable immediately after declaration or initialisation.
- Refactor one supplied snippet to remove one redundant variable, retain one useful single-use variable and justify both decisions.
- Explain one undefined-behaviour risk caused by uninitialised state and show the first line where the invalid read occurs.
- Write a short design justification comparing two acceptable variable designs using readability, invariant protection and maintainability.
Judgement questions
When is an extra variable a design improvement rather than clutter?
Answer using evidence about domain meaning, invariant protection, debugger visibility or reduction of repeated expressions.
What makes a variable's state invalid?
Use at least two categories: uninitialised state, stale state, domain-invalid state, misleading name, excessive scope or unintended mutation.
How would you defend a watch list during debugging?
Name the invariant being tested, the variables needed to test it, and the first transition where evidence should appear.
