Область видимости, ввод и итоговая практика
Последний урок Stage 00 закрывает два оставшихся вопроса — где именно «живёт» переменная и как программе получить данные не из кода, а от пользователя (std::cin) — а затем собирает всё, что было в этапе, в одной итоговой практике.
01Что ты изучишь
- Область видимости (scope) — где переменная существует, а где о ней уже «не знают»
- Как параметры функций и переменные внутри блоков соотносятся со scope
std::cin— чтение данных, введённых пользователем- Итоговую практику, использующую всё из Stage 00 сразу
02Основная идея: комнаты в доме
Область видимости — это как комнаты в доме. Вещь, оставленная в одной комнате (переменная, объявленная в блоке { }), доступна, пока ты находишься в этой комнате или в любой комнате внутри неё (вложенный блок) — но её не видно и не достать из соседней комнаты или из коридора снаружи. Когда ты выходишь из комнаты (блок заканчивается закрывающей }), всё, что было оставлено только в ней, становится недоступным — не потому что «пропало», а потому что ты уже не там, где оно лежит. Именно поэтому в две разные, не вложенные друг в друга комнаты можно положить вещи с одинаковым названием, не создавая путаницы — они просто в разных местах.
03Область видимости — где переменная существует
Перенесём аналогию с комнатами на настоящий код и посмотрим, где именно проходит граница. Правило простое, хоть и не сразу очевидное, пока не увидишь его в действии: переменная существует от точки объявления до конца того блока { }, в котором она объявлена, — и становится недоступна сразу за закрывающей скобкой. Это и называется областью видимости (scope): часть программы, где к переменной можно обратиться по имени.
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 или цикла, видны только внутри этого блока. Два разных блока могут использовать одно и то же имя переменной независимо друг от друга — это не конфликт, потому что их области видимости не пересекаются (см. практику ниже).
Вложенность может идти глубже одного уровня — правило то же самое на любой глубине:
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 — они из одной библиотеки.
int score = 0;
std::cout << "Enter score: ";
std::cin >> score; // ждёт ввод от пользователя, сохраняет его в score
std::cout << "You entered: " << score;
Оператор >> у cin — зеркальное отражение << у cout: данные «текут» в переменную, а не из неё. Тип переменной справа определяет, как cin разбирает введённый текст — для int score он ожидает число и остановится на первом пробеле или переводе строки.
Как и << у cout, оператор >> можно повторять подряд в одной инструкции — тогда cin читает несколько значений одно за другим, по порядку:
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Область видимости параметра функции шаг за шагом
Параметр функции (седьмой урок) — тоже переменная со своей областью видимости, просто её «комната» — это тело функции. Ниже — та же функция, к которой на каждом шаге добавляется ещё одна деталь, чтобы стало видно, где именно начинается и заканчивается эта комната.
void printDoubled(int value)
{
std::cout << value * 2 << "\n";
}
value существует и доступен по имени только внутри тела printDoubled — от открывающей до закрывающей { } этой функции.
void printDoubled(int value)
{
int doubled = value * 2; // локальная переменная, своя "комната" — тоже тело функции
std::cout << doubled << "\n";
}
doubled — обычная локальная переменная с тем же правилом scope, что и любой блок { }: она видна до конца функции, но не снаружи. И value, и doubled живут в одной и той же «комнате» — теле функции.
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Хуже → лучше: тень переменной
Задача — вывести бонусные очки внутри вложенного блока, а затем основной счёт снаружи. Оба значения существуют одновременно, но в разных областях видимости.
int score = 10;
{
int score = 99;
std::cout << score << "\n";
}
std::cout << score << "\n";
Программа работает правильно — вложенный score временно затеняет внешний, и каждое обращение видит то, что должно. Но при чтении кода (особенно если блоки станут длиннее) легко принять это за баг: «почему score вдруг стал 99, а потом снова 10?» — без внимательного взгляда на фигурные скобки ответ не очевиден.
int score = 10;
{
int bonusPoints = 99;
std::cout << bonusPoints << "\n";
}
std::cout << score << "\n";
Те же самые значения и тот же порядок вывода — но теперь по одним только именам понятно, что это два разных значения, а не одно и то же, «магическим образом» поменявшееся между строками.
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 существовал только внутри блока выше
Компилятор выдаёт ошибку «переменная не объявлена» — с его точки зрения такого имени в этой точке программы просто не существует, а не «значение потеряно» или что-то подобное.
std::cin >> score; // #include <iostream> выше отсутствует
Та же причина, что и для cout в самом первом уроке (и та же характерная ошибка компилятора «is not a member of 'std'», уже встречалась в третьем уроке) — cin тоже определён в iostream, без подключения компилятор его не найдёт.
Полный разбор с примером вывода — в разделе «Хуже → лучше» выше. В этом курсе стоит избегать повторного использования одного и того же имени во вложенных блоках, если только оно не обозначает буквально то же самое (как в практике ниже, где блоки не вложены, а идут последовательно).
10Итоговая практика Stage 00
Второе задание ниже (★★★) — это финал программы-персонажа, которую мы собирали от урока к уроку начиная с «Переменных»: hp появился там, типы и статы — в «Основных типах данных», операторы и условия — в «Операторах» и «Условиях», gradeFor — в прошлом уроке про функции. Здесь всё это соединяется в одну настоящую консольную программу с вводом от пользователя.
★ Напиши два отдельных блока { }, в каждом объяви переменную score со своим значением (разным в каждом блоке) и выведи её. Это покажет, что переменные в разных блоках независимы, даже с одинаковым именем.
Два блока должны идти один за другим на одном уровне, а не один внутри другого — каждый со своей открывающей и закрывающей { }, и каждый обязательно выводит свой score.
Решение: { int score = 10; std::cout << score << "\n"; } { int score = 20; std::cout << score << "\n"; } — два независимых блока подряд, не вложенных друг в друга.
★★ В отличие от задания выше, теперь продемонстрируй именно затенение (shadowing): объяви int score = 10; в начале main(), затем открой вложенный блок { } и внутри него объяви ещё один int score с другим значением (например, 99), выведи его через std::cout, закрой блок, а затем выведи score ещё раз — снаружи снова должно быть видно значение 10, как в разделе «Хуже → лучше» выше.
Второй score должен быть объявлен ВНУТРИ вложенного блока { }, а не рядом с первым, как в задании 1 — иначе это не затенение, а два независимых блока.
После того как вложенный блок закроется своей }, снаружи нужен ещё один std::cout << score — именно он должен показать, что видно снова внешнее значение (10), а не то, что было внутри блока.
Решение: int score = 10; { int score = 99; std::cout << score << "\n"; } std::cout << score << "\n"; — второй score объявлен внутри вложенного блока, а финальный вывод стоит уже после его закрывающей скобки.
★★★ Итоговая практика 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 и оценку.
Начни с сигнатуры и тела gradeFor отдельно от main() — она принимает int hp (а не int&: ей не нужно ничего менять в оригинале, только посчитать и вернуть), и внутри — цепочка if / else if / else, где каждая ветка сравнивает hp с порогом (например, >= 90, >= 70) и возвращает свою букву. Нужно минимум три разные буквы и хотя бы одно реальное сравнение hp с числом.
В main() оба std::cin должны идти сразу после соответствующего приглашения и до вызова gradeFor — сначала std::cin >> playerName;, затем std::cin >> hp;. Без обоих значений, прочитанных в переменные, gradeFor нечего будет передавать.
Результат вызова обязательно сохрани в переменную — char grade = gradeFor(hp); — а не просто вызови функцию саму по себе. Если оценка посчитана, но нигде не сохранена и не выведена, задание не засчитано.
Решение: char gradeFor(int hp) { if (hp >= 90) { return 'A'; } else if (hp >= 70) { return 'B'; } else { return 'F'; } } — в main(): std::cin >> playerName; std::cin >> hp; char grade = gradeFor(hp); std::cout << playerName << " " << hp << " " << grade; — один финальный вывод, включающий все три значения.
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 C++ точно так же. Классический пример — BeginPlay(), метод, который движок вызывает один раз при появлении Actor в игре:
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), но сама идея «дождаться данные снаружи, потом обработать» останется той же.