массив char в long приводит к неожиданному значению

Я попытался преобразовать массив байтов в длинный

long readAndSkipLong(char*& b)
{
    unsigned long ret = (b[0] << 56) | (b[1] << 48) | (b[2] << 40) | (b[3]<<32) | (b[4] << 24) | (b[5] << 16) | (b[6] << 8) | (b[7]);
    return ret;
}

Мое переключение кажется неправильным. По предполагаемой стоимости

152  --> 00000000 00000000 00000000 00000000 00000000 00000000 00000000 10011000

Я получил:

-104  --> 11111111 11111111 11111111 11111111 11111111 11111111 11111111 10011000 

Есть идеи, где ошибка?


person Anthea    schedule 20.03.2015    source источник
comment
Что это за компилятор? Может длинное 32-битное, нет?   -  person OldProgrammer    schedule 20.03.2015
comment
64-битная машина Windows, но я думаю, что 32-битная визуальная студия   -  person Anthea    schedule 23.03.2015


Ответы (3)


Это из-за продвижения типа и расширения знака. Каждое значение в вашем массиве char подписано, а битовый сдвиг — это целочисленная операция. Когда вы используете оператор сдвига, он оценивается как int, а поскольку ваши char являются знаковыми, их сдвиг приведет к созданию подписанных int.

Последний (самый правый) байт имеет 1 в качестве бита знака. При повышении до int его значение становится -104 по расширению знака. Поскольку вы объединили остальные числа с помощью операции ИЛИ, все 1 битов остались неизменными.

Чтобы избежать этой проблемы, вы можете преобразовать каждые chars в unsigned long перед сдвигом и операцией ИЛИ.

Еще одна вещь, которую вы можете сделать, это выполнить побитовое И для каждого char с 0xff, например ((b[i] & 0xff) << 24). Выполнение И с 0xff даст int, сохраняя младшие значащие 8 бит без изменений и нули слева, без расширения знака.

person Sufian Latif    schedule 20.03.2015
comment
после того, как char будет преобразован в int, сдвиг такого количества битов будет в основном UB. Я бы предложил сначала построить два int32_t, а затем соединить их вместе. Или, может быть, лучше, union и uint64_t с uint6_t[8], а затем используйте цикл, чтобы заполнить его. - person user3528438; 20.03.2015
comment
@user3528438 user3528438, использующий long long, также сделает это. - person Sufian Latif; 20.03.2015
comment
приведение каждого символа к длинному не имело никакого эффекта - не могли бы вы опубликовать код? - person Anthea; 23.03.2015
comment
@Anthea, извините, я ошибся - когда вы приводите char к long, оба типа подписываются, и происходит расширение знака. Вы можете привести к unsigned long или И значение с 0xff сначала. Я обновил свой ответ. - person Sufian Latif; 23.03.2015
comment
Я реализовал так, как задумал Сид Херор, и привел каждый символ к uint8_t. После этого я попробовал ваше решение с помощью ANDing с 0xff, и оно также работает. Я не уверен, какое решение лучше... - person Anthea; 23.03.2015
comment
приведение к unsigned long не будет работать, потому что long является 32-битным типом в Windows и не может хранить 64-битные - person phuclv; 08.11.2020

2 вещи:

  1. char может быть подписанным или неподписанным, поэтому его не следует использовать для хранения типов данных, отличных от символов.

    В C, C++ и большинстве C-подобных языков любые типы, более узкие, чем int, должны повышаться до int в выражении, и ваше заявление будет обработано так

     unsigned long ret = ((int)b[0] << 56) | ((int)b[1] << 48)
                       | ((int)b[2] << 40) | ((int)b[3] << 32)
                       | ((int)b[4] << 24) | ((int)b[5] << 16)
                       | ((int)b[6] <<  8) | ((int)b[7]);
    

    Если char подписано, оно будет повышено до int с помощью расширения подписи. В результате верхние биты будут заполнены единицами, если значение байта отрицательное.

    В MSVC char по умолчанию подписано. Вы можете использовать /J, чтобы сделать char беззнаковым, что решит часть вашей проблемы. Но тогда возникает другая проблема:

  2. В Windows long является 32-разрядным типом, поэтому вы не можете упаковать 8 байт в него. Кроме того, int также является 32-разрядным в большинстве современных систем, и после повышения b[i] до int сдвиг более чем на 31 является неопределенным поведением, которое это то, что делает ваша программа.

Итак, чтобы исправить все проблемы портативно, вам нужно:

  • Приведите все b[i] к unsigned char или uint8_t или маскируйте старшие биты, объединив их с 0xFF, как предложено 0605002. Или просто измените тип b на unsigned char&* вместо char&*
  • Измените тип возвращаемого значения на тип шириной не менее 64 бит, например (unsigned) long long, (u)int64_t или (u)int_least64_t.

Результат может выглядеть так

uint64_t readAndSkipLong(unsigned char*& b)
{
    return ((uint64_t)b[0] << 56) | ((uint64_t)b[1] << 48)
         | ((uint64_t)b[2] << 40) | ((uint64_t)b[3] << 32)
         | ((uint64_t)b[4] << 24) | ((uint64_t)b[5] << 16)
         | ((uint64_t)b[6] <<  8) | ((uint64_t)b[7]);
}

or

uint64_t readAndSkipLong(char*& b)
{
    return ((uint64_t)(uint8_t)b[0] << 56) | ((uint64_t)(uint8_t)b[1] << 48)
         | ((uint64_t)(uint8_t)b[2] << 40) | ((uint64_t)(uint8_t)b[3] << 32)
         | ((uint64_t)(uint8_t)b[4] << 24) | ((uint64_t)(uint8_t)b[5] << 16)
         | ((uint64_t)(uint8_t)b[6] <<  8) | ((uint64_t)(uint8_t)b[7]);
}

Однако на самом деле вам не нужно писать функцию для обратного порядка байтов. Для этого уже есть ntohll() и htonll()

reversedEndian = ntohll(originalValue);

Если ввод должен быть массивом char, просто скопируйте значение в массив uint64_t

memcpy(&originalValue, &b, sizeof originalValue);
reversedEndian = ntohll(originalValue);

Вы можете еще больше уменьшить все это до reversedEndian = ntohll(*(int64_t*)&b);, если строгий псевдоним разрешен, потому что на x86 невыровненный доступ обычно разрешен

person phuclv    schedule 23.03.2015

Пара вещей для размышления

  1. включите cstdint и используйте std::uint64_t и std::uint8_t для ваших типов, чтобы не было проблем со знаком.
  2. Логика также зависит от того, является ли ваша машина прямым порядком байтов или обратным порядком байтов. Для машин с прямым порядком байтов вам нужно сначала поместить младший значащий байт, а затем перейти к более высокому. Ваша логика для Big Endian.
  3. Возможно, у вас переполнение счетчика. Лучшим способом было бы явно объявить uint64_t и использовать его.

Вот небольшой код, который я написал для байтов до uint64_t на машине с прямым порядком байтов.

std::uint64_t bytesToUint64(std::uint8_t* b) {
    std::uint64_t msb = 0x0u;
    for (int i(0); i < 7; i++) {
        msb |= b[i];
        msb <<= 8;
    }
    msb |= b[7];

    return msb;
}

EDIT by OP (реализован совет 1):

long readAndSkipLong(char*& b)
{
    std::uint64_t ret = 
        ((std::uint8_t)b[0] << 56) | 
        ((std::uint8_t)b[1] << 48) | 
        ((std::uint8_t)b[2] << 40) | 
        ((std::uint8_t)b[3] << 32) | 
        ((std::uint8_t)b[4] << 24) | 
        ((std::uint8_t)b[5] << 16) | 
        ((std::uint8_t)b[6] <<  8) | 
        ((std::uint8_t)b[7]);
    b+=8;

    return ret;
}
person Sid Heroor    schedule 20.03.2015
comment
спасибо, на самом деле ваш код у меня не работает, но ваш совет использовать std::uint64_t и std::uint8_t Я обновил ваш ответ своим рабочим кодом для будущих ссылок! - person Anthea; 23.03.2015
comment
(std::uint8_t)b[0] << 56 не будет работать, потому что (std::uint8_t)b[0] будет преобразовано в int, не содержащее 64 бита. И вы все еще возвращаете long, который не является 64-битным типом в Windows. - person phuclv; 13.09.2018