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() не были затронуты, потому что это разные переменные.
