Tipi Ordinali e Interi in Object Pascal
I tipi ordinali sono categorie di tipi per cui è definito un ordine. In pratica, se x
e y
sono due variabili dello stesso tipo ordinale, è sempre possibile stabilire se x
è minore di y
, ossia se ha senso l'espressione x
< y
.
In Object Pascal esistono tre tipi ordinali built-in:
- I tipi interi
- il tipo
Char
- il tipo
Boolean
In questa lezione ci occuperemo dei tipi interi e vedremo gli altri tipi ordinali nelle prossime lezioni.
- I tipi ordinali sono tipi di dati che hanno un ordine definito tra i loro valori.
- In Object Pascal, i tipi ordinali includono tipi interi,
Char
eBoolean
. - I tipi interi in Object Pascal variano per dimensione (8, 16, 32, 64 bit) e per la presenza o meno del segno (signed/unsigned).
- Ogni tipo intero ha un intervallo specifico di valori che può rappresentare, e tentare di assegnare un valore fuori da questo intervallo può causare errori di overflow.
- Object Pascal fornisce operazioni e funzioni built-in per lavorare con tipi interi, come
ToString
,Size
,Inc
,Dec
,Low
,High
, e altre. - È possibile abilitare il controllo di overflow a runtime per rilevare errori di overflow durante l'esecuzione del programma.
Tipi Interi
Ogni linguaggio di programmazione che si rispetti permette allo sviluppatore di usare tipi numerici interi. Si parla di tipi interi al plurale in quanto, spesso, esistono diversi tipi di intero a seconda di come i numeri sono internamente rappresentati. Nel dettaglio, ciò che cambia è il numero di bit utilizzati e la presenza o meno del segno.
L'Object Pascal supporta un ampio spettro di tipi interi, riportati nella tabella seguente.
Da notare che oltre al nome, questi tipi hanno un alias, un nome alternativo che può essere usato al posto del nome vero e proprio. Questi alias sono più semplici da ricordare perché presentano nel nome sia il numero di bit usati (ad es. Int16
per un intero a 16 bit e Int32
per un intero a 32), sia se la rappresentazione prevede la presenza del segno o meno (un intero senza segno è indicato da una U
anteposta al nome che sta per Unsigned ossia senza segno).
Dimensione | Con Segno | Nome | Alias | Valore Minimo | Valore Massimo |
---|---|---|---|---|---|
8 Bit | No | Byte |
UInt8 |
||
8 Bit | Si | ShortInt |
Int8 |
||
16 Bit | No | Word |
UInt16 |
||
16 Bit | Si | SmallInt |
Int16 |
||
32 Bit | No | Cardinal |
UInt32 |
||
32 Bit | Si | Integer |
Int32 |
||
64 Bit | No | UInt64 |
|||
64 Bit | Si | Int64 |
Una nota sul tipo Integer
.
Nelle versioni a 64 bit di Object Pascal, ci si potrebbe sorprendere che il tipo Integer
è a 32 bit. Il fatto che sia a 32 bit è dovuto principalmente alle seguenti ragioni:
- La maggior parte del codice Object Pascal esistente è stato scritto per piattaforme a 32 bit, e cambiare la dimensione di
Integer
a 64 bit romperebbe la compatibilità con il codice esistente. - Le operazioni sui numeri a 32 bit sono generalmente più veloci delle operazioni sui numeri a 64 bit (anche se questo dipende dalla CPU specifica).
- La maggior parte delle API di sistema e delle librerie di terze parti con cui interagisce il codice Object Pascal utilizzano ancora tipi a 32 bit per gli interi.
Altri tipi come il tipo Pointer
(di cui parleremo più avanti nelle prossime lezioni) e altri tipi di riferimento correlati sono a 64 bit. Questo perché devono essere in grado di indirizzare l'intero spazio di memoria su una piattaforma a 64 bit.
In ogni caso, se si ha la necessità di un tipo numerico che si adatti alla dimensione del puntatore e alla piattaforma CPU nativa, si possono usare i due tipi alias speciali NativeInt
e NativeUInt
. Questi hanno la stessa dimensione di un puntatore sulla piattaforma specifica (cioè, 32 bit su piattaforme a 32 bit e 64 bit su piattaforme a 64 bit).
La situazione cambia per LargeInt
, che viene spesso utilizzato per mappare le funzioni API della piattaforma nativa. Questo è a 32 bit su piattaforme a 32 bit e su Windows 32 bit, mentre è a 64 bit su piattaforma ARM a 64 bit.
Funzioni di ausilio per Interi
I tipi Integer
e gli altri tipi interi in Object Pascal sono tipi primitivi, il che significa che non sono oggetti ma tipi di dati nativi del linguaggio. Questa scelta progettuale è stata fatta per motivi di efficienza, in quanto i tipi di dati nativi sono più veloci da manipolare rispetto agli oggetti.
Nonostante ciò, è possibile operare su variabili (e valori costanti) di questi tipi con operazioni che si applicano usando la notazione punto. Questa è la notazione generalmente utilizzata per applicare metodi agli oggetti.
Come anticipazione, la notazione punto ha una sintassi di questo tipo:
variabile.operazione(parametri)
Dove variabile
è una variabile di un certo tipo, operazione
è il nome dell'operazione che si vuole eseguire su di essa, e parametri
sono eventuali parametri richiesti dall'operazione.
Per chiarire meglio, osserviamo il codice seguente:
var
N: Integer;
begin
// Assegna un valore alla variabile
N := 10;
// Trasforma il valore in stringa e lo mostra a video
WriteLn(N.ToString);
// Mostra i byte richiesti per memorizzare il tipo
WriteLn(Integer.Size.ToString);
end.
L'output di questo codice sarà:
10
4
Di seguito sono elencate le operazioni che è possibile eseguire sui tipi interi:
Operazione | Descrizione |
---|---|
ToString |
Converte il numero in una stringa, usando il formato decimale |
ToBoolean |
Converte nel tipo Boolean |
ToHexString |
Converte in una stringa, usando il formato esadecimale |
ToSingle |
Converte nel tipo di dati a virgola mobile single |
ToDouble |
Converte nel tipo di dati a virgola mobile double |
ToExtended |
Converte nel tipo di dati a virgola mobile extended |
La prima e la terza operazione convertono il numero in una stringa, usando una rappresentazione decimale o esadecimale. La seconda è una conversione a Boolean, mentre le ultime tre sono conversioni ai tipi a virgola mobile che vedremo nelle prossime lezioni.
Ci sono altre operazioni che si possono applicare al tipo Integer
(e alla maggior parte degli altri tipi numerici), come:
Operazione | Descrizione |
---|---|
Size |
Il numero di byte richiesti per memorizzare una variabile di questo tipo |
Parse |
Converte una stringa nel valore numerico che rappresenta, sollevando un'eccezione se la stringa non rappresenta un numero |
TryParse |
Tenta di convertire la stringa in un numero |
Procedure standard per i tipi interi
Object Pascal fornisce tutta una serie di routine standard built-in per lavorare sui tipi interi oltre alle operazioni viste sopra. Queste routine sono definite per tutti i tipi ordinali, non solo per i tipi interi.
Alcuni esempi di queste routine sono:
SizeOf
che restituisce il numero di byte richiesti per memorizzare una variabile del tipo di dati;High
eLow
che restituiscono rispettivamente il valore più alto e più basso nell'intervallo del tipo di dati.
Proviamo ad applicare alcune di queste routine nel codice seguente in cui verifichiamo la dimensione in byte di alcuni tipi interi e ne stampiamo il valore più alto e più basso possibile:
program TestInteri;
var
I: Integer;
B: Byte;
S: SmallInt;
C: Cardinal;
L: Int64;
begin
WriteLn('Integer: Size = ', SizeOf(I), ' bytes, Low = ', Low(I), ', High = ', High(I));
WriteLn('Byte: Size = ', SizeOf(B), ' bytes, Low = ', Low(B), ', High = ', High(B));
WriteLn('SmallInt: Size = ', SizeOf(S), ' bytes, Low = ', Low(S), ', High = ', High(S));
WriteLn('Cardinal: Size = ', SizeOf(C), ' bytes, Low = ', Low(C), ', High = ', High(C));
WriteLn('Int64: Size = ', SizeOf(L), ' bytes, Low = ', Low(L), ', High = ', High(L));
end.
Eseguendo questo codice, otterremo un output simile al seguente (i valori di Low e High possono variare a seconda della piattaforma):
Integer: Size = 2 bytes, Low = -32768, High = 32767
Byte: Size = 1 bytes, Low = 0, High = 255
SmallInt: Size = 2 bytes, Low = -32768, High = 32767
Cardinal: Size = 4 bytes, Low = 0, High = 4294967295
Int64: Size = 8 bytes, Low = -9223372036854775808, High = 9223372036854775807
Le routine di sistema che funzionano sui tipi ordinali sono mostrate nella seguente tabella:
Nome | Descrizione |
---|---|
Dec |
Decrementa la variabile passata come parametro, di uno o del valore specificato come secondo parametro opzionale |
Inc |
Incrementa la variabile passata come parametro, di uno o del valore specificato come secondo parametro opzionale |
Odd |
Restituisce True se l'argomento è un numero dispari. Per testare numeri pari, dovresti usare un'espressione not (not Odd) |
Pred |
Restituisce il valore prima dell'argomento, cioè il predecessore, nell'ordine determinato dal tipo di dati |
Succ |
Restituisce il valore dopo l'argomento, cioè il successore |
Ord |
Restituisce un numero che indica l'ordine dell'argomento all'interno dell'insieme di valori del tipo di dati (usato per tipi ordinali non numerici) |
Low |
Restituisce il valore più basso nell'intervallo del tipo ordinale |
High |
Restituisce il valore più alto nell'intervallo del tipo di dati ordinale |
SizeOf |
Restituisce il numero di byte richiesti per memorizzare una variabile del tipo di dati |
Vediamo alcune osservazioni sulle routine Inc
e Dec
. Entrambe le routine accettano uno o due parametri. Il primo parametro è sempre la variabile da incrementare o decrementare. Il secondo parametro, opzionale, specifica di quanto incrementare o decrementare la variabile. Se il secondo parametro non è specificato, la variabile viene incrementata o decrementata di uno. In ogni caso, le routine non restituiscono alcun valore.
Ad esempio, se si ha una variabile N
di tipo Integer
, le seguenti chiamate sono equivalenti:
Inc(N); // Incrementa N di 1
Inc(N, 1); // Incrementa N di 1
Dec(N); // Decrementa N di 1
Dec(N, 1); // Decrementa N di 1
Le chiamate di sopra sono equivalenti a:
N := N + 1; // Incrementa N di 1
N := N - 1; // Decrementa N di 1
Similarità con C/C++
Per chi proviene da C o C++, è importante notare che le routine Inc
e Dec
in Delphi non sono esattamente equivalenti agli operatori di incremento (++
) e decremento (--
) in C/C++. In particolare, Inc
e Dec
non supportano le versioni post-incremento e post-decremento, e non restituiscono valori. Inoltre, l'uso di Inc
e Dec
può essere più chiaro in alcuni contesti, specialmente quando si lavora con tipi ordinali non numerici.
Un'altra osservazione riguarda il fatto che tali routine sono ottimizzate per i tipi ordinali. Questo significa che il compilatore può generare codice più efficiente per queste operazioni rispetto a un'operazione aritmetica standard come N := N + 1
. Ad esempio, se usiamo la routine High
su una variabile di tipo Integer
, il compilatore può sostituire la chiamata con il valore costante 32767
(o 2147483647
su piattaforme a 64 bit), evitando così una chiamata di funzione. Analogamente, se il compilatore vede una chiamata a Inc(N)
dove N
è di tipo Integer
, e il valore di N
è noto in fase di compilazione, può sostituire la chiamata con un'operazione di incremento diretta.
Overflow e tipi interi
Una variabile intera o ordinale ha solo un intervallo limitato di valori validi.
Se il valore che le viene assegnato è troppo grande, questo risulta in un errore. Analogamente, se viene assegnato un valore negativo a un tipo senza segno, questo risulta in un errore. Tali condizioni sono chiamate overflow (o underflow per i valori troppo piccoli).
Ci sono in realtà tre diversi tipi di errori che si possono incontrare con operazioni fuori intervallo.
Il primo tipo di errore è un errore del compilatore, che si verifica se si assegna un valore costante (o un'espressione costante) che è fuori intervallo. Per esempio, se scriviamo il codice che segue:
var
N: Byte;
begin
N := 100 + High(N);
end;
il compilatore genererà l'errore:
E1012 Constant expression violates subrange bounds
Il secondo scenario si verifica quando il compilatore non può anticipare la condizione di errore, perché dipende dai valori a runtime e quindi dallo stato del programma. Ad esempio, consideriamo il seguente codice:
Inc(N, High(N));
Il compilatore non genererà un errore perché c'è una chiamata di funzione, e il compilatore non conosce il suo effetto in anticipo (e l'errore dipenderebbe anche dal valore iniziale di N). In questo caso ci sono due possibilità. Per default, se si compila ed esegue questa applicazione, si ottiene un valore completamente illogico nella variabile (in questo caso l'operazione risulterà nel sottrarre 1). Questo è il peggior scenario possibile, perché non si ottiene alcun errore, ma il programma non è corretto.
Tuttavia, sia Delphi che FreePascal offrono un'opzione del compilatore chiamata "Overflow checking" che si attiva con le direttive:
{$Q+}
oppure
{$OVERFLOWCHECKS ON}
Attivando questa opzione, il compilatore inserisce del codice di controllo per rilevare le condizioni di overflow a runtime. Se si verifica un overflow, viene sollevata un'eccezione EIntOverflow
, che può essere gestita con un blocco try..except
. Studieremo la gestione delle eccezioni in una lezione successiva.
Bisogna tenere presente, però, che l'abilitazione del controllo di overflow può rallentare leggermente l'esecuzione del programma a causa del codice di controllo aggiuntivo.