2.4 – Локальная область видимости в C++

Добавлено 10 апреля 2021 в 14:04

Локальные переменные

Параметры функции, а также переменные, определенные внутри тела функции, называются локальными переменными (в отличие от глобальных переменных, которые мы обсудим в следующей главе).

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().

Чтобы лучше понять, как всё это сочетается, давайте проследим эту программу более подробно. Всё происходит в следующем порядке:

  1. выполнение начинается сверху main();
  2. переменная a функции main() создается, и ей присваивается значение 5;
  3. переменная b функции main() создается, и ей присваивается значение 6;
  4. функция add() вызывается со значениями 5 и 6 в качестве аргументов;
  5. переменная x функции add() создается и инициализируется значением 5;
  6. переменная y функции add() создается и инициализируется значением 6;
  7. operator+ вычисляет выражение x + y для получения значения 11;
  8. add() копирует значение 11 обратно в вызывающую функцию main();
  9. y и x функции add() уничтожаются;
  10. функция main() печатает 11 в консоль;
  11. функция main() возвращает 0 операционной системе;
  12. 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

Вот что происходит в этой программе:

  1. выполнение начинается сверху main();
  2. переменная x функции main() создается и инициализируется значением 1;
  3. переменная y функции main() создается и инициализируется значением 2;
  4. std::cout печатает main: x = 1 y = 2;
  5. doIt() вызывается с аргументом 1;
  6. параметр x функции doIt() создается и инициализируется значением 1;
  7. переменная y функции doIt() создается и инициализируется значением 4;
  8. doIt() печатает doIt: x = 1 y = 4;
  9. переменной x функции doIt() присваивается новое значение 3;
  10. std::cout печатает doIt: x = 3 y = 4;
  11. y и x функции doIt() уничтожаются;
  12. std::cout печатает main: x = 1 y = 2;
  13. main() возвращает 0 операционной системе
  14. y и x функции main() уничтожаются.

Обратите внимание, что даже несмотря на то, что значения переменных x и y функции doIt() были инициализированы, или им было присвоено что-то отличающееся от main(), значения x и y функции main() не были затронуты, потому что это разные переменные.

Теги

C++ / CppВремя жизниДля начинающихОбласть видимостиОбучениеПрограммирование

На сайте работает сервис комментирования DISQUS, который позволяет вам оставлять комментарии на множестве сайтов, имея лишь один аккаунт на Disqus.com.

В случае комментирования в качестве гостя (без регистрации на disqus.com) для публикации комментария требуется время на премодерацию.