Практический курс по C/C++: https://stepik.org/course/193691
Мы продолжаем тему глобальных и локальных переменных. Когда речь шла о глобальной области видимости, то мы говорили, что
неинициализированные переменные располагаются либо в секции .bss, а
инициализированные – в секции .data:
char name[] = "Variables"; /* в секции .data */
double big_ar[1000000]; /* в секции .bss */
int main(void)
{
int var_main; /* автоматическая локальная переменная */
return 0;
}
Причем,
расположение таких переменных в памяти устройства не меняется на всем
протяжении работы программы. О таких переменных говорят, что они статические,
то есть, не меняют свой адрес от запуска программы и до ее завершения. В
отличие от автоматических переменных, адрес которых может меняться, и точно
известен только после их появления в стековом фрейме. То есть, обычные
локальные переменные не статические. Однако, при необходимости, можно объявить
локальную переменную и в то же время статическую. Для этого нужно прописать
ключевое слово static перед типом переменной, например,
следующим образом:
int main(void)
{
int var_main; /* автоматическая локальная переменная */
static short var_st; /* локальная статическая переменная */
return 0;
}
Что значит
локальная статическая переменная? И какими свойствами она обладает? В
действительности такие переменные располагаются или в секции .bss (если не инициализированы)
или в секции .data (если
инициализированы). То есть там же, где и глобальные переменные. Соответственно,
на них распространяются те же правила инициализации, что и на глобальные
переменные: только константами и соответствующими выражениями. Если же
статическая переменная не имеет начального значения, то она равна нулю, так как
секция .bss при запуске
программы заполняется нулями. Мы в этом легко можем убедиться. Выведем их с
помощью функции printf():
printf("var_main = %d, var_st = %d\n", var_main, var_st);
Увидим
результат:
var_main =
3207168, var_st = 0
То есть,
автоматические переменные изначально принимают случайные значения, а
статические – нулевые. Мало того, локальные статические переменные
инициализируются и формируются в памяти устройства только один раз – в момент
запуска программы. А строчка:
лишь связывает
имя var_st с
соответствующей областью памяти либо из секции .bss либо из секции .data. Это, своего
рода, указатель на заранее выделенную статическую область памяти. В
действительности, все имена переменных на уровне машинных кодов заменяются
соответствующими адресами памяти. Поэтому имена переменных – это всего лишь некоторая
абстракция на уровне языка Си. В машинных кодах они не существуют.
Спрашивается,
зачем нужна такая локальная статическая переменная? Первая ее особенность нам
уже известна – возможность не занимать память в стековом фрейме, а
располагаться в основной памяти устройства. Следовательно, статические
переменные могут занимать большие объемы памяти без каких-либо последствий в
работоспособности программы. Я напомню, если бы мы захотели локально объявить
массив:
в функции main(), то программа
бы завершилась аварийно из-за нехватки памяти, отведенной под стековый фрейм.
Однако если прописать перед типом double ключевое слово static:
static double big_ar[1000000];
то никаких
проблем с выполнением такой программы не будет и, кроме того, массив станет
доступен по локальному имени big_ar. А это гораздо
лучше использования глобальной переменной.
Вторая
особенность локальной статической переменной проявляется из-за ее однократной
инициализации: в памяти она создается только один раз и продолжает существовать
даже после завершения функции, в которой была объявлена. Наглядно это можно
показать с помощью такой программы:
#include <stdio.h>
unsigned int counter()
{
static unsigned int cnt = 10;
return ++cnt;
}
int main(void)
{
unsigned int times;
times = counter();
printf("times = %u\n", times);
printf("counter(): %u\n", counter());
printf("counter(): %u\n", counter());
printf("counter(): %u\n", counter());
return 0;
}
Здесь внутри
функции counter() объявляется
локальная статическая переменная cnt с начальным значением 10. В основной
функции main() мы несколько
раз вызываем функцию counter() и получаем следующий вывод:
times
= 11
counter():
12
counter():
13
counter(): 14
О чем говорит
этот результат? О том, что переменная cnt была инициализирована
только один раз значением 10, и при последовательных вызовах функции counter() бралось ее
текущее значение и увеличивалось на единицу. Также это говорит о том, что
переменная cnt не исчезает
после завершения работы функции, а продолжает существовать в памяти устройства.
И мы теперь знаем, что она располагается в секции .data (так как имеет
начальное значение 10).
Ключевое слово extern с глобальными переменными
Давайте теперь посмотрим,
как можно воспользоваться глобальной переменной в другом файле текущего
проекта. Для этого создадим в том же рабочем каталоге файл, допустим, с именем modul.c и объявим в нем
глобальную переменную:
А в файле lessons.c с функцией main() запишем
следующее:
#include <stdio.h>
int main(void)
{
printf("global_var = %d\n", global_var);
return 0;
}
В настройках
компилятора дополнительно пропишем файл modul.c и попробуем
скомпилировать наш проект. Получим ошибку, что переменная global_var в файле lessons.c неизвестна. И
это неудивительно, так как любое объявление глобальных переменных по умолчанию
распространяется только на текущий файл. Чтобы получить доступ к переменной global_var в файле lessons.c нужно дать
описание этой переменной. Делается это с помощью ключевого слова extern следующим
образом:
После этого
программа скомпилируется, выполнится и выведется ее значение на экран:
global_var = 5
Обратите
внимание, что с помощью ключевого слова extern мы не объявляем
переменную global_var, а лишь говорим компилятору, что эта переменная будет
объявлена позже, либо в этом же файле, либо в другом. То есть, это именно описание
переменной, а не объявление. Память под нее здесь не резервируется.
Учитывая все
сказанное, мы можем вначале сформировать описание глобальной переменной, а в
конце файла объявить ее, например, так:
#include <stdio.h>
extern int global_var;
extern char global_str[];
int main(void)
{
printf("global_var = %d\n", global_var);
puts(global_str);
return 0;
}
char global_str[100] = "Hello";
После запуска программы увидим на экране строку «Hello».
И,
обратите внимание, на важный момент. В описании массива global_str не указывается
его размерность и уж, тем более, не прописывается инициализация. Через extern мы лишь задаем
тип и имя переменной, чтобы компилятор корректно сформировал представление этой
переменной в объектном файле. А объявление переменной должно быть в любом
другом месте и модуле проекта. На этапе его сборки линковщик объединит описания
переменных с их объявлениями. Кстати, если в конце убрать объявление переменной
global_str, то,
естественно, получим ошибку на этапе линковки проекта:
undefined
reference to `global_str'
неопределенная ссылка на `global_str'.
Ключевое слово extern с функциями
Аналогичные
описания можно определять и для функций. Мы с вами об этом уже говорили, когда
рассматривали прототипы функций. Здесь я лишь напомню и немного дополню тот
материал.
Если функция
определена в другом модуле, например, в файле modul.c:
#include <stdlib.h>
int global_seed_randint = 0;
int randint(int a, int b)
{
int right = a, left = b;
if(a > b) {
right = b;
left = a;
}
return rand() % (left - right + 1) + right;
}
То для ее
использования в файле lessons.c необходимо также
дать ее описание (записать прототип) следующим образом:
#include <stdio.h>
int randint(int, int);
int main(void)
{
int a = 1, b = 10;
printf("%d\n", randint(a, b));
printf("%d\n", randint(a, b));
printf("%d\n", randint(a, b));
return 0;
}
Обратите
внимание, имена параметров в прототипах указывать не обязательно. Компилятор
все равно их игнорирует. Ему здесь важна лишь сигнатура функции: возвращаемый
тип, имя функции и типы параметров. Этого достаточно, чтобы сформировать вызов
функции на уровне машинных кодов. А адрес ее вызова подставится позже на этапе
линковки проекта.
Итак,
получается, что для описания связи с глобальной переменной необходимо
прописывать ключевое слово extern:
extern int global_seed_randint;
а в прототипе
функции его нет. Почему? На самом деле его можно добавить и там:
extern int randint(int a, int b);
Но здесь оно
необязательно, т.к. компилятор легко может различить прототип от полного
объявления функции. А вот с переменной лучше явно указывать extern.
Ключевое слово static с глобальными переменными
А что если мы бы
хотели ограничить область видимости глобальной переменной только текущим
модулем. Например, сделать так, чтобы переменная global_seed_randint была доступна только
в пределах файла modul.c? Для этого
достаточно прописать ключевое слово static при объявлении
этой переменной:
static int global_seed_randint;
Тогда при
попытке ее использовать в файле lessons.c произойдет
ошибка на этапе линковки проекта:
#include <stdio.h>
extern int global_seed_randint;
int randint(int a, int b);
int main(void)
{
int a = 1, b = 10;
printf("global_seed_randint = %d\n", global_seed_randint);
printf("%d\n", randint(a, b));
printf("%d\n", randint(a, b));
printf("%d\n", randint(a, b));
return 0;
}
То есть,
ключевое слово static с глобальными переменными играет совсем
другую роль, чем с локальными. Если локальные из класса автоматических
переменных переходили в класс статических, то с глобальными оно лишь
ограничивает их область видимости в пределах текущего модуля. Вот это нужно
четко понимать и запомнить.
По аналогии дела
обстоят и с функциями. Если при ее реализации прописать ключевое слово static (в файле modul.c):
static int randint(int a, int b)
{
...
}
то использовать
ее можно только в текущем модуле и нельзя за его пределами. В программировании
такое поведение называют внутренним связыванием. И, наоборот, если
переменную или функцию можно использовать в других модулях, то говорят, что они
имеют внешнее связывание.
Заключение
Надеюсь, из
последних занятиях вы стали хорошо себе представлять, как объявляются
глобальные и локальные переменные, чем они принципиально отличаются друг от
друга, где и как размещаются в памяти. Также знаете роль ключевых слов static и extern, применительно
к переменным и функциям.
Практический курс по C/C++: https://stepik.org/course/193691