Студенту >> Язык программирования Си


Области видимости и глобальные данные

Любые имена программы на языке Си могут быть объявлены либо вне всех функций и блоков (то есть, на уровне файла), либо внутри функции или блока. В связи с этим различают файловую или глобальную область видимости (действия) имен и локальную область видимости (область видимости блока или функции).

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

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

После выполнения следующего примера:

       #include <stdio.h>

       int i=1;

       void PrintI(void)
       {
          printf( "i = %d\n", i );
       }

       void main(void)
       {
          int i=10;
          printf( "i = %d\n", i );
          PrintI();
          {
             int i=100;
             printf( "i = %d\n", i );
             PrintI();
          }
          printf( "i = %d\n", i );
          PrintI();
       }

будет напечатано

       i = 10
       i = 1
       i = 100
       i = 1
       i = 10
       i = 1

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

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

Никогда не следует делать глобальными вспомогательные переменные функции, например переменные циклов. В следующей программе использование глобальной переменной цикла приводит к ошибке, которую трудно обнаружить:

       #include <stdio.h>

       int i;

       int Sum(int A[], int n)
       {
          int s = 0; /* Сумма одномерного массива */
          for(i=0; i<; i++) s += A[i];
          return s;
       }

       void main(void)
       {
          int B[3][2] = { { 1, 1 },
                          { 2, 2 },
                          { 3, 3 } };
          int s = 0; /* Сумма двумерного массива */
          for(i=0; i<3; i++) s += Sum(B[i], 2);
          printf("s = %d\n", s);
       }

Никаких ошибок компиляции в программе нет. Если рассматривать все ее функции по отдельности, то ошибка тоже не видна. Но программа дает неверный результат: s = 2. Это происходит потому, что цикл головной программы выполняется всего один раз, так как после возврата из функции Sum(), значение глобальной переменной i будет равно 3. Если описать переменные циклов i внутри каждой функции, то программа станет выдавать правильную сумму элементов двумерного массива: s = 12.

Глобальные данные определяются и им выделяется память в конкретном исходном файле программы на языке Си, который образует модуль. Все функции модуля видят глобальные данные своего модуля и имеют к ним доступ. Функции других модулей тоже могут иметь доступ к глобальным данным первого модуля, однако, они не видят эти данные. Для обеспечения видимости используется специальное описание данных, не выделяющее память для них (то есть описание, а не определение). Это описание должно полностью повторять определение данных, по которому выделяется память, но перед ним должно стоять ключевое слово extern. Например, описание

       extern double Speed;

говорит о том, что в каком-то из модулей программы выделена память под переменную Speed типа double и функции данного модуля могут ее использовать.

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

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

       static int Count;

к глобальной переменной Count будут иметь доступ только функции того модуля, в котором собственно была выделена память под переменную Count.

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

       static double cotan(double x);

функция cotan будет доступна только в том модуле (файле), где она определена, то есть, где находится ее тело.

НАВЕРХ