Введение в C++: от основ до C++23 и Qt 6.0

Курс охватывает базовые принципы программирования на C++ и постепенно переходит к современным возможностям стандарта C++23. Завершается практическим освоением разработки приложений с использованием Qt 6.0: GUI, сигналы/слоты и работа с проектами.

1. Основы C++: синтаксис, типы, управление потоком

Основы C++: синтаксис, типы, управление потоком

C++ — компилируемый язык общего назначения. В рамках курса мы будем двигаться от базового синтаксиса к современным возможностям стандарта C++23 и затем — к разработке приложений с Qt 6.0. Эта статья закладывает фундамент: как выглядит программа на C++, какие бывают типы, как объявлять переменные и управлять выполнением кода.

Что такое программа на C++

Обычно проект на C++ состоит из файлов исходного кода (часто .cpp) и заголовков (часто .h/.hpp). Заголовки подключают директивой #include, а точка входа программы — функция main.

Минимальная программа:

Ключевые элементы:

  • #include <iostream> подключает стандартный ввод-вывод.
  • int main() — функция, с которой начинается выполнение.
  • std::cout — поток вывода в консоль; std:: — пространство имён стандартной библиотеки.
  • return 0; — код успешного завершения.
  • Комментарии:

    Имена, блоки и область видимости

    Код организуется в блоки { ... }. Переменные, объявленные внутри блока, обычно доступны только в нём.

    Практическое правило: объявляйте переменную как можно ближе к месту использования — так проще читать код и меньше риск ошибок.

    Переменные и инициализация

    Объявление переменной включает тип, имя и (обычно) инициализацию.

    Рекомендуемый стиль для базовых типов — инициализация {...}:

  • она единообразна
  • она предотвращает некоторые опасные неявные преобразования
  • auto

    auto просит компилятор вывести тип из инициализатора.

    Важно: auto не делает переменную «динамически типизированной». Тип всё равно фиксирован, просто выводится автоматически.

    const и constexpr

  • const запрещает менять значение после инициализации.
  • constexpr означает, что значение можно вычислить на этапе компиляции (если все данные известны).
  • Практическое правило:

  • используйте const, если значение должно быть неизменяемым
  • используйте constexpr, если значение является константой «по смыслу» и может быть вычислено компилятором
  • Базовые типы данных

    Ниже — самые частые типы, с которых обычно начинают.

    | Категория | Типы | Для чего обычно используют | |---|---|---| | Целые | int, long long, short | счётчики, индексы, целые значения | | Беззнаковые целые | unsigned int, std::size_t | размеры, индексы контейнеров | | Вещественные | float, double | вычисления с дробями | | Логический | bool | условия, флаги | | Символьные | char, wchar_t, char8_t | символы и кодировки |

    Замечания, которые важны с самого начала:

  • точный размер int зависит от платформы; чаще всего встречается 32-битный int
  • std::size_t — стандартный тип для размеров и индексов (беззнаковый)
  • для денег и точных десятичных расчётов double часто не подходит из-за ошибок представления; это отдельная тема, к которой вернёмся позже
  • Строки

    В C++ есть два наиболее распространённых «мира строк»:

  • C-строки: const char* (как в примере с auto name = "Bob";)
  • std::string из стандартной библиотеки
  • Для большинства задач в прикладном коде используйте std::string:

    Операторы и выражения

    Выражение — это то, что вычисляется в значение: a + b, x > 0, i++.

    Основные группы операторов:

  • арифметические: +, -, *, /, %
  • сравнения: ==, !=, <, <=, >, >=
  • логические: &&, ||, !
  • присваивание: = и составные +=, -=, *=, ...
  • Частая ошибка новичков — путать = и ==:

    Практическое правило: если выражение получается сложным, добавляйте скобки, даже если вы помните приоритеты операторов — читаемость важнее.

    Ссылки и указатели (минимально необходимое)

    Ссылки

    Ссылка (T&) — это другое имя для уже существующего объекта.

    Свойства:

  • ссылка должна быть привязана при создании
  • ссылку нельзя «перепривязать» к другому объекту
  • ссылку нельзя сделать «пустой» (в отличие от указателя)
  • Ссылки часто используют в параметрах функций, чтобы не копировать данные.

    Указатели

    Указатель (T*) хранит адрес объекта. Он может быть равен nullptr.

    Практическое правило: не используйте «сырые» указатели для владения памятью. Позже в курсе будут умные указатели (std::unique_ptr, std::shared_ptr) и RAII.

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

    Управление потоком — это конструкции, которые позволяют ветвиться и повторять действия.

    !Диаграмма показывает основные конструкции ветвления и циклов и то, как они влияют на порядок выполнения

    if / else

    Условие в if должно иметь тип, приводимый к bool.

    Тернарный оператор ?:

    Компактная форма выбора значения:

    switch

    switch удобен, когда есть много вариантов по одному целому значению или перечислению (enum).

    Важные детали:

  • case должны быть константными значениями
  • без break выполнение «провалится» в следующий case (иногда это нужно, но чаще это источник ошибок)
  • Цикл while

    Повторяет блок, пока условие истинно.

    Цикл do while

    Проверяет условие после выполнения тела: тело выполнится хотя бы один раз.

    Цикл for

    Классический цикл со счётчиком.

    Практическое правило: чаще используйте ++i (префиксный инкремент), это привычный стиль и потенциально эффективнее для сложных итераторов.

    Диапазонный for (range-based for)

    Используется для обхода контейнеров.

    break и continue

  • break — выйти из ближайшего цикла или switch
  • continue — перейти к следующей итерации цикла
  • Практические рекомендации, которые пригодятся дальше

  • Не пишите using namespace std; в учебных и рабочих проектах: это создаёт конфликты имён.
  • Всегда включайте предупреждения компилятора и относитесь к ним серьёзно.
  • Если тип «размер/индекс», рассматривайте std::size_t, но помните, что это беззнаковый тип: смешивание int и std::size_t в сравнениях — частый источник неожиданных результатов.
  • Для параметров функций часто подходят ссылки: const T& для чтения без копирования и T& для изменения.
  • Полезные ссылки

  • cppreference: базовые типы
  • cppreference: операторы
  • cppreference: операторы ветвления и циклы
  • C++ Core Guidelines
  • ISO C++ (официальный сайт сообщества)
  • 2. Функции, классы и ООП: инкапсуляция, наследование, полиморфизм

    Функции, классы и ООП: инкапсуляция, наследование, полиморфизм

    В предыдущей статье мы разобрали базовый синтаксис, типы и управление потоком. Следующий шаг — научиться структурировать программу: выносить логику в функции и собирать данные с поведением в классы. Это фундамент для современного C++ и для Qt 6.0, где большая часть API построена вокруг классов и полиморфизма.

    Функции как строительные блоки программы

    Функция — это именованный кусок кода, который можно вызвать из разных мест программы. Функции помогают:

  • не повторять один и тот же код
  • изолировать детали реализации
  • сделать программу читаемой и тестируемой
  • Объявление и определение

  • int перед именем — тип возвращаемого значения
  • параметры a, b существуют только внутри функции
  • return возвращает значение вызывающему коду
  • Если функция ничего не возвращает, используют void:

    Параметры: по значению, по ссылке, по константной ссылке

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

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

  • маленькие простые типы (int, double) часто передают по значению
  • крупные объекты (std::string, std::vector) часто передают как const T& для чтения без копирования
  • если функция должна изменить объект вызывающей стороны — T&
  • Возврат значений

    Возвращать можно и простые типы, и объекты:

    Современный C++ обычно эффективно возвращает объекты (компилятор оптимизирует лишние копии), поэтому возврат std::string — нормальная практика.

    Перегрузка функций

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

    Компилятор выбирает нужную перегрузку по типам и количеству аргументов.

    Значения параметров по умолчанию

    Вызовы:

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

    auto и тип возвращаемого значения

    Иногда тип возвращаемого значения писать неудобно (например, при работе с шаблонами). В базовых примерах чаще пишут явно, но полезно знать, что в C++ можно:

    Компилятор выведет тип по return.

    Классы: данные и поведение вместе

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

  • состояние (поля, или переменные-члены)
  • поведение (методы, или функции-члены)
  • class и struct

    В C++ struct и class почти одинаковы. Основная разница по умолчанию:

  • в struct члены по умолчанию public
  • в class члены по умолчанию private
  • Обратите внимание на name() const: это константный метод, он обещает не изменять объект.

    Инкапсуляция

    Инкапсуляция — это принцип, по которому внутреннее состояние объекта скрыто, а доступ к нему идёт через публичный интерфейс.

    Зачем это нужно:

  • можно гарантировать корректность данных (например, не допустить отрицательный возраст)
  • можно менять внутреннюю реализацию без переписывания всего кода, который пользуется классом
  • Пример с поддержанием инварианта (правила корректности состояния):

    balance_ закрыт от прямой записи снаружи, поэтому класс сам контролирует допустимые изменения.

    Конструкторы и инициализация

    Конструктор задаёт начальное состояние объекта.

    Почему важен список инициализации (: name_(name), age_(age)):

  • поля создаются сразу в нужном состоянии
  • некоторые поля нельзя сначала создать «пустыми», а потом присвоить (например, const-поля и ссылки)
  • Деструктор и управление ресурсами

    Деструктор вызывается при уничтожении объекта. Часто он вообще не нужен (стандартные типы и контейнеры сами всё корректно освобождают), но важно знать идею:

  • объект должен корректно «закрывать» свои ресурсы в конце жизни
  • Это тесно связано с принципом RAII: ресурс захватывается в конструкторе и освобождается в деструкторе. На практике в современном C++ чаще используют стандартные классы, которые уже реализуют RAII (например, std::string, std::vector, умные указатели).

    Наследование

    Наследование позволяет создать новый класс на основе существующего, расширяя или уточняя поведение.

    Пример: есть базовый тип «Фигура», и есть частные случаи — «Круг» и «Прямоугольник».

  • public Shape означает связь «является» (is-a): Circle является Shape
  • наследование — мощный инструмент, но его легко применять не там, где надо
  • Практическое правило: если отношение не выглядит как «является», часто лучше использовать композицию (когда один класс содержит другой как поле), а не наследование.

    protected

    Помимо public и private есть protected:

  • доступен внутри самого класса и его наследников
  • снаружи недоступен
  • Используйте protected осторожно: он сильнее связывает наследника с внутренностями базового класса.

    Полиморфизм

    Полиморфизм — это возможность работать с объектами разных конкретных типов единообразно через общий интерфейс.

    В C++ чаще всего под этим подразумевают динамический полиморфизм через virtual.

    Виртуальные функции и переопределение

    Что здесь происходит:

  • Shape::draw помечена как virtual и = 0, значит Shapeабстрактный класс (нельзя создать Shape напрямую)
  • override просит компилятор проверить, что вы действительно переопределили виртуальную функцию базового класса (это защита от ошибок в сигнатуре)
  • хранение через std::unique_ptr<Shape> позволяет держать в одном контейнере разные фигуры и вызывать методы через общий интерфейс
  • !Схема наследования и динамического полиморфизма через виртуальную функцию

    Зачем нужен виртуальный деструктор

    Если вы удаляете объект наследника через указатель/ссылку на базовый класс, деструктор базового класса должен быть virtual, иначе поведение некорректно.

    Правильный шаблон для базового класса:

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

    Срезка объекта (object slicing)

    Если попытаться скопировать наследника в базовый класс по значению, «часть наследника» будет потеряна.

    Практическое правило: для полиморфизма используйте ссылки (Base&, const Base&) или указатели (Base*, умные указатели), а не передачу/хранение базового типа по значению.

    final

    Иногда нужно запретить дальнейшее наследование или переопределение:

    Как это связано с Qt 6.0

    Qt — объектно-ориентированный фреймворк. В нём вы постоянно встречаете:

  • классы и иерархии (например, QWidget наследуется от QObject)
  • полиморфизм (вы работаете через базовые типы Qt, а реальный тип может быть более конкретным)
  • инкапсуляцию (вы используете публичный API Qt, не зная внутреннего устройства)
  • Позже в курсе, когда мы дойдём до Qt, эти принципы станут практикой: вы будете создавать собственные классы, переопределять виртуальные методы, управлять временем жизни объектов и строить архитектуру приложения.

    Практические рекомендации

  • Передавайте тяжёлые объекты как const T&, если не нужно владение и изменение.
  • Держите поля private, а изменения делайте через методы, которые поддерживают корректное состояние.
  • Предпочитайте композицию наследованию, если нет очевидной связи «является».
  • Если есть виртуальные функции — делайте деструктор virtual.
  • Всегда пишите override, когда переопределяете виртуальную функцию.
  • Полезные ссылки

  • cppreference: функции
  • cppreference: классы
  • cppreference: конструкторы
  • cppreference: виртуальные функции
  • cppreference: наследование
  • cppreference: умный указатель std::unique_ptr
  • 3. Память и ресурсы: указатели, RAII, умные указатели, исключения

    Память и ресурсы: указатели, RAII, умные указатели, исключения

    В прошлых статьях мы разобрали синтаксис, базовые типы, управление потоком, функции и ООП. Теперь важно понять, как в C++ живут объекты и как безопасно управлять ресурсами: памятью, файлами, сетевыми соединениями, мьютексами, дескрипторами ОС.

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

    Модель памяти и время жизни объектов

    В C++ объект всегда имеет:

  • время жизни (когда он существует)
  • область видимости (где к нему можно обратиться по имени)
  • место хранения (упрощённо: стек, куча, статическая память)
  • Чаще всего в прикладном коде вы встретите два варианта.

    Автоматические объекты (обычно “на стеке”)

    Плюсы:

  • создаются и уничтожаются автоматически
  • обычно быстрее и проще
  • Минусы:

  • время жизни ограничено блоком
  • Динамические объекты (обычно “в куче”)

    Динамическое создание возможно через new, но в современном C++ это почти всегда не то, с чего нужно начинать.

    Проблема не в том, что new “плохой”, а в том, что ручное управление легко ломается:

  • забыли delete и получили утечку
  • сделали delete дважды и получили неопределённое поведение
  • вернули “сырой” указатель наружу и получили висячий указатель
  • !Сравнение времени жизни объектов на стеке и в куче и типичные проблемы при ручном управлении

    Указатели: что это и почему с ними осторожно

    Указатель (T*) хранит адрес объекта типа T. Он может быть равен nullptr.

    Важно различать два смысла указателя:

  • невладеющий указатель (просто “наблюдает” за объектом, не управляет временем жизни)
  • владеющий указатель (отвечает за освобождение ресурса)
  • Практическое правило современного C++: сырые указатели (T*) обычно должны быть невладеющими. Владение выражайте через RAII-объекты, чаще всего через умные указатели.

    Висячие указатели (dangling pointer)

    Самая частая ошибка — сохранить адрес объекта, который уже уничтожен.

    После возврата &x указывает “в никуда”. Любое использование такого указателя — неопределённое поведение.

    RAII: основа безопасного кода

    RAII (Resource Acquisition Is Initialization) означает: захват ресурса происходит при инициализации объекта, освобождение — в деструкторе.

    Ключевой эффект: освобождение ресурса произойдёт в любом случае — при обычном выходе из функции и при выбросе исключения.

    Пример: файл

    std::ifstream сам открывает файл при создании и закрывает при уничтожении.

    Пример: мьютекс

    std::lock_guard гарантирует разблокировку.

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

    Умные указатели: владение памятью без delete

    Умные указатели — это RAII-обёртки над динамической памятью. Они выражают владение явно и освобождают память автоматически.

    std::unique_ptr: единоличное владение

    std::unique_ptr<T> означает: в каждый момент времени объект принадлежит ровно одному владельцу.

    Особенности:

  • не копируется (копирование запрещено)
  • перемещается (владение можно передать)
  • Практическое правило: по умолчанию выбирайте std::unique_ptr, если вам нужно владение.

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

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

    Когда полезно:

  • объект реально должен жить “пока кто-то им пользуется”, и владельцев несколько
  • Риски:

  • подсчёт ссылок имеет стоимость
  • легко создать цикл владения, из-за чего память не освободится
  • std::weak_ptr: наблюдатель для shared_ptr

    std::weak_ptr<T> не продлевает время жизни объекта, но позволяет безопасно проверить, жив ли объект.

    Практическое правило: если есть риск циклов, используйте weak_ptr для разрыва циклических ссылок.

    Сырые указатели всё ещё нужны

    Сырые указатели (T) удобны как невладеющие*:

  • для параметров, если “объект может отсутствовать” (тогда T* и проверка на nullptr)
  • для взаимодействия с C-API
  • для наблюдения за объектом, временем жизни которого управляет другой код
  • Но: если указатель владеющий, он должен быть умным указателем или RAII-классом.

    Исключения и безопасность ресурсов

    Исключения — механизм обработки ошибок, который “перепрыгивает” через уровни вызовов, пока не найдёт подходящий catch.

    try / catch

    Практические правила:

  • ловите исключения по const ссылке: catch (const std::exception& e)
  • бросайте исключения по значению: throw std::runtime_error("...")
  • Раскрутка стека (stack unwinding)

    При выбросе исключения C++ уничтожает все полностью созданные объекты на пути выхода из блоков: вызываются их деструкторы. Именно поэтому RAII работает.

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

    noexcept

    noexcept — обещание, что функция не выбрасывает исключений.

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

    Связь с Qt 6.0

    Qt активно использует объектную модель и управление временем жизни, но не только через стандартные умные указатели.

    Базовая идея, которую важно запомнить заранее:

  • многие Qt-объекты (наследники QObject) могут иметь родителя, и тогда родитель удаляет детей в своём деструкторе
  • Это тоже RAII, но в “Qt-стиле” через дерево владения. Позже, переходя к Qt, вы будете сочетать:

  • RAII стандартной библиотеки (std::unique_ptr, std::lock_guard)
  • механизмы Qt (родитель-владелец, а также специальные типы вроде QPointer)
  • Короткий чек-лист современного C++ по ресурсам

  • Избегайте new/delete в прикладном коде.
  • Для владения памятью по умолчанию используйте std::unique_ptr.
  • Используйте std::shared_ptr только когда действительно нужно совместное владение.
  • Для “наблюдения” при shared_ptr используйте std::weak_ptr.
  • Полагайтесь на RAII: ресурсы освобождаются в деструкторах.
  • Пишите код так, чтобы он был безопасен при исключениях: RAII-объекты создавайте как можно ближе к месту использования.
  • Полезные ссылки

  • cppreference: RAII
  • cppreference: std::unique_ptr
  • cppreference: std::shared_ptr
  • cppreference: std::weak_ptr
  • cppreference: исключения
  • Qt Documentation: QObject
  • 4. Современный C++ до C++23: шаблоны, STL, ranges, coroutines, modules

    Современный C++ до C++23: шаблоны, STL, ranges, coroutines, modules

    В первых статьях курса мы научились писать базовые программы, разобрали функции, классы, ООП, а затем — память, RAII, умные указатели и исключения. Теперь следующий логичный шаг: понять, как пишут прикладной код на современном C++, опираясь на стандартную библиотеку и возможности стандартов C++20–C++23.

    Эта статья — обзор и практический минимум по четырём крупным темам:

  • Шаблоны: как писать код «для любых типов» безопасно и понятно.
  • STL: контейнеры, алгоритмы, итераторы и полезные утилиты стандартной библиотеки.
  • Ranges: современный способ строить цепочки преобразований над коллекциями.
  • Coroutines: «кооперативная асинхронность» и ленивые вычисления.
  • Modules: современная альтернатива части проблем #include.
  • Шаблоны

    Шаблон — это механизм, который позволяет описать обобщённый код, а компилятор «подставит» конкретные типы при использовании.

    Функции-шаблоны

    Простейший пример — функция max для любых сравнимых типов:

    Что важно:

  • template <typename T> объявляет параметр типа T.
  • T выводится автоматически по аргументам вызова.
  • const T& соответствует идеям из прошлых статей: не копируем тяжёлые объекты без нужды.
  • Классы-шаблоны

    Самые популярные типы стандартной библиотеки — это классы-шаблоны: std::vector<T>, std::optional<T>, std::unique_ptr<T>.

    Пример: динамический массив целых — это std::vector<int>, а динамический массив строк — std::vector<std::string>.

    Concepts: ограничения на шаблоны (C++20)

    Одна из проблем «старых» шаблонов: ошибки компиляции могли быть очень длинными и непонятными, если тип не подходит. Concepts позволяют указать требования к типу прямо в сигнатуре.

    Здесь std::integral — готовый concept из стандартной библиотеки: он означает «целочисленный тип».

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

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

  • cppreference: templates
  • cppreference: concepts
  • STL: контейнеры, алгоритмы, итераторы

    STL (часть стандартной библиотеки C++) держится на трёх китах:

  • контейнеры: где хранятся данные
  • алгоритмы: что делаем с данными
  • итераторы: как алгоритмы «ходят» по контейнеру
  • Контейнеры

    На практике чаще всего используются:

    | Контейнер | Когда использовать | |---|---| | std::vector<T> | по умолчанию для последовательности, быстрый доступ по индексу | | std::string | строка | | std::array<T, N> | фиксированный размер, хранится как обычный объект | | std::deque<T> | вставки/удаления в начале/конце, но обычно реже чем vector | | std::map<K, V> | отсортированные ключи, логарифмический доступ | | std::unordered_map<K, V> | быстрый доступ по ключу в среднем, порядок не гарантирован | | std::set<T> / std::unordered_set<T> | множество уникальных значений |

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

  • если не уверены, начните с std::vector и поменяйте контейнер только при реальной необходимости.
  • Алгоритмы

    Алгоритмы — это функции вроде std::sort, std::find, std::count_if. Они работают не с контейнером напрямую, а через итераторы.

    Пример: отсортировать и удалить дубликаты:

    Что здесь происходит:

  • std::sort сортирует диапазон begin, end).
  • std::unique сдвигает уникальные элементы в начало и возвращает итератор на «новый конец».
  • erase физически удаляет «хвост».
  • Это классический приём: erase-remove idiom (историческое название; сейчас часто делают то же самое через ranges).

    Итераторы

    Итератор — это объект, который указывает на элемент в контейнере и умеет переходить к следующему. Для std::vector итератор похож на указатель, но обобщённее.

    Минимальный смысл:

  • container.begin() — «первый элемент»
  • container.end() — «позиция за последним»
  • диапазоны почти всегда задают как [begin, end)
  • Ссылки:

  • [cppreference: containers library
  • cppreference: algorithms library
  • Ranges: работа с данными как с конвейером (C++20)

    Ranges — это современное развитие идеи «алгоритмы + итераторы». Они позволяют:

  • писать преобразования цепочкой
  • уменьшить количество временных контейнеров
  • сделать код ближе к описанию задачи
  • !Диаграмма, показывающая pipeline-подход ranges

    Пример: фильтрация и преобразование

    Ключевая идея: view — это не новый std::vector, а представление (view), которое вычисляет элементы по мере запроса.

    Если нужно получить настоящий контейнер, часто делают явное копирование:

    std::ranges-алгоритмы

    Вместо std::sort(v.begin(), v.end()) можно писать:

    Преимущества:

  • меньше «шума» с итераторами
  • лучше сочетается с views
  • Ссылки:

  • cppreference: ranges library
  • cppreference: std::views
  • Coroutines: приостановка и продолжение вычисления (C++20)

    Короутина — это функция, которая может приостанавливать выполнение (co_await, co_yield, co_return) и потом продолжать с того же места.

    Важно: стандарт C++ даёт низкоуровневый механизм, а удобные типы-обёртки часто предоставляет фреймворк или библиотека. В Qt тема асинхронности тоже важна, но обычно выражается через сигналы/слоты и event loop; короутины могут дополнять эту модель.

    !Схема жизненного цикла coroutine и точек приостановки

    co_yield: ленивый генератор (идея)

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

    Стандартная библиотека до C++23 не включает универсальный generator как готовый контейнер/тип для повседневного использования, но короутины активно применяются в сторонних библиотеках и в инфраструктуре асинхронного кода.

    co_await: асинхронное ожидание (идея)

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

  • «магии» параллельности не появляется: короутина кооперативно уступает управление, чтобы другой код мог работать
  • конкретное поведение зависит от awaitable-объекта и планировщика (executor), которые задаёт библиотека
  • Ссылки:

  • cppreference: coroutines
  • cppreference: coroutine support library
  • Modules: современная замена части проблем #include (C++20)

    Традиционный #include буквально вставляет текст заголовка, что исторически приводило к проблемам:

  • долгие сборки из-за многократной обработки одних и тех же заголовков
  • макросы и порядок подключений могут влиять на результат
  • сложно надёжно изолировать интерфейс от реализации
  • Модули решают это иначе: вы явно экспортируете интерфейс, а потребитель делает import.

    !Диаграмма различий между #include и import модулей

    Минимальный пример

    Файл модуля (например, math.cppm):

    Использование в обычном .cpp:

    Практические замечания:

  • поддержка модулей зависит от компилятора и системы сборки; в реальных проектах внедрение идёт постепенно
  • Qt-проекты часто завязаны на CMake, moc и большую экосистему заголовков, поэтому модули нужно вводить осторожно и осознанно
  • Ссылки:

  • cppreference: modules
  • Как выбирать современные инструменты в повседневном коде

    Ниже — краткая «карта решений», которая связывает материал этой статьи с темами прошлых.

  • Данные храните в контейнерах STL: чаще всего std::vector, std::string, std::unordered_map.
  • Владение ресурсами выражайте RAII-объектами: контейнеры, std::unique_ptr, файловые потоки, std::lock_guard.
  • Для обработки коллекций сначала ищите алгоритм: std::ranges::sort, std::ranges::find_if, std::ranges::count.
  • Если логика похожа на конвейер преобразований, пробуйте ranges/views.
  • Шаблоны используйте для обобщения, а concepts — чтобы ограничения были явными.
  • Короутины рассматривайте как инструмент инфраструктуры: они мощные, но обычно раскрываются через библиотеку (асинхронность, генераторы).
  • Модули воспринимайте как стратегическое улучшение сборки, а не как обязательную часть каждого учебного примера.
  • Полезные ссылки

  • cppreference: standard library
  • cppreference: ranges
  • cppreference: coroutines
  • cppreference: modules
  • 5. Qt 6.0: основы фреймворка, Widgets/QML, сигналы и слоты, сборка проектов

    Qt 6.0: основы фреймворка, Widgets/QML, сигналы и слоты, сборка проектов

    Qt 6.0 завершает «базовый C++-цикл» курса: после синтаксиса, ООП, RAII и современной стандартной библиотеки мы переходим к практической разработке приложений. Qt — это фреймворк на C++, который даёт готовую инфраструктуру для GUI, событий, потоков, работы с файлами и сетью. При этом он активно опирается на идеи из предыдущих статей:

  • ООП и полиморфизм: большая часть API построена вокруг наследования от QObject и виртуальных методов.
  • RAII и управление временем жизни: у Qt есть свой механизм владения через дерево объектов (родитель удаляет детей).
  • Современный C++: вы можете подключать std::vector, std::unique_ptr, ranges и алгоритмы STL, а Qt добавляет свои типы и инструменты там, где нужно тесно связаться с GUI и мета-объектной системой.
  • Что такое Qt и из чего он состоит

    Qt поставляется в виде набора модулей (библиотек). В прикладной разработке чаще всего встречаются:

  • Qt Core: базовые типы (QString, QDateTime), событийная модель, QObject, таймеры, потоки.
  • Qt GUI: низкоуровневые вещи для графики и оконной системы.
  • Qt Widgets: классический GUI на виджетах (QWidget, QPushButton, QMainWindow).
  • Qt Quick (QML): современный декларативный UI (QML) + движок рендеринга.
  • Дополнительные: сеть, SQL, мультимедиа и другие.
  • Ключевая особенность Qt — мета-объектная система. Она добавляет возможности поверх C++:

  • сигналы и слоты
  • свойства (properties)
  • отражение части информации о классе во время выполнения (имя типа, список сигналов, свойств)
  • В Qt 6 большая часть этой магии по-прежнему строится вокруг QObject и специальных инструментов сборки.

    Минимальная программа Qt и цикл событий

    Большинство GUI-приложений Qt устроены одинаково:

  • создаётся объект приложения (QApplication для Widgets или QGuiApplication для Qt Quick)
  • создаются окна/виджеты или загружается QML
  • вызывается exec(), который запускает цикл событий
  • Минимальный пример для Widgets:

    Что делает цикл событий:

  • ждёт события ОС (мышь, клавиатура, перерисовка, таймеры)
  • доставляет их нужным объектам
  • обеспечивает выполнение слотов при приходе сигналов
  • !Как события и сигналы проходят через цикл событий Qt

    QObject, владение и время жизни объектов

    QObject — базовый класс для большинства «живых» объектов Qt. Он даёт:

  • механизм сигналов и слотов
  • иерархию владения: parent и список children
  • безопасное отключение соединений при уничтожении объектов
  • Владение через родителя

    Если создать QObject с родителем, родитель удалит ребёнка в своём деструкторе. Это один из главных механизмов управления временем жизни в Qt.

    Практические последствия:

  • владение часто выражается не std::unique_ptr, а корректно выставленным родителем
  • если объект живёт в GUI-иерархии (виджеты), то владение обычно уже «зашито» в контейнерных виджетах
  • deleteLater()

    В GUI-коде объект иногда нельзя удалять прямо сейчас (например, во время обработки события). Для этого есть QObject::deleteLater() — объект будет удалён безопасно, когда управление вернётся в цикл событий.

    Когда не надо смешивать родителей и std::unique_ptr

    Если вы передали QObject-объекту родителя, не делайте его одновременно владельцем std::unique_ptr, иначе появится риск двойного удаления.

    Если вы хотите владеть объектом через стандартный RAII, обычно выбирают один из подходов:

  • объект не имеет родителя и живёт в std::unique_ptr
  • объект имеет родителя и создаётся через new, а время жизни контролирует Qt
  • Ссылка: QObject

    Сигналы и слоты

    Сигналы и слоты — это типобезопасный механизм связи «событие → реакция».

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

    Ссылка: Signals & Slots

    Подключение (connect) в современном стиле

    Рекомендуемый стиль Qt 5/6 — через указатели на члены:

    Преимущества:

  • проверка типов на этапе компиляции
  • меньше ошибок из-за строковых имён
  • Подключение к лямбде

    Удобно, когда не хочется заводить отдельный слот:

    Пример на Widgets

    Потоки и тип соединения

    Когда сигнал и слот находятся:

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

  • GUI-объекты должны жить в GUI-потоке, а работа «в фоне» уходит в рабочие потоки
  • доставка результата обратно в GUI часто делается сигналом, который попадёт в GUI через очередь
  • Ссылка: QMetaObject::ConnectionType

    Widgets и QML (Qt Quick): что выбрать

    В Qt есть два основных подхода к UI.

    Qt Widgets

    Widgets — классический подход, где UI строится из объектов-виджетов.

  • подход «компоненты + раскладки»
  • хорошо подходит для традиционных десктоп-приложений
  • огромная база готовых виджетов
  • Ссылка: Qt Widgets

    QML и Qt Quick

    QML — декларативный язык описания интерфейса. Qt Quick — движок, который этот интерфейс рисует и оживляет.

  • удобно для динамичных интерфейсов, анимаций
  • разделяет UI (QML) и логику (C++ или JavaScript в QML)
  • часто используется в современных приложениях и встраиваемых системах
  • Ссылка: Qt Quick

    !Два стека UI в Qt: Widgets и QML/Qt Quick

    Краткое сравнение

    | Критерий | Widgets | QML / Qt Quick | |---|---|---| | Стиль | императивный C++ | декларативный QML | | Сильные стороны | «классический» десктоп, много готовых контролов | динамика, анимации, современный UI | | Взаимодействие с C++ | напрямую, всё в одном языке | часто C++ как backend, QML как UI | | Сложность старта | проще для C++-разработчика | проще для UI-ориентированной разработки |

    Минимальный пример приложения на QML

    main.cpp:

    Main.qml:

    Важно: пример использует подход Qt 6 с loadFromModule, который обычно сочетается со сборкой через qt_add_qml_module в CMake.

    Сборка проектов Qt 6: CMake, moc/uic/rcc, Qt Creator

    В Qt 6 стандартный путь сборки — CMake. Qt предоставляет CMake-функции, которые упрощают подключение модулей, генерацию кода и упаковку ресурсов.

  • moc (Meta-Object Compiler) генерирует код для QObject-классов (сигналы/слоты, свойства)
  • uic превращает .ui (формы из Qt Designer) в C++
  • rcc компилирует ресурсы (иконки, QML, файлы) из .qrc
  • На практике, если вы используете правильные CMake-функции Qt, эти шаги выполняются автоматически.

    Ссылка: Build with CMake

    Минимальный CMakeLists.txt для Widgets

    Минимальный CMakeLists.txt для QML (Qt Quick)

    Команды сборки из терминала

    Qt Creator умеет открывать CMake-проекты напрямую и управлять наборами сборки (компилятор, Qt SDK, отладчик).

    Ссылка: Qt Creator Documentation

    Практические рекомендации

  • Не «владейте дважды» QObject: либо родитель-владелец, либо RAII (например, std::unique_ptr) без родителя.
  • В GUI-коде предпочитайте deleteLater(), если объект удаляется «в ответ на событие».
  • Подключайте сигналы/слоты в типобезопасном стиле через &Class::member.
  • При работе с потоками помните, что GUI живёт в главном потоке, а доставку результатов делайте сигналами.
  • В Qt часто используется QString, а не std::string; конвертацию делайте осознанно, на границах слоя.
  • Полезные ссылки

  • Qt Documentation (главная)
  • Qt Core
  • Qt Widgets
  • Qt Quick
  • QObject
  • Signals & Slots
  • CMake (официальная документация)