Costruire nuovi tipi
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.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|