LEARNING · STAGE 00 · PROGRAMMING BASICS

Область видимости, ввод и итоговая практика

Последний урок Stage 00 закрывает два оставшихся вопроса — где именно «живёт» переменная и как программе получить данные не из кода, а от пользователя (std::cin) — а затем собирает всё, что было в этапе, в одной итоговой практике.

01Что ты изучишь

  • Область видимости (scope) — где переменная существует, а где о ней уже «не знают»
  • Как параметры функций и переменные внутри блоков соотносятся со scope
  • std::cin — чтение данных, введённых пользователем
  • Итоговую практику, использующую всё из Stage 00 сразу

02Основная идея: комнаты в доме

Область видимости — это как комнаты в доме. Вещь, оставленная в одной комнате (переменная, объявленная в блоке { }), доступна, пока ты находишься в этой комнате или в любой комнате внутри неё (вложенный блок) — но её не видно и не достать из соседней комнаты или из коридора снаружи. Когда ты выходишь из комнаты (блок заканчивается закрывающей }), всё, что было оставлено только в ней, становится недоступным — не потому что «пропало», а потому что ты уже не там, где оно лежит. Именно поэтому в две разные, не вложенные друг в друга комнаты можно положить вещи с одинаковым названием, не создавая путаницы — они просто в разных местах.

03Область видимости — где переменная существует

Перенесём аналогию с комнатами на настоящий код и посмотрим, где именно проходит граница. Правило простое, хоть и не сразу очевидное, пока не увидишь его в действии: переменная существует от точки объявления до конца того блока { }, в котором она объявлена, — и становится недоступна сразу за закрывающей скобкой. Это и называется областью видимости (scope): часть программы, где к переменной можно обратиться по имени.

main.cppC++
int main()
{
    int score = 10;   // score существует до конца main()

    {
        int bonus = 5;              // bonus существует только внутри этого блока
        std::cout << score + bonus; // здесь виден и score, и bonus
    }
    // здесь bonus уже недоступен — блок выше закончился

    std::cout << score;   // score всё ещё виден — main() не закончился

    return 0;
}

Это же правило уже работало в предыдущих уроках, просто без названия: параметр функции (седьмой урок) виден только внутри тела этой функции — это его scope; переменные, объявленные внутри тела if или цикла, видны только внутри этого блока. Два разных блока могут использовать одно и то же имя переменной независимо друг от друга — это не конфликт, потому что их области видимости не пересекаются (см. практику ниже).

Вложенность может идти глубже одного уровня — правило то же самое на любой глубине:

Три уровня вложенностиC++
int main()
{
    int a = 1;          // видна везде в main(), включая оба блока ниже

    {
        int b = 2;       // видна в этом блоке и во вложенном ниже, но не в main() после
        {
            int c = 3;     // видна только здесь, внутри самого глубокого блока
            std::cout << a + b + c;   // здесь видны все три: a, b и c
        }
        // c уже недоступна — самый глубокий блок закончился
        // std::cout << c;   // ошибка компиляции, если раскомментировать
    }
    // b тоже уже недоступна

    return 0;
}

Общее правило: переменная видна в своём блоке и во всех блоках, вложенных внутрь него, но не видна снаружи и не видна в блоках, объявленных раньше или позже на том же уровне. Ниже — та же мысль в виде таблицы:

Где объявленаГде виднаГде НЕ видна
В начале main()Везде до конца main(), включая все вложенные блокиНигде за пределами main()
Внутри блока { } первого уровняВнутри этого блока и во всех блоках, вложенных в негоВ main() после закрытия этого блока; в других, соседних блоках
Внутри параметра функцииВнутри тела этой функции (седьмой урок)В любом другом месте программы, включая другие функции
Внутри тела if/циклаВнутри этого if/цикла и вложенных в него блоковПосле закрывающей } этого if/цикла
С тем же именем, что и снаружи, во вложенном блоке (shadowing)Внутри вложенного блока — временно виден только внутренний вариантСнаружи вложенного блока — компилятор снова видит внешний, как будто внутреннего никогда не было (подробнее — в разделе «Хуже → лучше» ниже)

04std::cin — чтение ввода

Всё, что программа выводила до сих пор, было либо литералом, либо результатом вычисления в самом коде. std::cin (input stream) — симметричный std::cout инструмент: читает данные, которые вводит пользователь, вместо того чтобы их выводить. Тот же #include <iostream> подключает и cout, и cin — они из одной библиотеки.

main.cppC++
int score = 0;
std::cout << "Enter score: ";
std::cin >> score;   // ждёт ввод от пользователя, сохраняет его в score
std::cout << "You entered: " << score;

Оператор >> у cin — зеркальное отражение << у cout: данные «текут» в переменную, а не из неё. Тип переменной справа определяет, как cin разбирает введённый текст — для int score он ожидает число и остановится на первом пробеле или переводе строки.

Как и << у cout, оператор >> можно повторять подряд в одной инструкции — тогда cin читает несколько значений одно за другим, по порядку:

Чтение нескольких значений подрядC++
int width = 0;
int height = 0;

std::cout << "Enter width and height (через пробел): ";
std::cin >> width >> height;   // два значения одной инструкцией

int area = width * height;
std::cout << "Area: " << area;

Пользователь может ввести оба числа через пробел на одной строке (например, «4 5») или на разных строках — cin игнорирует пробелы и переводы строк между значениями и просто последовательно заполняет width, затем height, в том порядке, в котором они перечислены после cin.

05Что делает программу «настоящей консольной программой»

Все программы этого этапа были однонаправленными: код — на входе, текст на экране — на выходе, без участия пользователя в процессе выполнения. Полноценная консольная программа обычно устроена как цикл «ввод → обработка → вывод»: получить данные от пользователя (cin), что-то с ними сделать (переменные, операторы, условия, циклы, функции — всё, что уже пройдено), и показать результат (cout). Итоговая практика ниже построена именно по этой схеме.

06Область видимости параметра функции шаг за шагом

Параметр функции (седьмой урок) — тоже переменная со своей областью видимости, просто её «комната» — это тело функции. Ниже — та же функция, к которой на каждом шаге добавляется ещё одна деталь, чтобы стало видно, где именно начинается и заканчивается эта комната.

Шаг 1 — параметр используется внутри функцииC++
void printDoubled(int value)
{
    std::cout << value * 2 << "\n";
}

value существует и доступен по имени только внутри тела printDoubled — от открывающей до закрывающей { } этой функции.

Шаг 2 — добавили локальную переменную внутри функцииC++
void printDoubled(int value)
{
    int doubled = value * 2;   // локальная переменная, своя "комната" — тоже тело функции
    std::cout << doubled << "\n";
}

doubled — обычная локальная переменная с тем же правилом scope, что и любой блок { }: она видна до конца функции, но не снаружи. И value, и doubled живут в одной и той же «комнате» — теле функции.

Шаг 3 — вызов из main() показывает границуC++
void printDoubled(int value)
{
    int doubled = value * 2;
    std::cout << doubled << "\n";
}

int main()
{
    printDoubled(21);
    // value и doubled здесь не существуют — они жили только внутри printDoubled
    // std::cout << value;    // ошибка компиляции, если раскомментировать
    // std::cout << doubled;  // тоже ошибка компиляции

    return 0;
}

Из main() не видно ни value, ни doubled — обе переменные существовали только на время выполнения printDoubled и исчезли, как только функция завершилась. Это та же самая граница «комнаты», что и с обычным блоком { }, просто для функций она называется отдельным термином — область видимости функции.

07Хуже → лучше: тень переменной

Задача — вывести бонусные очки внутри вложенного блока, а затем основной счёт снаружи. Оба значения существуют одновременно, но в разных областях видимости.

Хуже — одинаковое имя в двух областяхC++
int score = 10;
{
    int score = 99;
    std::cout << score << "\n";
}
std::cout << score << "\n";
консоль (иллюстрация)
./main 99 10 Оба числа верны, но одно и то же имя означает разное в разных строках.

Программа работает правильно — вложенный score временно затеняет внешний, и каждое обращение видит то, что должно. Но при чтении кода (особенно если блоки станут длиннее) легко принять это за баг: «почему score вдруг стал 99, а потом снова 10?» — без внимательного взгляда на фигурные скобки ответ не очевиден.

Лучше — разные имена для разных значенийC++
int score = 10;
{
    int bonusPoints = 99;
    std::cout << bonusPoints << "\n";
}
std::cout << score << "\n";

Те же самые значения и тот же порядок вывода — но теперь по одним только именам понятно, что это два разных значения, а не одно и то же, «магическим образом» поменявшееся между строками.

Лучше всего — вывод тоже объясняет себяC++
int score = 10;
{
    int bonusPoints = 99;
    std::cout << "Bonus: " << bonusPoints << "\n";
}
std::cout << "Score: " << score << "\n";

Подписанный вывод (приём из самого первого урока) вместе с осмысленными именами (приём из третьего урока) — код и его результат теперь понятны без необходимости заглядывать в объявления переменных выше по файлу.

08Попробуй изменить

  • В примере из раздела «Область видимости» попробуй обратиться к bonus после закрывающей скобки его блока — компилятор откажется: имя вне своей области видимости не существует.
  • В примере из раздела «std::cin» замени тип score на double и попробуй ввести дробное число — cin прочитает его целиком, с дробной частью.

09Живая отладка: типичные ошибки

Обращение к переменной вне её области видимости
{
    int bonus = 5;
}
std::cout << bonus;   // bonus существовал только внутри блока выше
консоль (иллюстрация)
g++ main.cpp -o main main.cpp: In function 'int main()': main.cpp:4:15: error: 'bonus' was not declared in this scope 4 | std::cout << bonus; | ^~~~~ Компиляция не удалась.

Компилятор выдаёт ошибку «переменная не объявлена» — с его точки зрения такого имени в этой точке программы просто не существует, а не «значение потеряно» или что-то подобное.

Забыт #include <iostream> для cin
std::cin >> score;   // #include <iostream> выше отсутствует

Та же причина, что и для cout в самом первом уроке (и та же характерная ошибка компилятора «is not a member of 'std'», уже встречалась в третьем уроке) — cin тоже определён в iostream, без подключения компилятор его не найдёт.

Тень переменной (shadowing) сбивает с толку

Полный разбор с примером вывода — в разделе «Хуже → лучше» выше. В этом курсе стоит избегать повторного использования одного и того же имени во вложенных блоках, если только оно не обозначает буквально то же самое (как в практике ниже, где блоки не вложены, а идут последовательно).

10Итоговая практика Stage 00

Второе задание ниже (★★★) — это финал программы-персонажа, которую мы собирали от урока к уроку начиная с «Переменных»: hp появился там, типы и статы — в «Основных типах данных», операторы и условия — в «Операторах» и «Условиях», gradeFor — в прошлом уроке про функции. Здесь всё это соединяется в одну настоящую консольную программу с вводом от пользователя.

★ Напиши два отдельных блока { }, в каждом объяви переменную score со своим значением (разным в каждом блоке) и выведи её. Это покажет, что переменные в разных блоках независимы, даже с одинаковым именем.

★★ В отличие от задания выше, теперь продемонстрируй именно затенение (shadowing): объяви int score = 10; в начале main(), затем открой вложенный блок { } и внутри него объяви ещё один int score с другим значением (например, 99), выведи его через std::cout, закрой блок, а затем выведи score ещё раз — снаружи снова должно быть видно значение 10, как в разделе «Хуже → лучше» выше.

★★★ Итоговая практика Stage 00 — финальная сборка программы-персонажа. Напиши функцию char gradeFor(int hp) (как в прошлом уроке: 90 и выше — 'A', 70–89 — 'B', меньше 70 — 'F') через цепочку if/else if/else. В main() прочитай имя персонажа (std::string playerName) и его hp через std::cin, вызови gradeFor(hp), и выведи полный отчёт: имя, hp и оценку.

Шаги: 1) сигнатура char gradeFor(int hp) — принимает целое, возвращает символ; 2) тело — три ветки if/else if/else, каждая сравнивает hp с числом и возвращает свою букву; 3) в main() сначала читай оба значения через std::cin (после каждого приглашения — сразу чтение); 4) сохрани результат gradeFor(hp) в переменную; 5) один вывод, включающий все три значения — имя, hp и оценку.

11Проверь себя

Найди ошибку
Компилятор откажется собирать этот код. Почему?
{
  int bonus = 5;
}
std::cout << bonus;
Показать объяснение

bonus объявлена внутри блока { } и существует только до его закрывающей скобки (§03) — за пределами блока компилятор её просто не знает и выдаст ошибку вида «'bonus' was not declared in this scope» (см. §09).

Предскажи результат
Что выведет эта программа, в каком порядке?
int score = 1;
{
  int score = 2;
  std::cout << score;
}
std::cout << score;
Показать объяснение

21. Внутри вложенного блока внутренний score (значение 2) временно затеняет внешний — первый вывод показывает его. После закрывающей } внутренний score перестаёт существовать, и снова виден внешний score со значением 1 (§07, «Хуже → лучше»).

Почему так, а не иначе?
Зачем перед каждым std::cin >> переменная; в практике этого урока стоит отдельный std::cout с приглашением ("Enter name: " и т. п.)? Программа же сработала бы и без него.
Показать объяснение

Технически — да, cin прочитает ввод и без приглашения. Но пользователь, запустивший программу, увидит просто пустой мигающий курсор и не поймёт, что от него ждут ввода и в каком порядке. Приглашение — часть цикла «ввод → обработка → вывод» (§05): оно превращает программу, которая молча чего-то ждёт, в программу, которая объясняет себя пользователю.

12Термины

ТерминЗначит
Область видимости (scope)часть программы, где к переменной можно обратиться по имени
Затенение (shadowing)переменная во вложенном блоке временно скрывает переменную с тем же именем снаружи
std::cinпоток ввода — читает данные, введённые пользователем
Оператор >>читает значение из cin в переменную (зеркало << у cout)

13Что ты теперь умеешь

  • Определить границы области видимости любой переменной — от объявления до конца её блока { } — и объяснить, почему обращение к ней снаружи не скомпилируется.
  • Узнать затенение (shadowing) в чужом коде: понять, какое из двух одноимённых значений видно в конкретной строке, и предсказать, что снова станет видно после закрытия вложенного блока.
  • Прочитать данные, введённые пользователем, через std::cin — как одно значение, так и несколько подряд — и построить программу по циклу «ввод → обработка → вывод».
  • Собрать все восемь тем Stage 00 в одну работающую программу: переменные и типы, операторы и условия, циклы и функции, теперь ещё и с вводом от пользователя вместо статичных чисел в коде.

14Кратко

  • Область видимости переменной — от объявления до конца блока { }, в котором она объявлена.
  • Параметры функций и переменные внутри if/циклов/произвольных блоков подчиняются тому же правилу.
  • Одинаковое имя в разных, не вложенных блоках — не конфликт: области видимости не пересекаются.
  • Тень переменной (shadowing) во вложенном блоке компилируется, но затрудняет чтение — в этом курсе её стоит избегать.
  • std::cin >> переменная; — чтение пользовательского ввода, симметрично std::cout <<.
  • Полноценная консольная программа — это цикл «ввод → обработка → вывод».

На этом Stage 00 закончен: восемь уроков, от первой скомпилированной строчки до программы, которая сама читает данные от игрока и считает результат. Дальше начинается Stage 01 — и первый его урок, const, отвечает на вопрос, который до сих пор не поднимался ни разу: что мешает случайно изменить значение, которое по смыслу вообще не должно меняться (например, максимальный HP персонажа)?

🎮 А как это связано с Unreal Engine?

Область видимости работает в Unreal C++ точно так же. Классический пример — BeginPlay(), метод, который движок вызывает один раз при появлении Actor в игре:

Пример (Unreal C++, для примера)C++
void AMyActor::BeginPlay()
{
    Super::BeginPlay();

    int32 StartingBonus = 100;              // локальная переменная — живёт только внутри BeginPlay()
    Health = MaxHealth + StartingBonus;      // Health — переменная-член класса, видна во всех методах

    // StartingBonus здесь ещё существует...
}
// ...а здесь, в Tick() или любом другом методе того же класса, StartingBonus уже недоступна —
// её scope закончился вместе с BeginPlay(), в отличие от Health

StartingBonus — локальная переменная с тем же правилом scope, что и в этом уроке: видна только внутри BeginPlay(). А Health — переменная-член класса (Stage 07+), у неё другая, более широкая область видимости — она доступна из любого метода этого Actor, включая Tick(). std::cin напрямую в игре почти не используется — вместо консольного ввода в Unreal есть система Input (клавиатура/геймпад/мышь, Enhanced Input), но сама идея «дождаться данные снаружи, потом обработать» останется той же.