6.10 – Статические локальные переменные
Термин статический (static
) – один из самых запутанных терминов в языке C++, в значительной степени потому, что static
в разных контекстах имеет разные значения.
В предыдущих уроках мы рассмотрели, что глобальные переменные имеют статическую продолжительность, что означает, что они создаются при запуске программы и уничтожаются при ее завершении.
Мы также обсудили, как ключевое слово static
обеспечивает внутреннее связывание глобального идентификатора, что означает, что идентификатор может использоваться только в том файле, в котором он определен.
В этом уроке мы рассмотрим использование ключевого слова static
в применении к локальной переменной.
Статические локальные переменные
В уроке «2.4 – Локальная область видимости в C++» видимости вы узнали, что локальные переменные по умолчанию имеют автоматическую продолжительность, что означает, что они создаются в точке определения и уничтожаются при выходе из блока.
Использование ключевого слова static
для локальной переменной изменяет ее продолжительность с автоматической на статическую. Это означает, что теперь переменная создается при запуске программы и уничтожается при ее завершении (как глобальная переменная). В результате статическая переменная сохранит свое значение даже после выхода за пределы области видимости!
Показать разницу между автоматической и статической продолжительностями переменных проще всего на примере.
Автоматическая продолжительность (по умолчанию):
#include <iostream>
void incrementAndPrint()
{
int value{ 1 }; // автоматическая продолжительность по умолчанию
++value;
std::cout << value << '\n';
} // здесь value уничтожается
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}
Каждый раз, когда вызывается incrementAndPrint()
, создается переменная с именем value
, которой присваивается значение 1. incrementAndPrint()
увеличивает значение до 2, а затем печатает значение 2. Когда incrementAndPrint()
завершает работу, переменная выходит из области видимости и уничтожается. Следовательно, эта программа выводит:
2
2
2
Теперь рассмотрим статическую версию этой программы. Единственная разница между этой и приведенной выше программами состоит в том, что мы с помощью ключевого слова static
изменили продолжительность локальной переменной с автоматической на статическую.
Статическая продолжительность (с использованием ключевого слова static
):
#include <iostream>
void incrementAndPrint()
{
static int s_value{ 1 }; // статическая продолжительность с помощью ключевого слова static.
// Этот инициализатор выполняется только один раз.
++s_value;
std::cout << s_value << '\n';
} // s_value здесь не уничтожается, но становится недоступной,
// потому что выходит из области видимости
int main()
{
incrementAndPrint();
incrementAndPrint();
incrementAndPrint();
return 0;
}
В этой программе, поскольку s_value
была объявлена как статическая, s_value
создается и инициализируется один раз (при запуске программы). Если бы мы для инициализации s_value
не использовали константное выражение, при запуске программы она была бы инициализирована нулем, а затем, при первом обнаружении определения переменной, инициализировалась бы нашим предоставленным значением инициализации (но при последующих вызовах она не повторно инициализируется).
Когда s_value
выходит из области видимости в конце функции, она не уничтожается. Каждый раз, когда вызывается функция incrementAndPrint()
, значение s_value
остается таким, каким мы ее оставили ранее. Следовательно, эта программа выводит:
2
3
4
Точно так же, как мы используем "g_" для префикса глобальных переменных, для префикса статических (со статической продолжительностью) локальных переменных часто используется "s_".
Одно из наиболее распространенных применений локальных переменных статической продолжительности – генераторы уникальных идентификаторов. Представьте себе программу, в которой есть много похожих объектов (например, игра, в которой на вас нападает множество зомби, или симуляция, в которой вы показываете много треугольников). Если вы заметили дефект, будет практически невозможно определить, с каким объектом возникли проблемы. Однако если каждому объекту при создании присваивается уникальный идентификатор, тогда при дальнейшей отладке объекты будет проще различать.
Сгенерировать уникальный идентификационный номер очень просто с помощью локальной переменной статической продолжительности:
int generateID()
{
static int s_itemID{ 0 };
return s_itemID++; // делает копию s_itemID, увеличивает реальный s_itemID на единицу,
// а затем возвращает значение в копии
}
При первом вызове этой функции она возвращает 0. Во второй раз она возвращает 1. Каждый раз, когда она вызывается, она возвращает число на единицу больше, чем при предыдущем вызове. Вы можете присвоить эти номера своим объектам как уникальные идентификаторы. Поскольку s_itemID
является локальной переменной, другие функции не могут «подделать» ее.
Статические переменные обладают некоторыми преимуществами глобальных переменных (они не уничтожаются до конца работы программы), ограничивая при этом свою видимость до области видимости блока. Это делает их безопасными для использования, даже если вы регулярно меняете их значения.
Чрезмерное использование статических локальных переменных
Рассмотрим следующий код
#include <iostream>
int getInteger()
{
static bool s_isFirstCall{ true };
if (s_isFirstCall)
{
std::cout << "Enter an integer: ";
s_isFirstCall = false;
}
else
{
std::cout << "Enter another integer: ";
}
int i{};
std::cin >> i;
return i;
}
int main()
{
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
return 0;
}
Пример вывода программы:
Enter an integer: 5
Enter another integer: 9
5 + 9 = 14
Этот код делает то, что должен делать, но поскольку мы использовали статическую локальную переменную, мы усложнили понимание кода. Если кто-то прочитает код в main()
, не прочитав реализацию getInteger()
, у него не будет причин предполагать, что эти два вызова getInteger()
делают что-то разное. Но, если разница между этими двумя вызовами больше, чем просто измененная подсказка, это может сбивать с толку.
Допустим, вы нажимаете кнопку +1 на микроволновой печи, и микроволновая печь добавляет к оставшемуся времени 1 минуту. У вас теперь теплая еда, и вы счастливы. Прежде чем достать еду из микроволновки, вы видите за окном кота и секунду наблюдаете за ним, потому что кошки – это круто. Эта секунда оказалась дольше, чем вы ожидали, и когда вы откусываете первый кусок еды, обнаруживаете, что она уже остыла. Нет проблем, просто положите ее обратно в микроволновку и нажмите +1, чтобы включить ее на минуту. Но на этот раз микроволновая печь добавляет только 1 секунду, а не 1 минуту. Это тот случай, когда вы говорите «Я ничего не менял, но теперь она не работает» или «В прошлый раз работало». Если вы делаете то же самое действие снова, вы ожидаете того же поведения, что и в прошлый раз. То же самое и с функциями.
Предположим, мы хотим добавить в калькулятор вычитание, чтобы вывод программы выглядел следующим образом
Addition
Enter an integer: 5
Enter another integer: 9
5 + 9 = 14
Subtraction
Enter an integer: 12
Enter another integer: 3
12 - 3 = 9
Мы могли бы попытаться использовать getInteger()
для чтения следующих двух целых чисел, как мы это делали для сложения.
int main()
{
std::cout << "Addition\n";
int a{ getInteger() };
int b{ getInteger() };
std::cout << a << " + " << b << " = " << (a + b) << '\n';
std::cout << "Subtraction\n";
int c{ getInteger() };
int d{ getInteger() };
std::cout << c << " - " << d << " = " << (c - d) << '\n';
return 0;
}
Но это не сработало, на выходе мы получаем:
Addition
Enter an integer: 5
Enter another integer: 9
5 + 9 = 14
Subtraction
Enter another integer: 12
Enter another integer: 3
12 - 3 = 9
При запросе первого числа для вычитания мы получаем «Enter another integer» (введите другое целое число) вместо «Enter an integer» (введите целое число).
getInteger()
нельзя использовать повторно, потому что у нее есть внутреннее состояние (статическая локальная переменная s_isFirstCall
), которое не может быть сброшено извне. s_isFirstCall
– это не та переменная, которая должна быть уникальной во всей программе. Хотя наша программа отлично работала, когда мы написали ее впервые, статическая локальная переменная не позволяет нам повторно использовать функцию в дальнейшем.
Лучший способ реализовать getInteger
– передать s_isFirstCall
в качестве параметра. Это позволяет вызывающей функции выбрать, какая подсказка будет напечатана.
Статические локальные переменные следует использовать только в том случае, если во всей вашей программе и в обозримом будущем вашей программы переменная уникальна и не имеет смысла эту переменную сбрасывать.
Лучшая практика
Избегайте использования статических локальных переменных, если только переменную не нужно сбрасывать. Статические локальные переменные уменьшают возможность повторного использования и затрудняют понимание функций.
Небольшой тест
Вопрос 1
Как влияет использование ключевого слова static
на глобальную переменную? Как оно влияет на локальную переменную?
Ответ
При применении к глобальной переменной ключевое слово static
определяет глобальную переменную как имеющую внутреннее связывание, что означает, что переменная не может быть экспортирована в другие файлы.
При применении к локальной переменной ключевое слово static
определяет локальную переменную как имеющую статическую продолжительность, что означает, что переменная будет создана только один раз и не будет уничтожена до конца выполнения программы.