Локальные и глобальные переменные

Практический курс по C/C++: https://stepik.org/course/193691

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

#include <stdio.h>
/* Внешний блок кода */
char name[] = "Variables";
 
int main(void) 
{
    int var_main;  /* внутренняя переменная */
    return 0;
}

Так вот, переменные внутри функций и вне их ведут себя по-разному. Начнем с внутренних переменных.

Локальные переменные

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

Давайте детальнее посмотрим на поведение локальных переменных. Предположим мы имеем объявление целочисленной локальной переменной с именем var_main в функции main(). Эта переменная не инициализирована никаким начальным значением. Спрашивается, чему она равна? В общем случае мы не сможем ответить на этот вопрос, так как переменная var_main будет размещена в некоторой области стекового фрейма и принимать значение из тех данных, которые будут находиться в ячейках этой переменной. Попросту говоря, значение переменной var_main неопределенное, случайное. Мы в этом с вами уже убеждались на одном из прошлых занятий, но сделаем это еще раз, выведем ее значение на экран:

printf("%d\n", var_main);

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

3821568

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

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

int main(void) 
{
    int var_main;  /* внутренняя переменная */
    double big_ar[100000];
 
    return 0;
}

Из-за массива big_ar размер стекового фрейма сразу сокращается на 800 000 байт! А если добавить еще один порядок к его размеру:

double big_ar[1000000];

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

Глобальные переменные

Несколько иная картина вырисовывается для переменных, объявленных вне каких-либо функций. Если мы перенесем наш большой массив во внешний блок кода:

#include <stdio.h>
/* Внешний блок кода */
char name[] = "Variables";
double big_ar[1000000];
 
int main(void) 
{
    int var_main;  /* внутренняя переменная */
    return 0;
}

то программа скомпилируется, запустится и завершится без каких-либо ошибок. С чем это связано? Смотрите. Все переменные из внешнего блока размещаются либо в секции .bss, если не инициализированы начальными значениями, либо в секции .data, если начальная инициализация присутствует:

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

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

#include <stdio.h>
/* Внешний блок кода */
char name[] = "Variables";
int global_var_1 = 5;
int global_var_2 = 5 * 7;
int global_var_3 = sizeof(name) + 1;
 
int main(void) 
{
    int var_main;  /* внутренняя переменная */
    return 0;
}

А вот, например, использовать переменные недопустимо:

int global_var_4 = global_var_1;    // ошибка, нельзя использовать переменные

Хотя, если переменная будет объявлена с ключевым словом const, тогда компилятор поймет, что она неизменяема и подставит вместо нее соответствующее значение (ошибки не будет):

const int global_var_1 = 5;
...
int global_var_4 = global_var_1;   // ok

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

#include <stdio.h>
char name[] = "Variables";
int global_var_1 = 5;
int global_var_2 = 5 * 7;
int global_var_3 = sizeof(name) + 1;
 
int main(void) 
{
    int a = 1;
    int b = a * 2 + global_var_2;
    int size = global_var_3 * 10;
 
    return 0;
}

Как видите, здесь вполне можно использовать обычные переменные, в том числе, и глобальные. Сразу отмечу, что использование глобальных переменных при реализации логики работы программы – это очень плохая практика. Связано это с тем, что значения таких переменных можно менять в любом месте программы, в любой функции текущего файла. Из-за этого поведение функций, использующие глобальные переменные, становится непредсказуемым. А ошибки, к которым они могут приводить, – трудно отслеживаемые. Поэтому в практике программирования на языке Си (и то же касается языка С++) глобальные переменные используются, как правило, для определения набора констант (числовых или строковых) и не более того. Очень редко имеет смысл объявлять переменную как глобальную.

Возможно, здесь у вас возникает вопрос: а как тогда нам в программе хранить данные большого размера, если для этого не стоит использовать стековый фрейм (то есть, локальные переменные) и глобальные переменные? На самом деле для таких целей, когда данные занимают от нескольких сот килобайт и более, лучше самостоятельно выделять нужный объем памяти и, затем, освобождать ее. Это можно сделать с помощью функций malloc() и free(), о которых мы еще будем говорить. А пока главное запомнить, что обычные переменные для представления больших данных, лучше не использовать.

Практический курс по C/C++: https://stepik.org/course/193691

Видео по теме