In lesson 2.4 -- Introduction to local scope, we introduced local variables
, which are variables that are defined inside a function (including function parameters).
It turns out that C++ actually doesn’t have a single attribute that defines a variable as being a local variable. Instead, local variables have several different properties that help differentiate how local variables behave from other kinds of (non-local) variables. We’ll explore these properties in this and upcoming lessons, as well as a few other local variable related topics of note.
Local variables have block scope
An identifier’s scope determines where an identifier can be accessed within the source code. Scope is a compile-time property.
Local variables have block scope, which means they are in scope from their point of definition to the end of the block they are defined within.
1 2 3 4 5 6 7 |
int main() { int i { 5 }; // i enters scope here double d { 4.0 }; // d enters scope here return 0; } // i and d go out of scope here |
Although function parameters are not defined inside the function body, for typical functions they can be considered to be part of the scope of the function body block.
1 2 3 4 5 6 7 |
int max(int x, int y) // x and y enter scope here { // assign the greater of x or y to max int max = (x > y) ? x : y; // max enters scope here return max; } // x, y, and max leave scope here here |
The exception case is for function-level exception handling (which we cover in lesson %Failed lesson reference, id 5334%).
All variable names within a scope must be unique
Variable names must be unique within a given scope, otherwise any reference to the name will be ambiguous. Consider the following program:
1 2 3 4 5 6 7 8 9 |
void someFunction(int x) { int x{}; // compilation failure } int main() { return 0; } |
The above program doesn’t compile because the variable x
defined inside the function body and the function parameter x
have the same name and both are in the same block scope.
Local variables have automatic storage duration
A variable’s storage duration (usually just called duration) determines what rules govern when and how a variable will be created and destroyed. In most cases, a variable’s storage duration directly determines it’s lifetime
.
For example, local variables have automatic storage duration, which means they are created at the point of definition and destroyed at the end of the block they are defined in. For example:
1 2 3 4 5 6 7 |
int main() { int i { 5 }; // i created and initialized here double d { 4.0 }; // d created and initialized here return 0; } // i and d are destroyed here |
For this reason, local variables are sometimes called automatic variables.
Local variables in nested blocks
Local variables can be defined inside nested blocks. This works identically to local variables in function body block:
1 2 3 4 5 6 7 8 9 10 11 12 |
int main() // outer block { int x { 5 }; // x enters scope and is created here { // nested block int y { 7 }; // y enters scope and is created here } // y goes out of scope and is destroyed here // y can not be used here because it is out of scope in this block return 0; } // x goes out of scope and is destroyed here |
In the above example, variable y
is defined inside a nested block. Its scope is limited from its point of definition to the end of the nested block, and its lifetime is the same. Because the scope of variable y
is limited to the inner block in which it is defined, it’s not accessible anywhere in the outer block.
Note that nested blocks are considered part of the scope of the outer block in which they are defined. Consequently, variables defined in the outer block can be seen inside a nested block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 |
#include <iostream> int main() { // outer block int x { 5 }; // x enters scope and is created here { // nested block int y { 7 }; // y enters scope and is created here // x and y are both in scope here std::cout << x << " + " << y << " = " << x + y; } // y goes out of scope and is destroyed here // y can not be used here because it is out of scope in this block return 0; } // x goes out of scope and is destroyed here |
Local variables have no linkage
Identifiers have another property named linkage
. An identifier’s linkage determines whether other declarations of that name refer to the same object or not.
Local variables have no linkage
, which means that each declaration refers to a unique object. For example:
1 2 3 4 5 6 7 8 9 10 |
int main() { int x { 2 }; // local variable, no linkage { int x { 3 }; // this identifier x refers to a different object than the previous x } return 0; } |
Scope and linkage may seem somewhat similar. However, scope defines where a single declaration can be seen and used. Linkage defines whether multiple declarations refer to the same object or not.
Linkage isn’t very interesting in the context of local variables, but we’ll talk about it more in the next few lessons.
Shadowing
Each block defines its own scope region. So what happens when we have a variable inside a nested block that has the same name as a variable in an outer block? When this happens, the nested variable “hides” the outer variable in areas where they are both in scope. This is called name hiding or shadowing.
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 |
#include <iostream> int main() { // outer block int apples { 5 }; // here's the outer block apples { // nested block // apples refers to outer block apples here std::cout << apples << '\n'; // print value of outer block apples int apples{ 0 }; // define apples in the scope of the nested block // apples now refers to the nested block apples // the outer block apples is temporarily hidden apples = 10; // this assigns value 10 to nested block apples, not outer block apples std::cout << apples << '\n'; // print value of nested block apples } // nested block apples destroyed std::cout << apples << '\n'; // prints value of outer block apples return 0; } // outer block apples destroyed |
If you run this program, it prints:
5 10 5
In the above program, we first declare a variable named apples
in the outer block. This variable is visible within the inner block, which we can see by printing its value (5
). Then we declare a different variable (also named apples
) in the nested block. From this point to the end of the block, the name apples
refers to the nested block apples
, not the outer block apples
.
Thus, when we assign value 10
to apples
, we’re assigning it to the nested block apples
. After printing this value (10
), nested block apples
is destroyed. The existence and value of outer block apples
is not affected, and we prove this by printing the value of outer block apples
(5
).
Note that if the nested block apples
had not been defined, the name apples
in the nested block would still refer to the outer block apples
, so the assignment of value 10
to apples
would have applied to the outer block apples
:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 |
#include <iostream> int main() { // outer block int apples(5); // here's the outer block apples { // nested block // apples refers to outer block apples here std::cout << apples << '\n'; // print value of outer block apples // no inner block apples defined in this example apples = 10; // this applies to outer block apples std::cout << apples << '\n'; // print value of outer block apples } // outer block apples retains its value even after we leave the nested block std::cout << apples << '\n'; // prints value of outer block apples return 0; } // outer block apples destroyed |
The above program prints:
5 10 10
Shadowing should generally be avoided, as it can lead to inadvertent errors where the wrong variable is used or modified. Some compilers will issue a warning when a variable is shadowed.
Best practice
Avoid variable shadowing.
Variables should be defined in the most limited scope
If a variable is only used within a nested block, it should be defined inside that nested block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
#include <iostream> int main() { // do not define y here { // y is only used inside this block, so define it here int y { 5 }; std::cout << y; } // otherwise y could still be used here, where it's not needed return 0; } |
By limiting the scope of a variable, you reduce the complexity of the program because the number of active variables is reduced. Further, it makes it easier to see where variables are used (or aren’t used). A variable defined inside a block can only be used within that block (or nested blocks). This can make the program easier to understand.
If a variable is needed in an outer block, it needs to be declared in the outer block:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
#include <iostream> int main() { int y { 5 }; // we're declaring y here because we need it in this outer block later { int x{}; std::cin >> x; // if we declared y here, immediately before its actual first use... if (x == 4) y = 4; } // ... it would be destroyed here std::cout << y; // and we need y to exist here return 0; } |
The above example shows one of the rare cases where you may need to declare a variable well before its first use.
New developers sometimes wonder whether it’s worth creating a nested block just to intentionally limit a variable’s scope (and force it to go out of scope / be destroyed early). Doing so makes that variable simpler, but the overall function becomes longer and more complex as a result. The tradeoff generally isn’t worth it. If creating a nested block seems useful to intentionally limit the scope of a chunk of code, that code might be better to put in a separate function instead.
Best practice
Define variables in the most limited existing scope. Avoid creating new blocks whose only purpose is to limit the scope of variables.
Quiz time
Question #1
Write a program that asks the user to enter two integers. If the user enters a smaller value for the second integer, use a block and a temporary variable to swap the smaller and larger values. Then print the values of the smaller and larger variables. Add comments to your code indicating where each variable dies.
The program output should match the following:
Enter an integer: 4 Enter a larger integer: 2 Swapping the values The smaller value is 2 The larger value is 4
Question #2
What’s the difference between a variable’s scope, duration, and lifetime? By default, what kind of scope and duration do local variables have (and what do those mean)?
![]() |
![]() |
![]() |
Leave a Reply