Il C++ permette la definizione di nuovi tipi. I tipi definiti dal programmatore vengono detti "Tipi definiti dall'utente" e possono essere utilizzati ovunque sia richiesto un identificatore di tipo (con rispetto alle regole di visibilita` viste precedentemente). I nuovi tipi vengono definiti applicando dei costruttori di tipi ai tipi primitivi o a tipi precedentemente definiti dall'utente.
I costruttori di tipo disponibili sono:
- il costruttore di array: [ ]
- il costruttore di aggregati: struct
- il costruttore di unioni: union
- il costruttore di tipi enumerati: enum
- la keyword typedef
- il costruttore di classi: class
Per adesso tralasceremo il costruttore di classi, ci occuperemo di esso in seguito in quanto alla base della programmazione in C++ e meritevole di una trattazione separata e maggiormente approfondita.
Array
Per quanto visto precedentemente, una variabile puo` contenere un solo valore alla volta; il costruttore di array [ ] permette di raccogliere sotto un solo nome piu` variabili dello stesso tipo. La dichiarazione
int Array[10];
introduce con il nome Array 10 variabili di tipo int (anche se solitamente si parla di una variabile di tipo array); il tipo di Array e` array di 10 int(eri).
La sintassi per la generica dichiarazione di un array e`
<NomeTipo> <Identificatore> [ <NumeroElementi> ] ;
Al solito Tipo puo` essere sia un tipo primitivo che uno definito dal programmatore, Identificatore e` un nome scelto dal programmatore per identificare l'array, mentre NumeroElementi deve essere un intero positivo e indica il numero di singole variabili che compongono l'array.
Il generico elemento dell'array viene selezionato con la notazione Identificatore[Espressione], dove Espressione puo` essere una qualsiasi espressione che produca un valore intero; il primo elemento di un array e` sempre Identificatore[0], e di conseguenza l'ultimo e` Identificatore[NumeroElementi-1]:
float Pippo[10];
float Pluto;
Pippo[0] = 13.5; // Assegna 13.5 al primo elemento
Pluto = Pippo[9]; // Seleziona l'ultimo elemento di Pippo e lo assegna a Pluto
E` anche possibile dichiarare array multidimensionali (detti array di array o piu` in generale matrici) specificando piu` indici:
long double Qui[3][4]; // una matrice 3 x 4
short Quo[2][10]; // 2 array di 10 short int
int SuperPippo[12][16][20]; // matrice 12 x 16 x 20
La selezione di un elemento da un array multidimensionale avviene specificando un valore per ciascuno degli indici:
int Pluto = SuperPippo[5][7][9];
Quo[1][7] = Superpippo[2][2][6];
E` anche possibile specificare i valori iniziali dei singoli elementi dell'array tramite una inizializzazione aggregata:
int Pippo[5] = { 10, -5, 6, 110, -96 };
short Pluto[2][4] = { {4, 7, 1, 4}, {0, 3, 5, 9} };
int Quo[4][3][2] = { {{1, 2}, {3, 4}, {5, 6}},
{{7, 8}, {9, 10}, {11, 12}},
{{13, 14}, {15, 16}, {17, 18}},
{{19, 20}, {21, 22}, {23, 24}}
};
float Minni[ ] = { 1.1, 3.5, 10.5 };
long Manetta[ ][3] = { {5, -7, 2}, {1, 0, 5} };
La prima dichiarazione e` piuttosto semplice, dichiara un array di 5 elementi e per ciascuno di essi indica il valore iniziale a partire dall'elemento 0. Nella seconda riga viene dichiarata una matrice bidimensionale e se ne esegue l'inizializzazione, si noti l'uso delle parentesi graffe per raggruppare opportunamente i valori; la terza dichiarazione chiarisce meglio come procedere nel raggruppamento dei valori, si tenga conto che a variare per primo e` l'ultimo indice cosi` che gli elementi vengono inizializzati nell'ordine Quo[0][0][0], Quo[0][0][1], Quo[0][1][0], ..., Quo[3][2][1].
Le ultime due dichiarazioni sono piu` complesse in quanto non vengono specificati tutti gli indici degli array: in caso di inizializzazione aggregata il compilatore e` in grado di determinare il numero di elementi relativi al primo indice in base al valore specificato per gli altri indici e al numero di valori forniti per l'inizializzazione, cosi` che la terza dichiararzione introduce un array di 3 elementi e l'ultima una matrice 2 x 3. E` possibile omettere solo il primo indice e solo in caso di inizializzazione aggregata.
Gli array consentono la memorizzazione di stringhe:
char Topolino[ ] = "investigatore" ;
La dimensione dell'array e` pari a quella della stringa "investigatore" + 1, l'elemento in piu` e` dovuto al fatto che in C++ le stringhe per default sono tutte terminate dal carattere nullo ('\0')che il compilatore aggiunge automaticamente.
L'accesso agli elementi di Topolino avviene ancora tramite le regole viste sopra e non e` possibile eseguire un assegnamento con la stessa metodologia dell'inizializzazione:
char Topolino[ ] = "investigatore" ;
Topolino[4] = 't'; // assegna 't' al quinto elemento
Topolino[ ] = "basso"; // errore!
Topolino = "basso"; // ancora errore!
E` possibile inizializzare un array di caratteri anche nei seguenti modi:
char Minnie[ ] = { 'M', 'i', 'n', 'n', 'i', 'e' };
char Pluto[5] = { 'P', 'l', 'u', 't', 'o' };
In questi casi pero` non si ottiene una stringa terminata da '\0', ma semplici array di caratteri il cui numero di elementi e` esattamente quello specificato.
Strutture
Gli array permettono di raccogliere sotto un unico nome piu` variabili omogenee e sono solitamente utilizzati quando bisogna operare su piu` valori dello stesso tipo contemporaneamente (ad esempio per eseguire una ricerca). Tuttavia in generale per rappresentare entita` complesse e` necessario memorizzare informazioni di diversa natura; ad esempio per rappresentare una persona puo` non bastare una stringa per il nome ed il cognome, ma potrebbe essere necessario memorizzare anche eta` e codice fiscale. Memorizzare tutte queste informazioni in un'unica stringa non e` una buona idea poiche` le singole informazioni non sono immediatamente disponibili, ma e` necessario prima estrarle, inoltre nella rappresentazione verrebbero perse informazioni preziose quali il fatto che l'eta` e` sempre data da un intero positivo. D'altra parte avere variabili distinte per le singole informazioni non e` certamente una buona pratica, diventa difficile capire qual'e` la relazione tra le varie componenti. La soluzione consiste nel raccogliere le variabili che modellano i singoli aspetti in un'unica entita` che consenta ancora di accedere ai singoli elementi:
struct Persona {
char Nome[20];
unsigned short Eta;
char CodiceFiscale[16];
};
La precedente dichiarazione introduce un tipo struttura di nome Persona composto da tre campi: Nome (un array di 20 caratteri), Eta (un intero positivo), CodiceFiscale (un array di 16 caratteri).
La sintassi per la dichiarazione di una struttura e`
struct <NomeTipo> {
<Tipo> <NomeCampo>;
/* ... */
<Tipo> <NomeCampo>;
};
Si osservi che la parentesi graffa finale deve essere seguita da un punto e virgola, questo vale anche per le unioni, le enumerazioni e per le classi.
I singoli campi di una variabile di tipo struttura sono selezionabili tramite l'operatore di selezione . (punto), come mostrato nel seguente esempio:
struct Persona {
char Nome[20];
unsigned short Eta;
char CodiceFiscale[7];
};
Persona Pippo = { "Pippo", 40, "PPP718" };
Persona AmiciDiPippo[2] = { {"Pluto", 40, "PLT712"},
{"Minnie", 35, "MNN431"}
};
// esempi di uso di strutture:
Pippo.Eta = 41;
unsigned short Var = Pippo.Eta;
strcpy(AmiciDiPippo[0].Nome, "Topolino");
Innanzi tutto viene dichiarato il tipo Persona e quindi si dichiara la variabile Pippo di tale tipo; in particolare viene mostrato come inizializzare la variabile con una inizializzazione aggregata del tutto simile a quanto si fa per gli array, eccetto che i valori forniti devono essere compatibili con il tipo dei campi e dati nell'ordine definito nella dichiarazione. Viene mostrata anche la dichiarazione di un array i cui elementi sono di tipo struttura, e il modo in cui eseguire una inizializzazione fornendo i valori necessari all'inizializzazione dei singoli campi di ciascun elemento dell'array. Le righe successive mostrano come accedere ai campi di una variabile di tipo struttura, in particolare l'ultima riga assegna un nuovo valore al campo Nome del primo elemento dell'array tramite una funzione di libreria. Si noti che prima viene selezionato l'elemento dell'array e poi il campo Nome di tale elemento; analogamente se e` la struttura a contenere un campo di tipo non primitivo, prima si seleziona il campo e poi si seleziona l'elemento del campo che ci interessa:
struct Data {
unsigned short Giorno, Mese;
unsigned Anno;
};
struct Persona {
char Nome[20];
Data DataNascita;
};
Persona Pippo = { "pippo", {10, 9, 1950} };
Pippo.Nome[0] = 'P';
Pippo.DataNascita.Giorno = 15;
unsigned short UnGiorno = Pippo.DataNascita.Giorno;
Per le strutture, a differenza degli array, e` definito l'operatore di assegnamento:
struct Data {
unsigned short Giorno, Mese;
unsigned Anno;
};
Data Oggi = { 10, 11, 1996 };
Data UnaData = { 1, 1, 1995};
UnaData = Oggi;
Cio` e` possibile per le strutture solo perche`, come vedremo, il compilatore le tratta come classi i cui membri sono tutti pubblici.
L'assegnamento e` ovviamente possibile solo tra variabili dello stesso tipo struttura, ma quello che di solito sfugge e` che due tipi struttura che differiscono solo per il nome sono considerati diversi:
// con riferimento al tipo Data visto sopra:
struct DT {
unsigned short Giorno, Mese;
unsigned Anno;
};
Data Oggi = { 10, 11, 1996 };
DT Ieri;
Ieri = Oggi; // Errore di tipo!
Unioni
Un costrutto sotto certi aspetti simile alle strutture e quello delle unioni. Sintatticamente l'unica differenza e` che nella dichiarazione di una unione viene utilizzata la keyword union anzicche` struct:
union TipoUnione {
unsigned Intero;
char Lettera;
char Stringa[500];
};
Come per i tipi struttura, la selezione di un dato campo di una variabile di tipo unione viene eseguita tramite l'operatore di selezione . (punto).
Vi e` tuttavia una profonda differenza tra il comportamento di una struttura e quello di una unione: in una struttura i vari campi vengono memorizzati in indirizzi diversi e non si sovrappongono mai, in una unione invece tutti i campi vengono memorizzati a partire dallo stesso indirizzo. Cio` vuol dire che, mentre la quantita` di memoria occupata da una struttura e` data dalla somma delle quantita` di memoria utilizzata dalle singole componenti, la quantita` di memoria utilizzata da una unione e` data da quella della componente piu` grande (Stringa nell'esempio precedente).
Dato che le componenti si sovrappongono, assegnare un valore ad una di esse vuol dire distruggere i valori memorizzati accedendo all'unione tramite una qualsiasi altra componente.
Le unioni vengono principalmente utilizzate per limitare l'uso di memoria memorizzando negli stessi indirizzi oggetti diversi in tempi diversi. C'e` tuttavia un altro possibile utilizzo delle unioni, eseguire "manualmente" alcune conversioni di tipo. Tuttavia tale pratica e` assolutamente da evitare (almeno quando esiste una alternativa) poiche` tali conversioni sono dipendenti dall'architettura su cui si opera e pertanto non portabili, ma anche potenzialmete scorrette.
Enumerazioni
A volte puo` essere utile poter definire un nuovo tipo estensionalmente, cioe` elencando esplicitamente i valori che una variabile (o una costante) di quel tipo puo` assumere. Tali tipi vengono detti enumerati e sono definiti tramite la keyword enum con la seguente sintassi:
enum <NomeTipo> {
<Identificatore>,
/* ... */
<Identificatore>
};
Esempio:
enum Elemento {
Idrogeno,
Elio,
Carbonio,
Ossigeno
};
Elemento Atomo = Idrogeno;
Gli identificatori Idrogeno, Elio, Carbonio e Ossigeno costituiscono l'intervallo dei valori del tipo Elemento. Si osservi che come da sintassi, i valori di una enumerazione devono essere espressi tramite identificatori, non sono ammessi valori espressi in altri modi (interi, numeri in virgola mobile, costanti carattere...), inoltre gli identificatori utilizzati per esprimere tali valori devono essere distinti da qualsiasi altro identificatore visibile nello scope dell'enumerazione onde evitare ambiguita`.
Il compilatore rappresenta internamente i tipi enumerazione associando a ciascun identificatore di valore una costante intera, cosi` che un valore enumerazione puo` essere utilizzato in luogo di un valore intero, ma non viceversa:
enum Elemento {
Idrogeno,
Elio,
Carbonio,
Ossigeno
};
Elemento Atomo = Idrogeno;
int Numero;
Numero = Carbonio; // Ok!
Atomo = 3; // Errore!
Nell'ultima riga dell'esempio si verifica un errore perche` non esiste un operatore di conversione da int a Elemento, mentre essendo i valori enumerazione in pratica delle costanti intere, il compilatore e` in grado di eseguire la conversione a int. E` possibile forzare il valore intero da associare ai valori di una enumerazione:
enum Elemento {
Idrogeno = 2,
Elio,
Carbonio = Idrogeno - 10,
Ferro = Elio + 7,
Ossigeno = 2
};
Non e` necessario specificare un valore per ogni identificatore dell'enumerazione, non ci sono limitazioni di segno e non e` necessario usare valori distinti (anche se cio` probabilmente comporterebbe qualche problema). Si puo` utilizzare anche un identificatore dell'enumerazione precedentemente definito.
La possibilita` di scegliere i valori da associare alle etichette (identificatori) dell'enumerazione fornisce un modo alternativo di definire costanti di tipo intero.
La keyword typedef
Esiste anche la possibilita` di dichiarare un alias per un altro tipo (non un nuovo tipo) utilizzando la parola chiave typedef:
typedef <Tipo> <Alias>;
Il listato seguente mostra alcune possibili applicazioni:
typedef unsigned short int PiccoloIntero;
typedef long double ArrayDiReali[20];
typedef struct {
long double ParteReale;
long double ParteImmaginaria;
} Complesso;
Il primo esempio mostra un caso molto semplice: creare un alias per un nome di tipo. Nel secondo caso invece viene mostrato come dichiarare un alias per un tipo "array di 20 long double". Infine il terzo esempio e` il piu` interessante perche` mostra un modo alternativo di dichiarare un nuovo tipo; in realta` ad essere pignoli non viene introdotto un nuovo tipo: la definizione di tipo che precede l'identificatore Complesso dichiara una struttura anonima e poi l'uso di typedef crea un alias per quel tipo struttura.
E` possibile dichiarare tipi anonimi solo per i costrutti struct, union e enum e sono utilizzabili quasi esclusivamente nelle dichiarazioni (come nel caso di typedef oppure nelle dichiarazioni di variabili e costanti).
La keyword typedef e` utile per creare abbreviazioni per espressioni di tipo complesse, soprattutto quando l'espressione di tipo coinvolge puntatori e funzioni.