Skip to content

Perché il lungo 2147483647 + 1 = -2147483648?

Questa domanda può essere affrontata in diversi modi, tuttavia, a nostro avviso, ti diamo la soluzione più completa.

Soluzione:

2147483647 + 1 è valutato come la somma di due ints e quindi trabocca.

2147483648 è troppo grande per essere inserito in un int e quindi viene assunto dal compilatore come un long (o un long long in MSVC). Pertanto non va in overflow.

Per eseguire la somma come un long long utilizzare il suffisso costante appropriato, ad es.

a = 2147483647LL + 1;

Questo overflow del numero intero firmato è un comportamento non definito, come sempre in C/C++.

Ciò che ogni programmatore C dovrebbe sapere sul comportamento non definito

A meno che non si compili con gcc -fwrapv o equivalente per rendere l'overflow dei numeri interi firmati ben definito come un avvolgimento del complemento a 2. Con gcc -fwrapv o qualsiasi altra implementazione che definisca l'overflow degli interi = wraparound, il wrapping che vi è capitato di vedere in pratica è ben definito e deriva da altre regole ISO C per i tipi di letterali interi e per la valutazione delle espressioni.

T var = expression converte solo implicitamente l'espressione nel tipo Tdopo valutando l'espressione secondo le regole standard. Come (T)(expression), non come (int64_t)2147483647 + (int64_t)1.

Un compilatore potrebbe aver scelto di assumere che questo percorso di esecuzione non venga mai raggiunto ed emettere un'istruzione illegale o qualcosa del genere. L'implementazione del complemento a 2 in caso di overflow nelle espressioni costanti è solo una scelta che alcuni o la maggior parte dei compilatori fanno.


Lo standard ISO C specifica che un letterale numerico ha il tipo int a meno che il valore non sia troppo grande per adattarsi (può essere lungo o lungo lungo, o senza segno per l'esadecimale), o se viene utilizzato un override di dimensione. A quel punto si applicano le solite regole di promozione dei numeri interi per gli operatori binari come + e *indipendentemente dal fatto che faccia parte o meno di un'espressione costante in tempo di compilazione.

Questa è una regola semplice e coerente, facile da implementare per i compilatori, anche agli albori del C, quando i compilatori dovevano funzionare su macchine limitate.

Così in ISO C/C++ 2147483647 + 1 è comportamento non definito su implementazioni con 32-bit int. Trattarlo come int (e quindi avvolgere il valore in negativo firmato) segue naturalmente le regole ISO C per il tipo di espressione che dovrebbe avere e dalle normali regole di valutazione per il caso di non overflow. I compilatori attuali non scelgono di definire un comportamento diverso da questo.

ISO C/C++ lo lasciano indefinito, quindi un'implementazione potrebbe scegliere letteralmente qualsiasi cosa (compresi i demoni nasali) senza violare gli standard C/C++. In pratica questo comportamento (wrap + warn) è uno dei meno discutibili e deriva dal trattare l'overflow di un intero firmato come un wrapping, che è ciò che spesso accade in pratica a runtime.

Inoltre, alcuni compilatori dispongono di opzioni per definire questo comportamento ufficialmente per tutti i casi, non solo per le espressioni costanti in tempo di compilazione. (gcc -fwrapv).


I compilatori avvertono di questo

I buoni compilatori avvertono di molte forme di UB quando sono visibili in fase di compilazione, compreso questo. GCC e clang avvertono anche senza -Wall. Da l'esploratore di compilatori Godbolt:

  clang
:5:20: warning: overflow in expression; result is -2147483648 with type 'int' [-Winteger-overflow]
    a = 2147483647 + 1;
                   ^
  gcc
: In function 'void foo()':
:5:20: warning: integer overflow in expression of type 'int' results in '-2147483648' [-Woverflow]
    5 |     a = 2147483647 + 1;
      |         ~~~~~~~~~~~^~~

GCC ha questo avviso abilitato per impostazione predefinita almeno da GCC4.1 nel 2006 (la versione più vecchia su Godbolt), e clang dalla 3.3.

MSVC avverte solo con-Wallche per MSVC è insolitamente prolisso la maggior parte delle volte, ad es. stdio.h produce tonnellate di avvertimenti come 'vfwprintf': unreferenced inline function has been removed. L'avviso di MSVC è simile a:

  MSVC -Wall
(5): warning C4307: '+': signed integral constant overflow

@HumanJHawkins ha chiesto perché è stato progettato in questo modo:

Per me, questa domanda sta chiedendo, perché il compilatore non usa anche il tipo di dati più piccolo in cui il risultato di un'operazione matematica si adatta? Con i letterali interi, sarebbe possibile sapere in fase di compilazione che si sta verificando un errore di overflow. Ma il compilatore non si preoccupa di saperlo e di gestirlo. Perché?

"Non si preoccupa di gestirlo" è un po' forte; i compilatori rilevano l'overflow e lo segnalano. Ma seguono le regole ISO C che dicono int + int ha il tipo inte che i letterali numerici hanno ciascuno il tipo int. I compilatori scelgono semplicemente di proposito di andare a capo invece di allargare e dare all'espressione un tipo diverso da quello che ci si aspetterebbe. (Invece di rinunciare completamente a causa dell'UB).

L'avvolgimento è comune quando l'overflow firmato avviene a tempo di esecuzione, anche se nei loop i compilatori ottimizzano in modo aggressivo int i / array[i] per evitare di ripetere l'estensione del segno a ogni iterazione.

L'allargamento porterebbe con sé una serie di insidie (più piccole), come ad esempio printf("%d %dn", 2147483647 + 1, 2147483647); che ha un comportamento non definito (e in pratica fallisce su macchine a 32 bit) a causa di una mancata corrispondenza di tipo con la stringa di formato. Se 2147483647 + 1 viene implicitamente promosso a long longsarebbe necessario un elemento %lld per il formato della stringa. (E si romperebbe nella pratica, perché un int a 64 bit viene tipicamente passato in due slot di arg-passing su una macchina a 32 bit, quindi il secondo parametro %d probabilmente vedrebbe la seconda metà della prima long long.)

Per essere onesti, questo è già un problema per -2147483648. Come espressione nel sorgente C/C++ ha il tipo long oppure long long. Viene analizzata come 2147483648 separatamente dall'unario - e 2147483648 non si inserisce in un operatore firmato a 32 bit int. Pertanto, viene utilizzato il tipo successivo più grande che può rappresentare il valore.

Tuttavia, qualsiasi programma interessato da questo ampliamento avrebbe avuto UB (e probabilmente il wrapping) senza di esso, ed è più probabile che l'ampliamento faccia funzionare il codice. C'è un problema di filosofia progettuale qui: troppi strati di "capita che funzioni" e comportamenti indulgenti rendono difficile capire esattamente perché una cosa fa e difficile verificare che sia trasferibile ad altre implementazioni con altre larghezze di tipo. A differenza di linguaggi "sicuri" come Java, il C è molto insicuro e ha diverse implementazioni definite su piattaforme diverse, ma molti sviluppatori hanno solo un'implementazione su cui fare i test. (Soprattutto prima di Internet e dei test online di integrazione continua).


L'ISO C non definisce il comportamento, quindi sì un compilatore potrebbe definire un nuovo comportamento come estensione senza rompere la compatibilità con i programmi UB-free. Ma a meno che ogni non lo supportasse, non si potrebbe usare in programmi C portatili. Potrei immaginarlo come un'estensione GNU supportata almeno da gcc/clang/ICC.

Inoltre, un'opzione di questo tipo sarebbe in qualche modo in conflitto con -fwrapv che definisce il comportamento. Nel complesso, penso che sia improbabile che venga adottata, perché c'è una sintassi conveniente per specificare il tipo di un letterale (0x7fffffffUL + 1 fornisce un unsigned long che è garantito essere abbastanza ampio per quel valore come un intero senza segno a 32 bit).

Ma consideriamo questa come una scelta per il C in primo luogo, invece del design attuale.

Un progetto possibile sarebbe quello di dedurre il tipo di un'espressione costante intera dal suo valore, calcolato con precisione arbitraria. Perché precisione arbitraria invece di long long o unsigned long long? Potrebbero non essere abbastanza grandi per le parti intermedie dell'espressione se il valore finale è piccolo a causa di /, >>, -o & operatori.

Oppure un design più semplice, come il preprocessore C, in cui le espressioni intere costanti vengono valutate a una larghezza fissa definita dall'implementazione, come almeno 64 bit. (Ma poi assegnare un tipo in base al valore finale, o in base al valore temporaneo più ampio in un'espressione)? Ma questo ha l'ovvio svantaggio, per i primi C su macchine a 16 bit, di rendere più lenta la valutazione delle espressioni a tempo di compilazione rispetto al caso in cui il compilatore possa utilizzare internamente l'ampiezza nativa degli interi della macchina per int per le espressioni.

Le espressioni costanti integrali sono già un po' speciali in C, e in alcuni contesti devono essere valutate a tempo di compilazione., ad esempio per static int array[1024 * 1024 * 1024]; (dove le moltiplicazioni traboccano nelle implementazioni con int. a 16 bit).

Ovviamente non possiamo estendere in modo efficiente la regola di promozione alle espressioni non costanti; se (a*b)/c potrebbe dover valutare a*b come long long invece di int su una macchina a 32 bit, la divisione richiederà una precisione estesa. (Per esempio, l'istruzione di divisione 64-bit / 32-bit => 32-bit di x86 fallisce in caso di overflow del quoziente invece di troncare silenziosamente il risultato, quindi anche assegnando il risultato a un'istruzione int non permetterebbe al compilatore di ottimizzare bene in alcuni casi).

Inoltre, vogliamo davvero il comportamento/la definizione di a * b dipenda dal fatto che a e b sono static const o no? In generale, avere regole di valutazione in tempo di compilazione che corrispondano alle regole per le espressioni non costanti sembra una buona cosa, anche se lascia queste brutte insidie. Ma ancora una volta, questo è qualcosa di cui i buoni compilatori possono avvertire nelle espressioni costanti.


Altri casi più comuni di questo problema del C sono cose come 1<<40 invece di 1ULL << 40 per definire un flag di bit, o scrivere 1T come 1024*1024*1024*1024.

Bella domanda. Come hanno detto altri, i numeri per impostazione predefinita sono intquindi la vostra operazione per a agisce su due inte va in overflow. Ho provato a riprodurlo e a estendere un po' il cast del numero in long long e poi aggiungere il parametro 1 ad essa, in quanto la variabile c come nell'esempio seguente:

$ cat test.c 
#include 
#include 
#include 

void main() {
  long long a, b, c;

  a = 2147483647 + 1;
  b = 2147483648;

  c = 2147483647;
  c = c + 1;

  printf("%lldn", a);
  printf("%lldn", b);
  printf("%lldn", c);
}

Il compilatore avverte dell'overflow BTW, e normalmente si dovrebbe compilare il codice di produzione con -Werror -Wall per evitare inconvenienti come questo:

$ gcc -m64 test.c -o test
test.c: In function 'main':
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^

Infine, i risultati del test sono quelli attesi (int overflow nel primo caso, long long intnel secondo e nel terzo):

$ ./test 
-2147483648
2147483648
2147483648

Un'altra versione di gcc avverte ancora di più:

test.c: In function ‘main’:
test.c:8:16: warning: integer overflow in expression [-Woverflow]
 a = 2147483647 + 1;
                ^
test.c:9:1: warning: this decimal constant is unsigned only in ISO C90
 b = 2147483648;
 ^

Si noti anche che tecnicamente int e long e le loro variazioni dipendono dall'architettura, quindi la loro lunghezza in bit può variare.
Per i tipi di dimensioni prevedibili è meglio utilizzare il metodo int64_t, uint32_t e così via, comunemente definiti nei moderni compilatori e nelle intestazioni di sistema, in modo che, a prescindere dalla bitness per cui è stata costruita l'applicazione, i tipi di dati rimangano prevedibili. Si noti anche che la stampa e la scansione di tali valori è aggravata da macro come PRIu64 ecc.

valutazioni e recensioni

Ricordati di dare visibilità a questo scritto se per te ne è valsa la pena.



Utilizzate il nostro motore di ricerca

Ricerca
Generic filters

Lascia un commento

Il tuo indirizzo email non sarà pubblicato.