Как заставить C интерпретировать переменные как значения со знаком или без знака?

Я работаю над проектом, в котором мне часто нужно интерпретировать определенные переменные как значения со знаком или без знака и выполнять над ними операции со знаком; однако во многих случаях тонкие, кажущиеся незначительными изменения заменяли беззнаковую интерпретацию на подписанную, в то время как в других случаях я не мог заставить 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), но этого недостаточно, чтобы интерпретировать их как значения со знаком. Не могли бы вы объяснить, почему эти различия вызывают разницу в подписании? Где я могу найти больше информации об этом? Как я могу использовать более короткую версию первого примера, но при этом получать значения со знаком? Заранее спасибо!

Это может быть полезно stackoverflow.com/questions/46073295/…

Juan Carlos Ramirez 19.06.2019 22:09

Какой тип возврата у pop()?

Barmar 19.06.2019 22:11

Вы должны конвертировать в signed при первой же возможности: -1 * ((int32_t)pop() - (int32_t)pop())

Barmar 19.06.2019 22:12

Что касается вашего второго примера, вы должны проявлять особую осторожность при выполнении побитовых операций, особенно сдвигов, с операндами подписанных типов. Есть множество способов выстрелить себе в ногу с этим.

John Bollinger 19.06.2019 22:23

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

Ray 19.06.2019 23:35

Пожалуйста, разместите полные объявления push и pop. В настоящее время опубликованные ответы кажутся спекулятивными.

Lundin 20.06.2019 11:24
Стоит ли изучать 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
6
453
5
Перейти к ответу Данный вопрос помечен как решенный

Ответы 5

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

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

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

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

Я отредактировал вопрос, pop() возвращает подписанный int32_t. Я не знал, что C не определяет порядок оценки, спасибо, что указали на это! Проблема в этом случае, вероятно, заключалась в неправильном порядке вычисления двух вызовов pop(), потому что результат был обратным ожидаемому. Тогда это не было связано с подписанностью и умножением. К сожалению, насколько я знаю, я могу принять только один ответ, но я очень благодарен за помощь!

Peter 20.06.2019 01:45

Тип результата выражения в C определяется типами составных операндов этого выражения, а не каким-либо приведением, которое вы можете применить к этому результату. Как Бармар комментирует выше, чтобы принудительно указать тип результата, вы должны привести один из операндов.

Это то, что меня смутило, я отредактировал вопрос, pop() возвращает значение со знаком, поэтому приведение его к значению со знаком не имеет значения.

Peter 20.06.2019 00:44

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

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;
}

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

I am working on a project where I often need to interpret certain variables as signed or unsigned values and do signed operations on them.

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

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

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

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 });
Ответ принят как подходящий

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

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

6.3.1.1 Логические, символьные и целые числа

  1. If an int can represent all values of the original type, the value is converted to an int; otherwise, it is converted to an unsigned int. These are called the integer promotions. All other types are unchanged by the integer promotions.

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

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

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

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

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

  1. The result of E1 << E2 is E1 left-shifted E2 bit positions; vacated bits are filled with zeros. If E1 has an unsigned type, the value of the result is $E1 x 2^{E2}$,reduced modulo one more than the maximum value representable in the result type. If E1 has a signed type and nonnegative value, and $E1 x 2^{E2}$ is representable in the result type, then that is the resulting value; otherwise, the behavior is undefined.

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

Я пока пропускаю первый пример, так как не знаю, какой тип возвращает 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_ts, никогда не выполняете сдвиг более чем на 8 бит и сохраняете все промежуточные результаты в типах не менее 32 бит, верхние 16 бит всегда будут равны 0, а все значения будут неотрицательными, что означает, что типы со знаком и без знака представляют все значения, которые вы могли бы иметь здесь точно так же.

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

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

Спасибо, что провели меня через это! К сожалению, я не могу принять несколько ответов, но я ценю помощь! Означает ли это, что если я хочу выполнить сдвиг битов для типов меньшего размера, чем int, я должен привести количество битов для сдвига к этому типу? Например uint16_t foo; foo << (uint16_t)8;

Peter 20.06.2019 02:22

Пример ввода был 0xfff9, поэтому аргумент [0] = 0xff и аргумент [1] = 0xf9. 16-битная версия моей функции вернула -7, как и ожидалось, но 32-битная версия вернула 65 529, десятичное представление 0xfff9.

Peter 20.06.2019 02:26

@Peter Итак, вы пытались получить отрицательные числа из этих функций? В этом случае важно помнить о разнице между битами, хранящимися в значении, и тем, как эти биты интерпретируются. Существует несколько различных способов представления отрицательных чисел, но во всех старший бит равен 1. 0x0000FFF9 — положительное число; 0xFFF9 — отрицательное число. Но побитовые операции работают только с необработанными битами; они не знают или не заботятся об отрицательных значениях. У вас есть правильные биты; теперь просто приведите значение окончательный к int16_t, и оно будет работать так, как вы хотите.

Ray 20.06.2019 03:03

@Peter То есть проблема не в подписанной/неподписанной части типа; это размер. Я добавил еще один абзац в конец ответа, в котором более подробно объясняется, что вы должны делать.

Ray 20.06.2019 03:12

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