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

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

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

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

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

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

Анатомия программы на C++

Любая программа на C++ — это набор инструкций. Однако, в отличие от скриптовых языков (например, Python), C++ требует строгой структуры. Давайте рассмотрим классический пример «Hello, World!» и разберем его построчно.

Разбор кода

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

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

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

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

    Основные (фундаментальные) типы данных

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

    | Тип данных | Описание | Пример значения | | :--- | :--- | :--- | | int | Целое число (Integer) | 42, -10, 0 | | double | Число с плавающей точкой (двойной точности) | 3.14, -0.005 | | char | Одиночный символ | 'a', 'Z', '!' | | bool | Логическое значение | true, false | | void | Пустой тип (отсутствие значения) | Используется в функциях |

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

    Чтобы использовать переменную, её нужно объявить:

    > Важно: Если вы объявите локальную переменную, но не присвоите ей значение, она будет содержать случайные данные, оставшиеся в этой ячейке памяти. Это называется «неопределенным поведением».

    Размер данных в памяти

    Компьютеры работают с битами. Тип данных определяет, сколько памяти выделяется под переменную и как интерпретируются эти биты. Например, тип int обычно занимает 4 байта (32 бита).

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

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

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

    где — количество значений (от 0 до 255), а — количество бит.

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

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

    Запомните мнемоническое правило: * cout <<: данные выходят из программы на экран. * cin >>: данные входят в переменную из клавиатуры.

    Операторы

    C++ предоставляет богатый набор операторов для манипуляции данными.

    Арифметические операторы

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

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

    Логические операторы и операторы сравнения

    Используются для проверки условий: * == (равно), != (не равно) * > (больше), < (меньше) * && (логическое И), || (логическое ИЛИ), ! (логическое НЕ)

    Управление потоком: Ветвление

    Программы редко бывают линейными. Часто нам нужно выполнить код только при определенном условии. Для этого используется конструкция if.

    Конструкция if-else

    !Блок-схема, демонстрирующая логику работы конструкции if-else.

    Конструкция switch

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

    Ключевое слово break необходимо, чтобы выйти из switch. Без него программа продолжит выполнять код следующих case, игнорируя проверки.

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

    Циклы позволяют выполнять блок кода многократно. Это основа автоматизации.

    Цикл while

    Выполняется, пока условие истинно.

    Цикл for

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

    Цикл do-while

    Гарантирует, что тело цикла выполнится хотя бы один раз, так как проверка условия происходит в конце.

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

    Области видимости (Scope)

    Переменные, объявленные внутри фигурных скобок {}, видны только внутри этих скобок. Это называется локальной областью видимости.

    Понимание области видимости помогает избегать конфликтов имен и управлять памятью.

    Заключение

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

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

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

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

    В предыдущей статье мы рассмотрели базовые типы данных, такие как int или double. Мы создавали переменные, присваивали им значения и выводили их на экран. Но задумывались ли вы, где именно физически находятся эти данные? Как компьютер находит число 42, которое вы сохранили в переменной age?

    Ответ кроется в оперативной памяти (RAM). Понимание того, как C++ работает с памятью — это именно та черта, которая отделяет новичка от профессионала. Это дает вам суперсилу: полный контроль над ресурсами компьютера, но налагает и большую ответственность.

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

    Оперативная память как огромный массив

    Представьте оперативную память вашего компьютера как гигантский шкаф с миллионами пронумерованных ячеек. Каждая ячейка имеет свой уникальный номер — адрес. Этот адрес обычно записывается в шестнадцатеричном формате (например, 0x7ffee4).

    Когда вы пишете int a = 10;, происходят две вещи:

  • Система находит свободный блок памяти (для int это обычно 4 байта).
  • Система запоминает, что имя a связано с адресом первой ячейки этого блока.
  • В эти ячейки записывается значение 10.
  • Указатели (Pointers)

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

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

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

    !Визуализация того, как указатель хранит адрес другой переменной и ссылается на неё.

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

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

    > Важно: Никогда не используйте неинициализированные указатели. Если вы объявили int p; и сразу попытались сделать p = 5;, программа скорее всего аварийно завершится, так как p указывает на случайный участок памяти.

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

    Если указатель пока никуда не указывает, хорошей практикой является присвоение ему специального значения nullptr.

    Это позволяет проверить указатель перед использованием: if (p != nullptr) { ... }.

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

    Указатели можно складывать с целыми числами. Но это работает не так, как обычная математика. Если вы прибавите к указателю 1, адрес увеличится не на 1 байт, а на размер типа данных, на который он указывает.

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

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

    Если адрес int* p равен 1000, то p + 1 будет равен 1004.

    Ссылки (References)

    Указатели мощны, но синтаксис с * и & может быть запутанным и опасным. C++ предлагает более безопасную альтернативу — ссылки.

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

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

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

    Управление ресурсами: Стек и Куча

    Память, доступная вашей программе, делится на две основные области: Стек (Stack) и Куча (Heap).

    Стек (Stack)

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

    * Плюсы: Скорость, автоматическая очистка (когда переменная выходит из области видимости). * Минусы: Ограниченный размер, переменные живут только внутри своего блока {}.

    Куча (Heap)

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

    Для работы с кучей используются операторы new и delete.

    Если вы забудете вызвать delete, память останется занятой до конца работы программы. Это называется утечкой памяти (Memory Leak). В долго работающих программах (например, серверах или играх) утечки могут привести к тому, что вся оперативная память закончится, и программа зависнет.

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

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

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

    Современное управление ресурсами (RAII и умные указатели)

    Ручное управление памятью (new/delete) — источник постоянных ошибок. В современном C++ (начиная с C++11) рекомендуется использовать умные указатели (Smart Pointers). Они находятся в библиотеке <memory>.

    Умные указатели — это объекты, которые ведут себя как указатели, но автоматически вызывают delete, когда они больше не нужны. Это реализация идиомы RAII (Resource Acquisition Is Initialization — Получение ресурса есть инициализация).

    std::unique_ptr

    Владеет объектом единолично. Когда unique_ptr уничтожается, он удаляет и объект в памяти.

    std::shared_ptr

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

    Заключение

    Работа с памятью — это обоюдоострый меч C++. Указатели дают вам прямой доступ к «железу» и высокую производительность, но требуют строгой дисциплины. Ссылки упрощают работу с передачей данных, а динамическая память (куча) позволяет создавать гибкие структуры данных.

    Главный совет для современного C++ разработчика: избегайте ручного управления памятью (new/delete), когда это возможно. Используйте контейнеры (например, std::vector, о котором мы поговорим позже) и умные указатели. Это сделает ваш код безопасным и чистым.

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

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

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

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

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

    Классы и объекты: чертеж и здание

    Главная идея ООП заключается в объединении данных (переменных) и поведения (функций) в единую сущность. В C++ для этого используются классы.

    Что такое класс?

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

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

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

    Синтаксис класса

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

    Теперь мы можем создать объект этого класса в функции main:

    Инкапсуляция и модификаторы доступа

    В примере выше данные name и health находятся в секции public. Это значит, что кто угодно может изменить их извне. Представьте, что кто-то напишет hero.health = -500; без получения урона. Это нарушает логику игры.

    Инкапсуляция — это механизм скрытия внутренней реализации объекта от внешнего мира. Мы используем модификаторы доступа:

  • public: доступно всем.
  • private: доступно только методам самого класса.
  • protected: доступно классу и его наследникам (об этом ниже).
  • Правильный подход:

    Теперь мы не можем написать hero.health = -100, компилятор выдаст ошибку. Мы обязаны использовать метод takeDamage, который содержит проверку логики.

    Размер объекта в памяти

    Вспомним предыдущую статью о памяти. Сколько места занимает объект? Размер объекта класса равен сумме размеров всех его полей плюс выравнивание (padding).

    Формула размера объекта:

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

    Важно отметить: функции-члены (методы) не увеличивают размер каждого объекта. Код функций хранится в памяти в единственном экземпляре для всех объектов класса.

    Наследование: не повторяй себя

    Представьте, что в нашей игре есть не просто «Персонажи», а «Воины» и «Маги». У них много общего (имя, здоровье), но есть и различия (у воина — броня, у мага — мана).

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

    Теперь объект Warrior имеет доступ и к name, и к move(), и к своим собственным свойствам.

    !Иерархия наследования: базовый класс передает свои свойства производным классам.

    Полиморфизм: один интерфейс, много форм

    Полиморфизм — это, пожалуй, самая сложная, но и самая мощная концепция ООП. Слово происходит от греческого «много форм».

    Суть в том, чтобы работать с объектами разных типов (Воин, Маг) одинаковым образом, но получать разное поведение.

    Проблема без полиморфизма

    Допустим, мы хотим, чтобы все персонажи атаковали. У Воина атака — это удар мечом, у Мага — огненный шар.

    Если мы создадим указатель типа Character* на объект Warrior, произойдет следующее:

    Поскольку указатель имеет тип Character, компилятор будет искать метод attack именно в классе Character, игнорируя тот факт, что на самом деле там лежит Warrior.

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

    Чтобы решить эту проблему, в C++ используется ключевое слово virtual. Оно говорит компилятору: «Не смотри на тип указателя, смотри на реальный тип объекта в памяти во время выполнения программы».

    Теперь магия работает:

    Это называется динамическим полиморфизмом. Обратите внимание, что для его работы обязательно использование указателей или ссылок на базовый класс. Если передавать объекты по значению, произойдет «срезка» (object slicing), и полиморфизм не сработает.

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

    Иногда базовый класс настолько общий, что создавать его экземпляры не имеет смысла. Например, что такое просто «Животное»? Оно не может издать звук, пока мы не знаем, кошка это или собака.

    В C++ можно создать чисто виртуальную функцию (pure virtual function), присвоив ей 0.

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

    Заключение

    Мы рассмотрели фундамент современного C++:

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

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

    4. Стандартная библиотека шаблонов (STL): контейнеры и алгоритмы

    Стандартная библиотека шаблонов (STL): контейнеры и алгоритмы

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

    К счастью, создатели C++ позаботились об этом. Они подарили нам STL (Standard Template Library) — Стандартную библиотеку шаблонов. Это набор готовых, высокоэффективных инструментов, которые превращают C++ из «конструктора деталей» в мощный инженерный комплекс.

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

    Контейнеры: умные хранилища данных

    Вспомните, как мы создавали динамический массив через new int[size]. Нам приходилось помнить о размере, следить за утечками памяти и вручную перевыделять память, если массив переполнялся. STL предлагает контейнеры — готовые классы, которые берут всю эту рутину на себя.

    std::vector — Динамический массив

    std::vector — это самый популярный контейнер. Это массив, который умеет автоматически изменять свой размер. Если вы не знаете, какой контейнер выбрать, выбирайте вектор.

    Чтобы использовать его, нужно подключить библиотеку <vector>.

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

    Оценка эффективности (Big O)

    В программировании эффективность алгоритмов принято оценивать с помощью «О-большого» ().

    Для вектора доступ к элементу по индексу имеет сложность:

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

    !Сравнение организации памяти в векторе (непрерывный блок) и списке (связанные узлы).

    std::list — Связный список

    В отличие от вектора, std::list (из библиотеки <list>) хранит элементы не подряд, а в разных местах памяти, связывая их указателями. Каждый элемент знает, где находится предыдущий и следующий.

    Плюсы: * Мгновенная вставка и удаление элементов в любом месте списка (нужно просто перекинуть указатели).

    Минусы: * Нет доступа по индексу (нельзя написать myList[5]). Чтобы найти 5-й элемент, нужно перебрать первые четыре.

    std::map — Ассоциативный массив (Словарь)

    Иногда нам нужно хранить данные не по порядку (0, 1, 2...), а по ключам (например, имя человека -> номер телефона). Для этого используется std::map.

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

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

    Итераторы: универсальный доступ

    У вектора есть индексы [], у списка их нет, у карты ключи — строки. Как же перебрать все элементы контейнера универсальным способом? Для этого существуют итераторы.

    Итератор — это объект, который ведет себя как указатель. Он указывает на элемент контейнера и умеет переключаться на следующий.

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

    Пример перебора вектора с помощью итератора:

    !Визуализация работы итераторов begin() и end() в диапазоне данных.

    Алгоритмы: готовые решения

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

    Сортировка (std::sort)

    Зачем писать «пузырьковую сортировку», если есть оптимизированная std::sort?

    Поиск (std::find)

    Позволяет найти элемент в диапазоне.

    Выполнение действия (std::for_each)

    Если нужно применить функцию к каждому элементу:

    Range-based for loop (Цикл for по диапазону)

    Начиная с C++11, работа с контейнерами стала еще проще благодаря «синтаксическому сахару». Вместо явного использования итераторов можно писать так:

    Под капотом этот цикл все равно использует итераторы begin() и end(), но код выглядит намного чище.

    Как выбрать контейнер?

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

  • Вам нужен массив, и вы чаще всего добавляете данные в конец? -> std::vector (используйте в 90% случаев).
  • Вам нужно часто вставлять и удалять данные в середине или начале? -> std::list.
  • Вам нужно хранить пары «Ключ-Значение» и быстро искать по ключу? -> std::map.
  • Вам нужно хранить только уникальные элементы и быстро проверять их наличие? -> std::set.
  • Заключение

    STL — это шедевр инженерной мысли. Библиотека позволяет писать код, который: * Безопасен: меньше шансов ошибиться с памятью. * Эффективен: алгоритмы STL написаны экспертами и работают максимально быстро. * Читаем: std::sort понятнее, чем три вложенных цикла.

    Мы рассмотрели лишь верхушку айсберга. В STL есть еще очереди (queue), стеки (stack), множества (set) и десятки полезных алгоритмов. Но поняв принцип связки «Контейнер + Итератор + Алгоритм», вы сможете легко освоить любой инструмент из этой библиотеки.

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

    5. Продвинутые возможности и современные стандарты C++ (11/14/17/20)

    Продвинутые возможности и современные стандарты C++ (11/14/17/20)

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

    Стандарт C++11 и последующие обновления (C++14, C++17, C++20) превратили C++ в Modern C++ — современный, выразительный и безопасный язык. В этой статье мы разберем ключевые нововведения, которые обязан знать каждый профессиональный разработчик.

    Автоматический вывод типов: auto

    Раньше, объявляя итератор для вектора, нам приходилось писать громоздкие конструкции:

    Ключевое слово auto, введенное в C++11, позволяет компилятору самому определить тип переменной на основе того значения, которое ей присваивается.

    > Важно: auto не означает «динамический тип» (как в Python). Тип определяется один раз на этапе компиляции и больше не меняется. Это просто синтаксический сахар, который делает код чище.

    Лямбда-выражения: функции внутри функций

    Часто нам нужны маленькие функции для одноразового использования, например, для передачи в алгоритм std::sort или std::for_each. Писать для этого отдельную функцию или функтор (класс с перегруженным оператором ()) долго и неудобно.

    Лямбда-выражение — это анонимная функция, которую можно объявить прямо в месте использования.

    Синтаксис лямбды

    Общий вид выглядит так: []() {}.

  • []Список захвата (Capture clause): какие переменные из внешнего контекста доступны внутри лямбды.
  • ()Список параметров: аргументы функции.
  • {}Тело функции: код, который нужно выполнить.
  • Пример сортировки вектора по убыванию:

    Захват переменных

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

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

    !Структура лямбда-выражения: захват контекста, параметры и исполняемый код.

    Semantics Move (Семантика перемещения)

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

    Представьте, что вы переезжаете в новый дом. У вас есть два варианта:

  • Копирование: Купить точно такую же мебель, одежду и технику в новый дом, а старый дом сжечь вместе с вещами.
  • Перемещение: Взять вещи из старого дома и перевезти их в новый.
  • Очевидно, второй вариант эффективнее. В C++ это реализуется через R-value ссылки (обозначаются &&) и функцию std::move.

    L-value и R-value

    * L-value (Left value): объекты, у которых есть имя и адрес в памяти (переменные). Они могут стоять слева от знака =. * R-value (Right value): временные объекты, которые существуют только в момент вычисления выражения (числа, результаты функций). Обычно они стоят справа от =.

    Как работает перемещение

    Когда мы используем std::move(obj), мы превращаем obj в R-value, говоря компилятору: «Этот объект мне больше не нужен, можешь забрать его внутренности».

    Рассмотрим эффективность на примере математической оценки сложности алгоритма копирования и перемещения вектора.

    При копировании вектора размером сложность операции составляет:

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

    При перемещении того же вектора сложность составляет:

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

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

    Мы уже упоминали их в статье про память, но именно C++11 стандартизировал их. Они пришли на замену «сырым» указателям (new/delete) и std::auto_ptr (который теперь удален).

    * std::unique_ptr: Единоличное владение. Нельзя скопировать, можно только переместить. * std::shared_ptr: Совместное владение. Использует счетчик ссылок. * std::weak_ptr: Слабая ссылка, не увеличивает счетчик владельцев (нужна для разрыва циклических зависимостей).

    Нововведения C++17

    Структурное связывание (Structured Binding)

    Позволяет распаковывать кортежи, пары и структуры в отдельные переменные. Это очень похоже на множественное присваивание в Python.

    std::optional

    Часто функции должны возвращать значение, но иногда результат может отсутствовать (например, поиск элемента). Раньше возвращали -1 или nullptr. C++17 ввел std::optional — контейнер, который может содержать значение или быть пустым.

    Нововведения C++20

    Стандарт C++20 стал самым масштабным обновлением со времен C++11.

    Концепты (Concepts)

    Шаблоны (templates) в C++ — мощный инструмент, но они часто выдают ужасные, нечитаемые ошибки на 100 строк, если передать им неправильный тип. Концепты позволяют накладывать ограничения на типы шаблонов.

    Модули (Modules)

    С самого создания C++ использовал систему заголовочных файлов (#include). Это медленно и приводит к конфликтам имен. Модули призваны заменить эту архаичную систему.

    Вместо #include <iostream> мы будем писать import std;. Это значительно ускоряет компиляцию и изолирует код.

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

    До C++11 многопоточность зависела от операционной системы (WinAPI, POSIX). Теперь она встроена в язык.

    Класс std::thread позволяет запускать код параллельно.

    > Важно: Многопоточность — сложная тема, требующая синхронизации доступа к общим данным (используя мьютексы std::mutex), чтобы избежать «гонок данных» (data races).

    Заключение

    Современный C++ — это не просто «Си с классами». Это язык, который стремится быть максимально эффективным, но при этом удобным и безопасным. Использование auto, умных указателей, семантики перемещения и алгоритмов STL делает код короче и быстрее.

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

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