Сдвиг влево и кастинг

У меня есть поведение, которое я не понимаю, я пытаюсь построить целое число 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

Я не понимаю, почему это дает мне правильный результат, только если я привожу к типу, который имеет больше бит, чем размер сдвига. Я пробовал разные значения:

  • 0xFF-1 дает такой же плохой результат
  • 100 дают правильный результат без приведения

Вот я и хотел узнать, что за правило? и почему 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 показывает л.с. байт первый. В примерах значение будет повышено до int, и смещение значения со знаком может привести к проблемам. Даже когда вы переводите на uint16_t, это будет повышено до (подписано) int перед переключением.

Weather Vane 10.04.2023 19:55

Вы не показываете код для dump_bytes_as_hex, но... Зачем вы делаете: dump_bytes_as_hex( &u, 8 );? Почему бы не использовать: dump_bytes_as_hex(bytes, sizeof(bytes));?

Craig Estey 10.04.2023 20:02

@ Крейг Эсти, ты имеешь в виду dump_bytes_as_hex( &u, sizeof(u) )?

ikegami 10.04.2023 20:02

@ikegami Нет, это был код OP, который я только что скопировал (т.е. OP использовал 8). Я не пытаюсь исправить вызов &u таким образом.

Craig Estey 10.04.2023 20:05

Я обновил вопрос воспроизводимым примером.

mystackoverflowpseudo 10.04.2023 20:18

Совет: dump_bytes_as_hex можно заменить на printf( "%" PRIx32 "\n", u ); (необходимо также указать inttypes.h).

ikegami 10.04.2023 20:19

@ Крейг Эсти, тогда ты ошибаешься. Должно быть dump_bytes_as_hex( &u, sizeof(u) ), а не dump_bytes_as_hex(bytes, sizeof(bytes)). Они пытаются сбросить u, а не bytes.

ikegami 10.04.2023 20:26

@ikegami Как я уже упоминал, нам нужно было увидеть код для dump_bytes_as_hex [прежде чем мы должны комментировать дальше]. Они пытаются сбросить u но, имхо, это неправильно. При редактировании функция дампа сбрасывает поток байтов. И дамп bytes обычно более полезен, поскольку код u обрабатывает только один экземпляр в большом буфере (например, 1000 байт).

Craig Estey 10.04.2023 20:33

@Craig Estey, тогда это работает, только если объект имеет тип uint64_t. Использование функции общего назначения не является ошибкой. И видя, что ваша версия молча выдает неправильный результат, если вы передаете uint32_t, можно утверждать, что ваше решение хуже.

ikegami 10.04.2023 20:36

Re «сброс байтов в целом более полезен». ОП пытается отладить сдвиг. Сброс переменной, отличной от той, которую вы хотите изучить, не является «более полезным в целом».

ikegami 10.04.2023 20:39
Стоит ли изучать PHP в 2023-2024 годах?
Стоит ли изучать PHP в 2023-2024 годах?
Привет всем, сегодня я хочу высказать свои соображения по поводу вопроса, который я уже много раз получал в своем сообществе: "Стоит ли изучать PHP в...
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
Поведение ключевого слова "this" в стрелочной функции в сравнении с нормальной функцией
В JavaScript одним из самых запутанных понятий является поведение ключевого слова "this" в стрелочной и обычной функциях.
Приемы CSS-макетирования - floats и Flexbox
Приемы CSS-макетирования - floats и Flexbox
Здравствуйте, друзья-студенты! Готовы совершенствовать свои навыки веб-дизайна? Сегодня в нашем путешествии мы рассмотрим приемы CSS-верстки - в...
Тестирование функциональных ngrx-эффектов в Angular 16 с помощью Jest
В системе управления состояниями ngrx, совместимой с Angular 16, появились функциональные эффекты. Это здорово и делает код определенно легче для...
Концепция локализации и ее применение в приложениях React ⚡️
Концепция локализации и ее применение в приложениях React ⚡️
Локализация - это процесс адаптации приложения к различным языкам и культурным требованиям. Это позволяет пользователям получить опыт, соответствующий...
Пользовательский скаляр GraphQL
Пользовательский скаляр GraphQL
Листовые узлы системы типов GraphQL называются скалярами. Достигнув скалярного типа, невозможно спуститься дальше по иерархии типов. Скалярный тип...
0
10
111
2
Перейти к ответу Данный вопрос помечен как решенный

Ответы 2

Тип переменной, которой вы присваиваете результат, не имеет значения.

Это работает только в том случае, если тип левого операнда является типом, который после целочисленного преобразования может содержать результат.

  • (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 )

Однако внешнее приведение здесь можно опустить, так как присваивание будет неявно выполнять это приведение.

Спасибо, что нашли время ответить мне.

mystackoverflowpseudo 11.04.2023 19:22
Ответ принят как подходящий

Вот я и хотел узнать, что за правило?

На самом деле нет правила для ваших конкретных примеров...

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 10.04.2023 20:33

@ikegami, ни один из кодов в этом ответе не является моим решением. Все фрагменты цитируются из вопроса. Лично я мог бы сказать, что реализации, имеющие int уже 32 бита, не поддерживаются, а затем использовать unsigned int вместо uint32_t. Или, поскольку uint64_t в любом случае является целевым типом, может быть даже лучше использовать его с самого начала. Да, выбор правильных типов требует некоторого внимания, но это никоим образом не противоречит моему совету.

John Bollinger 10.04.2023 20:43

На самом деле это не «обычные арифметические преобразования», которые сопровождаются «балансировкой» между операндами. Вместо этого у операторов сдвига есть специальное правило: «Повышение целочисленности выполняется для каждого из операндов. Тип результата — тип повышенного левого операнда». Таким образом, что-то вроде my_char << 8ULL все равно приведет к повышению левого операнда до int, и результатом будет int.

Lundin 11.04.2023 10:15

Спасибо, я понял ваш ответ.

mystackoverflowpseudo 11.04.2023 19:03

Другие вопросы по теме