Перейти к содержимому

Как static влияет на глобальные локальные переменные

  • автор:

Статические глобальные переменные

Когда спецификатор static применяется к глобальной переменной, он сообщает компилятору о необходимости создания глобальной переменной, которая будет известна только в файле, где статическая глобальная переменная объявлена. Это означает, что, даже если переменная является глобальной, другие подпрограммы в других файлах не будут знать о ней. Таким образом, не возникает повода для побочных эффектов. В некоторых ситуациях, где локальные статические переменные неприменимы, можно создать раздельно компилируемый файл и использовать его без боязни возникновения побочных эффектов.

Для того, чтобы понять, как можно использовать статические глобальные переменные, пример с генератором последовательности из предыдущего раздела переделан таким образом, что стартовое значение может использоваться для инициализации серии путем вызова второй функции — series_start(). Ниже показан файл, содержащий series(), series_start() и series_num:

/* все должно быть в одном файле * /
static int series_num;

int series(void) ;
void series_start(int seed);

series(void)
series_num = series_num + 23;
return(series_num);
>

/* инициализация series_num */
void series_start (int seed)
series_num = seed;
>

Вызывая series_start() с некоторым известным целым числом, мы инициализируем генератор последовательности. После этого вызов series() приводит к генерации следующего элемента последовательности.

Имена статических локальных переменных известны только функции или блоку кода, в которых они объявлены, а имена статических глобальных переменных известны только в файле, в котором они находятся. Это означает, что если поместить функции series() и series_start() в отдельный файл, то можно использовать данные функции, но нельзя обращаться к переменной series_num. Она спрятана от остального кода программы. Фактически можно даже объявлять и использовать другую переменную, называемую series_num, в программе (в другом файле) и не бояться напутать. В сущности модификатор static разрешает использование функциями переменных, не беспокоя другие функции.

Статические переменные позволяют прятать части программы. Это может привести к большим преимуществам при разработке больших и сложных программ.

Статические локальные переменные

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

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

int count (int i) ;

do count(0);
>
while(!kbhit());
printf(«count called %d times», count (1));
return 0;
>

int count (int i)
static int c=0;

if(i) return c;
else c++;
return 0;
>

Иногда полезно знать, как часто функция вызывается во время работы программы. Это возможно сделать через использование глобальных переменных, но лучшим вариантом является использование функции, сохраняющей информацию внутри себя, как это сделано в функции count(). В данном примере, если count() вызывается со значением 0, то переменная с увеличивается. (Скорее всего в настоящих приложениях функция будет также выполнять некоторую другую полезную работу.) Если count() вызывается с любым другим значением, то она возвращает число сделанных вызовов. Подсчет числа вызовов функции может быть полезен при разработке программы, которая вызывает эти функции достаточно часто, и требуется привлечь к вызовам внимание.

Другим хорошим примером функции, требующей использования статических локальных переменных, является генератор последовательности чисел, создающий новое число, основываясь на старом. Это можно сделать, объявив глобальную переменную. Тем не менее, каждый раз, когда функция используется в программе, следует помнить об объявлении глобальной переменной и постоянно необходимо смотреть: не конфликтует ли переменная с другими, ранее объявленными, переменными. Также использование глобальной переменной приводит к тому, что функцию трудно поместить в библиотеку функций. Лучшим решением является объявление переменной, содержащей сгенерированное число как static, как в нижеприведенном фрагменте.

int series(void)
static int series_num;
series_num = series_num+23;
return(series_num);
>

В данном примере переменная series_num существует между вызовами функций вместо того, чтобы каждый раз создаваться и уничтожаться как обычная локальная переменная. Это означает, что каждый вызов series() может создать новый член серии, основываясь на последнем члене без глобального объявления переменной.

Можно было заметить нечто необычное в функции series(). Статическая переменная series_num не инициализируется. Это означает, что при первом вызове функции series_num имеет значение по умолчанию 0. Хотя это приемлемо для некоторых приложений, большинство генераторов последовательности требуют какую-либо другую стартовую точку. Чтобы сделать это, требуется инициализировать series_num до первого вызова series(), что может быть легко сделано, если series_num является глобальной переменной. Тем не менее, следует избегать использования series_num как глобальной переменной и лучше объявить ее как static. Это приводит ко второму способу использования static.

6.10 – Статические локальные переменные

Термин статический ( static ) – один из самых запутанных терминов в языке C++, в значительной степени потому, что static в разных контекстах имеет разные значения.

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

Мы также обсудили, как ключевое слово static обеспечивает внутреннее связывание глобального идентификатора, что означает, что идентификатор может использоваться только в том файле, в котором он определен.

В этом уроке мы рассмотрим использование ключевого слова static в применении к локальной переменной.

Статические локальные переменные

В уроке «2.4 – Локальная область видимости в C++» видимости вы узнали, что локальные переменные по умолчанию имеют автоматическую продолжительность, что означает, что они создаются в точке определения и уничтожаются при выходе из блока.

Использование ключевого слова static для локальной переменной изменяет ее продолжительность с автоматической на статическую. Это означает, что теперь переменная создается при запуске программы и уничтожается при ее завершении (как глобальная переменная). В результате статическая переменная сохранит свое значение даже после выхода за пределы области видимости!

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

Автоматическая продолжительность (по умолчанию):

#include void incrementAndPrint() < int value< 1 >; // автоматическая продолжительность по умолчанию ++value; std::cout // здесь value уничтожается int main()

Каждый раз, когда вызывается incrementAndPrint() , создается переменная с именем value , которой присваивается значение 1. incrementAndPrint() увеличивает значение до 2, а затем печатает значение 2. Когда incrementAndPrint() завершает работу, переменная выходит из области видимости и уничтожается. Следовательно, эта программа выводит:

2 2 2

Теперь рассмотрим статическую версию этой программы. Единственная разница между этой и приведенной выше программами состоит в том, что мы с помощью ключевого слова static изменили продолжительность локальной переменной с автоматической на статическую.

Статическая продолжительность (с использованием ключевого слова static ):

#include void incrementAndPrint() < static int s_value< 1 >; // статическая продолжительность с помощью ключевого слова static. // Этот инициализатор выполняется только один раз. ++s_value; std::cout // s_value здесь не уничтожается, но становится недоступной, // потому что выходит из области видимости int main()

В этой программе, поскольку 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 int getInteger() < static bool s_isFirstCall< true >; if (s_isFirstCall) < std::cout else < std::cout int i<>; std::cin >> i; return i; > int main() < int a< getInteger() >; int b< getInteger() >; std::cout 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 ; int b< getInteger() >; std::cout ; int d< getInteger() >; std::cout 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 определяет локальную переменную как имеющую статическую продолжительность, что означает, что переменная будет создана только один раз и не будет уничтожена до конца выполнения программы.

Пока смерть не разлучит нас или всё о static в C++

Всем привет. На одном из код-ревью я столкнулся с мыслью, что многие, а чего скрывать и я сам, не то чтобы хорошо понимаем когда нужно использовать ключевое слова static. В данной статье я хотел бы поделиться своими знаниями и информацией по поводу ключевого слова static. Статья будет полезна как начинающим программистам, так и людям, работающим с языком С++. Для понимания статьи у вас должны быть знания о процессе сборки проектов и владение языком С/С++ на базовом уровне. Кстати, static используется не только в С++, но и в С. В этой статье я буду говорить о С++, но имейте в виду, что всё то, что не связано с объектами и классами, в основном применимо и к языку С.

Что такое static?

Static — это ключевое слово в C++, используемое для придания элементу особых характеристик. Для статических элементов выделение памяти происходит только один раз и существуют эти элементы до завершения программы. Хранятся все эти элементы не в heap и не на stack, а в специальных сегментах памяти, которые называются .data и .bss (зависит от того инициализированы статические данные или нет). На картинке ниже показан типичный макет программной памяти.

Где используется?

Ниже приведена схема, как и где используется static в программе.

А теперь я постараюсь детально описать все то, что изображено на схеме. Поехали!

Статические переменные внутри функции

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

#include void counter() < static int count = 0; // строка 4 std::cout int main() < for (int i = 0; i < 10; ++i) < counter(); >return 0; >

Вывод программы:

0123456789

#include void counter() < int count = 0; // строка 4 std::cout int main() < for (int i = 0; i < 10; ++i) < counter(); >return 0; >

Вывод программы:

Если не использовать static в строке 4, выделение памяти и инициализация переменной count происходит при каждом вызове функции counter(), и уничтожается каждый раз, когда функция завершается. Но если мы сделаем переменную статической, после инициализации (при первом вызове функции counter()) область видимости count будет до конца функции main(), и переменная будет хранить свое значение между вызовами функции counter().

Статические объекты класса

Статический объект класса имеет такие же свойства как и обычная статическая переменная, описанная выше, т.е. хранится в .data или .bss сегменте памяти, создается на старте и уничтожается при завершении программы, и инициализируется только один раз. Инициализация объекта происходит, как и обычно — через конструктор класса. Рассмотрим пример со статическим объектом класса.

#include class Base < // строка 3 public: Base() < // строка 5 std::cout ~Base() < // строка 8 std::cout >; void foo() < static Base obj; // строка 14 >// строка 15 int main() < foo(); // строка 18 std::cout

Вывод программы:

Constructor
End of main()
Destructor

В строке 3 мы создаем класс Base с конструктором (строка 5) и деструктором (строка 8). При вызове конструктора либо деструктора мы выводим название метода класса в консоль. В строке 14 мы создаем статический объект obj класса Base. Создание этого статического объекта будет происходить только при первом вызове функции foo() в строке 18.

Из-за того, что объект статический, деструктор вызывается не при выходе из функции foo() в строке 15, а только при завершении программы, т.к. статический объект разрушается при завершении программы. Ниже приведен пример той же программы, за исключением того, что наш объект нестатический.

#include class Base < public: Base() < std::cout ~Base() < std::cout >; void foo() < Base obj; >// строка 15 int main()

Если мы уберем static при создании переменной в функции foo(), то разрушение объекта будет происходить в строке 15 при каждом вызове функции. В таком случае вывод программы будет вполне ожидаемый для локальной переменной с выделенной памятью на стеке:

Constructor
Destructor
End of main()

Статические члены класса

В сравнении с предыдущими вариантами использования, статические члены класса немного сложнее для понимания. Давайте разберемся, почему. Предположим, у нас есть следующая программа:

#include class A < // строка 3 public: A() < std::cout ~A() < std::cout >; class B < // строка 9 public: B() < std::cout ~B() < std::cout private: static A a; // строка 15 (объявление) >; int main() < B b; // строка 19 return 0; >

В нашем примере мы создали класс А (строка 3) и класс В (строка 9) со статическими членами класса (строка 15). Мы предполагаем, что при создании объекта b в строке 19 будет создан объект a в строке 15. Так бы и произошло, если бы мы использовали нестатические члены класса. Но вывод программы будет следующим:

Constructor B
Destructor B

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

#include class A < public: A() < std::cout ~A() < std::cout >; class B < public: B() < std::cout ~B() < std::cout private: static A a; // строка 15 (объявление) >; A B::a; // строка 18 (определение) int main()

Теперь, после того как мы определили наш статический член класса в строке 18, мы можем увидеть следующий результат программы:

Constructor A
Constructor B
Destructor B
Destructor A

Нужно помнить, что член класса будет один для всех экземпляров класса B, т.е. если мы создали три объекта класса B, то конструктор статического члена класса будет вызван только один раз. Вот пример того, о чем я говорю:

#include class A < public: A() < std::cout ~A() < std::cout >; class B < public: B() < std::cout ~B() < std::cout private: static A a; // объявление static int count; // объявление >; A B::a; // определение int B::count = 1; // определение int main()

Вывод программы:

Constructor A
Constructor B1
Constructor B2
Constructor B3
Destructor B3
Destructor B2
Destructor B1
Destructor A

Статические функции

Статические функции пришли в С++ из С. По умолчанию все функции в С глобальные и, если вы захотите создать две функции с одинаковым именем в двух разных .c(.cpp) файлах одного проекта, то получите ошибку о том, что данная функция уже определена (fatal error LNK1169: one or more multiply defined symbols found). Ниже приведен листинг трех файлов одной программы.

// extend_math.cpp int sum(int a, int b)
// math.cpp int sum(int a, int b)
// main.cpp int sum(int, int); // declaration int main()

Для того чтобы исправить данную проблему, одну из функций мы объявим статической. Например эту:

// extend_math.cpp static int sum(int a, int b)

В этом случае вы говорите компилятору, что доступ к статическим функциям ограничен файлом, в котором они объявлены. И он имеет доступ только к функции sum() из math.cpp файла. Таким образом, используя static для функции, мы можем ограничить область видимости этой функции, и данная функция не будет видна в других файлах, если, конечно, это не заголовочный файл (.h).

Как известно, мы не можем определить функцию в заголовочном файле не сделав ее inline или static, потому что при повторном включении этого заголовочного файла мы получим такую же ошибку, как и при использовании двух функций с одинаковым именем. При определении статической функции в заголовочном файле мы даем возможность каждому файлу (.cpp), который сделает #include нашего заголовочного файла, иметь свое собственное определение этой функции. Это решает проблему, но влечет за собой увеличение размера выполняемого файла, т.к. директива include просто копирует содержимое заголовочного файла в .cpp файл.

Статические функции-члены класса (методы)

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

  1. Внутри функции обращаться можно только к статическим членам данных, другим статическим функциям-членам и любым другим функциям извне класса.
  2. Статические функции-члены имеют область видимости класса, в котором они находятся.
  3. Вы не имеете доступа к указателю this класса, потому что мы не создаем никакого объекта для вызова этой функции.
#include class A < public: A() < std::cout ~A() < std::cout static void foo() < // строка 8 std::cout >; int main() < A::foo(); // строка 14 return 0; >

В классе A в строке 8 у нас есть статическая функция-член foo(). В строке 14, мы вызываем функцию используя имя класса и оператор разрешения области видимости и получаем следующий результат программы:

static foo()

Из вывода видно, что никакого создания объекта нет и конструктор/деструктор не вызывается.

Если бы метод foo() был бы нестатическим, то компилятор выдал бы ошибку на выражение в строке 14, т.к. нужно создать объект для того, чтобы получить доступ к его нестатическим методам.

Заключение

В одной статье в интернете я нашел совет от автора – «Используйте static везде, где только можно». Я хотел бы написать, почему так делать не стоит, а стоит использовать только в случае необходимости.

  • Статические переменные медленнее, чем нестатические переменные. Для того, чтобы обратиться к статической переменной, нам нужно сделать несколько дополнительных действий, таких как переход в другой сегмент памяти и проверка инициализации переменной. Чаще всего, быстрее выделить локальную переменную на стеке, чем делать дополнительные действия по использованию статической переменной.
  • Если вы используете многопоточность, то здесь вы должны быть крайне осторожными, т.к. возможна ситуация, когда два и более потока захотят писать в одну статическую переменную. Если вы будете использовать нестатические переменные в функциях, то избежите подобного, т.к. для каждого потока будет создана собственная нестатическая переменная.
  • Ключевое слово static является неотъемлемой частью порождающего шаблона проектирования Singleton, который гарантирует, что будет создан только один экземпляр этого класса. В реализации этого паттерна используется и статический объект, и статическая функция-член. На практике вы можете использовать Singleton для создания объекта трейсера, логгера или любого другого объекта, который должен быть один на всё ваше приложение.
  • Иногда для того, чтобы функция отработала только один раз без хранения предыдущего состояния где-то в объекте, используют статические переменные. Пример вы можете посмотреть в разделе «Статические переменные внутри функции». Но это не очень хороший подход, и может привести к долгим часам поиска ошибки, если вы используете многопоточность.
  • На практике, программисты C++ часто используют статические функции-члены как альтернативу обычным функциям, которые не требуют создания объекта для выполнения ее.

Добавить комментарий

Ваш адрес email не будет опубликован. Обязательные поля помечены *