Tipi Interi in Linguaggio C

In questa guida sul linguaggio C abbiamo, fino ad ora, utilizzato soltanto due tipi di dato di base (built-in): int e float.

Adesso è il momento di approfondire il catalogo dei tipi di dato di base del linguaggio C. In questa lezione, in particolare, inizieremo a studiare tipi numerici interi.

Tipi Interi

Il linguaggio C supporta due differenti gruppi di tipi numerici: i tipi interi e i tipi in virgola mobile chiamati anche tipi floating point.

I valori di tipo intero rappresentano, appunto, numeri interi privi di parte frazionaria. I tipi in virgola mobile, invece, consentono di rappresentare numeri dotati anche di parte frazionaria.

A loro volta, i tipi interi si suddividono in due categorie: i tipi interi con segno e i tipi interi senza segno chiamati, ripettivamente, interi signed e interi unsigned.

Definizione

Tipi Interi

In Linguaggio C i tipi interi vengono utilizzati per rappresentare numeri interi privi di parte frazionaria. I tipi interi si suddividono in due categorie: i tipi interi con segno e i tipi interi senza segno.

Interi con segno e senza segno

Per comprendere appieno la differenza tra i tipi interi con segno e senza segno, è necessario capire come vengono rappresentati i numeri in memoria.

Senza entrare troppo nel dettaglio, basta sapere che i numeri interi con segno vengono rappresentati riservando il bit più significativo, ossia il bit più a sinistra (lefmost bit) per il segno. Questo significa che un numero intero con segno può assumere valori sia positivi che negativi.

Ad esempio, se per rappresentare un intero usiamo 16 bit, il bit più a sinistra sarà riservato per il segno, mentre i restanti 15 bit saranno utilizzati per rappresentare il valore numerico.

Se prendiamo il seguente numero binario a 16 bit:

0111111111111111_b

Questo numero rappresenta il valore decimale 32767 ossia 2^{15} - 1. Inoltre, questo valore costituisce il massimo numero intero con segno che è possibile rappresentare con 16 bit.

Analogamente, se avessimo usato 8 bit per rappresentare un numero intero con segno, il massimo valore rappresentabile sarebbe 127, ossia 2^7 - 1:

01111111_b

Non entriamo, qui, nel merito di come siano rappresentati i numeri in sistema binario, ma è importante comprendere che i numeri interi con segno possono assumere valori sia positivi che negativi.

Quando, invece, abbiamo a che fare con numeri interi senza segno, il bit più a sinistra non è riservato per il segno, ma viene utilizzato per rappresentare il valore numerico insieme ai bit restanti. Questo significa che i numeri interi senza segno possono assumere solo valori positivi, incluso lo zero.

Ad esempio, se prendiamo il numero a 16 bit composto da soli 1:

1111111111111111_b

Questo numero rappresenta il valore decimale 65535, ossia 2^{16} - 1. Inoltre, questo valore costituisce il massimo numero intero senza segno che è possibile rappresentare con 16 bit.

Viceversa, il numero minimo senza segno che possiamo rappresentare con 16 bit è, giustamente, 0:

0000000000000000_b
Definizione

Interi con segno e senza segno

In Linguaggio C, i tipi interi si suddividono in due categorie: i tipi interi con segno e i tipi interi senza segno.

  • I tipi interi con segno (signed) possono rappresentare valori positivi e negativi. Il bit più a sinistra è riservato per il segno.
  • I tipi interi senza segno (unsigned) possono rappresentare solo valori positivi, zero incluso. Il bit più a sinistra è utilizzato per rappresentare il valore numerico.

In generale, la formula per determinare l'intervallo di rappresentazione dei numeri interi può essere espressa come segue:

  • Se abbiamo n bit a disposizione, l'intervallo di rappresentazione dei numeri interi con segno sarà:
\left[ -2^{n-1}, 2^{n-1} - 1 \right]
  • Mentre l'intervallo di rappresentazione dei numeri interi senza segno sarà:
\left[ 0, 2^n - 1 \right]

Di default, il linguaggio C considera tutti gli interi come interi con segno. Per dichiarare un intero senza segno, è necessario utilizzare il qualificatore unsigned.

La sintassi per specificare un intero senza segno consiste nel preporre la parola chiave unsigned al tipo di dato. Quindi, ad esempio, possiamo scrivere:

unsigned int x;

Analogamente, per specificare che si tratta di un intero con segno si adopera il qualificatore signed. Tuttavia, poiché tutti gli interi sono considerati di default come interi con segno, l'uso di signed è superfluo.

Definizione

signed e unsigned

In Linguaggio C, per specificare che un intero è senza segno si utilizza il qualificatore unsigned. Per specificare che un intero è con segno si può utilizzare il qualificatore signed, ma esso è superfluo poiché tutti gli interi sono considerati di default come interi con segno.

La sintassi è la seguente:

/* Intero senza segno */
unsigned int x;

/* Intero con segno */
signed int x;

Sia signed che unsigned sono parole chiave riservate del linguaggio C e non possono essere utilizzate come identificatori.

Ad esempio, le due scritture seguenti sono equivalenti:

/* Equivalenti */
signed int y;
int y;

I tipi interi senza segno sono spesso utilizzati per lavorare a basso livello, ossia per interagire con l'hardware della macchina o per scrivere programmi di sistema. Altri tipi di applicazione possono essere, ad esempio, la rappresentazione di valori che non possono essere negativi, come l'età di una persona o il numero di elementi in un array.

Man mano vedremo, nel corso di questa guida, quando conviene adoperare un tipo di dato piuttosto che un altro.

Dimensione dei tipi interi

Il linguaggio C mette a disposizione differenti tipi interi con dimensioni diverse. Tipicamente, il tipo int è un intero a 32 bit, ma non sempre ciò è vero.

La verità è che la dimensione di int dipende da due fattori:

  • L'architettura della macchina su cui il programma viene eseguito;
  • Il compilatore utilizzato per compilare il programma.

Tipicamente, su processori a 32 bit, il tipo intero int è a 32 bit, mentre su processori a 64 bit, int è a 64 bit. Tuttavia, non è garantito che ciò sia vero in tutti i casi.

Il tipo int non è l'unico tipo intero. Il linguaggio C mette a disposizione anche il tipo long che permette di memorizzare numeri molto grandi, ed il tipo short che può essere adoperato quando dobbiamo salvare spazio, ossia quando dobbiamo memorizzare un numero in un numero di bit inferiore.

Per costruire un intero della dimensione desiderata si possono combinare questi qualificatori con il tipo int ottenendo, di fatto, sei possibili combinazioni:

/* Short Integer con segno */
short int a;

/* Short Integer senza segno */
unsigned short int b;

/* Integer con segno */
int c;

/* Integer senza segno */
unsigned int d;

/* Long Integer con segno */
long int e;

/* Long Integer senza segno */
unsigned long int f;

La sintassi del C permette, inoltre, di omettere la parola chiave int quando si dichiara un intero. Quindi possiamo scrivere anche:

/* Equivalenti */
short a;
unsigned short b;
int c;
unsigned d;
long e;
unsigned long f;

Questa pratica di omettere int è molto comune nei programmi scritti in C. Essa è talmente comune che nei linguaggi con sintassi derivata dal C, come Java e C#, per dichiarare un intero short o long bisogna omettere int.

Per questo motivo, in questa guidai adotteremo la pratica di omettere int quando dichiariamo un intero short, long o unsigned.

Come abbiamo detto, la dimensione di un tipo intero dipende dall'architettura della macchina e dal compilatore. Tuttavia, esiste una serie di regole che tutti i compilatori C devono rispettare:

  • Sebbene i tipi interi possono avere dimensioni diverse, il compilatore deve garantire che essi abbiano una dimensione minima e una dimensione massima:

    • Un intero short deve avere almeno 16 bit;
    • Un intero int deve avere almeno 16 bit;
    • Un intero long deve avere almeno 32 bit.
  • La dimensione di un intero short deve essere minore o uguale a quella di un intero int, che a sua volta deve essere minore o uguale a quella di un intero long.

    La conseguenza è che un int potrebbe avere la stessa dimensione di uno short, e un long potrebbe avere la stessa dimensione di un int. Ma non deve mai accadere che un short abbia una dimensione maggiore di un int, o che un int abbia una dimensione maggiore di un long:

    \text{short} \leq \text{int} \leq \text{long}
Definizione

Dimensione degli interi in linguaggio C

Lo standard del linguaggio C non impone una dimensione specifica per i tipi interi. La dimensione di un tipo intero dipende dall'architettura della macchina e dal compilatore utilizzato.

Tuttavia lo standard impone che:

  • Un intero short deve avere almeno 16 bit;
  • Un intero int deve avere almeno 16 bit;
  • Un intero long deve avere almeno 32 bit.

Inoltre, la dimensione di un intero short deve essere minore o uguale a quella di un intero int, che a sua volta deve essere minore o uguale a quella di un intero long.

In generale, ci si può aspettare delle dimensioni tipiche a seconda dell'architettura della macchina.

Nella tabella che segue, mostriamo le dimensioni tipiche degli interi per un processore a 16 bit:

Tipo Dimensione Valore Minimo Valore Massimo
short 16 bit -2^{15} = -32768 2^{15} - 1 = 32767
unsigned short 16 bit 0 2^{16} - 1 = 65535
int 16 bit -2^{16} = -32768 2^{16} - 1 = 32767
unsigned int 16 bit 0 2^{16} - 1 = 65535
long 32 bit -2^{31} = -2147483648 2^{31} - 1 = 2147483647
unsigned long 32 bit 0 2^{32} - 1 = 4294967295
Tabella 1: Dimensione tipica degli interi per un processore a 16 bit

La prossima tabella mostra, invece, le dimensioni tipiche degli interi per un processore a 32 bit:

Tipo Dimensione Valore Minimo Valore Massimo
short 16 bit -2^{15} = -32768 2^{15} - 1 = 32767
unsigned short 16 bit 0 2^{16} - 1 = 65535
int 32 bit -2^{31} = -2147483648 2^{31} - 1 = 2147483647
unsigned int 32 bit 0 2^{32} - 1 = 4294967295
long 32 bit -2^{31} = -2147483648 2^{31} - 1 = 2147483647
unsigned long 32 bit 0 2^{32} - 1 = 4294967295
Tabella 2: Dimensione tipica degli interi per un processore a 32 bit

Infine, la tabella seguente mostra le dimensioni tipiche degli interi per un processore a 64 bit:

Tipo Dimensione Valore Minimo Valore Massimo
short 16 bit -2^{15} = -32768 2^{15} - 1 = 32767
unsigned short 16 bit 0 2^{16} - 1 = 65535
int 32 bit -2^{31} = -2147483648 2^{31} - 1 = 2147483647
unsigned int 32 bit 0 2^{32} - 1 = 4294967295
long 64 bit -2^{63} \approx -9 \cdot 10 ^ {-18} 2^{63} - 1 \approx 9 \cdot 10 ^ {18}
unsigned long 64 bit 0 2^{64} - 1 \approx 1.8 \cdot 10 ^ {19}
Tabella 3: Dimensione tipica degli interi per un processore a 64 bit

Anche se abbiamo riportato delle tabelle che mostrano la dimensione tipica degli interi a seconda dell'architettura del processore, è bene ricordare che le dimensioni riportate non sono mandatorie. Ciò vuol dire che non necessariamente un intero int su un processore a 32 bit sarà a 32 bit. Tuttavia, è molto probabile che ciò accada.

Esistono vari modi per determinare la dimensione di un tipo intero su una specifica architettura. Li vedremo nelle prossime lezioni.

Interi nello standard C99

Lo standard del C99 fornisce due nuovi tipi di intero: long long int e unsigned long long int.

Questi tipi furono aggiunti per andare incontro alle esigenze, sempre crescenti, di poter rappresentare e manipolare valori numerici sempre più grandi sui nuovi processori a 64 bit.

In base a quanto asserisce lo standard C99, una variabile di tipo long long int o di tipo unsigned long long int deve avere almeno 64 bit. Per cui, in base a ciò, siamo sicuri che tali tipi avranno le seguenti caratteristiche, indipendentemente dal compilatore o dall'architettura della macchina:

Tipo Dimensione Valore Minimo Tipico Valore Massimo Tipico
long long int 64 bit -2^{63} \approx -9 \cdot 10 ^ {-18} 2^{63} - 1 \approx 9 \cdot 10 ^ {18}
unsigned long long int 64 bit 0 2^{64} - 1 \approx 1.8 \cdot 10 ^ {19}
Tabella 4: Dimensione tipica degli interi long long

Ovviamente, a seconda dell'implementazione del compilatore, i tipi long long int e unsigned long long int potrebbero avere dimensioni maggiori di 64 bit.

In generale, nello standard C99 i tipi short, int, long e long long prendono il nome di tipi interi standard con segno (standard signed integer types).

Viceversa, i tipi unsigned short, unsigned int, unsigned long e unsigned long long prendono il nome di tipi interi standard senza segno (standard unsigned integer types).

Questi nomi sono stati scelti per differenziarli dai tipi interi estesi (extended integer types), che sono tipi interi che possono avere dimensioni maggiori di quelli standard e che i vari compilatori possono implementare a loro discrezione.

Definizione

Tipi interi long long in C99

Nello standard C99 sono stati introdotti due nuovi tipi interi: long long int e unsigned long long int.

In base allo standard, le variabili long long e le variabili unsigned long long devono utilizzare almeno 64 bit.

Tipi interi a dimensione nota

Per ovviare al problema della dimensione variabile dei tipi interi, lo standard C99 ha introdotto una serie di tipi interi a dimensione nota.

Tali tipi sono definiti nell'header stdint.h e sono i seguenti:

  • int8_t: intero a 8 bit con segno;
  • uint8_t: intero a 8 bit senza segno;
  • int16_t: intero a 16 bit con segno;
  • uint16_t: intero a 16 bit senza segno;
  • int32_t: intero a 32 bit con segno;
  • uint32_t: intero a 32 bit senza segno;
  • int64_t: intero a 64 bit con segno;
  • uint64_t: intero a 64 bit senza segno.

Questi tipi sono garantiti avere una dimensione fissa e sono stati introdotti per garantire la portabilità dei programmi C su differenti architetture.

Per cui, se dichiariamo, ad esempio, una variabile di tipo int32_t, possiamo essere certi che essa avrà sempre 32 bit, indipendentemente dall'architettura della macchina:

#include <stdint.h>

int32_t x;

In Sintesi

In questa lezione abbiamo approfondito il catalogo dei tipi numerici interi che il linguaggio C mette a disposizione del programmatore.

In particolare, abbiamo studiato che:

  • I tipi interi rappresentano numeri interi privi di parte frazionaria;
  • I tipi interi si suddividono in due categorie: i tipi interi con segno e i tipi interi senza segno;
  • I tipi interi con segno possono rappresentare valori positivi e negativi, mentre i tipi interi senza segno possono rappresentare solo valori positivi;
  • La dimensione di un tipo intero dipende dall'architettura della macchina e dal compilatore utilizzato;
  • Lo standard C99 ha introdotto i tipi long long int e unsigned long long int per rappresentare numeri molto grandi;
  • Lo standard C99 ha introdotto i tipi interi a dimensione nota per garantire la portabilità dei programmi C su differenti architetture.

Nella prossima lezione studieremo le costanti intere, chiamati anche valori letterali interi, e vedremo come si possono scrivere numeri nel codice sorgente.