Menu

Приведение типов в C++: static_cast, неявные преобразования и касты

Как работает приведение типов в C++: неявные преобразования, ловушка целочисленного деления и четыре именованных каста (static_cast, const_cast, reinterpret_cast, dynamic_cast) — с подводными камнями, которые приводят к тихой потере данных.

На этой странице есть исполняемые редакторы: меняйте, запускайте и сразу видите результат.

Что такое приведение типов

Приведение типов означает преобразование значения из одного типа в другой — превращение double в int, char в его числовой код или указателя на базовый класс в указатель на производный. C++ выполняет некоторые из этих преобразований за вас автоматически, но именно те, что он делает молча, и есть место, где прячутся ошибки.

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

Неявные преобразования

Когда вы смешиваете числовые типы в выражении, C++ повышает «меньший» тип до «большего», чтобы обе стороны совпадали. Обычно это делает именно то, что вам нужно.

Беда начинается, когда преобразование идёт в обратную сторону — от более широкого типа к более узкому. Это сужающее преобразование, и оно может молча потерять данные.

При приведении float к int происходит усечение в сторону нуля — не округление, поэтому 3.99 становится 3. А попытка втиснуть 300 в char приводит к переполнению. Многие компиляторы здесь предупреждают; некоторые — нет. Когда вы действительно намерены сузить, скажите об этом явно с помощью каста, чтобы следующий читатель знал, что это сделано намеренно.

Исправление ловушки целочисленного деления

Самая частая причина для каста — деление. Когда оба операнда целые, / выполняет целочисленное деление и отбрасывает остаток.

Исправление — static_cast<double> на один операнд до деления. Частая ошибка — static_cast<double>(got / total): это слишком поздно, потому что к моменту выполнения каста got / total уже равно 0, так что вы получаете 0.0. Приводите операнд, а не результат.

static_cast: ваш каст по умолчанию

C++ даёт вам четыре именованных каста. Тот, который вы будете использовать в 95 % случаев, — это static_cast<T>(value), выполняющий чётко определённые преобразования между родственными типами: числовые преобразования, enum в int, void* обратно в типизированный указатель и переход вверх/вниз по иерархии классов, когда тип вам уже известен.

Предпочитайте static_cast старому касту в стиле C (int)balance. Каст в стиле C попробует любое преобразование, лишь бы код скомпилировался, — включая опасные ниже, — поэтому он может молча снять const или переинтерпретировать сырые байты. static_cast разрешает только те преобразования, которые компилятор действительно может обосновать, а многословный static_cast<...> тривиально искать при ревью кода.

// Избегать - каст в стиле C, без страховки:
int dollars = (int) balance;

// Предпочесть - явный, проверяемый, легко находимый:
int dollars = static_cast<int>(balance);

Остальные три каста (используйте умеренно)

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

const_cast убирает (или добавляет) const. Его единственное законное применение — вызов API в стиле C, в котором забыли пометить параметр как const. Изменять через const_cast объект, который изначально был объявлен как const, — это неопределённое поведение.

void legacyApi(char* msg);   // старый API, не принимает const

const char* text = "hello";
legacyApi(const_cast<char*>(text));   // допустимо, только если legacyApi не пишет в него

reinterpret_cast переинтерпретирует сырой битовый узор — например, указатель как целочисленный адрес. Он не выполняет никакого преобразования и крайне небезопасен; почти всегда это признак того, что стоит пересмотреть проектное решение.

dynamic_cast безопасно преобразует указатель или ссылку на базовый класс в производный тип во время выполнения, используя фактический тип объекта. Он требует полиморфной базы (класса хотя бы с одной виртуальной функцией) и возвращает nullptr, если приведение неприменимо.

Если бы a указывал на какой-то другой Animal, dynamic_cast<Dog*> вернул бы nullptr и выполнилась бы ветка else — именно поэтому он безопаснее, чем слепое использование static_cast для спуска вниз по иерархии.

Распространённые ошибки, которых стоит избегать

  • Приведение результата вместо операнда. static_cast<double>(a / b) сначала отбрасывает вашу дробную часть. Приводите a или b.
  • Предположение, что float к int округляется. Он усекается: static_cast<int>(2.99) равно 2. Для округления используйте std::round, std::lround и т. д.
  • Использование каста в стиле C. Он скрывает, какое преобразование происходит. Используйте static_cast, и вы получите ошибку компиляции, когда преобразование небезопасно, вместо тихого сюрприза.
  • Сужение в слишком маленький тип. Приведение 300 к char или огромного long к int переполняется по кольцу или выходит за пределы. Выбирайте целевой тип, достаточно широкий для диапазона.

Далее: If-Else

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

Часто задаваемые вопросы

В чём разница между static_cast и приведением в стиле C в C++?

Приведение в стиле C вроде (int)x поочерёдно пробует каждое преобразование — оно может незаметно превратиться в опасный reinterpret_cast или снять const. static_cast<int>(x) выполняет только родственные преобразования, которые компилятор может проверить, поэтому компилятор отвергает бессмыслицу. В современном C++ всегда предпочитайте static_cast приведениям в стиле C; это безопаснее и гораздо проще искать через grep.

Как преобразовать int в double в C++?

Используйте static_cast<double>(x). Важнее всего это при делении: 5 / 2 — это целочисленное деление, дающее 2, а static_cast<double>(5) / 2 даёт 2.5. Приводите один из операндов до того, как произойдёт деление: приводить результат, static_cast<double>(5 / 2), уже поздно — всё равно получится 2.0.

Почему приведение большого значения к меньшему типу даёт неправильное число в C++?

Преобразование к типу, который не способен вместить значение, — это сужающее преобразование. При приведении float к int дробная часть отбрасывается (static_cast<int>(3.99) равно 3), а целое число вне диапазона либо переполняется по кольцу (беззнаковое), либо ведёт себя определённым реализацией образом (знаковое). Компилятор обычно вас не останавливает, поэтому приводите осознанно и убедитесь, что целевой тип достаточно широк.

Coddy programming languages illustration

Учитесь программировать с Coddy

НАЧАТЬ