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

Вспомните механический счётчик пробега в старой машине: ряд колёсиков с цифрами и ни одного знака минус. Если открутить его назад от 00000, он покажет 99999, потому что меньше нуля ему показывать нечего. Тип unsigned устроен так же: отрицательных чисел в нём нет, и шаг вниз от нуля уводит его к самому большому значению.
Сумма 3 и -5 равна -2, но эта программа печатает совсем другое число:
#include <iostream>
int main() {
int change = -5;
unsigned stock = 3u;
std::cout << stock + change << '\n'; // 4294967294
}Перед сложением компилятор привёл оба операнда к одному типу, и этим типом оказался unsigned. Число -5 в нём превратилось в огромное, как счётчик, открученный на пять шагов назад от нуля. Поэтому и сумма получилась огромной.
Неявные преобразования
Неявное преобразование — смена типа, которую компилятор делает сам, без указания в коде. Перед арифметикой и сравнением он приводит операнды к общему типу по двум правилам:
bool,charиshortсначала превращаются вint:true + true— этоintсо значением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 x = -5;
бит 31: в int весит -2147483648, в unsigned +2147483648
Старший бит 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.
#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:
#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. Такие правила пишет автор типа, а компилятор применяет их так же неявно.
Частая ошибка
Приведение применили к результату деления, а не к операнду:
#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 и подпишись на канал — откроются все уроки.