LEARNING · STAGE 00 · PROGRAMMING BASICS

Переменные, идентификаторы и типы

Ты уже писал 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; — программа не создаёт новое место в памяти, а обращается к тому же самому, по тому же имени. Посмотрим на все три строки подряд и на то, что происходит с памятью на каждом шаге:

Что в памяти, шаг за шагомC++
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.

Хуже — компилируется, но ничего не говорит о смыслеC++
int a = 100;
int b = 0;
std::cout << "HP: " << a << ", Attempts: " << b;

Синтаксически всё верно. Но убери на секунду соседнюю строку с std::cout — что такое a и b? Догадаться неоткуда. А если переменных станет двадцать, гадать придётся двадцать раз подряд.

Лучше — имя говорит, что внутриC++
int playerHealth = 100;
int attempts = 0;
std::cout << "HP: " << playerHealth << ", Attempts: " << attempts;

Теперь строка std::cout для понимания смысла уже не нужна вовсе — playerHealth и attempts говорят сами за себя.

Лучше всего — имя снимает и более тонкую неоднозначностьC++
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; — почему это разные вещи

Обе строки уже встречались в прошлом уроке без названий. Присмотримся к разнице внимательнее:

main.cppC++
int x = 10;   // инициализация
x = x + 5;    // присваивание

В первой строке слово int появляется — переменная создаётся здесь же, впервые, и сразу получает значение. Это называется инициализацией: переменная и её первое значение появляются одновременно, одной инструкцией.

Во второй строке слова int уже нет — и не может быть: x уже существует, повторно объявлять её не нужно и нельзя. Это присваивание: изменение значения уже существующей переменной. Инструкция берёт текущее значение x, вычисляет новое (15) и кладёт его обратно в ту же переменную.

Можно объявить переменную и без инициализации — тогда у неё сразу нет заданного значения, и присвоить его придётся отдельной инструкцией позже:

Объявление без инициализацииC++
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;   // идентификатор не может начинаться с цифры
консоль (иллюстрация)
g++ main.cpp -o main main.cpp:4:9: error: invalid suffix "ndTry" on integer constant 4 | int 2ndTry = 0; | ^~~~~~ Компиляция не удалась.

Компилятор видит 2 как начало числа, а не имени, и пытается разобрать 2ndTry как число со странным суффиксом — отсюда не самая очевидная формулировка ошибки. Исправление — переставить: secondTry или try2.

Использовано зарезервированное слово
int return = 5;   // return — ключевое слово, не может быть именем
консоль (иллюстрация)
g++ main.cpp -o main main.cpp:4:9: error: expected unqualified-id before 'return' 4 | int return = 5; | ^~~~~~ Компиляция не удалась.

То же самое с int, if, while и другими ключевыми словами — у каждого фиксированный смысл в языке, имени переменной из них не сделать.

Чтение переменной до присваивания значения
int score;              // объявлена, но не инициализирована
std::cout << score;   // читаем "мусор" из памяти — компилируется, но непредсказуемо

Эта ошибка не появится в консоли компилятора — программа успешно соберётся и запустится, но выведет непредсказуемое значение (может быть 0, может быть случайный «мусор»). Компиляторы нередко предупреждают об этом отдельно (warning, не error), и такое предупреждение игнорировать не стоит.

Повторное объявление переменной с тем же именем
int score = 0;
int score = 10;   // не присваивание, а повторное объявление — ошибка
консоль (иллюстрация)
g++ main.cpp -o main main.cpp:5:9: error: redefinition of 'int score' 5 | int score = 10; | ^~~~~ main.cpp:4:9: note: 'int score' previously declared here 4 | int score = 0; | ^~~~~ Компиляция не удалась.

У одного идентификатора не может быть двух объявлений в одной и той же области видимости (подробнее — в последнем уроке 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 уже не пишется (переменная существует), меняется только значение уже существующей ячейки памяти.

Предскажи результат
Что выведет первая строка — обязательно ли 0?
int score;
std::cout << score;
score = 42;
Показать объяснение

Неопределённое значение. score объявлена без инициализации — до строки score = 42; в её ячейке памяти лежит то, что там оказалось случайно (см. §09) — компилятор не обязан обнулять память сам. Компилируется без ошибки, но результат первого вывода непредсказуем.

11Практика

★ Замени бессмысленное имя a на осмысленное (например, playerHealth), инициализируй его значением 100, и выведи через std::cout в формате «HP: 100».

★★ Объяви переменную attempts без начального значения, затем отдельной инструкцией присвой ей 0, затем ещё одной отдельной инструкцией измени на 1, и выведи итоговое значение через std::cout.

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 Engine?

В Unreal C++ переменные объявляются точно так же — тип, имя, необязательная инициализация. Разница появится позже (Stage 07+), когда переменные-члены класса начнут помечаться макросом UPROPERTY(), чтобы Unreal видел их в редакторе и Blueprint — но само правило «осмысленное имя переменной экономит время при чтении» в Unreal-проектах, где счёт переменных идёт на тысячи, работает даже более критично, чем в учебных примерах здесь.