Практический курс по 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 неопределенное,
случайное. Мы в этом с вами уже убеждались на одном из прошлых занятий, но
сделаем это еще раз, выведем ее значение на экран:
После запуска
программы высветилось значение:
3821568
И так со всеми
локальными переменными, объявленными внутри тела функции. Все они размещаются в
стековом фрейме и принимают неопределенное начальное значение.
Также нужно
помнить, что стековый фрейм создается в момент запуска программы и, как
правило, имеет фиксированный, ограниченный размер в памяти устройства. Это
накладывает определенные ограничения на нас, как программистов: локальные
переменные не должны занимать слишком много памяти. В частности, объявлять
внутри функций массивы больших размеров далеко не лучшая практика. Например,
так:
int main(void)
{
int var_main; /* внутренняя переменная */
double big_ar[100000];
return 0;
}
Из-за массива big_ar размер стекового
фрейма сразу сокращается на 800 000 байт! А если добавить еще один порядок
к его размеру:
то стекового
фрейма в моем случае будет и вовсе недостаточно для размещения такого объема
данных! Поэтому логику программы следует продумывать так, чтобы все локальные
переменные играли служебную, вспомогательную роль, а не содержали большие
объемы данных, подменяя собой базы данных.
Глобальные переменные
Несколько иная
картина вырисовывается для переменных, объявленных вне каких-либо функций. Если
мы перенесем наш большой массив во внешний блок кода:
#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