Ключевые слова static и extern

Практический курс по 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

То есть, автоматические переменные изначально принимают случайные значения, а статические – нулевые. Мало того, локальные статические переменные инициализируются и формируются в памяти устройства только один раз – в момент запуска программы. А строчка:

static short var_st;

лишь связывает имя var_st с соответствующей областью памяти либо из секции .bss либо из секции .data. Это, своего рода, указатель на заранее выделенную статическую область памяти. В действительности, все имена переменных на уровне машинных кодов заменяются соответствующими адресами памяти. Поэтому имена переменных – это всего лишь некоторая абстракция на уровне языка Си. В машинных кодах они не существуют.

Спрашивается, зачем нужна такая локальная статическая переменная? Первая ее особенность нам уже известна – возможность не занимать память в стековом фрейме, а располагаться в основной памяти устройства. Следовательно, статические переменные могут занимать большие объемы памяти без каких-либо последствий в работоспособности программы. Я напомню, если бы мы захотели локально объявить массив:

double big_ar[1000000];

в функции 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 и объявим в нем глобальную переменную:

int global_var = 5;

А в файле 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 следующим образом:

extern int global_var;

После этого программа скомпилируется, выполнится и выведется ее значение на экран:

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

Видео по теме