Как заставить C интерпретировать переменные как значения со знаком или без знака? - PullRequest
0 голосов
/ 19 июня 2019

Я работаю над проектом, в котором мне часто приходится интерпретировать определенные переменные как значения со знаком или без знака и выполнять над ними операции со знаком;однако во многих случаях незначительные, казалось бы, незначительные изменения заменяли неподписанную интерпретацию на подписанную, в то время как в других случаях я не мог заставить C интерпретировать ее как значение со знаком, и оно оставалось без знака.Вот два примера:

int32_t pop();

//Version 1
push((int32_t)( (-1) * (pop() - pop()) ) );

//Version 2
int32_t temp1 = pop();
int32_t temp2 = pop();
push((int32_t)( (-1) * (temp1 - temp2) ) );

/*Another example */

//Version 1
int32_t get_signed_argument(uint8_t* argument) {
  return (int32_t)( (((int32_t)argument[0] << 8) & (int32_t)0x0000ff00 | (((int32_t)argument[1]) & (int32_t)0x000000ff) );
}

//Version 2
int16_t get_signed_argument(uint8_t* argument) {
  return (int16_t)( (((int16_t)argument[0] << 8) & (int16_t)0xff00 | (((int16_t)argument[1]) & (int16_t)0x00ff) );
}

В первом примере версия 1, по-видимому, не умножает значение на -1, в то время как версия 2 делает, но единственное отличие заключается в временном хранении промежуточных значений вычисленияпеременные в одном случае или нет в другом.

Во втором примере значение, возвращаемое версией 1, является интерпретацией без знака тех же байтов, что и возвращаемое значение версии 2, что интерпретирует его в дополнении 2,Единственное отличие заключается в использовании int16_t или int32_t.

. В обоих случаях я использую подписанные типы (int32_t, int16_t), но этого недостаточно для их интерпретации как знаковых значений.Не могли бы вы объяснить, почему эти различия вызывают различия в подписи?Где я могу найти больше информации об этом?Как я могу использовать более короткую версию первого примера, но все же получить подписанные значения?Заранее спасибо!

Ответы [ 5 ]

1 голос
/ 19 июня 2019

, если вы просто хотите преобразовать двоичный буфер в более длинные целые числа со знаком, например, полученную форму где-то (я предполагаю, что младший порядок)

int16_t bufftoInt16(const uint8_t *buff)
{
    return (uint16_t)buff[0] | ((uint16_t)buff[1] << 8);
}

int32_t bufftoInt32(const uint8_t *buff)
{
    return (uint32_t)buff[0] | ((uint32_t)buff[1] << 8) | ((uint32_t)buff[2] << 16) | ((uint32_t)buff[3] << 24) ;
}

int32_t bufftoInt32_2bytes(const uint8_t *buff)
{
    int16_t result = (uint16_t)buff[0] | ((uint16_t)buff[1] << 8);
    return result;
}


int main()
{
    int16_t x = -5;
    int32_t y = -10;
    int16_t w = -5567;

    printf("%hd %d %d\n", bufftoInt16(&x), bufftoInt32(&y), bufftoInt32_2bytes(&w));

    return 0;
}

приведение байтов к целым числам со знаком работает совершенно иначе, чем беззнаковоесмещение.

1 голос
/ 19 июня 2019

Я предполагаю, pop() возвращает тип без знака.Если это так, выражение pop() - pop() будет выполняться с использованием арифметики без знака, которая является модульной и оборачивается, если вторая pop() больше первой (кстати, C не указывает конкретный порядок вычисления, поэтому нетгарантируйте, какое полученное значение будет первым или вторым).

В результате значение, которое вы умножите на -1, может не соответствовать ожидаемой;если бы был перенос, это могло бы быть большое положительное значение, а не отрицательное значение.

Вы можете получить эквивалент временных значений, если произнесете хотя бы один из вызовов функции напрямую.

push(-1 * ((int32_t)pop() - pop()));
0 голосов
/ 20 июня 2019

Чтобы помочь вам увидеть, что происходит в вашем коде, я включил текст стандарта, который объясняет, как выполняются автоматические преобразования типов (для целых чисел), а также раздел о побитовом сдвиге, поскольку это работает немного по-другому.Затем я перебираю ваш код, чтобы увидеть, какие именно промежуточные типы существуют после каждой операции.

Соответствующие части стандарта

6.3.1.1 Булевы, символы и целые числа

Если int может представлять все значения исходного типа, значение преобразуется в int;в противном случае он конвертируется в беззнаковое целое.Они называются целочисленными акциями.Все остальные типы не изменяются целочисленными акциями.

6.3.1.8 Обычные арифметические преобразования

(я просто суммирую соответствующие части здесь.)

  1. Целочисленное продвижение выполнено.
  2. Если они оба подписаны или оба без знака, они оба преобразуются в больший тип.
  3. Если тип без знака больше, тип со знакомпреобразуется в тип без знака.
  4. Если тип со знаком может представлять все значения типа без знака, тип без знака преобразуется в тип со знаком.
  5. В противном случае они оба преобразуются втип без знака того же размера, что и тип со знаком.

(В основном, если у вас есть a OP b, размер используемого типа будет наибольшим из int, тип (a), типа (b), и он будет предпочитать типы, которые могут представлять все значения, представляемые типом (a) и типом (b). И, наконец, он предпочитает подписанные типы. В большинстве случаев это означает, что это будет int.)

6.5.7 Операции побитового сдвига

Результатом E1 << E2 является E1-сдвинутая влево позиция E2;освобожденные биты заполнены нулями.Если E1 имеет тип без знака, значение результата составляет $ E1 x 2 ^ {E2} $, уменьшенное по модулю на единицу больше максимального значения, представляемого в типе результата.Если E1 имеет тип со знаком и неотрицательное значение, а $ E1 x 2 ^ {E2} $ представимо в типе результата, то это итоговое значение;в противном случае поведение не определено. </li>

Как все это относится к вашему коду.

Сейчас я пропускаю первый пример, поскольку я нене знаю, какой тип pop () возвращает.Если вы добавите эту информацию в свой вопрос, я могу обратиться и к этому примеру.

Давайте рассмотрим, что происходит в этом выражении (обратите внимание, что у вас было лишнее ( после первого приведения в вашей версии; яя удалил это):

(((int32_t)argument[0] << 8) & (int32_t)0x0000ff00 | (((int32_t)argument[1]) & (int32_t)0x000000ff) )

Некоторые из этих преобразований зависят от относительных размеров типов.Пусть INT_TYPE будет больше int32_t и int в вашей системе.

((int32_t)argument[0] << 8)

  1. Аргумент [0] явно приведен к int32_t
  2. 8 уже являетсяint, поэтому преобразование не происходит
  3. (int32_t) аргумент [0] преобразуется в INT_TYPE.
  4. Сдвиг влево происходит, и результат имеет тип INT_TYPE.

(Обратите внимание, что если бы аргумент [0] мог быть отрицательным, сдвиг был бы неопределенным поведением. Но так как он был изначально без знака, так что вы здесь в безопасности.)

Пусть a представляет результат этихшаги.

a & (int32_t)0x0000ff00

  1. 0x000ff0 явно приведен к int32_t.
  2. Обычные арифметические преобразования.Обе стороны конвертируются в INT_TYPE.Результат имеет тип INT_TYPE.

Пусть b представляет результат этих шагов.

(((int32_t)argument[1]) & (int32_t)0x000000ff)

  1. Оба явных приведения происходят
  2. Выполнены обычные арифметические преобразования.Обе стороны теперь INT_TYPE.
  3. Результат имеет тип INT_TYPE.

Пусть c представляет этот результат.

b | c

  1. Обычные арифметические преобразования;без изменений, так как они оба INT_TYPE.
  2. Результат имеет тип INT_TYPE.

Заключение

Так что ни один из промежуточных результатов здесь не подписан.(Кроме того, большинство явных приведений были не нужны, особенно если в вашей системе sizeof(int) >= sizeof(int32_t)).

Кроме того, поскольку вы начинаете с uint8_t с, никогда не сдвигаете более 8 бит и сохраняете все промежуточные результаты в типах не менее 32 бит, верхние 16 бит всегда будут равны 0, а значениявсе они будут неотрицательными, что означает, что типы со знаком и без знака представляют все значения, которые вы могли бы иметь здесь точно такие же .

Что именно вы наблюдаете, что заставляет вас думать, что оно используетнеподписанные типы, где он должен использовать подписанные?Можем ли мы увидеть примеры входов и выходов вместе с ожидаемыми результатами?

Редактировать: Исходя из вашего комментария, получается, что причина, по которой он работает не так, как вы ожидали, не в том, что тип unsigned , но поскольку вы генерируете побитовые представления 16-битных знаковых целых, но сохраняете их в 32-битных знаковых целых.Избавьтесь от всех приведений, кроме (int32_t)argument[0] (и измените их на (int)argument[0]. int - это обычно размер, на котором система работает наиболее эффективно, поэтому ваши операции должны использовать int, если у вас нет определенногопричина использовать другой размер).Затем приведите final result к int16_t.

0 голосов
/ 20 июня 2019

Я работаю над проектом, в котором мне часто приходится интерпретировать определенные переменные как значения со знаком или без знака и выполнять над ними операции со знаком.

Это кажется чреватым. Я имею в виду, что вы хотите переосмыслить представления объектов как имеющие разные типы (различающиеся только по подписи) в разных ситуациях, или, возможно, вы хотите преобразовать значения, как если бы вы интерпретировали представления объектов. Такие вещи обычно приводят к беспорядку, хотя вы можете справиться с этим, если вы будете достаточно внимательны. Это может быть проще, если вы хотите зависеть от деталей вашей реализации, таких как ее представления различных типов.

Крайне важно в таких случаях знать и понимать все правила неявных преобразований , как целочисленные повышения, так и обычные арифметические преобразования, и при каких обстоятельствах они применяются. Важно понимать влияние этих правил на оценку ваших выражений - как тип, так и значение всех промежуточных и конечных результатов.

Например, лучшее, на что вы можете надеяться в отношении броска в

push((int32_t)( (-1) * (temp1 - temp2) ) );

в том, что это бесполезно. Если значение не представимо в этом типе, то (если это целочисленный тип со знаком) может быть подан сигнал, а если нет, то результат определяется реализацией. Однако, если значение является представимым, преобразование не изменит его. В любом случае результат не освобождается от дальнейшего преобразования в тип параметра push().

В другом примере разница между версией 1 и версией 2 вашего первого примера в значительной степени заключается в том, какие значения преобразованы, когда (но см. Также ниже). Если эти два результата действительно дают разные результаты, из этого следует, что тип возвращаемого значения pop() отличается от int32_t. В этом случае, если вы хотите преобразовать их в другой тип, чтобы выполнить над ними операцию, вы должны это сделать. Ваша версия 2 выполняет это, присваивая результаты pop() переменным желаемого типа, но было бы более идиоматично выполнять преобразования с помощью приведений:

push((-1) * ((int32_t)pop() - (int32_t)pop()));

Однако помните, что если результаты вызовов pop() зависят от их порядка - например, если они выталкивают элементы из стека - тогда у вас есть еще одна проблема: относительный порядок, в котором эти операнды Оценка не указана, и вы не можете с уверенностью предположить, что она будет согласованной. По этой причине, а не из-за соображений при наборе текста, здесь предпочтительна ваша версия 2.

В целом, однако, если у вас есть стек, элементы которого могут представлять значения различных типов, то я бы предложил сделать тип элемента объединением (если тип каждого элемента неявно задан из контекста) или теговым объединением (если элементы нужно нести информацию о своих типах. Например,

union integer {
    int32_t signed;
    uint32_t unsigned;
};

union integer pop();
void push(union integer i);

union integer first = pop();
union integer second = pop();
push((union integer) { .signed = second.signed - first.signed });
0 голосов
/ 19 июня 2019

Результат выражения в C имеет свой тип, определяемый типами компонентных операндов этого выражения, а не любым приведением, которое вы можете применить к этому результату.Как комментирует Barmar выше, для принудительного определения типа результата вы должны привести один из операндов.

...