У меня есть поведение, которое я не понимаю, я пытаюсь построить целое число 64 из массива байтов от старшего до младшего.
uint64_t u;
uint8_t bytes[2];
bytes[1] = 0xFF;
u = bytes[1] << 24 ;
dump_bytes_as_hex( &u, 8 );
00 00 00 FF FF FF FF FF
u = ( (uint16_t) bytes[1]) << 24 ;
dump_bytes_as_hex( &u, 8 );
00 00 00 FF FF FF FF FF
u = ( (uint32_t) bytes[1]) << 24 ;
dump_bytes_as_hex( &u, 8 );
00 00 00 ПФ 00 00 00 00
Я не понимаю, почему это дает мне правильный результат, только если я привожу к типу, который имеет больше бит, чем размер сдвига. Я пробовал разные значения:
Вот я и хотел узнать, что за правило? и почему 100 дают мне правильное значение.
Спасибо.
Редактировать :
Вот воспроизводимый пример:
#include <stdio.h>
#include <stdint.h>
void dump_bytes_as_hex( uint8_t* b, int count )
{
FILE* f;
f = stdout;
for( int c = 0; c < count; ++c )
{
fprintf( f, "%02X", b[c] );
fputc( ' ', f );
}
fputc( '\n', f );
fflush( f );
}
void test( uint8_t i )
{
uint64_t u;
uint8_t bytes[2];
fprintf( stdout, "Test with %d\n", (int) i );
u = 0;
bytes[1] = i;
u = bytes[1] << 24 ;
dump_bytes_as_hex( (uint8_t*) &u, 8 );
u = ( (uint16_t) bytes[1]) << 24 ;
dump_bytes_as_hex( (uint8_t*) &u, 8 );
u = ( (uint32_t) bytes[1]) << 24 ;
dump_bytes_as_hex( (uint8_t*) &u, 8 );
fprintf( stdout, "\n\n");
}
int main()
{
test( 0xFF );
test( 0xFF -1 );
test( 100 );
return 0;
}
Вы не показываете код для dump_bytes_as_hex
, но... Зачем вы делаете: dump_bytes_as_hex( &u, 8 );
? Почему бы не использовать: dump_bytes_as_hex(bytes, sizeof(bytes));
?
@ Крейг Эсти, ты имеешь в виду dump_bytes_as_hex( &u, sizeof(u) )
?
@ikegami Нет, это был код OP, который я только что скопировал (т.е. OP использовал 8
). Я не пытаюсь исправить вызов &u
таким образом.
Я обновил вопрос воспроизводимым примером.
Совет: dump_bytes_as_hex
можно заменить на printf( "%" PRIx32 "\n", u );
(необходимо также указать inttypes.h
).
@ Крейг Эсти, тогда ты ошибаешься. Должно быть dump_bytes_as_hex( &u, sizeof(u) )
, а не dump_bytes_as_hex(bytes, sizeof(bytes))
. Они пытаются сбросить u
, а не bytes
.
@ikegami Как я уже упоминал, нам нужно было увидеть код для dump_bytes_as_hex
[прежде чем мы должны комментировать дальше]. Они пытаются сбросить u
но, имхо, это неправильно. При редактировании функция дампа сбрасывает поток байтов. И дамп bytes
обычно более полезен, поскольку код u
обрабатывает только один экземпляр в большом буфере (например, 1000 байт).
@Craig Estey, тогда это работает, только если объект имеет тип uint64_t
. Использование функции общего назначения не является ошибкой. И видя, что ваша версия молча выдает неправильный результат, если вы передаете uint32_t
, можно утверждать, что ваше решение хуже.
Re «сброс байтов в целом более полезен». ОП пытается отладить сдвиг. Сброс переменной, отличной от той, которую вы хотите изучить, не является «более полезным в целом».
Тип переменной, которой вы присваиваете результат, не имеет значения.
Это работает только в том случае, если тип левого операнда является типом, который после целочисленного преобразования может содержать результат.
(uint8_t)0xFF << 24
В среде с типом int
16..32 бит (uint8_t)0xFF
повышается до int
. 0xFF × 224 слишком велик, чтобы уместиться в int
. Будучи подписанным типом, это приводит к неопределенному поведению.
В среде с типом int
33+ бит (uint8_t)0xFF
повышается до int
. 0xFF × 224 вписывается в int
. Это работает.
(uint16_t)0xFF << 24
В среде с типом int
16..24 бит (uint16_t)0xFF
приводит к 16-битному unsigned int
. Сдвиг влево на количество битов, превышающее или равное размеру операнда, является неопределенным поведением.
В среде с типом int
25..32 бит (uint16_t)0xFF
приводит к int
. 0xFF × 224 слишком велик, чтобы уместиться в int
. Будучи подписанным типом, это приводит к неопределенному поведению.
В среде с типом int
33+ бит (uint16_t)0xFF
приводит к int
. 0xFF × 224 вписывается в int
. Это работает.
(uint32_t)0xFF << 24
0xFF × 224 соответствует 32-битному целому числу без знака. Это работает.
Итак, чтобы создать uint64_t
, вам нужно
(uint64_t)( (uint64_t)u8 << 24 )
или
(uint64_t)( (uint32_t)u8 << 24 )
Однако внешнее приведение здесь можно опустить, так как присваивание будет неявно выполнять это приведение.
Спасибо, что нашли время ответить мне.
Вот я и хотел узнать, что за правило?
На самом деле нет правила для ваших конкретных примеров...
bytes[1] = 0xFF; u = bytes[1] << 24 ;
... и ...
u = ( (uint16_t) bytes[1]) << 24 ; dump_bytes_as_hex( &u, 8 );
... в том случае, если ваш int
имеет ширину 32 бита.
Левый операнд операции сдвига подвергается обычным арифметическим преобразованиям, результатом которых является преобразование 8- или 16-битного беззнакового значения левого операнда в (signed
) int
и получение результата этого типа. Если арифметический результат сдвига влево значения знакового типа (т. е. int
) не может быть представлен как значение этого типа, то поведение не определено. Здесь так и есть (поэтому я и сказал, что правила нет).
В вашем конкретном случае кажется, что ваша реализация выполняет сдвиг, как будто переинтерпретируя левый операнд как 32-битное значение без знака, переинтерпретируя результат как (со знаком) int. Присвоение типа uint64_t
затем продолжается (обычно) путем преобразования отрицательного правого операнда в ty uint64_t
путем добавления 264, чтобы ввести его в диапазон этого типа. (Такие преобразования выполняются только для преобразования в целочисленные типы без знака, а не в типы со знаком.) Вы работаете в системе с прямым порядком байтов и распечатываете полученные байты в порядке памяти, поэтому вы получаете:
00 00 00 FF FF FF FF FF
С другой стороны, если вы преобразуете операнд сдвига в uint32_t
в своей 32-битной системе,...
u = ( (uint32_t) bytes[1]) << 24 ; dump_bytes_as_hex( &u, 8 );
... тогда на результирующий тип не влияют стандартные арифметические преобразования, и результат преобразования имеет этот тип. uint32_t
может представлять арифметический результат сдвига (0xff000000
), так что это фактически результат. Это значение не меняется при преобразовании в тип uint64_t
для назначения.
Общий совет: используйте беззнаковые типы при работе с побитовыми операциями и особенно при выполнении сдвигов.
Re "Общий совет: используйте беззнаковые типы при работе с побитовыми операциями", это сложно. В системе с 64-битными целыми числами ваше решение использует подписанный тип.
@ikegami, ни один из кодов в этом ответе не является моим решением. Все фрагменты цитируются из вопроса. Лично я мог бы сказать, что реализации, имеющие int
уже 32 бита, не поддерживаются, а затем использовать unsigned int
вместо uint32_t
. Или, поскольку uint64_t
в любом случае является целевым типом, может быть даже лучше использовать его с самого начала. Да, выбор правильных типов требует некоторого внимания, но это никоим образом не противоречит моему совету.
На самом деле это не «обычные арифметические преобразования», которые сопровождаются «балансировкой» между операндами. Вместо этого у операторов сдвига есть специальное правило: «Повышение целочисленности выполняется для каждого из операндов. Тип результата — тип повышенного левого операнда». Таким образом, что-то вроде my_char << 8ULL
все равно приведет к повышению левого операнда до int
, и результатом будет int
.
Спасибо, я понял ваш ответ.
Я предполагаю, что
dump_bytes_as_hex
показывает л.с. байт первый. В примерах значение будет повышено доint
, и смещение значения со знаком может привести к проблемам. Даже когда вы переводите наuint16_t
, это будет повышено до (подписано)int
перед переключением.