Основы C++ для начинающих программистов

Базовый курс по языку C++ для тех, кто уже знаком с программированием на Python или JavaScript. Вы освоите синтаксис, работу с памятью и объектно-ориентированное программирование — фундамент для создания игр и системных приложений.

1. Основы синтаксиса и переменные

Основы синтаксиса и переменные

Представьте, что вы приехали в иностранную страну и вам нужно заказать обед. Вы знаете, что хотите сказать — но не знаете, как это произнести на местном языке. Именно так чувствует себя программист, переходящий с Python на C++: логика знакома, но синтаксис требует адаптации. И первое, что нужно освоить в новом «языке» — это как объявлять переменные и как устроена базовая структура программы.

Первая программа: что здесь происходит

Вот минимальная программа на C++, которая выводит текст в консоль:

Разберём по частям, как будто это инструкция к бытовому прибору. Директива #include <iostream> — это запрос на подключение библиотеки ввода-вывода. Без неё компилятор не знает, что такое std::cout. Функция main() — точка входа, откуда начинается выполнение программы. Каждая C++-программа обязана содержать ровно одну функцию с таким именем. Оператор return 0; сообщает операционной системе, что программа завершилась без ошибок.

> Ключевое отличие от Python: в C++ каждая инструкция заканчивается точкой с запятой, а блоки кода оборачиваются фигурными скобками {} — не отступами.

Переменные: контейнеры с ярлыками

Переменная в C++ — это именованная область памяти, в которой хранится значение определённого типа. Представьте шкаф с ящиками: на каждом ящике есть табличка с названием (имя переменной), и в ящик можно положить только вещи определённого размера (тип данных).

Базовые типы данных

| Тип | Размер | Аналогия | Пример значения | |-----|--------|----------|-----------------| | int | 4 байта | Целое число, как счётчик на кассе | 42 | | float | 4 байта | Десятичное число с приблизительной точностью | 3.14f | | double | 8 байт | Десятичное число с высокой точностью | 3.14159265358979 | | char | 1 байт | Один символ, как буква на клавиатуре | 'A' | | bool | 1 байт | Логическое значение: да или нет | true |

Обратите внимание: в C++ тип переменной нужно указывать явно при объявлении. Python сам догадывается, какой тип положить в переменную, а C++ требует, чтобы вы сами сказали компилятору: «Здесь будет целое число» или «Здесь будет текст».

Строки: особый случай

В Python строка — это встроенный тип. В C++ для работы со строками нужно подключить библиотеку <string> и использовать тип std::string:

Операция конкатенации (склеивания) строк работает через оператор +, как и в Python. Но помните: вы не можете складывать две строковые литералы через + без предварительного присвоения их переменным типа std::string.

Константы: когда значение не должно меняться

Иногда нужно зафиксировать значение — как табличка «Не трогать!» на музейном экспонате. Для этого служит ключевое слово const:

Если вы попытаетесь изменить значение константы после объявления, компилятор выдаст ошибку. Это защита от случайных изменений — и хороший инструмент для самодисциплины.

Ввод данных: диалог с пользователем

Выводить данные вы уже умеете через std::cout. Для ввода используется std::cin — он работает как кассир, который ждёт, пока вы назовёте число:

Здесь важный нюанс: std::cin >> читает только одно слово (до пробела), поэтому для ввода полного имени используется std::getline(). Это частая ловушка для новичков — если после cin >> вызвать getline, первый оставит в буфере символ новой строки, и getline считает пустую строку. Решение — вызвать std::cin.ignore() между ними.

Приведение типов: когда контейнер не подходит по размеру

Представьте, что вы пытаетесь перелить литр воды в стакан на 200 мл — часть жидкости прольётся. В C++ происходит то же самое при неявном приведении типов:

Компилятор выполнит приведение, но может предупредить о потере данных. Чтобы сделать приведение осознанным, используйте явное приведениеstatic_cast:

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

Пространства имён: избегаем конфликтов

Префикс std:: перед cout и string — это указание на пространство имён std. Представьте большой офис, где работают два Ивана Петровича. Чтобы не путать, вы говорите: «Иван Петрович из бухгалтерии». std::cout — это «cout из стандартной библиотеки».

Если писать std:: каждый раз утомительно, можно в начале файла написать using namespace std; — но в реальных проектах это считается плохой практикой, потому что повышает риск конфликтов имён.

На этом фундамент заложен: вы понимаете, как устроена программа, как объявлять переменные разных типов, как вводить и выводить данные. В следующей статье мы научим программу принимать решения и повторять действия — то есть управлять потоком выполнения.

2. Управление потоком выполнения

Управление потоком выполнения

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

Условные операторы: перекрёстки программы

Самый простой способ заставить программу принимать решения — это оператор if. Он работает как охранник на входе: «Если у вас есть пропуск — проходите. Если нет — стойте».

Условие записывается в круглых скобках и должно вычисляться в логическое значение (true или false). Обратите внимание: в C++ сравнение «больше или равно» записывается как >=, а не словами. Операторы сравнения в C++:

  • == — равно (не путайте с =, которое означает присваивание!)
  • != — не равно
  • <, >, <=, >= — стандартные сравнения
  • > Частая ошибка новичков: написать if (баллы = 100) вместо if (баллы == 100). Один знак = присваивает значение, а не сравнивает — и условие всегда будет истинным, если значение не ноль.

    Логические операторы

    Когда нужно проверить несколько условий одновременно, используются логические операторы:

  • && — логическое И (оба условия должны быть истинны)
  • || — логическое ИЛИ (хотя бы одно условие истинно)
  • ! — логическое НЕ (инвертирует результат)
  • Тернарный оператор: компактная альтернатива

    Если нужно выбрать одно из двух значений, можно обойтись без if-else, используя тернарный оператор условие ? значение_если_истина : значение_если_ложь:

    Это как мгновенный ответ на простой вопрос — без лишних церемоний.

    Переключатель switch

    Когда нужно сравнить одну переменную с несколькими конкретными значениями, switch работает чище, чем цепочка if-else:

    Ключевое слово break здесь критически важно — без него выполнение «провалится» в следующий case, как мяч, скатывающийся по лестнице. Это не баг, а особенность языка, и забытый break — источник множества трудноуловимых ошибок.

    Циклы: круговое движение

    Циклы позволяют повторять блок кода многократно. В C++ есть три вида циклов, и каждый подходит для своей ситуации.

    Цикл while: «пока условие истинно»

    Работает как дверь с датчиком: «Пока кто-то стоит в проходе — держать дверь открытой».

    Условие проверяется перед каждой итерацией. Если оно ложно с самого начала — тело цикла не выполнится ни разу.

    Цикл for: «для каждого шага в диапазоне»

    Когда известно количество итераций, for — самый удобный инструмент. Его конструкция компактна:

    Три части в скобках: инициализация (int i = 0), условие продолжения (i < 10) и действие после каждой итерации (i++). Это как настройка шагомера: задаёте начальное значение, конечное и шаг.

    Цикл do-while: «сделай сначала, проверь потом»

    Отличается от while тем, что тело выполнится хотя бы один раз, даже если условие ложно:

    Идеально подходит для меню и диалогов ввода — сначала показать вопрос, потом проверить ответ.

    Управление внутри циклов: break и continue

    Иногда нужно экстренно выйти из цикла или пропустить текущую итерацию.

    break — немедленно завершает цикл, как аварийный выход:

    continue — пропускает остаток текущей итерации и переходит к следующей, как если бы вы пролистали страницу:

    Вложенные циклы: перебор комбинаций

    Циклы можно вкладывать друг в друга — как коробки разного размера. Это полезно для работы с таблицами, матрицами и перебора пар:

    Предупреждение: вложенные циклы быстро наращивают сложность. Если внешний цикл выполняется 1000 раз, а внутренний — тоже 1000, итого будет миллион итераций. В игровых проектах это критично для производительности.

    Бесконечные циклы: намеренно и случайно

    Цикл while (true) — это бесконечный цикл, который работает, пока вы явно не вызовете break. Это нормальный паттерн для игровых циклов и серверов:

    А вот случайный бесконечный цикл — это баг. Если забыть изменить переменную условия внутри while, программа зависнет навсегда. Всегда проверяйте: что заставляет условие рано или поздно стать ложным?

    Теперь вы умеете управлять ходом программы — принимать решения и повторять действия. Следующий шаг — научиться группировать повторяющиеся блоки кода в функции, чтобы не копировать одни и те же инструкции снова и снова.

    3. Функции и модульность кода

    Функции и модульность кода

    Представьте повара, который каждый раз, когда ему нужно нарезать лук, заново читает инструкцию: взять нож, взять лук, положить на доску... Скоро рецепт на три блюда превратится в роман на сто страниц. Функции в программировании — это как фирменные рецепты: вы один раз описываете последовательность действий, даёте ей имя, и потом просто вызываете по имени, когда нужно.

    Зачем нужны функции

    Без функций любой нетривиальный проект превращается в бесконечную простыню кода. Функции решают три задачи:

  • Устранение дублирования — один и тот же код пишется один раз
  • Разделение ответственности — каждая функция делает одну вещь
  • Упрощение отладки — если что-то сломалось, вы точно знаете, в какой функции искать
  • Объявление и определение: две стороны одной медали

    В C++ функцию нужно сначала объявить (сказать компилятору, что она существует), а потом определить (написать её тело). Это как меню в ресторане: сначала вы видите список блюд, а потом официант приносит конкретное блюдо.

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

    Параметры и аргументы: передача данных

    Параметры — это переменные в объявлении функции (что она ожидает получить). Аргументы — конкретные значения при вызове (что вы передаёте).

    Передача по значению: копия документа

    По умолчанию C++ передаёт аргументы по значению — функция получает копию данных. Это как если бы вы сняли копию документа и отдали её коллеге: он может делать с копией что угодно, оригинал не пострадает.

    Передача по ссылке: оригинал на редактирование

    Если нужно, чтобы функция изменила исходную переменную, используют ссылки — это как передать оригинал документа с правом на исправления:

    Символ & после типа параметра создаёт ссылку — альтернативное имя для той же области памяти. Это одна из ключевых концепций C++, и мы подробнее разберём её в статье об указателях.

    Передача по константной ссылке: чтение без правки

    Когда нужно передать большой объект (например, строку или массив), копирование затратно. Но если функция только читает данные и не меняет их, используйте const-ссылку:

    Это как дать коллеге посмотреть документ через стекло — он видит содержимое, но не может ничего исправить. Такой приём совмещает эффективность (нет копирования) и безопасность (гарантия неизменности).

    Возвращаемое значение: результат работы

    Функция может возвращать результат через ключевое слово return. Тип возвращаемого значения указывается перед именем функции:

    Если функция ничего не возвращает, используется тип void — как работник, который выполняет действие, но не даёт вам никакого результата на руки (например, просто выводит текст на экран).

    Значения по умолчанию: необязательные параметры

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

    Параметры с значениями по умолчанию должны идти в конце списка параметров — иначе компилятор не поймёт, какой аргумент вы имели в виду.

    Перегрузка функций: одно имя, разные варианты

    C++ позволяет создавать несколько функций с одним именем, но с разными наборами параметров. Это называется перегрузкой (overloading). Компилятор сам выберет нужный вариант по типам и количеству аргументов:

    Это как слово «банк» — оно может означать финансовое учреждение или скамейку, и вы понимаете значение из контекста. Перегрузка делает код интуитивнее: вам не нужно запоминать имена maxInt, maxDouble, maxThree — везде просто максимум.

    Рекурсия: функция вызывает сама себя

    Иногда задача естественно раскладывается на подзадачи того же типа. Классический пример — вычисление факториала:

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

    > Важный принцип: каждая рекурсивная функция обязана иметь базовый случай. И на практике для простых итераций цикл for почти всегда эффективнее рекурсии в C++, потому что рекурсия потребляет стековую память на каждый вызов.

    Область видимости: где живут переменные

    Переменная, объявленная внутри функции, существует только внутри этой функции. Это как инструменты в мастерской: гаечный ключ из вашего гаража не существует в гараже соседа.

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

    Вы освоили инструменты для структурирования кода. Но пока мы работали с переменными напрямую — по имени. Впереди самая нетривиальная тема C++: указатели и прямое управление памятью. Именно они открывают дверь к низкоуровневому программированию и разработке игр.

    4. Указатели и управление памятью

    Указатели и управление памятью

    Представьте гостиничный коридор с сотнями дверей. За каждой дверью — номер с определённым содержимым. Вы можете обращаться к номеру по его номеру на двери — это и есть указатель: переменная, которая хранит не данные, а адрес данных в памяти. Именно эта концепция отличает C++ от высокоуровневых языков и даёт программисту власть — и ответственность.

    Адреса в памяти: нумерация комнат

    Каждая переменная в программе занимает место в оперативной памяти, и у этого места есть уникальный адрес — как почтовый адрес квартиры. Оператор & (в контексте выражения, не объявления) позволяет получить адрес переменной:

    Число вида 0x7ffd5e8a3b4c — это шестнадцатеричное представление адреса. Вам не нужно запоминать конкретные адреса — важно понимать, что они существуют и каждый уникален.

    Указатель: переменная-навигатор

    Указатель (pointer) — это переменная, которая хранит адрес другой переменной. Объявляется с помощью символа * после типа:

    Аналогия: score — это сам номер в отеле, а указательНаСчёт — это карточка с написанным на ней номером комнаты. Зная номер комнаты, вы можете дойти до неё и посмотреть, что внутри.

    Разыменование: заглянуть в комнату

    Чтобы получить значение по адресу, используют оператор разыменования * (тот же символ, но в выражении):

    Это как зайти в комнату по номеру с карточки и посмотреть, что там лежит. Через указатель можно не только читать, но и изменять значение:

    Переменная score изменилась, хотя мы обращались к ней через указатель. Это и есть сила указателей — доступ к данным по адресу.

    Указатель на указатель: карточка с адресом карточки

    Можно создать указатель, который хранит адрес другого указателя — как вторая карточка с номером комнаты, в которой лежит первая карточка:

    На практике двойные указатели встречаются редко, но понимание этой идеи помогает разобраться в работе сложных структур данных.

    Динамическая память: бронирование номеров

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

    Оператор new выделяет память в куче и возвращает адрес выделенного блока:

    После работы с динамической памятью её обязательно нужно освободить с помощью delete:

    > Представьте, что вы забронировали номер в отеле, но не выписались и уехали. Номер числится за вами, но он пуст — и никто другой заселиться не может. Это утечка памяти (memory leak). Каждый new должен сопровождаться парным delete.

    Динамические массивы

    Для создания массива с размером, известным только во время выполнения, используют new[] и delete[]:

    Здесь массив[i] — это синтаксический сахар для *(массив + i). Указатель на первый элемент массива и сам массив в C++ тесно связаны: имя массива ведёт себя как указатель на свой первый элемент.

    Умные указатели: автоматическая уборка

    Ручное управление памятью через new/delete чревато ошибками. В современном C++ (начиная с C++11) существуют умные указатели (smart pointers) — обёртки, которые автоматически освобождают память, когда указатель перестаёт использоваться.

    unique_ptr: один владелец

    std::unique_ptr — это указатель с единственным владельцем. Когда unique_ptr выходит из области видимости, память освобождается автоматически:

    Это как электронный замок с привязкой к одному ключу: ключ нельзя скопировать, и когда вы его выбрасываете, замок автоматически закрывается.

    shared_ptr: совместное владение

    std::shared_ptr позволяет нескольким указателям владеть одним объектом. Память освобождается, когда уничтожается последний shared_ptr, ссылающийся на объект:

    > Правило современного C++: всегда предпочитайте умные указатели сырым. Сырые указатели (new/delete) нужны только в особых случаях — например, при работе с legacy-кодом или в критичных по производительности участках.

    Null-указатель: пустая карточка

    Указатель может не указывать ни на что — это нулевой указатель. В C++ для этого используется nullptr (начиная с C++11):

    Разыменование нулевого указателя — это неопределённое поведение (undefined behavior), которое чаще всего приводит к аварийному завершению программы (крашу). Всегда проверяйте указатель перед разыменованием, если есть вероятность, что он может быть nullptr.

    Ссылки vs. указатели: когда что использовать

    | Свойство | Ссылка | Указатель | |----------|--------|-----------| | Синтаксис | int& ref = x; | int* ptr = &x; | | Может быть nullptr | Нет | Да | | Можно переназначить | Нет | Да | | Обязательно инициализировать | Да | Нет (но рекомендуется) | | Синтаксис доступа | Как обычная переменная | Нужно разыменование * |

    Для передачи параметров в функции в 95% случаев достаточно ссылок. Указатели нужны, когда требуется возможность указать на «ничто», переназначить цель или работать с динамической памятью.

    Вы освоили самый сложный и самый мощный инструмент C++. Теперь у вас есть полный набор: переменные, управление потоком, функции и прямой доступ к памяти. Осталось собрать всё вместе в объектно-ориентированную модель — и вы сможете строить полноценные проекты.

    5. Объектно-ориентированное программирование

    Объектно-ориентированное программирование

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

    Класс: чертёж автомобиля

    Класс (class) — это шаблон, по которому создаются объекты. Это как чертёж автомобиля: на чертеже указано, из чего состоит машина (данные) и что она умеет делать (методы). Сам чертёж — не машина, но по нему можно построить сколько угодно машин.

    Обратите внимание на ключевое слово public: — оно означает, что перечисленные ниже члены доступны извне. Класс завершается точкой с запятой — это обязательное требование C++, которое часто забывают.

    Объект: конкретная машина с конвейера

    Если класс — это чертёж, то объект — это конкретный экземпляр, построенный по этому чертежу:

    Каждый объект хранит свои собственные данные. Два объекта одного класса — это две разные машины, каждая со своей скоростью и своим пробегом.

    Инкапсуляция: закрыть капот на замок

    В примере выше поля марка, годВыпуска и скорость открыты — любой код может написать мояМашина.скорость = -500;, и машина поедет задом со сверхзвуковой скоростью. Инкапсуляция решает эту проблему: прячет данные за закрытыми дверями и предоставляет безопасный интерфейс.

    Ключевое слово private: скрывает поля от внешнего мира. Доступ к ним возможен только через публичные методы — как кассир в банке: вы не лезете в сейф сами, а просите кассира провести операцию. Конструктор (Автомобиль(std::string м, int год) : ...) — специальный метод, который вызывается при создании объекта и инициализирует его поля. Синтаксис после двоеточия — это список инициализации, более эффективный способ задать начальные значения.

    > Инкапсуляция — это не просто «спрятать данные». Это контроль над тем, как данные могут изменяться. Метод ускорить() проверяет, что значение положительное — вы не сможете случайно сломать логику программы.

    Наследование: семейство транспортных средств

    Наследование (inheritance) позволяет создавать новый класс на основе существующего, расширяя или изменяя его поведение. Это как генеалогическое дерево: ребёнок наследует черты родителей, но добавляет свои уникальные особенности.

    Класс Электромобиль наследует от Транспорт — он получает поле скорость и может переопределить метод перемещение(). Ключевое слово override явно указывает, что метод заменяет родительский — и компилятор проверит, что такой метод действительно существует в базовом классе.

    Модификатор protected: означает, что поле доступно самому классу и его наследникам, но не внешнему коду — промежуточный уровень между public и private.

    Типы наследования

    | Модификатор | Доступ для наследника | Аналогия | |-------------|----------------------|----------| | public | Наследует как есть | Семейная фамилия передаётся полностью | | protected | publicprotected | Секреты семьи, недоступные посторонним | | private | Всё становится private | Ничего не передаётся наружу |

    На практике в 90% случаев используется public наследование.

    Полиморфизм: одно имя, разные формы

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

    Функция путешествие() принимает указатель на Транспорт, но благодаря ключевому слову virtual и механизму позднего связывания (late binding) вызывается метод именно того класса, на который указывает указатель в данный момент. Это работает через таблицу виртуальных функций (vtable) — внутреннюю структуру, которую компилятор создаёт за кулисами.

    > Виртуальный деструктор (virtual ~Транспорт()) — обязательный элемент при использовании полиморфизма. Без него при удалении объекта через указатель базового класса будет вызван только базовый деструктор, и производный класс не сможет освободить свои ресурсы.

    Абстрактные классы: неполные чертежи

    Иногда базовый класс не должен создаваться сам по себе — он существует только как основа для наследников. Такой класс делают абстрактным, объявив хотя бы один чисто виртуальный метод:

    Символ = 0 после объявления метода означает, что базовый класс не предоставляет реализации — каждый наследник обязан её написать. Попытка создать объект абстрактного класса (Фигура f;) вызовет ошибку компиляции. Это как попытка построить здание по неполному чертежу — стены не могут висеть в воздухе.

    Композиция: собирать, а не наследовать

    Не всё является «частным случаем» чего-то. Двигатель — это не «вид автомобиля», а его часть. В таких случаях вместо наследования используют композицию — включение одного объекта в качестве поля другого:

    > Принцип проектирования: предпочитайте композицию наследованию. Наследование создаёт жёсткую связь между классами («is-a» — является), а композиция — гибкую («has-a» — содержит). Используйте наследование, когда один класс действительно является частным случаем другого, и композицию, когда один объект просто использует другой как компонент.

    Структуры: классы с открытыми дверями

    В C++ struct и class почти идентичны. Единственное отличие: в struct члены по умолчанию public, а в classprivate. По традиции struct используют для простых агрегатов данных без сложной логики:

    Если структура содержит методы, инкапсуляцию и наследование — используйте class. Если это просто контейнер для связанных данных — struct будет уместнее.

    Вы прошли полный путь от объявления переменных до проектирования объектов. C++ — это язык, который доверяет программисту: он не прячет память, не ограничивает архитектуру, но требует осознанности на каждом шагу. Именно это делает его незаменимым для разработки игр, где каждый такт процессора на счету, и для системного программирования, где нужен прямой контроль над железом.