Assinado a conversão não assinada em C - é sempre seguro?


135

Suponha que eu tenha o seguinte código C.

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

Quais conversões implícitas estão acontecendo aqui e esse código é seguro para todos os valores de ue i? (Seguro, no sentido de que, mesmo que o resultado neste exemplo transborde para um número positivo enorme, eu poderia convertê-lo novamente para um int e obter o resultado real.)

Respostas:


223

Resposta curta

Você iserá convertido em um número inteiro não assinado adicionando UINT_MAX + 1, a adição será realizada com os valores não assinados, resultando em um grande result(dependendo dos valores de ue i).

Resposta longa

De acordo com o padrão C99:

6.3.1.8 Conversões aritméticas usuais

  1. Se os dois operandos tiverem o mesmo tipo, nenhuma conversão adicional será necessária.
  2. Caso contrário, se os dois operandos tiverem tipos inteiros assinados ou os tipos inteiros não assinados, o operando com o tipo de classificação de conversão de número menor será convertido no tipo de operando com maior classificação.
  3. Caso contrário, se o operando que possui o tipo inteiro não assinado tiver uma classificação maior ou igual à classificação do tipo do outro operando, o operando com o tipo inteiro assinado será convertido no tipo de operando com o tipo inteiro não assinado.
  4. Caso contrário, se o tipo do operando com o tipo inteiro assinado puder representar todos os valores do tipo do operando com o tipo inteiro não assinado, o operando com o tipo inteiro não assinado será convertido no tipo do operando com o tipo inteiro assinado.
  5. Caso contrário, os dois operandos serão convertidos no tipo inteiro não assinado correspondente ao tipo do operando com o tipo inteiro assinado.

No seu caso, temos um int ( u) não assinado e um int ( ) assinado i. Referindo-se a (3) acima, como os dois operandos têm a mesma classificação, você iprecisará ser convertido em um número inteiro não assinado.

6.3.1.3 Inteiros assinados e não assinados

  1. Quando um valor com o tipo inteiro é convertido em outro tipo de número diferente de _Bool, se o valor puder ser representado pelo novo tipo, ele permanece inalterado.
  2. Caso contrário, se o novo tipo não estiver assinado, o valor será convertido adicionando ou subtraindo repetidamente um mais que o valor máximo que pode ser representado no novo tipo até que o valor esteja no intervalo do novo tipo.
  3. Caso contrário, o novo tipo é assinado e o valor não pode ser representado nele; o resultado é definido pela implementação ou um sinal definido pela implementação é gerado.

Agora precisamos nos referir a (2) acima. Você iserá convertido em um valor não assinado adicionando UINT_MAX + 1. Portanto, o resultado dependerá de como UINT_MAXé definido em sua implementação. Será grande, mas não excederá, porque:

6.2.5 (9)

Uma computação envolvendo operandos não assinados nunca pode exceder, porque um resultado que não pode ser representado pelo tipo inteiro não assinado resultante é reduzido por módulo, o número que é um maior que o maior valor que pode ser representado pelo tipo resultante.

Bônus: Conversão Aritmética Semi-WTF

#include <stdio.h>

int main(void)
{
  unsigned int plus_one = 1;
  int minus_one = -1;

  if(plus_one < minus_one)
    printf("1 < -1");
  else
    printf("boring");

  return 0;
}

Você pode usar este link para tentar isso online: https://repl.it/repls/QuickWhimsicalBytes

Bônus: efeito colateral da conversão aritmética

Regras de conversão aritmética podem ser usadas para obter o valor UINT_MAXinicializando um valor não assinado para -1, ou seja:

unsigned int umax = -1; // umax set to UINT_MAX

É garantido que ele seja portátil, independentemente da representação numérica assinada do sistema, devido às regras de conversão descritas acima. Consulte esta pergunta do SO para obter mais informações: É seguro usar -1 para definir todos os bits como verdadeiros?


Eu não entendo por que ele não pode simplesmente fazer um valor absoluto e então tratar é como não assinado, assim como com números positivos?
23813 Jose Salvatierra

7
@ D.Singh, você pode apontar as partes erradas na resposta?
Shmil The Cat

Para converter assinado em não assinado, adicionamos o valor máximo do valor não assinado (UINT_MAX +1). Da mesma forma, qual é a maneira mais fácil de converter de não assinado para assinado? Precisamos subtrair o número fornecido do valor máximo (256 no caso de caracteres não assinados)? Por exemplo: 140 quando convertido em número assinado passa a -116. Mas 20 se torna 20 em si. Algum truque fácil aqui?
Jon Wheelock


24

A conversão de assinado para não assinado não necessariamente copia ou reinterpreta a representação do valor assinado. Citando o padrão C (C99 6.3.1.3):

Quando um valor com o tipo inteiro é convertido em outro tipo de número diferente de _Bool, se o valor puder ser representado pelo novo tipo, ele permanece inalterado.

Caso contrário, se o novo tipo não estiver assinado, o valor será convertido adicionando ou subtraindo repetidamente um mais que o valor máximo que pode ser representado no novo tipo até que o valor esteja no intervalo do novo tipo.

Caso contrário, o novo tipo é assinado e o valor não pode ser representado nele; o resultado é definido pela implementação ou um sinal definido pela implementação é gerado.

Para a representação de complemento dos dois que é quase universal hoje em dia, as regras correspondem à reinterpretação dos bits. Mas para outras representações (sinal e magnitude ou complemento de um), a implementação C ainda deve providenciar o mesmo resultado, o que significa que a conversão não pode apenas copiar os bits. Por exemplo, (não assinado) -1 == UINT_MAX, independentemente da representação.

Em geral, as conversões em C são definidas para operar em valores, não em representações.

Para responder à pergunta original:

unsigned int u = 1234;
int i = -5678;

unsigned int result = u + i;

O valor de i é convertido em int sem sinal, produzindo UINT_MAX + 1 - 5678. Este valor é então adicionado ao valor não assinado 1234, produzindo UINT_MAX + 1 - 4444.

(Diferentemente do estouro não assinado, o estouro assinado invoca um comportamento indefinido. A envolvência é comum, mas não é garantida pelo padrão C - e as otimizações do compilador podem causar estragos no código que faz suposições injustificadas.)


5

Referindo-se à Bíblia :

  • Sua operação de adição faz com que o int seja convertido em um int não assinado.
  • Supondo representação de complemento de dois e tipos de tamanho igual, o padrão de bits não muda.
  • A conversão de int não assinado em int assinado é dependente da implementação. (Mas provavelmente funciona da maneira que você espera na maioria das plataformas atualmente.)
  • As regras são um pouco mais complicadas no caso de combinar tamanhos assinados e não assinados.

3

Quando uma variável não assinada e uma assinada são adicionadas (ou qualquer operação binária), ambas são implicitamente convertidas em não assinadas, o que, nesse caso, resultaria em um resultado enorme.

Portanto, é seguro no sentido de que o resultado pode ser enorme e errado, mas nunca falha.


Não é verdade. 6.3.1.8 Conversões aritméticas comuns Se você soma um int e um caracter não assinado, o último é convertido em int. Se você soma dois caracteres não assinados, eles são convertidos em int.
2501 5/05

3

Ao converter de assinado para não assinado, há duas possibilidades. Os números originalmente positivos permanecem (ou são interpretados como) o mesmo valor. O número originalmente negativo agora será interpretado como um número positivo maior.


1

Como foi respondido anteriormente, você pode alternar entre assinado e não assinado sem problemas. O caso de borda para números inteiros assinados é -1 (0xFFFFFFFF). Tente adicionar e subtrair isso e você descobrirá que pode rejeitar e fazer com que esteja correto.

No entanto, se você for transmitir de um lado para o outro, eu recomendo fortemente nomear suas variáveis ​​para que fique claro que tipo elas são, por exemplo:

int iValue, iResult;
unsigned int uValue, uResult;

É muito fácil se distrair com questões mais importantes e esquecer qual variável é de que tipo se elas são nomeadas sem uma dica. Você não deseja converter para um sinal não assinado e depois usá-lo como um índice de matriz.


0

Quais conversões implícitas estão acontecendo aqui,

Eu serei convertido em um número inteiro sem sinal.

e esse código é seguro para todos os valores de u e i?

Seguro no sentido de estar bem definido, sim (consulte https://stackoverflow.com/a/50632/5083516 ).

As regras são escritas em fala padrão de difícil leitura, mas essencialmente qualquer que seja a representação usada no número inteiro assinado, o número inteiro não assinado conterá uma representação complementar do número 2.

A adição, subtração e multiplicação funcionarão corretamente nesses números, resultando em outro número inteiro não assinado contendo um número de complemento de dois representando o "resultado real".

divisão e conversão para tipos inteiros não assinados maiores terão resultados bem definidos, mas esses resultados não serão as representações complementares de 2 do "resultado real".

(Seguro, no sentido de que, mesmo que o resultado neste exemplo ultrapasse um número positivo enorme, eu poderia convertê-lo novamente em um int e obter o resultado real.)

Enquanto as conversões de assinado para não assinado são definidas pelo padrão, o reverso é definido pela implementação, tanto gcc quanto msvc definem a conversão de forma que você obtenha o "resultado real" ao converter o número de complemento de 2s armazenado em um número inteiro não assinado em um número inteiro assinado. . Espero que você encontre apenas outro comportamento em sistemas obscuros que não usem o complemento 2 para números inteiros assinados.

https://gcc.gnu.org/onlinedocs/gcc/Integers-implementation.html#Integers-implementation https://msdn.microsoft.com/en-us/library/0eex498h.aspx


-17

Horrible Answers Galore

Ozgur Ozcitak

Quando você transmite de assinado para não assinado (e vice-versa), a representação interna do número não muda. O que muda é como o compilador interpreta o bit de sinal.

Isso está completamente errado.

Mats Fredriksson

Quando uma variável não assinada e uma assinada são adicionadas (ou qualquer operação binária), ambas são implicitamente convertidas em não assinadas, o que, nesse caso, resultaria em um resultado enorme.

Isso também está errado. As entradas não assinadas podem ser promovidas para entradas, se tiverem precisão igual devido aos bits de preenchimento no tipo não assinado.

smh

Sua operação de adição faz com que o int seja convertido em um int não assinado.

Errado. Talvez sim e talvez não.

A conversão de int não assinado em int assinado é dependente da implementação. (Mas provavelmente funciona da maneira que você espera na maioria das plataformas atualmente.)

Errado. É um comportamento indefinido se causar excesso ou o valor for preservado.

Anônimo

O valor de i é convertido em int não assinado ...

Errado. Depende da precisão de um int em relação a um int não assinado.

Preço de Taylor

Como foi respondido anteriormente, você pode alternar entre assinado e não assinado sem problemas.

Errado. Tentar armazenar um valor fora do intervalo de um número inteiro assinado resulta em um comportamento indefinido.

Agora posso finalmente responder à pergunta.

Se a precisão de int for igual a int sem sinal, u será promovido a um int assinado e você obterá o valor -4444 da expressão (u + i). Agora, se eu e eu tivermos outros valores, você pode ter um comportamento indefinido e indefinido, mas com esses números exatos você obterá -4444 [1] . Este valor terá o tipo int. Mas você está tentando armazenar esse valor em um int não assinado, para que seja convertido em um int não assinado e o valor que o resultado acabará tendo (UINT_MAX + 1) - 4444.

Se a precisão de unsigned int for maior que a de um int, o int assinado será promovido a um int não assinado, produzindo o valor (UINT_MAX + 1) - 5678 que será adicionado ao outro int não assinado 1234. Caso eu e eu tenhamos outros valores, que fazem a expressão ficar fora do intervalo {0..UINT_MAX}, o valor (UINT_MAX + 1) será adicionado ou subtraído até que o resultado fique dentro do intervalo {0..UINT_MAX) e nenhum comportamento indefinido ocorrerá .

O que é precisão?

Os números inteiros têm bits de preenchimento, bits de sinal e bits de valor. Inteiros não assinados não têm um sinal de bit, obviamente. Caracteres não assinados têm ainda a garantia de não ter bits de preenchimento. O número de bits de valores que um número inteiro tem é quanta precisão ele tem.

[Pegadinhas]

O tamanho macro da macro sozinho não pode ser usado para determinar a precisão de um número inteiro se houver bits de preenchimento. E o tamanho de um byte não precisa ser um octeto (oito bits), conforme definido por C99.

[1] O estouro pode ocorrer em um dos dois pontos. Antes da adição (durante a promoção) - quando você tem um int não assinado que é muito grande para caber dentro de um int. O estouro também pode ocorrer após a adição, mesmo que o int não assinado esteja dentro do intervalo de um int, após a adição o resultado ainda pode estourar.


6
"Ints não assinados podem ser promovidos a ints". Não é verdade. Nenhuma promoção inteira ocorre porque os tipos já estão na classificação> = int. 6.3.1.1: "A classificação de qualquer tipo de número inteiro não assinado deve ser igual à classificação do tipo de número inteiro assinado correspondente, se houver." e 6.3.1.8: "Caso contrário, se o operando que possui um número inteiro não assinado tiver uma classificação maior ou igual à classificação do tipo do outro operando, o operando com o tipo inteiro assinado será convertido no tipo de operando com um número inteiro não assinado. tipo." ambos garantem a intconversão para unsigned intquando as conversões aritméticas comuns se aplicam.
CB Bailey

1
6.3.1.8 Ocorre somente após a promoção inteira. O parágrafo de abertura diz "Caso contrário, as promoções inteiras são realizadas nos dois operandos. ENTÃO, as seguintes regras são aplicadas aos operandos promovidos". Portanto, leia as regras de promoção 6.3.1.1 ... "Um objeto ou expressão com um tipo inteiro cuja classificação de conversão de número inteiro seja menor ou igual à classificação de int e int não assinado" e "Se um int puder representar todos os valores de tipo original, o valor é convertido em um int ".
Elite Mx

1
6.3.1.1 Promoção de número inteiro usada para converter alguns tipos de números inteiros que não são intou unsigned intpara um desses tipos em que algo do tipo unsigned intou inté esperado. O "ou igual" foi adicionado no TC2 para permitir que tipos enumerados de classificação de conversão sejam iguais intou unsigned intsejam convertidos em um desses tipos. Nunca se pretendeu que a promoção descrita se convertesse entre unsigned inte int. A determinação de tipo comum entre unsigned inte intainda é governada por 6.3.1.8, mesmo após o TC2.
CB Bailey

19
Publicação de respostas erradas, enquanto respostas erradas criticando dos outros não soa como uma estratégia boa para começar o trabalho ... ;-)
R .. GitHub parar de ajudar ICE

6
Eu não estou votando para apagar como este nível de incorreção combinado com arrogância é muito divertido
MM
Ao utilizar nosso site, você reconhece que leu e compreendeu nossa Política de Cookies e nossa Política de Privacidade.
Licensed under cc by-sa 3.0 with attribution required.