C постоянно преобразует значения между типами. Часть таких преобразований вы пишете сами приведением; большинство компилятор выполняет молча по правилам, которые выбирали не вы. Понимание, где что, — это разница между «почему у меня среднее всегда 3?» и кодом, который делает то, что написано.
Неявное преобразование
Всякий раз, когда значение одного типа попадает в контекст, ожидающий другой, C его преобразует:
Преобразования, которые не могут потерять информацию (int в double, char в int, short в long), — расширяющие, и они всегда безопасны. Преобразования в обратную сторону — сужающие, и они могут потерять данные: 3.9 выше превратилось в 3 без всякого предупреждения, если только вы не попросили его через -Wconversion.
Обычные арифметические преобразования
Когда у бинарного оператора операнды разных типов, C приводит их к общему типу до выполнения работы. Лестница снизу вверх:
- Всё, что меньше
int(char,short,_Bool), продвигается доint. Это целочисленное продвижение, и оно происходит первым, всегда. - Если одна из сторон
long double, другая становитсяlong double. - Иначе, если одна
double, другая становитсяdouble. - Иначе, если одна
float, другая становитсяfloat. - Иначе среди целочисленных типов побеждает тот, у кого выше ранг, а при равенстве рангов побеждает беззнаковый.
Именно последнее правило порождает настоящие баги. Остальные интуитивны.
Целочисленное продвижение — причина, по которой арифметика с char не переполняется так, как можно было бы ожидать, и по которой сохранение результата обратно в char — переполняется.
Явные приведения
Приведение — это целевой тип в скобках:
(double)x
(int)3.9
(char)65
(unsigned int)n
Оно относится к выражению сразу за ним и связывает очень сильно — сильнее, чем *, / или +.
Первая строка делит как целые (получая 3), а затем преобразует 3 в 3.0 — слишком поздно. Вторая преобразует total к плавающей точности до деления, поэтому оператор / видит double и int, продвигает int и выполняет деление с плавающей точкой.
Приведения одного операнда достаточно. Об остальном позаботятся обычные арифметические преобразования.
Исправление целочисленного деления
Это самая частая причина написать приведение в C:
Строка с процентами поучительна: passed / n — это 3 / 5, то есть 0 в целых, а 0 * 100 — это 0. Умножение до деления (100 * passed / n) исправляет дело даже без приведения, потому что 300 / 5 вычисляется точно, — но это работает, только когда числа идут навстречу. Надёжное решение — приведение.
Усечение, а не округление
Приведение значения с плавающей точкой к целому отбрасывает дробную часть. Оно усекает в сторону нуля, а не округляет:
Если собираете это на своей машине, помните, что math.h требует -lm при компоновке в Linux.
Ещё одна опасность: преобразование значения с плавающей точкой, слишком большого для целочисленного типа, — это неопределённое поведение, а не заворачивание. (int)1e20 может выдать что угодно. Проверяйте диапазон перед приведением, когда значение не под вашим контролем.
char и int
char в C является маленьким целым, хранящим код символа. Преобразование между ними — повседневная работа:
digit - '0' — стандартная идиома превращения символа цифры в её значение, и она работает, потому что десять символов цифр гарантированно идут подряд. Для букв предпочитайте toupper() и tolower() из ctype.h арифметике с + 32: это смещение — факт про ASCII, а не гарантия C.
Смежная ловушка: функции из ctype.h вроде isdigit и toupper принимают int, который должен быть либо EOF, либо представим как unsigned char. Передача обычного char с отрицательным значением (что возможно, поскольку обычный char может быть знаковым) не определена. Приводите тип: isdigit((unsigned char)c).
Ловушка знакового и беззнакового
Пункт 5 лестницы преобразований — при равенстве рангов побеждает беззнаковый — порождает самое неожиданное сравнение в C:
-1 преобразуется в unsigned int, из-за чего его битовый набор трактуется как 4 294 967 295. Это больше 1, поэтому сравнение ложно.
То же преобразование заставляет циклы работать вечно:
/* БАГ: i беззнаковая, поэтому i >= 0 всегда истинно. Когда i равна 0, i-- заворачивается. */
for (size_t i = n - 1; i >= 0; i--) { ... }
И оно же ломает проверки длины:
/* БАГ: strlen возвращает size_t (беззнаковый). Если строка короче
пяти символов, len - 5 заворачивается в огромное число и проверка проходит. */
if (strlen(s) - 5 > 0) { ... }
Перепишите как if (strlen(s) > 5) — и вычитание вообще не произойдёт.
Защита: держите счётчики и индексы в одной знаковости на всём пути, собирайте с -Wsign-compare (входит в -Wextra), а когда смешивать всё-таки приходится, приводите явно, предварительно убедившись, что значение не может быть отрицательным.
Приведение указателей
Приведения также преобразуют типы указателей, и здесь риск реален, потому что они меняют то, как интерпретируется память, а не сами байты.
На машине с прямым порядком байтов (little-endian) это напечатает 01 00 00 00. Изучение представления объекта через unsigned char * — одно из немногих приведений указателей, которые стандарт явно одобряет.
Большинство остальных приведений указателей не одобрены. Чтение int через float * нарушает правило строгого алиасинга и является неопределённым поведением, хотя и компилируется; чтобы переинтерпретировать байты, используйте memcpy.
Два соглашения, о которых стоит знать. void * преобразуется в любой тип указателя на объект и обратно без приведения в C, и поэтому результат malloc приводить не нужно:
int *arr = malloc(n * sizeof *arr); /* правильный C */
int *arr = (int *)malloc(n * sizeof *arr); /* лишнее; прячет отсутствующий заголовок */
В C++ приведение обязательно, отчего его и встречают в таком количестве кода. В C оно может скрыть ошибку с забытым <stdlib.h>.
А printf("%p", ...) ожидает void *, поэтому указателям там приведение действительно нужно: printf("%p", (void *)p).
Когда приведение — неверный ответ
Приведение затыкает компилятор. Иногда компилятор был прав.
long big = 5000000000L;
int small = (int)big; /* приведение прячет настоящую потерю данных */
Если значение действительно помещается, приведение документирует, что вы это проверили. Если может не поместиться, приведение превратило предупреждение в тихий неверный ответ. Прежде чем его писать, спросите себя, не заключается ли решение в смене типа переменной: double вместо int, size_t вместо int, long long вместо long. Приведение — правильный инструмент главным образом тогда, когда два корректных типа должны встретиться ради одной операции, как в (double)sum / count.
Часто задаваемые вопросы
Как выполнить приведение типа в C?
Поставьте целевой тип в скобках перед значением: (double)x, (int)3.9, (char)65. Приведение относится к выражению сразу за ним, поэтому (double)a / b сначала преобразует a, а затем делит, тогда как (double)(a / b) делит как целые и преобразует результат.
Как преобразовать int в float в C?
Присваивание делает это неявно: double d = 5; сохранит 5.0. Внутри выражения часто нужно явное приведение: (double)total / count заставляет выполнить деление с плавающей точкой вместо целочисленного.
Что происходит при приведении float к int в C?
Дробная часть отбрасывается — усекается в сторону нуля, а не округляется. (int)3.9 даёт 3, а (int)-3.9 даёт -3. Чтобы округлить, прибавляйте 0.5 перед приведением для положительных чисел либо используйте round(), floor() или ceil() из math.h.
Почему сравнение знакового и беззнакового int даёт неверный ответ?
Обычные арифметические преобразования в C переводят знаковое значение в беззнаковое, поэтому -1 < 1u ложно: -1 превращается в огромное положительное число. Держите счётчики и размеры в одной знаковости либо приводите явно, убедившись, что значение не может быть отрицательным.