Ответ 1
C был разработан для неявного и бесшумного изменения целочисленных типов операндов, используемых в выражениях. Существует несколько случаев, когда язык заставляет компилятор либо изменять операнды на больший тип, либо изменять их подпись.
Основанием для этого является предотвращение случайных переполнений во время арифметики, а также возможность сосуществования операндов с разной подписью в одном и том же выражении.
К сожалению, правила неявного продвижения типов приносят гораздо больше вреда, чем пользы, вплоть до того, что они могут быть одним из самых больших недостатков языка Си. Эти правила часто даже не известны среднему программисту C и поэтому вызывают всевозможные очень тонкие ошибки.
Обычно вы видите сценарии, в которых программист говорит "просто приведите к типу x, и это сработает" - но они не знают почему. Или такие ошибки проявляют себя как редкое, прерывистое явление, проникающее из, казалось бы, простого и понятного кода. Неявное продвижение особенно проблематично в коде, выполняющем битовые манипуляции, так как большинство побитовых операторов в C имеют плохо определенное поведение при получении подписанного операнда.
Целочисленные типы и рейтинг конверсии
Целочисленные типы в C: char
, short
, int
, long
, long long
и enum
.
_Bool
/bool
также рассматривается как целочисленный тип, когда речь идет о продвижении по типу.
Все целые числа имеют определенный рейтинг конверсии. C11 6.3.1.1, особое внимание уделено наиболее важным частям:
Каждый целочисленный тип имеет целочисленный рейтинг преобразования, определенный следующим образом:
- Никакие два целых типа со знаком не должны иметь одинаковый ранг, даже если они имеют одинаковое представление.
- Ранг целочисленного типа со знаком должен быть больше ранга целочисленного типа со знаком с меньшей точностью.
- Рангlong long int
должен быть больше, чем рангlong int
, который должен быть больше, чем рангint
, который должен быть больше, чем рангshort int
, который должен быть больше, чем рангsigned char
.
- Ранг любого целого типа без знака должен равняться рангу соответствующего целого типа со знаком, если таковой имеется.
- Ранг любого стандартного целочисленного типа должен быть больше, чем ранг любого расширенного целочисленного типа с такой же шириной.
- Ранг чар равняется званию подписанного и без знака.
- Ранг _Bool должен быть меньше, чем ранг всех других стандартных целочисленных типов.
- ранг любого перечислимого типа должен равняться рангу совместимого целочисленного типа (см. 6.7.2.2).
Здесь также сортируются типы из stdint.h
с тем же рангом, что и любому типу, которому они соответствуют в данной системе. Например, int32_t
имеет тот же ранг, что и int
в 32-битной системе.
Кроме того, C11 6.3.1.1 определяет, какие типы рассматриваются как целочисленные типы (не формальный термин):
Следующее может использоваться в выражении везде, где
int
илиunsigned int
могут использоваться:- Объект или выражение с целочисленным типом (кроме
int
илиunsigned int
), чей ранг целочисленного преобразования меньше или равен рангуint
иunsigned int
.
На практике этот несколько загадочный текст означает, что _Bool
, char
и short
(а также int8_t
, uint8_t
и т.д.) Являются "типами малых целых чисел". Они рассматриваются особым образом и подлежат скрытому продвижению, как описано ниже.
Целочисленные акции
Всякий раз, когда в выражении используется маленький целочисленный тип, он неявно преобразуется в int
, который всегда подписывается. Это называется целочисленной рекламой или правилом целочисленной рекламы.
Формально правило гласит (C11 6.3.1.1):
Если
int
может представлять все значения исходного типа (как ограничено шириной для битового поля), значение преобразуется вint
; в противном случае он преобразуется вunsigned int
. Они называются целочисленными акциями.
Это означает, что все маленькие целочисленные типы, независимо от подписи, неявно преобразуются в (подпись) int
при использовании в большинстве выражений.
Этот текст часто неправильно понимают как: "все малые целочисленные типы со знаком преобразуются в целое число со знаком, а все малые целочисленные типы без знака преобразуются в целое число без знака". Это неверно Часть без знака здесь означает только то, что если у нас есть, например, операнд unsigned short
, а размер int
имеет тот же размер, что и short
в данной системе, то операнд unsigned short
преобразуется в unsigned int
. Как, впрочем, ничего особенного на самом деле не происходит. Но в случае, если short
является меньшим типом, чем int
, он всегда преобразуется в (подписанный) int
, независимо от того, был ли короткий подписан или не подписан!
Суровая реальность, вызванная целочисленными продвижениями, означает, что почти невозможно выполнить операцию на языке C для небольших типов, таких как char
или short
. Операции всегда выполняются на int
или более крупных типах.
Это может звучать как глупость, но, к счастью, компилятору разрешено оптимизировать код. Например, выражение, содержащее два операнда unsigned char
, получит операнды, повышенные до int
, а операция будет выполнена как int
. Но компилятору разрешено оптимизировать выражение, чтобы оно фактически выполнялось как 8-битная операция, как и следовало ожидать. Однако здесь возникает проблема: компилятору не разрешено оптимизировать неявное изменение подписи, вызванное целочисленным продвижением. Потому что компилятор не может определить, действительно ли программист намеренно полагается на неявное продвижение или он непреднамеренный.
Вот почему пример 1 в вопросе терпит неудачу. Оба беззнаковых операнда переводятся в тип int
, операция выполняется в типе int
, а результат x - y
имеет тип int
. Это означает, что мы получаем -1
вместо 255
, что можно было ожидать. Компилятор может генерировать машинный код, который выполняет код с 8-битными инструкциями вместо int
, но он может не оптимизировать изменение подписи. Это означает, что мы получаем отрицательный результат, который, в свою очередь, приводит к странному числу, когда вызывается printf("%u
. Пример 1 можно исправить, приведя результат операции обратно к типу unsigned char
.
За исключением нескольких особых случаев, таких как операторы ++
и sizeof
, целочисленные преобразования применяются почти ко всем операциям в C, независимо от того, используются ли унарные, двоичные (или троичные) операторы.
Обычные арифметические преобразования
Всякий раз, когда двоичная операция (операция с 2 операндами) выполняется в C, оба операнда оператора должны быть одного типа. Следовательно, в случае, если операнды имеют разные типы, C обеспечивает неявное преобразование одного операнда в тип другого операнда. Правила того, как это делается, называются обычными художественными преобразованиями (иногда неофициально именуемыми "балансировкой"). Они указаны в C11 6.3.18:
(Думайте об этом правиле как о длинном вложенном утверждении if-else if
, и его может быть легче прочитать :))
6.3.1.8 Обычные арифметические преобразования
Многие операторы, которые ожидают операнды арифметического типа, вызывают преобразования и дают результат печатает аналогичным образом. Цель состоит в том, чтобы определить общий реальный тип для операндов и результат. Для указанных операндов каждый операнд конвертируется без изменения типа домен, к типу, чей соответствующий действительный тип является общим действительным типом. Если не явно указано иное, общий реальный тип также является соответствующим реальным типом результат, тип которого домен является доменом типа операндов, если они одинаковы, и сложный в противном случае. Этот шаблон называется обычным арифметическим преобразованием:
- Во-первых, если соответствующий действительный тип одного из операндов равен
long double
, другой операнд преобразуется без изменения домена типа в тип, соответствующий действительный тип которого равенlong double
.- В противном случае, если соответствующий действительный тип одного из операндов равен
double
, другой операнд преобразуется без изменения домена типа в тип, соответствующий действительный тип которого равенdouble
.- В противном случае, если соответствующий действительный тип одного из операндов равен
float
, другой операнд преобразуется без изменения домена типа в тип, соответствующий действительный тип которого является float.В противном случае целочисленные продвижения выполняются для обоих операндов. Тогда к повышенным операндам применяются следующие правила:
- Если оба операнда имеют одинаковый тип, дальнейшее преобразование не требуется.
- В противном случае, если оба операнда имеют целочисленные типы со знаком или оба без знака целочисленные типы, операнд с типом меньшего целого ранга преобразования преобразован в тип операнда с большим рангом.
- В противном случае, если операнд с целым типом без знака имеет ранг больше или равен рангу типа другого операнда, то операнд с целочисленный тип со знаком преобразуется в тип операнда с беззнаковым целочисленный тип.
- В противном случае, если тип операнда со знаком целого типа может представлять все значения типа операнда с целым типом без знака, то операнд с целым типом без знака преобразуется в тип операнд со знаком целого типа.
- В противном случае оба операнда преобразуются в целочисленный тип без знака. соответствует типу операнда с целочисленным типом со знаком.
Здесь следует отметить, что обычные арифметические преобразования применяются как к переменным с плавающей точкой, так и к целочисленным переменным. В случае целых чисел, мы также можем заметить, что целочисленные продвижения вызываются из обычных арифметических преобразований. И после этого, когда оба операнда имеют хотя бы ранг int
, операторы уравновешиваются одним и тем же типом с одинаковой подписью.
По этой причине a + b
в примере 2 дает странный результат. Оба операнда являются целыми числами и имеют по крайней мере ранг int
, поэтому целочисленные повышения не применяются. Операнды не одного типа - a
это unsigned int
и b
это signed int
. Поэтому оператор b
временно преобразуется в тип unsigned int
. Во время этого преобразования он теряет информацию о знаке и в итоге становится большим значением.
Причина, по которой изменение типа на short
в примере 3 решает проблему, заключается в том, что short
является целочисленным типом малого размера. Это означает, что оба операнда являются целочисленными и переводятся в тип int
со знаком. После целочисленного преобразования оба операнда имеют один и тот же тип (int
), дальнейшее преобразование не требуется. И тогда операция может быть выполнена на подписанном типе, как и ожидалось.