Практический курс по C/C++: https://stepik.org/course/193691
Мы продолжаем тему областей видимости переменных. Теперь, когда мы в целом узнали, что из себя представляют локальные
и глобальные переменные, давайте более детально разберем поведение локальных
переменных.
В языке Си
операторные фигурные скобки образуют свой собственный независимый блок данных
со своим набором локальных переменных. Например, в функции main() можно
прописать в теле функции еще одни фигурные скобки и в них объявить внутреннюю
переменную
b:
#include <stdio.h>
int main(void)
{
int a = 1;
{
int b = 2;
printf("a = %d, b = %d\n", a, b);
}
printf("a = %d\n", a);
return 0;
}
Как следует
воспринимать такую конструкцию? Смотрите, в теле функции определена локальная
переменная с именем a и начальным значением 1. При этом, в стековом
фрейме только эта переменная и появляется. Затем, когда выполнение доходит до
внутреннего блока фигурных скобок, в стековом фрейме появляется еще одна
переменная b.
Соответственно, обе переменных существуют и могут быть выведены на экран с
помощью функции printf(). После завершения внутреннего блока,
все данные связанные с ним в стековом фрейме пропадают и, следовательно,
перестает существовать и переменная b. Поэтому вторая функция printf() может вывести
только одну переменную a. После запуска программы увидим
следующий результат:
a
= 1, b = 2
a = 1
Если же
попытаться вывести значение переменной b за пределами
вложенного блока, например:
printf("a = %d, b = %d\n", a, b);
то получим
ошибку на этапе компиляции программы, говорящей, что переменная b не существует.
Такая логика
работы довольно полезна, когда во внутренних блоках нужно объявить временные
переменные, которые не нужны за его пределами. Например, в функции main() объявлены
две локальные переменные a и b и мы бы хотели,
чтобы большее значение было в переменной a, а меньшее – в
переменной b. Реализовать
это можно следующим образом:
#include <stdio.h>
int main(void)
{
int a = 1, b = 3;
if(a < b)
{
int t = a;
a = b;
b = t;
}
printf("a = %d, b = %d\n", a, b);
return 0;
}
Здесь внутренний
блок срабатывает только при истинности условия и в нем объявляется
вспомогательная временная переменная t, которая существует только в нем
и недоступна за его пределами. Это очень удобно, учитывая, что такая переменная
вполне может быть за пределами внутреннего блока и играть свою собственную
роль. Например:
#include <stdio.h>
int main(void)
{
int a = 1, b = 3;
int t = a + b;
if(a < b)
{
int t = a;
a = b;
b = t;
}
printf("a = %d, b = %d, t = %d\n", a, b, t);
return 0;
}
Смотрите, как
здесь все работает. Вначале в функции main() определены
три локальных переменных a, b и t. Затем, по
условию отрабатывает внутренний блок, в котором объявляется переменная с тем же
именем t. Однако это имя
связано с совсем другой областью памяти, которая отводится в стековом фрейме. И
изменение этой переменной никак не скажется на значении другой переменной с тем
же именем t. В
программировании такой эффект называется сокрытием переменной. Действительно,
когда выполнение внутреннего блока завершается, все данные, связанные с ним в
стековом фрейме, исчезают, а имя t теперь будет вести на прежнюю область
памяти со значением a+b. Именно оно выводится функцией printf():
a = 3, b = 1, t
= 4
Разумеется, если
бы мы не объявляли внутреннюю переменную t, то имя t вело бы к
переменной из внешнего блока – из тела функции main(). Тогда
значение этой переменной изменилось бы.
Собственные блоки операторов if, while, for, do-while
В языке Си
стандарта C99 операторы if, while, for, do-while и некоторые
другие образуют свои собственные блоки. Это можно показать на следующем
примере:
#include <stdio.h>
int main(void)
{
int t = 3;
while(t-- > 0) {
int t = 10;
t--;
printf("t = %d\n", t);
}
printf("main: t = %d\n", t);
return 0;
}
Здесь в условии
цикла while используется
переменная t из функции main(), а в теле
цикла – новая переменная с тем же именем t. В результате,
цикл сработает ровно три раза и выведет строчки:
t
= 9
t
= 9
t
= 9
main: t = -1
Возможно, здесь
все достаточно очевидно. Но вот менее очевидный пример с оператором цикла for:
#include <stdio.h>
int main(void)
{
int t = 33;
for(int t = 0; t < 3; ++t) {
printf("t = %d\n", t);
}
printf("main: t = %d\n", t);
return 0;
}
Здесь оператор for образует свой
вложенный блок с переменной t, которая, затем, используется в его
теле цикла. После завершения цикла на экран выводится значение прежней
переменной t функции main:
t = 0
t = 1
t = 2
main: t = 33
Как видим,
локальная переменная t не была изменена
в операторе цикла for. Конечно, если бы мы не делали ее объявление в for:
for(t = 0; t < 3; ++t) ...
то
использовалась бы внешняя переменная t из функции main(). Увидели бы
результат:
t
= 0
t
= 1
t
= 2
main: t = 3
Регистровые переменные
В заключение
этого занятия отмечу еще один способ представления переменных доступный в языке
Си. На самом первом занятии мы с вами видели, что центральный процессор
компьютера сохраняет промежуточные данные в своих регистрах. Эти регистры
физически находятся внутри процессора, а потому с ними очень быстро происходит
обмен данными. Быстрее, чем с ячейками памяти, а значит, быстрее, чем с
переменными.
Так вот, составляя
программу, мы можем попросить компилятор разместить ту или иную переменную
непосредственно в одном из регистров процессора. Разумеется, размер этой
переменной (по числу бит) не должен превышать размер регистров ЦП. Делается это
с помощью ключевого слова register, следующим
образом:
#include <stdio.h>
int main(void)
{
int p = 1;
int n = 7;
for(register int i = 2; i <= n; ++i)
p *= i;
return 0;
}
Конечно,
гарантии того, что переменная i будет соответствовать некоторому
регистру ЦП, нет. Мы лишь выражаем свое желание, чтобы эта переменная стала
регистровой. А поместит ли ее компилятор в регистр или нет зависит от множества
факторов: от наличия свободного регистра; от типа и использования самой
переменной i в программе.
Например, если мы попытаемся получить адрес регистровой переменной, то она
точно не будет в регистре, т.к. у регистров нет адреса и компилятор решит, что
программист что-то напутал и сделает переменную самой обычной.
Во всем
остальном регистровые переменные ведут себя, как обычные локальные автоматические,
то есть, они автоматически создаются внутри блока и исчезают за пределами
блока.
Практический курс по C/C++: https://stepik.org/course/193691