C++
C++ — компилируемый язык общего назначения, на котором написаны игры (включая Unreal Engine), браузеры, операционные системы и высоконагруженные сервисы. Он даёт полный контроль над памятью и максимальную производительность.
1. Что такое C++
C++ появился в 1985 году как расширение языка C. Код компилируется в машинный, поэтому программы работают быстро и не требуют виртуальной машины.
| Характеристика | C++ | Python |
|---|---|---|
| Компиляция | да, в машинный код | интерпретация |
| Скорость | максимальная | средняя |
| Память | управляется вручную (или RAII) | автоматически |
| Применение | игры, системы, высоконагруженные бэкенды | наука, веб, скрипты |
Синтаксис C++ кажется сначала строгим: обязательны точки с запятой, фигурные скобки и указание типов. За это ты получаешь предсказуемость и скорость.
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()— точка входа программы, возвращает код завершения.std::cout— поток вывода;<<отправляет в него текст.std::endl— перевод строки.
Ввод данных с клавиатуры:
#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
Память бывает двух видов:
- Стек — быстрый, но маленький; автоматически освобождается при выходе из функции.
- Куча — большой, время жизни управляется вручную через
newиdelete.
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/app13. Move-семантика и умные указатели
С++11 добавил семантику перемещения: вместо копирования объекта ресурсы можно «передать» новому владельцу. Для этого используются rvalue-ссылки и конструктор перемещения:
std::vector<int> big = {1, 2, 3};
std::vector<int> other = std::move(big); // обмен содержимым, без копированияУмные указатели из <memory> заменяют ручные new/delete:
std::unique_ptr— единоличное владение;std::shared_ptr— совместное владение со счётчиком ссылок;std::weak_ptr— «наблюдатель», ломает циклические ссылки.
auto p = std::make_unique<Widget>();
std::shared_ptr<Widget> s = std::make_shared<Widget>();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';
}- бросай по значению, лови по константной ссылке;
- порядок catch-блоков: от узкого исключения к широкому;
- гарантийно измеряй исключения: успел выделить — освободи (умные указатели спасают);
- для функций, которые не бросают, указывай
noexcept.
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; // 200std::lock_guard— блокировка на время жизни;std::atomic— для простых счётчиков безопаснее мьютекса;std::async— запуск асинхронной задачи и получение результата.
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 report17. 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)]; // константное выражение в размереconst-методы можно вызывать на константных объектах: opaque «const-корректность» API;- параметры-ссылки читай как
const std::string&— без копии и без права мутации; - если значение можно посчитать в compile-time — используй
constexpr, а не макрос.
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 без объявленных деструктора и копирования — ошибка компиляции у
unique_ptr, поэтому ставь их явно; - фабрики удобно тестировать: подменяешь реальный объект заглушкой на входе.
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» добавляется автоматически- заголовочные guard
#pragma onceспасают от двойного включения; - динамические библиотеки (.so/.dll) ищут в LD_LIBRARY_PATH / PATH;
- не ломай 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);- порядок байт:
htons/ntohsпереводят в сетевой порядок; - используй
recvв цикле: одно сообщение может прийти частями; - не забывай закрывать дескрипторы — иначе дескриптор «утекает».
WSAStartup (winsock2.h); единая кроссплатформенная обёртка — библиотека Boost.Asio.21. Компиляция и линковка: три этапа
«Работает на моей машине» на C++ любит мстить. Разберись в этапах сборки:
- препроцессинг → компиляция в объектники → линковка;
- статические библиотеки (
.a) встраиваются в бинарь, динамические (.so/.dll) подгружаются при запуске; - линкер берёт только нужные объектники — отсюда «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 и версии — из коробки- говори правду:
--helpи выходные коды 0 / != 0; - конфиг в JSON/TOML читай один раз и валидируй типами;
- о неизвестном флаге печатай ошибку и возвращай 1 — не молчи.
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);24. Кейс: рефакторинг легаси безопасными шагами
Легаси не переписывают «за выходные». Стратегия «выкрути винт по одному»:
- покрой код характеристическими тестами (input → output, не внутренности);
- вынеси новый модуль рядом, оставив медленный роутер (strangler pattern);
- переноси логику траншами, каждый за закрытым PR.
// старый путь остаётся, новый включается флагом
if (useNewEngine) runNew(); else runLegacy();Что дальше
C++ — фундамент разработки игр. Если цель — создание игр, переходи к Unreal Engine 5, где C++ используется для игровой логики. Для командной работы и CI/CD пригодится GitLab, а системное администрирование и сборка в Linux описаны в курсе Linux.