# Глава 1.1. Что такое C++
C++ — это компилируемый, статически типизированный язык с прицелом на _эффективность._ На C++ можно писать драйверы, заточенные под конкретное устройство, а можно создавать высокоуровневую бизнес-логику кроссплатформенного проекта. C++ — универсальный язык, подходящий для решения практически любой задачи. При этом он позволяет выжать из железа максимум.
Пока вы читаете эту главу, C++ код исполняется в дефибрилляторах, AAA-играх, СУБД, текстовых процессорах, поисковиках. И даже [в марсоходе](https://www.youtube.com/watch?v=3SdSKZFoUa8) Curiosity:
 {.illustration}
За эффективность и универсальность C++ приходится платить:
- высоким порогом входа в язык,
- скоростью разработки,
- количеством деталей, о которых задумывается разработчик, чтобы писать качественный код.
## Философия С++
За свою 40-летнюю историю C++ стал одним из самых распространённых языков в мире. Секрет популярности кроется в философии C++. Она ставит во главу угла несколько принципов.
### Свобода выбора
Язык не навязывает «единственно верного» пути.
Вы можете вручную управлять ресурсами и контролировать каждое выделение памяти. А можете использовать удобные и высокоуровневые средства стандартной библиотеки.
Если на проект хорошо ложится обработка ошибок через исключения — пользуйтесь ими! Хотите работать со старыми-добрыми кодами ошибок? Пожалуйста. Понимаете, что такое алгебраические типы данных? Найдется и такое. Если вы пришли в мир C++ из Go, то почувствуйте разницу.
C++ предоставляет широкий арсенал возможностей. От разработчика же требуется наличие здравого смысла и некоего багажа знаний. Конечно, у такого богатства есть и обратная сторона: с годами C++ окончательно укоренился в роли самого сложного из мейнстримных языков.
С++ часто сравнивают со швейцарским ножом. _Очень многофункциональным_ швейцарским ножом.
 {.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, но остался недоволен его производительностью. Язык Си, с другой стороны, отличался быстродействием, но не способствовал удобному объединению данных и методов их обработки.
 {.illustration}
Страуструп создал расширение Си и назвал его «Си с классами» (C with classes). Оно завоевало популярность, обросло функционалом и было переименовано в C++ (инкремент от Си, что намекает на преемственность).
В 1998 году появился официальный стандарт C++98. С тех пор развитием языка занимается комитет по стандартизации, а новые версии C++ публикуются в виде стандартов.
В наши дни новые стандарты выходят раз в 3 года, но их полноценная поддержка компиляторами появляется далеко не сразу. Публикация стандарта C++26 запланирована на март 2026 года, и некоторые его фичи [уже реализованы](https://en.cppreference.com/w/cpp/compiler_support/26.html) в компиляторах.
 {.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. Здесь можно задавать вопросы и общаться.
Задонатить. Если вам нравится курс, вы можете поддержать развитие площадки!