Se un numero è troppo grande, passa alla posizione di memoria successiva?


30

Ho rivisto la programmazione in C e ci sono solo un paio di cose che mi danno fastidio.

Prendiamo ad esempio questo codice:

int myArray[5] = {1, 2, 2147483648, 4, 5};
int* ptr = myArray;
int i;
for(i=0; i<5; i++, ptr++)
    printf("\n Element %d holds %d at address %p", i, myArray[i], ptr);

So che un int può contenere un valore massimo di 2.147.483.647 positivi. Quindi superandone uno, "si riversa" sul successivo indirizzo di memoria che fa apparire l'elemento 2 come "-2147483648" a quell'indirizzo? Ma allora non ha davvero senso perché nell'output dice ancora che l'indirizzo successivo contiene il valore 4, quindi 5. Se il numero si fosse trasferito all'indirizzo successivo, allora non cambierebbe il valore memorizzato in quell'indirizzo ?

Ricordo vagamente dalla programmazione in MIPS Assembly e osservando gli indirizzi cambiare i valori durante il programma passo dopo passo che i valori assegnati a quegli indirizzi sarebbero cambiati.

A meno che non mi ricordi in modo errato, ecco un'altra domanda: se il numero assegnato a un indirizzo specifico è più grande del tipo (come in myArray [2]), non influisce sui valori memorizzati nell'indirizzo successivo?

Esempio: abbiamo int myNum = 4 miliardi all'indirizzo 0x10010000. Naturalmente myNum non può memorizzare 4 miliardi, quindi appare come un numero negativo a quell'indirizzo. Nonostante non sia in grado di memorizzare questo numero elevato, non ha alcun effetto sul valore memorizzato all'indirizzo successivo di 0x10010004. Corretta?

Gli indirizzi di memoria hanno spazio sufficiente per contenere determinate dimensioni di numeri / caratteri e se la dimensione supera il limite, verrà rappresentata in modo diverso (come tentare di archiviare 4 miliardi nell'int ma apparirà come un numero negativo) e quindi non ha alcun effetto sui numeri / caratteri memorizzati al prossimo indirizzo.

Scusa se sono andato in mare. Da questo giorno ho avuto una grande scoreggia cerebrale.


10
Potresti essere confuso con sovraccarichi di stringa .
Robbie Dee,

19
Compiti a casa: Modifica una semplice CPU in modo che esso fa fuoriuscita. Vedrai che la logica diventa molto più complessa, tutto per una "caratteristica" che garantirebbe buchi di sicurezza ovunque senza essere utile in primo luogo.
phihag,

4
Se hai bisogno di numeri davvero enormi, è possibile avere una rappresentazione numerica che aumenta la quantità di memoria utilizzata per adattarsi a numeri grandi. Il processore stesso non può farlo, e non è una caratteristica del linguaggio C, ma una libreria può implementarlo: una libreria C comune è la libreria aritmetica a precisione multipla GNU . La libreria deve gestire la memoria per memorizzare i numeri che hanno un costo prestazionale in cima all'aritmetica. Molte lingue hanno questo tipo di cose integrate (che non evita i costi).
Steve314,

1
scrivere un semplice test, non sono un programmatore in C ma qualcosa sulla falsariga di int c = INT.MAXINT; c+=1;e vedere cosa è successo a c.
JonH,

2
@JonH: il problema è l'overflow nel comportamento indefinito. Il compilatore CA può individuare quel codice e dedurre che è un codice non raggiungibile perché trabocca incondizionatamente. Poiché il codice non raggiungibile non ha importanza, può essere eliminato. Risultato finale: nessun codice rimasto.
MSalters il

Risposte:


48

No non lo fa. In C, le variabili hanno un set fisso di indirizzi di memoria con cui lavorare. Se stai lavorando su un sistema con 4 byte intse imposti una intvariabile su 2,147,483,647e poi aggiungi 1, la variabile di solito conterrà -2147483648. (Sulla maggior parte dei sistemi. Il comportamento in realtà non è definito.) Non verranno modificate altre posizioni di memoria.

In sostanza, il compilatore non ti permetterà di assegnare un valore troppo grande per il tipo. Ciò genererà un errore del compilatore. Se lo forzate con un caso, il valore verrà troncato.

Osservato in modo bit a bit, se il tipo può memorizzare solo 8 bit e si tenta di forzare il valore 1010101010101in un caso, si finiranno con gli 8 bit inferiori, oppure 01010101.

Nel tuo esempio, indipendentemente da ciò che fai myArray[2], myArray[3]conterrà "4". Non è possibile "riversarsi". Stai provando a mettere qualcosa che è più di 4 byte, eliminerà tutto dalla fascia alta, lasciando i 4 byte inferiori. Sulla maggior parte dei sistemi, ciò comporterà -2147483648.

Da un punto di vista pratico, vuoi solo assicurarti che ciò non accada mai e poi mai. Questi tipi di overflow spesso causano difetti difficili da risolvere. In altre parole, se pensi che ci sia qualche possibilità che i tuoi valori siano in miliardi, non usare int.


52
Se stai lavorando su un sistema con ints a 4 byte e imposti una variabile int su 2.147.483.647 e poi aggiungi 1, la variabile conterrà -2147483648. => No , è un comportamento indefinito , quindi potrebbe girare in circolo o potrebbe fare qualcos'altro interamente; Ho visto compilatori che ottimizzavano i controlli basati sull'assenza di overflow e ho ottenuto infiniti loop per esempio ...
Matthieu M.

Scusa, sì, hai ragione. Avrei dovuto aggiungere un "solito" lì dentro.
Gort the Robot

@MatthieuM dal punto di vista linguistico , è vero. In termini di esecuzione su un determinato sistema, che è ciò di cui stiamo parlando qui, è un'assurdità assoluta.
Hobbs,

@hobbs: Il problema è che quando i compilatori mangiano il programma a causa del comportamento indefinito, l'esecuzione del programma produrrà effettivamente un comportamento inaspettato, comparabile in effetti alla sovrascrittura della memoria.
Matthieu M.

24

L'overflow di numeri interi con segno è un comportamento indefinito. In questo caso, il programma non è valido. Il compilatore non è tenuto a verificarlo, quindi potrebbe generare un eseguibile che sembra fare qualcosa di ragionevole, ma non è garantito che lo farà.

Tuttavia, l'overflow di numeri interi senza segno è ben definito. Avvolgerà il modulo UINT_MAX + 1. La memoria non occupata dalla variabile non sarà interessata.

Vedi anche https://stackoverflow.com/q/18195715/951890


l'overflow di numeri interi con segno è definito come l'overflow di numeri interi senza segno. se la parola ha $ N $ bit, il limite superiore dell'overflow di numeri interi con segno è a $$ 2 ^ {N-1} -1 $$ (dove si avvolge intorno a $ -2 ^ {N-1} $) mentre il il limite superiore per l'overflow di numeri interi senza segno è pari a $$ 2 ^ N - 1 $$ (dove si avvolge intorno a $ 0 $). stessi meccanismi di addizione e sottrazione, stesse dimensioni dell'intervallo di numeri ($ 2 ^ N $) che possono essere rappresentati. solo un diverso limite di overflow.
robert bristow-johnson,

1
@ robertbristow-johnson: non conforme allo standard C.
Vaughn Cato,

bene, gli standard a volte sono anacronistici. guardando il riferimento SO, c'è un commento che lo colpisce direttamente: "La nota importante qui, tuttavia, è che nel mondo moderno non rimangono architetture che usano qualcosa di diverso dall'aritmetica firmata del complemento di 2. Che gli standard linguistici consentono ancora l'implementazione ad esempio un PDP-1 è un puro manufatto storico. - Andy Ross, 12 agosto 13 alle 20:12 "
robert bristow-johnson,

suppongo che non sia nello standard C, ma suppongo che ci potrebbe essere un'implementazione in cui non viene utilizzata la normale aritmetica binaria int. suppongo che potrebbero usare il codice Gray o BCD o EBCDIC . non so perché qualcuno dovrebbe progettare hardware per fare l'aritmetica con il codice Gray o EBCDIC, ma poi non so perché qualcuno dovrebbe fare unsignedcon il binario e firmare intcon qualcosa di diverso dal complemento di 2.
robert bristow-johnson il

14

Quindi, ci sono due cose qui:

  • il livello linguistico: quali sono le semantiche di C
  • il livello della macchina: quali sono le semantiche dell'assembly / CPU che usi

A livello di lingua:

In C:

  • overflow e underflow sono definiti come modulo aritmetico per numeri interi senza segno, quindi il loro valore "loop"
  • troppo pieno e underflow sono comportamento non definito per firmati interi, quindi tutto può succedere

Per quelli che vorrebbero un esempio "qualunque cosa", ho visto:

for (int i = 0; i >= 0; i++) {
    ...
}

trasformarsi in:

for (int i = 0; true; i++) {
    ...
}

e sì, questa è una trasformazione legittima.

Significa che ci sono effettivamente potenziali rischi di sovrascrivere la memoria in caso di overflow a causa di una strana trasformazione del compilatore.

Nota: su Clang o gcc utilizzare -fsanitize=undefinedin Debug per attivare il disinfettante comportamentale indefinito che si interromperà in caso di underflow / overflow degli interi con segno.

Oppure significa che è possibile sovrascrivere la memoria usando il risultato dell'operazione per indicizzare (non spuntato) in un array. Ciò è purtroppo molto più probabile in assenza di rilevamento di underflow / overflow.

Nota: su Clang o gcc usare -fsanitize=addressin Debug per attivare Address Sanitizer che si interromperà in caso di accesso fuori limite.


A livello di macchina :

Dipende molto dalle istruzioni di assemblaggio e dalla CPU che usi:

  • su x86, ADD utilizzerà il complemento 2 su overflow / underflow e imposta OF (Flag di overflow)
  • sulla futura CPU Mill, ci saranno 4 diverse modalità di overflow per Add:
    • Modulo: modulo a 2 complementi
    • Trap: viene generata una trap, arrestando il calcolo
    • Saturi: il valore viene bloccato al minimo su underflow o max su overflow
    • Doppia larghezza: il risultato viene generato in un registro a doppia larghezza

Si noti che se le cose accadono nei registri o nella memoria, in nessun caso la CPU sovrascrive la memoria in caso di overflow.


Le ultime tre modalità sono firmate? (Non importa per il primo, dato che è un complemento a 2).
Deduplicatore

1
@Deduplicator: secondo Introduzione al modello di programmazione CPU Mill ci sono diversi codici operativi per l'aggiunta firmata e l'aggiunta non firmata; Mi aspetto che entrambi i codici operativi supportino le 4 modalità (e siano in grado di operare su vari bit-width e scalari / vettori). Poi di nuovo, è l'hardware del vapore per ora;)
Matthieu M.

4

Per approfondire la risposta di @ StevenBurnap, la ragione per cui ciò accade è dovuta al modo in cui i computer lavorano a livello di macchina.

L'array è archiviato in memoria (ad es. Nella RAM). Quando viene eseguita un'operazione aritmetica, il valore in memoria viene copiato nei registri di ingresso del circuito che esegue l'aritmetica (ALU: Arithmetic Logic Unit ), l'operazione viene quindi eseguita sui dati nei registri di ingresso, producendo un risultato nel registro di output. Questo risultato viene quindi copiato nuovamente in memoria all'indirizzo corretto in memoria, lasciando intatte le altre aree della memoria.


4

Innanzitutto (assumendo lo standard C99), potresti voler includere <stdint.h>un'intestazione standard e utilizzare alcuni dei tipi qui definiti, in particolare int32_tche è esattamente un uint64_tintero con segno a 32 bit o che è esattamente un numero intero senza segno a 64 bit e così via. Potresti voler utilizzare tipi come int_fast16_tper motivi di prestazioni.

Leggi le altre risposte spiegando che l'aritmetica senza segno non si riversa (o trabocca mai) in posizioni di memoria adiacenti. Fai attenzione al comportamento indefinito in caso di overflow firmato .

Quindi, se hai bisogno di calcolare numeri interi esattamente enormi (ad es. Vuoi calcolare fattoriale di 1000 con tutte le sue 2568 cifre in decimali), vuoi i nati come numeri di precisione arbitrari (o bignum). Gli algoritmi per un'aritmetica bigint efficiente sono molto intelligenti e di solito richiedono l'uso di istruzioni specializzate per la macchina (ad esempio alcuni aggiungono parola con carry, se il tuo processore lo possiede). Quindi consiglio vivamente in tal caso di utilizzare alcune librerie bigint esistenti come GMPlib

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.