2.4 – Локальная область видимости в C++
Локальные переменные
Параметры функции, а также переменные, определенные внутри тела функции, называются локальными переменными (в отличие от глобальных переменных, которые мы обсудим в следующей главе).
int add(int x, int y) // параметры функции x и y являются локальными переменными
{
int z{ x + y }; // z тоже локальная переменная
return z;
}
В этом уроке мы более подробно рассмотрим некоторые свойства локальных переменных.
Время жизни локальной переменной
В уроке «1.3 – Знакомство с переменными в C++» мы обсудили, как определение переменной, например int x;
вызывает создание экземпляра переменной при выполнении этой инструкции. Параметры функции создаются и инициализируются при входе в функцию, а переменные в теле функции создаются и инициализируются в точке определения.
Например:
int add(int x, int y) // x и y создаются и инициализируются здесь
{
int z{ x + y }; // z создается и инициализируется здесь
return z;
}
Естественно возникает последующий вопрос: «Так, когда же будет уничтожена созданная переменная?». Локальные переменные уничтожаются в порядке, обратном созданию, в конце набора фигурных скобок, в котором они определены (или, в случае параметра функции, в конце функции).
int add(int x, int y)
{
int z{ x + y };
return z;
} // z, y и x уничтожаются здесь
Подобно тому, как жизнь человека определяется как время между его рождением и смертью, время жизни объекта определяется как время между его созданием и уничтожением. Обратите внимание, что создание и уничтожение переменных происходит во время работы программы (так называемое время выполнения, или runtime), а не во время компиляции. Следовательно, время жизни – это свойство времени выполнения.
Для продвинутых читателей
Вышеупомянутые правила создания, инициализации и уничтожения являются гарантиями. То есть объекты должны быть созданы и инициализированы не позднее точки определения и уничтожены не ранее закрывающей фигурной скобки в паре, в которой они определены (или, для параметров функции, в конце функции).
На самом деле спецификация C++ дает компиляторам большую гибкость в определении того, когда создаются и уничтожаются локальные переменные. Для оптимизации объекты могут быть созданы раньше или уничтожены позже. Чаще всего локальные переменные создаются при входе в функцию и уничтожаются в порядке, обратном созданию, при выходе из функции. Мы обсудим это более подробно в следующем уроке, когда будем говорить о стеке вызовов.
Вот чуть более сложная программа, демонстрирующая время жизни переменной с именем x
:
#include <iostream>
void doSomething()
{
std::cout << "Hello!\n";
}
int main()
{
int x{ 0 }; // время жизни x начинается здесь
doSomething(); // x все еще жив во время вызова этой функции
return 0;
} // здесь заканчивается время жизни x
В приведенной выше программе время жизни x
проходит от точки определения до конца функции main
. Сюда входит время, затраченное на выполнение функции doSomething
.
Локальная область видимости
Область видимости идентификатора определяет, где идентификатор может быть доступен в исходном коде. Когда к идентификатору можно получить доступ, мы говорим, что он находится в области видимости. Когда к идентификатору невозможно получить доступ, мы говорим, что он вне области видимости. Область видимости – это свойство времени компиляции, и попытка использовать идентификатор, когда он не входит в область видимости, приведет к ошибке компиляции.
Область видимости локальной переменной начинается в точке определения переменной и заканчивается в конце набора фигурных скобок, в которых она определена (или, для параметров функции, в конце функции). Это гарантирует, что переменные не могут использоваться до точки определения (даже если компилятор решит создать их до этого).
Ниже приведена программа, демонстрирующая область видимости переменной с именем x
:
#include <iostream>
// x не входит в область видимости этой функции
void doSomething()
{
std::cout << "Hello!\n";
}
int main()
{
// x здесь нельзя использовать, потому что он еще не входит в область видимости
int x{ 0 }; // x входит в область видимости и теперь может использоваться
doSomething();
return 0;
} // x здесь выходит из области видимости и больше не может использоваться
В приведенной выше программе переменная x
входит в область видимости в точке определения и выходит за пределы области видимости в конце функции main
. Обратите внимание, что переменная x
нигде не входит в область видимости функции doSomething
. Тот факт, что функция main
вызывает функцию doSomething
, в данном контексте не имеет значения.
Обратите внимание, что локальные переменные имеют одинаковые определения для области видимости и времени жизни. Для локальных переменных область видимости и время жизни связаны, то есть время жизни переменной начинается, когда она входит в область видимости, и заканчивается, когда она выходит за пределы области видимости.
Другой пример
Вот чуть более сложный пример. Помните, что время жизни – это свойство времени выполнения, а область видимости – свойство времени компиляции, поэтому, хотя мы говорим о них обоих в одной программе, они применяются в разных точках.
#include <iostream>
int add(int x, int y) // x и y здесь создаются и входят в область видимости
{
// x и y видны / могут использоваться только в этой функции
return x + y;
} // y и x здесь выходят за пределы области видимости и уничтожаются
int main()
{
int a{ 5 }; // a здесь создается, инициализируется и входит в область видимости
int b{ 6 }; // b здесь создается, инициализируется и входит в область видимости
// a и b можно использовать только в этой функции
std::cout << add(a, b) << '\n'; // вызывает функцию add() с x = 5 и y = 6
return 0;
} // b и a здесь выходят за пределы области видимости и уничтожаются
Параметры x
и y
создаются при вызове функции add()
, их можно видеть/использовать только внутри функции add()
, и они уничтожаются в конце add()
. Переменные a
и b
создаются внутри функции main()
, могут быть видны/использованы только внутри функции main()
и уничтожаются в конце main()
.
Чтобы лучше понять, как всё это сочетается, давайте проследим эту программу более подробно. Всё происходит в следующем порядке:
- выполнение начинается сверху
main()
; - переменная
a
функцииmain()
создается, и ей присваивается значение 5; - переменная
b
функцииmain()
создается, и ей присваивается значение 6; - функция
add()
вызывается со значениями 5 и 6 в качестве аргументов; - переменная
x
функцииadd()
создается и инициализируется значением 5; - переменная
y
функцииadd()
создается и инициализируется значением 6; operator+
вычисляет выражениеx + y
для получения значения 11;add()
копирует значение 11 обратно в вызывающую функциюmain()
;y
иx
функцииadd()
уничтожаются;- функция
main()
печатает 11 в консоль; - функция
main()
возвращает 0 операционной системе; b
иa
функцииmain()
уничтожаются.
И мы закончили.
Обратите внимание, что если функция add()
вызывается дважды, параметры x
и y
будут созданы и уничтожены дважды – по одному разу для каждого вызова. В программе с большим количеством функций и вызовов функций переменные часто создаются и уничтожаются.
Функциональное разделение
В приведенном выше примере легко увидеть, что переменные a
и b
отличаются от переменных x
и y
.
Теперь рассмотрим следующую аналогичную программу:
#include <iostream>
int add(int x, int y) // x и y функции add() здесь создаются и входят в область видимости
{
// x и y функции add() видимы / могут использоваться только в этой функции
return x + y;
} // x и y функции add() здесь выходят из области видимости и уничтожаются
int main()
{
int x{ 5 }; // x функции main() здесь создается, инициализируется и входит в область видимости
int y{ 6 }; // y функции main() здесь создается, инициализируется и входит в область видимости
// x и y функции main() видимы / могут использоваться только в этой функции
std::cout << add(x, y) << '\n'; // вызывает функцию add() с x = 5 и y = 6
return 0;
} // x и y функции main() здесь выходят из области видимости и уничтожаются
В этом примере всё, что мы сделали, это изменили имена переменных a
и b
внутри функции main
на x
и y
. Эта программа компилируется и запускается идентично, хотя обе функции, main
и add
, имеют переменные с именами x
и y
. Почему это работает?
Во-первых, мы должны признать, что хотя функции main
и add
имеют переменные с именами x
и y
, это разные переменные. x
и y
в функции main
не имеют ничего общего с x
и y
в функции add
– у них просто одинаковые имена.
Во-вторых, внутри функции main
имена x
и y
относятся к переменным x
и y
с локальной областью видимости main
. Эти переменные можно видеть (и использовать) только внутри main
. Точно так же, внутри функции add
, имена x
и y
относятся к параметрам функции x
и y
, которые можно видеть (и использовать) только внутри add
.
Короче говоря, ни add
, ни main
не знают, что другая функция имеет переменные с такими же именами. Поскольку области видимости не пересекаются, компилятору всегда ясно, какие x
и y
используются в любое время.
Ключевой момент
Имена, используемые для параметров функции или переменных, объявленных в теле функции, видны только в той функции, которая их объявляет. Это означает, что локальные переменные внутри функции могут быть названы независимо от имен переменных в других функциях. Это помогает сохранить независимость функций.
Мы поговорим больше о локальной области видимости и других видах области видимости в следующей главе.
Где определять локальные переменные
Локальные переменные внутри тела функции должны быть определены как можно ближе к их первому использованию:
#include <iostream>
int main()
{
std::cout << "Enter an integer: ";
int x{}; // x определяется здесь
std::cin >> x; // и используется здесь
std::cout << "Enter another integer: ";
int y{}; // y определяется здесь
std::cin >> y; // и используется здесь
int sum{ x + y }; // sum определяется здесь
std::cout << "The sum is: " << sum << '\n'; // и используется здесь
return 0;
}
В приведенном выше примере каждая переменная определяется непосредственно перед первым использованием. Нет необходимости строго придерживаться этого правила – если вы предпочитаете поменять местами строки 5 и 6, ничего страшного.
Лучшая практика
Определяйте ваши локальные переменные как можно ближе к их первому использованию.
Небольшой тест
Вопрос 1
Что напечатает следующая программа?
#include <iostream>
void doIt(int x)
{
int y{ 4 };
std::cout << "doIt: x = " << x << " y = " << y << '\n';
x = 3;
std::cout << "doIt: x = " << x << " y = " << y << '\n';
}
int main()
{
int x{ 1 };
int y{ 2 };
std::cout << "main: x = " << x << " y = " << y << '\n';
doIt(x);
std::cout << "main: x = " << x << " y = " << y << '\n';
return 0;
}
Ответ
main: x = 1 y = 2
doIt: x = 1 y = 4
doIt: x = 3 y = 4
main: x = 1 y = 2
Вот что происходит в этой программе:
- выполнение начинается сверху
main()
; - переменная
x
функцииmain()
создается и инициализируется значением 1; - переменная
y
функцииmain()
создается и инициализируется значением 2; std::cout
печатает main: x = 1 y = 2;doIt()
вызывается с аргументом 1;- параметр
x
функцииdoIt()
создается и инициализируется значением 1; - переменная
y
функцииdoIt()
создается и инициализируется значением 4; doIt()
печатает doIt: x = 1 y = 4;- переменной
x
функцииdoIt()
присваивается новое значение 3; std::cout
печатает doIt: x = 3 y = 4;y
иx
функцииdoIt()
уничтожаются;std::cout
печатает main: x = 1 y = 2;main()
возвращает 0 операционной системеy
иx
функцииmain()
уничтожаются.
Обратите внимание, что даже несмотря на то, что значения переменных x
и y
функции doIt()
были инициализированы, или им было присвоено что-то отличающееся от main()
, значения x
и y
функции main()
не были затронуты, потому что это разные переменные.