Переменные, идентификаторы и типы
Ты уже писал int x = 10; и x = x + 5; в прошлом уроке — и код работал, хотя ты, скорее всего, не задавался вопросом, что тут вообще x, почему перед ним int, и почему вторая строка выглядит короче первой. Пора спросить. Здесь же начинается программа, которая будет расти вместе с тобой все следующие уроки Stage 00 — карточка игрового персонажа.
01Что ты изучишь
- Что на самом деле происходит в памяти, когда выполняется
int x = 10; - Какие имена C++ разрешает давать переменным, а какие — нет
- Что такое тип и зачем он вообще нужен переменной
- Почему
int x = 10;иx = x + 5;— на самом деле два разных вида инструкций, а не один
02Что происходит, когда компьютер видит int x = 10;
Компьютер не «понимает» код так, как его понимаешь ты, — он выполняет его буквально, шаг за шагом, ничего не додумывая. Вот что на самом деле происходит, когда доходит очередь до строки int x = 10;: где-то в памяти находится свободное место под одно целое число, этому месту присваивается имя x, и туда сразу кладётся значение 10. Вот это место в памяти, доступное по имени, — и есть переменная.
Дальше, когда в коде снова встречается x — например, в x = x + 5; — программа не создаёт новое место в памяти, а обращается к тому же самому, по тому же имени. Посмотрим на все три строки подряд и на то, что происходит с памятью на каждом шаге:
int x = 10;
// память: [ x: 10 ] — ячейка создана, сразу с значением
x = x + 5;
// шаг 1: читаем текущее значение x → 10
// шаг 2: вычисляем выражение 10 + 5 → 15
// шаг 3: кладём 15 обратно в ту же ячейку x
// память: [ x: 15 ] — ячейка та же самая, значение другое
std::cout << x;
// читаем текущее значение ячейки x → выводим 15
// память: [ x: 15 ] — чтение не меняет значение
За все три строки у x был ровно один адрес в памяти. Три инструкции не создали три переменных — они три раза обратились к одной и той же ячейке по имени. Значение внутри менялось, а само место (и имя, по которому его находят) — нет. Это и отличает переменную от значения-константы.
03Как называть переменную, чтобы компилятор согласился
Имя переменной (как и имя функции — например, main) называется идентификатором. Если бы C++ разрешал в качестве имени абсолютно любой текст, тут же возникли бы проблемы: пробелы внутри имени сделали бы непонятным, где имя заканчивается, а где начинается следующая часть кода; имя, начинающееся с цифры, компилятор перепутал бы с числом; слово вроде return в роли имени переменной столкнулось бы с тем же словом, у которого уже есть фиксированный смысл в языке. Отсюда и растут реальные правила C++:
- можно использовать латинские буквы, цифры и знак подчёркивания
_; - первый символ не может быть цифрой (
2ndPlayer— ошибка,secondPlayer— нормально); - нельзя использовать зарезервированные слова языка —
return,int,ifи подобные уже заняты; - C++ различает регистр:
health,HealthиHEALTH— три разных идентификатора; - пробелы внутри имени запрещены.
Всё это компилятор проверяет сам и просто откажется собирать код при нарушении. Но есть вопрос, который компилятор не проверяет вообще, а платишь за него ты сам: x с точки зрения синтаксиса ничем не хуже playerHealth — компилятору всё равно. А вот человеку, который откроет этот код через месяц (в том числе тебе самому), x ничего не скажет о том, что там хранится, а playerHealth — скажет сразу, за то же количество усилий при наборе.
04Хуже → лучше: имена переменных
Задача одна и та же — хранить очки здоровья игрока и число попыток входа. Первая из этих двух переменных — playerHealth — не просто пример: с неё начинается программа-персонаж, которая будет расти от урока к уроку до самого конца Stage 00.
int a = 100;
int b = 0;
std::cout << "HP: " << a << ", Attempts: " << b;
Синтаксически всё верно. Но убери на секунду соседнюю строку с std::cout — что такое a и b? Догадаться неоткуда. А если переменных станет двадцать, гадать придётся двадцать раз подряд.
int playerHealth = 100;
int attempts = 0;
std::cout << "HP: " << playerHealth << ", Attempts: " << attempts;
Теперь строка std::cout для понимания смысла уже не нужна вовсе — playerHealth и attempts говорят сами за себя.
int playerHealth = 100;
int loginAttempts = 0; // именно попытки входа, а не, например, попытки атаки
std::cout << "HP: " << playerHealth << ", Attempts: " << loginAttempts;
attempts само по себе не однозначно — попытки чего именно? В маленькой программе это неважно, но с ростом кода такие мелкие уточнения экономят время всем, кто читает его позже.
Одна оговорка на будущее: дальше в курсе та же переменная будет называться короче — hp. Это не противоречит только что сказанному: hp — общепринятое сокращение в игровой разработке, его смысл понятен из контекста почти всегда, а не только внутри одной программы. Здесь мы сознательно выбираем полное имя, потому что сокращение ещё не введено, — не наталкиваемся на короткое имя по незнанию, а выбираем его позже осознанно.
05Тип — почему компилятору мало просто «переменной»
int x = 10; без слова int компилятору бессмысленно — «переменная, но переменная чего?», вопрос без ответа. Само слово int перед именем и есть ответ: это тип, указание, какого рода значение здесь будет храниться и сколько памяти под него нужно выделить. int означает «целое число». Компилятор использует эту информацию постоянно: сколько байт выделить под значение, что с ним можно делать (числа складывать можно, а текст, забегая вперёд, так же просто — нет), и не даст случайно положить в переменную что-то, что типу не соответствует.
Кроме int в C++ есть типы для дробных чисел, для true/false, для одного символа, для текста — это тема следующего урока целиком. Здесь достаточно унести один факт: у каждой переменной есть тип, он выбирается при объявлении и определяет, что с значением можно делать дальше.
06int x = 10; и x = x + 5; — почему это разные вещи
Обе строки уже встречались в прошлом уроке без названий. Присмотримся к разнице внимательнее:
int x = 10; // инициализация
x = x + 5; // присваивание
В первой строке слово int появляется — переменная создаётся здесь же, впервые, и сразу получает значение. Это называется инициализацией: переменная и её первое значение появляются одновременно, одной инструкцией.
Во второй строке слова int уже нет — и не может быть: x уже существует, повторно объявлять её не нужно и нельзя. Это присваивание: изменение значения уже существующей переменной. Инструкция берёт текущее значение x, вычисляет новое (15) и кладёт его обратно в ту же переменную.
Можно объявить переменную и без инициализации — тогда у неё сразу нет заданного значения, и присвоить его придётся отдельной инструкцией позже:
int attempts; // объявление БЕЗ инициализации — значения пока нет
attempts = 0; // присваивание — только теперь у attempts появляется значение
attempts = 1; // ещё одно присваивание — значение меняется
Три отдельные инструкции: одна объявляет переменную, две другие — присваивают ей значения по очереди. Здесь есть подвох: пока переменная объявлена, а значение ещё не присвоено, читать её (например, вывести в std::cout) уже можно синтаксически, но получишь неопределённый мусор, оставшийся в памяти от чего-то другого. Практическое правило: если начальное значение известно заранее — инициализируй сразу при объявлении, не откладывай без необходимости.
07Комментарии — заметки, которые компилятор не видит
Комментарии // инициализация и // присваивание в примере выше уже показывали, как это выглядит. Комментарий — текст в коде, который компилятор полностью игнорирует, а человек — читает. Два способа его написать:
| Синтаксис | Что комментирует |
|---|---|
// текст | всё до конца строки |
/* текст */ | всё между /* и */, может занимать несколько строк |
Полезный комментарий не пересказывает то, что и так видно из кода (x = x + 5; // прибавляем 5 к x — бесполезно, это и так читается), а объясняет то, что не видно: зачем именно это число, почему именно так, а не иначе. Чем сложнее становится программа, тем больше это окупается.
08Попробуй изменить
- В примере с
attemptsзамениint attempts;наint attempts = 0;и убери первую инструкцию присваивания — результат не изменится, но теперь это инициализация, а не последующее присваивание. - Попробуй объявить переменную с именем
int— компилятор откажется, это зарезервированное слово. - Объяви переменную с именем, начинающимся с цифры (например
int 2x = 5;) — тоже ошибка компиляции.
09Живая отладка: типичные ошибки
Точный текст ошибки зависит от компилятора и его версии — ниже характерный вид в стиле g++.
int 2ndTry = 0; // идентификатор не может начинаться с цифры
Компилятор видит 2 как начало числа, а не имени, и пытается разобрать 2ndTry как число со странным суффиксом — отсюда не самая очевидная формулировка ошибки. Исправление — переставить: secondTry или try2.
int return = 5; // return — ключевое слово, не может быть именем
То же самое с int, if, while и другими ключевыми словами — у каждого фиксированный смысл в языке, имени переменной из них не сделать.
int score; // объявлена, но не инициализирована
std::cout << score; // читаем "мусор" из памяти — компилируется, но непредсказуемо
Эта ошибка не появится в консоли компилятора — программа успешно соберётся и запустится, но выведет непредсказуемое значение (может быть 0, может быть случайный «мусор»). Компиляторы нередко предупреждают об этом отдельно (warning, не error), и такое предупреждение игнорировать не стоит.
int score = 0;
int score = 10; // не присваивание, а повторное объявление — ошибка
У одного идентификатора не может быть двух объявлений в одной и той же области видимости (подробнее — в последнем уроке Stage 00). Чтобы просто изменить значение, слово int перед именем повторно не пишут: score = 10; — это уже присваивание, а не новое объявление.
10Проверь себя
Три вопроса на разное: допустимость имени, разница между инициализацией и присваиванием, и предсказание неочевидного результата. Каждый вопрос сначала стоит попробовать закрыть рукой ответ и подумать самому — ответ никуда не убежит.
Показать объяснение
player_health — буквы и подчёркивание разрешены, имя не начинается с цифры. Остальные три отклонит: 2ndPlayer начинается с цифры (компилятор попытается прочитать «2» как число), int — зарезервированное слово языка, а Player Health содержит пробел — компилятор увидит два токена вместо одного идентификатора.
int score = 0;
score = 5;
score = score + 1;Показать объяснение
int score = 0; — инициализация: слово int присутствует, переменная создаётся и сразу получает значение одной инструкцией. Обе следующие строки — присваивания: int уже не пишется (переменная существует), меняется только значение уже существующей ячейки памяти.
int score;
std::cout << score;
score = 42;Показать объяснение
Неопределённое значение. score объявлена без инициализации — до строки score = 42; в её ячейке памяти лежит то, что там оказалось случайно (см. §09) — компилятор не обязан обнулять память сам. Компилируется без ошибки, но результат первого вывода непредсказуем.
11Практика
★ Замени бессмысленное имя a на осмысленное (например, playerHealth), инициализируй его значением 100, и выведи через std::cout в формате «HP: 100».
Раздел 04 показывает: имя должно говорить, что внутри — что-то вроде playerHealth вместо a.
Решение: int playerHealth = 100;, затем std::cout << "HP: " << playerHealth; — имя переменной должно совпадать в обеих строках.
★★ Объяви переменную attempts без начального значения, затем отдельной инструкцией присвой ей 0, затем ещё одной отдельной инструкцией измени на 1, и выведи итоговое значение через std::cout.
Раздел 06 показывает: объявление без инициализатора — это int attempts; без «=» — значение появляется только следующей отдельной инструкцией присваивания.
Нужны две отдельные инструкции присваивания подряд: сначала attempts = 0;, затем attempts = 1; — оба раза без слова int (оно нужно только один раз, при объявлении).
Решение: int attempts; attempts = 0; attempts = 1; std::cout << attempts; — объявление, два отдельных присваивания, вывод.
12Термины
| Термин | Значит |
|---|---|
| Переменная (variable) | именованная область памяти для хранения значения, доступная по имени |
| Идентификатор (identifier) | имя переменной, функции или другой сущности — по строгим синтаксическим правилам C++ |
| Тип (type) | указание, какого рода значение хранит переменная и что с ним можно делать |
| Инициализация (initialization) | первое значение переменной, заданное в той же инструкции, где переменная объявляется |
| Присваивание (assignment) | изменение значения уже существующей переменной отдельной инструкцией |
| Комментарий (comment) | текст в коде, который игнорируется компилятором и читается только человеком |
13Что ты теперь умеешь
До этого урока int x = 10; просто работал. Теперь у тебя есть модель того, что стоит за каждым словом в этой строке:
- Ты можешь объяснить, что происходит в памяти при выполнении
int x = 10;, и почему повторное обращение кxдальше не создаёт новую ячейку, а использует ту же самую. - Ты можешь определить, допустимо ли предложенное имя переменной как идентификатор C++, и объяснить конкретную причину, если компилятор его отклонит.
- Ты можешь отличить инициализацию от присваивания в любом фрагменте кода, а не только в примерах этого урока.
- Ты можешь предсказать, чем опасно чтение переменной до присваивания значения — и почему компилятор не обязан указать на эту ошибку явно.
- Ты можешь выбрать для новой переменной имя, которое говорит о содержимом само по себе, а не полагаться на то, что рядом есть поясняющий комментарий или вывод.
14Кратко
- Переменная — именованное место в памяти; имя не меняется, значение внутри — может.
- Идентификатор — имя переменной (или функции): буквы, цифры,
_, не с цифры, не зарезервированное слово, регистр важен. - Осмысленное имя — не требование синтаксиса, а требование читаемости кода человеком.
- Тип определяет, какого рода значение хранит переменная и что с ним можно делать — конкретные типы разбираются в следующем уроке.
- Инициализация — первое значение при объявлении (
int x = 10;); присваивание — изменение значения уже существующей переменной (x = x + 5;). - Переменную можно объявить без инициализации, но читать её значение до первого присваивания — ошибка.
До сих пор единственным типом был int — но дробные числа, true/false и текст устроены иначе и хранятся по-разному. Следующий урок разбирает эту разницу вместо того, чтобы оставлять её неявной.
В Unreal C++ переменные объявляются точно так же — тип, имя, необязательная инициализация. Разница появится позже (Stage 07+), когда переменные-члены класса начнут помечаться макросом UPROPERTY(), чтобы Unreal видел их в редакторе и Blueprint — но само правило «осмысленное имя переменной экономит время при чтении» в Unreal-проектах, где счёт переменных идёт на тысячи, работает даже более критично, чем в учебных примерах здесь.