C++

C++ — компилируемый язык общего назначения, на котором написаны игры (включая Unreal Engine), браузеры, операционные системы и высоконагруженные сервисы. Он даёт полный контроль над памятью и максимальную производительность.

Уровень: начальный 20 разделов ⏱ 3–4 часа

1. Что такое C++

C++ появился в 1985 году как расширение языка C. Код компилируется в машинный, поэтому программы работают быстро и не требуют виртуальной машины.

ХарактеристикаC++Python
Компиляцияда, в машинный кодинтерпретация
Скоростьмаксимальнаясредняя
Памятьуправляется вручную (или RAII)автоматически
Применениеигры, системы, высоконагруженные бэкендынаука, веб, скрипты

Синтаксис C++ кажется сначала строгим: обязательны точки с запятой, фигурные скобки и указание типов. За это ты получаешь предсказуемость и скорость.

Если ты пришёл из Python — не пугайся. Многие концепции (переменные, циклы, функции, классы) уже знакомы, отличается только синтаксис.

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

На Linux GCC обычно уже стоит. На Windows простое решение — MSYS2 (есть и установщик MinGW-w64) или пакет Build Tools от Visual Studio.

Проверь компилятор в терминале:

# Windows (MSYS2 / MinGW)
g++ --version

# Linux
sudo apt install g++ cmake
g++ --version

Скомпилируй и запусти первую программу:

# файл main.cpp
g++ main.cpp -o app
./app
Флаг -o app задаёт имя исполняемого файла. Флаг -Wall включает предупреждения компилятора — всегда добавляй его при обучении.

3. Первая программа

Классическое приветствие мира:

#include <iostream>

int main() {
    std::cout << "Привет, мир!" << std::endl;
    return 0;
}

Разбор по строкам:

Ввод данных с клавиатуры:

#include <iostream>

int main() {
    int age = 0;
    std::cout << "Сколько тебе лет? ";
    std::cin >> age;
    std::cout << "Тебе " << age << " лет" << std::endl;
    return 0;
}
В коде всегда должны быть точки с запятой после инструкций и закрывающая скобка у каждой функции. Самая частая ошибка новичка — именно её потеря.

4. Переменные и типы данных

Каждая переменная имеет тип — компилятор проверяет его на этапе сборки:

#include <iostream>
#include <string>

int main() {
    int count = 10;
    double price = 19.99;
    char grade = 'A';
    bool isDone = true;
    std::string name = "Аня";

    std::cout << name << " " << count << std::endl;
    return 0;
}

Тип можно не писать явно — его выведет компилятор с auto. Константы объявляются через const:

auto score = 100;        // int
auto text = "hello";     // const char*
const double PI = 3.14159;

Фигурные скобки проверяют, что значение помещается в тип:

int big = {100};       // ок
int small = {1.5};     // ошибка: сужение типа
Логические значения: && (И), || (ИЛИ), ! (НЕ). В коде они записываются латиницей и выглядят как a && b.

5. Условия и циклы

Условный оператор:

int score = 85;

if (score >= 90) {
    std::cout << "Отлично" << std::endl;
} else if (score >= 60) {
    std::cout << "Хорошо" << std::endl;
} else {
    std::cout << "Надо подтянуть" << std::endl;
}

Три вида циклов:

// for — заранее известно число повторов
for (int i = 0; i < 5; i++) {
    std::cout << i << " ";
}

// while — проверка в начале
int n = 3;
while (n > 0) {
    n--;
}

// do-while — тело выполнится хотя бы один раз
int m = 0;
do {
    m++;
} while (m < 3);

Оператор выбора для нескольких вариантов:

#include <iostream>

int main() {
    char c = 'a';
    switch (c) {
        case 'a':
            std::cout << "гласная" << std::endl;
            break;
        case 'b':
            std::cout << "согласная" << std::endl;
            break;
        default:
            std::cout << "что-то ещё" << std::endl;
    }
    return 0;
}
О шаге i++: это инкремент, то же самое, что i = i + 1. В заголовке цикла сравни i < 5 — от 0 до 4 включительно.

6. Функции

Функция имеет тип возврата, имя, параметры и тело:

int add(int a, int b) {
    return a + b;
}

double average(double x, double y) {
    return (x + y) / 2.0;
}

bool isEven(int n) {
    return n % 2 == 0;
}

Вызов из main:

#include <iostream>

int add(int a, int b);

int main() {
    int sum = add(3, 4);
    std::cout << sum << std::endl;
    return 0;
}

int add(int a, int b) {
    return a + b;
}

Перегрузка — несколько функций с одним именем и разными параметрами:

void print(int x)   { std::cout << x << std::endl; }
void print(double x){ std::cout << x << std::endl; }
void print(std::string s){ std::cout << s << std::endl; }
Перед вызовом функции компилятор должен «видеть» её объявление. Поэтому либо определяй функцию до main, либо пиши объявление (прототип) заранее.

7. Указатели и ссылки

Указатель хранит адрес переменной в памяти:

#include <iostream>

int main() {
    int x = 42;
    int* p = &x;   // адрес x

    std::cout << p << std::endl;   // 0x... адрес
    std::cout << *p << std::endl;  // 42 разыменование

    *p = 7;       // меняем x через указатель
    std::cout << x << std::endl;   // 7
    return 0;
}

Ссылка — «псевдоним» переменной, работает как безопасный указатель:

int y = 100;
int& ref = y;   // ссылка на y
ref = 55;
std::cout << y << std::endl;   // 55

Главное применение — передача в функцию без копирования:

#include <iostream>

void increment(int& n) {
    n++;
}

int main() {
    int score = 10;
    increment(score);
    std::cout << score << std::endl;   // 11
    return 0;
}
nullptr — «пустой» указатель. Проверяй его перед разыменованием: if (p != nullptr).

8. Память: стек, куча и RAII

Память бывает двух видов:

int* arr = new int[100];   // выделить в куче
arr[0] = 42;

delete[] arr;              // освободить (обязательно!)
Забытый delete — утечка памяти. Надёжнее использовать умные указатели: std::unique_ptr и std::shared_ptr из #include <memory>.
#include <iostream>
#include <memory>

int main() {
    auto p = std::make_unique<int>(100);
    std::cout << *p << std::endl;   // 100, память сама освободится
    return 0;
}

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

9. Классы и ООП

Класс объединяет данные и методы:

#include <iostream>
#include <string>

class BankAccount {
public:
    BankAccount(std::string owner, double balance)
        : name(owner), money(balance) {}

    void deposit(double amount) {
        money += amount;
    }

    double getBalance() const {
        return money;
    }

private:
    std::string name;
    double money = 0.0;
};

int main() {
    BankAccount acc("Аня", 1000);
    acc.deposit(500);
    std::cout << acc.getBalance() << std::endl;   // 1500
    return 0;
}

public — данные доступны снаружи, private — только внутри класса. Конструктор инициализирует объект; список инициализации : name(owner), money(balance) заполняет поля до тела конструктора.

// по умолчанию поля структуры открыты
struct Point {
    double x = 0.0;
    double y = 0.0;
};

Point p;
p.x = 10.0;
p.y = 20.0;

10. Наследование и полиморфизм

Наследование позволяет переиспользовать код базового класса:

#include <iostream>
#include <string>

class Animal {
public:
    Animal(std::string n) : name(n) {}
    virtual void speak() const {
        std::cout << name << " издаёт звук" << std::endl;
    }
    virtual ~Animal() = default;

protected:
    std::string name;
};

class Dog : public Animal {
public:
    Dog(std::string n) : Animal(n) {}
    void speak() const override {
        std::cout << name << " гавкает" << std::endl;
    }
};

int main() {
    Dog dog("Барсик");
    dog.speak();   // Барсик гавкает
    return 0;
}

virtual включает полиморфизм: вызов метода у объекта базового класса через указатель вызывает версию производного класса.

Animal* a = new Dog("Рекс");
a->speak();   // Рекс гавкает
delete a;

Абстрактный класс — с чисто виртуальным методом (присвоено = 0), такие классы нельзя создавать напрямую:

class Shape {
public:
    virtual double area() const = 0;
    virtual ~Shape() = default;
};

class Circle : public Shape {
public:
    Circle(double r) : radius(r) {}
    double area() const override {
        return 3.14159 * radius * radius;
    }
private:
    double radius;
};

11. STL — стандартная библиотека

STL даёт готовые контейнеры, алгоритмы и строки:

#include <iostream>
#include <vector>
#include <string>
#include <algorithm>

int main() {
    std::vector<int> nums = {5, 3, 8, 1};

    std::sort(nums.begin(), nums.end());
    for (int v : nums) {
        std::cout << v << " ";   // 1 3 5 8
    }

    auto it = std::find(nums.begin(), nums.end(), 8);
    if (it != nums.end()) {
        std::cout << "\nНашли 8" << std::endl;
    }

    std::string word = "hello";
    word += " c++";
    std::cout << word.length() << std::endl;   // 9
    return 0;
}

Ассоциативные контейнеры — пары «ключ-значение»:

#include <iostream>
#include <map>

int main() {
    std::map<std::string, int> scores;
    scores["Аня"] = 95;
    scores["Боря"] = 88;

    std::cout << scores["Аня"] << std::endl;   // 95

    for (const auto& pair : scores) {
        std::cout << pair.first << ": " << pair.second << std::endl;
    }
    return 0;
}
Строки — это std::string, а не голые массивы символов. Используй контейнеры STL вместо ручных массивов: проще и безопаснее.

12. Файлы и CMake

Запись и чтение файлов:

#include <iostream>
#include <fstream>
#include <string>

int main() {
    std::ofstream out("notes.txt");
    out << "Строка первая" << std::endl;
    out << "Строка вторая" << std::endl;
    out.close();

    std::ifstream in("notes.txt");
    std::string line;
    while (std::getline(in, line)) {
        std::cout << line << std::endl;
    }
    return 0;
}

Для больших проектов используют систему сборки CMake. Простейший CMakeLists.txt:

cmake_minimum_required(VERSION 3.20)
project(App VERSION 1.0)

set(CMAKE_CXX_STANDARD 17)
set(CMAKE_CXX_STANDARD_REQUIRED ON)

add_executable(app main.cpp utils.cpp)
cmake -S . -B build
cmake --build build
./build/app

13. Move-семантика и умные указатели

С++11 добавил семантику перемещения: вместо копирования объекта ресурсы можно «передать» новому владельцу. Для этого используются rvalue-ссылки и конструктор перемещения:

std::vector<int> big = {1, 2, 3};
std::vector<int> other = std::move(big);   // обмен содержимым, без копирования

Умные указатели из <memory> заменяют ручные new/delete:

auto p = std::make_unique<Widget>();
std::shared_ptr<Widget> s = std::make_shared<Widget>();
Старайся возвращать по значению: современные компиляторы применяют RVO/NRVO, и лишняя копия часто просто не создаётся.

14. Обработка ошибок: исключения

Исключения отделяют логику ошибок от основного потока. Базовый паттерн:

double divide(double a, double b) {
    if (b == 0.0) throw std::invalid_argument("деление на ноль");
    return a / b;
}

try {
    double r = divide(1.0, 0.0);
} catch (const std::exception& e) {
    std::cerr << "Ошибка: " << e.what() << '\n';
}
Не используй исключения для потока управления в хот-путях (например, в циклах по 100k элементов) — накладные расходы велики.

15. Многопоточность: std::thread и мьютексы

Многопоточность в C++ не требует внешних библиотек — достаточно <thread>:

std::mutex mtx;
int counter = 0;

auto worker = [&]() {
    for (int i = 0; i < 100; ++i) {
        std::lock_guard<std::mutex> lock(mtx);  // RAII-блокировка
        ++counter;
    }
};

std::thread t1(worker), t2(worker);
t1.join(); t2.join();
std::cout << counter;  // 200
Общая память + незащищённое изменение = data race. Это неопределённое поведение: программа может падать «странно» и не воспроизводимо. Проверяй сборки с санитайзерами.

16. Инструменты качества: dlv, санитайзеры, профилирование

Отладка C++ не сводится к «printf»:

# сборка с санитайзерами ловит утечки и data race
g++ -fsanitize=address,thread -g main.cpp -o app
./app
# gdb: точки останова и трассировка
gdb ./app
break main.cpp:42
run
bt        # стек вызовов
print x
# анализ производительности
perf record ./app
perf report
Курс Linux научит работать с gdb и утилитами для анализа производительности; автоматизация сборки и CI — из курса GitLab.

17. const и constexpr: корректность и быстродействие

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

const int port = 8080;             // не меняется после инициализации
constexpr int kBytes = 1024 * 64;  // вычисляется на этапе компиляции

constexpr int square(int x) { return x * x; }
int a[square(4)];                  // константное выражение в размере
Ошибка «can't assign to const» — не брак кода, а знак: убирай изменение, а не const.

18. Паттерны: Pimpl и фабрики

Pimpl (pointer to implementation) прячет приватные поля в отдельную структуру: заголовок меньше, перекомпиляция зависимых единиц — быстрее:

class Widget {
public:
  explicit Widget(int id);
  ~Widget();
private:
  struct Impl;                    // только объявление
  std::unique_ptr<Impl> pimpl_;
};

Фабрика собирает сложные объекты в одном месте, избавляя от разбросанных new:

std::unique_ptr<Report> make_report(ReportType t);
Не внедряй паттерны ради паттернов: Pimpl нужен там, где важен размер заголовка или время сборки, фабрика — где создание непростое.

19. Взаимодействие с C и внешние библиотеки

C++ легко линкуется с C-кодом, но имена функций в C++ меняются (name mangling). Оберни заголовки в extern "C":

#ifdef __cplusplus
extern "C" {
#endif
#include "mylib.h"
#ifdef __cplusplus
}
#endif
// сборка: g++ -I. -L. -mylib main.cpp -o app
app // -l префикс «lib» добавляется автоматически
После обновления двоичной библиотеки пересобирай клиентов: несовместимый ABI — источник «магических» падений в рантайме.

20. Основы сетей: TCP-сокеты

Простой TCP-клиент на системных сокетах:

#include <sys/socket.h>
#include <arpa/inet.h>
int fd = socket(AF_INET, SOCK_STREAM, 0);
sockaddr_in addr{};
addr.sin_family = AF_INET;
addr.sin_port = htons(8080);
inet_pton(AF_INET, "127.0.0.1", &addr.sin_addr);
connect(fd, (sockaddr*)&addr, sizeof(addr));
send(fd, "ping", 4, 0);
close(fd);
На Windows сокеты инициализируются WSAStartup (winsock2.h); единая кроссплатформенная обёртка — библиотека Boost.Asio.

21. Компиляция и линковка: три этапа

«Работает на моей машине» на C++ любит мстить. Разберись в этапах сборки:

  1. препроцессинг → компиляция в объектники → линковка;
  2. статические библиотеки (.a) встраиваются в бинарь, динамические (.so/.dll) подгружаются при запуске;
  3. линкер берёт только нужные объектники — отсюда «undefined reference».
g++ -std=c++20 -O2 -Wall -Wextra main.cpp lib/utils.a -o app
nm app | grep someSymbol   # есть ли символ в бинаре
Флаги сборки повторяй по чек-листу: -Wall -Wextra -Werror на CI ловят большинство наивных ошибок до дедлайна.

22. Парсинг аргументов и конфигов

Программа без удобного запуска — игрушка. Учись делать CLI правильно с нуля:

#include <CLI/CLI.hpp>
CLI::App app{"Сборщик отчётов"};
int workers = 4;
app.add_option("-j,--jobs", workers, "потоков")->check(CLI::Range(1, 64));
// --config, --help и версии — из коробки
CLI11 или cxxopts спасут от самодельного strcmp-парсера: меньше кода — меньше багов.

23. Сериализация: JSON и бинарные форматы

Обмен данными — вечный спутник C++-сервисов. Выбор формата — компромисс:

ФорматПлюсыКогда брать
JSON (nlohmann)читаемость, экосистемаконфиги, REST, отладка
Protobuf/FlatBuffersскорость, размермикросервисы, streaming
CSV/TSVпрост и всем понятендампы, аналитика
#include <nlohmann/json.hpp>
nlohmann::json j = { {"user", "alice"}, {"n", 42} };
auto s = j.dump(2);
Экранируй строки только через библиотеку: самодельные \u-переходы — классический источник «битых» данных.

24. Кейс: рефакторинг легаси безопасными шагами

Легаси не переписывают «за выходные». Стратегия «выкрути винт по одному»:

  1. покрой код характеристическими тестами (input → output, не внутренности);
  2. вынеси новый модуль рядом, оставив медленный роутер (strangler pattern);
  3. переноси логику траншами, каждый за закрытым PR.
// старый путь остаётся, новый включается флагом
if (useNewEngine) runNew(); else runLegacy();
Держи diff маленьким и сравнивай поведение, а не текст: рефакторинг ради красоты без тестов — мина замедленного действия.

Что дальше

C++ — фундамент разработки игр. Если цель — создание игр, переходи к Unreal Engine 5, где C++ используется для игровой логики. Для командной работы и CI/CD пригодится GitLab, а системное администрирование и сборка в Linux описаны в курсе Linux.

← Вернуться к курсам