Локальные переменные во вложенных блоках

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

Видео по теме