Один слайд — один вопрос. Сначала разбираем, зачем это нужно и как работает, потом смотрим код, где почти каждая строка снабжена комментарием.
Зачем это нужно. Любая программа на C++ начинается с одного и того же каркаса. Если понять его один раз, дальше ты просто добавляешь внутрь main нужные действия.
Как это работает. Компилятор ищет функцию main — это точка входа. Внутри неё выполняется код сверху вниз. std::cout — это поток вывода: всё, что ты в него «отправляешь» через <<, появляется на экране. return 0 сообщает операционной системе: «программа завершилась успешно». Если вернуть не 0, ОС поймёт, что была ошибка.
// Подключаем библиотеку ввода-вывода, чтобы был доступен std::cout
#include <iostream>
// Точка входа: с этой функции начинается выполнение программы
int main() {
// Отправляем строку в поток вывода на экран
std::cout << "Привет, мир!" << std::endl;
// Возвращаем 0 — программе всё удалось
return 0;
}
Сборка: g++ main.cpp -std=c++17 -Wall -o app, запуск: ./app.
Зачем это нужно. Программа хранит данные в переменных. Тип говорит компилятору, что именно там лежит: целое число, дробное, символ или строка. От типа зависит, какие операции разрешены и сколько памяти выделить.
Как это работает. При объявлении ты создаёшь «ячейку» с именем и типом. Если сразу не задать значение, внутри может оказаться мусор — поэтому лучше инициализировать. auto — это не «любой тип», а подсказка компилятору: «посмотри на значение справа и выведи тип сам». Тип всё равно остаётся строгим.
// Библиотека для вывода на экран
#include <iostream>
// Библиотека для строк std::string
#include <string>
int main() {
int age = 17; // целое число
double avg = 4.75; // дробное число
char grade = 'A'; // один символ в одинарных кавычках
bool ok = true; // логическое значение
std::string name = "Ира"; // строка
auto year = 2025; // компилятор сам поймёт: int
// Склеиваем строку, имя, возраст и перевод строки
std::cout << name << ", " << age << " лет\n";
}
Зачем это нужно. Деление — самая частая ловушка новичка. Если делить два int, C++ отбросит дробную часть, и результат может оказаться не тем, что ты ждёшь.
Как это работает. / ведёт себя по-разному в зависимости от типов: int / int — целочисленное деление, double / int — обычное. Оператор % даёт остаток и работает только с целыми. Если нужно дробное — сделай хотя бы один операнд double, явно через static_cast, чтобы намерение было видно в коде.
#include <iostream>
int main() {
int a = 7; // первое число
int b = 2; // второе число
std::cout << a / b << "\n"; // 7 / 2 = 3 (дробная часть отбрасывается)
std::cout << a % b << "\n"; // остаток от деления: 1
std::cout << a / 2.0 << "\n"; // 3.5: одно из чисел double
// static_cast явно превращает int в double перед делением
std::cout << static_cast<double>(a) / b << "\n";
}
Зачем это нужно. Без условий программа всегда делает одно и то же. Условия позволяют ей «выбирать» поведение в зависимости от данных: оценка, возраст, статус, ошибка.
Как это работает. if проверяет выражение: если оно истинно — выполняется блок в фигурных скобках. else if проверяется, только если предыдущие условия ложны. else — «во всех остальных случаях». Цепочка читается сверху вниз и останавливается на первом сработавшем условии.
#include <iostream>
int main() {
int mark = 4; // оценка для проверки
if (mark == 5) { // если ровно 5
std::cout << "Отлично\n";
} else if (mark == 4) { // иначе, если ровно 4
std::cout << "Хорошо\n";
} else if (mark >= 3) { // иначе, если 3 и больше
std::cout << "Удовлетворительно\n";
} else { // во всех остальных случаях
std::cout << "Неудовлетворительно\n";
}
}
Зачем это нужно. Циклы позволяют не копировать один и тот же код десять раз. Таблицы, перебор массивов, подсчёты — всё это делается циклом.
Как это работает. У for три части: инициализация (создаём счётчик), условие (пока истинно — крутимся) и шаг (что делать после каждой итерации). Тело выполняется, пока условие истинно. std::setw задаёт минимальную ширину поля — так колонки выравниваются.
#include <iostream>
#include <iomanip> // здесь живёт std::setw
int main() {
// i начинается с 1, крутимся, пока i <= 5, каждый шаг увеличиваем i
for (int i = 1; i <= 5; i++) {
std::cout << std::setw(3) << i // колонка шириной 3: число
<< std::setw(5) << i * i // колонка шириной 5: квадрат
<< "\n"; // конец строки таблицы
}
}
while отличается от do-while?Зачем это нужно. Оба цикла повторяют действия, но проверяют условие в разное время. Это важно, когда тело должно выполниться хотя бы раз (например, ввод с проверкой).
Как это работает. while сначала смотрит на условие: если оно ложно с самого начала — тело не выполнится ни разу. do-while сначала делает шаг, а потом проверяет: тело гарантированно выполнится один раз. Выбор зависит от того, допустим ли «нулевой» проход.
#include <iostream>
int main() {
int x = 5; // начальное значение
// Проверяем x < 3 — это ложь, поэтому тело не выполнится
while (x < 3) {
std::cout << "while: " << x++ << "\n";
}
// Тело выполнится один раз, потом проверим x > 3
do {
std::cout << "do-while: " << x-- << "\n";
} while (x > 3);
}
Зачем это нужно. Функции разбивают программу на маленькие понятные куски. Вместо одного длинного main получаются отдельные «кирпичи», которые можно переиспользовать и тестировать.
Как это работает. В объявлении указывают тип результата, имя и параметры. При вызове передают аргументы — конкретные значения. return возвращает результат в то место, откуда функцию вызвали. Если функция ничего не возвращает, пишут void.
#include <iostream>
// Функция возвращает int, принимает два int
int max(int a, int b) {
// Тернарный оператор: если a > b вернуть a, иначе b
return a > b ? a : b;
}
// Функция возвращает double, принимает два double
double avg(double a, double b) {
return (a + b) / 2.0; // среднее арифметическое
}
int main() {
std::cout << max(3, 7) << "\n"; // вызов max: получим 7
std::cout << avg(4.0, 5.0) << "\n"; // вызов avg: получим 4.5
}
Зачем это нужно. По умолчанию аргументы копируются. Если функция должна изменить переменную снаружи — например, поменять местами два значения — копия не поможет. Нужна ссылка.
Как это работает. Знак & в типе параметра означает «это не копия, а псевдоним исходной переменной». Всё, что функция делает с параметром, происходит с оригиналом. Ссылку нельзя «переприсвоить» на другой объект — она всегда связана с тем, что передали при вызове.
#include <iostream>
// a и b — ссылки: это те же переменные, что передали при вызове
void swap(int& a, int& b) {
int t = a; // сохранили значение a во временную t
a = b; // в a положили значение b
b = t; // в b положили старое значение a
}
int main() {
int x = 3; // первая переменная
int y = 7; // вторая переменная
swap(x, y); // меняем значения местами
std::cout << x << " " << y << "\n"; // 7 3
}
& изменения останутся в копиях и не повлияют на x и y.std::vector?Зачем это нужно. Обычный массив в C++ имеет фиксированный размер и не знает свою длину. vector умеет расти, помнит размер и сам управляет памятью — это контейнер по умолчанию для большинства задач.
Как это работает. std::vector<int> — динамический массив целых. push_back добавляет элемент в конец, size возвращает количество. Range-based for перебирает элементы без индексов и без риска выйти за границы.
#include <iostream>
#include <vector> // контейнер std::vector
int main() {
// Создаём vector из int и сразу заполняем тремя значениями
std::vector<int> v = {3, 1, 2};
v.push_back(4); // добавили 4 в конец
// Range-based for: перебираем каждый элемент x из v
for (int x : v) std::cout << x << " ";
// size() возвращает текущее количество элементов
std::cout << "\nРазмер: " << v.size() << "\n";
}
Зачем это нужно. Текст встречается везде: имена, сообщения, файлы. В C++ для этого есть std::string — безопасная замена «голым» массивам символов.
Как это работает. Строка помнит свой размер, умеет склеиваться через +, искать символы через find и вырезать части через substr. В отличие от C-строк, тебе не нужно вручную следить за нулевым символом в конце и длиной буфера.
#include <iostream>
#include <string> // строка std::string
int main() {
std::string name = "Алексей"; // исходная строка
// Конкатенация через + склеивает три части в одну строку
std::string hi = "Привет, " + name + "!";
std::cout << hi << "\n"; // печатаем результат
std::cout << "Длина: " << hi.size() << "\n"; // число символов
// substr(начало, длина): берём 7 символов с позиции 0
std::cout << "Первые 7: " << hi.substr(0, 7) << "\n";
// find ищет символ и возвращает позицию (или npos, если нет)
std::cout << "Позиция 'А': " << hi.find('А') << "\n";
}
Зачем это нужно. Указатели дают доступ к памяти напрямую: через них работают динамические структуры, передача больших объектов без копирования и взаимодействие с функциями, которые меняют данные «на месте».
Как это работает. Указатель хранит адрес, а не само значение. &x — «возьми адрес x», *p — «сходи по адресу и возьми значение». Через *p можно и прочитать, и изменить оригинал. Если указатель никуда не указывает, он должен быть nullptr, и перед разыменованием его нужно проверять — иначе программа упадёт.
#include <iostream>
int main() {
int x = 42; // обычная переменная
int* p = &x; // p хранит адрес x
std::cout << "x = " << x << "\n"; // значение x: 42
std::cout << "*p = " << *p << "\n"; // разыменование: тоже 42
*p = 100; // через указатель меняем x
std::cout << "x после *p = 100: " << x << "\n";
int* q = nullptr; // указатель «ни на что»
// Проверяем: если q не нулевой, только тогда разыменовываем
if (q) std::cout << *q;
}
Зачем это нужно. Ручной new/delete легко забыть — и память утечёт. Ещё хуже, если исключение прервёт код до delete. Умные указатели решают это автоматически.
Как это работает. unique_ptr — единственный владелец объекта: когда он выходит из области видимости, память освобождается. shared_ptr — общий владелец: он считает, сколько указателей смотрят на объект, и удаляет его, когда счётчик станет нулём. Это и есть RAII: ресурс живёт ровно столько, сколько живёт владелец.
#include <iostream>
#include <memory> // умные указатели
int main() {
// make_unique создаёт объект и передаёт его unique_ptr
auto p = std::make_unique<int>(42);
std::cout << *p << "\n"; // разыменовываем: 42
// make_shared создаёт объект с общим владением
auto s1 = std::make_shared<int>(7);
// Копия shared_ptr увеличивает счётчик владельцев
auto s2 = s1;
std::cout << *s2 << ", use_count = " << s1.use_count() << "\n";
} // память освободится автоматически
Зачем это нужно. Класс позволяет держать данные и операции над ними вместе. Это удобнее, чем куча отдельных переменных и функций: объект сам знает, как с собой работать.
Как это работает. Поля класса по умолчанию закрыты, снаружи доступно только то, что в public. Конструктор вызывается при создании объекта и задаёт начальные значения. Список инициализации : name(n), age(a) заполняет поля до тела конструктора — это правильный и эффективный способ.
#include <iostream>
#include <string>
class Student {
std::string name; // закрытое поле: имя
int age; // закрытое поле: возраст
public: // всё ниже — доступно снаружи
// Конструктор: name(n) и age(a) — список инициализации
Student(std::string n, int a) : name(n), age(a) {}
void print() const { // const: метод не меняет объект
std::cout << name << ", " << age << " лет\n";
}
};
int main() {
Student s("Ира", 18); // создаём объект: вызов конструктора
s.print(); // вызываем метод
}
Зачем это нужно. Если у нескольких классов есть общее поведение, его не нужно копировать. Выносим общее в базовый класс — и все наследники получают его бесплатно.
Как это работает. class Dog : public Animal означает: «Dog — это Animal, он умеет всё, что умеет Animal, плюс своё». Наследник видит public- и protected-члены базы. Это отношение «является» (is-a): собака — животное, но не наоборот.
#include <iostream>
class Animal {
public:
void eat() const { // общий метод для всех животных
std::cout << "Ест\n";
}
};
// Dog наследует Animal: получает метод eat()
class Dog : public Animal {
public:
void bark() const { // собственный метод
std::cout << "Гав!\n";
}
};
int main() {
Dog d; // создаём объект собаки
d.eat(); // метод из базового класса
d.bark(); // метод из производного класса
}
Зачем это нужно. Без полиморфизма код, который работает с базовым типом, не может вызвать поведение наследника. С virtual — может: выбирается реализация по фактическому типу объекта.
Как это работает. virtual говорит компилятору: «выбирай метод во время выполнения, а не по типу указателя». = 0 делает функцию чисто виртуальной: у базового класса нет реализации, наследники обязаны её дать. Класс с такой функцией становится абстрактным — создать его объект нельзя. Виртуальный деструктор нужен, чтобы при удалении через указатель на базу вызвался деструктор наследника.
#include <iostream>
class Shape {
public:
// Чисто виртуальная функция: у Shape нет своей реализации
virtual double area() const = 0;
// Виртуальный деструктор: чтобы удаление через Shape* было безопасным
virtual ~Shape() = default;
};
class Circle : public Shape {
double r; // радиус
public:
Circle(double r) : r(r) {} // конструктор
// override: подтверждаем, что переопределяем виртуальную функцию
double area() const override { return 3.14159 * r * r; }
};
int main() {
// Указатель базового типа смотрит на объект Circle
Shape* s = new Circle(2.0);
std::cout << s->area() << "\n"; // вызовется Circle::area
delete s; // освобождаем память
}
Зачем это нужно. Без шаблонов пришлось бы писать отдельную функцию для int, отдельную для double, отдельную для строк — хотя логика одна и та же. Шаблон избавляет от копирования.
Как это работает. template <class T> — это параметр-тип. Когда ты вызываешь функцию, компилятор подставляет фактический тип вместо T и создаёт конкретную версию. Ошибки в шаблоне видны при подстановке типа, поэтому важно, чтобы для T были доступны нужные операции (например, >).
#include <iostream>
#include <string>
// Шаблон: T — параметр-тип, подставится при вызове
template <class T>
T maxOf(T a, T b) {
// Сравнение работает для любого T, у которого есть оператор >
return a > b ? a : b;
}
int main() {
// Компилятор создаёт maxOf для int
std::cout << maxOf(3, 7) << "\n";
// Компилятор создаёт maxOf для double
std::cout << maxOf(2.5, 1.8) << "\n";
// Компилятор создаёт maxOf для std::string
std::cout << maxOf(std::string("a"), std::string("b")) << "\n";
}
vector?Зачем это нужно. Сортировка и поиск встречаются постоянно: списки, таблицы, рейтинги. Писать их вручную не нужно — в STL уже есть готовые и быстрые алгоритмы.
Как это работает. Алгоритмы STL работают с диапазоном итераторов [begin, end). sort упорядочивает элементы, find возвращает итератор на найденный элемент или end(), если ничего не нашёл. Проверка it != v.end() — обязательна, иначе можно обратиться к «несуществующему» элементу.
#include <iostream>
#include <vector>
#include <algorithm> // sort и find
int main() {
std::vector<int> v = {5, 2, 8, 1}; // исходные данные
// sort сортирует диапазон [begin, end) по возрастанию
std::sort(v.begin(), v.end());
for (int x : v) std::cout << x << " "; // печать: 1 2 5 8
std::cout << "\n";
// find ищет 8 и возвращает итератор на найденный элемент
auto it = std::find(v.begin(), v.end(), 8);
if (it != v.end()) { // проверка: точно нашли?
// it - v.begin() — расстояние от начала, т.е. индекс
std::cout << "8 на позиции " << (it - v.begin()) << "\n";
} else {
std::cout << "8 не найден\n";
}
}
Зачем это нужно. Алгоритмам вроде for_each или count_if нужно передать «что делать с элементом». Писать ради этого отдельную функцию неудобно — лямбда позволяет определить действие прямо на месте.
Как это работает. Синтаксис: [захват](параметры){ тело }. В квадратных скобках указывают, какие внешние переменные доступны внутри: [&sum] — по ссылке (можно менять), [=] — по значению (копия). Без захвата лямбда видит только свои параметры. Лямбды удобно передавать в алгоритмы как «мини-функцию».
#include <iostream>
#include <vector>
#include <algorithm> // for_each и count_if
int main() {
std::vector<int> v = {1, 2, 3, 4, 5}; // данные
int sum = 0; // сюда будем копить сумму
// [&sum] — захват по ссылке, чтобы менять sum снаружи
std::for_each(v.begin(), v.end(), [&sum](int x) { sum += x; });
std::cout << "Сумма: " << sum << "\n"; // 15
// [] — без захвата; считаем чётные элементы
auto even = std::count_if(v.begin(), v.end(), [](int x) { return x % 2 == 0; });
std::cout << "Чётных: " << even << "\n"; // 2
}
Зачем это нужно. Ошибки бывают разными: предсказуемые (неверный ввод) и исключительные (файл пропал, деление на ноль). Исключения позволяют «выбросить» проблему вверх и обработать её в одном месте, не засоряя каждый вызов проверками.
Как это работает. throw прерывает текущую функцию и передаёт объект-исключение вверх по стеку. Первый подходящий catch ловит его по типу. Если исключение не поймано — программа аварийно завершится. e.what() даёт текст ошибки. Для ожидаемых ошибок иногда лучше коды возврата, но для «дальше выполнять нельзя» — исключения.
#include <iostream>
#include <stdexcept> // std::runtime_error
double divide(double a, double b) {
if (b == 0) { // делить на ноль нельзя
// Бросаем исключение с сообщением
throw std::runtime_error("Деление на ноль");
}
return a / b; // обычный путь
}
int main() {
try {
// Если исключение брошено — управление уйдёт в catch
std::cout << divide(10, 0) << "\n";
} catch (const std::exception& e) {
// Ловим по базовому типу, what() — текст ошибки
std::cout << "Ошибка: " << e.what() << "\n";
}
}
Зачем это нужно. Программы часто работают с файлами: логи, данные, конфиги. Чтение построчно — базовый навык, без которого не обойтись.
Как это работает. ifstream открывает файл на чтение. Обязательно проверь, что он открылся: иначе будешь читать пустоту. getline читает строку до перевода строки и возвращает поток; в условии цикла это означает «пока файл не кончился». Поток закроется сам при выходе из области видимости благодаря RAII.
#include <iostream>
#include <fstream> // файловые потоки
#include <string> // std::string
int main() {
std::ifstream in("data.txt"); // открываем файл на чтение
if (!in) { // проверка: удалось открыть?
std::cout << "Не удалось открыть файл\n";
return 1; // код ошибки
}
std::string line; // буфер для строки
int n = 0; // счётчик строк
// getline читает одну строку до \n, пока файл не кончился
while (std::getline(in, line)) {
++n; // увеличили счётчик
std::cout << n << ": " << line << "\n"; // печать строки с номером
}
}
Аналогично std::ofstream пишет в файл через <<.
Теперь у тебя есть не только код, но и объяснение «зачем» для каждой темы. Попробуй пересказать любой слайд своими словами — если получается, значит, ты действительно понял.
Выбери 3–5 тем, которые дались сложнее, и напиши по одному своему примеру на каждую.