Course program← Программирование на языке C++
1Введение в язык C++
2Как выполняются программы на C++
3Структуры и классы
4Объектно-ориентированное программирование
5Дополнительные возможности C++
6Шаблоны
7Методические материалы

Откуда взялся C++ и зачем он такой

This lesson is not translated yet – the Russian original is below.

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++ выбирают, когда цена абстракции имеет значение. Если не имеет – берут язык попроще, и это разумно.