C++ появился не как новый язык, а как попытка починить старый, ничего в нём не сломав. Это объясняет почти все его странности – и заодно то, почему на нём до сих пор пишут всё, где важна скорость.
Как это начиналось
В 1979 году Бьёрн Страуструп работал в Bell Labs над моделированием распределённых систем. Ему нужны были две вещи сразу: классы, как в языке Simula, чтобы описывать модель, и скорость C, чтобы она успевала считаться. Существующие языки давали либо одно, либо другое.
Решение оказалось прагматичным: взять C и добавить классы поверх. Первая версия так и
называлась – «C with Classes», а работала через препроцессор, который переводил новый синтаксис
в обычный C. В 1983 году язык получил нынешнее имя: ++ – это оператор инкремента, «C, увеличенный
на единицу».
Отсюда родовая черта языка: почти любая корректная программа на C остаётся корректной программой на C++. Совместимость была не подарком, а условием выживания – переписывать существующий код никто бы не стал.
Стандарты
Первые пятнадцать лет язык жил без стандарта: эталоном была книга Страуструпа, а компиляторы понимали её каждый по-своему. Стандартизацию довели до конца только в 1998-м.
| Стандарт | Год | Что принёс |
|---|---|---|
| C++98 | 1998 | Первый ISO-стандарт: шаблоны, STL, исключения, пространства имён |
| C++03 | 2003 | Правка ошибок предыдущего, новых возможностей почти нет |
| C++11 | 2011 | Перелом: auto, лямбды, умные указатели, move-семантика, range-for |
| C++14 / 17 | 2014 / 2017 | Доводка: вывод типов, структурные привязки, std::optional, if constexpr |
| C++20 | 2020 | Концепты, модули, корутины, <ranges> |
| C++23 | 2023 | std::expected, std::print, доводка ranges |
Граница проходит по C++11. Код до и после отличается настолько, что говорят о разных языках: «классический» и «современный» C++. В этом курсе мы пишем на современном.
Много обучающих материалов в сети написано до 2011 года и учит приёмам, которые сегодня считаются ошибкой: ручные new и delete вместо умных указателей, макросы вместо констант. Понять их полезно – читать чужой код придётся всякий. Писать так не нужно.
Что за язык получился
Мультипарадигмальный. На C++ можно писать процедурно – функциями, как на C. Можно объектно-ориентированно – классами и наследованием. Можно обобщённо – шаблонами, где тип подставляется при компиляции. Можно функционально – лямбдами и алгоритмами. Ни одна из парадигм не навязана, и в одном файле спокойно уживаются все четыре.
Это одновременно сила и главная сложность: язык не диктует, как решать задачу, поэтому решений всегда несколько и выбирать приходится самому.
Компилируемый в машинный код. Между программой и процессором нет никакой прослойки. Из исходника получается файл с командами конкретной архитектуры, и запускается он напрямую.
// C++: компилятор выдаёт машинный код целевой платформы
int add(int a, int b) { return a + b; }
// C#: компилятор выдаёт IL, машинный код появляется при запуске
int Add(int a, int b) => a + b;
# Python: машинного кода нет вообще, инструкции исполняет интерпретатор
def add(a, b): return a + b
Плата за это – исполняемый файл нельзя перенести на другую систему. Переносим исходный код и собираем на месте.
Статически типизированный. Тип каждой переменной известен на этапе компиляции и после этого не меняется. Компилятор ловит несовпадения до запуска – но взамен требует называть типы.
Со сборкой мусора? Нет. И это главное решение, определяющее всё остальное.
Кто освобождает память
В C#, Java, Go, Python за памятью следит сборщик мусора: он находит объекты, до которых уже не добраться, и освобождает их. Вы не думаете об этом вообще.
В C++ сборщика нет. Каждый выделенный кусок памяти кто-то обязан вернуть, и этот кто-то – программа. Отсюда две ошибки, которых в языках со сборщиком просто не существует: утечка (память выделили и не вернули) и висячий указатель (память вернули, а обращаются к ней дальше).
Звучит как чистый недостаток – но в обмен вы получаете предсказуемость. Сборщик мусора включается когда сочтёт нужным, и программа на это время замирает. В торговой системе или в тормозах автомобиля пауза «примерно на пару миллисекунд, когда-нибудь» – неприемлема.
У языка есть ответ на это, и хороший: RAII – привязка времени жизни ресурса ко времени
жизни объекта. Освобождением занимается не программист, а деструктор, и вызывается он в момент,
который известен точно. Разберём в третьем модуле, а пока достаточно знать: писать delete
руками в современном C++ почти не приходится.
Zero-overhead: за что вы платите
Принцип, сформулированный Страуструпом, звучит так: вы не платите за то, чем не пользуетесь, а то, чем пользуетесь, нельзя написать вручную лучше.
Отсюда странность, которая бросается в глаза при переходе с других языков: почему методы надо
явно помечать словом virtual? В Java и C# все методы виртуальные по умолчанию.
class Shape {
public:
double Area() const; // обычный вызов, адрес известен при компиляции
virtual double Perimeter() const; // вызов через таблицу, адрес ищется при выполнении
};
Виртуальный вызов стоит одного дополнительного обращения к памяти. Немного – но если вы не собирались переопределять метод, платить за это незачем. Язык спрашивает явно, а не решает за вас.
Так устроено всё: копирование объекта, проверка границ массива, обработка исключений. Каждая возможность включается там, где нужна, и не стоит ничего там, где не нужна.
Обратная сторона: язык не мешает выстрелить себе в ногу. Обращение к сто первому элементу массива из ста не бросит исключение, как в C#, – оно молча прочитает чужую память, и программа продолжит работать с мусором. Ошибка проявится позже и совсем в другом месте.
Где на нём пишут сегодня
- игровые движки – Unreal Engine, физика, рендеринг: там, где счёт идёт на миллисекунды кадра;
- браузеры и операционные системы – Chrome, Windows, ядра баз данных;
- встраиваемые системы – автомобильная электроника, промышленные контроллеры, где сборщика мусора не может быть по определению;
- высокочастотный трейдинг и научные расчёты – всё, где предсказуемая задержка важнее удобства разработки;
- машинное обучение – под питоновским фасадом TensorFlow и PyTorch считает C++.
Общее у этого списка одно: C++ выбирают, когда цена абстракции имеет значение. Если не имеет – берут язык попроще, и это разумно.