Local variables

In the Introduction to local scope lesson, we introduced local variables, which are variables defined inside a function (including function parameters).

C++ doesn't actually have a single defining characteristic that makes a variable "local". Instead, local variables possess several distinct properties that differentiate their behavior from other types of (non-local) variables. We'll explore these properties in this and subsequent lessons.

In the Introduction to local scope lesson, we also introduced the concept of scope. An identifier's scope determines where within the source code that identifier can be accessed. When an identifier is accessible, we say it is in scope. When it cannot be accessed, we say it is out of scope. Scope is a compile-time property, and attempting to use an identifier when it is out of scope results in a compile error.

Local variables have block scope

Local variables have block scope, which means they are in scope from their point of definition until the end of the block in which they are defined.

Related content: Please review the Compound statements (blocks) lesson if you need a refresher on blocks.

int main()
{
    int temperature{72}; // temperature enters scope here
    double pressure{14.7}; // pressure enters scope here

    return 0;
} // pressure and temperature go out of scope here

Although function parameters aren't defined inside the function body, for typical functions they can be considered part of the scope of the function body block.

int calculateMinimum(int first, int second) // first and second enter scope here
{
    // determine the smaller of first or second
    int minimum{(first < second) ? first : second}; // minimum enters scope here

    return minimum;
} // minimum, second, and first leave scope here

Variable names within a scope must be unique

Variable names must be unique within a given scope, otherwise any reference to that name becomes ambiguous. Consider this program:

void someFunction(int temperature)
{
    int temperature{}; // compilation failure due to name collision with function parameter
}

int main()
{
    return 0;
}

This program doesn't compile because the variable temperature defined inside the function body and the function parameter temperature have identical names and both exist in the same block scope.

Local variables have automatic storage duration

A variable's storage duration (commonly just called duration) determines what rules govern when and how a variable will be created (instantiated) and destroyed. In most cases, a variable's storage duration directly determines its lifetime.

Related content: We discuss what a lifetime is in the Introduction to local scope lesson.

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 in which they are defined. For example:

int main()
{
    int temperature{72}; // temperature created and initialized here
    double pressure{14.7}; // pressure created and initialized here

    return 0;
} // pressure and temperature are destroyed here

Because of this property, local variables are sometimes called automatic variables.

Local variables in nested blocks

Local variables can be defined inside nested blocks. This behaves identically to local variables in function body blocks:

int main() // outer block
{
    int score{100}; // score enters scope and is created here

    { // nested block
        int bonus{25}; // bonus enters scope and is created here
    } // bonus goes out of scope and is destroyed here

    // bonus cannot be used here because it is out of scope in this block

    return 0;
} // score goes out of scope and is destroyed here

In the example above, variable bonus is defined inside a nested block. Its scope is limited from its point of definition to the end of that nested block, and its lifetime matches this scope. Since the scope of variable bonus is restricted to the inner block where it's defined, it cannot be accessed 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 accessed inside a nested block:

#include <iostream>

int main()
{ // outer block

    int score{100}; // score enters scope and is created here

    { // nested block
        int bonus{25}; // bonus enters scope and is created here

        // score and bonus are both in scope here
        std::cout << score << " + " << bonus << " = " << score + bonus << '\n';
    } // bonus goes out of scope and is destroyed here

    // bonus cannot be used here because it is out of scope in this block

    return 0;
} // score goes out of scope and is destroyed here

Local variables have no linkage

Identifiers have another property called linkage. An identifier's linkage determines whether a declaration of that same identifier in a different scope refers to the same object (or function).

Local variables have no linkage. Each declaration of an identifier with no linkage refers to a unique object or function.

For example:

int main()
{
    int count{10}; // local variable, no linkage

    {
        int count{20}; // this declaration of count refers to a different object than the previous count
    }

    return 0;
}

Scope and linkage may appear similar at first glance. However, scope determines where a declaration of a single identifier can be seen and used in the code. Linkage determines whether multiple declarations of the same identifier refer to the same object or not.

Related content: We discuss what happens when variables with the same name appear in nested blocks in the Variable shadowing (name hiding) lesson. Linkage isn't particularly interesting in the context of local variables, but we'll explore it more in the next few lessons.

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:

#include <iostream>

int main()
{
    // do not define bonus here

    {
        // bonus is only used inside this block, so define it here
        int bonus{50};
        std::cout << bonus << '\n';
    }

    // otherwise bonus could still be used here, where it's not needed

    return 0;
}

By limiting a variable's scope, you reduce program complexity because the number of active variables is minimized. Furthermore, 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 programs easier to understand.

If a variable is needed in an outer block, it must be declared in the outer block:

#include <iostream>

int main()
{
    int bonus{50}; // we're declaring bonus here because we need it in this outer block later

    {
        int input{};
        std::cin >> input;

        // if we declared bonus here, immediately before its actual first use...
        if (input == 10)
            bonus = 100;
    } // ... it would be destroyed here

    std::cout << bonus; // and we need bonus to exist here

    return 0;
}

The example above demonstrates 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 individual 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 placed 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.

Summary

  • Local variables: Variables defined inside a function (including function parameters)
  • Block scope: Local variables are in scope from their point of definition until the end of the block in which they're defined
  • Scope vs accessibility: When an identifier is accessible, it's "in scope"; when it cannot be accessed, it's "out of scope"
  • Compile-time property: Scope is determined at compile time; using an out-of-scope identifier causes a compile error
  • Unique names in scope: Variable names must be unique within a given scope to avoid ambiguity
  • Automatic storage duration: Local variables are created at their point of definition and destroyed at the end of the block
  • Automatic variables: Local variables are sometimes called "automatic variables" due to their automatic storage duration
  • Nested blocks: Variables defined in nested blocks have scope limited to that nested block
  • Outer block access: Variables from outer blocks can be accessed in nested blocks
  • No linkage: Local variables have no linkage; each declaration refers to a unique object
  • Scope vs linkage: Scope determines where a single identifier can be seen; linkage determines whether multiple declarations refer to the same object
  • Limited scope principle: Define variables in the most limited existing scope to reduce complexity and improve code understanding
  • Avoid artificial nesting: Don't create new blocks solely to limit variable scope; consider extracting code into separate functions instead

Local variables are fundamental to C++ functions, providing automatic memory management through block scope and automatic storage duration. Defining variables in the smallest possible scope reduces program complexity by minimizing the number of active variables at any point, making code easier to understand and maintain.