Как static влияет на глобальные локальные переменные
Перейти к содержимому

Как 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.

Arduino

Что использовать: статические или глобальные переменные

Давайте разберем тонкости использования глобальных и статических переменных в скетчах.

Начнем с примера. Рассмотрим простую задачу: нам нужно отправлять значение температуры с датчика. И, что важно, отправлять нужно только новое значение, если оно изменилось. Такой подход имеет преимущества: экономия трафика, упрощение логики на принимающей стороне. Скетч будет выглядеть примерно так:

#include . int lastTemperature = 0; . void setup() < . >void loop() < int newTemperature = sensor.getTemperature(); if (newTemperature != lastTemperature) < client.sendNewTemperature(newTemperature); lastTemperature = newTemperature; >> 

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

Рассмотрим второй вариант реализации:

#include . void setup() < . >void loop() < static int lastTemperature = 0; int newTemperature = sensor.getTemperature(); if (newTemperature != lastTemperature) < client.sendNewTemperature(newTemperature); lastTemperature = newTemperature; >> 

Разница в том, что для хранения последнего значения температуры мы используем статическую переменную. Что же такое статические переменные и чем они отличаются от обычных локальных переменных? Давайте обратимся к документации:

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

  • Локальная область видимости позволит избежать ошибок
  • Улучшится читаемость и поддерживаемость кода
  • В некоторых случаях код будет работать быстрее
Локальная область видимости

Обратите внимание на первый пример, в котором мы использовали глобальную переменную. Изменить ее значение можно из любой функции скетча. В приведенном примере менее 20 строчек кода и в нем трудно ошибиться, на деле же скетч будет сильно больше и есть много шансов допустить ошибку, перетерев значение глобальной переменной. Локальная же область видимости, во-первых, гарантирует, что значение переменной будет изменено только в текущей функции, а, во-вторых, позволит вынести этот функционал в отдельный класс или библиотеку, и переменная вообще не будет нам мешать.

Чище код

Когда мы читаем чужой или свой же код, написанный насколько лет назад, мы не помним всех его тонкостей. В коде могут полностью отсутствовать комментарии, а имена переменных могут быть не достаточно «говорящими», чтобы сразу понять, для чего эта переменная используется. И если мы видим, что переменная статическая, мы сразу будем знать, что ее значение не меняется нигде, кроме текущей функции. А также мы будем понимать, что ее не просто так объявили статической и она сохраняет какое-то значение между вызовами функции.

Код будет работать быстрее

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

Особенности использования extern и static в C/C++

Интенсивно занимаясь программированием на Си для AVR, в сети приходится искать некоторую информацию. Так получилось и на этот раз. Не помню, что я искал, но натолкнулся на один интересный комментарий на RSDN. К моей теме он не имел никакого отношения, да и к AVR, впринципе, тоже. Там на русском языке нормально описывалось применение модификаторов extern и static в C/C++. Ну уж очень хорошо написано. Толково и доходчиво. Поэтому я не мог пройти мимо и утащил к себе на сайт ( как Плюшкин, блин ), что б перед глазами было, что б не городить в браузере туеву хучу закладок, в которых потом теряешься. Может и еще кому интересно будет, пригодится. Вроде бы ссылки там в тексте были на стандарт С++, но он на английском, а здесь все на великом и могучем. Итак…

[stextbox взаимодействие[/stextbox]

Как указано в стандарте (см. п.п. 3.2/1), единица трансляции не должна содержать более одного определения любой переменной, функции, класса, перечисления и шаблона (в каждой области видимости, естественно). Это — так называемое правило одного определения (One-Definition Rule, ODR). В первую очередь это правило касается определений классов, переменных и функций. Для переменной компилятор по ее определению вычисляет размер и резервирует место в памяти, поэтому при наличии более одного определения в одной области видимости у него «возникают проблемы». Для класса компилятор тоже вычисляет размер, хотя обычно и не резервирует место в памяти — это делается при объявлении объектов класса.

[stextbox caption=»Примечание»]Для статических полей класса память резервируется до первого объявления объекта класса — при определении статического поля.[/stextbox]

Для функции компилятор генерирует код, который, в конечном счете, тоже займет свое место в памяти.
А вот объявлений может быть несколько. Объявление лишь добавляет некоторое имя в данную область видимости и обычно используется для согласования типов. Однако при разделении программы на отдельные модули возникают вопросы о взаимодействии определений и объявлений, прописанных в разных модулях. Естественно, речь не идет о локальных объектах — с ними все ясно.

Межмодульные переменные и функции

Начнем с простых переменных. Допустим, у нас есть два модуля A.cpp и B.cpp. В модуле A определена целая переменная i вне всех классов и функций:

Такая переменная называется глобальной. В файле A она видна от точки определения и до конца файла. Однако в модуле B эта переменная не видна. И если вдруг нам потребуется в модуле B присвоить ей другое значение, у нас возникнут некоторые проблемы. Нельзя просто написать:

В этом случае компилятор при обработке модуля B «не видит» модуль A и ничего не знает об определенной там переменной, поэтому мы получим сообщение о неопределенной переменной. Также нельзя написать:

Такая запись является повторным определением. Компилятор-то «возражать» не станет — он транслирует модули по отдельности, а вот компоновщик будет «воротить нос» и сообщит, что одна и та же переменная определена дважды. Для таких случаев в С++ включено специальное ключевое слово extern. В модуле B надо объявить переменную следующим образом:

После этого можно использовать переменную i в файле B любым разрешенным способом. Например, присвоить новое значение:

Однако попытка совместить объявление с присвоением значения является ошибкой:

extern int i = 1;

Такая запись служит определением, поэтому мы опять получим от компоновщика сообщение о повторном определении.

[stextbox caption=»Примечание»]Хотя ключевое слово extern в стандарте определено как один из четырех классов хранения, проще понимать его как обозначение «внешнего» имени для данного модуля. Имя называется внешним по отношению к модулю, если объект с этим именем не определен в данном модуле.[/stextbox]

Аналогичная картина наблюдается и с функциями. Определение функции включает тело, а объявлением является прототип. Пусть в модуле A определена функция:

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

Слова extern писать не требуется, хотя и не запрещается. Следующие прототипы эквивалентны:

void f(void);
extern void f(void);

Локализация имен в модуле

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

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

static int a = 1;
static void f(void)

Для определенных таким образом имен применяется внутренняя компоновка (internal linkage) — они являются локальными в модуле, где определены. Таким образом, можно говорить, что глобальные имена обладают свойством внешней или внутренней компоновки.

Разберемся с некоторыми подробностями на примере функций. Пусть у нас есть, как обычно, два модуля A.cpp и B.cpp:

//—модуль A.cpp
void f1(void) <. >; // определение глобальной функции
static void f2(void)<. >; // определение локальной функции
//—модуль B.cpp
f1(); // вызов глобальной функции
f2(); // ОШИБКА!! — вызов невидимой функции

Функция f2() не видна в модуле B, поэтому ее вызов ведет к ошибке трансляции.

Имя функции с атрибутом static должно быть уникальным в данном модуле, но может повторяться в других модулях. Более того, локальная в модуле функция (с атрибутом static) перекрывает глобальную функцию в пределах модуля (аналогично тому, как локальная переменная в теле функции перекрывает глобальную). Например, в следующем примере функция f1() в модуле B.cpp перекрывает глобальную функцию, определенную в модуле A.cpp.

//—модуль A.cpp
void f1(void) <. >; // определение глобальной функции
static void f2(void)<. >; // определение локальной функции
f1(); // вызов глобальной функции
//—модуль B.cpp
static void f1(void) <. >; // определение локальной функции
f1(); // вызов локальной функции

Все то же самое относится и к глобальным переменным с атрибутом static.

Атрибут static в данном случае похож на модификатор доступа private, работающий на уровне файла. Налицо два совершенно разных смысла одного ключевого слова: с одной стороны, для локальных переменных static означает класс хранения (в статической памяти); с другой стороны, на уровне модуля атрибут static имеет смысл ограничителя видимости имен. Последнее объявлено устаревшим, поэтому применение атрибута static в таком значении является нежелательным (см. п. D2). Вместо этого для решения проблем локализации в С++ включили пространства имен, которые мы рассмотрим далее.

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

// модуль A.cpp
extern const int a = 2;
// модуль B.cpp
extern const int a;

Тогда компоновщик будет считать, что в модулях A.cpp и B.cpp используется одна и та же целая константа с именем a. Если мы пропустим слово extern в объявлении константы в модуле А.cpp, то получим локализованную константу, и при обработке объявления в модуле В.cpp компоновщик выдаст сообщение о неопределенном имени.

Внутренняя компоновка констант оказывает «медвежью услугу» в шаблонах. Поскольку строковые литералы — это объекты со свойством внутренней компоновки, использовать их в качестве аргумента шаблона не разрешается:

template
class Template < . >;
Template T; // Ошибка!

Нельзя использовать и глобальный указатель:

template
class Template < . >;
char const *s = «Literal»;
Template < s >T; // Ошибка!

Однако глобальный символьный массив использовать можно:

template
class Template < . >;
char const s[] = «Literal»;
Template < s >T; // Ошибки нет!

Для функций, объявленных как подставляемые (с ключевым словом inline), по умолчанию тоже применяется внутренняя компоновка. Функция, определенная в одном модуле, не видна в другом модуле. Даже если определения в модулях абсолютно синтаксически совпадают — это все-таки могут быть разные функции, например:

// модуль A.cpp
const int a = 7;
inline void f(void)
< cout
// модуль B.cpp
const int a = 10;
inline void f(void)
< cout При вызове первой функции в модуле А получим на экране 7f(), а при вызове второй в модуле В на экране появится 10f().

Так же как и определение класса, определение подставляемой функции может быть включено в программу несколько раз — по одному определению в каждом модуле. Чтобы гарантированно иметь в разных модулях одно и то же определение, мы вынесем его в отдельный модуль и подключим этот модуль в нужных файлах с помощью директивы #include — компоновщик против не будет. Естественно, определение не должно зависеть от локализованных имен, как в приведенном ранее примере.

Можно сделать подставляемую функцию глобальной — точно так же, как и константу, указав в определении ключевое слово extern:

// модуль A.cpp
// Определение глобальной inline-функции
extern inline void f(void)

В другом модуле достаточно указать прототип:

// модуль B.cpp
// объявление внешней inline-функции
void f(void);

К прототипу можно добавить спецификаторы extern и inline:

extern inline void f(void);

Как видите, в этом случае можно обойтись без подключения модуля с определением.

Вот такой интересный комментарий. Все разложено по полочкам. Надеюсь вам пригодилось.

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

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