Основы и тонкости программирования на C++

Этот курс охватывает ключевые аспекты языка C++, от базового синтаксиса до продвинутых тем управления памятью и ООП. Вы научитесь писать эффективный код, использовать STL и применять современные стандарты языка.

1. Введение в C++: синтаксис, типы данных и управляющие конструкции

Введение в C++: синтаксис, типы данных и управляющие конструкции

Добро пожаловать в курс «Основы и тонкости программирования на C++». Это первая статья, с которой начнется ваше погружение в один из самых мощных и востребованных языков программирования в мире. C++ используется везде: от создания операционных систем и драйверов до разработки высокопроизводительных игр и финансовых систем.

В этой статье мы разберем фундамент языка: как выглядит программа, где хранятся данные и как управлять ходом выполнения кода.

Что такое C++ и как он работает?

C++ — это компилируемый, статически типизированный язык общего назначения. Он был разработан Бьерном Страуструпом в начале 1980-х годов как расширение языка C. Главная особенность C++ — это сочетание низкоуровневого доступа к памяти (как в C) и высокоуровневых абстракций (классы, шаблоны).

!Процесс превращения исходного кода в исполняемую программу

Когда вы пишете код на C++, компьютер не понимает его напрямую. Процесс запуска выглядит так:

  • Написание кода: Вы создаете текстовый файл с расширением .cpp.
  • Компиляция: Специальная программа (компилятор) переводит ваш текст в машинный код.
  • Линковка (компоновка): Собирает разрозненные части программы и библиотеки в один исполняемый файл.
  • Структура минимальной программы

    Давайте рассмотрим классический пример «Hello, World!» и разберем его построчно.

    Разбор кода

    * #include <iostream>: Это директива препроцессора. Она говорит компилятору подключить библиотеку ввода-вывода (iostream — Input/Output Stream). Без неё мы не сможем выводить текст на экран. * int main() { ... }: Это главная функция. Любая программа на C++ начинает свое выполнение именно с функции main. Фигурные скобки {} обозначают начало и конец блока кода. * std::cout: Объект, отвечающий за вывод данных в консоль. Приставка std:: означает, что cout находится в стандартном пространстве имен (standard namespace). * <<: Оператор вставки в поток. Он направляет строку "Hello, World!" в cout. * return 0;: Завершение функции. Возврат значения 0 традиционно сообщает операционной системе, что программа выполнилась успешно, без ошибок.

    > В C++ точка с запятой ; является обязательным разделителем команд. Пропуск этого символа — самая частая ошибка новичков.

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

    Программы должны обрабатывать данные. Для хранения данных в оперативной памяти используются переменные. C++ — язык со строгой статической типизацией. Это значит, что вы должны явно указать тип переменной при её создании, и этот тип не может измениться в процессе работы.

    !Визуализация хранения переменных разных типов в памяти

    Основные примитивные типы

    | Тип данных | Описание | Пример значения | Размер (обычно) | | :--- | :--- | :--- | :--- | | int | Целое число | 42, -10 | 4 байта | | double | Число с плавающей точкой (дробное) | 3.14, -0.01 | 8 байт | | char | Одиночный символ | 'A', 'z', '#' | 1 байт | | bool | Логическое значение | true, false | 1 байт |

    Объявление и инициализация

    Диапазоны значений

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

    Формула для расчета количества уникальных значений выглядит так:

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

    Например, для беззнакового типа unsigned char (8 бит) максимальное значение рассчитывается как:

    где — максимальное целое число, — основание двоичной системы, — количество бит, а вычитается, так как отсчет начинается с нуля.

    Ввод и вывод данных

    Мы уже видели std::cout для вывода. Для ввода данных с клавиатуры используется std::cin.

    Обратите внимание на направление «стрелочек» (операторов сдвига): cout << (данные уходят из программы в* консоль). cin >> (данные приходят из консоли в* переменную).

    Управляющие конструкции

    Код не всегда выполняется линейно сверху вниз. Иногда нужно принимать решения или повторять действия.

    Условный оператор if-else

    Позволяет выполнить блок кода только если условие истинно.

    Основные операторы сравнения: * == (равно) * != (не равно) * > (больше), < (меньше) * >= (больше или равно), <= (меньше или равно)

    Циклы

    Циклы используются для повторения кода.

    #### Цикл while

    Выполняется, пока условие истинно. Проверка происходит до итерации.

    #### Цикл for

    Самый популярный цикл, когда известно количество повторений. Он состоит из трех частей: инициализация, условие, обновление.

    !Логическая схема работы цикла for

    Арифметика и операции

    C++ поддерживает стандартные математические операции: * + (сложение) * - (вычитание) (умножение) * / (деление) * % (остаток от деления — только для целых чисел)

    Важный нюанс с делением: если оба операнда целые, результат тоже будет целым (дробная часть отбрасывается).

    Чтобы получить дробный результат, хотя бы одно число должно быть вещественным:

    Заключение

    Сегодня мы разобрали базовый скелет программы на C++. Вы узнали, как объявлять переменные, работать с вводом-выводом и управлять потоком выполнения с помощью условий и циклов. Это те кирпичики, из которых строятся даже самые сложные системы.

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

    Для более глубокого изучения стандартных функций рекомендую обращаться к документации cppreference.

    2. Работа с памятью: указатели, ссылки и динамическое выделение ресурсов

    Работа с памятью: указатели, ссылки и динамическое выделение ресурсов

    Приветствую вас во второй статье курса «Основы и тонкости программирования на C++». В предыдущем уроке мы научились создавать простые переменные, такие как int или double. Мы говорили, что переменная — это именованная область памяти. Но где именно находится эта память? Как к ней обратиться напрямую? И что делать, если мы заранее не знаем, сколько памяти нам понадобится?

    Сегодня мы заглянем «под капот» C++ и разберем одну из самых мощных и одновременно сложных тем: работу с памятью. Именно возможность ручного управления памятью делает C++ незаменимым инструментом для написания высокопроизводительного софта.

    Адресация памяти

    Представьте оперативную память (RAM) вашего компьютера как огромный шкаф с миллионами пронумерованных ячеек. Каждая ячейка имеет свой уникальный номер — адрес. Когда вы пишете int a = 10;, компьютер находит свободную ячейку (или несколько подряд), запоминает её адрес и кладет туда число 10.

    В C++ мы можем узнать адрес любой переменной, используя оператор взятия адреса & (амперсанд).

    На экране вы увидите что-то вроде 0x7ffee4b5, что является шестнадцатеричным представлением адреса ячейки памяти.

    Указатели (Pointers)

    Указатель — это переменная, которая хранит не само число или символ, а адрес другой переменной. Это как записка, на которой написано не «деньги», а «адрес банка, где лежат деньги».

    Объявление и инициализация

    Для объявления указателя используется символ * (звездочка) после типа данных.

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

    Разыменование (Dereferencing)

    Имея указатель, мы можем получить доступ к значению, которое хранится по этому адресу. Это называется разыменованием. Для этого снова используется оператор *, но уже перед именем указателя.

    Важно не путать: int p — объявление указателя. p — доступ к значению по адресу.

    Нулевой указатель

    Указатель может не указывать никуда. Для этого в современном C++ используется ключевое слово nullptr.

    Попытка разыменовать нулевой указатель приведет к критической ошибке программы (Segmentation fault).

    Ссылки (References)

    C++ предлагает еще один инструмент для косвенного доступа к данным — ссылки. Ссылка — это альтернативное имя (псевдоним) для уже существующей переменной.

    Для объявления ссылки используется символ & после типа данных.

    Отличия ссылок от указателей

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

    | Характеристика | Указатель (*) | Ссылка (&) | | :--- | :--- | :--- | | Инициализация | Можно не инициализировать сразу (но опасно) | Обязательна при объявлении | | Null | Может быть nullptr | Не может быть «пустой» | | Изменяемость | Можно перенаправить на другой адрес | Навсегда привязана к одной переменной | | Синтаксис | Требует * для доступа к значению | Используется как обычная переменная |

    Области памяти: Стек и Куча

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

  • Стек (Stack): Здесь хранятся локальные переменные функций. Память выделяется и освобождается автоматически. Это очень быстро, но размер стека ограничен, и переменные живут только пока выполняется функция.
  • Куча (Heap): Это большое пространство памяти, которым программист управляет вручную. Переменные здесь живут столько, сколько мы захотим.
  • Динамическое выделение памяти

    Представьте, что вы пишете программу для обработки списка студентов. Вы не знаете заранее, будет их 5 или 5000. Если вы создадите массив фиксированного размера в стеке (int students[100]), этого может не хватить, или наоборот — память будет потрачена впустую.

    Здесь на помощь приходит динамическая память.

    Операторы new и delete

    * new: Выделяет память в куче и возвращает указатель на неё. * delete: Освобождает память, когда она больше не нужна.

    Динамические массивы

    Чаще всего new используют для создания массивов.

    Арифметика указателей и массивы

    Массивы и указатели в C++ тесно связаны. Имя массива — это, по сути, указатель на его первый элемент. Когда мы обращаемся к элементу arr[i], компилятор вычисляет адрес ячейки памяти.

    Формула вычисления адреса -го элемента выглядит так:

    где — искомый адрес -го элемента, — базовый адрес (адрес начала массива), — индекс элемента (начиная с 0), а — размер одного элемента в байтах (например, 4 байта для типа int).

    Именно поэтому индексы в массивах начинаются с нуля: для первого элемента смещение равно .

    Утечки памяти (Memory Leaks)

    С большой силой приходит большая ответственность. Если вы выделили память через new, но забыли вызвать delete, эта память останется занятой до завершения программы. Это называется утечкой памяти.

    Если такая функция вызывается много раз (например, в игровом цикле), программа сожрет всю доступную оперативную память и «упадет».

    !Метафора утечки памяти: ресурсы расходуются безвозвратно, если их не освобождать.

    Заключение

    Сегодня мы разобрали фундаментальные концепции C++:

  • Указатели хранят адреса и позволяют работать с памятью напрямую.
  • Ссылки создают удобные псевдонимы для переменных.
  • Динамическая память (new/delete) позволяет создавать объекты, размер которых неизвестен на этапе компиляции.
  • Ручное управление памятью — это источник как высочайшей производительности, так и сложных ошибок. В современном C++ (стандарты C++11 и новее) программисты всё реже используют «сырые» указатели (new/delete), предпочитая им «умные указатели» (smart pointers) и контейнеры (например, std::vector), которые управляют памятью автоматически. Но чтобы понять, как работают они, необходимо понимать то, что мы изучили сегодня.

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

    3. Объектно-ориентированное программирование: классы, наследование и полиморфизм

    Объектно-ориентированное программирование: классы, наследование и полиморфизм

    Добро пожаловать в третью статью курса «Основы и тонкости программирования на C++». В прошлых материалах мы изучили синтаксис языка, научились управлять потоком выполнения и, что самое важное, разобрались с памятью и указателями. Теперь у нас есть все необходимые инструменты, чтобы перейти к архитектуре программ.

    До этого момента мы писали код в процедурном стиле: данные лежали отдельно, функции — отдельно. Пока программа маленькая, это работает. Но представьте, что вы пишете сложную RPG-игру. У вас есть сотни персонажей, у каждого свои характеристики, инвентарь и поведение. Управлять этим через разрозненные переменные и глобальные функции становится невозможно. Код превращается в «спагетти».

    Здесь на сцену выходит Объектно-Ориентированное Программирование (ООП). Это парадигма, которая предлагает строить программу как взаимодействие независимых блоков — объектов.

    Классы и объекты

    В основе ООП лежат два понятия: класс и объект. Их часто путают, поэтому давайте разберем разницу на примере чертежа.

    * Класс — это чертеж, шаблон или описание. Он не занимает места в оперативной памяти (пока мы не создадим объект) и лишь описывает, какими свойствами и поведением будет обладать сущность. * Объект (или экземпляр класса) — это конкретная реализация этого чертежа, созданная в памяти компьютера.

    !Иллюстрация различия между классом (чертежом) и объектами (реализациями).

    Создание первого класса

    В C++ классы объявляются с помощью ключевого слова class. Давайте создадим класс для персонажа игры.

    В этом примере name и health называются полями (или свойствами) класса, а функция sayHelloметодом класса.

    Инкапсуляция: защита данных

    Одна из главных идей ООП — инкапсуляция. Это объединение данных и методов работы с ними в одну сущность (класс) и скрытие внутренней реализации от внешнего мира.

    В примере выше мы использовали ключевое слово public. Это модификатор доступа. В C++ их три:

  • public (публичный): Доступ открыт всем. Любая часть программы может читать и менять эти данные.
  • private (приватный): Доступ есть только у методов самого класса. Из main или других функций эти данные не видны.
  • protected (защищенный): Доступ есть у самого класса и у его наследников (об этом ниже).
  • Зачем скрывать данные? Представьте, что кто-то случайно установит здоровье персонажа в -500 или 1000000, минуя игровую логику. Чтобы этого избежать, поля делают приватными, а доступ к ним дают через специальные публичные методы — геттеры (getters) и сеттеры (setters).

    Конструкторы и деструкторы

    Когда объект создается, его нужно инициализировать. Для этого используется специальный метод — конструктор. Он имеет то же имя, что и класс, и не возвращает никакого типа данных.

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

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

    Программисты ленивы, и это хорошо. Мы не хотим писать один и тот же код дважды. Наследование позволяет создать новый класс на основе уже существующего. Новый класс получает все свойства и методы родителя, но может добавлять свои собственные.

    Представьте, что у нас есть общий класс Character, а мы хотим создать Warrior (Воин) и Mage (Маг). Они оба — персонажи, у них обоих есть имя и здоровье. Но у воина есть броня, а у мага — мана.

    !Схема наследования: общий родительский класс и специализированные дочерние классы.

    Теперь объект класса Mage умеет и ходить (move), и колдовать (castSpell), хотя метод move мы в классе Mage не писали.

    Полиморфизм

    Это самое сложное и мощное понятие ООП. Полиморфизм (от греч. «много форм») позволяет работать с объектами разных типов так, как будто это объекты одного типа.

    Представьте, что у вас есть массив указателей на Character. В этом массиве могут лежать и Воины, и Маги, и Лучники. Вы хотите пройтись циклом по массиву и сказать каждому: «Атакуй!». Вам не важно, как именно он атакует (мечом или магией), вам важно, чтобы он просто выполнил действие.

    Для реализации этого в C++ используются виртуальные функции.

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

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

    Рассмотрим пример расчета урона. Допустим, у нас есть формула расчета урона для базового персонажа:

    где — итоговый урон, — базовый урон оружия, а — уровень персонажа.

    Однако для Мага формула может быть другой (с учетом интеллекта), а для Воина — третьей (с учетом силы). Полиморфизм позволяет переопределить этот метод.

    Как это работает с указателями

    Вспомним предыдущую статью про указатели. Полиморфизм в C++ работает только через указатели или ссылки на базовый класс.

    Если бы мы не написали virtual в базовом классе Character, то в обоих случаях вывелось бы «Персонаж просто бьет рукой», потому что компилятор смотрел бы только на тип указателя (Character*), а не на реальный объект.

    Абстрактные классы

    Иногда базовый класс настолько общий, что создавать его экземпляры бессмысленно. Например, в игре не может быть просто «Существо», должно быть конкретное существо (Орк, Эльф). В таком случае метод делают чисто виртуальным (pure virtual).

    Класс, содержащий хотя бы одну чисто виртуальную функцию, называется абстрактным. Создать объект такого класса нельзя (Entity e; вызовет ошибку), но можно использовать указатели на него.

    Заключение

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

  • Классы и объекты: как создавать свои типы данных.
  • Инкапсуляцию: как защищать данные от некорректного использования.
  • Наследование: как избегать дублирования кода.
  • Полиморфизм: как учить программу работать с разными объектами через единый интерфейс.
  • Эти принципы — фундамент современной разработки. В следующей статье мы рассмотрим шаблоны (templates) и стандартную библиотеку шаблонов (STL), которые позволят нам писать еще более универсальный и эффективный код, используя готовые контейнеры и алгоритмы.

    4. Обобщенное программирование и Стандартная библиотека шаблонов (STL)

    Обобщенное программирование и Стандартная библиотека шаблонов (STL)

    Приветствую вас в четвертой статье курса «Основы и тонкости программирования на C++». В предыдущих частях мы прошли долгий путь: научились управлять памятью вручную, создавали классы и выстраивали иерархии наследования. Мы узнали, что полиморфизм позволяет нам писать код, который работает с разными типами объектов, объединенных общим родителем.

    Но что, если нам нужно написать функцию, которая работает с абсолютно разными типами, не связанными наследованием? Например, функцию сортировки массива. Логика сортировки чисел int, дробных чисел double или даже строк std::string одинакова. Неужели нам придется писать три разные функции sortInt, sortDouble и sortString, дублируя код?

    В C++ для решения этой проблемы существует обобщенное программирование (Generic Programming). Сегодня мы узнаем, как писать код один раз для всех типов данных, и познакомимся с мощнейшим инструментом — Стандартной библиотекой шаблонов (STL).

    Шаблоны (Templates): чертежи для кода

    В основе обобщенного программирования лежат шаблоны. Если класс — это чертеж объекта, то шаблон — это чертеж для функции или класса.

    !Метафора работы шаблонов: одна логика применяется к разным типам данных.

    Шаблоны функций

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

    Это нарушение принципа DRY (Don't Repeat Yourself — не повторяйся). С помощью шаблона мы можем сказать компилятору: «Здесь будет какой-то тип T, подставь нужный потом».

    Ключевое слово template сообщает, что дальше идет шаблон. <typename T> (или class T) объявляет, что T — это имя типа-заполнителя. Когда вы вызовете эту функцию в коде, компилятор сам создаст нужную версию:

    Шаблоны классов

    Шаблоны применяются и к классам. Представьте, что вы хотите создать класс «Коробка», который может хранить один предмет любого типа.

    Обратите внимание: при создании объекта шаблонного класса мы обязаны явно указать тип в угловых скобках <int>, так как компилятор не всегда может вывести его автоматически.

    Стандартная библиотека шаблонов (STL)

    Вам не нужно писать свои контейнеры (динамические массивы, списки) или алгоритмы сортировки с нуля. Профессионалы уже сделали это за вас, оптимизировали до предела и включили в стандарт языка. Это и есть STL (Standard Template Library).

    STL состоит из трех главных компонентов, которые работают вместе как единый механизм:

  • Контейнеры: Где хранятся данные.
  • Алгоритмы: Как обрабатываются данные.
  • Итераторы: Связующее звено между первыми двумя.
  • 1. Контейнеры

    Контейнеры — это объекты, которые управляют коллекциями других объектов. Самый популярный и часто используемый контейнер — это std::vector.

    #### std::vector (Вектор)

    Это динамический массив. В отличие от обычного массива C (int arr[10]), вектор умеет автоматически изменять свой размер.

    Вектор хранит элементы в памяти подряд, поэтому доступ к любому элементу происходит мгновенно.

    #### std::list (Список)

    Это двусвязный список. Элементы разбросаны по памяти, но каждый элемент знает адрес предыдущего и следующего.

    !Вектор хранит данные непрерывно, а список — цепочкой связанных узлов.

    Вставка в середину списка происходит очень быстро (нужно просто перекинуть стрелочки), но чтобы найти 5-й элемент, нужно пройти по цепочке от начала.

    #### std::map (Словарь или Карта)

    Это ассоциативный контейнер, хранящий пары «ключ-значение». Ключи всегда уникальны и автоматически отсортированы.

    2. Итераторы

    Как перебрать элементы контейнера? У вектора есть индексы [i], но у списка или словаря индексов нет. Чтобы алгоритмы могли работать с любым контейнером одинаково, придумали итераторы.

    Итератор — это «умный указатель», который знает, как перейти к следующему элементу конкретного контейнера.

    У каждого контейнера есть методы: * begin(): возвращает итератор на первый элемент. end(): возвращает итератор на позицию после* последнего элемента (маркер конца).

    3. Алгоритмы

    Библиотека <algorithm> содержит сотни готовых функций: сортировка, поиск, копирование, переворот и т.д. Они работают не с контейнерами напрямую, а с диапазонами итераторов.

    Обратите внимание: функции std::sort все равно, сортирует она вектор чисел или вектор строк. Она использует оператор сравнения < для элементов и итераторы для перемещения.

    Сложность алгоритмов (Big O)

    Выбирая контейнер в STL, важно понимать, насколько быстро он работает. В программировании эффективность измеряется не в секундах (они зависят от мощности компьютера), а в количестве операций относительно количества данных . Это называется «О-большое» нотация.

    Рассмотрим формулу сложности для поиска элемента в std::map (который построен на основе бинарного дерева):

    где — время выполнения (количество операций), — обозначение порядка сложности, — логарифм по основанию 2, а — количество элементов в контейнере.

    Это означает, что если данных станет в 1000 раз больше, время поиска увеличится всего лишь в 10 раз (так как ). Это очень эффективно.

    Для сравнения, поиск в обычном несортированном векторе имеет сложность:

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

    | Операция | std::vector | std::list | std::map | | :--- | :--- | :--- | :--- | | Доступ по индексу | (Мгновенно) | Не поддерживается | | | Вставка в конец | | | | | Вставка в середину | (Медленно) | (Быстро) | - |

    Заключение

    Обобщенное программирование и STL — это то, что делает C++ мощным инструментом для промышленной разработки. Вам больше не нужно думать о том, как выделить память под массив или как написать быструю сортировку. Вы просто берете std::vector и std::sort.

    Главное правило новичка: прежде чем писать свой велосипед, проверь, нет ли этого в STL.

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

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

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

    Добро пожаловать в пятую статью курса «Основы и тонкости программирования на C++». В предыдущих уроках мы прошли путь от ручного управления памятью с помощью new и delete до использования контейнеров STL. Вы уже знаете, что управление ресурсами — это одна из самых сложных задач в C++. Ошибка в одном месте может привести к утечке памяти или падению программы.

    Начиная со стандарта C++11 (выпущенного в 2011 году), язык претерпел революционные изменения. Появилось понятие «Современный C++» (Modern C++). Его философия проста: код должен быть безопасным, выразительным и эффективным. Сегодня мы рассмотрим три кита современного C++: автоматическое управление памятью, анонимные функции и параллельные вычисления.

    Умные указатели (Smart Pointers)

    Вспомним проблему «сырых» указателей: выделив память, вы обязаны её освободить. Если вы забудете вызвать delete или если программа выбросит исключение до этого момента, произойдет утечка памяти.

    Современный C++ решает эту проблему с помощью идиомы RAII (Resource Acquisition Is Initialization — Получение ресурса есть инициализация). Суть проста: ресурс (память) захватывается в конструкторе объекта и освобождается в его деструкторе. Поскольку деструкторы локальных объектов вызываются автоматически при выходе из области видимости, память гарантированно будет освобождена.

    Классы, реализующие эту логику для указателей, называются умными указателями. Они находятся в библиотеке <memory>.

    !Визуализация принципа работы умного указателя как автоматической обертки над сырым ресурсом

    std::unique_ptr

    Это самый легкий и быстрый умный указатель. Он реализует концепцию эксклюзивного владения. Это значит, что на один объект в памяти может указывать только один unique_ptr.

    Главное правило: используйте std::unique_ptr по умолчанию. Он не создает накладных расходов по сравнению с обычным указателем.

    std::shared_ptr

    Иногда ресурсом должны владеть несколько объектов одновременно. Например, текстура в игре используется десятком разных моделей. Текстура должна быть удалена только тогда, когда исчезнет последняя модель, использующая её.

    std::shared_ptr использует подсчет ссылок. Внутри он хранит не только указатель на объект, но и счетчик того, сколько умных указателей сейчас смотрят на этот объект.

    Логика работы счетчика :

  • При создании первого shared_ptr: .
  • При копировании указателя: .
  • При уничтожении одного из указателей: .
  • Когда , реальный объект удаляется из памяти.
  • Лямбда-выражения

    Часто нам нужно передать небольшую функцию в алгоритм STL, например, условие для сортировки или поиска. Раньше приходилось писать отдельные функции или функторы, что загромождало код. C++11 ввел лямбда-выражения — анонимные функции, которые можно объявлять прямо в месте использования.

    Синтаксис

    Базовый синтаксис выглядит так:

    захват { тело функции }

    * [захват]: Какие переменные из внешнего контекста доступны внутри лямбды. * (параметры): Аргументы функции (как в обычной функции). * { тело }: Код, который нужно выполнить.

    Пример использования

    Допустим, мы хотим отсортировать вектор чисел по убыванию.

    Списки захвата

    Самое мощное в лямбдах — возможность использовать переменные, объявленные вне лямбды.

    * [=]: Захватить все внешние переменные по значению (создать копии). * [&]: Захватить все внешние переменные по ссылке (можно менять оригинал). * [x, &y]: Захватить x по значению, а y по ссылке.

    Многопоточность (Multithreading)

    Долгое время C++ не имел встроенной поддержки многопоточности, полагаясь на возможности операционной системы. В современном C++ многопоточность стала частью стандарта.

    Многопоточность позволяет программе выполнять несколько задач одновременно, используя ядра процессора эффективнее.

    Создание потока

    Для работы с потоками используется класс std::thread из библиотеки <thread>.

    Метод join() обязателен: он блокирует основной поток до тех пор, пока t1 не закончит работу. Если забыть join(), программа аварийно завершится при выходе из main, так как поток t1 будет уничтожен в процессе работы.

    Гонка данных (Data Race)

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

    Представьте, что два потока увеличивают переменную counter на 1. Операция counter++ на самом деле состоит из трех шагов:

  • Прочитать значение из памяти.
  • Увеличить значение.
  • Записать обратно.
  • Если потоки выполнят шаг 1 одновременно, они оба прочитают старое значение, увеличат его и запишут. В итоге вместо +2 мы получим +1.

    Мьютексы и блокировки

    Для защиты общих данных используются мьютексы (std::mutex). Мьютекс (Mutual Exclusion) — это объект, который позволяет только одному потоку владеть им в данный момент времени.

    !Схема работы мьютекса для синхронизации потоков

    Мы использовали std::lock_guard — это RAII-обертка для мьютекса. Она вызывает mtx.lock() в конструкторе и mtx.unlock() в деструкторе. Это защищает от вечной блокировки (deadlock), если внутри критической секции произойдет ошибка.

    Закон Амдала

    Важно понимать, что добавление потоков не всегда ускоряет программу линейно. Существует теоретический предел ускорения, описываемый законом Амдала:

    где — максимальное ускорение программы, — доля кода, которую можно распараллелить (от 0 до 1), а — количество процессоров (потоков).

    Даже если у вас бесконечное количество процессоров (), максимальное ускорение ограничено последовательной частью программы:

    где — предельное ускорение, а — доля последовательного кода.

    Например, если 20% программы должно выполняться последовательно (), то даже на суперкомпьютере вы не получите ускорение больше чем в 5 раз.

    Заключение

    Современный C++ предоставляет мощные инструменты, которые делают код чище и безопаснее:

  • Умные указатели (unique_ptr, shared_ptr) автоматизируют работу с памятью.
  • Лямбда-выражения позволяют писать компактные функции прямо на месте.
  • Многопоточность (thread, mutex) открывает доступ к параллельным вычислениям.
  • Эти инструменты — стандарт де-факто в индустрии сегодня. Используйте их вместо устаревших подходов языка C, чтобы писать надежный и производительный софт.

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