Перейти к содержимому
Теория · Приведения и псевдонимы · урок 1 из 7

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

Чему вы научитесь

Чему вы научитесь:

  • предсказывать, к какому общему типу компилятор приведёт операнды;
  • объяснять, почему отрицательный int превращается в огромный unsigned;
  • выбирать именованное приведение под задачу: static_cast, const_cast или reinterpret_cast;
  • давать типу второе имя через using.
Счётчик пробега unsigned открутили назад на -5, и его колёсики показывают 4294967294

Вспомните механический счётчик пробега в старой машине: ряд колёсиков с цифрами и ни одного знака минус. Если открутить его назад от 00000, он покажет 99999, потому что меньше нуля ему показывать нечего. Тип unsigned устроен так же: отрицательных чисел в нём нет, и шаг вниз от нуля уводит его к самому большому значению.

Сумма 3 и -5 равна -2, но эта программа печатает совсем другое число:

C++
#include <iostream>

int main() {
    int change = -5;
    unsigned stock = 3u;
    std::cout << stock + change << '\n';  // 4294967294
}

Перед сложением компилятор привёл оба операнда к одному типу, и этим типом оказался unsigned. Число -5 в нём превратилось в огромное, как счётчик, открученный на пять шагов назад от нуля. Поэтому и сумма получилась огромной.

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

Неявное преобразование — смена типа, которую компилятор делает сам, без указания в коде. Перед арифметикой и сравнением он приводит операнды к общему типу по двум правилам:

  1. bool, char и short сначала превращаются в int: true + true — это int со значением 2.
  2. Если типы всё ещё разные, оба операнда приводятся к общему типу.

Общий тип для частых пар:

  • int и double дают double;
  • int и long long дают long long;
  • int и unsigned дают unsigned.

Вместе эти правила называют обычными арифметическими преобразованиями.

Отрицательный int в unsigned

Отрицательное значение int при переходе в unsigned уходит по кругу, как счётчик пробега, и результат определён стандартом. -1 становится наибольшим значением unsigned, а -5 — числом на четыре меньше.

Наибольшее значение 4-байтового unsigned — 4294967295, поэтому -5 превращается в 4294967291, а 3 + 4294967291 даёт 4294967294 из первого примера.

Меняйте x кнопками и следите за старшим битом: пока он равен 1, int читает число как отрицательное, а unsigned — как огромное.

ВживуюОдни биты — два числаint -5 · unsigned 4294967291
int x = -5;

бит 31: в int весит -2147483648, в unsigned +2147483648

int-5
unsigned4294967291

Старший бит 1. int считает его за -2147483648, а unsigned — за +2147483648, поэтому те же биты в unsigned — 4294967296 + (-5) = 4294967291.

Следите за старшим битом

Аналогия неточна в одном: колёсики счётчика десятичные, а unsigned хранит биты, поэтому его максимум не 99999, а 4294967295.

Так же работает сравнение: < с операндами int и unsigned сравнивает уже преобразованные значения. Этот подвох часто разбирают на собеседованиях.

Сужающие преобразования

У отрицательного int в unsigned результат хотя бы определён. Преобразование в более узкий тип опаснее:

  • значение double, записанное в int, теряет дробную часть;
  • значение long long может не поместиться в int.

Такие преобразования называют сужающими. Запись через = с ними компилируется без ошибки, а фигурные скобки их запрещают, как мы видели в теме о переменных.

Приведение static_cast

Если нужного преобразования компилятор сам не сделает, вы пишете его в коде явно. Такая запись называется приведением: static_cast<double>(sum) даёт значение sum в виде double, а сама переменная остаётся int.

C++
#include <iostream>

int main() {
    int sum = 7;
    int count = 2;
    std::cout << static_cast<double>(sum) / count << '\n';  // 3.5
}

Делимое стало double, поэтому count тоже превращается в double, и деление сохраняет дробную часть. В обратную сторону static_cast<int>(2.9) даёт 2: дробная часть отбрасывается.

Тот же приём защищает от переполнения. Константы INT_MIN и INT_MAX из заголовка <climits> хранят наименьшее и наибольшее значения int:

C++
#include <climits>
#include <iostream>

int main() {
    int a = INT_MAX;
    int b = 1;
    long long sum = static_cast<long long>(a) + b;
    std::cout << sum << '\n';  // 2147483648
}

В типе int сумма a + b переполнилась бы, а это неопределённое поведение. Когда a приведена к long long, в этот же тип переходит и b, и числа складываются уже в широком типе.

В языке C тип меняют записью (double)sum. В C++ она тоже работает, но умеет и снять const, и переинтерпретировать указатель. По коду не видно, что из этого имелось в виду. У именованного приведения вроде static_cast одна задача, и по его имени вы найдёте в коде каждое место, где оно стоит.

Свои преобразования задают и типы стандартной библиотеки: строковый литерал неявно превращается в std::string, а std::string — в std::string_view. Такие правила пишет автор типа, а компилятор применяет их так же неявно.

Частая ошибка

Приведение применили к результату деления, а не к операнду:

C++
#include <iostream>

int main() {
    int sum = 7;
    int count = 2;
    double avg = static_cast<double>(sum / count);  // ✗
    std::cout << avg << '\n';  // 3
}

Программа печатает 3. В скобках стоит целочисленное деление 7 / 2, и в double превращается уже его результат 3. Приводите операнд: static_cast<double>(sum) / count даёт 3.5.

Главное

  • Перед арифметикой и сравнением компилятор приводит операнды к общему типу; для int и unsigned это unsigned.
  • Отрицательный int в unsigned уходит по кругу: -1 становится 4294967295.
  • static_cast<T>(x) меняет тип явно; приводите операнд, а не результат деления.
  • У именованного приведения одна задача, и его видно в коде, в отличие от (double)sum.

Подпишись, чтобы пройти курс

Курс бесплатный. Войди через Telegram и подпишись на канал — откроются все уроки.

Войти другим способом