Lesson 6 of 6
Two related questions with different answers. Scope asks where can I write this name? Lifetime asks when does this object exist? For local variables they mostly coincide, and lesson 8.2 is where they come apart in an important way.
Blocks create scope
A variable is visible from its declaration to the closing brace of the block containing it:
#include <iostream>
int main() {
int outer = 1;
{
int inner = 2;
std::cout << outer << " " << inner << "\n";
}
std::cout << outer << "\n";
return 0;
}
1 2
1
inner was visible inside the nested block; outer was visible in both. After the block closes,
inner no longer exists:
// WILL NOT COMPILE
int main() {
{
int inner = 2;
}
return inner;
}
error: 'inner' was not declared in this scope
That's the error from lesson 2.5 — and "declared inside a different block" is one of its three common causes.
Loops and if create scope too
#include <iostream>
int main() {
for (int i = 0; i < 3; ++i) {
int doubled = i * 2;
std::cout << doubled << " ";
}
std::cout << "\n";
// neither i nor doubled exists here
return 0;
}
0 2 4
i is scoped to the for statement and doubled to its body. This is why lesson 4.3 preferred
for over while with an external counter: the counter can't leak out and be reused by accident.
Each iteration gets a fresh doubled. It's created at the top of the body and destroyed at
the bottom, three times over.
Shadowing
An inner declaration can reuse an outer name. The inner one wins for the rest of that block:
#include <iostream>
int main() {
int value = 10;
std::cout << value << "\n";
{
int value = 20; // shadows the outer one
std::cout << value << "\n";
}
std::cout << value << "\n";
return 0;
}
10
20
10
The outer value was never touched. It was hidden, then revealed again.
This is legal and occasionally useful, but it's a common source of confusion — you edit what you
think is the outer variable and change a temporary instead. -Wshadow (not included in
-Wall -Wextra; add it explicitly) warns about it. Prefer distinct names.
Lifetime: destroyed at the closing brace
For local variables, lifetime follows scope exactly. This matters much more than it sounds, because in C++ objects can run code when they're destroyed:
#include <iostream>
#include <string>
struct Announcer {
std::string name;
Announcer(std::string n) : name(n) {
std::cout << "created " << name << "\n";
}
~Announcer() {
std::cout << "destroyed " << name << "\n";
}
};
int main() {
Announcer first{"first"};
{
Announcer second{"second"};
std::cout << "inside block\n";
}
std::cout << "after block\n";
return 0;
}
created first
created second
inside block
destroyed second
after block
destroyed first
Don't worry about the struct syntax — Module 9 covers it. What matters is the order:
secondis destroyed at the closing brace of its block, automatically.firstis destroyed at the end ofmain.- Objects are destroyed in reverse order of creation.
Nobody wrote any cleanup code. The compiler inserted those destructor calls at exactly the right places, guaranteed, on every exit path — including one taken by an exception.
That guarantee is the foundation of RAII, the idea that makes C++ memory-safe without a garbage collector. Lesson 8.2 makes the argument properly. For now, notice that a closing brace is not merely punctuation: it's where things get cleaned up.
Declare variables where you use them
C required all declarations at the top of a block. C++ doesn't, and you shouldn't:
// avoid
int result;
int count;
// ... thirty lines ...
result = compute();
// prefer
// ... thirty lines ...
int result = compute();
Declaring at first use means the initializer is right there (so nothing is uninitialized, per lesson 3.1), the name's scope is as small as possible, and a reader doesn't have to scroll up to find what a variable is.
Narrow scope is a genuine correctness tool: a variable that doesn't exist can't be misused.
Global variables
A variable declared outside any function is visible to the whole file and lives for the entire program:
#include <iostream>
int counter = 0;
void increment() {
++counter;
}
int main() {
increment();
increment();
std::cout << counter << "\n";
return 0;
}
2
This works, and you should avoid it. Any function can modify a global, so when its value is wrong, the suspect list is every function in the program. Globals also make testing hard (state persists between tests) and are actively dangerous with threads (Module 20).
constexpr or const globals are fine — nobody can change them, so none of those objections
apply.
Try it
Predict the output of this program before running it, then check:
int x = 1;
{
int x = 2;
{
int x = 3;
}
}
Print x at each of the four points: outer, inside the first block, inside the innermost
block, and after both blocks close.
Module 4 complete
Your programs can now make decisions and repeat work: if/else with braces always, switch
with deliberate fallthrough, three kinds of loop, range-based for with the right &, early
exits with break and continue, and an understanding of where names live and when their values
are destroyed.
That last idea — destruction at the closing brace — is the one to carry forward. It's the seed of RAII, and it's what Part II is built on.
Next up: Module 5 — Functions, where you start giving code a name.
Questions about this lesson
Log in to ask a question.