Contact
Evil Genius SocietyHomeLearnForumLeaderboardSocialsMessages
← Back to module

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:

  • second is destroyed at the closing brace of its block, automatically.
  • first is destroyed at the end of main.
  • 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.