Неявные правила продвижения типов
Этот пост предназначен для использования в качестве часто задаваемых вопросов, касающихся неявного целочисленного продвижения в C, особенно неявного продвижения, вызванного обычными арифметическими преобразованиями и / или целочисленными продвижениями.
Пример 1)
Почему это дает странное, большое целое число, а не 255?
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
Пример 2)
Почему это дает "-1 больше 0"?
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
Пример 3)
Почему изменение типа в приведенном выше примере на short
решить проблему?
unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print
(Эти примеры были предназначены для 32- или 64-разрядного компьютера с 16-разрядным сокращением.)
5 ответов
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
, Они называются целочисленными акциями.
Этот текст часто неправильно понимают как: "все маленькие целочисленные типы со знаком преобразуются в целое число со знаком, а все маленькие целочисленные типы без знака преобразуются в целое число без знака". Это неверно Часть без знака здесь только означает, что если мы имеем, например, 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 int
,
За исключением нескольких особых случаев, таких как ++
а также sizeof
операторы, целочисленные продвижения применяются почти ко всем операциям в C, независимо от того, используются ли унарные, бинарные (или троичные) операторы.
Обычные арифметические преобразования
Всякий раз, когда в C выполняется двоичная операция (операция с 2 операндами), оба операнда оператора должны быть одного типа. Поэтому, если операнды имеют разные типы, 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
), дальнейшее преобразование не требуется. И тогда операция может быть выполнена на подписанном типе, как и ожидалось.
Согласно предыдущему посту, я хочу дать больше информации о каждом примере.
Пример 1)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Поскольку unsigned char меньше, чем int, мы применяем к ним целочисленное продвижение, тогда мы имеем (int)x-(int)y = (int)(-1) и unsigned int (-1) = 4294967295.
Вывод из приведенного выше кода:(так же, как мы ожидали)
4294967295
-1
Как это исправить?
Я попробовал то, что рекомендовал предыдущий пост, но это на самом деле не работает. Вот код, основанный на предыдущем посте:
измените один из них на неподписанный int
int main(){
unsigned int x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Поскольку x уже является целым числом без знака, мы применяем только целочисленное продвижение к y. Тогда мы получим (без знака int) x- (int) y. Поскольку они по-прежнему не имеют одинаковый тип, мы применяем обычные арифметические преобразования, мы получаем (unsigned int)x-(unsigned int)y = 4294967295.
Вывод из приведенного выше кода:(так же, как мы ожидали):
4294967295
-1
Аналогично, следующий код получает тот же результат:
int main(){
unsigned char x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
изменить оба из них без знака Int
int main(){
unsigned int x = 0;
unsigned int y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
}
Поскольку оба они являются беззнаковыми int, целочисленное продвижение не требуется. По обычной арифметической конверсии (имеют одинаковый тип), (без знака int) x- (без знака int) y = 4294967295.
Вывод из приведенного выше кода:(так же, как мы ожидали):
4294967295
-1
Один из возможных способов исправить код:(добавьте приведение типа в конце)
int main(){
unsigned char x = 0;
unsigned char y = 1;
printf("%u\n", x - y);
printf("%d\n", x - y);
unsigned char z = x-y;
printf("%u\n", z);
}
Выход из вышеприведенного кода:
4294967295
-1
255
Пример 2)
int main(){
unsigned int a = 1;
signed int b = -2;
if(a + b > 0)
puts("-1 is larger than 0");
printf("%u\n", a+b);
}
Поскольку оба они являются целыми числами, целочисленное продвижение не требуется. При обычном арифметическом преобразовании мы получаем (int без знака)a+(int без знака)b = 1+4294967294 = 4294967295.
Вывод из приведенного выше кода:(так же, как мы ожидали)
-1 is larger than 0
4294967295
Как это исправить?
int main(){
unsigned int a = 1;
signed int b = -2;
signed int c = a+b;
if(c < 0)
puts("-1 is smaller than 0");
printf("%d\n", c);
}
Выход из вышеприведенного кода:
-1 is smaller than 0
-1
Пример 3)
int main(){
unsigned short a = 1;
signed short b = -2;
if(a + b < 0)
puts("-1 is smaller than 0");
printf("%d\n", a+b);
}
Последний пример исправил проблему, поскольку a и b оба преобразованы в int из-за целочисленного продвижения.
Выход из вышеприведенного кода:
-1 is smaller than 0
-1
Если я перепутал некоторые понятия, пожалуйста, дайте мне знать. Благодаря ~
Целочисленные и с плавающей запятой ранжирование и правила продвижения в C и C++
Я хотел бы попробовать это, чтобы обобщить правила, чтобы я мог быстро сослаться на них. Я полностью изучил вопрос и два других ответа здесь, включая основной ответ @Lundin. Если вам нужны дополнительные примеры помимо приведенных ниже, изучите этот ответ подробно, ссылаясь на мои сводки «правил» и «потока продвижения» ниже.
Я также написал свой собственный пример и демонстрационный код здесь: integer_promotion_overflow_underflow_undefined_behavior.c.
Несмотря на то, что обычно я сам невероятно многословен, я постараюсь сделать это кратким изложением, поскольку два других ответа плюс мой тестовый код уже содержат достаточно подробностей благодаря их необходимой многословности.
Краткое справочное руководство и сводка по целочисленному и переменному продвижению
3 простых правила
- Для любой операции, в которой задействовано несколько операндов (входных переменных) (например, математические операции, сравнения или троичные операции), переменные повышаются по мере необходимости до требуемого типа переменной перед выполнением операции.
- Следовательно, вы должны вручную явно привести вывод к любому желаемому типу, если вы не хотите, чтобы он был выбран для вас неявно. См. пример ниже.
- Все типы меньше (в моей 64-битной системе Linux) являются «малыми типами». Их нельзя использовать в ЛЮБОЙ операции. Итак, если все входные переменные являются «малыми типами», они ВСЕ сначала повышаются до ( в моей 64-битной системе Linux) перед выполнением операции.
- В противном случае, если хотя бы один из входных типов больше или больше, другой, меньший тип или типы ввода повышаются до типа этого самого большого типа ввода.
Пример
Пример: с этим кодом:
uint8_t x = 0;
uint8_t y = 1;
... если вы это сделаетеx - y
, они сначала неявно повышаются до (чтоint32_t
в моей 64-битной системе), и вы получите следующее:(int)x - (int)y
, что приводит кint
тип со значением-1
, а не тип значения . Чтобы получить желаемое255
результат, вручную привести результат обратно кuint8_t
, делая это:(uint8_t)(x - y)
.
Поток продвижения
Правила продвижения следующие. Продвижение от самых маленьких к самым большим типам происходит следующим образом.
Читать "
-->
" как "получает повышение".
Типы в квадратных скобках (например:[int8_t]
) являются типичными «целочисленными типами фиксированной ширины» для данного стандартного типа в типичной 64-разрядной архитектуре Unix (Linux или Mac). См., например:
- https://www.cs.yale.edu/homes/aspnes/pinewiki/C(2f)IntegerTypes.html
- https://www.ibm.com/docs/en/ibm-mq/7.5?topic=платформы-стандартные-типы-данных
- И, что еще лучше, проверьте это на своем компьютере , запустив мой код здесь!: stdint_sizes.c из моего репозитория eRCaGuy_hello_world .
1. Для целочисленных типов
Примечание: «маленькие типы» = (), , , , .
МАЛЕНЬКИЕ ТИПЫ:
bool
(
_Bool
),
char [int8_t]
,
unsigned char [uint8_t]
,
short [int16_t]
,
unsigned short [uint16_t]
-->
int [int32_t]
-->
unsigned int [uint32_t]
-->
long int [int64_t]
-->
unsigned long int [uint64_t]
-->
long long int [int64_t]
-->
unsigned long long int [uint64_t]
Указатели (например:void*
) иsize_t
оба 64-битные, поэтому я думаю, что они вписываются вuint64_t
категория выше.
2. Для типов с плавающей запятой
float [32-bits]
-->
double [64-bits]
-->
long double [128-bits]
Я хотел бы добавить два пояснения к отличному ответу @Lundin, касающемуся примера 1, где есть два операнда одинакового целочисленного типа, но это «маленькие типы», требующие целочисленного продвижения.
Я использую проект N1256 , так как у меня нет доступа к платной копии стандарта C.
Первое: (обязательно)
Определение целочисленного повышения в 6.3.1.1 не является инициирующим пунктом фактического выполнения целочисленного повышения. На самом деле это 6.3.1.8 Обычные арифметические преобразования.
В большинстве случаев «обычные арифметические преобразования» применяются, когда операнды имеют разные типы, и в этом случае необходимо повысить уровень хотя бы одного операнда. Но загвоздка в том, что для целочисленных типов во всех случаях требуется целочисленное продвижение.
[предложения типов с плавающей запятой идут первыми]
В противном случае целые повышения выполняются для обоих операндов. Затем к продвигаемым операндам применяются следующие правила:
- Если оба операнда имеют одинаковый тип, дальнейшее преобразование не требуется.
- В противном случае, если оба операнда имеют целые типы со знаком или оба имеют целые типы без знака, операнд с типом меньшего целочисленного ранга преобразования преобразуется в тип операнда с большим рангом.
- В противном случае, если операнд, имеющий целочисленный тип без знака, имеет ранг больше или равен рангу типа другого операнда, то операнд с целочисленным типом со знаком преобразуется в тип операнда с целочисленным типом без знака.
- В противном случае, если тип операнда с целочисленным типом со знаком может представлять все значения типа операнда с целочисленным типом без знака, то операнд с целочисленным типом без знака преобразуется в тип операнда с целочисленным типом со знаком.
- В противном случае оба операнда преобразуются в целочисленный тип без знака, соответствующий типу операнда с целочисленным типом со знаком.
Второй: (ненормативный)
В стандарте приведен явный пример, демонстрирующий это:
ПРИМЕР 2 При выполнении фрагмента
char c1, c2; /* ... */ c1 = c1 + c2;
«целочисленные продвижения» требуют, чтобы абстрактная машина увеличивала значение каждой переменной до размера, а затем добавляла два
s и обрезать сумму. При условии добавления двух s может выполняться без переполнения или с молчаливым переносом переполнения для получения правильного результата, фактическое выполнение должно давать только тот же результат, возможно, без продвижения.
В этом ответе я расскажу о флагах компилятора, которые вы можете использовать для отслеживания ошибок, связанных с неявным продвижением типов, поскольку я только что столкнулся с этой «функцией». В следующем фрагменте кода с ошибкой имеет типuint32_t
:
for (int32_t i = 22; i >= MAX(22 - exp + 1, 0); i--) {
...
}
Если код < 23 работает нормально, цикл if = 23 выполняется вечно, а еслиexp
> 23 цикл никогда не запускается. Исправление состоит в том, чтобы изменить первый аргумент наMAX
к22 - (int32_t)exp + 1
. Чтобы было легче обнаружить такие ошибки, рекомендую включить предупреждение-Wsign-compare
. Он входит в комплект поставки, который может быть немного тяжеловат для повседневного использования.
Ошибка в другом примере;
unsigned short a = 1;
signed short b = -2;
if(a + b > 0)
puts("-1 is larger than 0"); // will not print
пойман-Wsign-conversion
, также включенный в-Wextra
. В моей собственной кодовой базе этот флаг выдает около 40 предупреждений, каждое из которых совершенно безобидно и не стоит заморачиваться с исправлением.
К сожалению, ни в gcc, ни в clang нет предупреждений о пометке «подозрительных» рекламных акций, но оставлены безопасные (например,for (int i = 0; i < strlen(s); i++)
).
Вы можете прочитать «Друзья, не позволяйте друзьям использовать «-W» для получения (информированного) мнения о том, когда и когда не следует использовать предупреждающие флаги компилятора.