C++ Programming
Lesson 02 of 24

Lesson 02 · 24 lesson course

2. The Variable

An introduction to how a state is represented in programming

Programming glossary 42 terms mentioned in this lesson

Select any term for a clear definition. The return button brings you back to the exact term link you used.

Abstract translucent vessel holding a luminous value that can change state.

Lesson: Understanding Variables

A variable is an element of your program that represents some state. This state may, and is expected to, change during the execution of your program. Variables are the moving parts of your program — where errors and bugs often emerge. To use them effectively, you must understand both their purpose and their risks.

There are a few rules to consider when creating variables:

  • Only create them if required – The more variables you create, the more opportunity for failure, since each can be misused. Novice programmers often introduce unnecessary variables. You can usually tell when a variable is redundant if it appears on the left-hand side of an assignment operator (=) only once (or not at all!).
  • Give them meaningful identifiers – An identifier is the name you assign to the variable. While many terms in a programming language are predefined, variable names are under your control. Clear, descriptive names reduce errors and make your program easier to understand.
  • Variables are source code only – They exist as named elements in your code, but once compiled they are translated into memory locations and CPU registers. Identifiers disappear during compilation. This means memory itself is “variable,” and even parts of your program not defined as variables can be overwritten by incorrect use of the ones you created.
  • Variables define program correctness – Assuming a program compiles successfully and runs, errors occur only at the point of state changes — in other words, when variables are manipulated. This is why every variable you add is another opportunity for error. Use them sparingly and keep their scope narrow.

This is why variables are central to debugging: they are the “pressure points” of your program. Managing them well leads to correctness, clarity, and performance.


Example: Variables in Action

Here’s a simple C++ program using variables correctly:


// Calculate the average of three exam scores
#include <iostream>
using namespace std;

int main() {
    int score1 = 78;   // meaningful identifier
    int score2 = 85;
    int score3 = 92;

    int sum = score1 + score2 + score3;   // variable with clear purpose
    double average = sum / 3.0;           // avoid reusing "sum" incorrectly

    cout << "Average score: " << average << endl;
    return 0;
}

Notice that every variable has a purpose, a clear name, and is used more than once. If we had introduced an extra variable like int temp = score1; that was never used again, it would add confusion and risk without benefit.

Programming Insight (AI) — Smarter Use of Variables
  • Code review: Ask AI to highlight variables that are created but used only once.
  • Naming help: AI can suggest clearer identifiers (e.g., totalMarks instead of sum).
  • Memory focus: AI can explain how each variable compiles down to memory/registers.
  • Debugging: Paste an error trace into AI and ask which variable transitions to watch first.

Debugging and Variables

Because errors occur when variables change, they become the focal point of debugging. Watching variables, setting breakpoints, and testing edge cases all revolve around monitoring state. A good programmer treats variables as critical checkpoints in their program.

Programming Insight (AI) — Debugging with Variables
  • AI can generate watch lists for your IDE (which variables to track and why).
  • It can scaffold unit tests that drive variables into edge cases (e.g., division by zero, negatives).
  • AI can narrate expected state transitions step by step, so you compare “expected” vs “actual.”

Deep Dive: Why Variables Matter

Think of variables not just as storage, but as the moving parts of your program. Errors almost always happen when values change. A well-chosen, well-placed variable acts like a clearly labeled valve in a system: easy to monitor, easy to repair. A poorly chosen variable is like an unmarked pipe — you don’t know what flows through it, and when it leaks, chaos ensues.

As you progress, remember: fewer, clearer variables make debugging and reasoning much easier.

Mini Exercise — Audit & Refactor Your Variables

Goal: practise spotting redundant variables, tightening scope, choosing clear identifiers, and debugging by watching state changes.

  1. Spot the redundancies
    In the snippet below, which variables are unnecessary or risky? Rewrite it with fewer, clearer variables.
    // Compute average and pass/fail (threshold 50)
    #include <iostream>
    using namespace std;
    
    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 early, uninitialised
        avg = total / 3.0;
        bool t = true;     // <-- defaulted, then overwritten
        if (avg < 50.0) { t = false; }
        cout << "Average: " << avg << "  passed? " << (t ? "yes" : "no") << endl;
        return 0;
    }
    
    • List the variables to remove or rename and explain why.
    • Refactor so each variable has a clear purpose and is used > 1 time (or make it const if it shouldn’t change).
  2. Name for intent
    Improve the identifiers so the code reads like its intention (no comments needed).
    int x = 40; int y = 60; int z = x + y;
    double a1 = z / 2.0; // average?
    bool f = a1 > 50.0;  // pass flag?
    
    • Rename variables to communicate meaning (e.g., leftScore, rightScore, sum, average, passed).
    • Mark values that shouldn’t change as const.
  3. Tighten scope & add const
    Move declarations as close as possible to first use, and mark read-only variables as const.
    double average;
    int n; cin >> n;
    int sum = 0;
    for (int i = 0; i < n; ++i) {
        int v; cin >> v;
        sum += v;
    }
    average = sum / static_cast<double>(n);
    cout << "Average: " << average << "\n";
    
    • Which variables can be const?
    • Where can you narrow scope without hurting readability?
    • What edge case must you guard (hint: value of n)?
  4. Debug by watching state transitions
    The following program occasionally prints the wrong “max”. Identify the bug and the exact variable transition where it happens. Fix it.
    #include <iostream>
    using namespace std;
    
    int main() {
        int maxVal;            // <-- uninitialised
        int count; cin >> count;
        for (int i = 0; i < count; ++i) {
            int val; cin >> val;
            if (val > maxVal)  // compare against garbage on first iteration
                maxVal = val;
        }
        cout << "Max: " << maxVal << "\n";
    }
    
    • Propose two safe initialisation strategies for maxVal (think: first element vs sentinel).
    • List a minimal “watch list” of variables for the debugger, with why each matters.
  5. Edge cases & guards
    Add robust checks so the program behaves predictably for:
    • n == 0 (no inputs): what should “average” be or what should the program report?
    • Negative inputs if they’re not expected: how should the program react?
Programming Insight (AI) — Variable Coach
  • Redundancy scan: “List variables assigned once or never read; suggest removals or merges.”
  • Naming pass: “Propose clearer identifiers; explain each rename in terms of intent.”
  • Scope/const pass: “Move declarations to first use and mark immutable variables const.”
  • Debug plan: “Generate a watch list and expected state transitions for each loop iteration.”

Lesson 2 extension · a variable must represent something

What does the variable remember?

I can recognise int score{120}; as a variable declaration. That is useful, but it is not yet an understanding of the program. What does 120 mean? Which values are permitted? Who may change it, and which later decision will use it? A score, a frame number and a temperature could all be represented by an int, yet confusing one for another would still produce an inappropriate program. The declaration gives the compiler a type. The program must give the value a meaning.

This becomes particularly important when the source was written by somebody else or generated by a system. A plausible identifier and a type do not establish that the state model is correct. I still need to know where the value came from, which operations may change it, what must remain true after each change, and what will fail if the value is wrong. A variable remembers part of the program's state. It also creates a responsibility to account for every permitted transition.

A name is not the value

Consider int score{120};. Four related things are present, and muddling them produces muddled explanations. score is the identifier used in this scope. The type is int. An object of that type exists during execution, and the value currently held by that object is 120. A later assignment may replace 120 while the identifier and type remain unchanged. Therefore, saying that “the variable is 120” is convenient shorthand, but a programmer must be able to give the more precise account when correctness depends upon it.

QuestionAnswer for int score{120};Why it matters
What is it called here?scoreThe identifier lets source code refer to the object.
What operations are permitted?Those available for intThe type constrains representation and valid operations.
What exists during execution?An integer objectThe object has storage and a lifetime.
What does it currently hold?120The value is the state observed at this point in execution.

Initialisation supplies the first value as the object begins its lifetime. Assignment replaces the value of an object that already exists. Both examples contain an equals sign, which makes them look more alike than they are. Read the complete statement:

int score{120};   // definition and initialisation
score = 145;      // assignment to an existing object

A local fundamental object written as int score; has no initial value supplied by the programmer. In C++20, reading that indeterminate value can produce undefined behaviour. The useful rule is straightforward: give a local variable a meaningful first value at its definition. Hoping that every possible route through the program performs an assignment before the first read is not a substitute for establishing the initial state.

Follow the value through the program

The following program contains little syntax, but it has a complete state history. For each assignment, first evaluate the expression on the right using the current values; only then replace the object named on the left. Read the assignment as a transition, not as an isolated line:

#include <iostream>

int main() {
    int score{120};
    const int collectedPoints{25};

    score = score + collectedPoints;
    std::cout << "After collection: " << score << '\n';

    const int penalty{10};
    score = score - penalty;
    std::cout << "After penalty: " << score << '\n';
}
Point reachedscore beforeOperationscore after
InitialisationNo score objectBegin with 120120
Collection120Add 25145
Penalty145Subtract 10135

The two constants provide inputs to the transitions. Marking them const records that these objects are not intended to change after initialisation. The mutable object is reserved for the state that genuinely evolves. This tells the reader more than declaring everything mutable merely because the language permits it.

The expected output is After collection: 145, followed by After penalty: 135. What does that result prove? It supports the trace for these starting values and these two operations. It does not prove that a negative collection is rejected, that overflow is impossible or that every caller changes the score appropriately. If the output differs, the table gives us exact states to compare. Debugging becomes the search for the first transition at which the observed state leaves the required trace.

Who is allowed to change it?

A calculation does not need to be complicated to create a difficult state error. It is enough to let too many parts of the program change the same value. If five functions may alter a score, I must understand five sets of assumptions and every ordering in which those functions can run. Narrow scope, a meaningful identifier and a clear owner reduce the number of places in which the state can become wrong.

The earlier suggestion that a variable assigned only once is necessarily redundant must therefore be used as a question, not a verdict. A named intermediate value may expose meaning, prevent repeated work or make a debugger trace easier to understand. Conversely, a value assigned many times may reveal confused responsibility. Count assignments if the count points towards something worth inspecting. Decide by asking whether the object represents a necessary value and whether its permitted changes are clear.

Suppose the score starts at zero and the player collects ten points. I put a breakpoint on the assignment and watch the value become ten. Next I send an event that ought to be rejected. Does the score stay at ten? I then inspect the rendering function. It needs to read the score, but what requirement would justify letting it change the score? This investigation identifies the owner, the permitted transitions and the point at which a lower bound must be enforced. If the answer requires a hunt through five unrelated functions, responsibility for the state is not clear enough.

Programming Insight (AI): obtain a trace, then challenge it

An AI system can produce a value table quickly. Speed is the benefit; correctness is still the question. I give it a score that starts at zero, an accepted ten-point event and a twenty-point deduction that the requirement says must be refused. What value does it show after each line? I calculate the states from the C++, then observe the same transitions in the debugger. The requirement, the trace and the execution must agree. If they do not, the disagreement is useful: the instruction may be incomplete, the table may be wrong, or the program may not implement the requirement. The generated trace helps only when it gives me something precise to verify.

Test the state model

Take one short function from the exercises in this lesson. Choose a variable that changes and write down its intended meaning, initial value, permitted changes and required range. Predict its value after each statement before running the program. Then observe the same value in the debugger and locate the first difference, if one appears. Finally, identify one part of the program that may read the value but has no reason to change it. This is more demanding than recognising the declaration, which is precisely why it teaches more.

Reveal answer

I shall use the score example from this extension and follow score. It represents the player's current score. Its first value is 120, and this example permits two changes: add the collected 25 points, then subtract the 10-point penalty.

Statement reachedRelevant statescore afterwards
int score{120};The score object begins its lifetime with its first value.120
const int collectedPoints{25};A separate read-only input is established. It does not change score.120
score = score + collectedPoints;The old score, 120, is read; 25 is added; the result replaces the old value.145
The first output statementThe stream reads and reports the score. It does not own or change it.145
const int penalty{10};A second read-only input is established.145
score = score - penalty;The old score, 145, is read; 10 is subtracted; the result replaces the old value.135
The second output statementThe stream reports the final score without changing it.135

A debugger should therefore show the sequence 120, 145 and 135. If it does not, the useful observation is the first statement after which the watched value differs from this trace. That is where I would compare the current value, the input to the transition and the operation actually executed.

The source does not state a complete permitted range, so the model answer must supply one rather than pretend that the type decided it. For this exercise I require 0 <= score && score <= 1'000'000. An event whose result would leave that range is rejected and score remains unchanged. C++ also requires the arithmetic result to remain representable by int; a different game may choose a different upper bound, but it must state and enforce that decision just as plainly.

The output statements, and a rendering component in a larger program, have reason to read the score. They have no reason to change it. Reporting state is not ownership of that state.

A variable should leave the reader able to say what is remembered, why it exists, who may change it and what must remain true. That account scales from a five-line example to a large engine. The number of variables grows. The responsibility to explain the state does not disappear with the size of the program.