Understanding Local Variable Scope and Lifetime
Understand when local variables are created, destroyed, and accessible within blocks.
What Are Local Variables?
A local variable is a variable defined inside a function body, and function parameters count as local variables too. There is no local keyword and no single switch that makes a variable local. The term is shorthand for a bundle of behaviors that such variables share, and the useful way to learn them is to ask what the compiler has to decide about the name you just wrote.
#include <iostream>
int main()
{
int crateCount{ 18 }; // crateCount becomes visible here
double loadWeight{ 62.5 }; // loadWeight becomes visible here
std::cout << crateCount << " crates weighing " << loadWeight << " kg" << '\n';
return 0;
} // both names stop being visible at this closing brace
Output:
18 crates weighing 62.5 kg
Three Questions the Compiler Answers About a Name
Every name you introduce forces three separate decisions. They are easy to confuse because all three are settled by the same line of code, but they answer different questions and they are not even decided at the same time.
| Property | Question it answers | Decided | What a local variable gets |
|---|---|---|---|
| Scope | Where in the source can I write this name? | at compile time | block scope |
| Storage duration | When is the object created and destroyed? | governs run time | automatic duration |
| Linkage | Does this name mean the same object as that other one? | at link time | no linkage |
Two pieces of vocabulary go with the first row. Where a name can be used, it is in scope; anywhere else it is out of scope. Because scope is settled while the program is being compiled, using a name that is out of scope is a compile error, not something that goes wrong later while the program runs.
One more term goes with the second row. Storage duration is the rule; lifetime is the actual stretch of time a particular object exists under that rule. For local variables the rule is simple enough that the two are easy to run together, but they part company for other kinds of variables you will meet later.
Where the Name Can Be Used
Local variables have block scope: the name is usable from the line that defines it down to the closing brace of the enclosing block, and nowhere else. Order matters, so a name is not usable on the lines above its own definition.
Function parameters are not written inside the function body, but for ordinary functions you can treat them as though they were declared at the very top of it. They are in scope for the whole body and they leave scope at its closing brace, alongside everything the body declared.
#include <iostream>
int heavierCrate(int leftKilos, int rightKilos) // leftKilos and rightKilos become visible here
{
int heavier{ (leftKilos > rightKilos) ? leftKilos : rightKilos }; // heavier becomes visible here
return heavier;
} // heavier, rightKilos, and leftKilos all stop being visible at this closing brace
int main()
{
std::cout << "the heavier crate is " << heavierCrate(34, 47) << " kg" << '\n';
return 0;
}
Output:
the heavier crate is 47 kg
Note that the closing brace ends the scope of all three names at once, including the parameters. The return statement does not end anything early; it hands a copy of the value back, and the brace does the rest.
Every pair of braces that encloses statements is a block, whether it belongs to a function, an
if, a loop, or nothing at all. Scope is measured in blocks, so wherever you can put a brace you can control where a name reaches.
Nested Blocks See Outward, Not Inward
Blocks nest, and so does visibility, but only in one direction. A nested block is part of its enclosing block, so names from outside are in scope inside it. The reverse does not hold: a name defined in the nested block is finished at that block's closing brace and cannot be used afterwards.
This program is broken, and the last std::cout line is the reason:
#include <iostream>
int main()
{
int totalKilos{ 40 };
{
int pallet{ 15 };
std::cout << totalKilos + pallet << " kg" << '\n'; // fine: an outer name is visible inside
}
std::cout << pallet << " kg" << '\n'; // the name pallet is not visible out here
return 0;
}
The compiler rejects it:
s.cpp: In function 'int main()':
s.cpp:12:18: error: 'pallet' was not declared in this scope
12 | std::cout << pallet << " kg" << '\n'; // the name pallet is not visible out here
| ^~~~~~
"Was not declared in this scope" is the compiler saying that as far as this point in the program is concerned, the name does not exist. It existed a few lines earlier, and it is spelled correctly, but the block that owned it has closed.
Two Definitions Cannot Share a Block
Within one scope, each name may be defined once. If a name were defined twice in the same block, every use of it would be ambiguous, and there would be no rule the compiler could apply to pick a winner. So it refuses.
Since parameters belong to the function body block, this catches attempts to redefine a parameter inside the body. The following program does not compile:
#include <iostream>
void logCrate(int crateCount)
{
int crateCount{}; // the parameter already claimed this name in this block
std::cout << crateCount << '\n';
}
int main()
{
logCrate(7);
return 0;
}
s.cpp: In function 'void logCrate(int)':
s.cpp:5:9: error: declaration of 'int crateCount' shadows a parameter
5 | int crateCount{}; // the parameter already claimed this name in this block
| ^~~~~~~~~~
s.cpp:3:19: note: 'int crateCount' previously declared here
3 | void logCrate(int crateCount)
| ~~~~^~~~~~~~~~
The rule is about a single block, not about the whole function. The same name may appear in a nested block, and in an unrelated block elsewhere, without any complaint. Those cases are the subject of the next two sections.
When the Object Exists
Local variables have automatic storage duration: the object is created when control reaches the definition and destroyed when control leaves the block. Nothing in your code asks for either event. This is where the older name automatic variables comes from.
The word "when" is doing real work there. A block that runs four times creates and destroys its local variables four times, and each round gets a fresh object rather than the previous one carried over:
#include <iostream>
int main()
{
int totalKilos{ 0 };
for (int pallet{ 1 }; pallet <= 3; ++pallet)
{
int shelf{ pallet * 10 }; // created fresh every time the loop body is entered
totalKilos += shelf;
std::cout << "pallet " << pallet << " adds " << shelf << " kg" << '\n';
} // shelf is destroyed here, once per trip
std::cout << "total " << totalKilos << " kg" << '\n';
return 0;
}
Output:
pallet 1 adds 10 kg
pallet 2 adds 20 kg
pallet 3 adds 30 kg
total 60 kg
shelf has one scope, written once in the source, but three lifetimes, one per pass. That is the clearest illustration of why the two ideas need separate names: scope is a region of text, lifetime is a stretch of running time, and one region of text can produce many stretches of time.
totalKilos survives all three passes because it belongs to the outer block, which is entered once. Had it been defined inside the loop body it would have been created and set back to zero on every pass, and the running total would be lost each time round.
Whether Two Names Mean the Same Object
Linkage answers the third question: when the same name is declared in more than one place, do those declarations refer to one object, or to several unrelated ones?
Local variables have no linkage. Each declaration introduces its own object, and identical spelling means nothing:
#include <iostream>
int main()
{
{
int slot{ 1 };
std::cout << "first block reports " << slot << '\n';
}
{
int slot{ 2 }; // same spelling, completely unrelated object
std::cout << "second block reports " << slot << '\n';
}
return 0;
}
Output:
first block reports 1
second block reports 2
Two definitions, two objects, and no relationship between them. The first slot was already destroyed before the second was created.
It is worth being precise about how this differs from scope, because the two are easy to blur. Scope is about a single declaration and asks how far its name reaches through the source. Linkage is about several declarations and asks whether they are talking about the same thing. Local variables happen to answer "no" to the linkage question always, which is why linkage barely comes up until the lessons on variables declared outside of any function.
Choosing Where to Declare
Scope is something you choose. A variable used only inside a nested block belongs inside that block, where it is visible to the code that needs it and invisible to everything else. Two things improve when you do that: there are fewer names in play at any given point, and a reader who wants to know every place a variable is touched has a smaller region to read.
The counterweight is that a variable must outlive the block it is written from if anything later depends on it. An accumulator is the standard case: it is updated inside a loop body but read after the loop, so it has to be defined outside.
#include <iostream>
int main()
{
int runningTotal{ 0 }; // needed after the loop, so it has to live out here
for (int rowNumber{ 1 }; rowNumber <= 4; ++rowNumber)
{
int rowKilos{ rowNumber * rowNumber }; // used only inside, so it is declared inside
runningTotal += rowKilos;
}
std::cout << "runningTotal is " << runningTotal << '\n';
return 0;
}
Output:
runningTotal is 30
Both variables sit in the smallest scope that still works. rowKilos is finished with by the end of each pass, so it stays inside. runningTotal is read after the loop, so it cannot.
That "smallest scope that still works" phrasing has a limit. It is tempting to wrap a stretch of code in bare braces purely so that some variable dies sooner. That makes the one variable tidier while making the function longer and adding a level of indentation that means nothing to a reader. When a chunk of code feels like it deserves its own little world, the better move is almost always to give it its own function, which limits the scope and gives the chunk a name at the same time.
Define each variable in the smallest scope that already exists. Do not add braces whose only job is to shorten a variable's scope; extract a function instead.
Looking Forward
Nothing here explains what happens when a nested block defines a name that is already in use outside it. That is legal, unlike the parameter collision above, and the inner name hides the outer one for the length of the inner block. The rules and the traps are covered in the lesson on variable shadowing.
The three questions also have other answers. Variables defined outside every function get file scope or global scope instead of block scope, static storage duration instead of automatic, and internal or external linkage instead of none. Once those combinations are on the table, keeping scope, duration, and linkage apart in your head stops being pedantry and starts being how you predict what a program will do.
Key Terminology
Local variable: a variable defined inside a function body, including the function's parameters.
Block: a group of statements enclosed in braces, which is the unit that scope and automatic duration are measured in.
Scope: the region of source code in which a name may be used. A compile-time property, so a violation is a compile error.
Block scope: scope running from a name's definition to the closing brace of the block containing it.
Storage duration: the rule governing when an object is created and destroyed.
Automatic storage duration: creation on reaching the definition, destruction on leaving the block. Local variables have it, which is why they are also called automatic variables.
Lifetime: the actual stretch of running time between one object's creation and its destruction. A single variable in a loop body has one scope but a fresh lifetime per pass.
Linkage: whether declarations of the same name in different scopes refer to the same object. Local variables have no linkage, so every such declaration is its own object.
Summary
What counts as local: variables defined inside a function body, plus the function's parameters. No keyword marks them; the label is shorthand for the three properties below.
Scope: block scope, running from the point of definition to the closing brace of the enclosing block. It is a compile-time property, so using a name out of scope fails the build rather than misbehaving at run time.
Parameters: in scope for the whole function body and out of scope at its closing brace, exactly as if they had been declared on the body's first line.
Nesting: a nested block can use names from the blocks around it, but names it defines itself are gone at its closing brace and cannot be used in the enclosing block.
One definition per block: a name may be defined once per scope, since a second definition would make every use of the name ambiguous. This includes redefining a parameter in the function body.
Storage duration: automatic. The object is created at the definition and destroyed when control leaves the block, with no code from you either way. Re-entering a block creates a fresh object.
Scope against lifetime: scope is a region of text, lifetime is a stretch of running time. A variable in a loop body has one scope and one lifetime per pass.
Linkage: none. Two local declarations spelling the same name are two unrelated objects.
Where to declare: the smallest existing scope that still lets every use reach the variable. Variables read after a block ends must be defined before it starts. Do not invent blocks purely to shrink a scope; write a function instead.
How did you find this lesson?
Your rating helps us improve the content.
Create an account to track your progress and access interactive exercises. Already have one? Sign in.
Understanding Local Variable Scope and Lifetime - Quiz
Test your understanding of the lesson.
Practice Exercises
Local Variable Scope
Understand how local variables are scoped to their blocks and functions, and how inner variables can shadow outer ones.
Lesson Discussion
Share your thoughts and questions
No comments yet. Be the first to share your thoughts!