Overflow degli Interi in Linguaggio C

Abbiamo visto nelle lezioni precedenti che i tipi interi in linguaggio C hanno un valore minimo e un valore massimo rappresentabile.

Ma cosa accade se proviamo a superare tali limiti? Questa condizione prende il nome di Overflow e in questo articolo vedremo come il linguaggio C gestisce tale condizione.

Il concetto di Overflow

I processori di qualunque sistema, sia esso un PC, un server, un tablet o uno smartphone, sono in grado di eseguire operazioni matematiche su numeri interi.

In particolare, i processori moderni sono in grado di eseguire operazioni su numeri interi di dimensioni diverse, come ad esempio numeri interi a 8, 16, 32 o 64 bit.

A tali operazioni vi è un limite, che è dato dalla dimensione massima del numero intero che il processore è in grado di rappresentare.

Ad esempio, se il processore è a 16 bit, il numero intero massimo rappresentabile è 2^{16} - 1 = 65535.

Cosa accade quando proviamo a sommare, utilizzando un processore a 16 bit, il numero 65535 con 1?

Tale condizione prende il nome di Overflow. In italiano potremmo tradurre questo termine con straripamento o trabocco, proprio perché il valore numerico trabocca dal limite massimo rappresentabile.

Quello che succede quando si verifica un Overflow dipende dal processore ma in generale il risultato sarà un valore errato, che non corrisponde al risultato corretto dell'operazione.

Spesso, quello che accade è che il processore ricomincia a contare dal valore minimo rappresentabile. Tale valore potrebbe essere 0 o -2^{16}, a seconda se stavamo lavorando con numeri interi con segno o senza segno. Questo comportamento prende il nome di Wraparound che in italiano potremmo tradurre con arrotolamento.

Ritornando all'esempio di sopra, se proviamo a sommare 65535 con 1 otterremo 0.

La motivazione è che se prendiamo il valore binario di 65535, ossia 16 volte 1:

1111111111111111_b

Il risultato corretto dovrebbe essere:

\begin{array}{rc} 1111111111111111_b & + \\ 0000000000000001_b & = \\ \hline {\color{red}{1}}0000000000000000_b \end{array}

ossia un 1 seguito da 16 zeri. Ma il processore non è in grado di rappresentare tale valore e quindi mantiene esclusivamente gli ultimi 16 bit, che sono tutti zeri:

\cancel{\color{red}{1}}0000000000000000_b

L'effetto finale risulta essere come se il processore avesse ricominciato a contare da zero: wrap around.

Ricapitolando:

Definizione

Overflow e Wraparound

Una condizione di Overflow si verifica quando il risultato di un'operazione matematica supera il valore massimo rappresentabile da un processore.

In tal caso il risultato dell'operazione sarà errato e, nella maggior parte dei casi, il processore ricomincerà a contare dal valore minimo rappresentabile. Tale comportamento prende il nome di Wraparound.

In generale, in caso di overflow la maggior parte dei processori segnala, tramite dei meccanismi appositi verso il sistema operativo e il programma stesso, che tale condizione si è verificata. Tuttavia, non tutti i linguaggi di programmazione gestiscono tale segnalazione allo stesso modo.

Per questo motivo, è necessario capire come il linguaggio C si comporta in queste situazioni.

L'Overflow degli interi in Linguaggio C

In caso di overflow di un intero, il linguaggio C assume dei comportamenti che differiscono a seconda che si stia lavorando con numeri interi con segno o senza segno.

Partiamo dal caso di interi senza segno. Analizziamo il seguente programma:

#include <stdio.h>
#include <stdint.h>

int main() {
    uint32_t a = 4294967295;
    a = a + 1;

    printf("a = %u\n", a);

    return 0;
}

Il programma dichiara una variabile a di tipo uint32_t, ossia un intero senza segno a 32 bit. Inizializziamo a al valore massimo rappresentabile da un intero a 32 bit, ossia 2^{32} - 1 = 4294967295.

Successivamente, sommiamo a con 1 e stampiamo il risultato.

Se eseguiamo il programma, otterremo:

a = 0

Quello che è accaduto è che il processore in overflow ha effettuato un wraparound restituendo semplicemente 0. Il programma non segnala alcun errore.

Adesso, proviamo a modificare leggermente il programma sommando alla variabile, anziché 1, il valore 10:

#include <stdio.h>
#include <stdint.h>

int main() {
    uint32_t a = 4294967295;
    a = a + 10;

    printf("a = %u\n", a);

    return 0;
}

Se eseguiamo il programma, otterremo:

a = 9

Anche in questo caso, il processore ha effettuato un wraparound, ricominciando a contare da 0. A voler essere formalmente corretti, quello che è accaduto è che, se n è il numero di bit, il processore ha restituito l'operazione modulo 2^n.

Questo è il comportamento dettato dallo standard del C nel caso di overflow di interi senza segno.

Definizione

Overflow di interi senza segno in C

In linguaggio C, in caso di overflow di un intero senza segno, il risultato dell'operazione sarà il valore ottenuto effettuando un wraparound, ossia ricominciando a contare dal valore minimo rappresentabile.

Il risultato sarà pari al risultato dell'operazione modulo 2^n, dove n è il numero di bit dell'intero.

Questo comportamento è garantito dallo standard del C indipendentemente dal processore o compilatore.

Passiamo ora al caso di interi con segno.

In questo caso, lo standard del C non definisce un comportamento predefinito in caso di overflow. Questo significa che il comportamento potrebbe variare da compilatore a compilatore e da processore a processore.

In generale, la maggior parte dei compilatori e processori adotta il comportamento di effettuare un wraparound, come nel caso degli interi senza segno.

A titolo di esempio, proviamo a eseguire il seguente programma su di un processore x86 a 64 bit sotto il sistema operativo Linux:

#include <stdio.h>
#include <stdint.h>

int main() {
    int32_t a = 2147483647;
    a = a + 1;

    printf("a = %d\n", a);

    return 0;
}

Provando ad eseguire il programma, otterremo:

a = -2147483648

Il risultato ottenuto è -2^{31}, ossia il valore minimo rappresentabile da un intero a 32 bit con segno.

Definizione

Overflow di interi con segno in C

In linguaggio C, in caso di overflow di un intero con segno, non è definito un comportamento standard. Il comportamento potrebbe variare da compilatore a compilatore e da processore a processore.

La maggior parte dei compilatori, tuttavia, adotta il comportamento di effettuare un wraparound, come nel caso degli interi senza segno.

In Sintesi

In questo articolo abbiamo visto cosa si intende per Overflow e come il linguaggio C gestisce tale condizione.

In particolare, abbiamo visto che:

  • In caso di overflow di un intero senza segno, il risultato dell'operazione sarà il valore ottenuto effettuando un wraparound, ossia ricominciando a contare dal valore minimo rappresentabile. Il risultato sarà pari al risultato dell'operazione modulo 2^n, dove n è il numero di bit dell'intero.
  • In caso di overflow di un intero con segno, non è definito un comportamento standard. Il comportamento potrebbe variare da compilatore a compilatore e da processore a processore.