Я работаю над проектом, в котором мне часто нужно интерпретировать определенные переменные как значения со знаком или без знака и выполнять над ними операции со знаком; однако во многих случаях тонкие, кажущиеся незначительными изменения заменяли беззнаковую интерпретацию на подписанную, в то время как в других случаях я не мог заставить 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), но этого недостаточно, чтобы интерпретировать их как значения со знаком. Не могли бы вы объяснить, почему эти различия вызывают разницу в подписании? Где я могу найти больше информации об этом? Как я могу использовать более короткую версию первого примера, но при этом получать значения со знаком? Заранее спасибо!
Какой тип возврата у pop()
?
Вы должны конвертировать в signed
при первой же возможности: -1 * ((int32_t)pop() - (int32_t)pop())
Что касается вашего второго примера, вы должны проявлять особую осторожность при выполнении побитовых операций, особенно сдвигов, с операндами подписанных типов. Есть множество способов выстрелить себе в ногу с этим.
Можете ли вы показать нам примеры входных, выходных и ожидаемых выходных данных, которые заставляют вас думать, что значения со знаком представляются как беззнаковые, хотя это не должно быть?
Пожалуйста, разместите полные объявления push
и pop
. В настоящее время опубликованные ответы кажутся спекулятивными.
Я предполагаю, что pop()
возвращает беззнаковый тип. Если это так, выражение pop() - pop()
будет выполняться с использованием беззнаковой арифметики, которая является модульной и переносится, если второе pop()
больше, чем первое (кстати, C не определяет конкретный порядок вычисления, поэтому нет гарантии, какое значение выскочит будет первым или вторым).
В результате значение, которое вы умножаете на -1
, может не соответствовать ожидаемой вами разнице; если бы был перенос, это могло бы быть большое положительное значение, а не отрицательное значение.
Вы можете получить эквивалент временных объектов, если вызовете хотя бы один вызов функции напрямую.
push(-1 * ((int32_t)pop() - pop()));
Я отредактировал вопрос, pop() возвращает подписанный int32_t. Я не знал, что C не определяет порядок оценки, спасибо, что указали на это! Проблема в этом случае, вероятно, заключалась в неправильном порядке вычисления двух вызовов pop(), потому что результат был обратным ожидаемому. Тогда это не было связано с подписанностью и умножением. К сожалению, насколько я знаю, я могу принять только один ответ, но я очень благодарен за помощь!
Тип результата выражения в C определяется типами составных операндов этого выражения, а не каким-либо приведением, которое вы можете применить к этому результату. Как Бармар комментирует выше, чтобы принудительно указать тип результата, вы должны привести один из операндов.
Это то, что меня смутило, я отредактировал вопрос, pop() возвращает значение со знаком, поэтому приведение его к значению со знаком не имеет значения.
если вы просто хотите преобразовать двоичный буфер в более длинные целые числа со знаком, например, где-то полученную форму (я предполагаю, что прямой порядок байтов)
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 });
Чтобы помочь вам увидеть, что происходит в вашем коде, я включил текст стандарта, который объясняет, как выполняются автоматические преобразования типов (для целых чисел), а также раздел о побитовом сдвиге, поскольку он работает немного по-другому. Затем я прохожу ваш код, чтобы точно увидеть, какие промежуточные типы существуют после каждой операции.
- 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.
(Я просто резюмирую соответствующие части здесь.)
(В принципе, если у вас есть a OP b
, размер используемого шрифта будет наибольшим из int
, типа (a), типа (b) и его
предпочтут типы, которые могут представлять все значения, представляемые типом (a) и типом (b). И, наконец, он отдает предпочтение знаковым типам.
В большинстве случаев это означает, что это будет int.)
- 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)
(Обратите внимание, что если аргумент [0] мог быть отрицательным, сдвиг был бы неопределенным поведением. Но, поскольку изначально он был беззнаковым, так что здесь вы в безопасности.)
Пусть a
представляет собой результат этих шагов.
a & (int32_t)0x0000ff00
Пусть b
представляет собой результат этих шагов.
(((int32_t)argument[1]) & (int32_t)0x000000ff)
Пусть c
представляет этот результат.
b | c
Таким образом, ни один из промежуточных результатов здесь не беззнаковый. (Кроме того, большинство явных приведений были ненужными, особенно если sizeof(int) >= sizeof(int32_t)
в вашей системе).
Кроме того, поскольку вы начинаете с uint8_t
s, никогда не выполняете сдвиг более чем на 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;
Пример ввода был 0xfff9, поэтому аргумент [0] = 0xff и аргумент [1] = 0xf9. 16-битная версия моей функции вернула -7, как и ожидалось, но 32-битная версия вернула 65 529, десятичное представление 0xfff9.
@Peter Итак, вы пытались получить отрицательные числа из этих функций? В этом случае важно помнить о разнице между битами, хранящимися в значении, и тем, как эти биты интерпретируются. Существует несколько различных способов представления отрицательных чисел, но во всех старший бит равен 1. 0x0000FFF9 — положительное число; 0xFFF9 — отрицательное число. Но побитовые операции работают только с необработанными битами; они не знают или не заботятся об отрицательных значениях. У вас есть правильные биты; теперь просто приведите значение окончательный к int16_t, и оно будет работать так, как вы хотите.
@Peter То есть проблема не в подписанной/неподписанной части типа; это размер. Я добавил еще один абзац в конец ответа, в котором более подробно объясняется, что вы должны делать.
Это может быть полезно stackoverflow.com/questions/46073295/…