Menu

Приведение типов в C: неявные преобразования и явные приведения

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

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

C постоянно преобразует значения между типами. Часть таких преобразований вы пишете сами приведением; большинство компилятор выполняет молча по правилам, которые выбирали не вы. Понимание, где что, — это разница между «почему у меня среднее всегда 3?» и кодом, который делает то, что написано.

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

Всякий раз, когда значение одного типа попадает в контекст, ожидающий другой, C его преобразует:

Преобразования, которые не могут потерять информацию (int в double, char в int, short в long), — расширяющие, и они всегда безопасны. Преобразования в обратную сторону — сужающие, и они могут потерять данные: 3.9 выше превратилось в 3 без всякого предупреждения, если только вы не попросили его через -Wconversion.

Обычные арифметические преобразования

Когда у бинарного оператора операнды разных типов, C приводит их к общему типу до выполнения работы. Лестница снизу вверх:

  1. Всё, что меньше int (char, short, _Bool), продвигается до int. Это целочисленное продвижение, и оно происходит первым, всегда.
  2. Если одна из сторон long double, другая становится long double.
  3. Иначе, если одна double, другая становится double.
  4. Иначе, если одна float, другая становится float.
  5. Иначе среди целочисленных типов побеждает тот, у кого выше ранг, а при равенстве рангов побеждает беззнаковый.

Именно последнее правило порождает настоящие баги. Остальные интуитивны.

Целочисленное продвижение — причина, по которой арифметика с 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 превращается в огромное положительное число. Держите счётчики и размеры в одной знаковости либо приводите явно, убедившись, что значение не может быть отрицательным.

Coddy programming languages illustration

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

НАЧАТЬ