gadget59 · C++
1 / 22
Практикум

20 тем C++ с объяснениями и кодом

Один слайд — один вопрос. Сначала разбираем, зачем это нужно и как работает, потом смотрим код, где почти каждая строка снабжена комментарием.

🖨 вывод 🔢 типы 🔁 циклы 🧠 функции 📍 память 🏛 ООП 📦 STL
Как читать: прочитай объяснение, попробуй предсказать, что выведет программа, потом сверь с кодом и комментариями.
Управление: ← → или пробел · gadget59.ru
Тема 1

1Как написать и запустить первую программу?

Зачем это нужно. Любая программа на 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.

Тема 2

2Как объявить переменные разных типов?

Зачем это нужно. Программа хранит данные в переменных. Тип говорит компилятору, что именно там лежит: целое число, дробное, символ или строка. От типа зависит, какие операции разрешены и сколько памяти выделить.

Как это работает. При объявлении ты создаёшь «ячейку» с именем и типом. Если сразу не задать значение, внутри может оказаться мусор — поэтому лучше инициализировать. 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";
}
Тема 3

3Как работают арифметика и деление?

Зачем это нужно. Деление — самая частая ловушка новичка. Если делить два 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";
}
Тема 4

4Как проверить условие?

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

Как это работает. 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";
    }
}
Тема 5

5Как вывести таблицу через цикл?

Зачем это нужно. Циклы позволяют не копировать один и тот же код десять раз. Таблицы, перебор массивов, подсчёты — всё это делается циклом.

Как это работает. У 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";                     // конец строки таблицы
    }
}
Тема 6

6Чем 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);
}
Тема 7

7Как объявить и вызвать функцию?

Зачем это нужно. Функции разбивают программу на маленькие понятные куски. Вместо одного длинного 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
}
Тема 8

8Как передать аргумент по ссылке?

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

Как это работает. Знак & в типе параметра означает «это не копия, а псевдоним исходной переменной». Всё, что функция делает с параметром, происходит с оригиналом. Ссылку нельзя «переприсвоить» на другой объект — она всегда связана с тем, что передали при вызове.

#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.
Тема 9

9Как использовать 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";
}
Тема 10

10Как работать со строками?

Зачем это нужно. Текст встречается везде: имена, сообщения, файлы. В 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";
}
Тема 11

11Как работает указатель?

Зачем это нужно. Указатели дают доступ к памяти напрямую: через них работают динамические структуры, передача больших объектов без копирования и взаимодействие с функциями, которые меняют данные «на месте».

Как это работает. Указатель хранит адрес, а не само значение. &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;
}
Тема 12

12Зачем нужны умные указатели?

Зачем это нужно. Ручной 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";
} // память освободится автоматически
Тема 13

13Как объявить класс с конструктором?

Зачем это нужно. Класс позволяет держать данные и операции над ними вместе. Это удобнее, чем куча отдельных переменных и функций: объект сам знает, как с собой работать.

Как это работает. Поля класса по умолчанию закрыты, снаружи доступно только то, что в 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();                       // вызываем метод
}
Тема 14

14Что такое наследование?

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

Как это работает. 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();                       // метод из производного класса
}
Тема 15

15Что такое виртуальные функции и полиморфизм?

Зачем это нужно. Без полиморфизма код, который работает с базовым типом, не может вызвать поведение наследника. С 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;                       // освобождаем память
}
Тема 16

16Как работают шаблоны функций?

Зачем это нужно. Без шаблонов пришлось бы писать отдельную функцию для 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";
}
Тема 17

17Как отсортировать и найти элемент в 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";
    }
}
Тема 18

18Что такое лямбда-выражение?

Зачем это нужно. Алгоритмам вроде 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
}
Тема 19

19Как обработать ошибку через исключение?

Зачем это нужно. Ошибки бывают разными: предсказуемые (неверный ввод) и исключительные (файл пропал, деление на ноль). Исключения позволяют «выбросить» проблему вверх и обработать её в одном месте, не засоряя каждый вызов проверками.

Как это работает. 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";
    }
}
Тема 20

20Как прочитать файл построчно?

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

Как это работает. 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 пишет в файл через <<.

Финал

20 тем разобраны — закрепи практикой 🎉

Теперь у тебя есть не только код, но и объяснение «зачем» для каждой темы. Попробуй пересказать любой слайд своими словами — если получается, значит, ты действительно понял.

🖨 вывод 🔢 типы 🔁 циклы 🧠 функции 📍 память 🏛 ООП 📦 STL

Выбери 3–5 тем, которые дались сложнее, и напиши по одному своему примеру на каждую.

← Вернуться на gadget59.ru