# Глава 1.1. Что такое C++ C++ — это компилируемый, статически типизированный язык с прицелом на _эффективность._ На C++ можно писать драйверы, заточенные под конкретное устройство, а можно создавать высокоуровневую бизнес-логику кроссплатформенного проекта. C++ — универсальный язык, подходящий для решения практически любой задачи. При этом он позволяет выжать из железа максимум. Пока вы читаете эту главу, C++ код исполняется в дефибрилляторах, AAA-играх, СУБД, текстовых процессорах, поисковиках. И даже [в марсоходе](https://www.youtube.com/watch?v=3SdSKZFoUa8) Curiosity: ![Ровер Curiosity. На борту C++!](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/main/illustrations/cpp/mars_rover_curiosity.jpg) {.illustration} За эффективность и универсальность C++ приходится платить: - высоким порогом входа в язык, - скоростью разработки, - количеством деталей, о которых задумывается разработчик, чтобы писать качественный код. ## Философия С++ За свою 40-летнюю историю C++ стал одним из самых распространённых языков в мире. Секрет популярности кроется в философии C++. Она ставит во главу угла несколько принципов. ### Свобода выбора Язык не навязывает «единственно верного» пути. Вы можете вручную управлять ресурсами и контролировать каждое выделение памяти. А можете использовать удобные и высокоуровневые средства стандартной библиотеки. Если на проект хорошо ложится обработка ошибок через исключения — пользуйтесь ими! Хотите работать со старыми-добрыми кодами ошибок? Пожалуйста. Понимаете, что такое алгебраические типы данных? Найдется и такое. Если вы пришли в мир C++ из Go, то почувствуйте разницу. C++ предоставляет широкий арсенал возможностей. От разработчика же требуется наличие здравого смысла и некоего багажа знаний. Конечно, у такого богатства есть и обратная сторона: с годами C++ окончательно укоренился в роли самого сложного из мейнстримных языков. С++ часто сравнивают со швейцарским ножом. _Очень многофункциональным_ швейцарским ножом. ![C++ — это мультитул!](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/main/illustrations/cpp/multitool.jpg) {.illustration} ### Обратная совместимость {#block-backward-compatibility} Обратная совместимость — это возможность собрать старый код новым компилятором. Или, например, подключить к новому проекту библиотеку, написанную 30 лет назад. Фичи и улучшения вносятся в C++ предельно осторожно. Поломка обратной совместимости может быть _единственной_ причиной для отказа от ускорения на 10% контейнера из стандартной библиотеки. Из-за упора на обратную совместимость синтаксис С++ и стандартная библиотека порой выглядят неконсистентно. А устаревшие фичи остаются с нами надолго. Значит, обратная совместимость — скорее недостаток, чем достоинство? Не совсем. Назовите, какой ещё язык способен похвастаться таким количеством старых проектов, живущих и развивающихся и по сей день? Наверное, только Си. Порой при внесении изменений в язык на обратную совместимость все же закрывают глаза. И пример тому — замена внутренней реализации класса строки `std::string` в C++11. ### Эффективность {#block-efficiency} **Вы не платите за то, что не используете.** К примеру, в языке нет встроенных проверок выхода за границы массива, ведь в ряде случаев они избыточны. В остальных же случаях разработчик должен организовать их самостоятельно. Абстракции с нулевой стоимостью (zero-cost abstractions) делают код простым и понятным. А компилятор оптимизирует его так, чтобы он не уступал низкоуровневому аналогу. То есть стоимость у таких абстракций все-таки имеется, но взимается не в рантайме, а во время компиляции — сборки проекта. Поэтому более точное название этих абстракций — **абстракции с нулевым оверхедом** (zero-overhead abstractions). {#block-zero-overhead} Рассмотрим пример такой абстракции. Возьмем динамический массив `numbers` из целых чисел. Нужно пройтись по нему и поместить каждое число в диапазон: если оно меньше нуля, приравнять к нулю. Если больше 100 — сделать равным 100. В цикле переберём индексы массива и применим это условие к каждому элементу: {#block-naive} ```cpp {.example_for_playground .example_for_playground_001} std::vector<int> numbers = random_vector(); for (std::size_t i = 0; i < numbers.size(); ++i) { if (numbers[i] < 0) { numbers[i] = 0; } else if (numbers[i] > 100) { numbers[i] = 100; } } std::println("{}", numbers); ``` ``` [100, 72, 3, 0, 100, 100, 100, 45, 100, 100] ``` Нажмите на кнопку «Открыть в песочнице» в верхнем углу этого примера. Вы увидите расширенный фрагмент кода, к которому добавлено измерение времени выполнения. Запустите его. Кстати, если в примерах кода этой главы вам не все понятно, не пугайтесь. В следующих главах мы разберем нюансы. В этом коде приходится уделять внимание низкоуровневым деталям: - Допустимо ли использовать индексы для итерирования по контейнеру данного типа? - Не закралась ли ошибка при работе с индексами? Решим эту задачу иначе — с применением абстракций. Напишем функцию `clamp_to_pct()`, которая изменяет целое число по заданным правилам. Внутри она вызывает функцию стандартной библиотеки `std::clamp()`. Ознакомьтесь с её описанием [на сайте cppreference.com.](https://en.cppreference.com/w/cpp/algorithm/clamp) Это лучший справочник по C++, и вы часто будете в него заглядывать. {#block-clamp} ```cpp void clamp_to_pct(int & n) { n = std::clamp(n, 0, 100); } ``` А теперь заменим цикл на вызов функции стандартной библиотеки `std::for_each()`. Она применит `clamp_to_pct()` к каждому элементу массива: {#block-for-each} ```cpp {.example_for_playground .example_for_playground_002} std::vector<int> numbers = random_vector(); std::for_each(numbers.begin(), numbers.end(), clamp_to_pct); std::println("{}", numbers); ``` ``` [100, 72, 3, 0, 100, 100, 100, 45, 100, 100] ``` В этом примере появилось несколько абстракций: функция `clamp_to_pct()`, [функция высшего порядка](https://ru.wikipedia.org/wiki/%D0%A4%D1%83%D0%BD%D0%BA%D1%86%D0%B8%D1%8F_%D0%B2%D1%8B%D1%81%D1%88%D0%B5%D0%B3%D0%BE_%D0%BF%D0%BE%D1%80%D1%8F%D0%B4%D0%BA%D0%B0) `std::for_each()` и итераторы `begin()` и `end()`. Итератор — это объект, позволяющий перебирать элементы контейнера и поштучно предоставлять к ним доступ. В нашем случае итераторы послужили заменой индексам. Этот код выглядит высокоуровневым. Он: - Ограждает от потенциальных ошибок при работе с индексами. - Позволяет забыть о внутренней организации перебираемого контейнера. - Сконцентрирован только на логике того, что должна делать программа. Казалось бы, количество абстракций возросло, и это ударит по производительности. Однако все с точностью до наоборот. В релизной сборке второй вариант работает быстрее, чем первый! Не верите? Убедитесь сами: откройте его в песочнице и сравните время выполнения обеих реализаций. Но помните, что _не любая_ абстракция языка имеет нулевой оверхед. ## Что делает язык C++ языком C++ Рассмотрим ключевые особенности, на которых строится C++. ### Слабая статическая типизация Типизация в C++ статическая, слабая. Есть автоматический вывод типов. **Статическая типизация** гарантирует, что переменная связывается с типом в момент объявления. После этого тип переменной не меняется. Это отличает C++ от языков с динамической типизацией, таких как Python и JavaScript. В них переменная связывается с типом в момент присваивания значения. В C++ целочисленная переменная не может внезапно превратиться в строку. Компилятор просто не даст вам собрать и запустить такой код: ```cpp {.example_for_playground .example_for_playground_004} int len_km = 6; len_km = "six km"; // ошибка ``` ``` error: invalid conversion from 'const char*' to 'int' ``` Перед вами функция `get_price_with_discount()`. Она принимает два аргумента. Это число с плавающей точкой (цена товара `price`) и флаг `has_promocode`, свидетельствующий о наличии у покупателя промокода. Функция возвращает цену товара с учётом скидки. {.task_text} В ней допущена ошибка: указан не тот тип возвращаемого значения. {.task_text} Нажмите кнопку «Запустить», чтобы прочитать ошибку компиляции. {.task_text} Исправьте тип возвращаемого значения. {.task_text} ```cpp {.task_source #cpp_chapter_0011_task_0010} std::string get_price_with_discount(double price, bool has_promocode) { if (has_promocode) { return price * 0.9; } return price; } ``` Функция возвращает строку. Но в теле функции видно, что намерением было вернуть число с плавающей точкой. {.task_hint} ```cpp {.task_answer} double get_price_with_discount(double price, bool has_promocode) { if (has_promocode) { return price * 0.9; } return price; } ``` Статическая типизация на корню предотвращает целый класс ошибок, связанных с типами. Чем сложнее и обширнее кодовая база, тем очевиднее польза от статической типизации. У неё есть и ещё одно преимущество. Компилятор обладает знанием о типах всех сущностей в коде, а следовательно, у него развязаны руки для оптимизаций. Это делает программу более эффективной в плане производительности и экономии ресурсов. **Слабая (нестрогая) типизация** означает, что в C++ допустимо неявное приведение типов. При **неявном приведении** (implicit cast) компилятор, следуя правилам языка, выполняет преобразование значений одного типа в значения другого типа. Например, приводит целые числа к числам с плавающей точкой и наоборот. Это отличает C++ от [Rust,](https://senjun.ru/courses/rust/chapters/rust_chapter_0010/) в котором неявное приведение запрещено, и попытка сложить целое значение с дробным приводит к ошибке компиляции. {#block-implicit-cast} Неявное приведение типов обеспечивает гибкость в комбинировании данных и скорость разработки. С другой стороны, оно же — неиссякаемый источник ошибок. Пример неявного приведения типов: для преобразования числа с плавающей точкой в целое компилятор просто отбрасывает дробную часть: ```cpp {.example_for_playground .example_for_playground_005} int len_km = 6.8; std::println("{}", len_km); ``` ``` 6 ``` Предположите, что выведет этот код? Тип `bool` может принимать два значения: `true` либо `false`. Ему присваивается символ амперсанда. {.task_text} Вы можете воспользоваться подсказкой. Она доступна по кнопке со знаком вопроса. {.task_text} ```cpp {.example_for_playground .example_for_playground_003} bool x = '&'; std::println("{}", x); ``` ```consoleoutput {.task_source #cpp_chapter_0011_task_0020} ``` ASCII-код символа амперсанда — число 38. Срабатывает правило приведения целых к булевым значениям: 0 приводится к `false`, а все остальные числа — к `true`. {.task_hint} ```cpp {.task_answer} true ``` В C++ возможно и **явное приведение типов** (explicit cast). Есть специальная языковая конструкция и встроенные функции для указания, к какому типу требуется привести значение. Пример: ```cpp std::size_t stream_size = 65536; // беззнаковое целое int n = static_cast<int>(stream_size); // приводим к знаковому целому ``` ### ООП C++, как и любой популярный современный язык, позволяет писать код в разных стилях. Последние стандарты C++ особенно богаты на элементы функционального программирования. Но родным стилем C++ всегда был объектно-ориентированный. C++ зародился как надстройка над Си, добавлявшая всего одну возможность: классы. Язык так и назывался: «Си с классами». С тех пор C++ стал гораздо более мощным и продвинутым. Развивалась и поддержка ООП. В C++ она реализована через: - Классы с разграничением доступа к полям и методам. - Наследование, в том числе множественное. - Виртуальные функции. Это методы класса, которые переопределяются в классах-потомках так, что конкретная реализация метода подставляется во время исполнения, а не во время компиляции. ### Метапрограммирование Метапрограммирование (metaprogramming) — это написание кода, который порождает новый код. В той или иной степени оно реализовано во многих языках. Но именно C++ славится мощными средствами для метапрограммирования на этапе компиляции. C++ — язык, на котором можно _генерировать и выполнять_ код в процессе сборки программы, а не после её запуска. И это отличный пример абстракций с нулевым оверхедом. В C++ есть для этого три механизма: - **Шаблоны** (templates) предназначены для создания обобщённых алгоритмов без привязки к типам данных и константам. Разработчик пишет шаблонные классы и функции, а компилятор генерирует для них специализации. Введенные в C++20 **концепты** (concepts) делают шаблоны более удобными: они задают ограничения для параметров шаблонов. - **Вычисления на этапе компиляции** (compile-time evaluation). С помощью ключевых слов `constexpr`, `consteval` и `constinit` на этапе компиляции можно вызывать функции, выполнять циклы и условия. - **Макросы** (macros). Макроподстановки в коде осуществляются ещё до этапа компиляции, и отвечает за них препроцессор. Макросы достались C++ в наследство от Си. Они были полезны во времена, когда в C++ ещё не ввели шаблоны, вычисления на этапе компиляции и другие более современные инструменты. Практики Modern C++ рекомендуют избегать макросов. Код на макросах трудно отлаживать. А допустить в нем ошибку — наоборот очень легко. Зачатки метапрограммирования _во время исполнения_ (а не во время компиляции) в C++ тоже имеются. Например, можно проверить, установлено ли между классами отношение наследования, или получить типы аргументов функции. Это элементы **интроспекции** (introspection) — изучения свойств объектов в рантайме. Для более продвинутой интроспекции и кодогенерации в язык планируется ввести **рефлексию** (reflection) времени компиляции. С её помощью можно будет изменять свойства объектов на этапе сборки кода. ## Развитие языка За плечами C++ долгих 40 лет эволюции. Не удивительно, что код, написанный на старом и новом C++, порой разительно отличается. Взгляните. Hello World на текущем стандарте языка C++23: ```cpp {.example_for_playground} import std; int main() { std::println("Hello World"); } ``` Мы импортировали модуль стандартной библиотеки `std` и вызвали функцию `println()`. Обе эти возможности появились в C++23. Поэтому данный пример соберётся только [свежими версиями](https://en.cppreference.com/w/cpp/compiler_support#cpp23) компиляторов. А вот Hello World на C++17 — более старом и самом [распространённом](https://lp.jetbrains.com/the-state-of-cpp-2025/) стандарте: ```cpp {.example_for_playground} #include <iostream> int main() { std::cout << "Hello World" << std::endl; } ``` Здесь вместо импорта модуля `std` используется макрос для подключения хедера `iostream`, а вместо функции `println()` — стрим (stream) для печати в стандартный поток вывода `cout`. Как все это работает, мы рассмотрим в следующих главах. Оба примера показывают, что строки обрамляются двойными кавычкам: `"Hello World"`. А одинарные кавычки в С++ используются для отдельных символов: `'H'`. В этом курсе мы делаем упор на последние версии C++, но не забываем об экскурсах в историю и о ремарках, в каком стандарте появилась та или иная фича. Ведь индустрия переходит на новые версии языка с инерцией в годы. А началось все с того, что в конце 70-х Бьерн Страуструп (Bjarne Stroustrup) впечатлился классами языка Simula, но остался недоволен его производительностью. Язык Си, с другой стороны, отличался быстродействием, но не способствовал удобному объединению данных и методов их обработки. ![Бьерн Страуструп](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/main/illustrations/cpp/bjarne_stroustrup.jpg) {.illustration} Страуструп создал расширение Си и назвал его «Си с классами» (C with classes). Оно завоевало популярность, обросло функционалом и было переименовано в C++ (инкремент от Си, что намекает на преемственность). В 1998 году появился официальный стандарт C++98. С тех пор развитием языка занимается комитет по стандартизации, а новые версии C++ публикуются в виде стандартов. В наши дни новые стандарты выходят раз в 3 года, но их полноценная поддержка компиляторами появляется далеко не сразу. Публикация стандарта C++26 запланирована на март 2026 года, и некоторые его фичи [уже реализованы](https://en.cppreference.com/w/cpp/compiler_support/26.html) в компиляторах. ![Таймлайн развития C++](https://raw.githubusercontent.com/senjun-team/senjun-courses/refs/heads/main/illustrations/cpp/cpp_timeline.jpg) {.illustration} Начиная с версии C++11 можно говорить о зарождении **современного C++** (Modern C++). Это понятие включает в себя три аспекта: - Языковые средства. - Лучшие практики разработки. Они собраны в документе под названием [C++ Core Guidelines](https://isocpp.github.io/CppCoreGuidelines/CppCoreGuidelines) (CG). Мы будем ссылаться на рекомендации из CG на протяжении всего курса. - Актуальные версии компиляторов. Современный C++ позволяет создавать выразительный, масштабируемый и _безопасный_ код. В данном контексте под безопасностью понимается отсутствие ошибок управления памятью, способных открыть бреши для злоумышленников. К таким ошибкам относится переполнение буфера. Предположите, как будет называться следующий после C++26 стандарт языка? {.task_text} Формат ответа: `C++NN`. {.task_text} ```consoleoutput {.task_source #cpp_chapter_0011_task_0030} ``` Стандарты выходят раз в 3 года. {.task_hint} ```cpp {.task_answer} C++29 ``` ---------- ## Резюме - C++ предназначен для решения чрезвычайно широкого круга задач. - На C++ можно писать как низкоуровневый, так и высокоуровневый код. - C++ заточен под производительность и экономию ресурсов. - C++ — компилируемый язык со слабой статической типизацией. - В C++ есть абстракции с нулевым оверхедом и практикуется подход «не плати за то, что не используешь». - В C++ развито метапрограммирование на этапе компиляции. - Современный C++ (Modern C++) — это комбинация современных языковых средств, актуальных версий компиляторов и лучших практик по написанию выразительного и безопасного кода.
Отправка...
Наша группа в telegram. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!