Scope, Lifetime, and Storage Duration
Scope of a variable
Scope is simply the region of code where a variable’s name can be used. In C++, a new scope starts every time you open a curly brace { and ends at the matching }.
Here, bonus is scoped to the block on lines 6–9, so uncommenting line 11 will produce an error. Once the block closes, the variable name is no longer meaningful. We say bonus has block scope.
Variables declared inside an if, for, or while body have block scope too, since each of those bodies is itself a block.
for (int i = 0; i < 5; ++i) { int square = i * i; std::cout << square << " ";}Neither i nor square exist outside this for loop.
Function parameters and variables you declare inside a function also have block scope. These are called local variables.
Global scope
Not every variable is confined to a block. A variable declared at the top level of a file, outside of any function or block, has global scope, meaning its name is visible from its declaration all the way to the end of the file. This kind of variable is called a global variable.
PI isn’t declared inside circleArea or main, yet both can use it, since its scope isn’t tied to any single block.
Best practice: Declaring global variables as constants, like
PIabove, is quite common. Global variables that aren’t declaredconstare usually a bad idea, because any function anywhere in the program can change them, which makes it hard to reason about what value a variable holds at any given point.
Shadowing a variable in a nested block
C++ lets you declare a variable with the same name as one from an outer scope, inside a block nested within it. When you do, the inner variable is said to shadow the outer one, meaning any use of that name inside the inner block now refers to the new, inner variable instead of the outer one.
This is occasionally seen for loop variables:
Each inner loop counts its own i independently, and the outer i is unaffected once the inner loop ends.
Best practice: With shadowing, the code becomes harder to understand and you can imagine how it turns into a real problem when you use a variable in the inner scope thinking it’s the variable from the outer scope. It’s best not to reuse a variable name in a nested scope.
Lifetime of a variable
Scope tells you where a name is visible. Lifetime tells you for how long the variable actually exists in memory (when it’s created and when it’s destroyed). For ordinary local variables, scope and lifetime line up exactly. They are created at the time of declaration, and destroyed when the scope ends.
By “destroyed”, we mean the memory is no longer reserved for that variable and is free to be reused. A reference to a variable can still be used after that variable is destroyed, since it still refers to that same memory location. But the validity of data in that location is not guaranteed. For example, it’s possible to use a reference, like std::string&, as a return type:
In a case like this, where the return type is a reference, calling getGreeting(...) doesn’t return the value in greeting. It gives the caller a reference to this variable.
As greeting is a local variable, its lifetime ends the instant getGreeting returns. The caller now holds a reference to a variable that no longer exists. So, using it leads to undefined behavior. Sometimes it’ll look like it still works (accidentally at that), other times it’ll retrieve garbage.
As a rule, never return a reference to a local variable, even though the compiler will allow you to do that.
Lifetime depends on storage duration
When a program runs, it organizes data into different regions of memory. One of these regions is called the stack. Once a function is called, any memory required for its local variables is reserved in this region. When the function returns, that memory is freed, which is why a local variable’s lifetime ends when its function does.
Not all memory works this way. A different region, sometimes called static storage, holds memory that stays reserved for as long as the program runs. Two kinds of variables are stored here: global variables, which you’ve already seen, and static local variables, a local variable marked with the static keyword:
A static local variable is initialized exactly once, the first time execution reaches its declaration, and then it keeps its value between calls, until the program stops running. This is useful for persisting data across multiple function calls, without resorting to a global variable.
An ordinary local variable’s lifetime is called its automatic storage duration. The lifetime of a global or static local variable is called its static storage duration.
Checkpoint
What happens when this code is compiled?
void checkStock(int quantity) {
if (quantity < 10) {
int reorderAmount = 50;
}
std::cout << reorderAmount;
}What does this program print?
#include <iostream>
int score = 0;
void addPoints(int points) {
score += points;
}
void resetGame() {
int score = 0;
}
int main() {
addPoints(3);
resetGame();
addPoints(3);
std::cout << score;
}What gets printed after calling nextInvoiceNumber() three times in a row, printing each result?
int nextInvoiceNumber() {
static int counter = 500;
return counter++;
}