Firmato alla conversione senza segno in C - è sempre sicuro?


135

Supponiamo che io abbia il seguente codice C.

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

unsigned int result = u + i;

Quali conversioni implicite stanno accadendo qui e questo codice è sicuro per tutti i valori di ue i? (Sicuro, nel senso che anche se il risultato in questo esempio traboccasse di un numero enorme positivo, potrei riportarlo a un int e ottenere il risultato reale.)

Risposte:


223

Risposta breve

Il tuo iverrà convertito in un numero intero senza segno aggiungendo UINT_MAX + 1, quindi l'aggiunta verrà eseguita con i valori senza segno, risultando in un grande result(a seconda dei valori di ue i).

Risposta lunga

Secondo lo standard C99:

6.3.1.8 Conversioni aritmetiche usuali

  1. Se entrambi gli operandi hanno lo stesso tipo, non è necessaria alcuna ulteriore conversione.
  2. Altrimenti, se entrambi gli operandi hanno tipi interi con segno o entrambi hanno tipi interi senza segno, l'operando con il tipo di rango di conversione intero inferiore viene convertito nel tipo di operando con rango maggiore.
  3. Altrimenti, se l'operando con tipo intero senza segno ha un rango maggiore o uguale al rango del tipo di altro operando, l'operando con tipo intero con segno viene convertito nel tipo di operando con tipo intero senza segno.
  4. Altrimenti, se il tipo di operando con tipo intero con segno può rappresentare tutti i valori del tipo di operando con tipo intero senza segno, l'operando con tipo intero senza segno viene convertito nel tipo di operando con tipo intero con segno.
  5. Altrimenti, entrambi gli operandi vengono convertiti nel tipo intero senza segno corrispondente al tipo di operando con tipo intero con segno.

Nel tuo caso, abbiamo un int ( u) senza segno e int ( ) firmato i. Facendo riferimento a (3) sopra, poiché entrambi gli operandi hanno lo stesso rango, isarà necessario convertirli in un numero intero senza segno.

6.3.1.3 Numeri interi firmati e non firmati

  1. Quando un valore con tipo intero viene convertito in un altro tipo intero diverso da _Bool, se il valore può essere rappresentato dal nuovo tipo, rimane invariato.
  2. Altrimenti, se il nuovo tipo non è firmato, il valore viene convertito aggiungendo o sottraendo ripetutamente uno in più rispetto al valore massimo che può essere rappresentato nel nuovo tipo fino a quando il valore non rientra nell'intervallo del nuovo tipo.
  3. Altrimenti, il nuovo tipo è firmato e il valore non può essere rappresentato in esso; il risultato è definito dall'implementazione o viene generato un segnale definito dall'implementazione.

Ora dobbiamo fare riferimento a (2) sopra. Il tuo iverrà convertito in un valore non firmato aggiungendo UINT_MAX + 1. Quindi il risultato dipenderà da come UINT_MAXè definito sulla tua implementazione. Sarà grande, ma non traboccerà, perché:

6.2.5 (9)

Un calcolo che coinvolge operandi senza segno non può mai traboccare, poiché un risultato che non può essere rappresentato dal tipo intero senza segno risultante viene ridotto del modulo che è uno maggiore del valore più grande che può essere rappresentato dal tipo risultante.

Bonus: conversione aritmetica 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;
}

Puoi usare questo link per provarlo online: https://repl.it/repls/QuickWhimsicalBytes

Bonus: Effetto collaterale di conversione aritmetica

Le regole di conversione aritmetica possono essere utilizzate per ottenere il valore UINT_MAXinizializzando un valore senza segno -1, ovvero:

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

Questo è garantito per essere portatile indipendentemente dalla rappresentazione del numero con segno del sistema a causa delle regole di conversione sopra descritte. Vedi questa domanda SO per ulteriori informazioni: è sicuro usare -1 per impostare tutti i bit su true?


Non capisco perché non possa semplicemente fare un valore assoluto e quindi trattare è come senza segno, proprio come con i numeri positivi?
Jose Salvatierra,

7
@ D.Singh puoi gentilmente indicare le parti sbagliate all'interno della risposta?
Shmil The Cat,

Per la conversione firmata in non firmata, aggiungiamo il valore massimo del valore non firmato (UINT_MAX +1). Allo stesso modo qual è il modo semplice per convertire da unsigned a Sign? Dobbiamo sottrarre il numero dato dal valore massimo (256 in caso di carattere senza segno)? Ad esempio: 140 quando convertito in numero con segno diventa -116. Ma 20 diventa 20 stesso. Quindi qualche trucco facile qui?
Jon Wheelock,


24

La conversione da firmato a non firmato non necessariamente copia o reinterpreta la rappresentazione del valore firmato. Citando lo standard C (C99 6.3.1.3):

Quando un valore con tipo intero viene convertito in un altro tipo intero diverso da _Bool, se il valore può essere rappresentato dal nuovo tipo, rimane invariato.

Altrimenti, se il nuovo tipo non è firmato, il valore viene convertito aggiungendo o sottraendo ripetutamente uno in più rispetto al valore massimo che può essere rappresentato nel nuovo tipo fino a quando il valore non rientra nell'intervallo del nuovo tipo.

Altrimenti, il nuovo tipo è firmato e il valore non può essere rappresentato in esso; il risultato è definito dall'implementazione o viene generato un segnale definito dall'implementazione.

Per la rappresentazione del complemento a due che è quasi universale in questi giorni, le regole corrispondono a reinterpretare i bit. Ma per altre rappresentazioni (segno-e-grandezza o complemento di quelle), l'implementazione in C deve comunque disporre dello stesso risultato, il che significa che la conversione non può semplicemente copiare i bit. Ad esempio, (non firmato) -1 == UINT_MAX, indipendentemente dalla rappresentazione.

In generale, le conversioni in C sono definite per operare su valori, non su rappresentazioni.

Per rispondere alla domanda originale:

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

unsigned int result = u + i;

Il valore di i viene convertito in unsigned int, cedendo UINT_MAX + 1 - 5678. Questo valore viene quindi aggiunto al valore senza segno 1234, cedendoUINT_MAX + 1 - 4444 .

(A differenza dell'overflow senza segno, l'overflow con segno richiama un comportamento indefinito. L'avvolgimento è comune, ma non è garantito dallo standard C - e le ottimizzazioni del compilatore possono causare danni al codice che fa ipotesi ingiustificate.)


5

Facendo riferimento alla Bibbia :

  • L'operazione di aggiunta comporta la conversione di int in un int senza segno.
  • Supponendo che la rappresentazione del complemento a due e tipi di dimensioni uguali, il modello di bit non cambi.
  • La conversione da int senza segno a int con segno dipende dall'implementazione. (Ma probabilmente funziona come ti aspetti sulla maggior parte delle piattaforme in questi giorni.)
  • Le regole sono un po 'più complicate nel caso di combinazione di dimensioni diverse firmate e non firmate.

3

Quando vengono aggiunte una variabile senza segno e una con segno (o qualsiasi operazione binaria) entrambe vengono implicitamente convertite in non firmate, il che in questo caso comporterebbe un risultato enorme.

Quindi è sicuro nel senso che il risultato potrebbe essere enorme e sbagliato, ma non si bloccherà mai.


Non vero. 6.3.1.8 Conversioni aritmetiche usuali Se sommi un int e un carattere senza segno, quest'ultimo viene convertito in int. Se sommi due caratteri non firmati, questi vengono convertiti in int.
2501

3

Durante la conversione da firmato a non firmato ci sono due possibilità. I numeri che erano originariamente positivi rimangono (o sono interpretati come) lo stesso valore. I numeri che erano inizialmente negativi verranno ora interpretati come numeri positivi più grandi.


1

Come precedentemente risposto, è possibile eseguire il cast avanti e indietro tra firmato e non firmato senza problemi. Il caso limite per gli interi con segno è -1 (0xFFFFFFFF). Prova ad aggiungere e sottrarre ciò e scoprirai che puoi tornare indietro e averlo corretto.

Tuttavia, se stai per lanciare avanti e indietro, ti consiglio vivamente di nominare le tue variabili in modo che sia chiaro di che tipo sono, ad esempio:

int iValue, iResult;
unsigned int uValue, uResult;

È troppo facile distrarsi da problemi più importanti e dimenticare quale variabile è di che tipo se vengono nominati senza un suggerimento. Non si desidera eseguire il cast su un unsigned e quindi utilizzarlo come indice di array.


0

Quali conversioni implicite stanno accadendo qui,

sarò convertito in un numero intero senza segno.

e questo codice è sicuro per tutti i valori di te?

Sicuro nel senso di essere ben definito sì (vedi https://stackoverflow.com/a/50632/5083516 ).

Le regole sono scritte in termini standard di difficile lettura, ma essenzialmente qualunque rappresentazione sia stata utilizzata nell'intero con segno, l'intero senza segno conterrà una rappresentazione del complemento di 2 del numero.

Addizione, sottrazione e moltiplicazione funzioneranno correttamente su questi numeri risultando in un altro numero intero senza segno contenente un numero di complemento a due che rappresenta il "risultato reale".

la divisione e il casting in tipi di numeri interi senza segno più grandi avranno risultati ben definiti ma tali risultati non saranno rappresentazioni complementari di 2 del "risultato reale".

(Sicuro, nel senso che anche se il risultato in questo esempio traboccasse di un numero enorme positivo, potrei riportarlo a un int e ottenere il risultato reale.)

Mentre le conversioni da firmato a non firmato sono definite dallo standard, il contrario è definito dall'implementazione sia gcc che msvc definiscono la conversione in modo tale da ottenere il "risultato reale" quando si converte il numero del complemento di un 2 memorizzato in un numero intero senza segno in un intero con segno . Mi aspetto che troverai altri comportamenti solo su sistemi oscuri che non usano il complemento di 2 per numeri interi con segno.

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


-17

Risposte orribili a bizzeffe

Ozgur Ozcitak

Quando si esegue il cast da firmato a non firmato (e viceversa), la rappresentazione interna del numero non cambia. Ciò che cambia è il modo in cui il compilatore interpreta il bit di segno.

Questo è completamente sbagliato.

Mats Fredriksson

Quando vengono aggiunte una variabile senza segno e una con segno (o qualsiasi operazione binaria) entrambe vengono implicitamente convertite in non firmate, il che in questo caso comporterebbe un risultato enorme.

Anche questo è sbagliato. Gli ints senza segno possono essere promossi in ints se hanno la stessa precisione a causa di bit di riempimento nel tipo senza segno.

SMH

L'operazione di aggiunta comporta la conversione di int in un int senza segno.

Sbagliato. Forse lo fa e forse no.

La conversione da int senza segno a int con segno dipende dall'implementazione. (Ma probabilmente funziona come ti aspetti sulla maggior parte delle piattaforme in questi giorni.)

Sbagliato. È un comportamento indefinito se provoca overflow o se il valore viene conservato.

Anonimo

Il valore di i viene convertito in unsigned int ...

Sbagliato. Dipende dalla precisione di un int rispetto a un int senza segno.

Taylor Price

Come precedentemente risposto, è possibile eseguire il cast avanti e indietro tra firmato e non firmato senza problemi.

Sbagliato. Il tentativo di memorizzare un valore al di fuori dell'intervallo di un numero intero con segno comporta un comportamento indefinito.

Ora posso finalmente rispondere alla domanda.

Se la precisione di int è uguale a int senza segno, sarai promosso a un int con segno e otterrai il valore -4444 dall'espressione (u + i). Ora, se tu ed io avessimo altri valori, potresti ottenere un overflow e un comportamento indefinito, ma con quei numeri esatti otterrai -4444 [1] . Questo valore avrà tipo int. Ma stai provando a memorizzare quel valore in un int senza segno in modo che venga poi lanciato in un int senza segno e il valore che il risultato finirà per essere sarebbe (UINT_MAX + 1) - 4444.

Se la precisione di unsigned int è maggiore di quella di un int, l'int int con segno verrà promossa a un unsigned int producendo il valore (UINT_MAX + 1) - 5678 che verrà aggiunto all'altro int unsigned 1234. Dovremmo altri valori, che rendono l'espressione al di fuori dell'intervallo {0..UINT_MAX} il valore (UINT_MAX + 1) verrà aggiunto o sottratto fino a quando il risultato NON rientra nell'intervallo {0..UINT_MAX) e non si verificherà alcun comportamento indefinito .

Cos'è la precisione?

I numeri interi hanno bit di riempimento, bit di segno e bit di valore. Gli interi senza segno non hanno ovviamente un bit di segno. Il carattere senza segno è inoltre garantito per non avere bit di riempimento. Il numero di bit di valori di un numero intero è quanta precisione ha.

[Trabocchetti]

La dimensione della macro della sola macro non può essere utilizzata per determinare la precisione di un numero intero se sono presenti bit di riempimento. E la dimensione di un byte non deve essere un ottetto (otto bit) come definito da C99.

[1] Il trabocco può verificarsi in uno dei due punti. O prima dell'aggiunta (durante la promozione) - quando hai un int senza segno che è troppo grande per rientrare in un int. L'overflow può verificarsi anche dopo l'aggiunta anche se int senza segno rientra nell'intervallo di un int, dopo l'aggiunta il risultato potrebbe comunque traboccare.


6
"Gli ints non firmati possono essere promossi in ints". Non vero. Non si verifica alcuna promozione intera poiché i tipi sono già in classifica> = int. 6.3.1.1: "Il rango di qualsiasi tipo intero senza segno deve essere uguale al rango del tipo intero con segno corrispondente, se presente." e 6.3.1.8: "Altrimenti, se l'operando che ha un numero intero senza segno ha un rango maggiore o uguale al rango del tipo dell'altro operando, allora l'operando con tipo intero con segno viene convertito nel tipo di operando con intero senza segno genere." entrambi garantiscono che intviene convertito unsigned intquando si applicano le normali conversioni aritmetiche.
CB Bailey

1
6.3.1.8 Si verifica solo dopo la promozione di numeri interi. Il paragrafo di apertura dice "Altrimenti, le promozioni intere vengono eseguite su entrambi gli operandi. ALLORA le seguenti regole vengono applicate agli operandi promossi". Quindi leggi le regole di promozione 6.3.1.1 ... "Un oggetto o un'espressione con un tipo intero il cui rango di conversione intero è inferiore o uguale a rango di int e int senza segno" e "Se un int può rappresentare tutti i valori del tipo originale, il valore viene convertito in un int ".
Elite Mx

1
6.3.1.1 La promozione di interi utilizzata per convertire alcuni tipi di numeri interi che non lo sono into unsigned intin uno di quei tipi in cui si prevede qualcosa di tipo unsigned into intè previsto. Il "o uguale" è stato aggiunto in TC2 per consentire tipi enumerati di rango di conversione uguale into unsigned intda convertire in uno di quei tipi. Non è mai stato inteso che la promozione descritta si convertisse tra unsigned inte int. La determinazione del tipo comune tra unsigned inte intè ancora governata dal 6.3.1.8, anche dopo TC2.
CB Bailey

19
Pubblicare risposte sbagliate mentre si criticano le risposte sbagliate degli altri non sembra una buona strategia per mettersi al lavoro ... ;-)
R .. GitHub FERMA AIUTANDO ICE

6
Non sto votando per eliminare poiché questo livello di errore combinato con l'arroganza è troppo divertente
MM
Utilizzando il nostro sito, riconosci di aver letto e compreso le nostre Informativa sui cookie e Informativa sulla privacy.
Licensed under cc by-sa 3.0 with attribution required.