TIPI DATI
In matematica è usuale classificare le variabili rispetto ad alcune loro
caratteristiche importanti: si fanno chiare distinzioni tra variabili reali,
complesse e logiche, oppure tra variabili che rappresentano valori
individuali, insiemi di valori, o insiemi di insiemi, oppure tra funzioni,
funzionali, insiemi di funzioni, e così via.
Questa classificazione è ugualmente importante, se non di più, nella
elaborazione dei dati, dove vale il principio che:
tutte le costanti, le variabili, le espressioni
e le funzioni appartengono a un certo tipo.
Tale tipo caratterizza l’insieme di valori che può assumere una
costante o una variabile, o che possono essere prodotti da una
funzione.
Nei testi di matematica il tipo di una variabile può di solito essere
dedotto dai caratteri tipografici, indipendentemente dal contesto.
Ciò non è possibile con i programmi, che di solito sono scritti con un
solo repertorio di caratteri.
Perciò la regola, quasi ovunque accettata, stabilisce che il tipo
associato sia reso esplicito con una dichiarazione della costante,
della variabile o della funzione.
Tale dichiarazione deve precedere l’utilizzo della costante, della
variabile o della funzione.
Questa regola appare particolarmente sensata se si considera che
un compilatore deve fare una scelta di rappresentazione
dell’oggetto, nella memoria dell’elaboratore.
Evidentemente, la quantità di memoria che il compilatore riserva
(alloca) a una variabile deve essere scelta considerando la
dimensione dell’intervallo di valori che essa potrà assumere.
Quando tale informazione sia nota al compilatore, esso riserva la
quantità di memoria effettivamente necessaria a rappresentare la
variabile (allocazione statica)
Se invece la quantità di memoria da riservare per un tipo dati non è
nota (come succede con i file, che vedremo), il compilatore deve
effettuare una allocazione dinamica, ossia riservare memoria via via
che i valori si “espandono” e recuperarla quando essi si
“restringono”.
Le caratteristiche principali del concetto di tipo, che è incorporato nel
linguaggio C, sono dunque le seguenti:
1. un tipo dati determina l’insieme dei valori al quale appartiene una
costante, o i valori che una variabile o un’espressione possono
assumere, o che una funzione o un operatore possono produrre;
2. il tipo del valore rappresentato da una costante, una variabile o
un’espressione può derivare dalla sua forma o dalla sua
dichiarazione, senza necessità di eseguire il processo di
computazione;
3. ogni operatore e ogni funzione accettano argomenti di un tipo
fissato, e producono risultati di un tipo fissato; se un operatore
ammette argomenti di tipi diversi (ad es., + viene utilizzato per
sommare sia numeri interi, sia numeri reali), allora il tipo del risultato
può essere determinato mediante regole specifiche del linguaggio.
Di conseguenza, un compilatore può usare le informazioni di tipo
per controllare la compatibilità e la legalità dei vari costrutti. Ad
es., l’assegnamento di un valore booleano (logico) a una
variabile aritmetica (reale), può essere rilevato senza eseguire il
programma.
Questo tipo di ridondanza nel testo del programma è estremamente
utile per agevolare lo sviluppo degli algoritmi, e costituisce uno dei
vantaggi principali dei buoni linguaggi ad alto livello, rispetto al
codice macchina o al codice assemblativo simbolico.
Evidentemente, alla fine tutti i dati verranno rappresentati nella
memoria del computer sotto forma di cifre binarie,
indipendentemente dal fatto che il programma sia stato
inizialmente concepito in un linguaggio ad alto livello, dotato di tipi,
oppure in codice assemblativo senza tipi.
Dal punto di vista del computer, la memoria è un ammasso di bit,
senza una struttura apparente. Ma è proprio la struttura astratta
che, da sola, permette ai programmatori di dare un significato al
monotono panorama di una memoria di computer.
Il linguaggio C fornisce alcuni
• tipi dati predefiniti, che contengono numeri e caratteri, e possono
essere semplici o strutturati.
Inoltre consente di creare
• tipi dati definiti dall’utente.
Se esiste un ordine tra i valori individuali, allora si dice che il tipo è
ordinato. In C si assume che tutti i tipi non strutturati siano ordinati.
Tipi dati semplici
In C vi sono 4 tipi dati semplici:
- integer
- floating point
- double precision
- character
(intero)
(virgola mobile)
(doppia precisione)
(carattere)
Valori integer. Un valore intero, detto anche costante intera, è un
numero positivo o negativo senza la virgola decimale.
Esempi:
5
-10
+36
Come si vede, gli interi possono essere con segno (con un + o un iniziale) o senza segno (senza + o - iniziale).
I compilatori presentano limitazioni sui valori interi massimo (positivo)
e minimo (negativo) che si possono usare in un programma.
Queste limitazioni dipendono dalla implementazione, ossia dalla
quantità di memoria che il compilatore mette a disposizione per un
intero.
Le allocazioni di memoria più comuni sono indicate in tabella.
L’operatore sizeof, che vedremo più avanti, consente di
determinare la quantità di memoria riservata dal computer in uso per
ciascun tipo dati.
Numeri floating point e double precision. I numeri in virgola
mobile e in doppia precisione sono i numeri con o senza segno con
il punto decimale. Esempi:
+10.625
5.
-6.2
3251.92
0.0
0.33
+2
La differenza tra i numeri in virgola mobile e in doppia precisione sta
nella quantità di memoria riservata per ciascun tipo: molti computer
riservano una quantità di memoria doppia per i numeri in doppia
precisione rispetto a quella per i numeri in virgola mobile.
In tale caso un numero in doppia precisione ha approssimativamente
una precisione doppia rispetto a un numero in virgola mobile (che
per questa ragione è detto anche numero in precisione singola).
L’effettiva allocazione di memoria per ciascun tipo dati dipende
tuttavia dal particolare computer, e in quelli che usano la stessa
quantità di memoria per i numeri in doppia precisione e in virgola
mobile i due tipi dati diventano identici.
Notazione esponenziale. I numeri in virgola mobile e in doppia
precisione si possono scrivere in notazione esponenziale, che
viene usata di solito per esprimere in forma compatta numeri molto
grandi o molto piccoli.
I seguenti esempi illustrano come si possono esprimere in notazione
esponenziale numeri con il punto decimale.
Tipo character. Il quarto tipo dati fondamentale riconosciuto in C è il
tipo carattere. I caratteri sono le lettere dell’alfabeto (minuscole e
maiuscole), le dieci cifre da 0 a 9 e simboli speciali quali:
+
$
.
,
-
!
Una costante a carattere singolo è qualsiasi lettera, cifra o simbolo
speciale racchiuso tra apici singoli. Esempi:
‘A’
‘$’
‘b’
‘7’
Le costanti carattere sono memorizzate in un computer usando
tipicamente i codici ASCII o EBCDIC, e riservando 1 byte di
memoria per ciascun carattere.
Ad es., la parola Bytes sarebbe memorizzata come indica la figura.
Tipi dati strutturati
Le variabili usate finora avevano tutte una caratteristica in comune:
ognuna di esse poteva memorizzare un singolo valore alla volta del
tipo dichiarato.
Le variabili con questa caratteristica sono dette scalari. Una variabile
scalare non può essere ulteriormente suddivisa o separata in tipi dati
validi.
Vettori. Spesso però s’incontrano insiemi di variabili, tutte dello stesso
tipo dati, che formano un gruppo logico. Consideriamo, ad esempio, i
voti di uno studente, i codici di un colore e i prezzi dei prodotti su uno
scaffale.
Un gruppo di valori singoli tutti dello stesso tipo dati scalare è detto
vettore (a una dimensione), e i valori singoli sono detti le sue
componenti.
Così diremo che voti è un vettore di interi con 5 componenti,
codici un vettore di caratteri con 4 componenti, prezzi un
vettore in doppia precisione con 6 componenti.
Vettori a più dimensioni. Un vettore a due dimensioni consiste in
righe e colonne di elementi. Ad esempio, il vettore di numeri
consiste in 3 righe e 4 colonne.
In modo analogo si possono definire vettori a 3 o più dimensioni.
Concettualmente, come
indica la figura, un vettore
a 3 dimensioni si può
considerare come un libro
di tabelle di dati.
Con questa analogia, il
primo indice si può
pensare come la
localizzazione di una riga
della tabella, il secondo
come la localizzazione di
una colonna e il terzo
come il numero di pagina
della tabella.
Nella stessa maniera si possono dichiarare vettori di qualsiasi
dimensione.
Strutture. Talvolta, tuttavia, le variabili che formano un gruppo logico
non appartengono tutte allo stesso tipo dati, come avviene ad
esempio per le 5 variabili che costituiscono la seguente etichetta
postale:
Nome_Cognome:
Indirizzo:
C.A.P:
Città:
Stato:
Nella programmazione una unità d’informazione costituita da gruppi
di variabili correlate appartenenti a tipi dati diversi è detta record o,
in C, struttura; ciascuna variabile componente è detta campo o, in
C, membro della struttura.
Ciascun campo costituisce un’entità autonoma ma, presi insieme,
tutti i campi formano una singola unità, che rappresenta
un’organizzazione naturale dei dati di una etichetta postale.
Sebbene in un elenco postale completo vi possano essere migliaia
di nomi, indirizzi, C.A.P., città e Stati, la forma di ciascuna etichetta
postale è identica.
Occorre distinguere tra forma e contenuto di una struttura.
La forma consiste nei
• tipi dati,
• nomi simbolici,
• disposizione dei dati individuali
presenti nella struttura.
Il contenuto si riferisce ai dati effettivamente memorizzati nei nomi
simbolici. Ad es., un contenuto valido per la struttura precedente
potrebbe essere:
Francis Bronson
Via Nazionale, 128
00100
Roma
Italia
La differenza tra vettori e strutture risiede quindi nei tipi degli
elementi che contengono:
• un vettore è un tipo dati omogeneo, in quanto tutte le sue
componenti sono dello stesso tipo;
• una struttura è un tipo dati eterogeneo, in quanto ciascuno dei suoi
campi può essere di un tipo dati differente.
Così, un vettore di strutture sarebbe un tipo dati omogeneo, i cui
elementi sono dello stesso tipo eterogeneo
Liste concatenate. Un problema classico di gestione di dati è quello
di compiere aggiunte o cancellazioni a record esistenti che vadano
mantenuti in un ordine specifico.
Ciò è illustrato considerando la parte di elenco telefonico indicata in
figura.
Aloisi, Sandro
0432 174973
Dolan, Edih
02 385602
Lisi, Giovanni
0556 390048
Melloni, Paolo
02 35581224
Zermann Harold
091 1275294
A partire da questo insieme iniziale di nomi e numeri di telefono,
vogliamo:
• inserire nuovi record alla lista nella corretta sequenza alfabetica;
• cancellare i record esistenti in modo tale che lo spazio di memoria
per i record cancellati sia eliminato.
Sebbene l’inserimento e la cancellazione di record ordinati si
possano eseguire con un vettore di strutture, esso non è una
rappresentazione efficiente per aggiungere o cancellare record
interni al vettore.
I vettori sono fissi e di dimensione specificata in precedenza.
La cancellazione di un record da un vettore crea uno spazio vuoto
che richiede o una marcatura particolare o lo spostamento “in
alto” di tutti gli elementi al di sotto del record cancellato per
chiudere lo spazio vuoto.
Analogamente, l’aggiunta di un record al corpo di un vettore di
strutture richiede che tutti gli elementi al di sotto di quello aggiunto
siano spostati “in basso” per fare posto al nuovo entrato.
In alternativa, si può aggiungere il nuovo elemento alla fine del vettore
e riordinare quest’ultimo in modo da ripristinare l’ordine corretto dei
record.
Perciò, sia l’aggiunta sia la cancellazione di record a tale lista
richiede in genere di ristrutturare e riscrivere la lista: operazione
noiosa, lunga e inefficiente.
Una lista concatenata fornisce un metodo conveniente per mantenere
una lista che cambia in continuazione, senza la necessità di
riordinare e ristrutturare in continuazione l’intera lista.
Una lista concatenata è semplicemente un insieme di strutture
ognuna delle quali contiene almeno un membro il cui valore è
l’indirizzo della successiva struttura in ordine logico della lista.
Anziché richiedere che ogni record sia fisicamente memorizzato
nell’ordine corretto, ogni nuovo record è fisicamente aggiunto o alla
fine della lista esistente, o dovunque il computer abbia spazio libero
nella sua area di memoria.
I record sono concatenati insieme inserendo l’indirizzo del record
successivo in quello che lo precede nell’ordine logico.
Dal punto di vista del programmatore, il record corrente che viene
elaborato contiene l’indirizzo del record successivo, in maniera
indipendente da dove esso sia effettivamente memorizzato.
Il concetto di lista concatenata è illustrato in figura.
Sebbene i dati effettivi per la struttura Lisi illustrata in figura possano
essere memorizzati fisicamente in qualsiasi punto della memoria, il
membro aggiuntivo inserito alla fine della struttura Dolan mantiene
l’ordine alfabetico corretto.
Questo membro fornisce l’indirizzo di partenza della locazione dove è
memorizzato il record Lisi.
Unioni. Una unione è un tipo dati che riserva la stessa area di
memoria per due o più variabili, ciascuna delle quali può essere di
un tipo dati diverso.
Una variabile dichiarata come tipo dati unione può essere usata per
contenere una variabile carattere, una intera, una in doppia
precisione o qualsiasi altro tipo dati valido in C.
Ognuno di essi, ma solamente uno alla volta, possono effettivamente
essere assegnati alla variabile unione.
Alberi. Gli alberi sono un esempio di struttura che può essere definita
in modo elegante attraverso la ricorsività, come segue:
Una struttura ad albero, con tipo base T, può essere:
• la struttura vuota
• un nodo di tipo T, associato a un numero finito di strutture ad
albero disgiunte aventi tipo base T, denominate sottoalberi.
Da questo punto di vista, la sequenza (o lista) è una struttura nella
quale ogni nodo ha al più un sottoalbero, per cui viene anche
chiamata albero degenere.
Una struttura ad albero può essere rappresentata in diversi modi: la
figura seguente ne mostra due per un tipo base T che varia
nell’insieme delle lettere. Le due rappresentazioni mostrano la stessa
struttura e sono perciò equivalenti.
La struttura di grafo illustra esplicitamente le relazioni di diramazione
che hanno condotto alla denominazione di albero. Il nodo in cima (A)
viene chiamato radice.
Un albero si dice ordinato se i rami di ogni nodo sono ordinati.
Quindi i due alberi indicati in figura sono oggetti distinti e diversi.
A
B
A
C
C
B
Un nodo y, direttamente collegato sotto il nodo x, viene chiamato
un discendente (diretto) di x; se x si trova al livello i, allora si
dice che y è al livello i+1. Reciprocamente, il nodo x si dice avo
(diretto) di y.
Per definizione, la radice di un albero si trova al livello 1. Il massimo
livello degli elmenti di un albero si dice profondità o altezza
dell’albero.
Se un elemento non ha discendenti, viene chiamato elemento
terminale o foglia, mentre gli elementi che non sono terminali si
chiamano nodi interni.
Il numero di discendenti (diretti) di un nodo interno è il suo grado. Il
massimo grado di tutti i nodi è il grado dell’albero.
Il numero di rami, o archi, che si devono attraversare per procedere
dalla radice a un nodo x si chiama lunghezza del cammino di x.
La radice ha lunghezza 1, i suoi discendenti hanno lunghezza 2,
ecc. In generale, un nodo al livello i ha lunghezza di cammino
pari a i.
La lunghezza del cammino di un albero viene definita come la
somma delle lunghezze dei cammini di tutte le sue componenti, e
viene anche chiamata lunghezza del cammino interno.
Ad es., per l’albero mostrato nella figura iniziale la lunghezza del
cammino interno è 52.
Gli alberi ordinati di grado 2 si chiamano alberi binari e hanno
particolare importanza; quelli di grado maggiore di 2 si chiamano
alberi generali.
Definiamo un albero binario ordinato come
un insieme finito di elementi (nodi) , che può essere vuoto,
oppure consiste in un nodo radice con due alberi binari
disgiunti, detti sottoalbero sinistro e destro della radice.
Due esempi familiari di alberi binari sono:
• la storia di un torneo di calcio, dove ogni nodo, simboleggiato dal
nome della squadra vincitrice, rappresenta una partita che ha
come discendenti le due partite precedenti tra gli avversari;
• un’espressione aritmetica con operatori diadici, nella quale ogni
operatore denota un nodo interno che ha i propri operandi come
sottoalberi.
La figura seguente è una rappresentazione ad albero dell’espressione
(a+b/c)*(d-e*f)
File di dati. I dati per i programmi che abbiamo visto finora sono stati
definiti all’interno dei programmi stessi, oppure immessi in modo
interattivo durante l’esecuzione. In entrambi i casi risulta poco pratico
gestire grandi quantità di dati e in particolare condividerli tra
programmi.
Affinché i dati prodotti in uscita da un programma possano essere
immessi direttamente in un altro programma, senza dover essere
ricreati o ridefiniti ogni volta, essi vanno salvati in modo indipendente
e separato dal programma - ossa all’esterno di esso - di solito su
dispositivi di memoria secondaria quali i dischi fissi o rimovibili, i cdrom o i nastri magnetici.
La struttura dati utilizzata in questo caso è il file (dati), che è una
collezione di dati memorizzati insieme sotto un nome comune su un
supporto di memorizzazione diverso dalla memoria principale di un
computer.
Un file è un insieme di record correlati, quale ad es. il file degli stipendi
di un’azienda, il quale contiene un record per ciascun impiegato.
Per facilitare il recupero di un record specifico da un file, si sceglie
almeno uno dei suoi campi come chiave del record, che lo identifica
come appartenente a una particolare persona. Ad es., nel file degli
stipendi si può scegliere come chiave il codice fiscale.
I record di un file sono organizzati di solito in due modi:
• in un file sequenziale i record sono ordinati il base al campo chiave
del record, e possono avere lunghezze differenti;
• in un file ad accesso casuale i record hanno tutti la stessa lunghezza,
in modo che si possa accedere a ciascuno in modo diretto, cioè
senza passare attraverso gli altri record.
Vedremo più avanti le istruzioni C necessarie per
 creare
 scrivere o modificare
 leggere
un file dati.
Tipi dati definiti dall’utente
Oltre ai tipi dati predefiniti, il linguaggio C specifica alcuni metodi di
definizione dei tipi dati.
Ove sia necessario, è possibile costruire nuovi tipi di dati mediante tipi
definiti in precedenza.
I valori che appartengono a un tipo definito in questo modo sono detti,
di solito, strutturati, e sono agglomerati di valori componenti,
appartenenti ai tipi costituenti definiti in precedenza.
Nel caso in cui ci sia un solo tipo costituente, cioè in cui tutte le
componenti siano di quel tipo, esso si chiamerebbe tipo base.
Il numero di valori discreti che appartengono a un tipo T viene detto
cardinalità di T.
La cardinalità fornisce una misura della quantià di memoria necessaria
per memorizzare una variabile x di tipo T.
Poiché i tipi costituenti potrebbero essere a loro volta strutturati, si
possono costruire intere gerarchie di tipi; ovviamente, però, le
componenti più elementar di una struttura devono essere atomiche.
Quindi è necessario disporre di una notazione per introdurre tali tipi
primitivi non strutturati.
Un metodo immediato è l’enumerazione dei valori che costituiscono
un tipo.
Per esempio, in un programma relativo alla geometria piana si
potrebbe introdurre un tipo primitivo detto figura, i cui valori
potrebbero essere indicati con gli identificatori rettangolo, quadrato,
ellisse, circonferenza.