CORSO C++ STANDARD - Didattica.it
CORSO C++ STANDARD - Didattica.it
CORSO C++ STANDARD - Didattica.it
You also want an ePaper? Increase the reach of your titles
YUMPU automatically turns print PDFs into web optimized ePapers that Google loves.
<strong>CORSO</strong> <strong>C++</strong> <strong>STANDARD</strong><br />
Indice degli argomenti trattati<br />
• Introduzione<br />
Obiettivi e Prerequis<strong>it</strong>i<br />
Contenuto generale del Corso<br />
Nota storica<br />
• Caratteristiche generali del linguaggio<br />
"Case sens<strong>it</strong>iv<strong>it</strong>y"<br />
Moduli funzione<br />
Entry-point del programma: la funzione main<br />
Le tre parti di una funzione<br />
Aree di commento<br />
Primo programma di esempio (con tabella esplicativa di ogni<br />
simbolo usato)<br />
• Cominciamo dalla funzione printf<br />
Perché una funzione di I/O del C ?<br />
Operazioni della funzione printf<br />
Argomenti della funzione printf<br />
Scr<strong>it</strong>tura della control string sullo schermo<br />
Definizione di sequenza di escape<br />
Principali sequenze di escape<br />
La funzione printf con più argomenti<br />
Definizione di specificatore di formato<br />
Principali specificatori di formato in free-format<br />
Specificatori di formato con ampiezza di campo e precisione<br />
Altri campi degli specificatori di formato<br />
• Tipi, Variabili, Costanti<br />
Tipi delle variabili<br />
Tipi intrinseci del linguaggio<br />
Dichiarazione e definizione degli identificatori<br />
Qualificatori e specificatori di tipo<br />
Tabella di occupazione della memoria dei vari tipi di dati<br />
L'operatore sizeof<br />
Il tipo "booleano"<br />
Definizione con Inizializzazione<br />
Le Costanti in <strong>C++</strong><br />
Specificatore const<br />
• Visibil<strong>it</strong>à e tempo di v<strong>it</strong>a<br />
Visibil<strong>it</strong>à di una variabile<br />
Tempo di v<strong>it</strong>a di una variabile<br />
Visibil<strong>it</strong>à globale<br />
• Operatori e operandi<br />
Definizione di operatore e regole generali<br />
Operatore di assegnazione<br />
Operatori matematici<br />
Operatori a livello del b<strong>it</strong><br />
Operatori binari in notazione compatta<br />
Operatori relazionali
Operatori logici<br />
Operatori di incremento e decremento<br />
Operatore condizionale<br />
Conversioni di tipo<br />
Precedenza fra operatori (tabella)<br />
Ordine di valutazione<br />
• Introduzione all'I/O sui dispos<strong>it</strong>ivi standard<br />
Dispos<strong>it</strong>ivi standard di I/O<br />
Oggetti globali di I/O<br />
Operatori di flusso di I/O<br />
Output tram<strong>it</strong>e l'operatore di inserimento<br />
Input tram<strong>it</strong>e l'operatore di estrazione<br />
Memorizzazione dei dati introdotti da tastiera<br />
Comportamento in caso di errore in lettura<br />
• Il Compilatore GNU gcc in ambiente Linux<br />
Un compilatore integrato C/<strong>C++</strong><br />
Il progetto GNU<br />
Quale versione di gcc sto usando?<br />
I passi della compilazione<br />
Estensioni<br />
L'input/output di gcc<br />
Il valore rest<strong>it</strong>u<strong>it</strong>o al sistema<br />
Passaggi intermedi di compilazione<br />
I messaggi del compilatore<br />
Controlliamo i livelli di warning<br />
Compilare per effetture il debug<br />
Autopsia di un programma defunto<br />
Ottimizzazione<br />
Compilazione di un programma modulare<br />
Inclusione di librerie in fase di compilazione<br />
• Il Comando 'make' in ambiente Linux<br />
Perche' utilizzare il comando make?<br />
Il Makefile ed i target del make<br />
Dipendenze<br />
Macro e variabili ambiente<br />
Compiliamo con make<br />
Alcuni target standard<br />
• Istruzioni di controllo<br />
Istruzione di controllo if<br />
Istruzione di controllo while<br />
Istruzione di controllo do ... while<br />
Istruzione di controllo for<br />
Istruzioni continue, break e goto<br />
Istruzione di controllo sw<strong>it</strong>ch ... case<br />
• Array<br />
Cos'è un array ?<br />
Definizione e inizializzazione di un array<br />
L'operatore [ ]<br />
Array multidimensionali<br />
L'operatore sizeof e gli array<br />
Gli array in <strong>C++</strong><br />
• Stringhe di caratteri
Le stringhe come particolari array di caratteri<br />
Definizione di variabili stringa<br />
Inizializzazione di variabili stringa<br />
Funzioni di libreria gets e puts<br />
Conversioni fra stringhe e numeri<br />
Le stringhe in <strong>C++</strong><br />
• Funzioni<br />
Definizione di una funzione<br />
Dichiarazione di una funzione<br />
Istruzione return<br />
Comunicazioni fra programma chiamante e funzione<br />
Argomenti di default<br />
Funzioni con overload<br />
Funzioni inline<br />
Trasmissione dei parametri tram<strong>it</strong>e l'area stack<br />
Ricorsiv<strong>it</strong>à delle funzioni<br />
Funzioni con numero variabile di argomenti<br />
Cenni sulla Run Time Library<br />
• Riferimenti<br />
Costruzione di una variabile mediante copia<br />
Cosa sono i riferimenti ?<br />
Comunicazione per "riferimento" fra programma e funzione<br />
• Direttive al Preprocessore<br />
Cos'é il preprocessore ?<br />
Direttiva #include<br />
Direttiva #define di una costante<br />
Confronto fra la direttiva #define e lo specificatore const<br />
Direttiva #define di una macro<br />
Confronto fra la direttiva #define e lo specificatore inline<br />
Direttive condizionali<br />
Direttiva #undef<br />
• Sviluppo delle applicazioni in ambiente Windows<br />
Definizioni di IDE e di "progetto"<br />
Gestione di files e progetti<br />
Ed<strong>it</strong>or di testo<br />
Gestione delle finestre<br />
Costruzione dell'applicazione eseguibile<br />
Debug del programma<br />
Utilizzo dell'help in linea<br />
• Indirizzi e Puntatori<br />
Operatore di indirizzo &<br />
Cosa sono i puntatori ?<br />
Dichiarazione di una variabile di tipo puntatore<br />
Assegnazione di un valore a un puntatore<br />
Ar<strong>it</strong>metica dei puntatori<br />
Operatore di dereferenziazione *<br />
Puntatori a void<br />
Errori di dangling references<br />
Funzioni con argomenti puntatori<br />
• Puntatori ed Array<br />
Analogia fra puntatori ed array<br />
Combinazione fra operazioni di deref. e di incremento
Confronto fra operatore [ ] e deref. del puntatore "offsettato"<br />
Funzioni con argomenti array<br />
Funzioni con argomenti puntatori passati by reference<br />
Array di puntatori<br />
• Elaborazione della riga di comando<br />
Esecuzione di un programma tram<strong>it</strong>e riga di comando<br />
Argomenti passati alla funzione main<br />
• Puntatori e Funzioni<br />
Funzioni che rest<strong>it</strong>uiscono puntatori<br />
Puntatori a Funzione<br />
Array di puntatori a funzione<br />
Funzioni con argomenti puntatori a funzione<br />
• Puntatori e Costanti<br />
Puntatori a costante<br />
Puntatori costanti<br />
Puntatori costanti a costante<br />
Funzioni con argomenti costanti trasmessi by value<br />
Funzioni con argomenti costanti trasmessi by reference<br />
• Tipi defin<strong>it</strong>i dall'utente<br />
Concetti di oggetto e istanza<br />
Typedef<br />
Strutture<br />
Operatore .<br />
Puntatori a strutture - Operatore -><br />
Unioni<br />
Dichiarazione di strutture e membri di tipo struttura<br />
Strutture di tipo b<strong>it</strong> field<br />
Tipi enumerati<br />
• Allocazione dinamica della memoria<br />
Memoria stack e memoria heap<br />
Operatore new<br />
Operatore delete<br />
• Namespace<br />
Programmazione modulare e compilazione separata<br />
Definizione di namespace<br />
Risoluzione della visibil<strong>it</strong>à<br />
Membri di un namespace defin<strong>it</strong>i esternamente<br />
Namespace annidati<br />
Namespace sinonimi<br />
Namespace anonimi<br />
Estendibil<strong>it</strong>à della definizione di un namespace<br />
Parola-chiave using<br />
Precedenze e confl<strong>it</strong>ti fra i nomi<br />
Collegamento fra namespace defin<strong>it</strong>i in files diversi<br />
• Eccezioni<br />
Segnalazione e gestione degli errori<br />
Il costrutto try<br />
L'istruzione throw<br />
Il gestore delle eccezioni: costrutto catch<br />
Riconoscimento di un'eccezione fra diverse alternative<br />
Blocchi innestati<br />
Eccezioni che non sono errori
• Classi e data hiding<br />
Analogia fra classi e strutture<br />
Specificatori di accesso<br />
Data hiding<br />
Funzioni membro<br />
Risoluzione della visibil<strong>it</strong>à<br />
Funzioni-membro di sola lettura<br />
Classi membro<br />
Polimorfismo<br />
Puntatore nascosto this<br />
• Membri a livello di classe e accesso "friend"<br />
Membri di tipo enumerato<br />
Dati-membro statici<br />
Funzioni-membro statiche<br />
Funzioni friend<br />
Classi friend<br />
• Costruttori e distruttori degli oggetti<br />
Costruzione e distruzione di un oggetto<br />
Costruttori<br />
Costruttori e conversione implic<strong>it</strong>a<br />
Distruttori<br />
Oggetti allocati dinamicamente<br />
Membri puntatori<br />
Costruttori di copia<br />
Liste di inizializzazione<br />
Membri oggetto<br />
Array di oggetti<br />
Oggetti non locali<br />
Oggetti temporanei<br />
Util<strong>it</strong>à dei costruttori e distruttori<br />
• Overload degli operatori<br />
Estendibil<strong>it</strong>à del <strong>C++</strong><br />
Ridefinizione degli operatori<br />
Metodi della classe o funzioni esterne ?<br />
Il ruolo del puntatore nascosto this<br />
Overload degli operatori di flusso di I/O<br />
Operatori binari e conversioni<br />
Operatori unari e casting a tipo nativo<br />
Operatori in namespace<br />
Oggetti-array e array associativi<br />
Oggetti-funzione<br />
Puntatori intelligenti<br />
Operatore di assegnazione<br />
Ottimizzazione delle copie<br />
Espressioni-operazione<br />
• Ered<strong>it</strong>a'<br />
L'ered<strong>it</strong>à in <strong>C++</strong><br />
Classi base e derivata<br />
Accesso ai membri della classe base<br />
Conversioni fra classi base e derivata<br />
Costruzione della classe base<br />
Regola della dominanza
Ered<strong>it</strong>à e overload<br />
La dichiarazione using<br />
Ered<strong>it</strong>à multipla e classi basi virtuali<br />
• Polimorfismo<br />
Late binding e polimorfismo<br />
Ambigu<strong>it</strong>à dei puntatori alla classe base<br />
Funzioni virtuali<br />
Tabelle delle funzioni virtuali<br />
Costruttori e distruttori virtuali<br />
Scelta fra veloc<strong>it</strong>à e polimorfismo<br />
Classi astratte<br />
Un rudimentale sistema di figure geometriche<br />
Un rudimentale sistema di visualizzazione delle figure<br />
• Template<br />
Programmazione generica<br />
Definizione di una classe template<br />
Istanza di un template<br />
Parametri di default<br />
Funzioni template<br />
Differenze fra funzioni e classi template<br />
Template e modular<strong>it</strong>à<br />
• General<strong>it</strong>à sulla Libreria Standard del <strong>C++</strong><br />
Campi di applicazione<br />
Header files<br />
Il namespace std<br />
La Standard Template Library<br />
• La Standard Template Library<br />
General<strong>it</strong>à<br />
Iteratori<br />
Conten<strong>it</strong>ori Standard<br />
Algor<strong>it</strong>mi e oggetti-funzione<br />
• Una classe <strong>C++</strong> per le stringhe<br />
La classe string<br />
Confronto fra string e vector<br />
Il membro statico npos<br />
Costruttori e operazioni di copia<br />
Gestione degli errori<br />
Conversioni fra oggetti string e stringhe del C<br />
Confronti fra stringhe<br />
Concatenazioni e inserimenti<br />
Ricerca di sotto-stringhe<br />
Estrazione e sost<strong>it</strong>uzione di sotto-stringhe<br />
Operazioni di input-output<br />
• Librerie statiche e dinamiche in Linux<br />
Introduzione<br />
Librerie in ambiente Linux<br />
Un programma di prova<br />
Librerie statiche<br />
Come costruire una libreria statica<br />
Link con una libreria statica<br />
I lim<strong>it</strong>i del meccanismo del link statico<br />
Librerie condivise
Come costruire una libreria condivisa<br />
Link con una libreria condivisa<br />
La variabile ambiente LD_LIBRARY_PATH<br />
La flag -rpath<br />
Che tipo di libreria sto usando?<br />
Un aspetto pos<strong>it</strong>ivo dell'utilizzo delle librerie condivise<br />
Librerie statiche vs librerie condivise<br />
• Le operazioni di input-ouput in <strong>C++</strong><br />
La gerarchia di classi stream<br />
Operazioni di output<br />
Operazioni di input<br />
Stato dell'oggetto stream e gestione degli errori<br />
Formattazione e manipolatori di formato<br />
Cenni sulla bufferizzazione<br />
• Conclusioni
Obiettivi<br />
INTRODUZIONE<br />
Obiettivi e Prerequis<strong>it</strong>i<br />
Acquisire le conoscenze necessarie per lo sviluppo di applicazioni in linguaggio<br />
<strong>C++</strong>, usando la tecnica della "Programmazione orientata a Oggetti" o OOP<br />
(Object Oriented Programming").<br />
Un linguaggio di programmazione ha due scopi principali:<br />
1. Fornire i mezzi perchè il programmatore possa specificare le azioni da<br />
eseguire;<br />
2. Fornire un insieme di concetti per pensare a quello che può essere fatto.<br />
Il primo scopo richiede che il linguaggio sia vicino alla macchina (il C fu progettato<br />
con questo scopo); il secondo richiede che il linguaggio sia vicino al problema da<br />
risolvere, in modo che i concetti necessari per la soluzione siano esprimibili<br />
direttamente e in forma concisa. La OOP è stata appos<strong>it</strong>amente pensata per<br />
questo scopo e le potenzial<strong>it</strong>à aggiunte al C per creare il <strong>C++</strong> ne cost<strong>it</strong>uiscono<br />
l'aspetto principale e caratterizzante.<br />
Prerequis<strong>it</strong>i<br />
Conoscenza dei concetti base e della terminologia informatica (es. : linguaggio,<br />
programma, istruzione di programma, costante, variabile, funzione, operatore,<br />
locazione di memoria, codice sorgente, codice oggetto, compilatore, linker ecc…)<br />
Non è necessaria la conoscenza del C ! Infatti il <strong>C++</strong> è anche (ma non solo)<br />
un'estensione del C, che mantiene nel suo amb<strong>it</strong>o come sottoinsieme. E quindi un<br />
corso, base ed essenziale ma completo, di <strong>C++</strong>, è anche un corso di C.<br />
Livello di partenza<br />
Contenuto generale del Corso<br />
Concetti fondamentali di programmazione in C e <strong>C++</strong>, tenendo presente gli<br />
obiettivi, e quindi:
Livello avanzato<br />
• il C verrà trattato solo negli aspetti che si mantengono inalterati in <strong>C++</strong><br />
Esempi: le istruzioni di controllo if ….. else, i costrutti while, do...while,<br />
for ecc...<br />
• il C non verrà trattato dove è stato sost<strong>it</strong>u<strong>it</strong>o dal <strong>C++</strong><br />
Esempio : le funzioni C di allocazione di memoria malloc e free, sost<strong>it</strong>u<strong>it</strong>i<br />
in <strong>C++</strong> dagli operatori new e delete<br />
• il Corso riguarderà soltanto il <strong>C++</strong> standard, indipendentemente dalla<br />
piattaforma hardware o dal sistema operativo, e quindi non verranno<br />
trattati tutti quegli argomenti legati all'ambiente specifico<br />
La Programmazione a Oggetti: concetti di: tipo astratto, classe, istanza,<br />
incapsulamento, overload di funzioni e operatori, costruttore e distruttore,<br />
ered<strong>it</strong>arietà, polimorfismo, funzione virtuale, template ecc...<br />
La libreria standard del <strong>C++</strong> : classi iostream (per l'input-output) e string, classi<br />
conten<strong>it</strong>ore (vector, list, queue, stack, map ecc. ), algor<strong>it</strong>mi e <strong>it</strong>eratori.<br />
Nota Storica<br />
Il <strong>C++</strong> fu "inventato" nel 1980 dal ricercatore informatico danese Bjarne Stroustrup, che<br />
ricavò concetti già presenti in precedenti linguaggi (come il Simula67) per produrre una<br />
verisone modificata del C, che chiamò: "C con le classi". Il nuovo linguaggio univa la potenza e<br />
l'efficienza del C con la nov<strong>it</strong>à concettuale della programmazione a oggetti, allora ancora in<br />
stato "embrionale" (c'erano già le classi e l'ered<strong>it</strong>à, ma mancavano l'overload, le funzioni<br />
virtuali, i riferimenti, i template, la libreria e moltre altre cose).<br />
Il nome <strong>C++</strong> fu introdotto per la prima volta nel 1983, per suggerire la sua natura evolutiva dal<br />
C, nel quale ++ è l'operatore di incremento (taluni volevano chiamarlo D, ma <strong>C++</strong> prevalse, per<br />
i motivi detti).<br />
All'inizio, comunque, e per vari anni, il <strong>C++</strong> restò un esercizio quasi "privato" dell'Autore e dei<br />
suoi collaboratori, progettato e portato avanti, come egli stesso disse, "per rendere più facile e<br />
piacevole la scr<strong>it</strong>tura di buoni programmi".<br />
Tuttavia, alla fine degli anni 80, risultò chiaro che sempre più persone apprezzavano ed<br />
utilizzavano il linguaggio e che la sua standardizzazione formale era un obiettivo da perseguire.<br />
Nel 1990 si formò un com<strong>it</strong>ato per la standardizzazione del <strong>C++</strong>, cui ovviamente partecipò lo<br />
stesso Autore. Da allora in poi, il com<strong>it</strong>ato, nelle sue varie articolazioni, divenne il luogo<br />
deputato all'evoluzione e al raffinamento del linguaggio.<br />
Finalmente l'approvazione formale dello standard si ebbe alla fine del 1997. In questi ultimi<br />
anni il <strong>C++</strong> si è ulteriormente evoluto, soprattutto per quello che riguarda l'implementazione di<br />
nuove classi nella libreria standard.
Caratteristiche generali del linguaggio<br />
"Case sens<strong>it</strong>iv<strong>it</strong>y"<br />
Il linguaggio <strong>C++</strong> (come il C) distingue i caratteri maiuscoli da quelli minuscoli.<br />
Esempio: i nomi MiaVariabile e miavariabile indicano due variabili diverse<br />
Moduli funzione<br />
In <strong>C++</strong> (come in C) ogni modulo di programma è una funzione.<br />
Non esistono subroutines o altri tipi di sottoprogramma.<br />
Ogni funzione è identificata da un nome<br />
Entry point del programma: la funzione main<br />
Quando si manda in esecuzione un programma, questo inizia sempre dalla<br />
funzione identificata dalla parola chiave main<br />
Il main è chiamato dal sistema operativo, che gli può passare dei parametri; a<br />
sua volta il main può rest<strong>it</strong>uire al sistema un numero intero (di sol<strong>it</strong>o analizzato<br />
come possibile codice di errore).<br />
Le tre parti di una funzione<br />
• lista degli argomenti passati dal programma chiamante: vanno indicati fra parentesi<br />
tonde dopo il nome della funzione; void indica che non vi sono argomenti (si può
omettere)<br />
• blocco (amb<strong>it</strong>o di azione, amb<strong>it</strong>o di visibil<strong>it</strong>à, scope) delle istruzioni della funzione:<br />
va racchiuso fra parentesi graffe;<br />
ogni istruzione deve terminare con ";" (può estendersi su più righe o vi possono essere<br />
più istruzioni sulla stessa riga);<br />
un'istruzione è cost<strong>it</strong>u<strong>it</strong>a da una successione di "tokens": un "token" è il più piccolo<br />
elemento di codice individualmente riconosciuto dal compilatore; sono "tokens" : gli<br />
identificatori, le parole-chiave, le costanti letterali o numeriche, gli operatori e<br />
alcuni caratteri di punteggiatura;<br />
i blanks e gli altri caratteri "separatori" (horizontal or vertical tabs, new lines, formfeeds)<br />
fra un token e l'altro o fra un'istruzione e l'altra, sono ignorati; in assenza di "separatori"<br />
il compilatore analizza l'istruzione da sinistra a destra e tende, nei casi di ambigu<strong>it</strong>à, a<br />
separare il token più lungo possibile.<br />
Es. l'istruzione a = i+++j;<br />
può essere interpretata come: a = i + ++j; oppure come: a = i++ + j;<br />
il compilatore sceglie la seconda interpretazione.<br />
• tipo del valore di r<strong>it</strong>orno al programma chiamante: va indicato prima del nome della<br />
funzione ed è obbligatorio; se è void indica che non c'è valore di r<strong>it</strong>orno<br />
Commenti<br />
I commenti sono brani di programma (che il compilatore ignora) inser<strong>it</strong>i al solo<br />
scopo di documentazione, cioè per spiegare il significato delle istruzioni e così<br />
migliorare la leggibil<strong>it</strong>à del programma. Sono molto utili anche allo stesso autore,<br />
per ricordargli quello che ha fatto, quando ha necess<strong>it</strong>à di rivis<strong>it</strong>are il programma<br />
per esigenze di manutenzione o di aggiornamento. Un buon programma si<br />
caratterizza anche per il fatto che fa abbondante uso di commenti.<br />
In <strong>C++</strong> ci sono due modi possibili di inserire i commenti:<br />
• l'area di commento è introdotta dal doppio carattere /* e termina con il doppio carattere<br />
*/ (può anche estendersi su più righe)<br />
• l'area di commento inizia con il doppio carattere // e termina alla fine della riga<br />
Esempio di programma
Cominciamo dalla funzione "printf"<br />
Perché una funzione di input-output del C ?<br />
La funzione printf è importante perché utilizza gli specificatori di formato, che<br />
definiscono il modo di scrivere i dati (formattazione). Tali specificatori sono<br />
usati, con le stesse regole della printf, da tutte le funzioni (anche non di inputoutput),<br />
che eseguono conversioni di formato sui dati<br />
Operazioni della funzione printf<br />
La funzione printf formatta e scrive una serie di caratteri e valori sul dispos<strong>it</strong>ivo<br />
standard di output (stdout), associato di default allo schermo del video, e<br />
rest<strong>it</strong>uisce al programma chiamante il numero di caratteri effettivamente scr<strong>it</strong>ti<br />
(oppure un numero negativo in caso di errore).<br />
Quando si usa la funzione printf bisogna prima includere il file header<br />
<br />
Argomenti della funzione printf<br />
La funzione printf riceve dal programma chiamante uno o più argomenti. Solo il<br />
primo è obbligatorio e deve essere una stringa, che si chiama control string<br />
(stringa di controllo)<br />
Scr<strong>it</strong>tura della control string sullo schermo
Quando printf è chiamata con un solo argomento, la control string viene<br />
trasfer<strong>it</strong>a sullo schermo, carattere per carattere (compresi gli spazi bianchi), salvo<br />
quando sono incontrati i seguenti caratteri particolari:<br />
" (termina la control string)<br />
% (introduce uno specificatore di formato - da non usare in questo caso)<br />
\ (introduce una sequenza di escape)<br />
Sequenze di escape<br />
Il carattere \ (backslash) non viene trasfer<strong>it</strong>o sullo schermo, ma utilizzato in<br />
combinazione con i caratteri successivi (un solo carattere se si tratta di una<br />
lettera, oppure una sequenza di cifre numeriche); l'insieme viene detto: escape<br />
sequence, e viene interpretato come un unico carattere.<br />
Le sequenze di escape sono usate tipicamente per specificare caratteri speciali<br />
che non hanno il loro equivalente stampabile (come newline, carriage return,<br />
tabulazioni, suoni ecc...), oppure caratteri, che da soli, hanno una funzione<br />
speciale, come le virgolette o lo stesso backslash<br />
Principali sequenze di escape<br />
\a suona il campanello (bell) \b carattere backspace<br />
\f salta pagina (form-feed) \n va a capo (newline)<br />
\r<br />
r<strong>it</strong>orno carrello (carriagereturn)<br />
\t tabulazione orizzontale<br />
\\ carattere backslash \" carattere virgolette<br />
\nnn carattere con codice ascii nnn (tre cifre in ottale)<br />
\nn carattere con codice ascii nn (due cifre in esadecimale)<br />
%% carattere "%" - atipico: è introdotto da % anziché da \<br />
\ da solo alla fine della riga = continua la control string nella riga successiva
La funzione printf con più argomenti<br />
Eventuali altri argomenti successivi alla control string, nella chiamata a printf,<br />
rappresentano i dati da formattare e scrivere, e possono essere cost<strong>it</strong>u<strong>it</strong>i da<br />
costanti, variabili, espressioni, o altre funzioni (in questo caso in realtà<br />
l'argomento è il valore di r<strong>it</strong>orno della funzione, la quale viene esegu<strong>it</strong>a prima<br />
della printf). Per il momento, dato che le variabili non sono state ancora<br />
introdotte, supponiamo che i dati siano cost<strong>it</strong>u<strong>it</strong>i da costanti o da espressioni<br />
fra costanti<br />
Specificatori di formato<br />
Ad ogni argomento successivo alla control string, deve corrispondere, all'interno<br />
della stessa control string e nello stesso ordine, uno specificatore di formato,<br />
cost<strong>it</strong>u<strong>it</strong>o da un gruppo di caratteri introdotto dal carattere "%". Nella sua forma<br />
generale uno specificatore di formato ha la seguente sintassi:<br />
%[flags][width][.precision]type<br />
dove i termini indicati con il colore fuchsia cost<strong>it</strong>uiscono i campi dello<br />
specificatore (senza spazi in mezzo), e sono tutti opzionali salvo l'ultimo (type),<br />
che determina come deve essere interpretato il corrispondente argomento della<br />
printf (numero intero, numero floating, carattere, stringa ecc...). I campi<br />
opzionali controllano invece il formato di scr<strong>it</strong>tura. Se sono omessi, cioè lo<br />
specificatore assume la forma minima %type, i dati sono scr<strong>it</strong>ti in free-format<br />
(cioè in modo da occupare lo spazio strettamente necessario). Per esempio, se a<br />
un certo punto della control string compare lo specificatore %d, significa che in<br />
quella posizione deve essere scr<strong>it</strong>to, in free-format, il valore del corrispondente<br />
argomento della printf, espresso come numero intero decimale, come nel<br />
caso della seguente istruzione:<br />
printf("Ci sono %d iscr<strong>it</strong>ti a questo corso!\nTemevo fossero solo<br />
%d!",3215+1,2);<br />
che scrive su video la frase:<br />
Ci sono 3216 iscr<strong>it</strong>ti a questo corso!<br />
Temevo fossero solo 2!
Principali specificatori di formato in free-format<br />
In uno specificatore di formato il campo obbligatorio type può assumere uno<br />
dei seguenti valori:<br />
u, o, x<br />
valori interi assoluti, basi: decimale, ottale,<br />
esadecimale<br />
X come x ma con le cifre letterali maiuscole<br />
d, i valori interi relativi, base decimale<br />
f, e valori floating, notazione normale o esponenziale<br />
g come f o e (sceglie il più comodo)<br />
E, G come e e g (scrive "E" al posto di "e")<br />
c carattere<br />
s stringa di caratteri<br />
p indirizzo di memoria (in esadecimale)<br />
Specificatori di formato con ampiezza di campo e precisione<br />
Anziché in free-format, si possono scrivere i dati in formato defin<strong>it</strong>o, tram<strong>it</strong>e gli<br />
specificatori numerici di ampiezza di campo e precisione.<br />
In uno specificatore di formato il campo opzionale width, cost<strong>it</strong>u<strong>it</strong>o da un<br />
numero intero pos<strong>it</strong>ivo, determina l'ampiezza di campo, cioè il numero di<br />
caratteri minimo con cui deve essere scr<strong>it</strong>to il dato corrispondente. Se il numero di<br />
caratteri effettivo è inferiore, il campo viene riemp<strong>it</strong>o (normalmente) a sinistra con<br />
spazi bianchi; se invece il numero è superiore, il campo viene espanso fino a<br />
raggiungere la lunghezza effettiva (in altre parole il dato viene sempre scr<strong>it</strong>to per<br />
intero, anche se il valore specificato in width è insufficiente). Se al posto di un<br />
numero si specifica nel campo width un asterisco, il valore viene desunto in<br />
esecuzione dalla lista degli argomenti della printf; in questo caso il valore<br />
dell'ampiezza di campo deve precedere immediatamente il dato a cui lo<br />
specificatore in esame si riferisce.<br />
In uno specificatore di formato il campo opzionale precision, se presente,<br />
deve essere sempre preceduto da un punto (che lo separa dal campo width), ed<br />
[p02]
è cost<strong>it</strong>u<strong>it</strong>o da un numero intero non negativo, con significato che dipende dal<br />
contenuto del campo obbligatorio type, come si evince dalla seguente tabella:<br />
contenuto<br />
campo type<br />
d,i,u,o,x,X<br />
(valori interi)<br />
f,e,E<br />
(valori<br />
floating)<br />
g,G<br />
(valori<br />
floating)<br />
c<br />
(carattere)<br />
s<br />
(stringa)<br />
significato campo precision<br />
La precisione specifica il minimo<br />
numero di cifre che devono essere<br />
scr<strong>it</strong>te. Se il numero di cifre effettive<br />
del dato corrispondente è minore<br />
della precisione, vengono scr<strong>it</strong>ti<br />
degli zeri sulla sinistra fino a<br />
completare il campo. Se invece il<br />
numero di cifre effettive è superiore,<br />
il dato è comunque scr<strong>it</strong>to per intero<br />
senza nessun troncamento. Infine, se<br />
la precisione è .0 (oppure<br />
semplicemente .) e il dato è zero,<br />
non viene scr<strong>it</strong>to nulla.<br />
La precisione specifica il numero di<br />
cifre che devono essere scr<strong>it</strong>te dopo il<br />
punto decimale. L'ultima cifra è<br />
arrotondata. Se la precisione è .0<br />
(oppure semplicemente .), non è<br />
scr<strong>it</strong>to neppure il punto decimale (in<br />
questo caso è arrotondata la cifra<br />
intera delle un<strong>it</strong>à).<br />
La precisione specifica il massimo<br />
numero di cifre significative che<br />
devono essere scr<strong>it</strong>te. L'ultima cifra è<br />
arrotondata. Gli zeri non significativi a<br />
destra non vengono scr<strong>it</strong>ti.<br />
La precisione non ha effetto.<br />
La precisione specifica il massimo<br />
numero di caratteri che devono<br />
essere scr<strong>it</strong>ti. I caratteri in eccesso<br />
non vengono scr<strong>it</strong>ti.<br />
1<br />
default<br />
6 cifre decimali<br />
6 cifre significative<br />
La stringa è scr<strong>it</strong>ta per<br />
intero<br />
Come per l'ampiezza di campo, anche per la precisione, se al posto di un<br />
numero si specifica un asterisco, il valore viene desunto in esecuzione dalla lista<br />
degli argomenti della printf; anche in questo caso il valore della precisione deve<br />
precedere immediatamente il dato a cui lo specificatore in esame si riferisce.<br />
Altri campi degli specificatori di formato
-<br />
+<br />
In uno specificatore di formato il campo opzionale flags è cost<strong>it</strong>u<strong>it</strong>o da uno o<br />
più caratteri, ciascuno dei quali svolge una funzione particolare, come descr<strong>it</strong>to<br />
dalla seguente tabella:<br />
flag<br />
spazio<br />
0<br />
# (usato<br />
con<br />
o,x,X)<br />
# (usato<br />
con<br />
e,E,f,g,G)<br />
# (usato<br />
con g,G)<br />
significato<br />
Allinea la scr<strong>it</strong>tura a sinistra in un campo con<br />
ampiezza specificata da width.<br />
Mette il segno (+ o –) davanti al numero.<br />
Mette uno spazio bianco davanti al numero,<br />
se questo è pos<strong>it</strong>ivo.<br />
Aggiunge zeri sulla sinistra fino a raggiungere<br />
l'ampiezza specificata da width. Se appaiono<br />
insieme 0 e -, 0 è ignorato.<br />
Mette davanti a ogni valore diverso da zero i<br />
prefissi 0, 0x, o 0X, rispettivamente.<br />
Scrive sempre il punto decimale.<br />
Riempie tutto il campo con ampiezza<br />
specificata da width, scrivendo anche gli zeri<br />
non significativi.<br />
default<br />
Allineamento a destra<br />
Il segno appare solo se il<br />
numero è negativo<br />
Nessuno spazio davanti<br />
ai numeri pos<strong>it</strong>ivi<br />
Il campo specificato da<br />
width è riemp<strong>it</strong>o da<br />
spazi bianchi<br />
Nessun prefisso davanti<br />
ai numeri<br />
Il punto decimale è<br />
scr<strong>it</strong>to solo se è segu<strong>it</strong>o<br />
da altre cifre<br />
Gli zeri non significativi<br />
non vengono scr<strong>it</strong>ti
Classificazione delle variabili in tipi<br />
Tipi, Variabili, Costanti<br />
Tipi delle variabili<br />
Si dice che il <strong>C++</strong> (come il C) è un linguaggio "tipato", per il fatto che pretende<br />
che di ogni variabile venga dichiarato il tipo di appartenenza.<br />
Definizione di tipo di una variabile<br />
Il tipo è un termine di classificazione che raggruppa tutte quelle variabili che sono<br />
memorizzate nello stesso modo e a cui si applica lo stesso insieme di operazioni.<br />
Controllo forte sui tipi<br />
Il <strong>C++</strong> eserc<strong>it</strong>a un forte controllo sui tipi (strong type checking), nel senso che<br />
regola e lim<strong>it</strong>a la conversione da un tipo all'altro (casting) e controlla<br />
l'interazione fra variabili di tipo diverso.<br />
Tipi intrinseci del linguaggio<br />
In <strong>C++</strong> esistono solo 5 tipi, detti "intrinseci o "nativi" del linguaggio :<br />
int<br />
char<br />
float<br />
double<br />
bool<br />
numero intero di 2 o 4 byte<br />
numero intero di 1 byte (interpretabile come codice ascii di un<br />
carattere)<br />
numero in virgola mobile con 6-7 cifre significative (4 byte )<br />
numero in virgola mobile con 15-16 cifre significative (8 byte )<br />
valore booleano: true o false (1 byte )<br />
In realtà il numero di tipi possibili è molto più grande, sia perché ogni tipo nativo<br />
può essere specializzato mediante i qualificatori di tipo, sia perché il<br />
programma stesso può creare propri tipi personalizzati (detti "tipi astratti")
Cos'è un identificatore ?<br />
Dichiarazione e definizione degli identificatori<br />
Un identificatore è un nome simbolico che il programma assegna a un'ent<strong>it</strong>à del<br />
linguaggio, per modo che il compilatore sia in grado di riconoscere quell'ent<strong>it</strong>à<br />
ogni volta che incontra il nome che le è stato assegnato.<br />
Sono pertanto identificatori i nomi delle variabili, delle funzioni, degli array,<br />
dei tipi astratti, delle strutture, delle classi ecc...<br />
Ogni identificatore consiste di una sequenza di lettere (maiuscole o minuscole)<br />
e di cifre numeriche, senza caratteri di altro tipo o spazi bianchi (a parte<br />
l'underscore "_", che è considerato una lettera). Il primo carattere deve essere<br />
una lettera.<br />
Non sono validi gli identificatori che coincidono con le parole-chiave del<br />
linguaggio (come da Tabella sotto riportata).<br />
Esempi di identificatori validi: hello deep_space9 a123<br />
_7bello<br />
Esempi di identificatori non validi:<br />
Tabella delle parole-chiave del <strong>C++</strong><br />
un amico (contiene uno spazio)<br />
un'amica (contiene un apostrofo)<br />
7bello (il primo carattere non è una lettera)<br />
for (è una parola-chiave del <strong>C++</strong>)<br />
auto bool break case<br />
catch char class const<br />
const_class continue default delete<br />
do double dynamic_cast else<br />
enum explic<strong>it</strong> extern false<br />
float for friend goto<br />
if inline int long<br />
main mutable namespace new<br />
operator private protected public<br />
register reinterpret_class return short<br />
signed sizeof static static_cast<br />
struct sw<strong>it</strong>ch template this<br />
throw true try typedef
typeid typename union unsigned<br />
using virtual void volatile<br />
wmain while<br />
Dichiarazione obbligatoria degli identificatori<br />
In <strong>C++</strong> tutti gli identificatori di un programma devono essere dichiarati prima<br />
di essere utilizzati (non necessariamente all'inizio del programma), cioè deve<br />
essere specificato il loro tipo. Per dichiarare un identificatore bisogna scrivere<br />
un'istruzione appos<strong>it</strong>a in cui l'identificatore è preceduto dal tipo di<br />
appartenenza. Es.<br />
int Variabile_Intera;<br />
Più identificatori dello stesso tipo possono essere dichiarati nella stessa<br />
istruzione e separati l'uno dall'altro da una virgola. Es.<br />
int ore, giorni, mesi;<br />
Definizione obbligatoria degli identificatori<br />
Un'istruzione di dichiarazione si lim<strong>it</strong>a ad informare il compilatore del <strong>C++</strong> che<br />
un certo identificatore appartiene a un certo tipo, ma può non essere<br />
considerata in fase di esecuzione del programma. Quando una dichiarazione<br />
comporta anche un'operazione eseguibile, allora si dice che è anche una<br />
definizione.<br />
Per esempio, l'istruzione: extern int error_number;<br />
è soltanto una dichiarazione, in quanto (come vedremo più avanti) con lo<br />
specificatore extern informa il compilatore (o meglio il linker) che la variabile<br />
error_number è defin<strong>it</strong>a in un altro file del programma (e quindi l'istruzione<br />
serve solo ad identificare il tipo della variabile e a permetterne l'utilizzo);<br />
mentre l'istruzione: int error_number;<br />
è anche una definizione, in quanto non si lim<strong>it</strong>a ad informare il compilatore che<br />
la variabile error_number è di tipo int, ma crea la variabile stessa, allocando<br />
un'appos<strong>it</strong>a area di memoria.<br />
Per meglio comprendere la differenza fra dichiarazione e definizione, si<br />
considerino le seguenti regole:<br />
• tutte le definizioni sono anche dichiarazioni (ma non è vero il<br />
contrario);<br />
• deve esserci una ed una sola definizione per ogni identificatore che<br />
appare nel programma (o meglio, per ogni identificatore che appare in<br />
uno stesso amb<strong>it</strong>o, altrimenti si tratta di identificatori diversi, pur<br />
avendo lo stesso nome), mentre possono esserci più dichiarazioni<br />
(purchè non in contraddizione fra loro);
• un identificatore deve essere dichiarato prima del suo utilizzo, ma può<br />
essere defin<strong>it</strong>o dopo (o altrove, come abbiamo visto nell'esempio<br />
precedente);<br />
• la semplice dichiarazione (cioè senza specificatore) di una variabile di<br />
tipo nativo è sempre anche una definizione, in quanto comporta<br />
l'allocazione di un'area di memoria;<br />
Qualificatori e specificatori di tipo<br />
Definizione di "qualificatore" e "specificatore"<br />
Un qualificatore di tipo è una parola-chiave che, in una istruzione di<br />
dichiarazione, si premette a un tipo nativo, per indicare il modo in cui la<br />
variabile dichiarata deve essere immagazzinata in memoria. Se il tipo è omesso, è<br />
sottointeso int.<br />
Esistono 4 qualificatori: short, long, signed, unsigned.<br />
Uno specificatore è una parola-chiave che, in una istruzione di dichiarazione,<br />
si premette al tipo (che può essere qualsiasi, anche non nativo) e all'eventuale<br />
qualificatore, per definire ulteriori caratteristiche dell'ent<strong>it</strong>à dichiarata. Esistono<br />
svariati tipi di specificatori, con funzioni diverse: li introdurremo via via durante<br />
il corso, quando sarà necessario.<br />
Qualificatori short e long<br />
I qualificatori short e long si applicano al tipo int. Essi definiscono la<br />
dimensione della memoria occupata dalle rispettive variabili di appartenenza.<br />
Purtroppo lo standard non garantisce che tale dimensione rimanga inalterata<br />
trasportando il programma da una piattaforma all'altra, in quanto essa dipende<br />
esclusivamente dalla piattaforma utilizzata. Possiamo solo dire così: a tutt'oggi,<br />
nelle implementazioni più diffuse del <strong>C++</strong> , il qualificatore short definisce<br />
variabili di 16 b<strong>it</strong> (2 byte) e il qualificatore long definisce variabili di 32 b<strong>it</strong> (4<br />
byte), mentre il tipo int "puro" definisce variabili di 32 b<strong>it</strong> (cioè long e int sono<br />
equivalenti).<br />
Vedremo fra poco che esiste un operatore che permette di conoscere la effettiva<br />
occupazione di memoria dei diversi tipi di variabili.<br />
Per completezza aggiungiamo che il qualificatore long si può applicare anche al<br />
tipo double (la cosidetta "precisione estesa"), ma, da prove fatte sulle<br />
macchine che generalmente usiamo, è risultato che conviene non applicarlo!<br />
Qualificatori signed e unsigned<br />
I qualificatori signed e unsigned si applicano ai tipi "interi" int e char. Essi<br />
determinano se le rispettive variabili di appartenenza possono assumere o meno<br />
valori negativi.
E' noto che i numeri interi negativi sono rappresentati in memoria mediante<br />
l'algor<strong>it</strong>mo del "complemento a 2" (dato un numero N rappresentato da una<br />
sequenza di b<strong>it</strong>, -N si rappresenta invertendo tutti i b<strong>it</strong> e aggiungendo 1). E' pure<br />
noto che, in un'area di memoria di m b<strong>it</strong>, esistono 2 m diverse possibili<br />
configurazioni (cioè un numero intero può assumere 2 m valori). Pertanto un<br />
numero con segno ha un range (intervallo) di variabil<strong>it</strong>à da -2 m-1 a +2 m-1 -1,<br />
mentre un numero assoluto va da 0 a +2 m -1.<br />
Se il tipo è int, i qualificatori signed e unsigned possono essere combinati<br />
con short e long, dando luogo, insieme a signed char e unsigned char, a 6<br />
diversi tipi interi possibili.<br />
E i tipi int e char "puri" ? Il tipo int è sempre con segno (e quindi signed int e<br />
int sono equivalenti), mentre, per quello che riguarda il tipo char, ancora una<br />
volta dipende dall'implementazione: "in generale" (ma non sempre) coincide con<br />
signed char.
L'operatore sizeof<br />
L'operatore sizeof(operando) rest<strong>it</strong>uisce la lunghezza in byte di identificatori<br />
appartenenti a un dato tipo; operando specifica il tipo in esame o un qualunque<br />
identificatore dichiarato di quel tipo. Per esempio, sizeof(int) può essere usato<br />
per sapere se il tipo int è di 2 o di 4 byte.<br />
Confronto dei risultati fra diverse arch<strong>it</strong>etture<br />
tipo<br />
Lunghezza della voce di memoria in byte<br />
PC (32 b<strong>it</strong>)<br />
con Windows<br />
PC (32 b<strong>it</strong>)<br />
con Linux<br />
DEC ALPHA (64 b<strong>it</strong>)<br />
con Unix<br />
char 1 1 1<br />
[p04]
short 2 2 2<br />
int 4 4 4<br />
long 4 4 8<br />
float 4 4 4<br />
double 8 8 8<br />
long double 8 12 16<br />
bool 1 1 1<br />
Definizione con Inizializzazione<br />
Abbiamo visto finora che ogni dichiarazione o definizione di un identificatore<br />
consiste di tre parti:<br />
• uno o più specificatori (opzionali);<br />
• il tipo (eventualmente preceduto da uno o più qualificatori);<br />
• l'identificatore.<br />
NOTA<br />
Per completezza aggiungiamo che a sua volta l'identificatore può essere<br />
preceduto (e/o segu<strong>it</strong>o) da un "operatore di dichiarazione".<br />
I più comuni operatori di dichiarazione sono:<br />
* puntatore prefisso<br />
*const puntatore costante prefisso<br />
& riferimento prefisso<br />
[] array suffisso<br />
( ) funzione suffisso<br />
Ne parleremo al momento opportuno.<br />
Esiste una quarta parte, opzionale, che si chiama inizializzatore (e che si può<br />
aggiungere solo nel caso della definizione di una variabile): un inizializzatore è<br />
un'espressione che definisce il valore iniziale assunto dalla variabile, ed è<br />
separato dal resto della definizione dall'operatore "=".<br />
Quindi, ricap<strong>it</strong>olando (nel caso che l'identificatore sia il nome di una variabile):<br />
• la semplice dichiarazione assegna un tipo alla variabile;
• la definizione crea la variabile in memoria, ma non il suo contenuto, che<br />
rimane, per il momento, indefin<strong>it</strong>o (forse resta quello che c'era prima nella<br />
stessa locazione fisica di memoria);<br />
• la definizione con inizializzazione attribuisce un valore iniziale alla<br />
variabile defin<strong>it</strong>a.<br />
Es. unsigned peso = 57;<br />
n.b. un'inizializzazione è concettualmente diversa da un'assegnazione<br />
In <strong>C++</strong> i valori di inizializzazione possono essere dati non solo da costanti, ma<br />
anche da espressioni che includono variabili defin<strong>it</strong>e precedentemente.<br />
Es. int lordo = 45; int tara = 23; int netto = lordo-tara;<br />
Il tipo "booleano"<br />
Il tipo bool non faceva parte inizialmente dei tipi nativi del C e solo<br />
recentemente è stato introdotto nello standard del <strong>C++</strong>.<br />
Una variabile "booleana" (cioè dichiarata bool) può assumere solo due valori:<br />
true e false. Tuttavia, dal punto di vista dell'occupazione di memoria, il tipo<br />
bool è identico al tipo char, cioè occupa un intero byte (anche se in pratica<br />
utilizza un solo b<strong>it</strong>).<br />
Nelle espressioni ar<strong>it</strong>metiche e logiche valori booleani e interi possono essere<br />
mescolati insieme: se un booleano è convert<strong>it</strong>o in un intero, per definizione<br />
true corrisponde al valore 1 e false corrisponde al valore 0; viceversa, se un<br />
intero è convert<strong>it</strong>o in un booleano, tutti i valori diversi da zero diventano true<br />
e zero diventa false. Esempi:<br />
Costanti intere<br />
bool b = 7; ( b è inizializzata con true )<br />
int i = true; ( i è inizializzata con 1 )<br />
int i = 7 < 2; ( espressione falsa: i è inizializzata con 0 )<br />
Le Costanti in <strong>C++</strong>
Una costante intera è un numero decimale (base 10), ottale (base 8) o<br />
esadecimale (base 16) che rappresenta un valore intero pos<strong>it</strong>ivo o negativo. Un<br />
numero senza prefissi o suffissi è interpretato in base decimale e di tipo int (o<br />
unsigned int se la costante specificata è maggiore del massimo numero pos<strong>it</strong>ivo<br />
signed int). La prima cifra del numero non deve essere 0.<br />
Un numero con prefisso 0 è interpretato in base ottale e di tipo int (o<br />
unsigned int).<br />
Es. a = 0100; (in a è memorizzato il numero 64)<br />
Un numero con prefisso 0x o 0X è interpretato in base esadecimale e di tipo<br />
tipo int (o unsigned int). Le "cifre" a,b,c,d,e,f possono essere scr<strong>it</strong>te sia in<br />
maiuscolo che in minuscolo.<br />
Es. a = 0x1B; (in a è memorizzato il numero 27)<br />
In qualunque caso, la presenza del suffisso L indica che il numero deve essere di<br />
tipo long int, mentre la presenza del suffisso U indica che il numero deve essere<br />
di tipo unsigned int.<br />
Es. a = 0x1BL; a = 0x1BU;<br />
Costanti in virgola mobile<br />
Una costante in virgola mobile è un numero decimale (base 10), che<br />
rappresenta un valore reale pos<strong>it</strong>ivo o negativo.<br />
Può essere specificato in 2 modi:<br />
• parte_intera.parte_decimale (il punto è obbligatorio)<br />
• notazione esponenziale (il punto non è obbligatorio)<br />
Esempi: 15.75 -1.5e2 25E-4 10.<br />
In qualunque notazione, se il numero è scr<strong>it</strong>to senza suffissi, è assunto di tipo<br />
double. Per forzare il tipo float bisogna apporre il suffisso f (o F)<br />
Es. 10.3 è di tipo double 1.4e-5f è di tipo float<br />
Costanti carattere<br />
Una costante carattere è rappresentata inserendo fra singoli apici un carattere<br />
stampabile oppure una sequenza di escape.<br />
Esempi: 'A' carattere A<br />
'\n' carattere newline<br />
'\003' carattere cuoricino<br />
In memoria un carattere è rappresentato da un numero intero di 1 byte (il suo<br />
codice ascii). Le conversioni fra tipo char e tipo int sono automatiche (purché il<br />
valore intero da convertire sia compreso nel range del tipo char) e quindi i due<br />
tipi possono essere mescolati insieme nelle espressioni ar<strong>it</strong>metiche.<br />
Per esempio, l'operazione: MiaVar = 'A' + 1;<br />
è ammessa, se la variabile MiaVar è stata dichiarata int oppure char.<br />
Il carattere NULL ha codice ascii 0 e si rappresenta con '\0' (da non<br />
confondere con il carattere 0 decimale che ha codice ascii 48).
Costanti stringa<br />
Una costante stringa è rappresentata inserendo un insieme di caratteri (fra cui<br />
anche sequenze di escape) fra doppi apici (virgolette).<br />
Es. "Ciao Universo\n"<br />
In <strong>C++</strong> (come in C) non esistono le stringhe come tipo intrinseco. Infatti esse<br />
sono defin<strong>it</strong>e come sequenze (array) di caratteri, con una differenza rispetto ai<br />
normali array: il compilatore, nel creare una costante stringa, aggiunge<br />
automaticamente un NULL dopo l'ultimo carattere (si dice che le stringhe sono<br />
"array di caratteri null terminated"). E quindi, per esempio, 'A' e "A" sono<br />
due costanti diverse :<br />
'A' è un carattere e occupa 1 byte (con il numero 65)<br />
"A" è una stringa e occupa 2 byte (con i numeri 65 e 0)<br />
Per inizializzare una stringa bisogna definirla di tipo char e aggiungere al<br />
nome della variabile l'operatore di dichiarazione [].<br />
Es. char MiaStr[] = "Sono una stringa";<br />
Specificatore const<br />
Se nella definizione di una variabile, si premette al tipo (e ai suoi eventuali<br />
qualificatori), lo specificatore const, il contenuto della variabile non può più<br />
essere modificato. Ovviamente una variabile defin<strong>it</strong>a const deve sempre essere<br />
inizializzata.<br />
Es. const double pigreco = 3.14159265385;<br />
L'uso di const è fortemente consigliato rispetto all'alternativa di scrivere più volte<br />
la stessa costante nelle istruzioni del programma; infatti se il programmatore<br />
decide di cambiarne il valore, e ha usato const, è sufficiente che modifichi la sola<br />
istruzione di definizione.<br />
D'ora in poi, quando parleremo di "costanti", intenderemo riferirci a "variabili<br />
defin<strong>it</strong>e const" (distinguendole dalle costanti "dirette" che saranno invece<br />
chiamate "costanti letterali" o "l<strong>it</strong>erals").
Amb<strong>it</strong>o di azione<br />
Visibil<strong>it</strong>à e tempo di v<strong>it</strong>a<br />
Visibil<strong>it</strong>à di una variabile<br />
Abbiamo visto che, in via del tutto generale, si definisce amb<strong>it</strong>o di azione (o<br />
amb<strong>it</strong>o di visibil<strong>it</strong>à o scope) l'insieme di istruzioni di programma comprese fra<br />
due parentesi graffe: {....}.<br />
Le istruzioni di una funzione devono essere comprese tutte nello stesso amb<strong>it</strong>o;<br />
ciò non esclude che si possano definire più amb<strong>it</strong>i innestati l'uno dentro l'altro<br />
(ovviamente il numero di parentesi chiuse deve bilanciare quello di parentesi<br />
aperte, e ogni parentesi chiusa termina l'amb<strong>it</strong>o iniziato con la parentesi aperta<br />
più interna).<br />
Variabili locali<br />
In ogni caso una variabile è visibile al programma e utilizzabile solo nello stesso<br />
amb<strong>it</strong>o in cui è defin<strong>it</strong>a (variabili locali). Se si tenta di utilizzare una variabile in<br />
amb<strong>it</strong>i diversi da quello in cui è defin<strong>it</strong>a (o in amb<strong>it</strong>i superiori in caso di più<br />
amb<strong>it</strong>i innestati), il compilatore non la riconosce.<br />
Il <strong>C++</strong> ammette che si ridefinisca più volte la stessa variabile, purché in amb<strong>it</strong>i<br />
diversi; in questo caso riconosce la variabile defin<strong>it</strong>a nel proprio amb<strong>it</strong>o o in<br />
quello superiore più vicino.<br />
Variabili globali<br />
Una variabile è globale, cioè è visibile in tutto il programma, solo se è defin<strong>it</strong>a<br />
al di fuori di qualunque amb<strong>it</strong>o (che viene per questo defin<strong>it</strong>o: amb<strong>it</strong>o globale).<br />
Le definizioni (con eventuali inizializzazioni) sono le uniche istruzioni del<br />
linguaggio che possono anche risiedere esternamente all'amb<strong>it</strong>o delle funzioni.<br />
In caso di concorrenza fra una variabile globale e una locale viene riconosciuta<br />
la variabile locale; tuttavia la variabile globale prevale se è specificata con<br />
prefisso :: (operatore di riferimento globale).<br />
Tempo di v<strong>it</strong>a di una variabile
Variabili automatiche<br />
Una variabile è detta automatica (o dinamica), se cessa di esistere non appena<br />
il flusso del programma esce dalla funzione in cui la variabile è defin<strong>it</strong>a. Se il<br />
flusso del programma torna nella funzione, la variabile viene ricreata ex-novo e, in<br />
particolare, viene reinizializzata sempre con lo stesso valore. Tutte le variabili<br />
locali sono, per default, automatiche ("tempo di v<strong>it</strong>a" lim<strong>it</strong>ato all'esecuzione<br />
della funzione).<br />
Variabili statiche<br />
Una variabile è detta statica se il suo "tempo di v<strong>it</strong>a" coincide con l'intera<br />
durata del programma: quando il flusso del programma torna nella funzione in<br />
cui è defin<strong>it</strong>a una variabile statica, r<strong>it</strong>rova la variabile come l'aveva lasciata (cioè<br />
con lo stesso valore); ciò significa in particolare che l'istruzione di definizione<br />
(con eventuale annessa inizializzazione) viene esegu<strong>it</strong>a solo la prima volta. Per<br />
ottenere che una variabile sia statica, bisogna preporre lo specificatore static<br />
nella definizione della variabile.<br />
Esiste anche, per le variabili automatiche, lo specificatore auto, ma è inutile<br />
in quanto di default (può essere usato per migliorare la leggibil<strong>it</strong>à del<br />
programma).<br />
A differenza dalle variabili automatiche, (in cui, in assenza di inizializzatore, il<br />
contenuto iniziale è indefin<strong>it</strong>o), le variabile statiche sono inizializzate di default a<br />
zero (in modo appropriato al tipo).<br />
Variabili globali statiche<br />
Visibil<strong>it</strong>à globale<br />
Una variabile locale può essere automatica o statica; una variabile globale è<br />
sempre statica (se è visibile dall'esterno deve essere anche viva!) e quindi lo<br />
specificatore static non avrebbe significato.<br />
In realtà, nella definizione di una variabile globale, lo specificatore static ha<br />
un significato differente: quello di lim<strong>it</strong>are la visibil<strong>it</strong>à della variabile al solo file in<br />
cui è defin<strong>it</strong>a (file scope). Senza lo specificatore static, la variabile è visibile<br />
anche negli altri files, purché in essi venga dichiarata con lo specificatore<br />
extern.
Visibil<strong>it</strong>à di variabili globali<br />
Se una variabile globale, visibile in tutti i files sorgente del programma (cioè<br />
defin<strong>it</strong>a senza lo specificatore static), non é inizializzata, deve esistere una<br />
e una sola dichiarazione senza lo specificatore extern, altrimenti il linker<br />
darebbe errore, con messaggio "unresolved symbol" (se tutte le dichiarazioni<br />
hanno extern), oppure "one or more multiply defined symbols" (se ci sono<br />
due dichiarazioni senza extern); se invece la variabile é inizializzata,<br />
l'inizializzazione deve essere presente in un solo file (in questo caso lo<br />
specificatore extern é opzionale), mentre negli altri files la variabile deve<br />
essere dichiarata con extern e non deve essere inizializzata.<br />
Visibil<strong>it</strong>à di costanti globali<br />
In <strong>C++</strong> le costanti globali (cioè le variabili globali defin<strong>it</strong>e const, con<br />
inizializzazione obbligatoria), obbediscono a regole differenti e precisamente:<br />
• di default le costanti globali hanno file scope;<br />
• affinché una costante globale sia visibile dappertutto, è necessaria la<br />
presenza dello specificatore extern anche nella dichiarazione in cui la<br />
costante è inizializzata (ovviamente, come per le variabili,<br />
l'inizializzazione deve essere presente una sola volta).<br />
Tabella riassuntiva<br />
Variabile globale senza<br />
inizializzazione<br />
Variabile globale con<br />
inizializzazione<br />
specificatore extern<br />
nel file di definizione<br />
Visibil<strong>it</strong>à globale<br />
specificatore extern<br />
negli altri files<br />
File scope<br />
vietato obbligatorio specificatore static<br />
opzionale<br />
Costante globale obbligatorio<br />
obbligatorio<br />
senza inizializzazione<br />
obbligatorio<br />
senza inizializzazione<br />
specificatore static<br />
default
Operatori e operandi<br />
Definizione di operatore e regole generali<br />
Un operatore è un token che agisce su una coppia di dati (o su un singolo<br />
dato), detti operandi, ottenendo un nuovo dato (risultato dell'operazione).<br />
Ogni operatore è identificato da un particolare simbolo grafico, cost<strong>it</strong>u<strong>it</strong>o di sol<strong>it</strong>o<br />
da un solo carattere (ma talvolta anche da due o più caratteri). Non tutti gli<br />
operatori possono applicarsi ad ogni tipo di operando, ma, per ogni operatore<br />
esiste un ben defin<strong>it</strong>o insieme di tipi di operandi a cui l'operatore è applicabile.<br />
Un operatore è detto binario se agisce su due operandi, unario se agisce su<br />
un solo operando. Se l'operatore è binario, i due operandi sono detti leftoperand<br />
e right-operand.<br />
Un'espressione è una successione di operazioni in cui il risultato di ogni<br />
operazione diviene operando per le operazioni successive, fino a giungere ad<br />
un unico risultato. L'ordine in cui le operazioni sono esegu<strong>it</strong>e è regolato secondo<br />
precisi cr<strong>it</strong>eri di precedenza e associativ<strong>it</strong>à fra gli operatori.<br />
Es. a op1 b op2 c (a, b, c sono operandi, op1 e op2 sono<br />
operatori)<br />
1. op1 ha la precedenza: il risultato di a op1 b diventa left-operand di<br />
op2 c<br />
2. op2 ha la precedenza: il risultato di b op2 c diventa right-operand di a<br />
op1<br />
3. op1 e op2 hanno la stessa precedenza, ma l'associativ<strong>it</strong>à procede da<br />
sinistra a destra: come nel caso 1.<br />
4. op1 e op2 hanno la stessa precedenza, ma l'associativ<strong>it</strong>à procede da<br />
destra a sinistra: come nel caso 2.<br />
Per ottenere che un'operazione venga comunque esegu<strong>it</strong>a con precedenza<br />
rispetto alle altre, bisogna racchiudere operatore e operandi fra parentesi<br />
tonde.<br />
Es. a op1 ( b op2 c ) (la seconda operazione viene esegu<strong>it</strong>a per<br />
prima)<br />
Operatore di assegnazione
L'operatore binario di assegnazione = copia il contenuto del right-operand<br />
(detto nello specifico r-value) nel left-operand (detto l-value).<br />
a = b<br />
b (r-value) può essere una qualunque espressione che rest<strong>it</strong>uisce un valore di<br />
tipo nativo;<br />
a (l-value) ha un amb<strong>it</strong>o di scelta molto più ristretto (tutti gli l-values possono<br />
essere r-values, ma non viceversa); in pratica, salvo poche eccezioni, a deve<br />
essere una variabile.<br />
I tipi di a e b devono coincidere, oppure il tipo di b deve essere convertibile<br />
implic<strong>it</strong>amente nel tipo di a.<br />
Operatori matematici<br />
Come in tutti i linguaggi di programmazione, le operazioni matematiche<br />
fondamentali (addizione, sottrazione, moltiplicazione, divisione) sono<br />
esegu<strong>it</strong>e rispettivamente dai seguenti operatori binari:<br />
+ - * /<br />
Se la divisione è fra due numeri interi, il risultato dell'operazione è ancora un<br />
numero intero (troncamento).<br />
Es. 27 / 4 da' come risultato 6 (anziché 6.75).<br />
Il resto di una divisione fra numeri interi si calcola con l'operatore binario<br />
%<br />
Es. 27 % 4 da' come risultato 3<br />
Operatori a livello del b<strong>it</strong><br />
Il <strong>C++</strong> può, a differenza da altri linguaggi, operare sulle variabili intere a livello<br />
del b<strong>it</strong>.
L'operatore binario >> produce lo scorrimento a destra (right-shift) dei b<strong>it</strong><br />
del left-operand, in quant<strong>it</strong>à pari al right-operand. In pratica esegue una<br />
divisione intera (con divisore uguale a una potenza di 2)<br />
Es. a >> n equivale a a / 2 n<br />
Dalla sinistra entrano cifre binarie 0 se il numero a è pos<strong>it</strong>ivo, oppure cifre binarie<br />
1 se il numero a è negativo (a causa della notazione a complemento a 2 dei<br />
numeri negativi).<br />
L'operatore binario
Data l'espressione:<br />
a = a op b<br />
dove op è un'operatore matematico o a livello del b<strong>it</strong>, b è un'espressione<br />
qualsiasi e a è una variabile, le due operazioni possono essere sintetizzate in<br />
una tram<strong>it</strong>e l'operatore binario op=<br />
Es. MiaVariabile *= 4 equivale a MiaVariabile =<br />
MiaVariabile*4<br />
La notazione compatta è conveniente soprattutto quando il nome delle variabili<br />
è lungo!<br />
Gli operatori binari relazionali sono:<br />
Operatori relazionali<br />
> >= < b rest<strong>it</strong>uisce true se a é maggiore di b<br />
• a >= b rest<strong>it</strong>uisce true se a é maggiore o uguale a b<br />
• a < b rest<strong>it</strong>uisce true se a é minore di b<br />
• a 3; (in bvar viene memorizzato true)<br />
• bool bvar = 7 < 3; (in bvar viene memorizzato false)<br />
Operatori logici
Gli operatori logici sono:<br />
&& || !<br />
Questi operatori agiscono su operandi booleani e rest<strong>it</strong>uiscono un valore<br />
booleano:<br />
L'operatore binario && esegue l'AND logico fra gli operandi:<br />
a && b risultato: true se entrambi a e b sono true; altrimenti:<br />
false<br />
L'operatore binario || esegue l'OR logico fra gli operandi:<br />
a || b risultato: false se entrambi a e b sono false;<br />
altrimenti: true<br />
L'operatore unario ! esegue il NOT logico dell'operando:<br />
!a risultato: true se a é false o viceversa<br />
Notare la differenza fra gli operatori logici && e || e gli operatori di<br />
confronto b<strong>it</strong> a b<strong>it</strong> & e |<br />
Es: 5 && 2<br />
5 & 2<br />
rest<strong>it</strong>uisce true in quanto entrambi gli operandi sono true (ogni<br />
intero diverso da zero è convert<strong>it</strong>o in true)<br />
rest<strong>it</strong>uisce 0 (e quindi false se convert<strong>it</strong>o in booleano) in quanto i<br />
b<strong>it</strong> corrispondenti sono tutti diversi<br />
Operatori di incremento e decremento<br />
Gli operatori unari di incremento ++ o decremento -- fanno aumentare o<br />
diminuire di un'un<strong>it</strong>à il valore dell'operando (che deve essere un l-value di<br />
qualunque tipo nativo). Equivalgono alla sintesi di un operatore binario di<br />
addizione o sottrazione, in cui il right-operand è 1, con un operatore di<br />
assegnazione, in cui il left-operand coincide con il left-operand<br />
dell'addizione o sottrazione.<br />
Es. MiaVariabile++; equivale a MiaVariabile =<br />
MiaVariabile+1;<br />
la prima forma è più rapida e compatta (specialmente se il nome della variabile è<br />
lungo!) .
L'operatore può seguire (suffisso) o precedere (prefisso) l'operando. Nella<br />
forma prefisso l'incremento (o il decremento) viene esegu<strong>it</strong>o prima che la<br />
variabile sia utilizzata nell'espressione, nella forma suffisso avviene il contrario.<br />
Es: int a, b, c=5 ;<br />
a = c++;<br />
b = ++c;<br />
alla fine di queste operazioni si trovano, nella variabili a, b e c, rispettivamente i<br />
valori 5, 7 e 7.<br />
Da questo esempio si capisce anche che la forma incremento (o decremento)<br />
conviene non solo perché è più compatta, ma soprattutto perché consente di<br />
ridurre il numero di istruzioni.<br />
Operatore condizionale<br />
L'operatore condizionale é l'unico operatore ternario (tre operandi):<br />
condizione ? espressioneA : espressioneB<br />
(dove condizione é un'espressione logica) l'operazione<br />
rest<strong>it</strong>uisce il valore dell'espressioneA se la condizione e' true o<br />
il valore dell'espressioneB se la condizione e' false<br />
Es: minimo = a < b ? a : b ;<br />
L'operatore condizionale gode della rara proprietà di rest<strong>it</strong>uire un ammissibile<br />
l-value (non in tutti i compilatori, però!)<br />
Es. (m < n ? a : b) = c ;<br />
(memorizza il valore di c in a se m é minore di n, altrimenti memorizza il valore di<br />
c in b)<br />
in questo caso però a e b non possono essere né espressioni né costanti, ma<br />
soltanto l-values.<br />
Conversioni di tipo
Conversioni di tipo implic<strong>it</strong>e<br />
Il <strong>C++</strong> eserc<strong>it</strong>a un forte controllo sui tipi e da' messaggio di errore quando si<br />
tenta di eseguire operazioni fra operandi di tipo non ammesso. Es.<br />
l'operatore % richiede che entrambi gli operandi siano interi.<br />
I quattro operatori matematici si applicano a qualsiasi tipo intrinseco, ma i<br />
tipi dei due operandi devono essere uguali. Tuttavia, nel caso di due tipi diversi,<br />
il compilatore esegue una conversione di tipo implic<strong>it</strong>a su uno dei due<br />
operandi, seguendo la regola di adeguare il tipo più semplice a quello più<br />
complesso, secondo la seguente gerarchia (in ordine crescente di<br />
compless<strong>it</strong>à):<br />
bool - char - unsigned char - short - unsigned short - long - unsigned<br />
long - float - double - long double<br />
Es: nell'operazione 3.4 / 2 il secondo operando è trasformato in 2.0 e il risultato<br />
è correttamente 1.7<br />
Nelle assegnazioni, il tipo del right-operand viene sempre trasformato<br />
implic<strong>it</strong>amente nel tipo del left-operand (con un messaggio warning se la<br />
conversione potrebbe implicare loss of data (perd<strong>it</strong>a di dati), trasformando un<br />
tipo più complesso in un tipo più semplice).<br />
Es: date le variabili char c e double d, l'assegnazione c = d è<br />
ammessa, ma genera un messaggio warning in fase di compilazione.<br />
Nelle operazioni fra tipi interi, se il valore ottenuto esce dal range (overflow),<br />
l'errore non viene segnalato. La stessa cosa dicasi se l'overflow si verifica a<br />
segu<strong>it</strong>o di una conversione di tipo.<br />
Es: short n = 32767 ;<br />
n++ ;<br />
(l'errore non viene segnalato, ma in n si r<strong>it</strong>rova il numero -32768)<br />
Conversioni di tipo esplic<strong>it</strong>e (casting)<br />
Quando si vuole ottenere una conversione di tipo che non verrebbe esegu<strong>it</strong>a<br />
implic<strong>it</strong>amente, bisogna usare l'operatore binario di casting (o conversione<br />
esplic<strong>it</strong>a), che consiste nell'indicazione del nuovo tipo fra parentesi davanti al<br />
nome della variabile da trasformare.<br />
Es. se la variabile n é di tipo int, l'espressione (float)n trasforma il contenuto di<br />
n da int in float.<br />
In <strong>C++</strong> si può usare anche il formato funzione (function-style casting):
float(n) é equivalente a (float)n<br />
va detto che il function-style casting non è sempre possibile (per esempio con i<br />
puntatori non si può fare).<br />
Tutti i tipi nativi consentono il casting, fermo restando il fatto che, se la<br />
variabile da trasformare è operando di una certa operazione, il tipo risultante<br />
deve essere fra quelli ammissibili (altrimenti viene generato un errore in<br />
compilazione). Per esempio: float(n) % 3 é errato in quanto l'operatore %<br />
ammette solo operandi interi.<br />
Vediamo ora un esempio in cui si evidenzia la necess<strong>it</strong>à del casting:<br />
int m=10, n=4;<br />
float r, a=2.7F;<br />
r = m/n+a;<br />
nell'ultima istruzione la divisione è fra due numeri interi e quindi, essendo i due<br />
operandi dello stesso tipo, la conversione implic<strong>it</strong>a non viene esegu<strong>it</strong>a e il<br />
risultato della divisione è il numero intero 2; solo successivamente questo<br />
numero viene convert<strong>it</strong>o in modo implic<strong>it</strong>o in 2.0 per essere sommato ad a. Se<br />
vogliamo che la conversione a float avvenga prima della divisione, e che<br />
questa fornisca il risultato esatto (cioè 2.5), dobbiamo convertire<br />
esplic<strong>it</strong>amente almeno uno dei due operandi e quindi riscrivere così la terza<br />
istruzione:<br />
r = (float)m/n+a; (non servono altre parentesi perchè il casting ha la precedenza<br />
sulla divisione)<br />
Il casting che abbiamo esaminato finora è quello del C (C-style casting). Il<br />
<strong>C++</strong> ha aggiunto altri quattro operatori di casting, suddividendo le conversioni<br />
di tipo in altrettante categorie e riservando un operatore per ciascuna di esse<br />
(per fornire al compilatore strumenti di controllo più raffinati). D'altra parte il Cstyle<br />
casting (che li comprende tutti) è ammesso anche in <strong>C++</strong>, e pertanto non<br />
tratteremo in questo corso degli altri operatori di casting, lim<strong>it</strong>andoci a fornirne<br />
l'elenco:<br />
static_cast(E)<br />
dynamic_cast(E)<br />
reinterpret_cast(E)<br />
const_cast(E)<br />
dove è E un'espressione qualsiasi il cui tipo è convert<strong>it</strong>o nel tipo T.<br />
Precedenza fra operatori
Nella seguente tabella gli operatori sono in ordine di precedenza decrescente<br />
(nello stesso blocco di righe hanno uguale precedenza):<br />
[Legenda degli operandi: id=identificatore; pid=puntatore a<br />
identificatore; expr=espressione; lv=l-value; ...=operandi opzionali]<br />
DESCRIZIONE OPERATORE CATEGORIA<br />
risoluzione di visibil<strong>it</strong>à<br />
riferimento globale<br />
selezione di un membro<br />
selezione di un membro puntato<br />
indicizzazione array<br />
chiamata di funzione<br />
incremento suffisso<br />
decremento suffisso<br />
identificazione di tipo<br />
dimensione di un oggetto<br />
complemento a 1<br />
NOT logico<br />
incremento prefisso<br />
decremento prefisso<br />
segno - algebrico<br />
segno + algebrico<br />
indirizzo di memoria<br />
dereferenziazione<br />
allocazione di memoria<br />
deallocazione di memoria<br />
casting (conversione di tipo)<br />
moltiplicazione<br />
divisione<br />
resto di divisione intera<br />
addizione<br />
sottrazione<br />
binario<br />
unario<br />
binario<br />
binario<br />
binario<br />
binario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
unario<br />
ternario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
SIMBOLO E<br />
OPERANDI<br />
id::id<br />
::id<br />
id.id<br />
pid->id<br />
id[expr]<br />
id(expr)<br />
lv++<br />
lv--<br />
typeid(expr)<br />
sizeof(expr)<br />
~ expr<br />
! expr<br />
++lv<br />
--lv<br />
- expr<br />
+ expr<br />
&lv<br />
*pid<br />
new tipo ...<br />
delete ... pid<br />
(tipo)expr<br />
expr * expr<br />
expr / expr<br />
expr % expr<br />
expr + expr<br />
expr - expr<br />
ASSOCIATIVITA'<br />
da sinistra a<br />
destra<br />
----<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
----<br />
----<br />
----<br />
----<br />
----<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
----<br />
----<br />
da destra a<br />
sinistra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra
scorrimento a destra<br />
scorrimento a sinistra<br />
minore<br />
minore o uguale<br />
maggiore<br />
maggiore o uguale<br />
uguale<br />
diverso<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
expr >> expr<br />
expr = expr<br />
expr == expr<br />
expr != expr<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
da sinistra a<br />
destra<br />
AND b<strong>it</strong> a b<strong>it</strong> binario expr & expr da sinistra a<br />
destra<br />
XOR b<strong>it</strong> a b<strong>it</strong> binario expr ^ expr da sinistra a<br />
destra<br />
OR b<strong>it</strong> a b<strong>it</strong> binario expr | expr da sinistra a<br />
destra<br />
AND logico binario expr && expr da sinistra a<br />
destra<br />
OR logico binario expr || expr da sinistra a<br />
destra<br />
espressione condizionale ternario expr ? expr :<br />
expr<br />
assegnazione<br />
moltiplicazione e assegnazione<br />
divisione e assegnazione<br />
resto e assegnazione<br />
addizione e assegnazione<br />
sottrazione e assegnazione<br />
scorrimento a destra e<br />
assegnazione<br />
scorrimento a sinistra e<br />
assegnazione<br />
AND b<strong>it</strong> a b<strong>it</strong> e assegnazione<br />
OR b<strong>it</strong> a b<strong>it</strong> e assegnazione<br />
XOR b<strong>it</strong> a b<strong>it</strong> e assegnazione<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
binario<br />
lv = expr<br />
lv *= expr<br />
lv /= expr<br />
lv %= expr<br />
lv += expr<br />
lv -= expr<br />
lv >>= expr<br />
lv
da destra a<br />
sinistra<br />
da destra a<br />
sinistra<br />
serializzazione delle espressioni binario expr , expr da sinistra a<br />
destra<br />
Ordine di valutazione<br />
Le regole di precedenza e associativ<strong>it</strong>à fra gli operatori non garantiscono che<br />
l'ordine di valutazione delle sotto-espressioni all'interno di un espressione<br />
sia sempre defin<strong>it</strong>o. Per esempio, si consideri l'espressione:<br />
a = fun1(5) + fun2(3); (dove fun1 e fun2 sono funzioni)<br />
le regole di precedenza assicurano che prima vengano esegu<strong>it</strong>e le chiamate<br />
delle funzioni, poi l'addizione e infine l'assegnazione, ma non è defin<strong>it</strong>o quale<br />
delle due funzioni venga chiamata per prima.<br />
Un altro caso di ordine di valutazione indefin<strong>it</strong>o si ha fra gli argomenti di<br />
chiamata di una funzione:<br />
Es. funz(expr1,expr2) valuta prima expr1 o expr2 ?<br />
All'opposto, in molti casi, l'ordine di valutazione è univocamente defin<strong>it</strong>o, come<br />
per esempio nelle operazioni logiche:<br />
1. expr1 && expr2<br />
2. expr1 || expr2<br />
in entrambi i casi expr1 è sempre valutata prima di expr2; in più, expr2 è<br />
valutata (e quindi esegu<strong>it</strong>a) solo se è necessario. In altre parole:<br />
• nel caso 1. expr2 non viene esegu<strong>it</strong>a se expr1 è false<br />
• nel caso 2. expr2 non viene esegu<strong>it</strong>a se expr1 è true<br />
questo tipo di azione si chiama valutazione cortocircu<strong>it</strong>ata ed è molto utile<br />
perché consente di ridurre il numero di istruzioni.
Introduzione all'I/O sui dispos<strong>it</strong>ivi standard<br />
In questa lezione introdurremo le caratteristiche principali dell'I/O in <strong>C++</strong>, lim<strong>it</strong>andoci per il<br />
momento all'I/O in free-format sui dispos<strong>it</strong>ivi standard di input e di output.<br />
Precisiamo che useremo una libreria (dichiarata nell'header-file: ) che è ormai<br />
"superata" dalla Libreria Standard (alcuni compilatori danno un messaggio di warning,<br />
avvisando che si sta usando una "deprecated" (?!) library). Tuttavia questa libreria è ancora<br />
integrata nello standard e ci sembra un buon approccio per introdurre l'argomento.<br />
Dispos<strong>it</strong>ivi standard di I/O<br />
In <strong>C++</strong> (come in C) sono defin<strong>it</strong>i i seguenti dispos<strong>it</strong>ivi standard di I/O<br />
(elenchiamo i tre principali):<br />
• stdout standard output (di default associato al video)<br />
• stderr standard output per i messaggi (associato al video)<br />
• stdin standard input (di default associato alla tastiera)<br />
stdin e stdout sono reindirizzabili a files nella linea di comando quando si<br />
lancia il programma eseguibile.<br />
Oggetti globali di I/O<br />
In <strong>C++</strong> i dispos<strong>it</strong>ivi standard di I/O stdout, stderr e stdin sono "collegati"<br />
rispettivamente agli oggetti globali cout, cerr e cin.<br />
Oggetto (definizione temporanea): variabile appartenente a un tipo astratto,<br />
non nativo del linguaggio.<br />
Globale: visibile sempre e dappertutto.<br />
Un oggetto globale é creato appena si lancia il programma, prima che venga<br />
esegu<strong>it</strong>a la prima istruzione del main.
Per definire gli oggetti globali di I/O bisogna includere l'header-file:<br />
.<br />
Operatori di flusso di I/O<br />
In <strong>C++</strong> sono defin<strong>it</strong>i gli operatori di flusso di I/O<br />
e<br />
> (estrazione)<br />
i cui left-operand sono rispettivamente cout (oppure cerr, che non<br />
menzioneremo più, in quanto le sue proprietà sono identiche a quelle di cout) e<br />
cin.<br />
Il compilatore distingue gli operatori di flusso da quelli di shift dei b<strong>it</strong><br />
(identificati dagli stessi simboli) in base al contesto, cioè in base al tipo degli<br />
operandi.<br />
Output tram<strong>it</strong>e l'operatore di inserimento<br />
In <strong>C++</strong> un'operazione di output si identifica con un'operazione di inserimento<br />
nell'oggetto cout:<br />
cout
Esempi: cout
l'input è una stringa, non deve contenere blanks (né tabs) e non può essere<br />
spezzata in due righe. D'altra parte l'esistenza dei terminatori (blank, tab o<br />
CR) consente di immettere più dati nella stessa riga.<br />
Casi particolari:<br />
• i terminatori inser<strong>it</strong>i ripetutamente o prima del dato da leggere sono<br />
ignorati<br />
• se il dato da leggere é di tipo numerico, la lettura é terminata quando<br />
incontra un carattere non valido (compreso il punto decimale se il<br />
numero é intero, cioè non esegue conversioni di tipo)<br />
• se il dato da leggere é di tipo char, legge un solo carattere<br />
Memorizzazione dei dati introdotti da tastiera<br />
Se stdin é associato, come di default, alla tastiera, la memorizzazione dei dati<br />
segue delle regole generali, che sono le stesse sia in <strong>C++</strong> (lettura tram<strong>it</strong>e<br />
l'oggetto cin) che in C (lettura tram<strong>it</strong>e le funzioni di libreria):<br />
• la lettura non avviene direttamente, ma tram<strong>it</strong>e un'area di memoria, detta<br />
buffer di input;<br />
• il programma, appena incontra un'istruzione di lettura, si appresta a<br />
memorizzare i dati (che distingue l'uno dall'altro riconoscendo i<br />
terminatori) trasferendoli dal buffer di input, finché questo non resta<br />
vuoto;<br />
• se il buffer di input si svuota prima che la lettura sia terminata (oppure se<br />
il buffer é già vuoto all'inizio della lettura, come dovrebbe succedere<br />
sempre), il programma si ferma in attesa di input e il controllo passa<br />
all'operatore, che viene abil<strong>it</strong>ato a introdurre dati da tastiera fino a quando<br />
non invia un enter (indipendentemente dal numero di dati da leggere);<br />
l'intera riga dig<strong>it</strong>ata dall'operatore viene poi trasfer<strong>it</strong>a nel buffer di input,<br />
al quale il programma riaccede per completare l'operazione di lettura;<br />
• se nel buffer di input restano ancora dati dopo che l'operazione di lettura<br />
é fin<strong>it</strong>a, questi verranno memorizzati durante la lettura successiva.<br />
Come si può notare, la presenza del buffer di input (molto utile peraltro per<br />
migliorare l'efficienza del programma) crea una specie di "asincronismo" fra<br />
operatore e programma, che può essere facilmente causa di errore: bisogna fare<br />
attenzione a fornire ogni volta esattamente il numero di dati richiesti.
Comportamento in caso di errore in lettura<br />
Le operazioni di estrazione non rest<strong>it</strong>uiscono mai esplic<strong>it</strong>i messaggi di errore,<br />
tuttavia,<br />
• se il primo carattere letto non é valido (per esempio una lettera se vuole<br />
leggere un numero), il programma non memorizza il dato e imposta una<br />
condizione di errore interna che inibisce anche le successive operazioni di<br />
lettura (nel senso che tutte le istruzioni di lettura, dal punto dell'errore in<br />
poi, vengono "saltate");<br />
• se invece il carattere non valido non è il primo, il programma accetta il dato<br />
letto fino a quel momento, ma il carattere invalido resta nel buffer,<br />
disponibile per le operazioni di lettura successive.<br />
Per accorgersi di un errore (e per porvi rimedio) bisogna utilizzare alcune<br />
proprietà dell'oggetto cin (di cui parleremo più avanti).
Il Compilatore GNU gcc in ambiente Linux<br />
Un compilatore integrato C/<strong>C++</strong><br />
Per Linux e' disponibile un compilatore integrato C/<strong>C++</strong>: si tratta dei comandi<br />
GNU gcc e g++, rispettivamente.<br />
In realta g++ e' uno script che chiama gcc con opzioni specifiche per riconoscere<br />
il <strong>C++</strong>.<br />
Il progetto GNU<br />
Il comando gcc, GNU Compiler Collection, fa parte del progetto GNU (web server<br />
www.gnu.org). Il progetto GNU fu lanciato nel 1984 da Richard Stallman con lo<br />
scopo di sviluppare un sistema operativo di tipo Unix che fosse completamente<br />
"free" software.<br />
Cosa è GNU/Linux?<br />
Gnu Non è Unix!<br />
"GNU, che sta per "Gnu's Not Unix" (Gnu Non è Unix), è il nome del<br />
sistema<br />
software completo e Unix-compatibile che sto scrivendo per<br />
distribuirlo<br />
liberamente a chiunque lo possa utilizzare. Molti altri volontari mi<br />
stanno aiutando. Abbiamo gran necess<strong>it</strong>à di contributi in tempo,<br />
denaro,<br />
programmi e macchine."<br />
[Richard Stallman, Dal manifesto GNU, http://www.gnu.org/gnu/manifesto.html<br />
Quale versione di gcc sto usando?<br />
Si puo' determinare la versione del compilatore invocando:<br />
gcc -v<br />
gcc version 2.96 20000731 (Red Hat Linux 7.1<br />
2.96-98)<br />
I passi della compilazione<br />
Sia gcc che g++ processano file di input attraverso uno o piu' dei seguenti passi:<br />
1) preprocessing<br />
-rimozione dei commenti<br />
-interpretazioni di speciali direttive per il preprocessore denotate da "#"
come:<br />
#include - include il contenuto di un determinato file, Es.<br />
#include<br />
#define -definisce un nome simbolico o una variabile, Es. #define<br />
MAX_ARRAY_SIZE 100<br />
2) compilation<br />
-traduzione del codice sorgente ricevuto dal preprocessore in codice<br />
assembly<br />
3) assembly<br />
-creazione del codice oggetto<br />
4) linking<br />
-combinazione delle funzioni defin<strong>it</strong>e in altri file sorgenti o defin<strong>it</strong>e in<br />
librerie con la funzione main() per creare il file eseguibile.<br />
Estensioni<br />
Alcuni suffissi di moduli implicati nel processo di compilazione:<br />
.c modulo sorgente C; da preprocessare, compilare e assemblare<br />
.cc modulo sorgente <strong>C++</strong>; da preprocessare, compilare e assemblare<br />
.cpp modulo sorgente <strong>C++</strong>; da preprocessare, compilare e assemblare<br />
.h modulo per il preprocessore; di sol<strong>it</strong>o non nominato nella riga di commando<br />
.o modulo oggetto; da passare linker<br />
.a sono librerie statiche<br />
.so sono librerie dinamiche<br />
L' input/output di gcc<br />
gcc accetta in input ed effettua la compilazione di codice C o <strong>C++</strong> in un solo<br />
colpo.<br />
Consideriamo il seguente codice sorgente C:<br />
/* il codice C pippo.c */<br />
#include <br />
int main() {<br />
puts("ciao pippo!");<br />
return 0;<br />
}<br />
Per effettuare la compilazione
gcc pippo.c<br />
In questo caso l' output di default e' direttamente l'eseguibile a.out.<br />
Di sol<strong>it</strong>o si specifica il nome del file di output utilizzando l' opzione -o :<br />
gcc -o prova pippo.c<br />
L'eseguibile puo' essere lanciato usando semplicemente<br />
./prova<br />
ciao pippo!<br />
Nota. Usare "./" puo' sembrare superfluo. In realta' si dimostra molto utile per<br />
ev<strong>it</strong>are di lanciare involontariamente un programma omonimo, per esempio il<br />
comando "test"!<br />
Consideriamo ora un codice sorgente <strong>C++</strong> analogo:<br />
Questa volta compiliamo usando<br />
// Il codice <strong>C++</strong> pippo.cpp<br />
#include<br />
int main() {<br />
cout
Passaggi intermedi di compilazione<br />
Per compilare senza effettuare il link usare<br />
g++ -c pippo.cpp<br />
In questo caso viene creato il file oggetto pippo.o .<br />
Per effettuare il link usiamo<br />
g++ -o prova pippo.o<br />
I messaggi del compilatore<br />
Il compilatore invia spesso dei messaggi all'utente. Questi messaggi si possono<br />
classificare in due famiglie: messaggi di avvertimento (warning messagges) e<br />
messaggi di errore (error messagges). I messaggi di avvertimento indicano la<br />
presenza di parti di codice presumibilmente mal scr<strong>it</strong>te o di problemi che<br />
potrebbero avvenire in segu<strong>it</strong>o, durante l'esecuzione del programma. I messaggi<br />
di avvertimento non interrompono comunque la compilazione.I messaggi di errore<br />
invece indicano qualcosa che deve essere necessariamente corretto e causano<br />
l'interruzione della compilazione.<br />
Esempio di un codice <strong>C++</strong> che genera un warning:<br />
// example1.cpp<br />
#include<br />
float multi(int a, int b) {<br />
return a*b;<br />
};<br />
int main() {<br />
float a=2.5;<br />
int b=1;<br />
cout
example1.cpp: In function `int main ()':<br />
example1.cpp:12: warning: passing `float' for<br />
argument passing 1 of<br />
`multi (int, int)'<br />
example1.cpp:12: warning: argument to `int'<br />
from `float'<br />
Il messaggio ci avvisa che alla linea 12 del main() e' stato passato alla funzione<br />
multi un float invece che un int.<br />
Esempio di un codice che genera un messaggio di errore:<br />
// example1.cpp<br />
#include<br />
float multi(int a, int b) {<br />
return a*b<br />
};<br />
int main() {<br />
int a=2;<br />
int b=1;<br />
cout
g++ -w -o prova example1.cpp<br />
Per usare il massimo livello di warning usare l' opzione -Wall<br />
g++ -Wall -o prova example1.cpp<br />
Compilare per effetture il debug<br />
Se siete intenzionati ad effettuare il debug di un programma,<br />
utilizzate sempre l'opzione -g:<br />
g++ -Wall -g -o pippo example1.cpp<br />
L' opzione -g fa in modo che il programma eseguibile contenga<br />
informazioni supplementari che permettono al debugger di collegare<br />
le istruzioni in linguaggio macchina che si trovano nell'eseguibile alle<br />
righe del codice corrispondenti nei sorgenti C/<strong>C++</strong>.<br />
Autopsia di un programma defunto<br />
Il seguente codice <strong>C++</strong>, wrong.cpp, genera un errore (nella<br />
fattispecie una divisione per 0) in fase di esecuzione che porta alla<br />
terminazione innaturale del programma<br />
#include<br />
int div(int a, int b) {<br />
return a/b;<br />
};<br />
int main() {<br />
int a=2;<br />
int b=0;<br />
cout
Compiliamo il file wrong_code.cpp<br />
g++ -Wall -g -o wrong_program<br />
wrong_code.cpp<br />
Il codice e' sintatticamente ineccepibile e verra' compilato senza<br />
problemi. In fase di esecuzione si verifica tuttavia una divisione per<br />
zero che causa la morte del programma.<br />
./wrong_program<br />
a=2, b=0<br />
Floating exception (core dumped)<br />
Linux genera nella directory corrente un file in cui scarica la memoria<br />
memoria assocciata al programma (core dump):<br />
ls -sh<br />
total 132k<br />
100k core 4.0k wrong_code.cpp 28k<br />
wrong_program*<br />
Il file core contiene l 'immagine della memoria (rifer<strong>it</strong>a al nostro<br />
programma) al momento dell'errore.<br />
Possiamo effettuare l' autopsia del programma utilizzando il<br />
debugger GNU gdb<br />
gdb wrong_program core<br />
...<br />
Core was generated by `wrong_prog'.<br />
...<br />
#0 0x080486a5 in div (a=2, b=0) at<br />
wrong_code.cpp:4<br />
4 return a/b;<br />
(gdb) where<br />
#0 0x080486a5 in div (a=2, b=0) at<br />
wrong_code.cpp:4<br />
#1 0x08048737 in main () at<br />
wrong_code.cpp:13<br />
#2 0x400b1647 in __libc_start_main
(main=0x80486b0 , argc=1,<br />
ubp_av=0xbfffe614, in<strong>it</strong>=0x80484e8<br />
, fini=0x80487b0 ,<br />
rtld_fini=0x4000dcd4 ,<br />
stack_end=0xbfffe60c)<br />
at ../sysdeps/generic/libc-start.c:129<br />
(gdb) qu<strong>it</strong><br />
Il comando where di gdb ci informa che l' errore si e' verificato alla<br />
riga 4 del modulo wrong_code.cpp.<br />
Esiste una versione con interfaccia grafica di gdb : kdbg<br />
Ottimizzazione<br />
Il compilatore gcc consente di utilizzare diverse opzioni per ottenere un risultato<br />
più o meno ottimizzato. L'ottimizzazione richiede una potenza elaborativa<br />
maggiore, al crescere del livello di ottimizzazione richiesto. L' opzione -On<br />
ottimizza il codice, dove n é il livello di ottimizzazione. Il massimo livello di<br />
ottimizzazione allo stato attuale é il 3, quello generalmente più usato é 2.<br />
Quando non si deve eseguire il debug é consigliato ottimizzare il codice.<br />
Opzione Descrizione<br />
-O, -O1 Ottimizzazione minima<br />
-O2 Ottimizzazione media<br />
-O3 Ottimizzazione massima<br />
-O0 Nessuna ottimizzazione<br />
Esempio di un codice chiaramente inefficiente<br />
int main() {<br />
int a=10;<br />
int b=1;<br />
int c;<br />
for (int i=0; i
Confronto dei tempi di esecuzione in funzione di livelli di ottimizzazione crescente<br />
Livello Tempo di esecuzione (secondi)<br />
O0 32.2<br />
O1 5.4<br />
O2 5.2<br />
O3 5.2<br />
Compilazione di un programma modulare<br />
Un programma modulare e' un programma spezzettato in componenti piu' piccole<br />
con funzioni specifiche. La programmazione modulare e' piu' facile da<br />
comprendere e da correggere.<br />
Nel segu<strong>it</strong>o abbiamo un programma <strong>C++</strong> composto da<br />
tre moduli: main.cpp, myfunc.cpp e myfunc.h .<br />
// main.cpp<br />
#include<br />
#include"myfunc.h"<br />
int main() {<br />
int a=6;<br />
int b=3;<br />
cout
1. Per compilare usiamo<br />
int mul(int a, int b) {<br />
return a*b;<br />
};<br />
g++ -Wall -g -o prova main.cpp<br />
myfunc.cpp<br />
Si noti che il file myfunc.h non appare nella riga di comando, verra' incluso<br />
dal gcc in fase di precompilazione.<br />
Inclusione di librerie in fase di compilazione<br />
L'opzione -lnome_libreria compila utilizzando la libreria indicata, tenendo<br />
presente che, per questo, verrà cercato un file che inizia per lib, continua con il<br />
nome indicato e termina con .a oppure .so.<br />
Modifichiamo i moduli myfunc.h e myfunc.cpp aggiungendo la<br />
funzione pot:<br />
// myfunc.h<br />
int div(int a, int b);<br />
int mul(int a, int b);<br />
float pot(float a, float b);<br />
// myfunc.cpp<br />
int div(int a, int b) {<br />
return a/b;<br />
};<br />
int mul(int a, int b) {<br />
return a*b;<br />
};<br />
float pot(float a, float b) {<br />
return pow(a,b);<br />
}<br />
La compilazione pero' si interrompe<br />
g++ -Wall -g -o prova main.cpp<br />
myfunc.cpp<br />
myfunc.cpp: In function `float pot (float,
float)':<br />
myfunc.cpp:11: `pow' undeclared (first use<br />
this function)<br />
myfunc.cpp:11: (Each undeclared identifier is<br />
reported only once for<br />
each function <strong>it</strong> appears in.)<br />
La funzione pow e' contenuta nella libreria matematica<br />
math, dobbiamo allora aggiungere l'istruzione include<br />
nel modulo :<br />
// myfunc.cpp<br />
#include<br />
int div(int a, int b) {<br />
return a/b;<br />
};<br />
int mul(int a, int b) {<br />
return a*b;<br />
};<br />
float pot(float a, float b) {<br />
return pow(a,b);<br />
}<br />
e compilare con un link alla libreria libm.so<br />
g++ -Wall -g -o prova main.cpp<br />
myfunc.cpp -lm<br />
Di default il compilatore esegue la ricerca della libreria nel direttorio<br />
standard /usr/lib/. Tram<strong>it</strong>e l' opzione -L/nome_dir, e' possibile<br />
aggiunge la directory /nome_dir alla lista di direttori in cui gcc<br />
cerca le librerie in fase di linking.
Il Comando 'make' in ambiente Linux<br />
Perche' utilizzare il comando make?<br />
Immaginate un progetto molto esteso, formato da decine e decine di moduli, e di<br />
voler cambiare solo una piccola parte di codice e di voler poi testare il programma.<br />
Ovviamente, per ev<strong>it</strong>are di ricompilare tutto il codice ad ogni modifica, e'<br />
conveniente compilare solo i moduli appena modificati e poi effettuare il link con<br />
la parte di codice rimasta immutata. Potrebbe pero' essere difficile, o quanto<br />
meno noioso, controllare ripetutamente quali moduli devono essere per forza<br />
ricompilati e quali no. Il comando make fa questo per voi!<br />
Il Makefile ed i target del make<br />
Per funzionare make ha bisogno che voi scriviate un file chiamato Makefile in cui<br />
siano descr<strong>it</strong>te le relazioni fra i vostri files ed i comandi per aggiornarli. Quando il<br />
make viene invocato esegue le istruzioni contenute nel Makefile.<br />
Una idea base che bisogna capire del make e' il concetto di target . Il primo target<br />
in assoluto e' il Makefile stesso. se si lancia il make senza aver preparato un<br />
Makefile si ottiene il seguente risultato<br />
make<br />
make: *** No targets specified and no<br />
makefile found. Stop.<br />
Quello che segue e' un semplice Makefile in cui sono stati defin<strong>it</strong>i tre target e tre azioni<br />
corrispondenti:<br />
# Un esempio di Makefile<br />
one:<br />
@echo UNO!<br />
two:<br />
@echo DUE!<br />
three:<br />
@echo E TRE!<br />
La definizione di un target inizia sempre all'inizio della riga ed segu<strong>it</strong>o da : . Le<br />
azioni (in questo caso degli output su schermo) seguono le definizioni di ogni<br />
target e, anche se in questo esempio sono singole, possono essere molteplici. La<br />
prima riga, che inizia con #, e' un commento.
Per utilizare i target invochiamoli sulla riga di comando del make:<br />
make one<br />
UNO!<br />
make one two three<br />
UNO!<br />
DUE!<br />
E TRE!<br />
Se non si invoca nessun target nella linea di comando, make assume come<br />
default il primo che trova nel Makefile:<br />
make<br />
UNO!<br />
IMPORTANTE: le linee in cui si specificano le azioni corrispondenti ad ogni target<br />
(Es. @echo UNO!) devono iniziare con un separatore !<br />
Il seguente Makefile non e' valido perche' la riga seguente la definizione del target non inizia<br />
con un separatore :<br />
# Un esempio di Makefile mal scr<strong>it</strong>to<br />
one:<br />
@echo UNO!<br />
make one<br />
Makefile:4: *** missing separator. Stop.<br />
Le righe di azione devo iniziare invariabilmente con un separatore , NON<br />
POSSONO ESSERE UITLIZZATI DEGLI SPAZI!<br />
Dipendenze<br />
E' possibile definire delle dipendenze fra i target all' interno del Makefile<br />
# Un esempio di Makefile con dipendenze<br />
one:<br />
@echo UNO!<br />
two: one
@echo DUE!<br />
three: one two<br />
@echo E TRE!<br />
all: one two three<br />
@echo TUTTI E TRE!<br />
Si noti come i target vengono elaborati in sequenza:<br />
make three<br />
UNO!<br />
DUE!<br />
E TRE!<br />
make all<br />
UNO!<br />
DUE!<br />
E TRE!<br />
TUTTI E TRE!<br />
Macro e variabili ambiente<br />
E' possibile definere delle Macro all' interno del Makefile<br />
#Definiamo la Macro OBJECT<br />
OBJECT=PIPPO<br />
one:<br />
@echo CIAO $(OBJECT)!<br />
make<br />
CIAO PIPPO!<br />
Possiamo ridefinire il valore della macro OBJECT direttamente sulla riga di<br />
comando, senza alterare il Makefile!<br />
make OBJECT=pippa<br />
CIAO pippa!<br />
Il Makefile puo' accedere alle variabili ambiente:
# Usiamo una variabile ambiente<br />
OBJECT=$(TERM)<br />
one:<br />
@echo CIAO $(OBJECT)!<br />
make<br />
CIAO xterm!<br />
Compiliamo con make (finalmente)<br />
Supponiamo di voler compilare il seguente codice <strong>C++</strong> composto da tre moduli<br />
(main.cpp, myfunc.cpp e myfunc.h) usando il comando make.<br />
// main.cpp<br />
#include<br />
#include"myfunc.h"<br />
int main() {<br />
int a=6;<br />
int b=3;<br />
cout
int div(int a, int b);<br />
int mul(int a, int b);<br />
float pot(float a, float b);<br />
Un semplice Makefile si presenta cosi':<br />
OBJECTS=main.o myfunc.o<br />
CFLAGS=-g -Wall<br />
LIBS=-lm<br />
CC=g++<br />
PROGRAM_NAME=prova<br />
$(PROGRAM_NAME):$(OBJECTS)<br />
$(CC) $(CFLAGS) -o<br />
$(PROGRAM_NAME) $(OBJECTS) $(LIBS)<br />
@echo " "<br />
@echo "Compilazione completata!"<br />
@echo " "<br />
Il make ricompilera' il target prova se i files da cui questo dipende (gli OBJECTS<br />
main.o e myfunc.o) sono stati modificati dopo che prova e' stato modificato<br />
l'ultima volta oppure non esistono. Il processo di ricompilazione avverra' secondo<br />
la regola descr<strong>it</strong>ta nell' azione del target e usando le Macro defin<strong>it</strong>e dall' utente<br />
(CC, CFLAGS, LIBS).<br />
Per compilare usiamo semplicemente<br />
make<br />
g++ -c -o main.o main.cpp<br />
g++ -c -o myfunc.o myfunc.cpp<br />
g++ -g -Wall -o prova main.o myfunc.o -lm<br />
Compilazione completata!<br />
Se modifichiamo solo un modulo, per esempio myfunc.cpp, il make effettuera' la<br />
compilazione di questo file solamente.<br />
make<br />
g++ -c -o myfunc.o myfunc.cpp<br />
g++ -g -Wall -o prova main.o myfunc.o -lm<br />
Compilazione completata!<br />
Alcuni target standard<br />
Esistono alcuni target standard usati da programmatori Linux e GNU. Fra questi:
• install, viene utilizzato per installare i file di un progetto e puo'<br />
comprendere la creazione di nuove directory e la assegnazione di dir<strong>it</strong>ti di<br />
accesso ai file.<br />
• clean, viene utilizzato per rimuovere dal sistema i file oggetto (*.o), i file<br />
core, e altri file tempornei creati in fase di compilazione<br />
• all, di sol<strong>it</strong>o utilizzato per richiamare altri target con lo scopo di costruire<br />
l'intero progetto.<br />
Aggiungiamo il target clean al nostro Makefile:<br />
OBJECTS=main.o myfunc.o<br />
CC=g++<br />
CFLAGS=-g -Wall<br />
LIBS=-lm<br />
PROGRAM_NAME=prova<br />
$(PROGRAM_NAME):$(OBJECTS)<br />
$(CC) $(CFLAGS) -o<br />
$(PROGRAM_NAME) $(OBJECTS) $(LIBS)<br />
@echo " "<br />
@echo "Compilazione completata!"<br />
@echo " "<br />
clean:<br />
rm -f *.o<br />
rm -f core<br />
Invocare il target clean comporta la cancellazione di tutti i file<br />
oggetto e del file core.<br />
make clean<br />
rm -f *.o<br />
rm -f core
Istruzioni di controllo<br />
Si chiamano "istruzioni di controllo" in <strong>C++</strong> (come in C) quelle istruzioni che modificano<br />
l'esecuzione sequenziale di un programma.<br />
Sintassi:<br />
Istruzione di controllo if<br />
if (condizione) istruzione;<br />
(dove condizione é un'espressione logica) se la condizione é<br />
true il programma esegue l'istruzione, altrimenti passa<br />
direttamente all'istruzione successiva<br />
Nel caso di due scelte alternative, all'istruzione if si può associare l'istruzione else<br />
:<br />
if (condizione) istruzioneA;<br />
else istruzioneB;<br />
se la condizione é true il programma esegue l'istruzioneA,<br />
altrimenti esegue l'istruzioneB<br />
Se le istruzioni da eseguire in base alla condizione sono più di una, bisogna<br />
creare un amb<strong>it</strong>o, cioè raggruppare le istruzioni fra parentesi graffe:<br />
e analogamente:<br />
if (condizione) { ....... blocco di istruzioni ...... }<br />
else { ....... blocco di istruzioni ...... }<br />
Se l'istruzione controllata da un if consiste a sua volta in un altro if (sono<br />
possibili più istruzioni if "innestate"), ogni eventuale else si riferisce sempre all'if<br />
immediatamente superiore (in assenza di parentesi graffe).<br />
[p11]
Es. if (cond1) if (cond2) istr1; else istr2;<br />
(istr2 é esegu<strong>it</strong>a se cond1 é true e cond2 é false)<br />
Invece: if (cond1) { if (cond2) istr1;} else istr2;<br />
(istr2 é esegu<strong>it</strong>a se cond1 é false, indipendentemente da<br />
cond2).<br />
Per essere sicuri di ottenere quello che si vuole, mettere sempre le parentesi<br />
graffe, anche se sono ridondanti, e quindi il primo caso è equivalente (ma più<br />
chiaro) se si scrive:<br />
if (cond1) { if (cond2) istr1; else istr2; }<br />
Sintassi:<br />
Istruzione di controllo while<br />
while (condizione) istruzione;<br />
(dove condizione é un'espressione logica) il programma<br />
esegue ripetutamente l'istruzione finchè la condizione é true e<br />
passa all'istruzione successiva appena la condizione diventa<br />
false.<br />
Ovviamente, affinché il loop (ciclo) non si ripeta all'infin<strong>it</strong>o,<br />
l'istruzione deve modificare qualche parametro della<br />
condizione.<br />
Se le istruzioni da eseguire in base alla condizione sono più di una, bisogna<br />
creare un amb<strong>it</strong>o, cioè raggruppare le istruzioni fra parentesi graffe:<br />
while (condizione) { ....... blocco di istruzioni ...... }<br />
La condizione viene verificata all'inizio di ogni <strong>it</strong>erazione del ciclo: é pertanto<br />
possibile, se la condizione é già inizialmente false, che il ciclo non venga<br />
esegu<strong>it</strong>o neppure una volta.<br />
E' ammessa anche la forma: while (condizione) ;<br />
in questo caso, per ev<strong>it</strong>are un loop infin<strong>it</strong>o, la condizione deve essere in grado<br />
di automodificarsi.
Sintassi:<br />
Istruzione di controllo do ... while<br />
do { ... blocco di istruzioni ... } while ( condizione ) ;<br />
(dove condizione é un'espressione logica) funziona come l'istruzione<br />
while, con la differenza che la condizione é verificata alla fine di ogni<br />
<strong>it</strong>erazione e pertanto il ciclo é sempre esegu<strong>it</strong>o almeno una volta. Se la<br />
condizione é true il programma torna all'inizio del ciclo ed esegue una<br />
nuova <strong>it</strong>erazione, se é false, passa all'istruzione successiva. Le parentesi<br />
graffe sono obbligatorie, anche se il blocco è cost<strong>it</strong>u<strong>it</strong>o da una sola<br />
istruzione.<br />
Sintassi:<br />
Istruzione di controllo for<br />
for (inizializzazione; condizione; modifica) istruzione;<br />
(dove inizializzazione é un'espressione esegu<strong>it</strong>a solo la prima volta,<br />
condizione é un'espressione logica, modifica é un'espressione<br />
esegu<strong>it</strong>a alla fine di ogni <strong>it</strong>erazione) il programma esegue ripetutamente<br />
l'istruzione finchè la condizione é true e passa all'istruzione successiva<br />
appena la condizione diventa false.<br />
Se le istruzioni da eseguire in base alla condizione sono più di una, bisogna<br />
creare un amb<strong>it</strong>o, cioè raggruppare le istruzioni fra parentesi graffe:<br />
for (inizializzazione; condizione; modifica)<br />
{ ....... blocco di istruzioni ...... }
L'istruzione for é simile all'istruzione while, con le differenze che in while<br />
l'inizializzazione é impostata precedentemente e la modifica é esegu<strong>it</strong>a<br />
all'interno del blocco di istruzioni del ciclo. Come in while, anche in for la<br />
condizione viene verificata all'inizio di ogni <strong>it</strong>erazione.<br />
Esempio (confronto fra for e while):<br />
int conta;<br />
for (conta=0; conta
Il <strong>C++</strong> mantiene la "vecchia" istruzione goto :<br />
goto identificatore;<br />
............................<br />
............................<br />
identificatore: istruzione;<br />
il flusso del programma "salta" direttamente all'istruzione labellata (etichettata)<br />
identificatore.<br />
L'istruzione goto ha pochi utilizzi nella normale programmazione ad alto livello.<br />
Può essere importante nei rari casi in cui è richiesta la massima efficienza (per<br />
esempio in applicazioni in tempo reale), oppure per uscire direttamente dal più<br />
interno di diversi cicli innestati.<br />
Istruzione di controllo sw<strong>it</strong>ch ... case<br />
Sintassi (le parti fra parentesi quadra sono opzionali):<br />
sw<strong>it</strong>ch( espressione )<br />
{<br />
[case costante1 : [ blocco di istruzioni 1]]<br />
[case costante2 : [ blocco di istruzioni 2]]<br />
..............<br />
[default : [blocco di istruzioni]]<br />
}<br />
L'istruzione sw<strong>it</strong>ch confronta il valore dell'espressione (che deve rest<strong>it</strong>uire un<br />
risultato intero) con le diverse costanti (dello stesso tipo dell'espressione) e,<br />
appena ne trova una uguale, esegue tutte le istruzioni da quel punto in poi<br />
(anche se le istruzioni relative allo stesso case sono più d'una, non è necessario<br />
inserirle fra parentesi graffe). Se nessuna costante é uguale al valore<br />
dell'espressione, esegue, se esistono, le istruzioni dopo default:<br />
Per ottenere che le istruzioni selezionate siano esegu<strong>it</strong>e in alternativa alle altre,<br />
bisogna inserire alla fine del corrispondente blocco l'istruzione break<br />
[p15]
Array<br />
Cos'è un array ?<br />
Un array é un insieme di variabili che occupano locazioni consecutive in memoria<br />
e sono caratterizzate dall'appartenere tutte allo stesso tipo, detto tipo dell'array<br />
(può anche essere un tipo astratto).<br />
Ogni variabile di tale insieme é detta elemento dell'array ed é identificata dalla<br />
sua posizione d'ordine nell'array (indice). L'intero array é identificato da un<br />
nome (che va specificato secondo le regole generali di specifica degli<br />
identificatori).<br />
Il numero di elementi di un array (detto dimensione dell'array ) é predefin<strong>it</strong>o<br />
e invariabile. In <strong>C++</strong> (come in C) l'indice può assumere valori compresi fra zero<br />
e il numero di elementi meno 1.<br />
Definizione e inizializzazione di un array<br />
Per definire un array bisogna specificare prima il tipo e poi il nome dell'array,<br />
segu<strong>it</strong>o dalla sua dimensione fra parentesi quadre (la dimensione deve essere<br />
espressa da una costante).<br />
Es. int valori[30];<br />
In fase di definizione un array può essere anche inizializzato. I valori iniziali<br />
dei suoi elementi devono essere specificati fra parentesi graffe e separati l'un<br />
l'altro da una virgola; inoltre la dimensione dell'array, essendo determinata<br />
automaticamente, può essere omessa (non però le parentesi quadre, che<br />
cost<strong>it</strong>uiscono l'operatore di dichiarazione dell'array).<br />
Es. int valori[] = {32, 53, 28, 85, 21};<br />
nel caso dell'esempio la dimensione 5 é automaticamente calcolata.<br />
L'operatore [ ]
L'operatore binario [ ] richiede come left-operand il nome di un array e<br />
come secondo operando (racchiuso fra le due parentesi quadre) una qualunque<br />
espressione con risultato intero (interpretato come indice dell'array).<br />
Il significato dell'operatore [ ] é duplice:<br />
• usato per rest<strong>it</strong>uire un l-value, é un operatore di inserimento di dati<br />
nell'array.<br />
Es. valori[3] = 45;<br />
(il numero 45 viene assegnato alla variabile identificata dall'indice 3<br />
dell'array valori)<br />
• usato per rest<strong>it</strong>uire un r-value, é un operatore di estrazione di dati<br />
dall'array.<br />
Es. a = valori[4] ;<br />
(il contenuto della variabile identificata dall'indice 4 dell'array valori<br />
viene assegnato alla variabile a)<br />
Array multidimensionali<br />
In <strong>C++</strong> (come in C) sono possibili array con qualsivoglia numero di dimensioni;<br />
tali array vanno defin<strong>it</strong>i come nel seguente esempio (array tridimensionale):<br />
float tabella[3][4][2];<br />
NOTA : la formulazione appare un po' "strana", ma chiarisce il fatto che un array<br />
multidimensionale è da intendersi come un array di array. Nell'esempio:<br />
tabella è un array di 3 elementi, ciascuno dei quali è un array di 4 elementi,<br />
ciascuno dei quali è un array di 2 elementi di tipo float.<br />
A differenza dal FORTRAN, in <strong>C++</strong> (come in C) gli array multidimensionali<br />
sono memorizzati con gli indici meno significativi a destra ("per riga", nel caso di<br />
array bidimensionali).<br />
Per esempio, dato l'array A[2][3], i suoi elementi sono memorizzati nel seguente<br />
ordine:<br />
A[0][0] , A[0][1] , A[0][2] , A[1][0] , A[1][1] , A[1][2]<br />
Per inizializzare un array multidimensionale, bisogna innestare tanti gruppi<br />
di parentesi graffe quante sono le singole porzioni monodimensionali<br />
dell'array, ed elencare gli elementi nello stesso ordine in cui saranno<br />
memorizzati.<br />
Esempio, nel caso bidimensionale: int dati[3][2] = { {8, -5} , {4,<br />
0} , {-2, 6 } };
L'operatore sizeof e gli array<br />
L'operatore sizeof, se l'operando é il nome di un array, rest<strong>it</strong>uisce il numero<br />
di bytes complessivi dell'array, che é dato dal numero degli elementi<br />
moltiplicato per la lunghezza in byte di ciascun elemento (la quale ovviamente<br />
dipende dal tipo dell'array).<br />
Gli array in <strong>C++</strong><br />
Gli array descr<strong>it</strong>ti finora sono quelli "in stile C". Nei programmi in <strong>C++</strong> ad alto<br />
livello sono scarsamente utilizzati. Al loro posto si preferisce usare alcune classi<br />
della Libreria Standard (come vedremo) che offrono flessibil<strong>it</strong>à molto maggiori<br />
(per esempio la dimensione è modificabile dinamicamente e inoltre, negli array<br />
multidimensionali, si possono definire singole porzioni monodimensionali con<br />
dimensioni diverse).<br />
[p16]
Stringhe di caratteri<br />
Le stringhe come particolari array di caratteri<br />
Abbiamo già visto che le stringhe non cost<strong>it</strong>uiscono un tipo intrinseco del <strong>C++</strong><br />
e di conseguenza non sono ammesse come operandi dalla maggior parte degli<br />
operatori (compreso l'operatore di assegnazione).<br />
Sono tuttavia riconosciute da alcuni operatori (come per esempio gli operatori di<br />
flusso di I/O del <strong>C++</strong>) e da numerose funzioni di libreria del C (come per<br />
esempio la printf, insieme a molte altre che hanno il comp<strong>it</strong>o specifico di<br />
manipolare le stringhe).<br />
In memoria le stringhe sono degli array di tipo char, con una particolar<strong>it</strong>à in<br />
più, che le fa riconoscere da operatori e funzioni come stringhe e non come<br />
normali array: l'elemento dell'array che segue l'ultimo carattere della stringa<br />
deve contenere il carattere NULL (detto in questo caso terminatore); si dice<br />
pertanto che una stringa é un "array di tipo char null terminated".<br />
Definizione di variabili stringa<br />
Consideriamo il seguente esempio. L'istruzione:<br />
char MiaVar[30];<br />
definisce la variabile MiaVar come array di tipo char con massimo 30<br />
elementi, ma non ancora come stringa. Affinché MiaVar sia identificata da<br />
operatori e funzioni come stringa, dobbiamo non solo definire una variabile<br />
array di tipo char, ma anche inserire nell'array una serie di caratteri terminata<br />
da un NULL.<br />
Per esempio, se vogliamo che MiaVar presenti a operatori e funzioni la<br />
stringa "Ciao", dobbiamo scrivere le istruzioni:<br />
MiaVar[0] = 'C';<br />
MiaVar[1] = 'i';<br />
MiaVar[2] = 'a';<br />
MiaVar[3] = 'o';<br />
MiaVar[4] = '\0';<br />
impegnando così 5 elementi dell'array dei 30 disponibili (i rimanenti 25 saranno<br />
ignorati).
Inizializzazione di variabili stringa<br />
Benché le stringhe non siano ammesse nelle operazioni di assegnazione, lo<br />
sono in quelle di inizializzazione (il che conferma che si tratta di due operazioni<br />
diverse!):<br />
Sequenza non valida Sequenza valida<br />
char Saluto[10]; char Saluto[10] = "Ciao";<br />
Saluto = "Ciao";<br />
Nelle inizializzazioni si utilizzano le costanti stringa, i cui caratteri vengono<br />
inser<strong>it</strong>i nei primi elementi dell'array dichiarato; il terminatore viene aggiunto<br />
automaticamente nell'elemento successivo a quello in cui é stato inser<strong>it</strong>o l'ultimo<br />
carattere. La stringa può essere "allungata" fino a un massimo di caratteri<br />
(terminatore compreso) pari alla dimensione dell'array.<br />
E' anche possibile inizializzare una stringa come un normale array; in questo<br />
caso, però, il terminatore deve essere inser<strong>it</strong>o esplic<strong>it</strong>amente:<br />
char Saluto[] = { 'C', 'i', 'a', 'o', '\0' };<br />
ovviamente questa seconda forma, inutilmente più "faticosa", non é mai usata!<br />
Se nella inizializzazione si omette la dimensione dell'array, questa viene<br />
automaticamente defin<strong>it</strong>a dalla lunghezza della costante stringa aumentata di<br />
uno, per far posto al terminatore (in questo caso la stringa non può più essere<br />
"allungata"!):<br />
char Saluto[] = "Ciao"; (allocato in memoria array con 5<br />
elementi).<br />
In caso che si creino delle stringhe con un numero di caratteri (compreso il<br />
terminatore) maggiore di quello dichiarato, il programma non produce<br />
direttamente messaggi di errore, ma invade zone di memoria non di sua<br />
pertinenza, con conseguenze imprevedibili (spesso si verifica un errore fatale a<br />
livello di sistema operativo).<br />
Funzioni di libreria gets e puts
Benché le funzioni gets e puts facciano parte della libreria di I/O del C, il loro<br />
uso é abbastanza frequente anche in programmi <strong>C++</strong>, a causa di alcune<br />
peculiar<strong>it</strong>à che le distinguono da tutte le altre funzioni di I/O.<br />
La funzione gets(argomento) trasferisce l'intero buffer di input di stdin nella<br />
variabile stringa argomento, riconoscendo come unico terminatore il carattere<br />
new-line ('\n') (che é sempre l'ultimo carattere del buffer) e sost<strong>it</strong>uendolo con<br />
il carattere NULL ('\0'). Ne consegue che la stringa può contenere anche blanks<br />
e tabulazioni (a differenza dalle stringhe lette mediante cin o le altre funzioni<br />
di input del C). In pratica, la gets legge da tastiera un'intera riga di testo,<br />
compreso il r<strong>it</strong>orno a capo che trasforma nel terminatore della stringa.<br />
La funzione puts(argomento) trasferisce in stdout il contenuto della variabile<br />
stringa argomento, sost<strong>it</strong>uendo il terminatore di stringa NULL con il carattere<br />
new-line. In pratica, la puts scrive su video un'intera riga di testo, compreso il<br />
r<strong>it</strong>orno a capo.<br />
In entrambi i casi la variabile argomento deve essere stata defin<strong>it</strong>a (nel<br />
programma chiamante) come array di tipo char.<br />
Conversioni fra stringhe e numeri<br />
Le conversioni fra stringhe (contenenti caratteri numerici) e numeri (e<br />
viceversa) non si possono fare direttamente mediante casting, in quanto le<br />
stringhe sono degli array e, in più, ogni elemento che le cost<strong>it</strong>uisce è<br />
convertibile nel corrispondente codice ascii, non nel valore numerico della cifra<br />
rappresentata.<br />
Es.: int('1') da' come risultato il numero 49 (codice ascii del carattere 1) e non<br />
il numero 1<br />
Bisogna invece ricorrere a opportune funzioni di libreria.<br />
Conversioni da stringhe a numeri - Le funzioni atoi e atof<br />
Per convertire una stringa in un numero, la via più semplice è usare le funzioni<br />
di libreria (del C) atoi e atof :<br />
• atoi(argomento), dove argomento è una stringa contenente la<br />
rappresentazione decimale di un numero intero, esegue la conversione di<br />
argomento e rest<strong>it</strong>uisce un valore di tipo int<br />
• atof(argomento), dove argomento è una stringa contenente la<br />
rappresentazione decimale di un numero floating (in notazione normale o
esponenziale), esegue la conversione di argomento e rest<strong>it</strong>uisce un valore<br />
di tipo double<br />
Entrambe le funzioni vanno utilizzate includendo l'header-file: <br />
Il processo di conversione si interrompe (con il numero calcolato fino a quel<br />
momento) appena è incontrato un carattere non valido (senza messaggi di<br />
errore). Se nessun carattere è convert<strong>it</strong>o atoi e atof r<strong>it</strong>ornano rispettivamente 0<br />
e 0.0<br />
Conversioni da numeri a stringhe - La funzione sprintf<br />
Per convertire numeri in stringhe, è più conveniente (rispetto ad altre<br />
possibil<strong>it</strong>à) usare la funzione di libreria (del C) sprintf. Infatti questa funzione,<br />
non solo esegue la conversione, ma permette anche di ottenere una stringa<br />
formattata nel modo desiderato.<br />
All'inizio di questo corso abbiamo trattato della funzione printf, che utilizza gli<br />
specificatori di formato per scrivere dati sul dispos<strong>it</strong>ivo standard di output<br />
(stdout). La funzione sprintf é identica alla printf salvo il fatto che scrive in una<br />
stringa anziché su stdout. Richiede due argomenti fissi, segu<strong>it</strong>i da un numero<br />
qualsiasi di argomenti opzionali:<br />
• il primo argomento é la variabile stringa (defin<strong>it</strong>a come array di tipo<br />
char) in cui inserire i dati formattati<br />
• il secondo argomento é la control-string (come il primo della printf)<br />
• il terzo argomento e i successivi sono i dati da formattare (come il<br />
secondo e i successivi della printf)<br />
Le stringhe in <strong>C++</strong><br />
Le stringhe descr<strong>it</strong>te finora sono quelle "in stile C". In <strong>C++</strong> si usano ancora, ma<br />
si ricorre più spesso alla classe string della Libreria Standard, che offre<br />
maggiori flessibil<strong>it</strong>à e incapsula tutte le funzioni di manipolazione delle stringhe.
Una funzione è così defin<strong>it</strong>a:<br />
tipo nome(argomenti)<br />
{<br />
}<br />
Funzioni<br />
Definizione di una funzione<br />
... istruzioni ... (dette: codice di implementazione della funzione)<br />
(notare che la prima istruzione è senza punto e virgola, in quanto é completata<br />
dall'amb<strong>it</strong>o che segue)<br />
• tipo: il tipo del valore di r<strong>it</strong>orno della funzione (con eventuali<br />
specificatori e/o qualificatori), detto anche tipo della funzione; se la<br />
funzione non ha valore di r<strong>it</strong>orno, bisogna specificare void<br />
• nome: l'identificatore della funzione; segue le regole generali di<br />
specifica degli identificatori<br />
• argomenti: lista degli argomenti passati dal programma chiamante;<br />
se non vi sono argomenti, si può specificare void (o, più comodamente,<br />
non scrivere nulla fra le parentesi)<br />
Gli argomenti vanno specificati insieme al loro tipo (come nelle dichiarazioni<br />
delle variabili) e, se più d'uno, separati con delle virgole.<br />
Es. char MiaFunz(int dato, float valore)<br />
la funzione MiaFunz riceve dal programma chiamante gli argomenti:<br />
dato (di tipo int), e valore (di tipo float), e r<strong>it</strong>orna un risultato di tipo<br />
char<br />
Dichiarazione di una funzione<br />
Se in un file di codice sorgente una funzione é chiamata prima di essere<br />
defin<strong>it</strong>a, bisogna dichiararla prima di chiamarla.<br />
La dichiarazione di una funzione (detta anche prototipo) é un'unica<br />
istruzione, formalmente identica alla prima riga della sua definizione, salvo il
fatto che deve terminare con un punto e virgola. Tornando all'esempio precedente<br />
la dichiarazione della funzione MiaFunz é:<br />
char MiaFunz(int dato, float valore);<br />
Nella dichiarazione di una funzione i nomi degli argomenti sono f<strong>it</strong>tizi e non é<br />
necessario che coincidano con quelli dalla definizione (non é neppure necessario<br />
specificarli); invece i tipi sono obbligatori: devono coincidere ed essere nello<br />
stesso ordine di quelli della definizione. Es., un'altra dichiarazione valida della<br />
funzione MiaFunz é:<br />
char MiaFunz(int, float);<br />
NOTA IMPORTANTE<br />
La tendenza dei programmatori in <strong>C++</strong> é di separare le dichiarazioni dalle altre<br />
istruzioni di programma: le prime, che possono riguardare non solo funzioni, ma<br />
anche costanti predefin<strong>it</strong>e o definizioni di tipi astratti, sono sistemate in<br />
header-files (con estensione del nome .h), le seconde in implementation-files<br />
(con estensione .c, .cpp o .cxx); ogni implementation-file che contiene<br />
riferimenti a funzioni (o altro) dichiarate in header-files, deve includere<br />
quest'ultimi mediante la direttiva #include.<br />
Istruzione return<br />
Nel codice di implementazione di una funzione l'istruzione di r<strong>it</strong>orno al<br />
programma chiamante é:<br />
return espressione;<br />
il valore calcolato dell'espressione viene rest<strong>it</strong>u<strong>it</strong>o al programma<br />
chiamante come valore di r<strong>it</strong>orno della funzione (se il suo tipo non<br />
coincide con quello dichiarato della funzione, il compilatore segnala un<br />
errore, oppure, quando può, esegue una conversione implic<strong>it</strong>a, con<br />
warning se c'é pericolo di loss of data)<br />
Non é necessario che tale istruzione sia fisicamente l'ultima (e non é neppure<br />
necessario che ve ne sia una sola: dipende dalla presenza delle istruzioni di<br />
controllo, che possono interrompere l'esecuzione della funzione in punti<br />
diversi). Se la funzione non ha valore di r<strong>it</strong>orno (tipo void), bisogna<br />
specificare return; (da solo). Questa istruzione può essere omessa quando il<br />
punto di r<strong>it</strong>orno coincide con la fine fisica della funzione.
Comunicazioni fra programma chiamante e funzione<br />
Da programma chiamante a funzione<br />
La chiamata di una funzione non di tipo void può essere inser<strong>it</strong>a come<br />
operando in qualsiasi espressione o come argomento nella chiamata di<br />
un'altra funzione (in questo caso il compilatore controlla che il tipo della<br />
funzione sia ammissibile): la chiamata viene esegu<strong>it</strong>a con precedenza rispetto<br />
alle altre operazioni e al suo posto viene sost<strong>it</strong>u<strong>it</strong>o il valore di r<strong>it</strong>orno rest<strong>it</strong>u<strong>it</strong>o<br />
dalla funzione.<br />
Il valore di r<strong>it</strong>orno può non essere utilizzato dal programma chiamante,<br />
come se la funzione fosse di tipo void; in questi casi (cioè se la funzione è di<br />
tipo void, oppure il valore di r<strong>it</strong>orno non interessa), la chiamata non può<br />
essere inser<strong>it</strong>a in una espressione, ma deve assumere la forma di un'istruzione a<br />
se stante.<br />
Quando esegue la chiamata di una funzione, il programma costruisce una copia<br />
di ogni argomento, creando delle variabili locali nell'amb<strong>it</strong>o della funzione<br />
(passaggio degli argomenti per valore). Ciò significa che tutte le modifiche,<br />
fatte dalla funzione al valore di un argomento, hanno effetto soltanto<br />
nell'amb<strong>it</strong>o della funzione stessa.<br />
Es. funzione: funz(int a) { ..... a = a+1; .... }<br />
prog. chiamante: int b = 0 ...... funz(b); .....<br />
il programma, prima di chiamare funz, copia il valore della propria variabile b<br />
nell'argomento a, che diventa una variabile locale nell'amb<strong>it</strong>o di funz; per cui a<br />
"muore" appena il controllo r<strong>it</strong>orna al programma e il valore di b resta invariato,<br />
qualunque modifica abbia sub<strong>it</strong>o a durante l'esecuzione di funz.<br />
A questa regola fa eccezione (per motivi che vedremo in segu<strong>it</strong>o) il caso in cui gli<br />
argomenti sono nomi di array (e quindi in particolare di stringhe). Per<br />
trasmettere un intero array a una funzione (nel caso di singoli elementi non ci<br />
sarebbe eccezione alla regola generale) bisogna inserire nella chiamata il nome<br />
dell'array (senza parentesi quadre) e, corrispondentemente nella funzione la<br />
dichiarazione di una variabile segu<strong>it</strong>a dalla coppia di parentesi quadre. Non<br />
serve specificare la dimensione in quanto la stessa é già stata dichiarata nel<br />
programma chiamante (tuttavia, se l'array é multidimensionale l'unico<br />
indice che si può omettere é quello all'estrema sinistra). In questa s<strong>it</strong>uazione,<br />
tutte le modifiche fatte ai singoli elementi dell'array vengono riprodotte sull'array<br />
del programma chiamante.<br />
Da funzione a programma chiamante
Quando il controllo torna da una funzione al programma chiamante, tram<strong>it</strong>e<br />
l'istruzione: return espressione;, il programma costruisce una copia del valore<br />
calcolato dell'espressione (che "muore" appena termina la funzione), creando<br />
un valore locale nell'amb<strong>it</strong>o del programma chiamante.<br />
Es. nel programma chiamante: ..... int a = funz(); .......<br />
nella funzione: int funz() { ...... return b; .... }<br />
funz rest<strong>it</strong>uisce al programma non la variabile b (che, in quanto locale in funz<br />
muore appena funz termina), ma una sua copia, che sopravvive a funz e diventa<br />
un valore locale (temporaneo, cioè non identificato da un nome) del<br />
programma chiamante, assegnato alla variabile a.<br />
Argomenti di default<br />
In <strong>C++</strong> é consent<strong>it</strong>o "inizializzare" un argomento: come conseguenza, se<br />
nella chiamata l'argomento é omesso, il suo valore é assunto, di default,<br />
uguale alla costante (o variabile globale) usata per l'inizializzazione. Questa<br />
deve essere fatta un'unica volta (e quindi in generale nel prototipo della<br />
funzione, ma non nella sua definizione). Es.<br />
prototipo: void scrive(char [ ] = "Messaggio di saluto");<br />
chiamata: scrive(); equivale a: scrive("Messaggio di saluto");<br />
definizione: void scrive(char ave[ ] ) { ............ }<br />
Se una funzione ha diversi argomenti, di cui alcuni required (da specificare) e<br />
altri di default, quelli required devono precedere tutti quelli di default.<br />
Funzioni con overload<br />
A differenza dal C, il <strong>C++</strong> consente l'esistenza di più funzioni con lo stesso<br />
nome, che sono chiamate: "funzioni con overload". Il compilatore distingue<br />
[p20]
una funzione dall'altra in base alla lista degli argomenti: due funzioni con<br />
overload devono differire per il numero e/o per il tipo dei loro argomenti.<br />
Es. funz(int); e funz(float); verranno chiamate con lo stesso<br />
nome funz, ma sono in realtà due funzioni diverse, in quanto la prima ha un<br />
argomento int, la seconda un argomento float.<br />
Non sono ammesse funzioni con overload che differiscano solo per il tipo del<br />
valore di r<strong>it</strong>orno ; né sono ammesse funzioni che differiscano solo per<br />
argomenti di default.<br />
Es.<br />
Es.<br />
void funz(int); e int funz(int);<br />
non sono accettate, in quanto generano ambigu<strong>it</strong>à: infatti, in una chiamata tipo<br />
funz(n), il programma non saprebbe se trasferirsi alla prima oppure alla seconda<br />
funzione (non dimentichiamo che il valore di r<strong>it</strong>orno può non essere utilizzato).<br />
funz(int); e funz(int, double=0.0);<br />
non sono accettate, in quanto generano ambigu<strong>it</strong>à: infatti, in una chiamata tipo<br />
funz(n), il programma non saprebbe se trasferirsi alla prima funzione (che ha un<br />
solo argomento), oppure alla seconda (che ha due argomenti, ma il secondo<br />
può essere omesso per default).<br />
La tecnica dell'overload, comune sia alle funzioni che agli operatori, é molto<br />
usata in <strong>C++</strong>, perché permette di programmare in modo semplice ed efficiente:<br />
funzioni che eseguono operazioni concettualmente simili possono essere<br />
chiamate con lo stesso nome, anche se lavorano su dati diversi.<br />
Es., per calcolare il valore assoluto di un numero, qualunque sia il suo tipo, si<br />
potrebbe usare sempre una funzione con lo stesso nome (per esempio abs).<br />
Funzioni inline<br />
In <strong>C++</strong> esiste la possibil<strong>it</strong>à di chiedere al compilatore di espandere ogni<br />
chiamata di una funzione con il codice di implementazione della funzione<br />
stessa. Questo si ottiene premettendo alla definizione di una funzione lo<br />
specificatore inline.<br />
Es.<br />
inline double cubo(double x) { return x*x*x ; }<br />
ogni volta che il compilatore trova nel programma la chiamata:<br />
cubo(espressione); la trasforma nell'istruzione : (espressione) *<br />
(espressione) * (espressione) ;
L'uso dello specificatore inline é molto comune, in quanto permette di eliminare<br />
il sovraccarico di lavoro dovuto alla gestione della comunicazione fra programma e<br />
funzione. Se però il numero di chiamate della funzione é molto elevato ed é in<br />
punti diversi del programma, il vantaggio potrebbe essere annullato dall'eccessivo<br />
accrescimento della lunghezza del programma (il vantaggio invece é evidente<br />
quando vi sono poche chiamate ma inser<strong>it</strong>e in cicli while o for: in questo caso lo<br />
specificatore inline fa crescere di poco la dimensione del programma, ma il<br />
numero delle chiamate in esecuzione può essere molto elevato).<br />
In ogni caso il compilatore si riserva il dir<strong>it</strong>to di accettare o rifiutare lo<br />
specificatore inline: in pratica, una funzione che consista di più di 4 o 5 righe<br />
di istruzioni viene compilata come funzione separata, indipendentemente dalla<br />
presenza o meno dello specificatore inline.<br />
Cenni sulle liste<br />
Trasmissione dei parametri tram<strong>it</strong>e l'area stack<br />
In qualsiasi linguaggio di programmazione le liste di dati possono essere<br />
accessibili in vari modi (per esempio in modo randomatico), ma esistono due<br />
particolari categorie di liste caratterizzate da metodi di accesso ben defin<strong>it</strong>i e<br />
utilizzate in numerose circostanze:<br />
• le liste di tipo queue (coda), accessibili con il metodo FIFO (first in-first<br />
out): il primo dato che entra nella lista è il primo a essere serv<strong>it</strong>o; tipiche<br />
queues sono le code davanti agli sportelli, le code di stampa (prior<strong>it</strong>à a<br />
parte) ecc...<br />
• le liste di tipo stack (pila), accessibili con il metodo LIFO (last in-first<br />
out): l'ultimo dato che entra nella lista è il primo a essere serv<strong>it</strong>o.<br />
Uso dell'area stack<br />
Nella trasmissione dei parametri fra programma chiamante e funzione<br />
vengono utilizzate liste di tipo stack: quando una funzione A chiama una<br />
funzione B, sistema in un'area di memoria, detta appunto stack, un pacchetto<br />
di dati, comprendenti:<br />
1. l'area di memoria per tutte le variabili automatiche di B;<br />
2. la lista degli argomenti di B in cui copia i valori trasmessi da A;<br />
3. l'indirizzo di rientro in A (cioè il punto di A in cui il programma deve<br />
tornare una volta completata l'esecuzione di B, trasferendovi l'eventuale<br />
valore di r<strong>it</strong>orno).
La funzione B utilizza tale pacchetto e, se a sua volta chiama un'altra funzione<br />
C, sistema nell'area stack un altro pacchetto, "impilato" sopra il precedente,<br />
come nel seguente schema (tralasciamo le aree riservate alle variabili<br />
automatiche):<br />
Area stack Commenti<br />
Indirizzo di rientro in B<br />
Argomento 1 passato a<br />
C<br />
Argomento 2 passato a<br />
C<br />
Indirizzo di rientro in A<br />
Argomento 1 passato a<br />
B<br />
Argomento 2 passato a<br />
B<br />
Argomento 3 passato a<br />
B<br />
La funzione B chiama la funzione C con due<br />
argomenti<br />
La funzione A chiama la funzione B con tre<br />
argomenti<br />
Quando il controllo deve tornare da C a B, il programma fa riferimento all'ultimo<br />
pacchetto entrato nello stack per conoscere l'indirizzo di rientro in B e,<br />
esegu<strong>it</strong>a tale operazione, rimuove lo stesso pacchetto dallo stack (cancellando<br />
di conseguenza anche le variabili automatiche di C).<br />
La stessa cosa succede quando il controllo rientra da B in A; dopodiché lo stack<br />
rimane vuoto.<br />
Ricorsiv<strong>it</strong>à delle funzioni<br />
Tornando all'esempio precedente, la trasmissione dei parametri attraverso lo<br />
stack garantisce che il meccanismo funzioni comunque, sia che A, B e C siano<br />
funzioni diverse, sia che si tratti della stessa funzione (ogni volta va a cercare<br />
nello stack l'indirizzo di rientro nel programma chiamante e quindi non cambia<br />
nulla se tale indirizzo si trova all'interno della stessa funzione).<br />
Ne consegue che in <strong>C++</strong> (come in C) le funzioni possono chiamare se stesse<br />
(ricorsiv<strong>it</strong>à delle funzioni). Ovviamente tali funzioni devono sempre contenere<br />
un'istruzione di controllo che, se si verificano certe condizioni, ha il comp<strong>it</strong>o di<br />
interrompere la successione delle chiamate.<br />
Esempio tipico di una funzione chiamata ricorsivamente è quello del calcolo del<br />
fattoriale di un numero intero:<br />
int fact(int n) {
Fattoriale in pps<br />
if ( n
1. anz<strong>it</strong>utto deve definire una variabile, di tipo (astratto) va_list (creato in<br />
), che serve per accedere alle singole voci dello stack<br />
Es. : va_list marker ;<br />
2. poi deve chiamare la funzione di libreria va_start, per posizionarsi<br />
nello stack sull'inizio degli argomenti opzionali.<br />
Es. : va_start(marker,b) ; dove b é l'ultimo degli<br />
argomenti fissi;<br />
3. poi, per ogni argomento opzionale che si aspetta di trovare, deve<br />
chiamare la funzione di libreria va_arg<br />
Es. : c = va_arg(marker,int) ;<br />
(notare che il secondo argomento di va_arg definisce il tipo<br />
dell'argomento opzionale, il cui valore sarà trasfer<strong>it</strong>o in c).<br />
4. infine deve chiamare la funzione di libreria va_end per chiudere le<br />
operazioni<br />
Es. : va_end(marker) ;<br />
La libreria standard del C<br />
Cenni sulla Run Time Library<br />
La Run Time Library è la libreria standard del C, usata anche dal <strong>C++</strong>, e<br />
contiene diverse centinaia di funzioni.<br />
Il codice di implementazione delle funzioni di libreria è forn<strong>it</strong>o in forma già<br />
compilata e risiede in files binari (.obj o .lib), mentre i prototipi sono disponibili<br />
in formato sorgente e si trovano distribu<strong>it</strong>i in vari header-files (.h).<br />
Il linker, lanciato da un ambiente di sviluppo, accede in genere automaticamente<br />
ai codici binari della libreria. Il compilatore, invece, richiede che tutte le funzioni<br />
usate in ogni file sorgente di un'applicazione siano espressamente dichiarate,<br />
tram<strong>it</strong>e inclusione dei corrispondenti header-files.<br />
Principali categorie di funzioni della Run-time library<br />
Elenchiamo le principali categorie in cui possono essere classificate le funzioni<br />
della Run Time Library. Per informazioni sulle funzioni individualmente<br />
consultare l'help dell'ambiente di sviluppo disponibile.<br />
Categorie Header-files<br />
Operazioni di Input/Output , <br />
Funzioni matematiche e statistiche ,
Attributi del carattere <br />
Conversioni numeri-stringhe <br />
Gestione e manipolazione stringhe <br />
Gestione dell'ambiente , <br />
Gestione degli errori , <br />
Ricerca e ordinamento dati , <br />
Gestione della data e dell'ora <br />
Gest. numero variabile di argomenti
Riferimenti<br />
Costruzione di una variabile mediante copia<br />
Riassumiamo i casi in cui una variabile (o più in generale un oggetto) viene<br />
costru<strong>it</strong>a (creata) mediante copia di una variabile esistente dello stesso tipo:<br />
• una variabile é defin<strong>it</strong>a e inizializzata con il valore di una costante o di<br />
una variabile esistente;<br />
• l'argomento di una funzione é passato by value (per valore) dal<br />
programma chiamante alla funzione;<br />
• il valore di r<strong>it</strong>orno di una funzione é passato by value dalla funzione<br />
al programma chiamante.<br />
Cosa sono i riferimenti ?<br />
In <strong>C++</strong> i riferimenti sono variabili introdotte dall'operatore di dichiarazione :<br />
Il loro significato è quello di occupare la stessa memoria delle variabili a cui si<br />
riferiscono (in altre parole sono degli alias di altre variabili).<br />
&<br />
Si definiscono come nel seguente esempio: int & ref = var;<br />
(dove var è una variabile di tipo int precedentemente defin<strong>it</strong>a, oppure una<br />
qualunque espressione che rest<strong>it</strong>uisce un l-value di tipo int) la variabile ref è<br />
un riferimento a var: qualsiasi modifica apportata a var si r<strong>it</strong>rova in ref (e<br />
viceversa). I tipi di ref e var devono coincidere, non è ammesso il casting in<br />
nessun caso (anche quando i tipi sono in pratica gli stessi, come int e long).<br />
L'insieme int & assume la connotazione di un nuovo tipo: il tipo di riferimento<br />
a int.<br />
Nota: nelle definizioni multiple & va ripetuto: in altre parole, l'operatore di<br />
dichiarazione & va considerato, dal punto di vista sintattico, un prefisso<br />
dell'identificatore e non un suffisso del tipo.<br />
Va da sé che i riferimenti vanno sempre inizializzati. L'inizializzazione, tuttavia,<br />
non comporta la costruzione di una nuova variabile mediante copia, in quanto,<br />
per il programma, si tratta sempre della stessa variabile (la differenza fra i nomi<br />
"scompare" dopo la compilazione).<br />
E' anche possibile dichiarare un riferimento con lo specificatore const :<br />
const int & ref = var;
si può sempre modificare var (e di conseguenza resta modificato ref), ma non si<br />
può modificare direttamente ref, che per questo viene anche detto alias di<br />
riferimento a sola lettura.<br />
I riferimenti dichiarati const si possono anche inizializzare con un non lvalue<br />
(e non necessariamente dello stesso tipo, purchè convertibile<br />
implic<strong>it</strong>amente). Es.:<br />
int & ref = var+1; non ammesso: var+1 non è un l-value<br />
const int & ref = var+1; ammesso, anche se var non è int<br />
Ciò è possibile perchè in questo caso il programma crea una variabile temporanea<br />
di tipo int (chiamiamola temp) che inizializza con var+1 (dopo aver<br />
convert<strong>it</strong>o, se necessario, il tipo di var in int) e poi definisce:<br />
const int & ref = temp;<br />
La variabile temp persiste nello stesso amb<strong>it</strong>o di ref, ma non è accessibile e<br />
quindi in questo caso ref non può più cambiare anche se cambia var.<br />
Comunicazione per "riferimento" fra programma e funzione<br />
L'uso più frequente dei riferimenti si ha nelle comunicazioni fra funzione e programma<br />
chiamante. Infatti, mentre in C il passaggio degli argomenti e del valore di r<strong>it</strong>orno avviene<br />
sempre e soltanto by value, in <strong>C++</strong> può avvenire anche by reference (per riferimento).<br />
Da programma chiamante a funzione<br />
Un argomento passato a una funzione può essere dichiarato come<br />
riferimento:<br />
funz(int& num)<br />
in questo caso l'argomento è passato by reference, cioè non ne viene<br />
costru<strong>it</strong>a una copia, ma la variabile num é un alias di riferimento della sua<br />
corrispondente nel programma chiamante.<br />
Ne consegue che ogni modifica apportata a num in funz viene effettuata anche<br />
nel programma chiamante.<br />
Es.: funzione: funz(int& a) { ..... a = a+1; .... }<br />
prog. chiamante: int b = 0; ...... funz(b); .....
alla fine in b si r<strong>it</strong>rova il valore 1<br />
In base alle regole enunciate nel paragrafo precedente, il valore passato alla<br />
funzione deve essere un l-value ed esattamente dello stesso tipo del<br />
corrispondente argomento dichiarato nella funzione (a meno che non venga<br />
dichiarato const, nel qual caso non ci sono restrizioni, purchè sia ammessa la<br />
conversione di tipo implic<strong>it</strong>a).<br />
Da funzione a programma chiamante<br />
Anche il valore di r<strong>it</strong>orno rest<strong>it</strong>u<strong>it</strong>o da una funzione può essere dichiarato<br />
come riferimento.<br />
Es.: nel programma chiamante: ..... funz( ); .......<br />
nella funzione: int& funz( ) { ...... return b; .... }<br />
anche questa volta il valore è passato by reference, cioè non ne viene<br />
costru<strong>it</strong>a una copia, ma nel programma chiamante viene utilizzato<br />
direttamente il riferimento al valore b rest<strong>it</strong>u<strong>it</strong>o da funz (per le note regole b<br />
deve essere un l-value, a meno che il valore di r<strong>it</strong>orno non sia dichiarato<br />
const) .<br />
In questo caso però, onde ev<strong>it</strong>are errori in esecuzione, é necessario che b<br />
sopravviva a funz; ciò é possibile soltanto in uno dei seguenti tre casi:<br />
• b é una variabile globale<br />
• b é una variabile locale di funz, ma dichiarata static<br />
• b é essa stessa un argomento di funz, a sua volta passato by reference.<br />
Il valore di r<strong>it</strong>orno è un l-value (se non è dichiarato const). Questo significa<br />
che la chiamata di una funzione che r<strong>it</strong>orna un valore by reference può<br />
essere messa a sinistra di un'operazione di assegnazione !!!<br />
Ciò è possibile solo in <strong>C++</strong> !
Direttive al Preprocessore<br />
Cos'é il preprocessore ?<br />
In <strong>C++</strong> (come in C), prima che il compilatore inizi a lavorare, viene attivato un<br />
programma, detto preprocessore, che ricerca nel file sorgente speciali<br />
istruzioni, chiamate direttive.<br />
Una direttiva inizia sempre con il carattere # (a colonna 1) e occupa una sola<br />
riga (non ha un terminatore, in quanto finisce alla fine della riga; riconosce però<br />
i commenti, introdotti da // o da /*, e la continuazione alla riga successiva,<br />
defin<strong>it</strong>a da \).<br />
Il preprocessore crea una copia del file sorgente (da far leggere al<br />
compilatore) e, ogni volta che incontra una direttiva, la esegue sost<strong>it</strong>uendola<br />
con il risultato dell'operazione. Pertanto il preprocessore, eseguendo le<br />
direttive, non produce codice binario, ma codice sorgente per il compilatore.<br />
Ogni file sorgente, dopo la trasformazione operata dal preprocessore, prende<br />
il nome di translation un<strong>it</strong>. Ogni translation un<strong>it</strong> viene poi compilata<br />
separatamente, con la creazione del corrispondente file oggetto, in codice<br />
binario. Spetta al linker, infine, collegare tutti i files oggetto, generando un<br />
unico programma eseguibile.<br />
Nel linguaggio esistono molte direttive (alcune delle quali dipendono dal sistema<br />
operativo). In questo corso tratteremo soltanto delle seguenti: #include ,<br />
#define , #undef e direttive condizionali.<br />
Direttiva #include<br />
Ci é già noto il significato della direttiva #include:<br />
#include oppure #include "filename"<br />
che determina l'inserimento, nel punto in cui si trova la direttiva, dell'intero<br />
contenuto del file con nome filename.<br />
Se si usano le parentesi angolari, si intende che filename vada cercato nella<br />
directory di default del linguaggio; se invece si usano le virgolette, il file si trova<br />
nella directory del programma.
La direttiva #include viene usata quasi esclusivamente per inserire gli headerfiles<br />
(.h) ed é particolarmente utile quando in uno stesso programma ci sono più<br />
implementation-files che includono lo stesso header-file.<br />
Direttiva #define di una costante<br />
Quando il preprocessore incontra la seguente direttiva:<br />
#define identificatore valore<br />
dove, identificatore è un nome simbolico (che segue le regole generali di<br />
specifica di tutti gli altri identificatori) e valore é un'espressione qualsiasi,<br />
delim<strong>it</strong>ata a sinistra da blanks o tabs e a destra da blanks, tabs o new-line (i<br />
blanks e tabs interni fanno parte di valore), sost<strong>it</strong>uisce identificatore con<br />
valore in tutto il file (da quel punto in poi).<br />
Es. #define bla frase qualsiasi anche con "virgolette"<br />
sost<strong>it</strong>uisce (da quel punto in poi) in tutto il file la parola bla con la frase: frase<br />
qualsiasi anche con "virgolette" (la "stranezza" dell'esempio riportato ha lo<br />
scopo di dimostrare che la sost<strong>it</strong>uzione é assolutamente fedele e cieca, qualunque<br />
sia il contenuto dell'espressione che viene sost<strong>it</strong>u<strong>it</strong>a all'identificatore; il<br />
comp<strong>it</strong>o di "segnalare gli errori" viene lasciato al compilatore!)<br />
In generale la direttiva #define serve per assegnare un nome a una costante<br />
(che viene detta "costante predefin<strong>it</strong>a").<br />
Es. #define ID_START 3457<br />
da questo punto in poi, ogni volta che il programma deve usare il numero 3457,<br />
si può specificare in sua vece ID_START<br />
Esistono principalmente due vantaggi nell'uso di #define:<br />
• se il programmatore decide di cambiare valore a una costante, é sufficiente<br />
che lo faccia in un solo punto del programma;<br />
• molto spesso i nomi sono più significativi e mnemonici dei numeri (oppure<br />
più brevi delle stringhe, se rappresentano costanti stringa) e perciò<br />
l'uso delle costanti predefin<strong>it</strong>e permette una maggiore leggibil<strong>it</strong>à del<br />
codice e una maggiore efficienza nella programmazione.<br />
In pratica la direttiva #define produce gli stessi risultati dello specificatore di<br />
tipo const; al posto della direttiva dell'esempio precedente si sarebbe potuto<br />
scrivere la dichiarazione:
const int ID_START = 3457;<br />
Confronto fra la direttiva #define e lo specificatore const<br />
Vantaggi nell'uso di const:<br />
• il tipo della costante é dichiarato; un eventuale errore di dichiarazione<br />
viene segnalato immediatamente;<br />
• la costante é riconosciuta, e quindi analizzabile, nelle operazioni di debug.<br />
Vantaggi nell'uso di #define:<br />
• una costante predefin<strong>it</strong>a a volte è più comoda e immediata (è una<br />
questione sostanzialmente "estetica"!) e può essere usata anche per altri<br />
scopi (per esempio per sost<strong>it</strong>uire o mascherare nomi).<br />
Direttiva #define di una macro<br />
Quando il preprocessore incontra la seguente direttiva:<br />
#define identificatore(argomenti) espressione<br />
riconosce una macro, che distingue dalla definizione di una costante per la<br />
presenza della parentesi tonda sub<strong>it</strong>o dopo identificatore (senza blanks in<br />
mezzo).<br />
Una macro é molto simile a una funzione. Il suo uso é chiar<strong>it</strong>o dal seguente<br />
esempio:<br />
#define Max(a,b) a > b ? a : b<br />
tutte le volte che il preprocessore trova nel programma una chiamata della<br />
macro, per esempio Max(x,y), la espande, sost<strong>it</strong>uendola con: x > y ? x : y<br />
Come nel caso di definizione di una costante, anche per una macro la<br />
sost<strong>it</strong>uzione avviene in modo assolutamente fedele: a parte i nomi degli<br />
argomenti, che sono ricopiati dalla chiamata e non dalla definizione, tutti gli<br />
altri simboli usati nella definizione sono riprodotti senza alcuna modifica (per
esempio il punto e virgola di fine istruzione viene messo solo se compare anche<br />
nella definizione).<br />
Nella chiamata di una macro si possono mettere, al posto degli argomenti,<br />
anche delle espressioni (come nelle chiamate di funzioni); sarà comp<strong>it</strong>o,<br />
come al sol<strong>it</strong>o, del compilatore controllare che l'espressione risultante sia<br />
accettabile. Riprendendo l'esempio precedente, la seguente chiamata:<br />
Max(x+1,y) espansa in x+1 > y ? x+1 : y<br />
sarà accettata dal compilatore, in istruzioni del tipo :<br />
c = Max(x+1,y);<br />
ma rigettata in istruzioni come:<br />
Max(x+1,y) = c;<br />
in quanto, in questo caso, gli operandi di un operatore condizionale devono<br />
essere l-values.<br />
In altri casi, la sost<strong>it</strong>uzione "cieca" può causare errori che lo stesso compilatore<br />
non é in grado di riconoscere.<br />
Es. #define quadrato(x) x*x<br />
la chiamata: quadrato(2+3) viene espansa in 2+3*2+3 con risultato,<br />
evidentemente, errato.<br />
Per ev<strong>it</strong>are tale errore si sarebbe dovuto scrivere: #define quadrato(x)<br />
(x)*(x)<br />
Agli effetti pratici (purché si usino le dovute attenzioni!), la definizione di una<br />
macro produce gli stessi risultati dello specificatore inline di una funzione.<br />
Confronto fra la direttiva #define e lo specificatore inline<br />
Vantaggi nell'uso di inline:<br />
• il tipo della funzione é dichiarato e controllato ;<br />
• la funzione é riconosciuta, e quindi analizzabile, nelle operazioni di<br />
debug;<br />
• l'espansione di una funzione inline é fatta non in modo "cieco", ma in<br />
modo "intelligente" (vantaggio decisivo!).<br />
Vantaggi nell'uso di #define:<br />
• Una macro e più immediata e più semplice da scrivere di una funzione.<br />
Le macro, usatissime in C, sono raramente utilizzate in <strong>C++</strong>, se non per<br />
funzioni molto brevi e adoperate a livello locale (cioè nello stesso modulo in cui<br />
sono defin<strong>it</strong>e). Un uso più frequente delle macro si ha quando non<br />
corrispondono a funzioni ma a espressioni "parametrizzate" molto lunghe che<br />
compaiono più volte nel programma.
Direttive condizionali<br />
Il preprocessore dispone di un suo mini-linguaggio di controllo, che consiste<br />
nelle seguenti direttive condizionali:<br />
#if espressione1 oppure #if defined(identificatore1) oppure ...<br />
#if !defined(identificatore1)<br />
...... blocco di direttive e/o istruzioni ........<br />
#elif espressione2 oppure #elif defined(identificatore2) oppure ...<br />
#else<br />
#endif<br />
dove:<br />
#elif !defined(identificatore2)<br />
...... blocco di direttive e/o istruzioni ........<br />
...... blocco di direttive e/o istruzioni ........<br />
espressione é un espressione logica che può contenere solo identificatori<br />
di costanti predefin<strong>it</strong>e o costanti l<strong>it</strong>erals, ma non variabili e neppure<br />
variabili dichiarate const, che il preprocessore non riconosce<br />
defined(identificatore) rest<strong>it</strong>uisce vero se identificatore é defin<strong>it</strong>o<br />
(cioè se é stata esegu<strong>it</strong>a la direttiva: #define identificatore); al posto di<br />
#if defined(identificatore) si può usare la forma: #ifdef identificatore<br />
!defined(identificatore) rest<strong>it</strong>uisce vero se identificatore non é<br />
defin<strong>it</strong>o; al posto di #if !defined(identificatore) si può usare la forma:<br />
#ifndef identificatore<br />
#elif sta per else if ed é opzionale (possono esserci più blocchi<br />
consecutivi, ciascuno introdotto da un #elif)<br />
#else é opzionale (se esiste, deve introdurre l'ultimo blocco prima di<br />
#endif)<br />
#endif (obbligatorio) termina la sequenza iniziata con un #if<br />
non é necessario racchiudere i blocchi fra parentesi graffe, perché ogni blocco é<br />
terminato da #elif, o da #else, o da #endif<br />
Il preprocessore identifica il blocco (se esiste) che corrisponde alla prima<br />
condizione risultata vera, oppure il blocco relativo alla direttiva #else (se esiste)<br />
nel caso che tutte le condizioni precedenti siano risultate false. Tale blocco può<br />
contenere sia istruzioni di programma che altre direttive, comprese direttive<br />
condizionali (possono esistere più blocchi #if "innestati"): il preprocessore<br />
esegue le direttive e presenta al compilatore le istruzioni che si trovano nel
locco selezionato, scartando sia direttive che istruzioni contenute negli altri<br />
blocchi della sequenza #if ... #endif.<br />
La direttiva:<br />
Direttiva #undef<br />
#undef identificatore<br />
indica al preprocessore di disattivare l'identificatore specificato, cioè<br />
rimuovere la corrispondenza fra l'identificatore e una costante,<br />
precedentemente stabil<strong>it</strong>a con la direttiva:<br />
#define identificatore costante<br />
Nelle istruzioni successive alla direttiva #undef, lo stesso nome potrà essere<br />
adib<strong>it</strong>o ad altri usi.<br />
Es. #ifdef EOF<br />
#undef EOF<br />
#endif<br />
char EOF[] = "Ente Opere Filantropiche";
Sviluppo delle applicazioni in ambiente Windows<br />
Definizioni di IDE e di "progetto"<br />
Un IDE (Integrated Development Environment) è un programma interattivo che<br />
si lancia da sistema operativo e che aiuta lo sviluppatore di software (cioè il<br />
programmatore) a costruire un progetto.<br />
Un progetto è un insieme di files, contenenti codice sorgente, che vengono<br />
letti ed elaborati dal compilatore separatamente e poi collegati insieme (tram<strong>it</strong>e<br />
il linker) per costruire un unico file in codice binario, contenente il programma<br />
eseguibile, che può essere a sua volta lanciato dallo stesso IDE o<br />
autonomamente da sistema operativo.<br />
Lo sviluppatore interagisce con IDE tram<strong>it</strong>e menù di tipo pop-up (a tendina); in<br />
genere le voci di menù più significative sono selezionabili anche tram<strong>it</strong>e toolbars<br />
(gruppi di icone) o tram<strong>it</strong>e i cosiddetti acceleratori (tasti della keyboard che<br />
eseguono la stessa funzione della corrispondente voce di menù).<br />
Un IDE può aprire sullo schermo e usare parecchie finestre<br />
contemporaneamente, contenenti i files sorgente (uno per ogni finestra), l'output<br />
del programma, le informazioni acquis<strong>it</strong>e in fase di debug ecc… Possono esistere<br />
anche finestre che contengono l'elenco dei files, delle funzioni, o anche delle<br />
singole variabili utilizzate; "cliccando" su queste voci si può raggiungere<br />
rapidamente la parte di programma che interessa esaminare o modificare.<br />
Nel segu<strong>it</strong>o illustreremo brevemente l'utilizzo del seguente IDE: Microsoft<br />
Visual <strong>C++</strong>, versione 6, che gira nel sistema operativo Windows. Teniamo a<br />
precisare che il Visual <strong>C++</strong> non è soltanto un IDE, ma un linguaggio vero e<br />
proprio, essendo dotato di funzional<strong>it</strong>à e librerie che vanno ben oltre lo standard<br />
<strong>C++</strong>. Noi ci lim<strong>it</strong>eremo, però, ad illustrare il suo ambiente di sviluppo, nella<br />
versione "ridotta" per applicazioni che utilizzano solo codice standard.<br />
Gestione di files e progetti<br />
• creazione nuovo progetto o nuovo file<br />
• apertura e chiusura progetto<br />
• inserimento di files esistenti nel progetto aperto<br />
• apertura, salvataggio (con eventuale cambiamento del nome) e chiusura file<br />
• tutte le operazioni di selezione di file o directory sono eseguibili tram<strong>it</strong>e dialog box<br />
oppure direttamente dalla lista dei MRU (Most Recently Used)
Ed<strong>it</strong>or di testo<br />
Un IDE è normalmente provvisto di tutte le funzional<strong>it</strong>à standard di un ed<strong>it</strong>or di<br />
testo interattivo (cut, copy, paste, delete, find, replace, undo, redo, ecc…). In più,<br />
il suo ed<strong>it</strong>or è "intelligente", nel senso che è in grado di riconoscere ed<br />
interpretare il testo in modo da renderlo di più facile comprensione (per esempio,<br />
scrive le parole-chiave con un altro colore, "indenta" automaticamente le<br />
istruzioni che continuano nella riga successiva o che appartengono ad un amb<strong>it</strong>o<br />
interno ecc...).<br />
Gestione delle finestre<br />
• full screen della finestra attiva<br />
• selezione della finestra da porre in primo piano<br />
• visione contemporanea di più finestre (allineate orizzontalmente, verticalmente o in<br />
cascade) ecc…<br />
Costruzione dell'applicazione eseguibile<br />
• file make: è creato e aggiornato automaticamente; contiene tutte le relazioni fra i files<br />
sorgente e le opzioni di compilazione e link del progetto<br />
• programma make: legge il file make ed esegue:<br />
o la compilazione di tutti i files del progetto, creando un file binario .obj per ogni<br />
file sorgente incluso nel progetto; inoltre la compilazione è di tipo<br />
incrementale, nel senso che ricompila solo i files sorgente che sono stati modificati<br />
dopo la creazione dei rispettivi .obj<br />
o il link di tutti i .obj per la creazione del programma eseguibile, che ha<br />
estensione .exe; anche in questo caso l'operazione è di tipo incrementale, cioè<br />
viene esegu<strong>it</strong>a solo se almeno un .obj è stato modificato (o se il .exe non esiste).
Debug del programma<br />
Eseguendo il programma in modo debug, è possibile inserire dei breakpoints<br />
(punti di interruzione del programma) direttamente nel codice sorgente e poi<br />
esaminare il valore corrente delle variabili (con il comando watch, oppure<br />
semplicemente posizionando il cursore del mouse sulla variabile da ispezionare: si<br />
apre una finestrella gialla (tip) che mostra il contenuto della variabile), oppure<br />
eseguire il programma step-by-step (una istruzione alla volta) ecc…<br />
Utilizzo dell'help in linea<br />
Ogni buon IDE è provvisto di un robusto sistema di documentazione che spiega il<br />
significato e il modo di utilizzo delle parole-chiave del linguaggio, dei simboli,<br />
delle variabili predefin<strong>it</strong>e e, ovviamente, delle funzioni di libreria; di sol<strong>it</strong>o è<br />
organizzato per topics (argomenti), ma esiste anche la possibil<strong>it</strong>à di eseguire la<br />
ricerca di ogni singolo termine presente del sistema accedendo a un elenco<br />
generale in ordine alfabetico.<br />
Inoltre è disponibile il "context sens<strong>it</strong>ive help" che permette di accedere<br />
direttamente all'informazione desiderata posizionando il cursore del mouse<br />
all'interno della finestra di ed<strong>it</strong>or del proprio file sorgente, sopra la variabile o<br />
funzione da esaminare, e poi spingendo il tasto F1.<br />
Infine il testo della guida in linea è accessibile con la funzional<strong>it</strong>à copy dell'ed<strong>it</strong>or<br />
(ovviamente non con cut o paste, essendo in sola lettura): ciò consente di<br />
selezionare e trasferire nel proprio programma brani di codice (per esempio nomi<br />
di funzioni o variabili predefin<strong>it</strong>e) senza possibil<strong>it</strong>à di errore.
L'operatore unario di indirizzo :<br />
Indirizzi e Puntatori<br />
Operatore di indirizzo &<br />
rest<strong>it</strong>uisce l'indirizzo della locazione di memoria dell'operando.<br />
L'operando deve essere un ammissibile l-value. Il valore rest<strong>it</strong>u<strong>it</strong>o<br />
dall'operatore non può essere usato come l-value (in quanto l'indirizzo di<br />
memoria di una variabile non può essere assegnato in un'istruzione, ma è<br />
predeterminato dal programma).<br />
Esempi (notare l'uso delle parentesi per alterare l'ordine delle precedenze):<br />
&a<br />
&<br />
ammesso, purché a sia un l-value<br />
&(a+1) non ammesso, in quanto a+1 non é un l-value<br />
&(a>b?a:b) ammesso, in quanto l'operatore condizionale può rest<strong>it</strong>uire un<br />
l-value,<br />
purché a e b siano l-values<br />
&a = b non ammesso, in quanto l'operatore & non può rest<strong>it</strong>uire un lvalue<br />
Gli indirizzi di memoria sono rappresentati da numeri interi, in byte, e, nelle<br />
operazioni di output, sono scr<strong>it</strong>ti, di default, in forma esadecimale.<br />
Cosa sono i puntatori ?<br />
I puntatori sono particolari tipi del linguaggio. Una variabile di tipo puntatore<br />
é designata a contenere l'indirizzo di memoria di un'altra variabile (detta<br />
variabile puntata), la quale a sua volta può essere di qualunque tipo, anche<br />
non nativo (persino un altro puntatore!).
Dichiarazione di una variabile di tipo puntatore<br />
Benché gli indirizzi siano numeri interi e quindi una variabile puntatore possa<br />
contenere solo valori interi, tuttavia il <strong>C++</strong> (come il C) pretende che nella<br />
dichiarazione di un puntatore sia specificato anche il tipo della variabile<br />
puntata (in altre parole un dato puntatore può puntare solo a un determinato<br />
tipo di variabili, quello specificato nella dichiarazione).<br />
Per ottenere ciò, bisogna usare l'operatore di dichiarazione : *<br />
Es. :<br />
int * pointer<br />
dichiara (e definisce) la variabile pointer, puntatore a<br />
variabile di tipo int<br />
Nota: nelle definizioni multiple * va ripetuto: in altre parole, l'operatore di<br />
dichiarazione * va considerato, dal punto di vista sintattico, un prefisso<br />
dell'identificatore e non un suffisso del tipo.<br />
Si può dire pertanto che, a questo punto della nostra conoscenza, il numero dei<br />
tipi del <strong>C++</strong> é "raddoppiato": esistono tanti tipi di puntatori quanti sono i tipi<br />
delle variabili puntate.<br />
Un puntatore accetta quasi sempre il casting, purché il risultato della<br />
conversione sia ancora un puntatore. Tornando all'esempio precedente,<br />
l'operazione di casting:<br />
(double*)pointer<br />
rest<strong>it</strong>uisce un puntatore a una variabile di tipo double.<br />
Nota2: nel casting, invece, l'operatore di dichiarazione * è un suffisso del<br />
tipo. (!)<br />
Si può anche dichiarare un puntatore a puntatore.<br />
Es. : double** pointer_to_pointer<br />
dichiara (e definisce) la variabile pointer_to_pointer,<br />
puntatore a puntatore a variabile di tipo double<br />
Assegnazione di un valore a un puntatore<br />
Sappiamo che gli indirizzi di memoria non possono essere assegnati da<br />
istruzioni di programma, ma sono determinati automaticamente in fase di
esecuzione; quindi non si possono assegnare valori a un puntatore, salvo che in<br />
questi quattro casi:<br />
• a un puntatore é assegnato il valore NULL (non punta a "niente");<br />
• a un puntatore é assegnato l'indirizzo di una variabile esistente,<br />
rest<strong>it</strong>u<strong>it</strong>o dall'operatore &<br />
( Es. : int a; int* p; p = &a; );<br />
• é esegu<strong>it</strong>a un'operazione di allocazione dinamica della memoria (di cui<br />
tratteremo più avanti);<br />
• a un puntatore é assegnato il valore che deriva da un'operazione di<br />
ar<strong>it</strong>metica dei puntatori (vedere prossima sezione).<br />
Quanto detto per le assegnazioni vale anche per le inizializzazioni.<br />
Va precisato, comunque, che ogni tentativo di assegnare valori a un puntatore<br />
in casi diversi da quelli sopraelencati (per esempio l'assegnazione di una<br />
costante) cost<strong>it</strong>uisce un errore che non viene segnalato dal compilatore, ma che<br />
può produrre effetti indesiderabili (o talvolta disastrosi) in fase di esecuzione.<br />
Ar<strong>it</strong>metica dei puntatori<br />
Abbiamo detto che il valore assunto da un puntatore é un numero intero che<br />
rappresenta, in byte, un indirizzo di memoria. Il <strong>C++</strong> (come il C) ammette le<br />
operazioni di somma fra un puntatore e un valore intero (con risultato<br />
puntatore), oppure di sottrazione fra due puntatori (con risultato intero).<br />
Tali operazioni vengono però esegu<strong>it</strong>e in modo "intelligente", cioè tenendo conto<br />
del tipo della variabile puntata. Per esempio, se si incrementa un puntatore a<br />
float di 3 un<strong>it</strong>à, in realtà il suo valore viene incrementato di 12 byte.<br />
Queste regole dell'ar<strong>it</strong>metica dei puntatori assicurano che il risultato sia<br />
sempre corretto, qualsiasi sia la lunghezza in byte della variabile puntata. Per<br />
esempio, se p punta a un elemento di un array, p++ punterà all'elemento<br />
successivo, qualunque sia il tipo (anche non nativo) dell'array.<br />
Operatore di dereferenziazione *
L'operatore unario di dereferenziazione * (che abbrevieremo in deref.) di un<br />
puntatore rest<strong>it</strong>uisce il valore della variabile puntata dall'operando ed ha un<br />
duplice significato:<br />
• usato come r-value, esegue un'operazione di estrazione.<br />
Es. a = *p ; (assegna ad a il valore della variabile puntata da p)<br />
• usato come l-value, esegue un'operazione di inserimento.<br />
Es. *p = a ; (assegna il valore di a alla variabile puntata da p)<br />
In pratica l'operazione di deref. é inversa a quella di indirizzo. Infatti, se<br />
assegniamo a un puntatore p l'indirizzo di una variabile a,<br />
p = &a ;<br />
allora la relazione logica: *p == a risulta vera, cioè la deref. di p coincide<br />
con a.<br />
Ovviamente non é detto il contrario, cioè, se assegniamo alla deref. di p il<br />
valore di a,<br />
p = &b ;<br />
*p = a ;<br />
ciò non comporta automaticamente che in p si r<strong>it</strong>rovi l'indirizzo di a (dove invece<br />
resta l'indirizzo di b), ma semplicemente che il valore della variabile puntata<br />
da p (cioè b) coinciderà con a.<br />
Puntatori a void<br />
Contrariamente all'apparenza un puntatore dichiarato a void,<br />
es.: void* vptr;<br />
può puntare a qualsiasi tipo di variabile. Ne consegue che a un puntatore a<br />
void si può assegnare il valore di qualunque puntatore, ma non viceversa (é<br />
necessario operare il casting).<br />
Es.: defin<strong>it</strong>i: int* iptr; e void* vptr;<br />
é ammessa l'assegnazione: vptr = iptr;<br />
ma non: iptr = vptr;<br />
bensì: iptr = (int*)vptr;<br />
I puntatori a void non possono essere dereferenziati nè possono essere<br />
inser<strong>it</strong>i in operazioni di ar<strong>it</strong>metica dei puntatori. In generale si usano quando il<br />
tipo della variabile puntata non è ancora stabil<strong>it</strong>o al momento della<br />
definizione del puntatore, ma è determinato successivamente, in base al flusso<br />
di esecuzione del programma.
Errori di dangling references<br />
In <strong>C++</strong> (come in C) l'assegnazione dell'indirizzo di una variabile a a un<br />
puntatore p :<br />
p = &a ;<br />
e il successivo accesso ad a tram<strong>it</strong>e deref. di p, possono portare a errori di<br />
dangling references (perd<strong>it</strong>a degli agganci) se puntatore e variabile<br />
puntata non condividono lo stesso amb<strong>it</strong>o d'azione. Infatti, se l'amb<strong>it</strong>o di p é<br />
più esteso di quello di a (per esempio se p é una variabile globale) e a va out of<br />
scope mentre p continua ad essere visibile, la deref. di p accede ad un'area della<br />
memoria non più allocata al programma, con risultati spesso imprevedibili.<br />
Funzioni con argomenti puntatori<br />
Quando, nella chiamata di una funzione, si passa come argomento un<br />
indirizzo (sia che si tratti di una variabile puntatore oppure del risultato di<br />
un'operazione di indirizzo), per esempio (essendo, al sol<strong>it</strong>o, p un puntatore e<br />
a una qualsiasi variabile):<br />
funz(.... p ....) oppure funz(.... &a ....)<br />
nella definizione (e ovviamente anche nella dichiarazione) della funzione il<br />
corrispondente argomento va dichiarato come puntatore; continuando<br />
l'esempio (se a é di tipo int):<br />
void funz(.... int* p ....)<br />
L'argomento é, come sempre, passato by value. In <strong>C++</strong> é anche possibile,<br />
passarlo by reference, nel qual caso bisogna indicare entrambi gli operatori di<br />
dichiarazione * e & :<br />
void funz(.... int*& p ....)<br />
Se il puntatore é passato by value, nella funzione viene creata una copia del<br />
puntatore e, qualsiasi modifica venga fatta al suo valore, il corrispondente<br />
valore nel programma chiamante rimane inalterato. In questo caso, tuttavia,<br />
tram<strong>it</strong>e l'operazione di deref., la variabile puntata (che si trova nel<br />
programma chiamante), é accessibile e modificabile dall'interno della<br />
funzione.
Es.: programma chiamante: int a = 10; ...... funz(&a);<br />
funzione: void funz( int* p) { ....*p = *p+5; .... }<br />
alla fine, nella variabile a si trova il valore 15 (in questo caso non esistono<br />
problemi di scope, in quanto la variabile a, pur non essendo direttamente visibile<br />
dalla funzione, é ancora in v<strong>it</strong>a e quindi é accessibile tram<strong>it</strong>e un'operazione di<br />
deref.).<br />
Per i motivi suddetti, quando l'argomento della chiamata é un indirizzo, si dice<br />
impropriamente che la variabile puntata é trasmessa by address e che, per<br />
questa ragione, é modificabile. In realtà l'argomento non é la variabile<br />
puntata, ma il puntatore, e questo é trasmesso, come ogni altra variabile, by<br />
value.
Puntatori ed Array<br />
Analogia fra puntatori ed array<br />
Quando abbiamo trattato gli array, avremmo dovuto fare le seguente riflessione:<br />
"Il <strong>C++</strong> é un linguaggio tipato (ogni ent<strong>it</strong>à del linguaggio deve appartenere a un<br />
tipo); e allora, cosa sono gli array ?".<br />
La risposta é: "Gli array sono dei puntatori!".<br />
Quando si dichiara un array, in realtà si dichiara un puntatore, con alcune<br />
caratteristiche in più:<br />
• la dichiarazione di un puntatore comporta allocazione di memoria<br />
per una variabile puntatore, ma non per la variabile puntata.<br />
Es.: int* lista; alloca memoria per la variabile puntatore lista ma non<br />
per la variabile puntata da lista<br />
• la dichiarazione di un array comporta allocazione di memoria non<br />
solo per una variabile puntatore (il nome dell'array), ma anche per<br />
l'area puntata, di cui viene predefin<strong>it</strong>a la lunghezza; inoltre il puntatore<br />
viene dichiarato const e inizializzato con l'indirizzo dell'area puntata<br />
(cioè del primo elemento dell'array).<br />
Es.:<br />
int lista[5];<br />
1. alloca memoria per il puntatore<br />
costante lista;<br />
2. alloca memoria per 5 valori di tipo int;<br />
3. inizializza lista con &lista[0]<br />
Il fatto che il puntatore venga assunto const comporta che l'indirizzo<br />
dell'array non é modificabile e quindi il nome dell'array non può essere<br />
usato come l-value (mentre un normale puntatore sì).<br />
Esiste un'altra differenza fra la dichiarazione di un'array e quella di un<br />
puntatore: in un array l'area puntata può essere inizializzata tram<strong>it</strong>e la lista<br />
degli elementi dell'array, mentre in un puntatore ciò non é ammesso. A questa<br />
regola fa eccezione il caso di un puntatore a char quando l'area puntata é<br />
inizializzata mediante una stringa l<strong>it</strong>eral (per compatibil<strong>it</strong>à con vecchie<br />
versioni del linguaggio).<br />
Es.: char saluto[ ] = "Ciao"; ammesso - saluto é const<br />
char saluto[ ] = {'C','i','a','o','\0'}; ammesso - saluto é const<br />
char* saluto = {'C','i','a','o','\0'}; non ammesso<br />
char* saluto = "Ciao"; ammesso !!! - saluto non é const !!!
nell'ultimo caso, tuttavia, non è concesso modificare la stringa (anche se è<br />
concesso modificare il puntatore!): il programma da' errore in fase di<br />
esecuzione! Per esempio, se poniamo:<br />
saluto[2] = 'c';<br />
la stringa diventa correttamente "Cico" se saluto è stato dichiarato array di<br />
char, mentre risulta un errore di "access violation" della memoria (?!) se<br />
saluto è stato dichiarato puntatore a char. Conclusioni: non inizializzare mai<br />
un puntatore a char con una stringa l<strong>it</strong>eral! (oppure farlo solo se si è sicuri<br />
che la stringa non verrà mai modificata).<br />
Combinazione fra operazioni di deref. e di incremento<br />
Le operazioni di deref. e di incremento (o decremento) possono applicarsi<br />
contemporaneamente allo stesso operando puntatore.<br />
Es. : *p++<br />
In questo caso l'incremento opera sul puntatore e non sulla variabile<br />
puntata e, al sol<strong>it</strong>o, agisce prima della deref. se é prefisso, oppure dopo la<br />
deref. se é suffisso. Da notare che l'espressione nel suo complesso può essere<br />
un l-value, mentre il semplice incremento (o decremento) di una variabile non<br />
lo é. Infatti, un'istruzione del tipo:<br />
*p++ = c ; viene espansa in : *p = c ; p = p+1 ;<br />
e quindi é accettabile perché l'operazione di deref. può essere un l-value,<br />
mentre l'istruzione: a++ = c ;<br />
é inaccettabile in quanto l'operazione di incremento non é un l-value.<br />
Confronto fra operatore [ ] e deref. del puntatore "offsettato"<br />
Poiché il nome (usato da solo) di un array ha il significato di puntatore al primo<br />
elemento dell'array, ogni altro elemento é accessibile tram<strong>it</strong>e un'operazione<br />
di deref. del puntatore-array "offsettato", cioè incrementato di una quant<strong>it</strong>à<br />
pari all'indice dell'elemento. Da questo e dalle note regole di ar<strong>it</strong>metica dei<br />
puntatori consegue che le espressioni (dato un array A):<br />
A[i] e *(A+i)<br />
conducono ad identico risultato e quindi sono perfettamente intercambiabili e<br />
possono essere entrambe usate sia come r-value che come l-value.
Funzioni con argomenti array<br />
Quando, nella chiamata di una funzione, si passa come argomento un array<br />
(senza indici), in realtà si passa un puntatore, cioè l'indirizzo del primo<br />
elemento dell'array e pertanto i singoli elementi sono direttamente modificabili<br />
dall'interno della funzione. Questo spiega l'apparente anomalia di<br />
comportamento degli argomenti array (e in particolare delle stringhe), a cui<br />
abbiamo accennato trattando del passaggio degli argomenti by value.<br />
Es. :<br />
nel programma chiamante:<br />
int A[ ] = {0,0,0}; .... funz(.... A,....) ; ....<br />
nella funzione: void funz(....int A[ ] , ....) { ....A[1] = 5;<br />
....}<br />
il secondo elemento dell'array A risulta modificato, perché in realtà nella<br />
funzione viene esegu<strong>it</strong>a l'operazione: *(A+1)= 5 (il valore 5 viene inser<strong>it</strong>o<br />
nella locazione di memoria il cui indirizzo é A+1).<br />
Nella dichiarazione (e nella definizione) della funzione, un argomento<br />
array può essere indifferentemente dichiarato come array o come puntatore<br />
(in questo caso non c'è differenza perché la memoria é già allocata nel<br />
programma chiamante). Tornando all'esempio, la funzione funz avrebbe<br />
potuto essere defin<strong>it</strong>a nel seguente modo: void funz(....int* A, ....)<br />
Le due dichiarazioni sono perfettamente identiche; di sol<strong>it</strong>o si preferisce la<br />
seconda per evidenziare il fatto che il valore dell'argomento é un indirizzo (il<br />
puntatore creato per copia non é mai assunto const, anche se l'argomento é<br />
dichiarato come array: resta comunque valida la regola che ogni modifica del suo<br />
valore fatta sulla copia non si ripercuote sull'originale).<br />
Funzioni con argomenti puntatori passati by reference<br />
Quando un argomento puntatore é dichiarato in una funzione come<br />
riferimento,<br />
es. void funz(....int*& A, ....),<br />
nel programma chiamante il corrispondente argomento non può essere<br />
dichiarato come array, in quanto, se così fosse, sarebbe const e quindi non l-
value (ricordiamo che gli argomenti passati by reference devono essere degli<br />
l-value, a meno che non siano essi stessi dichiarati const nella funzione).<br />
Array di puntatori<br />
In <strong>C++</strong> (come in C) i puntatori, come qualsiasi altra variabile, possono essere<br />
raggruppati in array e defin<strong>it</strong>i come nel seguente esempio:<br />
int* A[10]; (definisce un array di 10 puntatori a int)<br />
Come un array equivale a un puntatore, così un array di puntatori equivale a<br />
un puntatore a puntatore (con in più l'allocazione della memoria puntata,<br />
come nel caso di array generico). Se questo viene passato come argomento di<br />
una funzione, nella stessa può essere dichiarato indifferentemente come array<br />
di puntatori o come puntatore a puntatore.<br />
Continuando l'esempio precedente:<br />
programma chiamante: funz(.... A,....) ;<br />
dichiarazione di funz: void funz(....int** A, ....);<br />
Il caso più frequente di array di puntatori é quello dell'array di stringhe, che<br />
consente anche l'inizializzazione tram<strong>it</strong>e l'elenco, non dei valori dei puntatori,<br />
ma (atipicamente) delle stesse stringhe che cost<strong>it</strong>uiscono l'array.<br />
Es.: char* colori[3] = {"Blu", "Rosso", "Verde"} ;<br />
Come appare nell'esempio, le stringhe possono anche essere di differente<br />
lunghezza; in memoria sono allocate consecutivamente e, per ciascuna di esse,<br />
sono riservati tanti bytes quant'é la rispettiva lunghezza (terminatore<br />
compreso). Da certi compilatori la memoria allocata per ogni stringa é<br />
arrotondata per eccesso a un multiplo di un numero prefissato di bytes.
Elaborazione della riga di comando<br />
Esecuzione di un programma tram<strong>it</strong>e riga di comando<br />
Un caso tipico di utilizzo di array di stringhe si ha quando il sistema operativo<br />
passa a un programma una serie di parametri, elencati nella riga di<br />
comando.<br />
Es.: copy file1 file2 copy é il programma<br />
file1 e file2 sono i parametri<br />
Anche un programma scr<strong>it</strong>to in <strong>C++</strong> (e trasformato dall'ambiente di sviluppo in<br />
un modulo eseguibile) può essere lanciato da sistema operativo come se fosse<br />
un comando, e può essere accompagnato da parametri. Il <strong>C++</strong> (come il C) si<br />
incarica di trasformare tali parametri in argomenti trasmessi alla funzione<br />
main, per modo che il programma possa elaborarli.<br />
Argomenti passati alla funzione main<br />
Finora abbia supposto che il main fosse una funzione priva di argomenti. In<br />
realtà il sistema operativo passa al main un certo numero di argomenti, di<br />
cui, in questo caso, ci interessano i primi due:<br />
int argc numero di voci presenti nella riga di comando (compreso lo<br />
stesso nome del programma)<br />
char** argv<br />
array di stringhe, in cui ogni elemento corrisponde a una<br />
voce della riga di comando (in fondo viene aggiunta una<br />
stringa NULL)<br />
Pertanto, se il programma deve utilizzare dei parametri, il main va defin<strong>it</strong>o<br />
come segue:<br />
int main(int argc, char** argv)<br />
Per esempio, se la riga di comando contiene: copy file1 file2<br />
argc contiene il numero 3<br />
argv[0] contiene la stringa "copy"<br />
argv[1] contiene la stringa "file1"<br />
argv[2] contiene la stringa "file2"
argv[3] contiene NULL
Puntatori e Funzioni<br />
Funzioni che rest<strong>it</strong>uiscono puntatori<br />
Il valore di r<strong>it</strong>orno rest<strong>it</strong>u<strong>it</strong>o da una funzione può essere di qualsiasi tipo,<br />
compreso il tipo puntatore.<br />
Es.:<br />
int* funz();<br />
dichiara una funzione funz che rest<strong>it</strong>uisce un valore<br />
puntatore a int<br />
Come in generale, il valore di r<strong>it</strong>orno, anche se é un puntatore, viene<br />
trasmesso by value e quindi ne viene creata una copia nel programma<br />
chiamante; ciò garantisce che il puntatore sopravviva alla funzione anche se è<br />
stato creato all'interno del suo amb<strong>it</strong>o.<br />
Tuttavia la variabile puntata potrebbe non sopravvivere alla funzione (se é<br />
stata creata nel suo amb<strong>it</strong>o e non dichiarata static). Ciò porterebbe a un errore<br />
di dangling references. Notare l'analogia con il tipo di errore generato quando<br />
un valore di r<strong>it</strong>orno, trasmesso by reference, corrisponde a una variabile che<br />
cessa di esistere: in quel caso tuttavia, il compilatore ha il controllo della<br />
s<strong>it</strong>uazione e quindi può segnalare l'errore (o almeno un warning); nel caso di un<br />
puntatore, invece, il suo contenuto (cioè l'indirizzo della variabile puntata) é<br />
determinato in esecuzione e quindi l'errore non può essere segnalato dal<br />
compilatore. Spetta al programmatore fare la massima attenzione a che ciò non si<br />
verifichi.<br />
Il più frequente uso di funzioni che rest<strong>it</strong>uiscono puntatori si ha nel caso di<br />
puntatori a char, cioè di stringhe. Nella stessa libreria Run-time ci sono molte<br />
funzioni che rest<strong>it</strong>uiscono stringhe.<br />
Esempio di funzione di libreria:<br />
char* strcat(char* str1, char* str2);<br />
concatena la stringa str2 alla stringa str1 e rest<strong>it</strong>uisce il risultato sia nella<br />
stessa str1 che come valore di r<strong>it</strong>orno. Notare che in questo caso non c'è<br />
pericolo di errore, purchè lo spazio di memoria per str1 sia stato adeguatamente<br />
allocato nel programma chiamante.<br />
Puntatori a Funzione<br />
In <strong>C++</strong> (come in C) esistono anche i puntatori a funzione! Questi servono<br />
quando il programma deve scegliere quale funzione chiamare fra diverse<br />
possibili, e la scelta non é defin<strong>it</strong>a a priori ma dipende dai dati del programma<br />
stesso. Questo processo si chiama late binding ("aggancio r<strong>it</strong>ardato"): gli
indirizzi delle funzioni da chiamare non vengono risolti al momento della<br />
compilazione, come avviene normalmente (early binding) ma al momento<br />
dell'esecuzione.<br />
I puntatori a funzione non devono essere defin<strong>it</strong>i, ma solo dichiarati, come<br />
nel seguente esempio:<br />
int* (*pfunz)(double , char* );<br />
dichiara un puntatore a funzione pfunz che rest<strong>it</strong>uisce un puntatore a int e<br />
ha due argomenti: il primo é di tipo double, il secondo é un puntatore a<br />
char. Notare le parentesi intorno al nome della funzione, in assenza delle quali<br />
la dichiarazione sarebbe interpretata in modo diverso (una normale funzione<br />
pfunz che rest<strong>it</strong>uisce un puntatore a puntatore a int).<br />
Nel corso del programma il puntatore a funzione deve essere assegnato (o<br />
inizializzato) con il nome di una funzione "vera", che deve essere<br />
precedentemente dichiarata con lo stesso tipo del valore di r<strong>it</strong>orno e gli stessi<br />
argomenti del puntatore. Continuando l'esempio precedente:<br />
int* funz1(double , char* );<br />
int* funz2(double , char* );<br />
if ( ......... ) pfunz = funz1 ;<br />
else pfunz = funz2;<br />
notare che i nomi delle funzioni e del puntatore vanno indicati da soli, senza i<br />
loro argomenti (e senza le parentesi).<br />
In una chiamata della funzione, tutti i testi di C dicono che il puntatore va<br />
dereferenziato (in realtà non é necessario):<br />
(*pfunz)(12.3,"Ciao"); ... ma va bene anche:<br />
pfunz(12.3,"Ciao");<br />
Array di puntatori a funzione<br />
In <strong>C++</strong> (come in C) è consent<strong>it</strong>o dichiarare array di puntatori a funzione,<br />
nella forma specificata dal seguente esempio:<br />
double (*apfunz[5])(int);<br />
dichiara l'array apfunz di 5 puntatori a funzione, tutti con valore di r<strong>it</strong>orno<br />
di tipo double e con un argomento di tipo int.<br />
L'array può essere inizializzato con un elenco di nomi di funzioni, già<br />
dichiarate e condividenti tutte le stesso tipo di valore di r<strong>it</strong>orno e gli stessi<br />
argomenti:<br />
double (*apfunz[5])(int) = {f1, f2, f3, f4, f5} ;
dove f1 ecc... sono tutte funzioni dichiarate: double f1(int); ecc...<br />
I singoli elementi dell'array possono anche essere assegnati tram<strong>it</strong>e<br />
l'operatore [ ], che funziona come l-value nel modo consueto:<br />
apfunz[3]= fn;<br />
dove fn é una funzione dichiarata: double fn(int);<br />
Nelle chiamate, si usa ancora l'operatore [ ] per selezionare l'elemento<br />
desiderato:<br />
apfunz[i](n); (non é necessario dereferenziare il<br />
puntatore)<br />
dove l'indice i permette di accedere alla funzione precedentemente assegnata<br />
all'i-esimo elemento dell'array.<br />
Gli array di puntatori a funzione possono essere utili, per esempio, quando la<br />
funzione da eseguire é selezionata da un menù: in questo caso l'indice i ,<br />
corrispondente a una voce di menù, indirizza direttamente la funzione prescelta,<br />
senza bisogno di istruzioni di controllo, come if o sw<strong>it</strong>ch, per determinarla.<br />
Funzioni con argomenti puntatori a funzione<br />
E' noto che, quando nella chiamata di una funzione compare come argomento<br />
un'altra funzione, questa viene esegu<strong>it</strong>a per prima e il suo valore di r<strong>it</strong>orno é<br />
utilizzato come argomento dalla prima funzione. Quindi il vero argomento<br />
della prima funzione non é la seconda funzione, ma un normale valore, che<br />
può avere qualsiasi origine (variabile, espressione ecc...), e in particolare in<br />
questo caso è il risultato dell'esecuzione di un'altra funzione (il cui tipo di valore<br />
di r<strong>it</strong>orno deve coincidere con il tipo dichiarato dell'argomento).<br />
Quando invece una funzione dichiara fra i suoi argomenti un puntatore a<br />
funzione, allora sono parametrizzate proprio le funzioni e non i loro valori di<br />
r<strong>it</strong>orno. Nelle chiamate é necessario specificare come argomento il nome di<br />
una funzione "vera", precedentemente dichiarata, che viene sost<strong>it</strong>u<strong>it</strong>o a quello<br />
del puntatore.<br />
Es.: dichiarazioni: void fsel(int (*)(float));<br />
int funz1(float);<br />
int funz2(float);<br />
chiamate: fsel(funz1);<br />
fsel(funz2);<br />
definizione fsel: void fsel(int (*pfunz)(float))<br />
{ .... n = pfunz(r); .....}<br />
(dove n é di tipo int e r é di tipo float)
l'istruzione n=pfunz(r) viene sost<strong>it</strong>u<strong>it</strong>a la prima volta con n=funz1(r) e la<br />
seconda volta con n=funz2(r) . Notare che, nelle chiamate, l'argomentofunzione<br />
deve essere a sua volta specificato senza argomenti e senza le<br />
parentesi tonde.<br />
Nell'esempio abbiamo supposto che la variabile r, argomento della pfunz, sia<br />
creata all'interno della fsel; anche se r fosse passato dal programma<br />
chiamante, la forma: fsel(funz1(r)) sarebbe comunque errata: l'unico modo<br />
per passare r potrebbe essere quello di dichiararlo come ulteriore argomento<br />
della fsel, cioè:<br />
void fsel(float, int (*pfunz)(float)); e nelle chiamate<br />
specificare:<br />
fsel(r, funz1); ...oppure... fsel(r, funz2);
Puntatori e Costanti<br />
Puntatori a costante<br />
Nelle definizioni di una variabile puntatore, lo specificatore di tipo const<br />
indica che deve essere considerata costante la variabile puntata (non il<br />
puntatore!).<br />
Es.: const float* ptr;<br />
definisce il puntatore variabile ptr a costante float.<br />
In realtà a un puntatore a costante si può anche assegnare l'indirizzo di una<br />
variabile, ma non é vero il contrario: l'indirizzo di una costante può essere<br />
assegnato solo a un puntatore a costante. In altre parole il <strong>C++</strong> accetta<br />
conversioni da puntatore a variabile a puntatore a costante, ma non<br />
viceversa.<br />
L'operazione di deref. di un puntatore a costante non é mai accettata come lvalue,<br />
anche se la variabile puntata non é const.<br />
Es.: int datov=50; (datov é una variabile int)<br />
const int datoc=50; (datoc é una costante int)<br />
int* ptv; (ptv é un puntatore a variabile int)<br />
const int* ptc; (ptc é un puntatore a costante int)<br />
ptc = &datov; (valida, in quanto le conversioni da int* a const<br />
int* sono ammesse)<br />
ptv = &datoc; (non valida, in quanto le conversioni da const int* a<br />
int* non sono ammesse)<br />
*ptc = 10; (deref. l-value non valida, anche se ptc punta a<br />
una variabile)<br />
datov=10; cout
Es.: float* const ptr;<br />
definisce il puntatore costante ptr a variabile float<br />
Un puntatore costante segue la regola di tutte le costanti: deve essere<br />
inizializzato, ma non può più essere modificato (non é un l-value). Resta lvalue,<br />
invece, la deref. di un puntatore costante che punta a una variabile.<br />
Es.: int dato1,dato2; (dato1 e dato2 sono due variabili int)<br />
int* const ptr =<br />
&dato1;<br />
(ptr é un puntatore costante, inizializzato con<br />
l'indirizzo di dato1)<br />
*ptr = 10; (valida, in quanto ptr punta a una variabile)<br />
ptr = &dato2; (non valida, in quanto ptr é costante)<br />
Casi tipici di puntatori costanti sono gli array.<br />
Puntatori costanti a costante<br />
Ripetendo due volte const (come specificatore di tipo e come operatore di<br />
dichiarazione), si può definire un puntatore costante a costante (di uso<br />
piuttosto raro).<br />
Es.: const char dato='A'; (dato é una costante char, inizializzata con 'A')<br />
const char* const ptr =<br />
&dato;<br />
(ptr é un puntatore costante a costante ,<br />
inizializzato con l'indirizzo della costante<br />
dato)<br />
Nel caso di un puntatore costante a costante, non sono l-values né il<br />
puntatore né la sua deref.<br />
Funzioni con argomenti costanti trasmessi by value<br />
Le regole di ammissibil<strong>it</strong>à degli argomenti di una funzione, dichiarati const<br />
(nella funzione e/o nel programma chiamante) e trasmessi by value, sono
iconducibili alle regole generali applicate a una normale dichiarazione con<br />
inizializzazione; come é noto, infatti, la trasmissione by value comporta una<br />
creazione per copia, che equivale alla dichiarazione dell'argomento come<br />
variabile locale della funzione, inizializzata con il valore passato dal<br />
programma chiamante.<br />
Quindi un argomento può essere dichiarato const nel programma chiamante<br />
e non nella funzione o viceversa, senza lim<strong>it</strong>azioni (in quanto la creazione per<br />
copia "separa i destini" delle due variabili), salvo in un caso: un puntatore a<br />
costante non può essere dichiarato tale nel programma chiamante se non lo é<br />
anche nella funzione (in quanto non sono ammesse le conversioni da puntatore<br />
a costante a puntatore a variabile).<br />
Es.: void funz(int*); void main() { const int* ptr; ... funz(ptr); ... }<br />
(errore : l'argomento é dichiarato puntatore a costante nel main e<br />
puntatore a variabile nella funzione)<br />
void funz(const int*); void main() {int*ptr; ... funz(ptr); ... }<br />
( ok! )<br />
Da quest'ultimo esempio si capisce anche qual'é l'uso principale di un puntatore<br />
a costante: come argomento passato a una funzione, se non si desidera che<br />
la variabile puntata subisca modifiche dall'interno della funzione stessa<br />
(tram<strong>it</strong>e operazioni di deref.), anche se ciò é possibile nel programma<br />
chiamante.<br />
Funzioni con argomenti costanti trasmessi by reference<br />
Sappiamo che, se un argomento é passato a una funzione by reference, non<br />
ne viene costru<strong>it</strong>a una copia, ma il suo nome nella funzione é assunto come<br />
alias del nome corrispondente nel programma chiamante (cioè i due nomi si<br />
riferiscono alla stessa locazione di memoria). Per questo motivo il controllo sui tipi<br />
e più rigoroso rispetto al caso di passaggio by value: in particolare, qualsiasi sia<br />
l'argomento (puntatore o no), non é ammesso dichiararlo const nel<br />
programma chiamante e non nella funzione.<br />
La dichiarazione inversa (const solo nella funzione) é invece possibile, in quanto<br />
corrisponde alla definizione di un alias di sola lettura: l'argomento, pur<br />
essendo modificabile nel programma chiamante, non lo é dall'interno della<br />
funzione.<br />
Il passaggio by reference di argomenti dichiarati const nella funzione é in<br />
uso molto frequente in <strong>C++</strong>, perché combina insieme due vantaggi: quello di
proteggere i dati del programma da modifiche indesiderate (come nel passaggio<br />
by value), e quello di una migliore efficienza; infatti il passaggio by reference,<br />
non comportando la creazione di nuove variabili, é più veloce del passaggio by<br />
value.<br />
Quando un argomento é passato by reference ed é dichiarato const nella<br />
funzione, non esiste più la condizione che nel programma chiamante il<br />
corrispondente argomento sia un l-value (può anche essere il risultato di<br />
un'espressione).<br />
Se si vuole dichiarare un argomento: puntatore costante passato by<br />
reference, bisogna specificare entrambi gli operatori di dichiarazione *const<br />
e & (nell'ordine)<br />
Es.: funz(int* const & ptr);<br />
dichiara che l'argomento ptr é un puntatore costante a variabile int,<br />
passato by reference
Tipi defin<strong>it</strong>i dall'utente<br />
Il termine "tipo astratto", usato in contrapposizione ai tipi nativi del linguaggio, non é molto<br />
appropriato: il <strong>C++</strong> consente al programmatore di definire nuovi tipi, estendendo così le<br />
capac<strong>it</strong>à effettive del linguaggio; ma, una volta defin<strong>it</strong>i, questi tipi sono molto "concreti" e sono<br />
trattati esattamente come i tipi nativi. Per questo motivo, la tendenza "moderna" è di<br />
identificare i tipi non nativi con il termine: "tipi defin<strong>it</strong>i dall'utente" e di confinare<br />
l'aggettivo "astratto" a una precisa sottocategoria di questi (di cui parleremo più avanti).<br />
Tuttavia noi continueremo, per comod<strong>it</strong>à, a usare la "vecchia" terminologia.<br />
In questo cap<strong>it</strong>olo parleremo dei tipi astratti comuni sia al C che al <strong>C++</strong>, usando però la<br />
nomenclatura (oggetti, istanze ecc...) del <strong>C++</strong>.<br />
Concetti di oggetto e istanza<br />
Il termine oggetto é sostanzialmente sinonimo del termine variabile. Benché<br />
questo termine si usi soprattutto in relazione a tipi astratti (come strutture o<br />
classi), noi possiamo generalizzare il concetto, definendo oggetto una variabile<br />
di qualunque tipo, non solo formalmente defin<strong>it</strong>a, ma anche già creata e<br />
operante.<br />
E' noto infatti che l'istruzione di definizione di una variabile non si lim<strong>it</strong>a a<br />
dichiarare il suo tipo, ma crea fisicamente la variabile stessa, allocando la<br />
memoria necessaria (nella terminologia <strong>C++</strong> si dice che la variabile viene<br />
"costru<strong>it</strong>a"): pertanto la definizione di una variabile comporta la "costruzione"<br />
di un oggetto.<br />
Il termine istanza é quasi simile al termine oggetto; se ne differenzia in quanto<br />
sottolinea l'appartenenza dell'oggetto a un dato tipo (istanza di ... "qualcosa").<br />
Per esempio, la dichiarazione/definizione:<br />
int ivar ;<br />
costruisce l'oggetto ivar, istanza del tipo int.<br />
Esiste anche il verbo: istanziare (o instanziare) un certo tipo, che significa<br />
creare un'istanza di quel tipo.<br />
Typedef
L'istruzione introdotta dalla parola-chiave typedef definisce un sinonimo di un<br />
tipo esistente, cioè non crea un nuovo tipo, ma un nuovo identificatore di un<br />
tipo (nativo o astratto) precedentemente defin<strong>it</strong>o.<br />
Es.: typedef unsigned long int* pul ;<br />
definisce il nuovo identificatore di tipo pul, che potrà essere usato, nelle<br />
successive dichiarazioni (all'interno dello stesso amb<strong>it</strong>o), per costruire<br />
oggetti di tipo puntatore a unsigned long:<br />
unsigned long a; pul ogg1 = &a; pul parray[100]; ecc...<br />
L'uso di typedef permette di semplificare dichiarazioni lunghe di variabili dello<br />
stesso tipo. Per esempio, supponiamo di dover dichiarare molti array, tutti dello<br />
stesso tipo e della stessa dimensione:<br />
double a1[100]; double a2[100]; double a3[100]; ecc...<br />
usando typedef la semplificazione é evidente:<br />
typedef double a[100]; a a1; a a2; a a3; ecc...<br />
Un caso in cui si evidenzia in modo eclatante l'util<strong>it</strong>à di typedef è quello in cui si<br />
devono dichiarare più funzioni con lo stesso puntatore a funzione come<br />
argomento.<br />
Es.: typedef bool (*tpfunz)(const int&, int&, const char*, int&, char*&,<br />
int&);<br />
in questo caso tpfunz è il nome di un tipo puntatore a funzione e può essere<br />
sost<strong>it</strong>u<strong>it</strong>o nelle dichiarazioni delle funzioni chiamanti al posto dell'intera<br />
stringa di cui sopra:<br />
void fsel1(tpfunz); int fsel2(tpfunz); double fsel3(tpfunz); ecc....<br />
infine, nelle definizioni delle funzioni chiamanti bisogna specificare un<br />
argomento di "tipo" tpfunz e usare questo per le chiamate. Es:<br />
void fsel1(tpfunz pfunz) { ... if(pfunz(4,a,"Ciao",b,pc,m)) .... }<br />
Un altro utilizzo di typedef è quello di confinare in unico luogo i riferimenti diretti<br />
a un tipo. Per esempio, se il programma lavora in una macchina in cui il tipo int<br />
corrisponde a 32 b<strong>it</strong> e noi poniamo:<br />
typedef int int32;<br />
avendo cura poi di attribuire il tipo int32 a tutte le variabili intere che vogliamo a<br />
32 b<strong>it</strong>, possiamo portare il programma su una macchina a 16 b<strong>it</strong> ridefinendo<br />
solamente int32 :<br />
typedef long int32;
Strutture<br />
Come gli array, in <strong>C++</strong> (e in C) le strutture sono gruppi di dati; a differenza<br />
dagli array, i singoli componenti di una struttura possono essere di tipo<br />
diverso.<br />
Esempio di definizione di una struttura:<br />
struct anagrafico<br />
{<br />
} ;<br />
char nome[20];<br />
int anni;<br />
char indirizzo[30];<br />
Dopo la parola-chiave struct segue l'identificatore della struttura, detto<br />
anche marcatore o tag, e, fra parentesi graffe, l'elenco dei componenti della<br />
struttura, detti membri; ogni membro é dichiarato come una normale<br />
variabile (è una semplice dichiarazione, non una definizione, e pertanto non<br />
comporta la creazione dell'oggetto corrispondente) e può essere di qualunque<br />
tipo (anche array o puntatore o una stessa struttura). Dopo la parentesi<br />
graffa di chiusura, è obbligatoria la presenza del punto e virgola (diversamente dai<br />
blocchi delle funzioni).<br />
In <strong>C++</strong> (e non in C) la definizione di una struttura comporta la creazione di<br />
un nuovo tipo, il cui nome coincide con il tag della struttura. Pertanto,<br />
riprendendo l'esempio, anagrafico è a pieno t<strong>it</strong>olo un tipo (come int o double),<br />
con la sola differenza che si tratta di un tipo astratto, non nativo del<br />
linguaggio.<br />
Per questo motivo l'enunciato di una struttura è una definizione e non una<br />
semplice dichiarazione: crea un'ent<strong>it</strong>à (il nuovo tipo) e ne descrive il<br />
contenuto. Ma, diversamente dalle definizioni delle variabili, non alloca memoria,<br />
cioè non crea oggetti. Perchè ciò avvenga, il nuovo tipo deve essere<br />
istanziato, esattamente come succede per i tipi nativi. Riprendendo l'esempio,<br />
l'istruzione di definizione:<br />
anagrafico ana1, ana2, ana3 ;<br />
costruisce gli oggetti ana1, ana2 e ana3, istanze del tipo anagrafico. Solo<br />
adesso viene allocata memoria, per ogni oggetto in quant<strong>it</strong>à pari alla somma<br />
delle memorie che competono ai singoli membri della struttura (l'operazione<br />
sizeof(anagrafico), oppure sizeof(ana1) ecc..., rest<strong>it</strong>uisce il numero dei bytes<br />
allocati ad ogni istanza di anagrafico).<br />
La collocazione ideale della definizione di una struttura é in un header-file:<br />
conviene infatti separarla dalle sue istanze, in quanto la definizione deve essere<br />
(di sol<strong>it</strong>o) accessibile dappertutto, mentre le istanze sono normalmente locali e
quindi lim<strong>it</strong>ate dal loro amb<strong>it</strong>o di visibil<strong>it</strong>à. Potrebbe però sorgere un problema:<br />
se un programma è suddiviso in più files sorgente e tutti includono lo stesso<br />
header-file contenente la definizione di una struttura, dopo l'azione del<br />
preprocessore risulteranno diverse translation un<strong>it</strong> con la stessa definizione<br />
e quindi sembrerebbe violata la "regola della definizione unica" (o ODR,<br />
dall'inglese one-defin<strong>it</strong>ion-rule). In realtà, per la definizione dei tipi astratti<br />
(e di altre ent<strong>it</strong>à del linguaggio, come i template, che vedremo più avanti), la<br />
ODR si esprime in modo meno restr<strong>it</strong>tivo rispetto al caso della definizione di<br />
variabili e funzioni (non inline): in questi casi, due definizioni sono ancora<br />
r<strong>it</strong>enute esemplari della stessa, unica, definizione, se e solo se:<br />
1. appaiono in differenti translation un<strong>it</strong>s ,<br />
2. sono identiche nei rispettivi elementi lessicali,<br />
3. il significato dei rispettivi elementi lessicali è lo stesso in entrambe le<br />
translation un<strong>it</strong>s<br />
e tali condizioni sono senz'altro verificate se due files sorgente includono lo<br />
stesso header-file (purchè in uno dei due non si alteri il significato dei nomi con<br />
typedef o #define !).<br />
Operatore .<br />
La grande util<strong>it</strong>à delle strutture consiste nel fatto che i nomi delle sue istanze<br />
possono essere usati direttamente come operandi in molte operazioni o come<br />
argomenti nelle chiamate di funzioni, consentendo un notevole risparmio,<br />
soprattutto quando il numero di membri é elevato.<br />
In alcune operazioni, tuttavia, é necessario accedere a un membro<br />
individualmente. Ciò é possibile grazie all'operatore binario . di accesso al<br />
singolo membro: questo operatore ha come left-operand il nome<br />
dell'oggetto e come right-operand quello del membro.<br />
Es.: ana2.indirizzo<br />
Come altri operatori che svolgono comp<strong>it</strong>i analoghi (per esempio l'operatore [ ]<br />
di accesso al singolo elemento di un array), anche l'operatore . può<br />
rest<strong>it</strong>uire sia un r-value (lettura di un dato) che un l-value (inserimento di un<br />
dato).<br />
Es.: int a = ana1.anni; inizializza a con il valore del membro anni dell'oggetto<br />
ana1<br />
ana3.anni = 27; inserisce 27 nel membro anni dell'oggetto ana3
Puntatori a strutture - Operatore -><br />
Come tutti i tipi del <strong>C++</strong> (e del C), anche i tipi astratti, e in particolare le<br />
strutture, hanno i propri puntatori. Per esempio (notare le differenze):<br />
int* p_anni = &ana1.anni;<br />
anagrafico* p_anag = &ana1;<br />
nel primo caso definisce un normale puntatore a int, che inizializza con<br />
l'indirizzo del membro anni dell'oggetto ana1; nel secondo caso definisce<br />
un puntatore al tipo-struttura anagrafico, che inizializza con l'indirizzo<br />
dell'oggetto ana1.<br />
Per accedere a un membro di un oggetto (istanza di una struttura) di cui é<br />
dato il puntatore, bisogna eseguire un'operazione di deref. . Riprendendo<br />
l'esempio precedente, si potrebbe pensare che la forma corretta dell'operazione<br />
sia:<br />
*p_anag.anni<br />
e invece non lo é, in quanto l'operatore . ha la precedenza sull'operatore di<br />
deref. e quindi il compilatore darebbe messaggio di errore, interpretando<br />
p_anag.anni come un indirizzo da dereferenziare (l'interpretazione sarebbe<br />
giusta se esistesse un oggetto di nome p_anag con un membro di nome anni<br />
defin<strong>it</strong>o puntatore a int, e invece esiste un puntatore di nome p_anag a un<br />
oggetto con un membro di nome anni defin<strong>it</strong>o int).<br />
Perché il risultato sia corretto bisognerebbe inserire la deref. del puntatore fra<br />
parentesi, cioè:<br />
(*p_anag).anni<br />
il <strong>C++</strong> (come il C) consente di ev<strong>it</strong>are questa "fatica" mettendo a disposizione un<br />
altro operatore, che rest<strong>it</strong>uisce un identico risultato:<br />
p_anag->anni<br />
In generale l'operatore -> permette di accedere a un membro (indicato dal<br />
right-operand) di un oggetto, istanza di una struttura, il cui indirizzo é<br />
dato nel left-operand (ovviamente anche questo operatore può rest<strong>it</strong>uire sia un<br />
r-value che un l-value).<br />
Unioni
Le unioni sono identiche alle strutture (sono introdotte dalla parola-chiave<br />
union al posto di struct), eccetto nel fatto che i membri di ogni loro istanza<br />
occupano la stessa area di memoria.<br />
In pratica un'unione consente di utilizzare un solo membro per ogni oggetto<br />
(anche se i membri defin<strong>it</strong>i sono più d'uno) e servono quando può essere<br />
comodo selezionare ogni volta il membro più appropriato, in base alle necess<strong>it</strong>à.<br />
L'occupazione di memoria di un'unione coincide con quella del membro di<br />
dimensioni maggiori.<br />
Array di strutture<br />
Abbiamo visto negli esempi che i membri di una struttura possono essere<br />
array. Anche le istanze di una struttura possono essere array.<br />
Es.:<br />
definizione:<br />
costruzione oggetti:<br />
struct tipo_stud { char nome[20]; int voto[50];}<br />
;<br />
tipo_stud studente[40];<br />
accesso: studente[5].voto[10] = 30;<br />
(lo studente n.5 ha preso 30 nella prova n.10 !)<br />
Dichiarazione di strutture e membri di tipo struttura<br />
I membri di una struttura possono essere a loro volta di tipo struttura. Esiste<br />
però il problema di fare riconoscere tale struttura al compilatore. Le soluzione<br />
più semplice è definire la struttura a cui appartiene il membro prima della<br />
struttura che contiene il membro (così il compilatore é in grado di riconoscerne<br />
il tipo). Tuttavia cap<strong>it</strong>a non di rado che la stessa struttura a cui appartiene il<br />
membro contenga informazioni che la collegano alla struttura principale: in<br />
questi casi viene a determinarsi la cosidetta "dipendenza circolare",<br />
apparentemente senza soluzione.<br />
In realtà il <strong>C++</strong> offre una soluzione semplicissima: dichiarare la struttura<br />
prima di definirla! La dichiarazione di una struttura consiste in una istruzione
in cui appaiono esclusivamente la parola-chiave struct e l'identificatore della<br />
struttura.<br />
Es.: struct data ;<br />
chiaramente si tratta di una dichiarazione-non-definizione (questo è il terzo<br />
caso che incontriamo, dopo le dichiarazioni di variabili con le specificatore<br />
extern e le dichiarazioni di funzioni), nel senso che non rende ancora la<br />
struttura utilizzabile, ma è sufficiente affinchè il compilatore accetti data come<br />
tipo di una struttura defin<strong>it</strong>a successivamente.<br />
Allora il problema è risolto ? No ! Perchè no ? Perchè il compilatore ha un'altra<br />
esigenza oltre quella di riconoscere i tipi: deve essere anche in grado di calcolare<br />
le dimensioni di una struttura e non lo può fare se questa contiene membri di<br />
strutture non defin<strong>it</strong>e. Solo nel caso che i membri in questione siano<br />
puntatori questo problema non sussiste, in quanto le dimensioni di un<br />
puntatore sono fisse e indipendenti dal tipo della variabile puntata.<br />
Pertanto, la dipendenza circolare fra membri di strutture diverse può essere<br />
spezzata solo se almeno in una struttura i membri coinvolti sono puntatori.<br />
Per esempio, una sequenza corretta potrebbe essere:<br />
struct data ; dichiarazione anticipata della struttura data<br />
struct persona { char<br />
nome[20]; data* pnasc<strong>it</strong>a;} ;<br />
struct data { int giorno; int<br />
mese; int anno; persona<br />
caio; } ;<br />
definizione della struttura principale persona<br />
con un membro puntatore a data<br />
definizione della struttura data con un membro<br />
di tipo persona<br />
in questo modo il membro pnasc<strong>it</strong>a della struttura persona è riconosciuto<br />
come puntatore al tipo data prima ancora che la struttura data sia defin<strong>it</strong>a.<br />
Con lo stesso ragionamento si può dimostrare che è possibile dichiarare dei<br />
membri di una struttura come puntatori alla struttura stessa (per esempio,<br />
quando si devono costruire delle liste concatenate). In questo caso, poi, la<br />
dichiarazione anticipata non serve in quanto il compilatore conosce già il nome<br />
della struttura che appare all'inizio della sua definizione.<br />
Nota:<br />
La dipendenza circolare si può avere anche fra le funzioni (una funzione A<br />
che chiama una funzione B che chiama una funzione C che a sua volta<br />
chiama la funzione A). Ma in questi casi le dichiarazioni contengono già tutte<br />
le informazioni necessarie e quindi il problema si risolve semplicemente<br />
dichiarando A prima di definire (nell'ordine) C, B e la stessa A.<br />
Per accedere a un membro di una la struttura al cui tipo appartiene il<br />
membro di un certo oggetto, é necessario ripetere due volte l'operazione con<br />
l'operatore . (e/o con l'operatore -> se il membro è un puntatore).<br />
Segu<strong>it</strong>ando con lo stesso esempio :
costruzione oggetto:<br />
persona tizio; (da qualche altra parte bisogna anche<br />
creare un oggetto di tipo data e assegnare il suo<br />
indirizzo a tizio.pnasc<strong>it</strong>a)<br />
accesso: tizio.pnasc<strong>it</strong>a->anno = 1957;<br />
come si può notare dall'esempio, il numero 1957 é stato inser<strong>it</strong>o nel membro<br />
anno dell'oggetto il cui indirizzo si trova nel membro puntatore pnasc<strong>it</strong>a<br />
dell'istanza tizio della struttura persona.<br />
Strutture di tipo b<strong>it</strong> field<br />
Le strutture di tipo b<strong>it</strong> field permettono di riservare ad ogni membro un<br />
determinato numero di b<strong>it</strong> di memoria, consentendo notevoli risparmi; il tipo di<br />
ogni membro deve essere unsigned int.<br />
Es.: struct b<strong>it</strong> { unsigned int ma:2; unsigned int mb:1; } ;<br />
la presenza dei due punti, segu<strong>it</strong>a dal numero di b<strong>it</strong> riservati, identifica la<br />
definizione di una struttura di tipo b<strong>it</strong> field.<br />
Tipi enumerati<br />
Con la parola-chiave enum si definiscono i tipi enumerati, le cui istanze<br />
possono assumere solo i valori specificati in un elenco.<br />
Es.: enum feriale { Lun, Mar, Mer, Gio, Ven } ;<br />
dove: feriale è il nome del tipo enumerato e le costanti fra parentesi graffe<br />
sono i valori possibili (detti enumeratori).<br />
In realtà agli enumeratori sono assegnati numeri interi, a partire da 0 e con<br />
incrementi di 1, come se si usassero le direttive:<br />
#define Lun 0 #define Mar 1 ecc...<br />
Volendo assegnare numeri diversi (comunque sempre interi), bisogna specificarlo.<br />
Es.: enum dati { primo, secondo=12, terzo } ;
in questo caso alla costante primo è assegnato 0, a secondo è assegnato 12 e<br />
a terzo è assegnato 13. Comunque l'uso degli enumeratori, anzichè quello<br />
diretto delle costanti numeriche corrispondenti, è utile in quanto permette di<br />
scrivere codice più chiaro ed più esplicativo di ciò che si vuole fare.<br />
Analogamente al tag di una struttura, il nome di un tipo enumerato é<br />
assunto, in <strong>C++</strong> come un nuovo tipo del linguaggio.<br />
Es.: feriale oggi = Mar ;<br />
costruisce l'oggetto oggi, istanza del tipo enumerato feriale e lo<br />
inizializza con il valore dell'enumeratore Mar.<br />
Un oggetto di tipo enumerato può assumere valori anche diversi da quelli<br />
specificati nella definizione. L'intervallo di valid<strong>it</strong>à (detto dominio) di un tipo<br />
enumerato contiene tutti i valori dei propri enumeratori arrotondati alla<br />
minima potenza di 2 maggiore o uguale al massimo enumeratore meno 1. Il<br />
dominio comincia da 0 se il minimio enumeratore non è negativo; altrimenti è<br />
il valore maggiore tra le potenze di due negative minori o uguali del minimo<br />
enumeratore (si uguagliano poi minimo e massimo scegliendo il più grande in<br />
valore assoluto). In ogni caso il dominio non può superare il range del tipo int.<br />
Es.: enum en1 { bello, brutto } ; dominio 0:1<br />
enum en2 { a=3, b=10 } ; dominio 0:15<br />
enum en3 { a=-38, b=850 } ; dominio -1024:1023<br />
come si può notare, il numero complessivo degli enumeratori possibili è sempre<br />
una potenza di 2.<br />
Per inizializzare un oggetto di tipo enumerato con un valore intero (anche<br />
diverso dalle costanti incluse nella definizione, purchè compreso nel dominio)<br />
è obbligatorio il casting.<br />
Es.:<br />
en2 oggetto1 = (en2)14 ;<br />
en2 oggetto2 = (en2)20 ;<br />
OK, 14 è compreso nel dominio<br />
risultato indefin<strong>it</strong>o, 20 non è compreso nel<br />
dominio<br />
en2 oggetto3 = 3 ; errore: conversione implic<strong>it</strong>a non ammessa<br />
Gli enumeratori sono ammessi nelle operazioni fra numeri interi e, in questi<br />
casi, sono conver<strong>it</strong><strong>it</strong>i implic<strong>it</strong>amente in int.
Allocazione dinamica della memoria<br />
Memoria stack e memoria heap<br />
Abbiamo già sent<strong>it</strong>o parlare dell'area di memoria stack: é quella in cui viene<br />
allocato un pacchetto di dati non appena l'esecuzione passa dal programma<br />
chiamante a una funzione. Abbiamo detto che questo pacchetto (il quale<br />
contiene l'indirizzo di rientro nel programma chiamante, la lista degli<br />
argomenti passati alla funzione e tutte le variabili automatiche defin<strong>it</strong>e nella<br />
funzione) viene "impilato" sopra il pacchetto precedente (quello del<br />
programma chiamante) e poi automaticamente rimosso dalla memoria appena<br />
l'esecuzione della funzione é terminata.<br />
Sappiamo anche che, grazie a questo meccanismo, le funzioni possono essere<br />
chiamate ricorsivamente e inoltre si possono gestire funzioni con numero<br />
variabile di argomenti. Le variabili automatiche defin<strong>it</strong>e nella funzione hanno<br />
lifetime lim<strong>it</strong>ato all'esecuzione della funzione stessa proprio perché, quando la<br />
funzione termina, il corrispondente pacchetto allocato nell'area stack viene<br />
rimosso.<br />
Un'altra area di memoria è quella in cui vengono allocate le variabili non locali e<br />
le variabili locali statiche. A differenza dalla precedente, quest'area viene<br />
mantenuta in v<strong>it</strong>a fino alla fine del programma, anche se ogni variabile è visibile<br />
solo all'interno del proprio amb<strong>it</strong>o.<br />
Esiste una terza area di memoria che il programma può utilizzare. Questa area,<br />
detta heap, è soggetta a regole di visibil<strong>it</strong>à e tempo di v<strong>it</strong>a diverse da quelle<br />
che governano le due aree precedenti, e precisamente:<br />
• l'area heap non é allocata automaticamente, ma può essere allocata o<br />
rimossa solo su esplic<strong>it</strong>a richiesta del programma (allocazione dinamica<br />
della memoria);<br />
• l'area allocata non é identificata da un nome, ma é accessibile<br />
esclusivamente tram<strong>it</strong>e deref. di un puntatore;<br />
• il suo scope coincide con quello del puntatore che contiene il suo<br />
indirizzo;<br />
• il suo lifetime coincide con l'intera durata del programma, a meno che non<br />
venga esplic<strong>it</strong>amente deallocata; se il puntatore va out of scope, l'area<br />
non é più accessibile, ma continua a occupare memoria inutilmente: si<br />
verifica l'errore di memory leak, opposto a quello di dangling<br />
references.<br />
Operatore new
In <strong>C++</strong>, l'operatore new costruisce uno o più oggetti nell'area heap e ne<br />
rest<strong>it</strong>uisce l'indirizzo. In caso di errore (memoria non disponibile) rest<strong>it</strong>uisce<br />
NULL.<br />
Gli operandi di new (tutti alla sua destra) sono tre, di cui solo il primo é<br />
obbligatorio (le parentesi quadre nere racchiudono gli operandi opzionali):<br />
new tipo [[dimensione]] [(valore iniziale)]<br />
• tipo é il tipo (anche astratto) dell'oggetto (o degli oggetti) da creare;<br />
• dimensione é il numero degli oggetti, che vengono sistemati nella<br />
memoria heap consecutivamente (come gli elementi di un array); se<br />
questo operando é omesso, viene costru<strong>it</strong>o un solo oggetto; se é<br />
presente, l'indirizzo rest<strong>it</strong>u<strong>it</strong>o da new punta al primo oggetto;<br />
• valore iniziale é il valore con cui l'area allocata viene inizializzata (deve<br />
essere dello stesso tipo di tipo); se é omesso l'area non é inizializzata.<br />
NOTA: si è potuto riscontrare che a volte i due operandi opzionali sono<br />
mutuamente incompatibili (alcuni compilatori più antichi danno errore): in pratica<br />
(vedremo perchè parlando dei costruttori), se il tipo è nativo inizializza<br />
comunque tutti i valori con zero, se il tipo è astratto funziona bene (a certe<br />
condizioni).<br />
Ovviamente l'operatore new non può rest<strong>it</strong>uire un l-value; può essere invece<br />
un r-value sia nelle inizializzazioni che nelle assegnazioni, e può far parte di<br />
operazioni di ar<strong>it</strong>metica fra puntatori . Esempi:<br />
inizializzazione: int* punt = new int (7);<br />
assegnazione con operazione ar<strong>it</strong>metica:<br />
struct anagrafico { ....... } ;<br />
anagrafico* p_anag ;<br />
p_anag = new anagrafico [100] + 9 ;<br />
nel primo esempio alloca un oggetto int (inizializzato con il valore 7) nell'area<br />
heap e usa il suo indirizzo per inizializzare il puntatore punt; nel secondo<br />
esempio definisce la struttura anagrafico e definisce un puntatore a tale<br />
struttura, a cui assegna l'indirizzo del decimo di cento oggetti di tipo<br />
anagrafico, allocati nell'area heap.<br />
Operatore delete<br />
In <strong>C++</strong>, l'operatore binario delete (con un operando opzionale e l'altro<br />
obbligatorio) dealloca la memoria dell'area heap puntata dall'operando<br />
(obbligatorio). Non rest<strong>it</strong>uisce alcun valore e quindi deve essere usato da solo in<br />
un'istruzione (non essendo né un l-value né un r-value non può essere usato in<br />
un'espressione con altre operazioni).
Es.: allocazione: int* punt = new int ;<br />
deallocazione: delete punt ;<br />
Contrariamente all'apparenza l'operatore delete non cancella il puntatore né<br />
altera il suo contenuto: l'unico effetto é di liberare la memoria puntata rendendola<br />
disponibile per ulteriori allocazioni (se l'operando non punta a un'area heap<br />
alcuni compilatori generano un messaggio di errore (o di warning), altri no, ma<br />
in ogni caso l'operatore delete non ha effetto).<br />
Se l'operando punta a un'area in cui è stato allocato un array di oggetti,<br />
bisogna inserire dopo delete l'operando opzionale, che consiste in una coppia<br />
di parentesi quadre (senza la dimensione dell'array, che il <strong>C++</strong> é in grado di<br />
riconoscere automaticamente).<br />
Es.: float* punt = new float [100] ; (alloca 100 oggetti float )<br />
delete [ ] punt ; (libera tutta la memoria allocata)<br />
L'operatore delete cost<strong>it</strong>uisce l'unico mezzo per deallocare memoria heap,<br />
che, altrimenti, sopravvive fino alla fine del programma, anche quando non é più<br />
raggiungibile.<br />
Es.: int* punt = new int ;<br />
(alloca un oggetto int nell'area heap e inizializza<br />
punt con il suo indirizzo)<br />
int a ; (definisce un oggetto int nell'area stack)<br />
punt = &a ; (assegna a punt un indirizzo dell'area stack;<br />
l'oggetto int dell'area heap non é più raggiungibile)
Namespace<br />
Programmazione modulare e compilazione separata<br />
Nel corso degli anni, l'enfasi nella progettazione dei programmi si è spostata dal<br />
progetto delle procedure all'organizzazione dei dati, in ragione anche dei<br />
problemi di sviluppo e manutenzione del software che sono direttamente correlati<br />
all'aumento di dimensione dei programmi. La possibil<strong>it</strong>à di suddividere grossi<br />
programmi in porzioni il più possibile ridotte e autosufficienti (detti moduli) è<br />
pertanto caratteristica di un modo efficiente di produrre software, in quanto<br />
permette di sviluppare programmi più chiari e più facili da mantenere ed<br />
aggiornare (specie se i programmatori che lavorano a un stesso progetto sono<br />
molti).<br />
Un modulo è cost<strong>it</strong>u<strong>it</strong>o da dati logicamente correlati e dalle procedure che li<br />
utilizzano. L'idea-base è quella del "data hiding" (occultamento dei dati), in<br />
ragione della quale un programmatore "utente" del modulo non ha bisogno di<br />
conoscere i nomi delle variabili, dei tipi, delle funzioni e in generale delle<br />
caratteristiche di implementazione del modulo stesso, ma è sufficiente che sappia<br />
come utilizzarlo, cioè come mandargli le informazioni e ottenere le risposte. Un<br />
modulo è pertanto paragonabile a un dispos<strong>it</strong>ivo (il cui meccanismo interno è<br />
sconosciuto), con il quale comunicare attraverso operazioni di input-output. Tali<br />
operazioni sono a loro volta raggruppate in un modulo separato, detto<br />
interfaccia che rappresenta l'unico canale di comunicazione fra il modulo e i<br />
suoi utenti.<br />
La programmazione modulare offre così un duplice vantaggio: quello di<br />
separare l'interfaccia dal codice di implementazione del modulo, dando la<br />
possibil<strong>it</strong>à al modulo di essere modificato senza che il codice dell'utente ne sia<br />
influenzato; e quello di permettere all'utente di definire i nomi delle variabili, dei<br />
tipi, delle funzioni ecc.. senza doversi preoccupare di eventuali confl<strong>it</strong>ti con i<br />
nomi usati nel modulo e dell'insorgere di errori dovuti a simboli duplicati.<br />
Parallelo al concetto di programmazione modulare è quello di compilazione<br />
separata. Per motivi di efficienza la progettazione di un programma (specie se di<br />
grosse dimensioni) dovrebbe prevedere la sistemazione dei moduli in files<br />
separati: in questo modo ogni intervento di modifica o di correzione degli errori di<br />
un singolo modulo comporterebbe la ricompilazione di un solo file. E' utile che<br />
anche l'interfaccia di un modulo risieda in un file separato sia dal codice<br />
dell'utente che da quello di implementazione del modulo stesso. Entrambi questi<br />
files dovrebbero poi contenere la direttiva #include (file dell'interfaccia) così<br />
che il preprocessore possa creare due translation un<strong>it</strong>s indipendenti, ma<br />
collegate entrambe alla stessa interfaccia (questo approccio è molto più<br />
conveniente di quello di creare due soli files entrambi con il codice<br />
dell'interfaccia, in quanto permette al progettista del modulo di modificare<br />
l'interfaccia senza implicare che la stessa modifica venga esegu<strong>it</strong>a anche nel file<br />
dell'utente).
Definizione di namespace<br />
Dal punto di vista sintattico, la definizione di un namespace somiglia molto a<br />
quella di una struttura (cambia la parola-chiave e inoltre il punto e virgola in<br />
fondo non è obbligatorio). Esempio:<br />
namespace Stack<br />
{<br />
}<br />
const int max_size = 100;<br />
char v[max_size ];<br />
int top = 0;<br />
void push(char c) {......}<br />
char pop( ) {......}<br />
I membri di un namespace sono dichiarazioni o definizioni (con eventuali<br />
inizializzazioni) di identificatori di qualunque genere (variabili, funzioni,<br />
typedef, strutture, enumeratori, altri tipi astratti qualsiasi ecc...). Anche il<br />
nome di un namespace (Stack, nell'esempio) è un identificatore. Pertanto<br />
definire un namespace significa dichiarare/definire un gruppo di nomi a sua<br />
volta identificato da un nome.<br />
A differenza dalle strutture, Stack non è un tipo (non può essere istanziato da<br />
oggetti) ma identifica semplicemente un amb<strong>it</strong>o di visibil<strong>it</strong>à (scope). I<br />
membri di Stack sono perciò identificatori locali, visibili soltanto nello scope<br />
defin<strong>it</strong>o da Stack. Il programmatore è perciò libero di definire gli stessi nomi al<br />
di fuori, senza pericolo di confl<strong>it</strong>ti o ambigu<strong>it</strong>à.<br />
Non è ammesso definire un namespace all'interno di un altro scope (per<br />
esempio nel block scope di una funzione o una struttura); e quindi il suo<br />
nome ha global scope cioè è riconoscibile dappertutto. E' però possibile<br />
"annidare" un namespace all'interno di un altro namespace: in questo caso il<br />
suo scope coincide con quello degli altri membri del namespace superiore.<br />
In defin<strong>it</strong>iva, il termine namespace si identifica con quello di "amb<strong>it</strong>o<br />
dichiarativo con un nome". In questo senso, anche i blocchi delle funzioni e<br />
delle strutture sono dei namespace (con molte funzional<strong>it</strong>à in più) e tutto ciò<br />
che è al di fuori (le variabili globali) è detto appartenere al "namespace<br />
globale".<br />
Risoluzione della visibil<strong>it</strong>à
Sorge a questo punto spontanea una domanda: come comunicare fra i<br />
namespace? In altre parole, se i membri di un namespace non sono accessibili<br />
dall'esterno, come si possono usare nel programma ?<br />
Per accedere a un nome defin<strong>it</strong>o in un namespace, bisogna "qualificarlo",<br />
associandogli il nome del namespace (che invece è visibile, avendo global<br />
scope), tram<strong>it</strong>e l'operatore binario di risoluzione di visibil<strong>it</strong>à :: (doppi due<br />
punti).<br />
Segu<strong>it</strong>ando nell'esempio precedente:<br />
Stack::top (accede al membro top del namespace Stack)<br />
Notare l'analogia di questo operatore con quello unario di riferimento globale<br />
(già visto a propos<strong>it</strong>o dell'accesso alle variabili globali). Infatti, se il leftoperand<br />
manca, vuol dire che il nome dato dal right-operand deve essere<br />
cercato nel namespace globale.<br />
Membri di un namespace defin<strong>it</strong>i esternamente<br />
Abbiamo visto che i membri di un namespace possono essere sia dichiarati<br />
che defin<strong>it</strong>i. Sappiamo però che alcune dichiarazioni non sono definizioni e<br />
che in generale un identificatore è utilizzabile dal programma se è defin<strong>it</strong>o (da<br />
qualche parte) ed è dichiarato prima del punto in cui lo si vuole utilizzare.<br />
Possiamo perciò separare, dove è possibile, le dichiarazioni dalle definizioni e<br />
includere solo le prime fra i membri di un namespace, ponendo le seconde al<br />
di fuori. Nelle definizioni esterne però, i nomi devono essere qualificati,<br />
altrimenti non sarebbero riconoscibili.<br />
La separazione fra dichiarazioni e definizioni è applicata soprattutto alle<br />
funzioni. Segu<strong>it</strong>ando con lo stesso esempio:<br />
namespace Stack<br />
{<br />
}<br />
const int max_size = 100;<br />
char v[max_size ];<br />
int top = 0;<br />
void push(char);<br />
char pop( );
void Stack::push(char c) {......}<br />
char Stack::pop( ) {......}<br />
Le funzioni push e pop sono soltanto dichiarate nella definizione del<br />
namespace Stack, e defin<strong>it</strong>e altrove con i nomi qualificati. Non è necessario,<br />
invece, qualificare i membri di Stack utilizzati all'interno delle funzioni, in<br />
quanto il compilatore, se incontra una variabile locale non defin<strong>it</strong>a nell'amb<strong>it</strong>o<br />
della funzione, la va a cercare nel namespace a cui la funzione appartiene.<br />
Quando viene chiamata una funzione membro di un namespace, con<br />
argomenti di cui almeno uno è di tipo astratto membro dello stesso<br />
namespace, la qualificazione del nome della funzione non è necessaria.<br />
Esempio:<br />
#include <br />
namespace A { struct AS {int k;}; char ff(AS); }<br />
char A::ff(AS m) { return (char)m.k; }<br />
int main()<br />
{<br />
A::AS m;<br />
m.k = 65;<br />
cout
}<br />
}<br />
void h( );<br />
la funzione f è dichiarata nel namespace globale; la funzione g è dichiarata<br />
nel namespace A; e infine la funzione h è dichiarata nel namespace B<br />
defin<strong>it</strong>o nel namespace A.<br />
Per accedere (dall'esterno) a un membro del namespace B bisogna ripetere due<br />
volte l'operazione di risoluzione di visibil<strong>it</strong>à.<br />
Es.: void A::B::h( ) {......} (definizione esterna della funzione h)<br />
Per i namespace "annidati" valgono le normali regole di visibil<strong>it</strong>à e di<br />
qualificazione: all'interno della funzione h non occorre qualificare i membri<br />
di B (come sempre), ma neppure quelli di A, in quanto i nomi defin<strong>it</strong>i in amb<strong>it</strong>i<br />
superiori sono ancora visibili negli amb<strong>it</strong>i sottostanti; viceversa, all'interno della<br />
funzione g bisogna qualificare i membri di B (perchè i nomi defin<strong>it</strong>i in<br />
amb<strong>it</strong>i inferiori non solo visibili in quelli superiori), ma non quelli di A, per cui è<br />
sufficiente applicare la risoluzione di visibil<strong>it</strong>à a un solo livello.<br />
Es. void A::g( ) {.... B::h ( ) ....} ( funzione h chiamata dalla<br />
funzione g )<br />
Infine, dall'interno della funzione globale f bisogna qualificare sia i membri di<br />
A (a un livello: A::) che quelli di B (a due livelli: A::B::) in quanto nessun nome<br />
defin<strong>it</strong>o nei due namespace è visibile nel namespace globale.<br />
Namespace sinonimi<br />
La scelta del nome di un namespace è importante: se è troppo breve, rischia il<br />
confl<strong>it</strong>to con i nomi di altri namespace (per esempio includendo librerie create<br />
da altri programmatori); se è molto lungo, può ev<strong>it</strong>are il confl<strong>it</strong>to con altri nomi,<br />
ma diventa scomodo se lo si usa ripetutamente per qualificare esternamente i<br />
suoi membri.<br />
Es.: namespace creato_appos<strong>it</strong>amente_da_me_medesimo {... int x<br />
...}<br />
con un nome così lungo (e così "stupido") non c'è pericolo di confl<strong>it</strong>to, ma è<br />
scomodissimo utilizzare in altri amb<strong>it</strong>i il suo membro x:<br />
creato_appos<strong>it</strong>amente_da_me_medesimo::x = 20;
Entrambi gli inconvenienti possono essere superati, definendo, in un amb<strong>it</strong>o<br />
ristretto (e quindi con scarso pericolo di confl<strong>it</strong>to), un sinonimo breve di un<br />
nome "vero" lungo (i sinonimi possono anche essere defin<strong>it</strong>i localmente, a<br />
differrenza dei namespace). Per definire un sinonimo si usa la seguente<br />
sintassi (segu<strong>it</strong>ando con l'esempio):<br />
namespace STUP = creato_appos<strong>it</strong>amente_da_me_medesimo;<br />
da questo punto in poi (nello stesso amb<strong>it</strong>o in cui è defin<strong>it</strong>o il sinonimo STUP)<br />
si può ogni volta qualificare un membro del suo namespace utilizzando come<br />
left-operand il sinonimo:<br />
STUP::x = 20;<br />
I namespace sinonimi sono utili non solo per abbreviare nomi lunghi, ma<br />
anche per localizzare in un unico punto una modifica che altrimenti si dovrebbe<br />
ripetere in molti punti del programma (come nelle definizioni con const,<br />
#define e typedef). Per esempio, se il nome di un namespace si riferisce alla<br />
versione di una libreria usata dal programma, e questa potrebbe essere<br />
successivamente aggiornata, è molto conveniente creare un sinonimo da<br />
utilizzare nel programma al posto del nome della libreria: in questo modo, in caso<br />
di cambiamento di versione della libreria, si può modificare solo l'istruzione di<br />
definizione del sinonimo, assegnando allo stesso sinonimo il nuovo nome<br />
(altrimenti si dovrebbero modificare tutte le istruzioni che utilizzano quel nome<br />
nel programma).<br />
Namespace anonimi<br />
Nella definizione di un namespace, il nome non è obbligatorio. Se lo si omette,<br />
si crea un namespace anonimo.<br />
Es.: namespace { int a = 10; int b; void c(double); }<br />
I membri a, b e c del namespace anonimo sono visibili in tutto il file (file<br />
scope), non devono essere qualificati, ma non possono essere utilizzati in files<br />
differenti da quello in cui sono stati defin<strong>it</strong>i (cioè, diversamente dagli oggetti<br />
globali, non possono essere collegati dall'esterno tram<strong>it</strong>e lo specificatore<br />
extern).<br />
In altre parole i membri di un namespace anonimo hanno le stesse identiche<br />
proprietà degli oggetti globali defin<strong>it</strong>i con lo specificatore static. Per questo<br />
motivo, e allo scopo di ridurre le ambigu<strong>it</strong>à nel significato delle parole-chiave del<br />
linguaggio, il com<strong>it</strong>ato per la definizione dello standard (pur mantenendo, per<br />
compatibiltà con i "vecchi" programmi, il doppio significato di static), suggerisce
di usare sempre i namespace anonimi per definire oggetti con file scope, e di<br />
mantenere l'uso di static esclusivamente per l'allocazione permanente (cioè con<br />
lifetime illim<strong>it</strong>ato) di oggetti con visibil<strong>it</strong>à locale (block scope).<br />
Estendibil<strong>it</strong>à della definizione di un namespace<br />
Al contrario delle strutture, i namespace sono costrutti "aperti", nel senso che<br />
possono essere defin<strong>it</strong>i più volte con lo stesso nome. Non si tratta però di<br />
diverse definizioni, bensì di estensioni della definizione iniziale. E quindi, pur<br />
essendovi blocchi diversi di un namespace con lo stesso nome, l'amb<strong>it</strong>o<br />
defin<strong>it</strong>o dal namespace con quel nome resta unico.<br />
Ne consegue che, per la ODR (one defin<strong>it</strong>ion rule), i membri<br />
complessivamente defin<strong>it</strong>i in un namespace (anche se frammentato in più<br />
blocchi) devono essere tutti diversi (cioè nelle estensioni è consent<strong>it</strong>o<br />
aggiungere nuovi membri ma non ridefinire membri defin<strong>it</strong>i precedentemente).<br />
Es.:<br />
namespace A {<br />
}<br />
int x ;<br />
namespace B {<br />
}<br />
int x ;<br />
void f( ) {... A::y= ...}<br />
OK: A::x e B::x sono defin<strong>it</strong>i in due amb<strong>it</strong>i<br />
diversi<br />
errore: y non ancora dichiarato in A<br />
namespace A { OK: estensione del namespace A<br />
}<br />
int x ; errore: x è già defin<strong>it</strong>o nell'amb<strong>it</strong>o di A<br />
int y ; OK: y è un nuovo membro di A<br />
void f( ) {... A::y= ...} adesso è OK<br />
La possibil<strong>it</strong>à di suddividere un namespace in blocchi separati consente, da un<br />
lato, di racchiudere grandi frammenti di programma in un unico namespace e,<br />
dall'altro, di presentare diverse interfacce a diverse categorie di utenti,<br />
mostrandone parti differenti.
Parola-chiave using<br />
Quando un membro di un namespace viene usato ripetutamente fuori dal suo<br />
amb<strong>it</strong>o, esiste la possibil<strong>it</strong>à, aggiungendo una sola istruzione, di ev<strong>it</strong>are il fastidio<br />
di qualificarlo ogni volta.<br />
La parola-chiave using serve a questo scopo e può essere usata in due modi<br />
diversi:<br />
• con un'istruzione di "using-declaration" si rende accessibile un membro<br />
di un namespace nello stesso amb<strong>it</strong>o in cui è inser<strong>it</strong>a l'istruzione.<br />
Tornando all'esempio del namespace Stack, l'istruzione:<br />
using Stack::top;<br />
permette di accedere al membro top di Stack, senza bisogno di<br />
qualificarlo, in tutte le istruzioni che seguono all'interno dello stesso<br />
amb<strong>it</strong>o. In sostanza, con la using-declaration, si introduce il sinonimo<br />
locale top di Stack::top .<br />
In una using-declaration va specificato solo il nome del membro<br />
interessato, per cui, in particolare, se il membro è una funzione,<br />
l'elenco degli argomenti non va indicato (e neppure le parentesi tonde);<br />
nel caso di più funzioni in overload con lo stesso nome, la usingdeclaration<br />
le rende accessibili tutte.<br />
• con un'istruzione di "using-directive" si rendono accessibili tutti i membri<br />
di un namespace nello stesso amb<strong>it</strong>o in cui è inser<strong>it</strong>a l'istruzione.<br />
Tornando all'esempio, l'istruzione:<br />
using namespace Stack;<br />
permette di accedere a tutti i membri di Stack, senza bisogno di<br />
qualificarli, in tutte le istruzioni che seguono all'interno dello stesso<br />
amb<strong>it</strong>o.<br />
Entrambe le istruzioni using possono essere inser<strong>it</strong>e in qualunque amb<strong>it</strong>o e in<br />
esso mettono a disposizione sinonimi che a loro volta seguono le normali regole<br />
di visibil<strong>it</strong>à. In particolare:<br />
• se le istruzioni using sono inser<strong>it</strong>e in un blocco (di una funzione,<br />
struttura o altro), i sinonimi hanno block scope;<br />
• se sono inser<strong>it</strong>e nel namespace globale o in un namespace anonimo, i<br />
sinonimi hanno file scope;<br />
• infine, se sono inser<strong>it</strong>e in un altro namespace, i sinonimi hanno lo stesso<br />
scope del namespace che li osp<strong>it</strong>a.<br />
Spesso la using-directive a livello globale è usata come "strumento di<br />
transizione", cioè per trasportare in <strong>C++</strong> vecchio codice scr<strong>it</strong>to in C. Esistono<br />
infatti centinaia di librerie scr<strong>it</strong>te in C, con centinaia di migliaia di righe di codice,<br />
che fanno un uso massiccio ed estensivo di nomi globali. Molte di queste librerie<br />
sono ancora utili e cost<strong>it</strong>uiscono un "patrimonio" che non va disperso. D'altra<br />
parte, "affollare" così pesantemente il namespace globale non fa parte della
"logica" del <strong>C++</strong>. Il problema è stato risolto racchiudendo le librerie in tanti<br />
namespace e facendo ricorso alle using-directive per renderle accessibili<br />
(quando serve). In questo modo si mantiene la compatibil<strong>it</strong>à con i vecchi<br />
programmi, ma i nomi utilizzati dalle librerie non occupano il namespace<br />
globale e quindi non rischiano di creare confl<strong>it</strong>ti in altri contesti.<br />
Precedenze e confl<strong>it</strong>ti fra i nomi<br />
Abbiamo visto che le istruzioni using forniscono la possibil<strong>it</strong>à di ev<strong>it</strong>are la<br />
qualificazione ripetuta dei nomi defin<strong>it</strong>i in un namespace. D'altra parte,<br />
rendendo accessibili delle parti di programma che altrimenti sarebbero nascoste,<br />
indeboliscono il "data hiding" e aumentano la probabil<strong>it</strong>à di confl<strong>it</strong>ti fra nomi e<br />
di errori non sempre riconoscibili. Si tratta pertanto di operare di volta in volta la<br />
scelta più opportuna, bilanciando "comod<strong>it</strong>à" e "sicurezza".<br />
A questo scopo il <strong>C++</strong> definisce delle regole precise che, in taluni casi, vietano i<br />
confl<strong>it</strong>ti di nomi (nel senso che all'occorrenza il compilatore segnala errore) e, in<br />
altri, stabiliscono delle precedenze fra nomi uguali (cioè il nome con precedenza<br />
superiore "nasconde" quello con precedenza inferiore). Tali regole sono diverse se<br />
si usa una using-declaration o una using-directive :<br />
• una using-declaration aggiunge, nello scope in cui è inser<strong>it</strong>a, un nuovo<br />
nome, che si comporta esattamente come tutti gli altri nomi (il fatto che<br />
sia sinonimo di un membro di un namespace è del tutto ininfluente), e<br />
pertanto:<br />
o entra il confl<strong>it</strong>to con un nome uguale defin<strong>it</strong>o nello stesso amb<strong>it</strong>o;<br />
o nasconde i nomi uguali (non qualificati) defin<strong>it</strong>i in amb<strong>it</strong>i superiori.<br />
• una using-directive mette a disposizione, nello scope in cui è inser<strong>it</strong>a,<br />
tutti i nomi defin<strong>it</strong>i in un namespace; si sottolinea il fatto che li "mette a<br />
disposizione" ma non li aggiunge immediatamente, e pertanto questa<br />
istruzione non genera confl<strong>it</strong>ti di per sè. Al momento dell'utilizzo, ogni<br />
nome del namespace interessato si comporta nel modo seguente:<br />
o è nascosto da un nome uguale defin<strong>it</strong>o nello stesso amb<strong>it</strong>o o in<br />
amb<strong>it</strong>i superiori, esclusi il namespace globale e il namespace<br />
anonimo;<br />
o entra il confl<strong>it</strong>to con un nome uguale (non qualificato) defin<strong>it</strong>o nel<br />
namespace globale o nel namespace anonimo.<br />
Queste regole (un po' "atipiche") sui nomi resi accessibili dalla usingdirective,<br />
sono state concep<strong>it</strong>e al duplice scopo di permettere l'inclusione<br />
di grandi librerie con molti nomi globali senza segnalare i potenziali<br />
confl<strong>it</strong>ti dei nomi che non vengono di fatto utilizzati, e, all'opposto, di non<br />
incoraggiare l'utente "pigro" che continua a definire nomi globali piuttosto<br />
che fare ricorso ai namespace.
• nel caso di concorrenza fra using-declaration e using-directive, le<br />
prime prevalgono sulle seconde e risolvono anche i potenziali confl<strong>it</strong>ti<br />
Es.:<br />
namespace A { ... int x ; ...<br />
}<br />
namespace B { ... int x ; ... }<br />
using namespace A;<br />
A e B hanno un membro con lo stesso<br />
nome<br />
using namespace B; Achtung ! potenziale confl<strong>it</strong>to ..........<br />
using A::x; .......... risolto a favore di A !<br />
Collegamento fra namespace defin<strong>it</strong>i in files diversi<br />
Finora abbiamo trattato i namespace intendendo che fossero sempre defin<strong>it</strong>i<br />
nello stesso file. Ci chiediamo ora in che modo è possibile il collegamento fra<br />
namespace di file diversi. Prima, però, è opportuno ricordare la differenza che<br />
intercorre fra file sorgente e translation un<strong>it</strong>:<br />
• un file sorgente contiene le istruzioni del programma create dal<br />
programmatore;<br />
• una translation un<strong>it</strong> è lo stesso file visto dal compilatore, dopo che il<br />
preprocessore ha incluso gli header-files ed esegu<strong>it</strong>o le altre eventuali<br />
direttive (#define, direttive condizionali ecc...).<br />
Due namespace con lo stesso nome appartenenti a due diverse translation<br />
un<strong>it</strong>s non sono in confl<strong>it</strong>to, ma sono da considerarsi come facenti parte dello<br />
stesso unico namespace (per la proprietà di estendibil<strong>it</strong>à dei namespace). Il<br />
confl<strong>it</strong>to, semmai, può sorgere fra i nomi dei membri del namespace, se viene<br />
violata la ODR. D'altra parte ogni translation un<strong>it</strong> viene compilata<br />
separatamente e quindi ogni nome utilizzato in una translation un<strong>it</strong> deve<br />
essere, nella stessa, anche dichiarato. Ne consegue che i membri di uno stesso<br />
namespace che vengono utilizzati in entrambe le translation un<strong>it</strong>s, devono<br />
essere, in una delle due, defin<strong>it</strong>i, e nell'altra dichiarati senza essere defin<strong>it</strong>i<br />
(questo discorso vale per gli oggetti e le funzioni non inline, mentre le<br />
funzioni inline, i tipi astratti e altre ent<strong>it</strong>à del linguaggio che vederemo, come i<br />
template, possono anche essere ridefin<strong>it</strong>i, purchè gli elementi lessicali di ogni<br />
definizione siano identici).<br />
Diverso è l'approccio, se si considerano i file sorgente: ogni file (cioè ogni<br />
modulo del programma) dovrebbe essere progettato in modo da non contenere<br />
duplicazioni e da localizzare questo problema soltanto nelle eventuali interfacce<br />
incluse da più moduli. Queste interfacce dovrebbero contenere solo<br />
dichiarazioni o definizioni "ripetibili".
Quindi il "trucco" consiste sostanzialmente nel progettare al meglio le interfacce<br />
comuni: una "buona" interfaccia dovrebbe essere tale da minimizzare le<br />
dipendenze fra le varie parti del programma, in quanto interfacce con<br />
dipendenze minime conducono a sistemi più facili da comprendere, con dettagli<br />
implementativi invisibili (data-hiding), più facili da modificare e più veloci da<br />
compilare.<br />
Riprendiamo a questo propos<strong>it</strong>o il nostro esempio iniziale del namespace Stack<br />
e mettiamoci "nei panni" sia del progettista che dell'utente.<br />
• Il progettista deve individuare con quale strumenti rappresentare la pila<br />
(per esempio con un array, ma ci potrebbero essere anche altre soluzioni),<br />
quali siano le informazioni da memorizzare e mantenere (l'indice<br />
corrispondente all'ultimo dato inser<strong>it</strong>o e la massima dimensione di<br />
accrescimento della pila), quali algor<strong>it</strong>mi applicare per le operazioni di<br />
inserimento ed estrazione dei dati, e infine come dare all'utente la<br />
possibil<strong>it</strong>à di operare.<br />
• L'utente ha bisogno di conoscere solo due cose: il nome della funzione<br />
per inserire un dato e il nome della funzione per estrarlo. E' quindi<br />
opportuno che acceda esclusivamente a tali informazioni (le dichiarazioni<br />
delle due funzioni), che cost<strong>it</strong>uiranno l'interfaccia comune fra il file<br />
sorgente del progettista e quello dell'utente.<br />
Si deduce pertanto che il progettista dovrà spezzare la definizione del<br />
namespace Stack in due (per fortuna ciò è possibile!): nella prima parte<br />
metterà solo le dichiarazioni delle funzioni push e pop; nella seconda tutto il<br />
resto. Creerà poi due files separati: nel primo (l'interfaccia comune) metterà<br />
soltanto la prima definizione del namespace Stack , nel secondo metterà<br />
l'estensione di Stack e, esternamente al namespace, le definizioni delle due<br />
funzioni. A sua volta l'utente non dovrà fare altro che inserire nel suo file<br />
sorgente la direttiva di inclusione dell'interfaccia comune. Così, qualsiasi<br />
modifica o miglioramento venga fatto al codice di implementazione dello Stack, i<br />
programmi degli utenti non ne verranno minimamente influenzati (al massimo<br />
dovrano essere ri-linkati).
Eccezioni<br />
Segnalazione e gestione degli errori<br />
Il termine eccezione (dall'inglese exception) deriva dall'ottimistica assunzione<br />
che nell'esecuzione di un programma gli errori cost<strong>it</strong>uiscano una "circostanza<br />
eccezionale". Anche condividendo tale ottimismo, il problema di come individuare<br />
gli errori e di come gestirli una volta individuati deve essere sempre affrontato con<br />
grande cura nella progettazione di un programma.<br />
Anche in un programma "perfetto" gli errori in fase di esecuzione possono<br />
sempre cap<strong>it</strong>are, perchè sono commessi in larga parte da operatori "umani" (quelli<br />
che usano il programma), e quindi è lo stesso programma che deve essere in<br />
grado di prevederli e di eseguire le azioni di ripristino, quando è possibile.<br />
Quando un programma, specie se di grosse dimensioni, è composto da moduli<br />
separati, e soprattutto se i moduli provengono da librerie sviluppate da altri<br />
programmatori, anche la gestione degli errori deve essere tale da minimizzare le<br />
dipendenze fra un modulo e l'altro. In generale, quando un modulo verifica una<br />
condizione di errore, deve lim<strong>it</strong>arsi a segnalare tale condizione, in quanto l'azione<br />
di ripristino dipende più spesso dal modulo che ha invocato l'operazione piuttosto<br />
che da quello che ha riscontrato l'errore mentre cercava di eseguirla. Separando i<br />
due momenti (la rilevazione dell'errore e la sua gestione) si mantiene il<br />
massimo di indipendenza fra i moduli: l'interfaccia comune conterrà gli<br />
strumenti necessari, attivati dal modulo "rilevatore" e utilizzati dal modulo<br />
"gestore"<br />
Il <strong>C++</strong> mette a disposizione un meccanismo semplice ma molto efficace di<br />
rilevazione e gestione degli errori: l'idea base è che, quando una funzione rileva<br />
un errore che non è in grado di affrontare direttamente, l'esecuzione della<br />
funzione termina, ma il controllo non r<strong>it</strong>orna al punto in cui la funzione è stata<br />
chiamata, bensì in un altro punto del programma, dove viene esegu<strong>it</strong>a la<br />
procedura di gestione dell'errore. In termini tecnici, la funzione che rileva<br />
l'errore "solleva" o "lancia" (throw) un'eccezione ("marcandola" in qualche<br />
modo, come vedremo) e termina: l'area stack è ripercorsa all'indietro e cancellata<br />
(stack unwinding) a vari livelli finchè il flusso del programma non raggiunge il<br />
punto (se esiste) in cui l'eccezione può essere riconosciuta e "catturata" (catch);<br />
in questo punto viene esegu<strong>it</strong>a la procedura di gestione dell'errore; se il punto<br />
non esiste l'intero programma "abortisce".<br />
Il costrutto try<br />
La parola-chiave try introduce un blocco di istruzioni.<br />
Es. : try
{<br />
}<br />
m = c / b;<br />
double f = 10.7;<br />
res = fun(f ,m+n);<br />
Le istruzioni contenute in un blocco try sono "sotto controllo": in esecuzione,<br />
qualcuna di esse potrebbe generare un errore. Nell'esempio, la funzione fun<br />
potrebbe chiamare un'altra funzione e questa un'altra ancora ecc... , generando<br />
una serie di pacchetti che si accumula sullo stack. L'area dello stack che va da<br />
un un blocco try in su è detta: exception stack frame e cost<strong>it</strong>uisce l'insieme di<br />
tutte le istruzioni controllate.<br />
L'istruzione throw<br />
Dal punto di visto sintattico, l'istruzione throw è identica all'istruzione return di<br />
una funzione (e si comporta all'incirca nello stesso modo):<br />
throw espressione;<br />
Un'istruzione throw può essere collocata soltanto in un exception stack frame<br />
e segnala il punto in cui si è ricontrato un errore (o, come si dice, è "sollevata"<br />
un'eccezione). Il valore calcolato dell'espressione, detto: "valore<br />
dell'eccezione" (il cui tipo è detto: "tipo dell'eccezione"), ripercorre<br />
"all'indietro" l'exception stack frame (cancellandolo): se a un certo punto del<br />
suo "cammino" l'eccezione viene "catturata" (vedremo come), l'errore può<br />
essere gest<strong>it</strong>o, altrimenti il programma abortisce (ed è quello che succede in<br />
particolare se l'istruzione throw non è inser<strong>it</strong>a all'interno di un exception stack<br />
frame).<br />
In pratica throw si comporta come un return "multilivello". Il valore<br />
dell'eccezione viene di sol<strong>it</strong>o utilizzato per la descrizione dell'errore commesso<br />
(non è però obbligatorio utilizzarlo). Il suo tipo è invece di importanza<br />
fondamentale in quanto (come vedremo) cost<strong>it</strong>uisce la "marca" di riconoscimento<br />
dell'eccezione e ne permette la "cattura".<br />
Il gestore delle eccezioni: costrutto catch<br />
La parola-chiave catch introduce un blocco di istruzioni che ha lo stesso<br />
formato sintattico della definizione di una funzione, con un solo argomento e<br />
senza valore di r<strong>it</strong>orno.
catch (tipo argomento ) { .......... blocco di istruzioni .............. }<br />
Fisicamente un blocco catch deve seguire immediatamente un blocco try. Dal<br />
punto di vista della successione logica delle operazioni, invece, un blocco catch<br />
cost<strong>it</strong>uisce il punto terminale di r<strong>it</strong>orno di un exception stack frame: questo<br />
viene costru<strong>it</strong>o (verso l'alto), a partire da un blocco try, fino a un'istruzione<br />
throw, da cui l'eccezione "sollevata" ridiscende (stack unwinding) fino al<br />
blocco catch corrispondente al blocco try di partenza (oppure passa<br />
direttamente dal blocco try al blocco catch se l'istruzione throw si trova già<br />
nel blocco try di partenza; in questo caso l'istruzione throw non si comporta<br />
come un return, ma piuttosto come un goto). A questo punto l'eccezione può<br />
essere "catturata" o meno: se è catturata, vengono esegu<strong>it</strong>e le istruzioni del<br />
blocco catch (detto "gestore dell'eccezione") e poi il flusso del programma<br />
prosegue normalmente; se invece l'eccezione non è catturata, il programma<br />
abortisce. Se infine non vengono sollevate eccezioni, cioè l'exception stack<br />
frame non incontra istruzioni throw, il flusso del programma ridiscende per vie<br />
normali tornando al blocco try da cui era part<strong>it</strong>o, esegu<strong>it</strong>o il quale prosegue<br />
"saltando" il successivo blocco catch.<br />
Un'eccezione viene "catturata" se il suo tipo coincide esattamente con il tipo<br />
dell'argomento di catch. Non sono ammesse conversioni di tipo, neppure<br />
implic<strong>it</strong>e (neanche se i due tipi sono uguali in pratica, come int e long in una<br />
machina a 32 b<strong>it</strong>). Verificata la coincidenza dei tipi, il valore dell'eccezione<br />
viene trasfer<strong>it</strong>o nell'argomento di catch (come se l'istruzione throw<br />
"chiamasse" la "funzione" catch); il trasferimento avviene normalmente per copia<br />
(by value), a meno che l'argomento di catch non sia un riferimento, nel qual<br />
caso il passaggio è by reference, che però ha senso solo se l'espressione di<br />
throw è un l-value e se "sopravvive" alla distruzione dello stack (cioè è un<br />
oggetto globale, o è defin<strong>it</strong>o in un namespace, oppure è locale ma<br />
dichiarato static). E' possibile anche che l'argomento di catch sia dichiarato<br />
const, nel qual caso valgono le stesse regole e lim<strong>it</strong>azioni che ci sono per il<br />
passaggio degli argomenti delle funzioni (vedere il cap<strong>it</strong>olo: Puntatori e<br />
costanti - Passaggio degli argomenti trasmessi by value e by reference).<br />
Nel costrutto catch la specifica dell'argomento non è obbligatoria (lo è solo se<br />
l'argomento viene usato nel blocco di istruzioni). Il tipo, invece, deve essere<br />
sempre specificato, perchè serve per la "cattura" dell'eccezione. A questo<br />
propos<strong>it</strong>o è utile aggiungere che la scelta del tipo dell'eccezione è libera, ma,<br />
per una migliore leggibil<strong>it</strong>à del programma e per ev<strong>it</strong>are confusioni con le altre<br />
eccezioni (in special modo con quelle generate dalle librerie del sistema, fuori dal<br />
nostro controllo), è vivamente consigliata la creazione di tipi "ad hoc",<br />
preferibilmente uno per ogni possibile eccezione e con attinenza mnemonica fra<br />
il nome del tipo e il significato dell'errore a cui è associato: quindi, ev<strong>it</strong>are l'uso<br />
di tipi nativi (anche se non sarebbe vietato), ma usare solo tipi astratti (per<br />
esempio strutture con nomi "ad hoc").<br />
E' bene che il trattamento delle eccezioni venga usato quando la rilevazione e<br />
la gestione di un errore devono avvenire in parti diverse del programma.<br />
Quando invece un errore può essere trattato localmente è sufficiente servirsi dei<br />
normali controlli del linguaggio (come i costrutti if o sw<strong>it</strong>ch).<br />
NOTA: per completezza precisiamo che un'eccezione può essere "catturata"<br />
anche quando il suo tipo è di una classe "derivata" da quella a cui appartiene
l'argomento di catch, ma di questo parleremo quando tratteremo delle classi<br />
e dell'ered<strong>it</strong>à.<br />
Riconoscimento di un'eccezione fra diverse alternative<br />
Finora abbiamo detto che a un blocco try deve sempre seguire blocco catch. In<br />
realtà i blocchi catch possono anche essere più di uno, disposti<br />
consecutivamente e con tipi di argomento diversi.<br />
Quando un'eccezione, discendendo lungo l'exception stack frame, incontra<br />
una serie di blocchi catch, il suo tipo viene confrontato a uno a uno con quelli<br />
dei blocchi catch e, se si verifica una coincidenza, l'eccezione viene "catturata"<br />
e vengono esegu<strong>it</strong>e le istruzioni del blocco catch in cui la coincidenza è stata<br />
trovata. Dopodichè il flusso del programma "salta" gli eventuali blocchi catch<br />
successivi e riprende normalmente dalla prima istruzione dopo l'ultimo blocco<br />
catch del gruppo. Il programma abortisce se nessun blocco catch cattura<br />
l'eccezione. Se invece non vengono sollevate eccezioni, il flusso del<br />
programma, esegu<strong>it</strong>e le istruzioni del blocco try, "salta" tutti i blocchi catch del<br />
gruppo.<br />
Se un costrutto catch, al posto del tipo e dell'argomento, presenta "tre<br />
puntini" (ellipsis), significa che è in grado di catturare qualsiasi eccezione,<br />
indipendentemente dal suo tipo.<br />
L'ordine in cui appaiono i diversi blocchi catch associati a un blocco try è<br />
importante: infatti il confronto con il tipo dell'eccezione da catturare viene<br />
sempre fatto a partire dal primo blocco catch che segue il blocco try e procede<br />
nello stesso ordine: da ciò consegue che l'eventuale catch con ellipsis deve<br />
essere sempre l'ultimo blocco del gruppo. L'esempio che segue schematizza la<br />
s<strong>it</strong>uazione di un blocco try segu<strong>it</strong>o da tre blocchi catch, di cui l'ultimo con<br />
ellipsis.<br />
try { blocco try } se non solleva eccezioni, esegue blocco try e salta a<br />
istruzione<br />
catch (tipo1) {<br />
blocco1}<br />
catch (tipo2) {<br />
blocco2}<br />
altrimenti, se il tipo dell'eccezione coincide con tipo1,<br />
cattura l'eccezione, esegue blocco1 e salta a istruzione<br />
altrimenti, se il tipo dell'eccezione coincide con tipo2,<br />
cattura l'eccezione, esegue blocco2 e salta a istruzione<br />
catch (...) {blocco3} altrimenti, cattura comunque l'eccezione ed esegue<br />
blocco3<br />
istruzione ......... riprende il flusso normale del programma
Blocchi innestati<br />
Una sequenza di blocchi try....catch può essere a sua volta "innestata" in un<br />
blocco try o in un blocco catch (o in una funzione chiamata, direttamente o<br />
indirettamente, da un blocco try o da un blocco catch).<br />
Se la nuova sequenza è interna a un blocco try (cioè nella fase "ascendente"<br />
dell'exception stack frame) e successivamente viene sollevata un'eccezione, il<br />
controllo per la cattura dell'eccezione viene fatto anz<strong>it</strong>utto sui blocchi catch<br />
interni (che sono incontrati prima nella fase di stack unwinding): se<br />
l'eccezione è catturata, il problema è risolto e anche tutti i blocchi catch<br />
associati al blocco try esterno vengono "saltati"; se invece nessun blocco<br />
interno cattura l'eccezione, il programma non abortisce, ma il controllo passa ai<br />
blocchi catch associati al blocco try esterno.<br />
Se la nuova sequenza è interna a un blocco catch (cioè se l'eccezione è già<br />
stata catturata), si crea un nuovo exception stack frame a partire da quel<br />
punto: pertanto, se è sollevata una nuova eccezione e questa viene catturata, il<br />
programma esegue il blocco catch interno che ha catturato la nuova eccezione<br />
e poi completa l'esecuzione del blocco catch esterno che ha catturato<br />
l'eccezione precedente; se invece la nuova eccezione non è catturata, il<br />
programma abortisce.<br />
Anche l'istruzione throw può comparire in un blocco catch o in una funzione<br />
chiamata, direttamente o indirettamente, da un blocco catch (la sua<br />
collocazione "normale" sarebbe invece in un blocco try o in una funzione<br />
chiamata, direttamente o indirettamente, da un blocco try). In questo caso si<br />
dice che l'eccezione è "ri-sollevata", ma non può essere gest<strong>it</strong>a allo stesso livello<br />
del blocco catch da cui parte, in quanto un blocco catch non può essere<br />
"chiamato" ricursivamente. Pertanto un'eccezione sollevata dall'interno di un<br />
blocco catch non fa abortire il programma solo se lo stesso blocco catch fa<br />
parte di una sequenza innestata in un blocco try esterno (e saranno i<br />
corrispondenti blocchi catch a occuparsi della sua cattura).<br />
Un caso particolare di eccezione "ri-sollevata" si ha quando l'istruzione throw<br />
appare da sola, senza essere segu<strong>it</strong>a da un'espressione; in questo caso il valore<br />
e il tipo dell'eccezione sono gli stessi del blocco catch in cui l'istruzione throw<br />
è inser<strong>it</strong>a (cioè il programma "ri-solleva" la stessa eccezione che sta gestendo).<br />
Eccezioni che non sono errori
Come abbiamo detto all'inizio, il concetto di eccezione è di norma legato a<br />
quello di errore. Tuttavia il meccanismo di gestione delle eccezioni altro non è<br />
che un particolare algor<strong>it</strong>mo di "controllo", meno strutturato e meno efficiente<br />
rispetto alle strutture di controllo locali (quali if, sw<strong>it</strong>ch, for ecc...), che però<br />
permette operazioni, come i return "multilivello", che con le strutture tradizionali<br />
sarebbero più difficili da ottenere o porterebbero a un codice non in grado di<br />
mantenere un adeguato livello di indipendenza fra i diversi moduli del<br />
programma.<br />
Quindi la convenienza o meno dell'utilizzo delle eccezioni non si basa tanto sulla<br />
distinzione fra errori o altre s<strong>it</strong>uazioni, quanto piuttosto sul fatto che le due<br />
operazioni di "controllo" e "azione conseguente" siano localizzate insieme (nel qual<br />
caso conviene usare le strutture tradizionali), oppure siano separate in aree<br />
diverse dello stack (e allora è preferibile usare le eccezioni).<br />
Per esempio, l'utilizzo delle eccezioni come strutture di controllo potrebbe essere<br />
una tecnica elegante per terminare funzioni di ricerca, soprattutto se la ricerca<br />
avviene attraverso chiamate ricorsive, che "impilano" un numero imprecisato di<br />
pacchetti sullo stack.<br />
Altre "correnti di pensiero", invece, suggersicono di mantenere strettamente<br />
correlato il concetto di eccezione con quello di errore, per ev<strong>it</strong>are la generazione<br />
di codice ambiguo e poco comprensibile (e quindi meno portabile e, in sostanza,<br />
"più costoso").
Classi e data hiding<br />
Analogia fra classi e strutture<br />
In <strong>C++</strong> le classi sono identiche alle strutture, con l'unica differenza formale di<br />
essere introdotte dalla parola-chiave class anziché struct.<br />
In realtà la principale differenza fra classi e strutture è di natura "storica": le<br />
strutture sono nate in C, con alcune proprietà (descr<strong>it</strong>te nel cap<strong>it</strong>olo: "Tipi<br />
defin<strong>it</strong>i dall'utente"); le classi sono nate in <strong>C++</strong>, con le stesse proprietà delle<br />
strutture e molte altre proprietà in più. Successivamente si è pensato di<br />
attribuire alle strutture le stesse proprietà delle classi. Pertanto le strutture<br />
<strong>C++</strong> sono molto diverse dalle strutture C, essendo invece identiche alle classi<br />
(a parte una sola differenza sostanziale, di cui parleremo fra poco). Per questo<br />
motivo, d'ora in poi tratteremo solo di classi, sottintendendo che, in <strong>C++</strong>,<br />
quanto detto vale anche per le strutture.<br />
Esempio di definizione di una classe:<br />
class point<br />
{ double x; double y; double z; } ;<br />
ogni istanza della classe point rappresenta un punto nello spazio e i suoi<br />
membri sono le coordinate cartesiane del punto.<br />
Specificatori di accesso<br />
In <strong>C++</strong>, nel blocco di definizione di una classe, é possibile utilizzare dei nuovi<br />
specificatori, detti specificatori di accesso, che sono i seguenti:<br />
private: protected: public:<br />
gli specificatori private: e protected: hanno significato analogo: la loro<br />
differenza riguarda esclusivamente le classi ered<strong>it</strong>ate, di cui parleremo più<br />
avanti; per il momento, useremo soltanto lo specificatore private: .<br />
Questi specificatori possono essere inser<strong>it</strong>i più volte all'interno della definizione<br />
di una classe: private: fa sì che tutti i membri dichiarati da quel punto in poi<br />
(fino al termine della definizione della classe o fino a un nuovo specificatore)<br />
acquisiscano la connotazione di membri privati (in che senso? ... vedremo più
avanti); public: fa sì che tutti i membri successivamente dichiarati siano<br />
pubblici.<br />
L'unica differenza sostanziale fra classe e struttura consiste nel fatto che i<br />
membri di una struttura sono, di default, pubblici, mentre quelli di una<br />
classe sono, di default, privati.<br />
Data hiding<br />
Il "data hiding" (occultamento dei dati) consiste nel rendere certe aree del<br />
programma invisibili ad altre aree del programma. I suoi vantaggi sono evidenti:<br />
favorisce la programmazione modulare, rende più agevoli le operazioni di<br />
manutenzione del software e, in ultima analisi, permette un modo di programmare<br />
più efficiente.<br />
Introducendo i namespace, abbiamo detto che il data hiding si realizza<br />
sostanzialmente racchiudendo i nomi all'interno di amb<strong>it</strong>i di visibil<strong>it</strong>à e<br />
definendo dei canali di comunicazione, ben circoscr<strong>it</strong>ti e controllati, come uniche<br />
vie di accesso ai nomi di amb<strong>it</strong>i diversi. Se tutto quello che serve è la protezione<br />
dei nomi degli oggetti, i namespace sono sufficienti a questo scopo.<br />
D'altra parte, questo livello di protezione, lim<strong>it</strong>ato ai soli oggetti, può rivelarsi<br />
inadeguato, se gli oggetti sono istanze di strutture o classi, cioè possiedono<br />
membri. E' sorto quindi il problema di proteggere, non solo un oggetto, ma<br />
anche i suoi membri, facendo in modo che, anche quando l'oggetto é visibile,<br />
l'accesso ai suoi membri sia rigorosamente controllato.<br />
Il <strong>C++</strong> ha realizzato questo obiettivo, estendendo il data hiding anche ai<br />
membri degli oggetti. L'istanza di una classe é regolarmente visibile all'interno<br />
del proprio amb<strong>it</strong>o, ma i suoi membri privati non lo sono: non é possibile, da<br />
programma, accedere direttamente ai membri privati di una classe.<br />
Es.: class Persona {<br />
int soldi ;<br />
public:<br />
} ;<br />
char telefono[20] ;<br />
char indirizzo[30] ;<br />
Persona Giuseppe ; (istanza della classe Persona)<br />
il programma può accedere a Giuseppe.telefono e Giuseppe.indirizzo, ma<br />
non a Giuseppe.soldi!
Funzioni membro<br />
A questo punto, la domanda d'obbligo é: se i membri privati di una classe sono<br />
inaccessibili, a che cosa servono ?<br />
In realtà i membri privati sono inaccessibili direttamente, ma possono essere<br />
raggiunti indirettamente, tram<strong>it</strong>e le cosiddette funzioni-membro.<br />
Infatti il <strong>C++</strong> ammette che i membri di una classe possano essere cost<strong>it</strong>u<strong>it</strong>i non<br />
solo da dati, ma anche da funzioni. Queste funzioni possono essere, come ogni<br />
altro membro, pubbliche o private, ma, in ogni caso, possono accedere a<br />
qualunque altro membro della classe, anche ai membri privati. D'altra parte,<br />
mentre una funzione-membro privata può essere chiamata solo da un'altra<br />
funzione-membro, una funzione-membro pubblica può anche essere<br />
chiamata dall'esterno, e pertanto cost<strong>it</strong>uisce l'unico tram<strong>it</strong>e fra il programma e i<br />
membri della classe.<br />
Questo tipo di arch<strong>it</strong>ettura del <strong>C++</strong> cost<strong>it</strong>uisce la base fondamentale della<br />
programmazione a oggetti: ogni istanza di una classe è caratterizzata dalle<br />
sue proprietà (dati-membro) e dai suoi comportamenti (funzionimembro),<br />
detti anche metodi della classe. Con proprietà e metodi, un<br />
oggetto diviene un'ent<strong>it</strong>à attiva e autosufficiente, che comunica con il<br />
programma in modo rigorosamente controllato. L'azione di chiamare dall'esterno<br />
una funzione-membro pubblica di una classe viene rifer<strong>it</strong>a con il termine:<br />
"inviare un messaggio a un oggetto", per evidenziare il fatto che il programma<br />
si lim<strong>it</strong>a a dire all'oggetto cosa vuole, ma in realtà é l'oggetto stesso ad eseguire<br />
l'operazione, tram<strong>it</strong>e i suoi metodi e agendo sulle sue proprietà (si dice anche<br />
che le funzioni-membro sono incapsulate negli oggetti).<br />
Nella definizione di una funzione-membro, gli altri membri della sua stessa<br />
classe vanno indicati esclusivamente con il loro nome (senza operatori . o ->).<br />
Il <strong>C++</strong>, ogni volta che incontra una variabile non dichiarata nella funzione,<br />
cerca, prima di segnalare l'errore, di identificare il suo nome con quello di un<br />
membro della classe (esattamente come accade per i membri di un<br />
namespace, utilizzati in una funzione membro dello stesso namespace).<br />
I metodi possono essere inser<strong>it</strong>i nella definizione di una classe in due diversi<br />
modi: o come funzioni inline, cioè con il loro codice (ma la parola-chiave<br />
inline può essere omessa in quanto all'interno della definizione di una classe è<br />
di default), oppure con la sola dichiarazione separata dal codice, che viene<br />
scr<strong>it</strong>to in altra parte del programma. Riprendendo l'esempio della classe point<br />
(che, per semplic<strong>it</strong>à, riduciamo a due dimensioni):<br />
Esempio del primo modo Esempio del secondo modo
class point { class point {<br />
double x; double x;<br />
double y; double y;<br />
public: public:<br />
void set(double x0, double y0) void set(double, double ) ;<br />
{ x=x0 ; y=y0 ; } } ;<br />
} ;<br />
Se la definizione della funzione-membro set non è inser<strong>it</strong>a nell'amb<strong>it</strong>o della<br />
definizione della classe point (secondo modo), il suo nome dovrà essere<br />
qualificato con il nome della classe (come vedremo fra poco).<br />
Seguendo l'esempio, definiamo ora l'oggetto p come istanza della classe<br />
point:<br />
point p;<br />
il programma, che non può accedere alle proprietà private p.x e p.y, può però<br />
accedere a un metodo pubblico dello stesso oggetto, con l'istruzione:<br />
p.set(x0,y0) ;<br />
e quindi agire sull'oggetto nel solo modo che gli sia consent<strong>it</strong>o.<br />
Nel caso che una variabile venga defin<strong>it</strong>a come puntatore a una classe,<br />
valgono le stesse regole, con la differenza che bisogna usare (per le funzioni<br />
come per i dati) l'operatore -><br />
Tornando all'esempio:<br />
point * ptr = new point;<br />
ptr->set(1.5, 0.9) ;<br />
Risoluzione della visibil<strong>it</strong>à<br />
Se il codice di un metodo si trova all'esterno della definizione della classe a cui<br />
appartiene, bisogna "qualificare" il nome della funzione associandogli il nome<br />
classe, tram<strong>it</strong>e l'operatore :: di risoluzione di visibil<strong>it</strong>à. Segu<strong>it</strong>ando<br />
nell'esempio precedente, la definizione esterna della funzione-membro set è:<br />
void point::set(double x0, double y0)<br />
{<br />
x = x0 ;<br />
y = y0 ;
}<br />
notiamo che questa regola è la stessa che abbiamo visto per i namespace; in<br />
realtà si tratta di una regola generale che si applica ogni volta che si deve<br />
accedere dall'esterno a un nome dichiarato in un certo amb<strong>it</strong>o di visibil<strong>it</strong>à, e<br />
lo stesso amb<strong>it</strong>o di visibil<strong>it</strong>à è identificato da un nome (come sono appunto sia<br />
i namespace che le classi).<br />
La scelta se un metodo debba essere scr<strong>it</strong>to in forma inline o meno è arb<strong>it</strong>raria:<br />
se è inline, l'esecuzione è più veloce, se non lo è, la definizione della classe<br />
appare in una forma più "leggibile". Per esempio, si potrebbero lasciare inline<br />
solo i metodi privati. E' anche possibile scrivere il codice esternamente alla<br />
definizione della classe, ma specificare esplic<strong>it</strong>amente che la funzione deve<br />
essere trattata come inline, con la seguente istruzione (riprendendo il sol<strong>it</strong>o<br />
esempio):<br />
inline void point::set(double x0, double y0)<br />
in ogni caso il compilatore separa automaticamente il codice se la funzione è<br />
troppo lunga.<br />
Quando, nella definizione di una classe, si lasciano solo i prototipi dei<br />
metodi, si suole dire che viene creata un'intestazione di classe. La<br />
consuetudine prevalente dei programmatori in <strong>C++</strong> è quella di creare librerie di<br />
classi, separando in due gruppi distinti, le intestazioni, distribu<strong>it</strong>e in headerfiles,<br />
dal codice delle funzioni, compilate separatamente e distribu<strong>it</strong>e in librerie<br />
in formato binario; infatti ai programmatori che utilizzano le classi non interessa<br />
sapere come sono fatte le funzioni di accesso, ma solo come usarle.<br />
Funzioni-membro di sola lettura<br />
Quando un metodo ha il solo comp<strong>it</strong>o di riportare informazioni su un oggetto,<br />
senza modificarne il contenuto, si può, per ev<strong>it</strong>are errori, imporre tale condizione a<br />
priori, inserendo lo specificatore const dopo la lista degli argomenti della<br />
funzione (sia nella dichiarazione che nella definizione). Riprendendo<br />
l'esempio della classe point, aggiungiamo la funzione-membro get:<br />
void point::get(double& x0, double& y0) const<br />
{<br />
}<br />
x0 = x ;<br />
y0 = y ;<br />
la funzione-membro get non può modificare i membri della sua classe.
Classi membro<br />
Una classe può anche essere defin<strong>it</strong>a all'interno di un'altra classe (oppure<br />
semplicemente dichiarata, e poi defin<strong>it</strong>a esternamente, nel qual caso però il<br />
suo nome deve essere qualificato con il nome della classe di appartenenza).<br />
Esempio di definizione di un metodo f di una classe B, defin<strong>it</strong>a all'interno di<br />
un'altra classe A:<br />
void A::B::f( ) {......}<br />
Le classi defin<strong>it</strong>e all'interno delle altre classi sono dette: classi-membro o<br />
classi annidate. A parte i problemi inerenti all'amb<strong>it</strong>o di visibil<strong>it</strong>à e alla<br />
conseguente necess<strong>it</strong>à di qualificare i loro nomi, queste classi si comportano<br />
esattamente come se fossero indipendenti. Se però sono collocate nella sezione<br />
privata della classe di appartenenza, possono essere istanziate solo dai<br />
metodi di detta classe. In sostanza, annidare una classe dentro un'altra<br />
classe permette di controllare la creazione dei suoi oggetti. L'accesso ai suoi<br />
membri, invece, non dipende dalla collocazione nella classe di appartenenza, ma<br />
solo da come sono dichiarati gli stessi membri al suo interno (cioè se pubblici<br />
o privati).<br />
Polimorfismo<br />
Per una programmazione efficiente, anche la scelta dei nomi delle funzioni ha la<br />
sua importanza. In particolare é utile che funzioni che svolgono la stessa azione<br />
abbiano lo stesso nome.<br />
Il <strong>C++</strong> consente questa possibil<strong>it</strong>à: non solo i metodi di una classe possono<br />
agire su istanze diverse della stessa classe, ma sono anche ammessi metodi di<br />
classi diverse con lo stesso nome e gli stessi argomenti (non confondere con<br />
l'overload, che implica funzioni con lo stesso nome, ma con diverse liste di<br />
argomenti). Il <strong>C++</strong> é in grado di riconoscere in esecuzione l'oggetto a cui il<br />
metodo é applicato e di selezionare ogni volta la funzione che gli compete.<br />
Questa att<strong>it</strong>udine del linguaggio di rispondere in modo diverso allo stesso<br />
messaggio si chiama "polimorfismo": risponde all'esigenza del <strong>C++</strong> di<br />
modellarsi il più possibile sui concetti della v<strong>it</strong>a reale e, in questo modo, rendere la<br />
programmazione più facile ed efficiente che in altri linguaggi. L'importanza del<br />
polimorfismo si comprenderà a pieno quando parleremo dell'ered<strong>it</strong>à e delle<br />
funzioni virtuali.
Puntatore nascosto this<br />
Ci potremmo chiedere, a questo punto, come fa il <strong>C++</strong> ad attuare il<br />
polimorfismo: in programmi in formato eseguibile, i nomi degli oggetti e delle<br />
funzioni sono spar<strong>it</strong>i, e sono rimasti solo indirizzi e istruzioni. In altre parole,<br />
come fa il programma a sapere, in esecuzione, su quale oggetto applicare una<br />
funzione?<br />
In realtà il compilatore trasforma il codice sorgente, introducendo un puntatore<br />
costante "nascosto" (identificato dalla parola-chiave this) ogni volta che<br />
incontra la chiamata di una funzione-membro, e inserendo lo stesso<br />
puntatore come primo argomento nella funzione.<br />
Chiariamo quanto detto con il seguente esempio, in cui ogg è un'istanza di una<br />
certa classe myclass e in<strong>it</strong>() è una funzione-membro che utilizza un datomembro<br />
x, entrambi della stessa classe myclass:<br />
la definizione della funzione:<br />
void myclass::in<strong>it</strong>() {..... x = .....}<br />
viene trasformata in: void in<strong>it</strong>(myclass* const this) {..... this-<br />
>x = .....}<br />
e quindi .....<br />
l'istruzione di chiamata della<br />
funzione:<br />
ogg.in<strong>it</strong>( ) ;<br />
viene tradotta in: in<strong>it</strong>(&ogg) ;<br />
Come si può notare dall'esempio, il puntatore nascosto this punta all'oggetto<br />
utilizzato dalla funzione. Il programmatore non é tenuto a conoscerlo, tuttavia,<br />
se vuole, può utilizzarlo in sola lettura (per esempio, in una funzione che deve<br />
rest<strong>it</strong>uire l'oggetto stesso, può usare l'istruzione return *this; ).<br />
Nel caso che la funzione abbia degli argomenti, il puntatore this viene<br />
inser<strong>it</strong>o per primo, e gli altri argomenti vengono spostati in avanti di una<br />
posizione.<br />
Se la funzione è un metodo in sola lettura, il compilatore trasforma la sua<br />
definizione nel seguente modo (per esempio):<br />
int myclass::get( ) const ----------> int get(const myclass* const<br />
this)<br />
cioè this diventa un puntatore costante a costante. Questo fa sì che si<br />
possano definire due metodi identici, l'uno const e l'altro no, perchè in realtà i<br />
tipi del primo argomento sono diversi (e quindi l'overload è ammissibile).
L'introduzione del puntatore this spiega l'apparente "stranezza" di istruzioni<br />
come ogg.in<strong>it</strong>() (in realtà il codice della funzione in memoria é uno solo, cioè<br />
non ne esiste uno per ogni oggetto come per i dati-membro). Pertanto, le<br />
operazioni di accesso ai membri di un oggetto (con gli operatori . e ->),<br />
producono risultati diversi se il right-operand è un dato-membro o una<br />
funzione-membro:<br />
• se il right-operand è un dato-membro (per esempio in un'operazione<br />
tipo ogg.x) il programma accede effettivamente alla memoria in cui è<br />
localizzato il membro x dell'oggetto ogg;<br />
• se il right-operand è una funzione-membro (per esempio in<br />
ogg.in<strong>it</strong>()), il programma esegue la funzione in<strong>it</strong> (che è unica per tutta<br />
la classe), aggiungendo, come primo argomento della funzione,<br />
l'indirizzo dell'oggetto ogg.
Membri a livello di classe e accesso "friend"<br />
Membri di tipo enumerato<br />
Ricordiamo che un oggetto é di tipo enumerato se può assumere solo un<br />
defin<strong>it</strong>o e lim<strong>it</strong>ato insieme di valori interi, detti enumeratori.<br />
Quando un tipo enumerato é defin<strong>it</strong>o all'interno di una classe, il tipo stesso é<br />
identificato esternamente dal suo nome preceduto dal nome della classe con il<br />
sol<strong>it</strong>o operatore :: di risoluzione di visibil<strong>it</strong>à. La stessa regola vale quando si<br />
accede separatamente a un singolo enumeratore.<br />
Chiariamo quanto detto con un esempio: definiamo una classe A, contenente la<br />
definizione del tipo enumerato festivo, con enumeratori Sabato e<br />
Domenica, e un membro giorno, di tipo festivo:<br />
class A { public: enum festivo { Sabato, Domenica} giorno; };<br />
vediamo ora vari modi di utilizzo nel programma:<br />
1. A::festivo oggi = A::Sabato ;<br />
crea l'oggetto oggi, istanza del tipo enumerato festivo della classe A<br />
e lo inizializza con il valore dell'enumeratore Sabato;<br />
2. A a; a.giorno = A::Sabato; ... oppure ... a.giorno = oggi;<br />
crea l'oggetto a, istanza della classe A e assegna il valore<br />
dell'enumeratore Sabato (oppure dell'oggetto oggi dell'esempio<br />
precedente) al membro giorno dell'oggetto a;<br />
3. int domani = A::Domenica ;<br />
crea l'intero domani e lo inizializza con il valore dell'enumeratore<br />
Domenica (conversione di tipo implic<strong>it</strong>a); questa istruzione é ammessa<br />
anche se non sono state create istanze di A o di festivo.<br />
Da questi esempi si può notare, fra l'altro, che gli enumeratori sono identificati<br />
dalla classe e non dal tipo enumerato a cui appartengono: ne consegue che<br />
non possono esistere due enumeratori con lo stesso nome defin<strong>it</strong>i nella stessa<br />
classe (anche se in due tipi enumerati diversi), mentre possono esistere due<br />
enumeratori con lo stesso nome defin<strong>it</strong>i in due classi diverse.<br />
Notiamo inoltre, esaminando la definizione della classe A, che:<br />
• il tipo enumerato festivo é stato defin<strong>it</strong>o nella sezione pubblica: se<br />
così non fosse, sarebbe accessibile, come di regola, solo dai metodi di A;<br />
• le specificazioni del tipo enumerato (festivo) e del membro di A di tipo<br />
festivo (giorno) sono opzionali: si possono omettere quando nel<br />
programma si usano solo gli enumeratori (come nell'esempio 3):<br />
class A { public: enum { Sabato,<br />
Domenica} ; };<br />
questo è in realtà l'uso più frequente che si fa dei tipi enumerati<br />
all'interno di una classe: si definisce e si utilizza una serie di<br />
enumeratori, a livello di classe e non dei singoli oggetti
Dati-membro statici<br />
In <strong>C++</strong> la parola-chiave static ha un ulteriore significato: se un datomembro<br />
di una classe è dichiarato static, la variabile è unica per tutta la<br />
classe, indipendentemente dal numero di istanze della classe. In altre parole, il<br />
<strong>C++</strong> riserva un'area di memoria per ogni oggetto, salvo per i membri static, a<br />
ciascuno dei quali corrisponde un'unica locazione.<br />
Pertanto i membri static appartengono alla classe e non ai singoli oggetti. Per<br />
individuarli si usa il nome della classe con l'operatore ::<br />
Esempio: se sm è un membro static di una classe A, la "variabile" sm è<br />
individuata dal costrutto: A::sm<br />
I membri static non vengono creati tram<strong>it</strong>e istanze della classe a cui<br />
appartengono, ma devono essere defin<strong>it</strong>i direttamente, nello stesso amb<strong>it</strong>o in<br />
cui è defin<strong>it</strong>a la classe. Nei rari casi, però, in cui la classe è defin<strong>it</strong>a in un<br />
block scope, i membri static non sono ammessi. Pertanto un membro static<br />
può essere defin<strong>it</strong>o solo in un namespace (se la classe è defin<strong>it</strong>a in quel<br />
namespace) o nel namespace globale. Di default un membro static è<br />
inizializzato con zero (in modo appropriato al tipo), come tutte le variabili<br />
statiche e globali.<br />
Esempio (supponiamo che la classe sia defin<strong>it</strong>a nel namespace globale):<br />
class A {<br />
..................<br />
static int sm ;<br />
..................<br />
};<br />
(sm è un membro static della classe A, che può essere<br />
privato o pubblico ; se è privato, è gestibile solo da un<br />
metodo della classe A, pur essendo una variabile statica)<br />
int A::sm = 10 ; (a questo punto definisce e inizializza, con operazione<br />
nell'amb<strong>it</strong>o globale, la variabile statica: A::sm)<br />
int main ( )<br />
ecc...<br />
I membri static sono molto utili per gestire informazioni comuni a tutti gli<br />
oggetti di una classe (per esempio possono fornire i dati di default per<br />
l'inizializzazione degli oggetti), ma nel contempo, essendo essi stessi membri<br />
della classe, permettono di ev<strong>it</strong>are il ricorso a variabili esterne, salvaguardando<br />
così il data hiding e l'indipendenza del codice di implementazione della classe<br />
dalle altre parti del programma.<br />
NOTA: la principale differenza di significato dello specificatore static, se<br />
applicato a un membro o a un oggetto di una classe, consiste nel fatto che,<br />
nel primo caso, si crea una variabile nell'amb<strong>it</strong>o di una classe (che deve<br />
appartenere a sua volta a un namespace o al namespace globale), nel
secondo si crea una variabile locale nell'amb<strong>it</strong>o di un blocco; in entrambi i casi<br />
il lifetime della variabile persiste fino alla fine del programma. Se invece static è<br />
applicato a un oggetto non locale (da ev<strong>it</strong>are, meglio ricorrere al namespace<br />
anonimo), il suo significato è completamente diverso (visibil<strong>it</strong>à lim<strong>it</strong>ata al file<br />
scope).<br />
NOTA2: per i motivi anzidetti, l'attributo static di un membro di una classe<br />
deve essere specificato soltanto nella dichiarazione e non nella definizione,<br />
perchè in quest'ultima assumerebbe il significato di lim<strong>it</strong>are la sua visibil<strong>it</strong>à al<br />
file scope.<br />
Funzioni-membro statiche<br />
Anche le funzioni-membro di una classe possono essere dichiarate static.<br />
Es.:<br />
class A { .....<br />
static int conta( ) ;<br />
(prototipo)<br />
..... };<br />
int A::conta( ) { ..... }<br />
(definizione)<br />
Nel prog. chiamante:<br />
int n = A::conta( );<br />
come si può notare dall'esempio, nella chiamata di una funzione-membro<br />
static, bisogna qualificare il suo nome con quello della classe di appartenenza.<br />
Notare inoltre che, nella definizione della funzione, lo specificatore static<br />
non va messo (per lo stesso motivo per cui non va messo davanti alla<br />
definizione di un dato-membro static).<br />
Una funzione-membro static (che, come tutti gli altri membri, può essere<br />
privata o pubblica), accede ai membri della classe ma non è collegata a un<br />
oggetto in particolare e quindi non ha il puntatore nascosto this. Ne consegue<br />
che, se deve operare su oggetti, questi devono essere trasmessi esplic<strong>it</strong>amente<br />
come argomenti.<br />
Normalmente i metodi static vengono usati per trattare dati-membro static o,<br />
in generale, quando non si pone la necess<strong>it</strong>à di operare su un singolo oggetto<br />
della classe (cioè quando la presenza del puntatore nascosto this sarebbe un<br />
sovraccarico inutile). Viceversa, quando un metodo deve operare direttamente su<br />
un oggetto (uno e uno solo alla volta), è più conveniente che sia incapsulato<br />
nell'oggetto stesso e quindi non venga dichiarato static.<br />
Funzioni friend
Una normale dichiarazione di un metodo specifica tre cose logicamente<br />
distinte:<br />
1. la funzione può accedere ai membri privati della classe;<br />
2. la funzione è nell' amb<strong>it</strong>o di visibil<strong>it</strong>à della classe;<br />
3. la funzione è incapsulata negli oggetti (possiede il puntatore this).<br />
Abbiamo visto che, dichiarando un metodo con lo specificatore static, è<br />
possibile fornire alla funzione le prime due proprietà, ma non la terza. Se invece<br />
dichiariamo una funzione con lo specificatore friend, è possibile fornirle solo<br />
la prima proprietà.<br />
Una funzione si dice "friend" di una classe, se è defin<strong>it</strong>a in un amb<strong>it</strong>o diverso<br />
da quello della classe, ma può accedere ai suoi membri privati. Per ottenere<br />
ciò, bisogna inserire il prototipo della funzione nella definizione della classe<br />
(non importa se nella sezione privata o pubblica), facendo precedere lo<br />
specificatore friend.<br />
Es.: DEFINIZIONE CLASSE DEFINIZIONE FUNZIONE<br />
class A { void amica(A ogg, .....)<br />
int mp ; .......... {<br />
friend void amica(A, .....) ; ........ ogg.mp ........<br />
........ }; }<br />
la funzione amica, che non è un metodo della classe A (nell'esempio è<br />
defin<strong>it</strong>a nel namespace globale), è tuttavia dichiarata con lo specificatore<br />
friend nella definizione della classe A, e quindi può accedere al suo membri<br />
privati (nell'esempio, a mp). Notare che la funzione, essendo priva del<br />
puntatore this (come i metodi static), può operare sugli oggetti della classe<br />
solo se gli oggetti interessati le sono trasmessi come argomenti.<br />
Se una stessa funzione è friend di due o più classi, il suo prototipo preceduto<br />
da friend va inser<strong>it</strong>o nelle definizioni di tutte le classi interessate. Sorge allora<br />
un problema, come si può vedere dall'esempio seguente:<br />
class A {...friend int fun(A,B, .....);...};
piuttosto che a un'altra. In ogni caso, per favorire la programmazione<br />
modulare, è consigliabile aggregare in uno stesso amb<strong>it</strong>o (per esempio in un<br />
namespace) classi e funzioni friend collegate.<br />
Classi friend<br />
Quando tutte le funzioni-membro di una classe B sono friend di una classe<br />
A, è possibile, anziché dichiarare ciascuna funzione individualmente, inserire<br />
una sola dichiarazione in A, indicante che l'intera classe B è friend:<br />
class A {..........friend class B;..........};<br />
L'uso di funzioni e classi friend permette al <strong>C++</strong> di aggirare il data hiding<br />
ogni volta che classi diverse devono interagire strettamente o condividere gli<br />
stessi dati, pur restando distinte.<br />
C'è da dire infine che le relazioni di tipo friend non sono simmetriche (se A è<br />
friend di B non è detto che B sia friend di A), né trans<strong>it</strong>ive (se A è friend di B<br />
e B è friend di C, non è detto che A sia friend di C). In sostanza ogni relazione<br />
deve essere esplic<strong>it</strong>amente dichiarata.
Costruttori e distruttori degli oggetti<br />
Costruzione e distruzione di un oggetto<br />
Abbiamo detto più volte che quando un oggetto, istanza di un tipo nativo o<br />
astratto, viene creato, si dice che quell'oggetto è costru<strong>it</strong>o. Analogamente,<br />
quando l'oggetto cessa di esistere, si dice che quell'oggetto è distrutto.<br />
Vediamo le varie circostanze in cui un oggetto può essere costru<strong>it</strong>o o<br />
distrutto:<br />
1. Un oggetto automatico (cioè locale non statico) viene costru<strong>it</strong>o ogni<br />
volta che la sua definizione viene incontrata durante l'esecuzione del<br />
programma, e distrutto ogni volta che il programma esce dall'amb<strong>it</strong>o in<br />
cui tale definizione si trova.<br />
2. Un oggetto locale statico viene costru<strong>it</strong>o la prima volta che la sua<br />
definizione viene incontrata durante l'esecuzione del programma, e<br />
distrutto una sola volta, quando il programma termina.<br />
3. Un oggetto allocato nella memoria dinamica (area heap ) viene<br />
costru<strong>it</strong>o mediante l'operatore new e distrutto mediante l'operatore<br />
delete.<br />
4. Un oggetto, membro non statico di una classe, viene costru<strong>it</strong>o ogni<br />
volta che (o meglio, immediatamente prima che) viene costru<strong>it</strong>o un<br />
oggetto della classe di cui è membro, e distrutto ogni volta che (o<br />
meglio, immediatamente dopo che) lo stesso oggetto viene distrutto.<br />
5. Un oggetto, elemento di un array, viene costru<strong>it</strong>o o distrutto ogni<br />
volta che l'array di cui fa parte viene costru<strong>it</strong>o o distrutto.<br />
6. Un oggetto globale, un oggetto di un namespace o un membro<br />
statico di una classe, viene costru<strong>it</strong>o una sola volta, alla "partenza" del<br />
programma e distrutto quando il programma termina.<br />
7. Infine, un oggetto temporaneo viene costru<strong>it</strong>o per memorizzare<br />
risultati parziali durante la valutazione di un'espressione, e distrutto<br />
alla fine dell'espressione completa in cui compare.<br />
Come si può notare, la costruzione o distruzione di un oggetto può avvenire<br />
in momenti diversi, in base alla categoria dell'oggetto che si sta considerando. In<br />
ogni caso, sia durante la costruzione che durante la distruzione, potrebbero<br />
rendersi necessarie delle operazioni specifiche. Per esempio, se un membro di<br />
una classe è un puntatore, potrebbe essere necessario creare l'area puntata<br />
(che non viene fatto automaticamente, come nel caso degli array) e allocarla<br />
dinamicamente con l'operatore new; quest'area dovrà però essere rilasciata,<br />
prima e poi (con l'operatore delete), e cap<strong>it</strong>a non di rado che non lo si possa<br />
fare prima della distruzione dell'oggetto. Poichè d'altra parte un oggetto può<br />
anche essere costru<strong>it</strong>o o distrutto automaticamente, si pone il problema di<br />
come "intercettare" il momento della sua costruzione o distruzione.<br />
Nel caso che gli oggetti siano istanze di una classe, il <strong>C++</strong> mette a<br />
disposizione un mezzo molto potente, che consiste nella possibil<strong>it</strong>à di definire dei<br />
particolari metodi della classe, che il programma riconosce come funzioni da<br />
eseguire al momento della costruzione o distruzione di un oggetto. Questi
metodi prendono il nome di costruttori e distruttori degli oggetti. Il loro<br />
scopo principale è, per i costruttori, di inizializzare i membri e/o allocare<br />
risorse, per i distruttori, di rilasciare le risorse allocate.<br />
Costruttori<br />
I costruttori degli oggetti devono sottostare alle seguenti regole (ci rifaremo al<br />
sol<strong>it</strong>o esempio della classe point):<br />
1. devono avere lo stesso nome della classe<br />
prototipo: point(......);<br />
definizione esterna: point::point(......) {......}<br />
2. non bisogna specificare il tipo di r<strong>it</strong>orno (neanche void)<br />
NOTA: in realtà la chiamata di un costruttore può anche essere inser<strong>it</strong>a<br />
in un'espressione; ciò significa che un costruttore r<strong>it</strong>orna "qualcosa" e<br />
precisamente .... l'oggetto che ha appena creato!<br />
3. ammettono argomenti e defaults; i costruttori senza argomenti (o<br />
con tutti argomenti di default) sono detti: "costruttori di default"<br />
prototipo di costruttore di default della classe point: point( );<br />
prototipo di costruttore della classe point con un argomento<br />
required e uno di default:<br />
point(double,double=0.0);<br />
4. possono esistere più costruttori, in overload, in una stessa classe. Il<br />
<strong>C++</strong> li distingue in base alla lista degli argomenti. Come tutte le<br />
funzioni in overload, non sono ammessi costruttori che differiscano<br />
solo per gli argomenti di default.<br />
5. devono essere dichiarati come funzioni-membro pubbliche, in quanto<br />
sono sempre chiamati dall'esterno della classe a cui appartengono.<br />
I costruttori non sono obbligatori: se una classe non ne possiede, il <strong>C++</strong><br />
fornisce un costruttore di default con "corpo nullo" .<br />
Il costruttore di default (dichiarato nella classe oppure forn<strong>it</strong>o dal <strong>C++</strong>)<br />
viene esegu<strong>it</strong>o automaticamente nel momento in cui l'oggetto viene creato nel<br />
programma (si vedano i vari casi elencati nella sezione precedente). Esempio :<br />
definizione del costruttore di default di<br />
point:<br />
definizione dell'oggetto p, istanza di point: point p ;<br />
point::point( ) {x=3.5;<br />
y=2.1;}
nel momento in cui è l'esegu<strong>it</strong>a l'istruzione di definizione dell'oggetto p, il<br />
costruttore di default va in esecuzione automaticamente, inizializzando p con<br />
3.5 nel membro x e 2.1 nel membro y.<br />
Se invece in una classe esiste almeno un costruttore con argomenti, il <strong>C++</strong><br />
non mette a disposizione alcun costruttore di default e perciò questo, se<br />
necessario, va esplic<strong>it</strong>amente defin<strong>it</strong>o come metodo della classe. In sua<br />
assenza, i costruttori con argomenti non vengono invocati automaticamente e<br />
pertanto ogni istruzione del programma che determini, direttamente o<br />
indirettamente, la creazione di un oggetto, deve contenere la chiamata esplic<strong>it</strong>a<br />
di uno dei costruttori disponibili, nel modo che dipende dalla categoria<br />
dell'oggetto interessato. Esamineremo i vari casi separatamente, rifacendoci<br />
all'elenco illustrato nella sezione precedente.<br />
Per il momento consideriamo il caso più frequente, che è quello di un oggetto<br />
singolo creato direttamente mediante la definizione del suo nome (casi 1., 2. e<br />
6.): i modi possibili per invocare un costruttore con argomenti sono due, come<br />
è mostrato dal seguente esempio:<br />
definizione del costruttore di point : point::point(double x0, double<br />
y0)<br />
{x=x0; y=y0;}<br />
definizione dell'oggetto p, istanza di<br />
point :<br />
prima forma : point p (3.5, 2.1);<br />
seconda forma : point p = point(3.5, 2.1);<br />
la prima forma è più concisa, ma la seconda è più chiara, in quanto ha proprio<br />
l'aspetto di una inizializzazione tram<strong>it</strong>e chiamata esplic<strong>it</strong>a di una funzione. In<br />
entrambi i casi viene invocato un costruttore con due argomenti di tipo<br />
double, che inizializza p inserendo i valori dei due argomenti rispettivamente<br />
nel membro x e nel membro y. Aggiungiamo che la chiamata esplic<strong>it</strong>a può<br />
essere utilizzata anche per invocare un costruttore di default (è necessaria,<br />
per esempio, quando l'oggetto è creato all'interno di un'espressione), per<br />
esempio:<br />
throw Error( ) ;<br />
(solleva un'eccezione e trasmette un oggetto della classe Error, creato con il<br />
costruttore di default).<br />
Terminiamo questa sezione osservando che anche i tipi nativi hanno i loro<br />
costruttori di default (sebbene di sol<strong>it</strong>o non si usino), che però, quando<br />
servono, vanno esplicimente chiamati, come nel seguente esempio:<br />
int i = int();<br />
i costruttori di default dei tipi nativi inizializzano le variabili con zero (in<br />
modo appropriato al tipo). Sono utili quando si ha a che fare con tipi<br />
parametrizzati (come i template, che vedremo più avanti), in cui non è noto a<br />
priori se al parametro verrà sost<strong>it</strong>u<strong>it</strong>o un tipo nativo o un tipo astratto.
Costruttori e conversione implic<strong>it</strong>a<br />
Un'attenzione particolare mer<strong>it</strong>a il costruttore con un solo argomento. In<br />
questo caso, infatti, il costruttore definisce anche una conversione implic<strong>it</strong>a di<br />
tipo dal tipo dell'argomento a quello della classe (ovviamente, spetta al codice<br />
di implementazione del costruttore assicurare che la conversione venga esegu<strong>it</strong>a<br />
in modo corretto). Esempio:<br />
definizione del costruttore di point : point::point(double d)<br />
{x=d; y=d;}<br />
definizione dell'oggetto p, istanza di point :<br />
point p = 3;<br />
è equivalente a : point p = point(3.0);<br />
Notare che il numero 3 (che è di tipo int) è convert<strong>it</strong>o implic<strong>it</strong>amente, prima a<br />
double, e poi nel tipo point (tram<strong>it</strong>e esecuzione del costruttore, che lo utilizza<br />
per inizializzare l'oggetto p). Notare anche (per "chiudere il cerchio") che<br />
un'espressione del tipo point(3.0) è formalmente identica a un'operazione di<br />
casting in function-style (è persino ammessa la forma in C-style !).<br />
Le conversioni implic<strong>it</strong>e sono molto utili nella definizione degli operatori in<br />
overload (come vedremo prossimamente).<br />
La conversione implic<strong>it</strong>a può essere esclusa premettendo, nella dichiarazione<br />
(non nella definizione esterna) del costruttore lo specificatore explic<strong>it</strong> :<br />
explic<strong>it</strong> point(double);<br />
il casting continua invece ad essere ammesso (anche nella forma in C-style), in<br />
quanto coincide puramente con la chiamata del costruttore.<br />
Distruttori<br />
I distruttori degli oggetti devono sottostare alle seguenti regole (ci rifaremo al<br />
sol<strong>it</strong>o esempio della classe point):<br />
1. devono avere lo stesso nome della classe preceduto da una tilde (~)
prototipo: ~point( );<br />
definizione esterna: point::~point( ) {......}<br />
2. non bisogna specificare il tipo di r<strong>it</strong>orno (neanche void)<br />
3. non ammettono argomenti<br />
4. ciascuna classe può avere al massimo un distruttore<br />
5. devono essere dichiarati come funzioni-membro pubbliche, in quanto<br />
sono sempre chiamati dall'esterno della classe a cui appartengono.<br />
Come i costruttori, i distruttori non sono obbligatori; sono richiesti quando è<br />
necessario liberare risorse allocate dagli oggetti o ripristinare le condizioni<br />
preestistenti alla loro creazione. Se esiste, un distruttore è sempre chiamato<br />
automaticamente ogni volta che l'oggetto di cui fa parte sta per essere<br />
distrutto.<br />
Quando più oggetti sono costru<strong>it</strong>i in sequenza, e poi sono distrutti<br />
contemporaneamente (per esempio se sono oggetti automatici che escono dal<br />
loro amb<strong>it</strong>o di visibil<strong>it</strong>à), i loro distruttori sono normalmente esegu<strong>it</strong>i in<br />
sequenza inversa a quella di costruzione.<br />
Oggetti allocati dinamicamente<br />
Se il programma non definisce direttamente un oggetto, ma un suo puntatore,<br />
il costruttore non entra in azione al momento della definizione del puntatore,<br />
bensì quando viene allocata dinamicamente la memoria per l'oggetto (caso 3.<br />
dell'elenco). Sol<strong>it</strong>o esempio:<br />
point* ptr; costruisce la "variabile" puntatore ma non l'area puntata<br />
ptr = new point; costruisce l'area puntata<br />
la seconda istruzione dell'esempio esegue varie cose in una sola volta:<br />
• alloca memoria dinamica per un oggetto della classe point<br />
• assegna l'indirizzo dell'oggetto, rest<strong>it</strong>u<strong>it</strong>o dall'operatore new, al<br />
puntatore ptr<br />
• inizializza l'oggetto eseguendo il costruttore di default della classe<br />
point<br />
Quando si vuole che nella creazione di un oggetto sia esegu<strong>it</strong>o un costruttore<br />
con argomenti, bisogna aggiungere, nell'istruzione di allocazione della<br />
memoria, l'elenco dei valori degli argomenti (fra parentesi tonde):<br />
ptr = new point (3.5, 2.1);
questa istruzione cerca, fra i costruttori della classe point, quello con due<br />
argomenti di tipo double, e lo esegue al posto del costruttore di default .<br />
Se si alloca dinamicamente un array di oggetti, sappiamo che la<br />
dimensione dell'array va specificata fra parentesi quadre dopo il nome della<br />
classe. Poichè il costruttore chiamato è unico per tutti gli elementi<br />
dell'array, questi vengono tutti inizializzati nello stesso modo. Nessun problema<br />
se si usa il costruttore di default (purchè sia disponibile):<br />
ptr = new point [10];<br />
ma, quando si vuole usare un costruttore con argomenti:<br />
ptr = new point [10] (3.5, 2.1);<br />
non sempre l'istruzione viene esegu<strong>it</strong>a correttamente: anz<strong>it</strong>utto alcuni compilatori<br />
più antichi (come il Visual <strong>C++</strong>, vers. 6) non l'accettano; quelli che l'accettano<br />
la eseguono bene se il tipo è astratto (come nell'esempio di cui sopra), ma se il<br />
tipo è nativo, per es.:<br />
ptr = new int [10] (3);<br />
disponendo solo del costruttore di default, tutti gli elementi dell'array<br />
vengono comunque inizializzati con 0 (cioè la parte dell'istruzione fra parentesi<br />
tonde viene ignorata).<br />
Gli oggetti allocati dinamicamente non sono mai distrutti in modo<br />
automatico. Per ottenere che vengano distrutti, bisogna usare l'operatore<br />
delete. Es. (al sol<strong>it</strong>o ptr punta a oggetti della classe point):<br />
delete ptr; (per un singolo oggetto) delete [ ] ptr; (per un<br />
array)<br />
a questo punto viene esegu<strong>it</strong>o, per ogni oggetto, il distruttore della classe<br />
point (se esiste) .<br />
Membri puntatori<br />
Una particolare attenzione va rivolta alla programmazione dei costruttori e del<br />
distruttore di un oggetto che contiene membri puntatori.<br />
Infatti, a differenza dal caso degli array, l'area puntata non è defin<strong>it</strong>a<br />
automaticamente e quindi (a meno che al puntatore non venga successivamente<br />
assegnato l'indirizzo di un'area già esistente) cap<strong>it</strong>a quasi sempre che l'area<br />
debba essere allocata nella memoria heap. e che questa operazione venga<br />
esegu<strong>it</strong>a proprio da un costruttore dell'oggetto.<br />
Analogamente, quando l'oggetto è distrutto (per esempio se è un oggetto<br />
automatico che va out of scope), sono del pari distrutti tutti i suoi membri,<br />
compresi i membri puntatori, ma non le aree puntate, che continuano ad<br />
esistere senza essere più raggiungibili (errore di memory leak).
Pertanto è indispensabile che sia lo stesso distruttore dell'oggetto a incaricarsi<br />
di distruggere esplic<strong>it</strong>amente le aree puntate, cosa che può essere fatta<br />
solamente usando l'operatore delete. Esempio:<br />
CLASSE COSTRUTTORE DISTRUTTORE<br />
class Persona { Persona::Persona (int n) Persona::~Persona ( )<br />
char* nome; { {<br />
char* cognome; nome = new char [n]; delete [ ] nome;<br />
public: cognome = new char [n]; delete [ ] cognome;<br />
Persona (int); } }<br />
~Persona ( ); DEFINIZIONE DELL'OGGETTO NEL PROGRAMMA<br />
.... altri metodi }; Persona Tizio(25);<br />
l'oggetto Tizio, istanza della classe Persona, viene costru<strong>it</strong>o<br />
automaticamente nella memoria stack, e così pure i suoi membri. In aggiunta, il<br />
costruttore dell'oggetto alloca nella memoria heap due aree di 25 byte, e<br />
sistema i rispettivi indirizzi nei membri puntatori Tizio.nome e<br />
Tizio.cognome. Quando l'oggetto Tizio va out of scope, il distruttore entra<br />
in azione automaticamente e, con l'operatore delete, libera la memoria heap<br />
allocata per le due aree. Senza il distruttore, sarebbe stata liberata soltanto la<br />
memoria stack occupata dall'oggetto Tizio e dai suoi membri puntatori , ma<br />
non l'area heap indirizzata da questi.<br />
Costruttori di copia<br />
I costruttori di copia sono particolari costruttori che vengono esegu<strong>it</strong>i quando<br />
un oggetto é creato per copia. Ricordiamo brevemente in quali casi ciò si<br />
verifica:<br />
• definizione di un oggetto e sua inizializzazione tram<strong>it</strong>e un oggetto<br />
esistente dello stesso tipo;<br />
• passaggio by value di un argomento a una funzione;<br />
• rest<strong>it</strong>uzione by value del valore di r<strong>it</strong>orno di una funzione;<br />
• passaggio di un'eccezione al costrutto catch.<br />
Un costruttore di copia deve avere un solo argomento, dello stesso tipo<br />
dell'oggetto da costruire; l'argomento (che rappresenta l'oggetto esistente)<br />
deve essere dichiarato const (per sicurezza) e passato by reference (altrimenti<br />
si creerebbe una copia della copia!). Riprendendo il sol<strong>it</strong>o esempio, il costruttore<br />
di copia della classe point é:<br />
point::point(const point& q) {......}<br />
e viene chiamato automaticamente ogni volta che si verifica una delle quattro<br />
circostanze sopraelencate.
Per esempio, se definiamo un oggetto p e lo inizializziamo con un oggetto<br />
preesistente q:<br />
point p = q ;<br />
questa istruzione aziona il costruttore di copia, a cui é trasmesso q come<br />
argomento.<br />
I costruttori di copia, come ogni altro costruttore, non sono obbligatori: se<br />
una classe non ne possiede, il <strong>C++</strong> fornisce un costruttore di copia di default<br />
che esegue la copia membro a membro. Questo può essere soddisfacente nella<br />
maggioranza dei casi. Tuttavia, se la classe possiede dei membri puntatori,<br />
l'azione di default copia i puntatori, ma non le aree puntate: alla fine si<br />
r<strong>it</strong>rovano due oggetti i cui rispettivi membri puntatori puntano alla stessa area.<br />
Ciò potrebbe essere pericoloso, perché, se viene chiamato il distruttore di uno<br />
dei due oggetti, il membro puntatore dell'altro, che esiste ancora, punta a<br />
un'area che non esiste più (errore di dangling references).<br />
Nell'esempio seguente una classe di nome A contiene, fra l'altro, un membro<br />
puntatore a int e un costruttore di copia che esegue le operazioni idonee ad<br />
ev<strong>it</strong>are l'errore di cui sopra:<br />
CLASSE COSTRUTTORE DI COPIA<br />
class A { A::A(const A& a)<br />
int* pa; {<br />
public: pa = new int ;<br />
A(const A&); *pa = *a.pa ;<br />
........ }; }<br />
in questo modo, a segu<strong>it</strong>o della creazione di un oggetto a2 per copia da un<br />
esistente oggetto a1:<br />
A a2 = a1;<br />
il costruttore di copia fa si che la variabile puntata *a1.pa venga copiata in<br />
*a2.pa; senza il costruttore sarebbe copiato il puntatore a1.pa in a2.pa.<br />
Liste di inizializzazione<br />
Quando un costruttore deve, fra l'altro, inizializzare i membri della propria<br />
classe, lo può fare tram<strong>it</strong>e una lista di inizializzazione (introdotta dal segno ":"<br />
e inser<strong>it</strong>a nella definizione del costruttore dopo la lista degli argomenti), la<br />
quale sost<strong>it</strong>uisce le istruzioni di assegnazione (in effetti un costruttore non<br />
dovrebbe assegnare bensì solo inizializzare, anche se la distinzione può<br />
sembrare solo formale).<br />
La sintassi di una lista di inizializzazione si desume dal seguente esempio:
class A {<br />
CLASSE<br />
int m1, m2; {<br />
COSTRUTTORE<br />
A::A(int p, double q) : m1(p), m2(0),<br />
r(q)<br />
double r; .... eventuali altre operazioni....<br />
public: }<br />
A(int,double);<br />
........ };<br />
Notare che alcuni membri possono essere inizializzati con valori costanti, altri<br />
con i valori degli argomenti passati al costruttore. L'ordine nella lista è<br />
indifferente; in ogni i caso i membri sono costru<strong>it</strong>i e inizializzati nell'ordine in<br />
cui appaiono nella definizione della classe.<br />
E' buona norma utilizzare le liste di inizializzazione ogni volta che é possibile. Il<br />
loro uso é indispensabile quando esistono membri della classe dichiarati const<br />
o come riferimenti, per i quali l'inizializzazione è obbligatoria.<br />
Membri oggetto<br />
Riprendiamo ora ad esaminare l'elenco presentato all'inizio di questo cap<strong>it</strong>olo e<br />
consideriano la costruzione e distruzione degli oggetti, quando sono membri<br />
non statici di una classe (caso 4. dell'elenco).<br />
Sappiamo già che una classe può avere anche tipi classe fra i suoi membri;<br />
per esempio:<br />
class A { class C {<br />
int aa; ........ }; A ma;<br />
class B { B mb;<br />
int bb; ........ }; int mc; ........ };<br />
La classe C del nostro esempio viene detta classe composta, in quanto<br />
contiene, fra i suoi membri, oggetti di altre classi (il membro-oggetto ma<br />
della classe A e il membro-oggetto mb della classe B).<br />
Sappiamo inoltre che, creata un'istanza cc di C, le variabili corrispondenti ai<br />
singoli membri vanno indicate nel programma con espressioni del tipo: cc.ma.aa<br />
oppure cc.mb.bb (dir<strong>it</strong>ti di accesso permettendo).
Nel momento in cui un oggetto di una classe composta sta per essere<br />
costru<strong>it</strong>o, e prima ancora che il suo costruttore completi l'operazione, sono<br />
esegu<strong>it</strong>i automaticamente i costruttori che inizializzano i membri delle classi<br />
componenti. Se esistono e si vogliono utilizzare i costruttori di default, non<br />
esiste problema. Ma se deve essere chiamato un costruttore con argomenti, ci<br />
si chiede in che modo tali argomenti possano essere passati, visto che il<br />
costruttore di un membro-oggetto non è chiamato esplic<strong>it</strong>amente.<br />
In questi casi, spetta al costruttore della classe composta provvedere a che<br />
vengano esegu<strong>it</strong>i correttamente anche i costruttori delle classi componenti.<br />
Per ottenere ciò, deve includere, nella sua lista di inizializzazione, tutti (e soli) i<br />
membri-oggetto che non utilizzano il proprio costruttore di default, ciascuno<br />
con i valori di inzializzazione che corrispondono esattamente (cioè con gli stessi<br />
tipi e nello stesso ordine) alla lista degli argomenti del rispettivo costruttore.<br />
Segu<strong>it</strong>ando con il nostro esempio:<br />
costruttore di A : A::A(int x) : aa(x) { ........ }<br />
costruttore di B : B::B(int x) : bb(x) { ........ }<br />
costruttore di C : C::C(int x, int y, int z) : ma(z), mb(x), mc(y) { ........ }<br />
Le classi componenti A e B hanno anche una loro v<strong>it</strong>a autonoma e in<br />
particolare possono essere istanziate con oggetti propri. In questo caso il<br />
costruttore di C può generare i suoi membri-oggetto copiando oggetti già<br />
costru<strong>it</strong>i delle classi componenti. Riprendendo l'esempio, un'altra forma del<br />
costruttore di C potrebbe essere:<br />
}<br />
C::C(int x, const A& a, const B& b) : ma(a), mb(b), mc(x) { ........<br />
dove gli argomenti a e b corrispondono a istanze già create rispettivamente di<br />
A e di B; in tale caso viene esegu<strong>it</strong>o il costruttore di copia, se esiste, oppure di<br />
default viene fatta la copia membro a membro.<br />
Quando un oggetto di una classe composta viene distrutto, vengono<br />
successivamente e automaticamente distrutti tutti i membri delle classi<br />
componenti, in ordine inverso a quello della loro costruzione.<br />
Array di oggetti<br />
Gli elementi di un array di oggetti (caso 5. dell'elenco iniziale) vengono<br />
inizializzati, tram<strong>it</strong>e il costruttore della classe comune di appartenenza, non<br />
appena l'array è defin<strong>it</strong>o.
Come al sol<strong>it</strong>o, non esiste nessun problema se si utilizza il costruttore di<br />
default:<br />
point pt[5];<br />
(costruisce 5 oggetti della classe point, invocando, per ciascuno di essi, il<br />
costruttore di default).<br />
Se invece si vuole (o si deve, per mancanza del costruttore di default) utilizzare<br />
un costruttore con argomenti, bisogna considerare a parte il caso di<br />
costruttore con un solo argomento (o con più argomenti di cui uno solo<br />
required). Ricordiamo a questo propos<strong>it</strong>o come si inizializza un array di tipo<br />
nativo:<br />
int valori[] = {32, 53, 28, 85, 21};<br />
nello stesso modo si può inizializzare un array di tipo astratto:<br />
point pt[] = {2.3, -1.2, 0.0, 1.4, 0.5};<br />
ma in questo caso ogni valore di inizializzazione , relativo a un elemento<br />
dell'array, viene passato come argomento al costruttore. Ciò è possibile in<br />
quanto, grazie alla presenza del costruttore con un solo argomento, ogni<br />
valore è convert<strong>it</strong>o implic<strong>it</strong>amente in un oggetto della classe point<br />
(chiamiamolo pn) e quindi l'espressione precedente diventa:<br />
point pt[] = {p0, p1, p2, p3, p4};<br />
l'inizializzazione in questa forma di un array di un certo tipo, tram<strong>it</strong>e<br />
elementi dello stesso tipo precedentemente costru<strong>it</strong>i, è sempre consent<strong>it</strong>a,<br />
anche per i tipi astratti.<br />
Non esiste invece alcuna possibil<strong>it</strong>à di utilizzare costruttori con due o più<br />
argomenti.<br />
Oggetti non locali<br />
Abbiamo già considerato i casi degli oggetti globali, degli oggetti nei<br />
namespace e dei membri statici delle classi (numero 6. dell'elenco iniziale),<br />
come casi particolari di oggetto singolo creato direttamente mediante la<br />
definizione del suo nome (vedere sezione: Costruttori). Sappiamo che tali<br />
oggetti non locali sono costru<strong>it</strong>i una sola volta, alla partenza del programma, e<br />
distrutti solo quando il programma termina.<br />
Qui vogliamo solo aggiungere alcune considerazioni riguardo all'ordine di<br />
costruzione e distruzione di più oggetti:<br />
• due oggetti defin<strong>it</strong>i nella stessa translation un<strong>it</strong> sono costru<strong>it</strong>i nello<br />
stesso ordine in cui la loro definizione appare nel programma, e distrutti<br />
in ordine inverso;<br />
• l'ordine di costruzione (e di distruzione) è invece indeterminato se i due<br />
oggetti sono defin<strong>it</strong>i in translation un<strong>it</strong> diverse.<br />
Ne consegue che è molto "imprudente" inserire, nel codice del costruttore di un<br />
oggetto non locale, operazioni che coinvolgano oggetti defin<strong>it</strong>i in altre
translation un<strong>it</strong> (in particolare ev<strong>it</strong>are istruzioni con cin e cout, in quanto non si<br />
può essere sicuri che gli oggetti globali delle classi di flusso di I/O siano già<br />
stati costru<strong>it</strong>i).<br />
Oggetti temporanei<br />
Abbiamo detto che un oggetto temporaneo (caso 7. dell'elenco iniziale) viene<br />
costru<strong>it</strong>o per memorizzare risultati parziali durante la valutazione di<br />
un'espressione, e distrutto alla fine dell'espressione completa in cui<br />
compare (con il termine "espressione completa" si intende un'espressione<br />
che non sia sotto-espressione di un'altra, cioè, in pratica, un'intera istruzione di<br />
programma).<br />
Finora abbiamo considerato soltanto operazioni fra tipi nativi, per i quali il<br />
problema della costruzione di un oggetto temporaneo non si pone. Ma, come<br />
vedremo nel prossimo cap<strong>it</strong>olo, il <strong>C++</strong> consente anche operazioni fra tipi<br />
astratti, tram<strong>it</strong>e la possibil<strong>it</strong>à di ridefinire, in overload, le funzioni che<br />
competono all'azione di molti operatori (overload degli operatori). Per<br />
esempio, si potrebbe ridefinire l'operatore di somma (+) in modo che accetti<br />
fra i suoi operandi anche oggetti della classe classe point (si tratterebbe in<br />
questo caso di una somma "vettoriale", ottenuta mediante somma membro a<br />
membro delle coordinate dei punti):<br />
point p = p1 + p2;<br />
dove p1 e p2 sono istanze già create della stessa classe.<br />
In questo caso è costru<strong>it</strong>o l'oggetto temporaneo p1 + p2, che viene<br />
distrutto dopo che l'istruzione è stata esegu<strong>it</strong>a. Ci chiediamo però: cosa succede<br />
se la classe point non ha un costruttore di default ? La risposta è che spetta<br />
al codice di implementazione della funzione, che definisce l'operatore di<br />
somma in overload, provvedere a che l'operazione sia esegu<strong>it</strong>a correttamente<br />
(per esempio potrebbe definire un'istanza locale di point, con valori di<br />
inizalizzazione qualsiasi, usarla per memorizzare la somma di p1 e p2 membro<br />
a membro, e infine trasmetterla come valore di r<strong>it</strong>orno by value, da copiare<br />
in p).<br />
In generale, tutte le volte che un'operazione crea un oggetto temporaneo, la<br />
funzione che compete a quell'operazione deve creare nel proprio amb<strong>it</strong>o<br />
locale un corrispondente oggetto, che, in quanto costru<strong>it</strong>o mediante<br />
definizione con un nome (categoria 1. del nostro elenco), non pone problemi,<br />
possegga o meno il costruttore di default.
Util<strong>it</strong>à dei costruttori e distruttori<br />
Poiché in <strong>C++</strong> ogni oggetto ha una sua precisa connotazione, caratterizzata da<br />
proprietà e metodi, i costruttori e i distruttori hanno in realtà un campo di<br />
applicazione molto più vasto della semplice inizializzazione o liberazione di<br />
risorse: in senso lato possono servire ogni qual volta un oggetto necess<strong>it</strong>a di ben<br />
defin<strong>it</strong>e operazioni iniziali e finali, incapsulate nell'oggetto stesso. Per esempio,<br />
se l'oggetto consiste in una procedura di help, il costruttore potrebbe servire<br />
per creare la "finestra di aiuto", mentre il distruttore avrebbe il comp<strong>it</strong>o di<br />
ripristinare le condizioni preesistenti dello schermo.
Overload degli operatori<br />
Estendibil<strong>it</strong>à del <strong>C++</strong><br />
In tutti i linguaggi, gli operatori sono dei simboli convenzionali che rendono più<br />
agevole la presentazione e lo sviluppo di concetti di uso frequente. Per esempio,<br />
la notazione:<br />
a+b*c<br />
risulta più agevole della frase:<br />
"moltiplica b per c aggiungi il risultato ad a"<br />
L'utilizzo di una notazione concisa per le operazioni di uso comune è di importanza<br />
fondamentale.<br />
Il <strong>C++</strong> supporta, come ogni altro linguaggio, un insieme di operazioni per i suoi<br />
tipi nativi. Tuttavia la maggior parte dei concetti utilizzati comunemente non<br />
sono facilmente rappresentabili per mezzo di tipi nativi, e bisogna spesso fare<br />
ricorso ai tipi astratti. Per esempio, i numeri complessi, le matrici, i segnali, le<br />
stringhe di caratteri, le aggregazioni di dati, le code, le liste ecc... sono tutte ent<strong>it</strong>à<br />
che meglio si prestano a essere rappresentate mediante le classi. E' pertanto<br />
necessario che anche le operazioni fra queste ent<strong>it</strong>à possano essere descr<strong>it</strong>te<br />
tram<strong>it</strong>e simboli convenzionali, in alternativa alla chiamata di funzioni specifiche<br />
(come avviene negli altri linguaggi), che non permetterebbero quella notazione<br />
concisa che, come si è detto, è di importanza fondamentale per una<br />
programmazione più semplice e chiara.<br />
Il <strong>C++</strong> consente di soddisfare questa esigenza tram<strong>it</strong>e l'overload degli<br />
operatori: il programmatore ha la possibil<strong>it</strong>à di creare nuove funzioni che<br />
ridefiniscono il significato dei simboli delle operazioni, rendendo queste<br />
applicabili anche ai tipi astratti (estendibil<strong>it</strong>à del <strong>C++</strong>). La caratteristica<br />
determinante per il reale vantaggio di questa tecnica, è che, a differenza dalle<br />
normali funzioni, quelle che ridefiniscono gli operatori possono essere<br />
chiamate mediante il solo simbolo dell'operazione (con gli argomenti della<br />
funzione che diventano operandi): in defin<strong>it</strong>iva la chiamata della funzione<br />
"scompare" dal codice del programma e al suo posto si può inserire una "semplice<br />
e concisa" operazione. Per esempio, se viene creata una funzione che<br />
ridefinisce la somma (+) fra due oggetti, a e b, istanze di una certa classe, in<br />
luogo della chiamata della funzione si può semplicemente scrivere: a+b. Se si<br />
pensa che un'espressione può essere cost<strong>it</strong>u<strong>it</strong>a da parecchie operazioni<br />
insieme, il vantaggio di questa tecnica per la concisione e la leggibil<strong>it</strong>à del codice<br />
risulta evidente (in alternativa a ripetute chiamate di funzioni, "innestate" l'una<br />
nell'altra). Per esempio, tornando all'espressione iniziale, cost<strong>it</strong>u<strong>it</strong>a da solo due<br />
operazioni:<br />
operatori in overload : a+b*c<br />
chiamata di funzioni specifiche : somma(a,moltiplica(b,c))
Ridefinizione degli operatori<br />
Per ottenere l'overload di un operatore bisogna creare una funzione il cui<br />
nome (che eccezionalmente non segue le regole generali di specifica degli<br />
identificatori) deve essere cost<strong>it</strong>u<strong>it</strong>o dalla parola-chiave operator segu<strong>it</strong>a,<br />
con o senza blanks in mezzo, dal simbolo dell'operatore (es.: operator+). Gli<br />
argomenti della funzione devono corrispondere agli operandi dell'operatore.<br />
Ne consegue che per gli operatori unari è necessario un solo argomento, per<br />
quelli binari ce ne vogliono due (e nello stesso ordine, cioè il primo argomento<br />
deve corrispondere al left-operand e il secondo argomento al right-operand).<br />
Non è concesso "inventare" nuovi simboli, ma si possono solo utilizzare i simboli<br />
degli operatori esistenti. In più, le regole di precedenza e associativ<strong>it</strong>à<br />
restano legate al simbolo e non al suo significato, come pure resta legata al<br />
simbolo la categoria dell'operatore (unario o binario). Per esempio, un<br />
operatore in overload associato al simbolo della divisione (/) non può mai<br />
essere defin<strong>it</strong>o unario e ha sempre la precedenza sull'operatore associato al<br />
simbolo +, qualunque sia il significato di entrambi.<br />
E' possibile avere overload di quasi tutti gli operatori esistenti, salvo: ?:,<br />
sizeof, typeid e pochi altri, fra cui quelli (come :: e .) che hanno come operandi<br />
nomi non "parametrizzabili" (come i nomi delle classi o dei membri di una<br />
classe).<br />
Come per le funzioni in overload, nel caso dello stesso operatore ridefin<strong>it</strong>o più<br />
volte con tipi diversi, il <strong>C++</strong> risolve l'ambigu<strong>it</strong>à in base al contesto degli<br />
operandi, riconoscendone il tipo e decidendo di conseguenza quale operatore<br />
applicare.<br />
Torniamo ora alla classe point e vediamo un esempio di possibile operatore di<br />
somma (il nostro intento è di ottenere la somma "vettoriale" fra due punti);<br />
supponiamo che la classe sia provvista di un costruttore con due argomenti:<br />
operazione :<br />
funzione somma<br />
:<br />
p = p1+p2 ;<br />
point operator+(const point& p1, const point&<br />
p2)<br />
{<br />
}<br />
point ptemp(0.0,0.0);<br />
ptemp.x = p1.x + p2.x ;<br />
ptemp.y = p1.y + p2.y ;<br />
return ptemp ;
Notare:<br />
1. la funzione ha un valore di r<strong>it</strong>orno di tipo point;<br />
2. gli argomenti-operandi sono passati by reference e dichiarati const,<br />
per maggiore sicurezza (const) e rapid<strong>it</strong>à di esecuzione (passaggio by<br />
reference);<br />
3. nella funzione è defin<strong>it</strong>o un oggetto automatico (ptemp),<br />
inizializzato compatibilmente con il costruttore disponibile (vedere il<br />
problema della inizializzazione degli oggetti temporanei nel cap<strong>it</strong>olo<br />
precedente);<br />
4. in ptemp i due operandi sono sommati membro a membro (la somma<br />
è ammessa in quanto fra due tipi double);<br />
5. in usc<strong>it</strong>a ptemp (essendo un oggetto automatico) "muore", ma una sua<br />
copia è passata by value al chiamante, dove è successivamente<br />
assegnata a p<br />
Nota ulteriore: è ammessa anche la chiamata della funzione nella forma<br />
tradizionale:<br />
p = operator+(p1, p2) ;<br />
ma in questo caso si vanificherebbero i vantaggi offerti dalla notazione simbolica<br />
delle operazioni.<br />
Metodi della classe o funzioni esterne?<br />
Finora abbiamo parlato delle funzioni che ridefiniscono gli operatori in<br />
overload, senza preoccuparci di dove tali funzioni debbano essere defin<strong>it</strong>e.<br />
Quando esse accedono a membri privati della classe, possono appartenere<br />
soltanto a una delle seguenti tre categorie:<br />
1. sono metodi pubblici non statici della classe;<br />
2. sono metodi pubblici statici della classe;<br />
3. sono funzioni friend della classe.<br />
Escludiamo sub<strong>it</strong>o che siano metodi statici, non perchè non sia permesso, ma<br />
perchè non sarebbe conveniente, in quanto un metodo statico può essere<br />
chiamato solo se il suo nome è qualificato con il nome della classe di<br />
appartenenza,<br />
es.: p = point::operator+(p1, p2) ;<br />
e quindi non esiste il modo di utilizzarlo nella rappresentazione simbolica di<br />
un'operazione.<br />
Restano pertanto a disposizione solo i metodi non statici e le funzioni friend<br />
(o esterne, se non accedono a membri privati). La scelta più appropriata<br />
dipende dal contesto degli operandi e dal tipo di operazione. In generale<br />
conviene che sia un metodo quando l'operatore è unario, oppure (e in questo
caso è obbligatorio) quando il primo operando è oggetto della classe e la<br />
funzione lo rest<strong>it</strong>uisce come l-value, come accade per esempio per gli<br />
overload degli operatori di assegnazione (=) e in notazione compatta (+=<br />
ecc...). Viceversa, non ha molto senso che sia un metodo l'overload<br />
dell'addizione (che abbiamo visto come esempio nella sezione precedente), il<br />
quale opera su due oggetti e rest<strong>it</strong>uisce un risultato da memorizzare in un terzo.<br />
La miglior progettazione degli operatori di una classe consiste nell'individuare<br />
un insieme ben defin<strong>it</strong>o di metodi per le operazioni che si applicano su un unico<br />
oggetto o che modificano il loro primo operando, e usare funzioni esterne (o<br />
friend) per le altre operazioni; il codice di queste funzioni risulta però<br />
facil<strong>it</strong>ato, in quanto può utilizzare gli stessi operatori già defin<strong>it</strong>i come metodi<br />
(vedremo più avanti un'alternativa dell'operatore + come funzione esterna, che<br />
usa l'operatore += implementato come metodo).<br />
NOTA: nei tipi astratti, l'esistenza degli operatori in overload + e = non<br />
implica che sia automaticamente defin<strong>it</strong>o anche l'operatore in overload +=<br />
Il ruolo del puntatore nascosto this<br />
E' chiaro a tutti perchè un'operazione che si applica su un unico oggetto o che<br />
modifica il primo operando è preferibile che sia implementata come metodo<br />
della classe? Perchè, in quanto metodo non statico, può sfruttare la presenza<br />
del puntatore nascosto this, che, come sappiamo, punta allo stesso oggetto<br />
della classe in cui il metodo è incapsulato e viene automaticamente inser<strong>it</strong>o<br />
dal <strong>C++</strong> come primo argomento della funzione.<br />
Ne consegue che:<br />
1. un operatore in overload può essere implementato come metodo di una<br />
classe solo se il primo operando è un oggetto della stessa classe; in<br />
caso contrario deve essere una funzione esterna (dichiarata friend se<br />
accede a membri privati) ;<br />
2. nella definizione del metodo il numero degli argomenti deve essere<br />
ridotto di un'un<strong>it</strong>à rispetto al numero di operandi; in pratica, se<br />
l'operatore è binario, ci deve essere un solo argomento (quello<br />
corrispondente al secondo operando), se l'operatore è unario, la<br />
funzione non deve avere argomenti.<br />
3. se il risultato dell'operazione è l'oggetto stesso l'istruzione di r<strong>it</strong>orno deve<br />
essere:<br />
return *this;<br />
Vediamo ora, a t<strong>it</strong>olo di esempio, una possibile implementazione di overload<br />
dell'operatore in notazione compatta += della nostra classe point:
Notare:<br />
operazione : p += p1 ;<br />
definizione metodo : point& point::operator+=(const point& p1)<br />
{<br />
}<br />
x += p1.x ;<br />
y += p1.y ;<br />
return *this ;<br />
1. la funzione ha un un solo argomento, che corrisponde al secondo<br />
operando p1, in quanto il primo operando p è l'oggetto stesso,<br />
trasmesso per mezzo del puntatore nascosto this;<br />
2. la funzione è un metodo della classe, e quindi i membri dell'oggetto<br />
p sono indicati solo con il loro nome (il compilatore aggiunge this-><br />
davanti a ognuno di essi);<br />
3. nel codice della funzione l'operatore += è "conosciuto", in quanto agisce<br />
sui membri della classe, che sono di tipo double;<br />
4. la funzione r<strong>it</strong>orna l'oggetto stesso p (deref. di this), by reference<br />
(cioè come l-value), modificato dall'operazione (non esistono problemi<br />
di lifetime in questo caso, essendo l'oggetto p defin<strong>it</strong>o nel<br />
chiamante);<br />
5. la chiamata della funzione nella forma tradizionale sarebbe:<br />
p.operator+=(p1) ;<br />
tradotta dal compilatore in:<br />
operator+=(&p,p1) ;<br />
Adesso che abbiamo defin<strong>it</strong>o l'operatore += come metodo della classe,<br />
l'implementazione dell'operatore +, che invece preferiamo sia una funzione<br />
esterna, può essere fatta in modo più semplice (non occorre che sia dichiarata<br />
friend in quanto non accede a membri privati):<br />
operazione : p = p1+p2 ;<br />
funzione somma : point operator+(const point& p1, const point& p2)<br />
{<br />
}<br />
point ptemp = p1; (uso il costruttore di copia)<br />
return ptemp += p2 ;<br />
Overload degli operatori di flusso di I/O
Un caso particolare rappresenta l'overload dell'I/O, cioè degli operatori di<br />
flusso "" (estrazione). Notiamo che questi sono già<br />
degli operatori in overload, in quanto il significato originario dei simboli > è quello di operatori di scorrimento di b<strong>it</strong> (se gli operandi sono interi).<br />
Se invece il left-operand non è un intero, ma l'oggetto cout, abbiamo visto<br />
che l'operatore
4. per i motivi suddetti, e per l'associativ<strong>it</strong>à dell'operatore & | ^)<br />
• in notazione compatta (+= -= *= / = %= = &= | =<br />
^=)<br />
• relazionali (== != < >=);<br />
• logici (&& ||)<br />
• di serializzazione ( , )<br />
e di altri che tratteremo separatamente (per una maggiore leggibil<strong>it</strong>à del<br />
programma, si consiglia, anche se non è obbligatorio, che gli overload di questi<br />
operatori mantengano comunque qualche "somiglianza" con il loro significato<br />
originario).<br />
Tutti gli operatori sopra riportati avranno ovviamente almeno un operando che<br />
è oggetto della classe, non importa se left o right (a parte gli operatori in<br />
notazione compatta, per i quali l'oggetto della classe deve essere sempre<br />
left). L'altro operando può essere un altro oggetto della stessa classe (come<br />
nell'esempio della somma che abbiamo visto prima), oppure un oggetto di<br />
qualsiasi altro tipo, nativo o astratto. Pertanto possono esistere parecchi<br />
overload dello stesso operatore, ciascuno con un operando di tipo diverso.<br />
Non solo, ma se si vuole salvaguardare la proprietà "commutativa" di certe<br />
operazioni (+ * & | ^ == != && ||), o la "simmetria" di altre (< con<br />
>= e > con
Ne consegue che, se gli operatori da applicare in overload a una certa classe<br />
non sono progettati attentamente, si rischia di generare una pletora di funzioni,<br />
con varianti spesso molto piccole da una all'altra.<br />
Il <strong>C++</strong> offre una soluzione a questo problema, che è molto semplice ed efficace:<br />
il numero di funzioni può essere minimizzato utilizzando i costruttori con un<br />
argomento, che, come abbiamo visto, definiscono anche una conversione<br />
implic<strong>it</strong>a di tipo: se "attrezziamo" la classe con un insieme opportuno di<br />
costruttori con un argomento, possiamo ottenere che tutti i tipi coinvolti nelle<br />
operazioni siano convert<strong>it</strong>i implic<strong>it</strong>amente nel tipo della classe e che ogni<br />
operazione sia perciò implementata da una sola funzione, quella che opera su<br />
due oggetti della stessa classe. Notare che la conversione implic<strong>it</strong>a viene<br />
esegu<strong>it</strong>a indipendentemente dalla posizione dell'operando, e ciò permette in<br />
particolare che ogni operazione "commutativa" sia definibile con una sola<br />
funzione.<br />
Riprendendo la nostra classe point, vogliamo per esempio definire un operazione<br />
di somma fra un vettore p, oggetto di point, e un valore s di tipo double<br />
(detto: "scalare"), in modo tale che lo scalare venga sommato a ogni componente<br />
del vettore. Se definiamo il costruttore:<br />
point::point(double d) : x(d), y(d) { }<br />
otterremo che entrambe le operazioni:<br />
p + s e s + p<br />
comportino la conversione implic<strong>it</strong>a di s da tipo double a tipo point, e si<br />
trasformino nell'unica operazione di somma fra due oggetti di point (della<br />
quale abbiamo già visto un esempio di implementazione).<br />
Operatori unari e casting a tipo nativo<br />
Si possono definire gli overload dei seguenti operatori unari :<br />
• incremento e decremento (suffisso) (++ - - );<br />
• incremento e decremento (prefisso) (++ - - );<br />
• segni algebrici (+ -)<br />
• complemento a 1 e NOT (~ !);<br />
• indirizzo e deref. (& *)<br />
• casting ( (tipo) )<br />
Gli operatori unari devono avere come unico operando un oggetto della<br />
classe in cui sono defin<strong>it</strong>i e quindi possono convenientemente essere defin<strong>it</strong>i<br />
come metodi della stessa classe, nel qual caso le funzioni che li implementano<br />
devono essere senza argomenti.<br />
Tutti gli operatori sopra menzionati sono prefissi dell'operando, salvo gli<br />
operatori di incremento e decremento che possono essere sia prefissi che<br />
suffissi. Per distinguerli, è applicata la seguente convenzione: se la funzione è<br />
senza argomenti, si tratta di un prefisso, se la funzione contiene un
argomento f<strong>it</strong>tizio di tipo int (che il sistema non usa in quanto l'operatore è<br />
unario) si tratta di un suffisso. Inoltre, per i prefissi, il valore di r<strong>it</strong>orno deve<br />
essere passato by reference, mentre per i suffissi deve essere passato by<br />
value (questo perché i prefissi possono essere degli l-values mentre i suffissi<br />
no). Infine, gli operatori suffissi devono essere progettati con particolare<br />
attenzione, se si vuole conservare la loro proprietà di eseguire un'operazione<br />
"posticipata", nonostanza la precedenza alta. Per esempio, un operatore di<br />
incremento suffisso di una generica classe A, potrebbe essere implementato<br />
così (supponiamo che il corrispondente operatore prefisso sia già stato<br />
defin<strong>it</strong>o):<br />
A A::operator++(int)<br />
{<br />
}<br />
A temp = *this;<br />
++*this ;<br />
return temp ;<br />
come si può notare, l'oggetto è correttamente incrementato, ma al chiamante<br />
non torna l'oggetto stesso, bensì una sua copia precedente (temp); in questo<br />
modo, non è l'oggetto, ma la sua copia precedente ad essere utilizzata come<br />
operando nelle eventuali successive operazioni dell'espressione di cui fa<br />
parte; solo dopo che l'intera espressione è stata esegu<strong>it</strong>a, un nuovo accesso al<br />
nome dell'oggetto r<strong>it</strong>roverà l'oggetto incrementato.<br />
Un caso a parte è quello dell'operatore di casting. Come abbiamo visto, la<br />
conversione di tipo può essere esegu<strong>it</strong>a usando un costruttore con un<br />
argomento: questo consente conversioni, anche implic<strong>it</strong>e, da tipi nativi a tipi<br />
astratti (o fra tipi astratti), ma non può essere utilizzato per conversioni da<br />
tipi astratti a tipi nativi, in quanto i tipi nativi non hanno costruttori con<br />
un argomento. A questo scopo occore invece definire esplic<strong>it</strong>amente un<br />
overload dell'operatore di casting, che deve essere espresso nella seguente<br />
forma (esempio di casting da una classe A a double):<br />
A::operator double( )<br />
notare che il tipo di r<strong>it</strong>orno non deve essere specificato in quanto il <strong>C++</strong> lo<br />
riconosce già dal nome della funzione; notare anche che esiste uno spazio<br />
(obbligatorio) fra le parole operator e double.<br />
La conversione può essere esegu<strong>it</strong>a implic<strong>it</strong>amente o esplic<strong>it</strong>amente, in Cstyle<br />
o in function-style. Se è esegu<strong>it</strong>a implic<strong>it</strong>amente, può verificarsi<br />
un'ambigu<strong>it</strong>à nel caso sia defin<strong>it</strong>a anche la conversione in senso inverso.<br />
Esempio:<br />
A a ; double d ;<br />
a + d ; deve convertire un tipo A in double o un double in A ?<br />
Nell'esempio sopra riportato si è supposto che:
1. la classe A abbia un metodo che definisce un overload dell'operatore<br />
di casting da A a double;<br />
2. la classe A abbia un costruttore con un argomento double;<br />
3. esista una funzione esterna che definisce un overload dell'operatore di<br />
somma fra due oggetti di A.<br />
in queste condizioni il compilatore segnala un errore di ambigu<strong>it</strong>à, perchè non sa<br />
quale delle due conversioni implic<strong>it</strong>e selezionare. In ogni caso, quando si tratta<br />
di operatori in overload, il <strong>C++</strong> non fa preferenza fra i metodi della classe e<br />
le altre funzioni .<br />
Operatori in namespace<br />
Abbiamo visto che, per una migliore organizzazione degli operatori in overload<br />
di una classe, è preferibile utilizzare in maggioranza funzioni non metodi (se si<br />
tratta di operatori binari), che si appoggino a un insieme lim<strong>it</strong>ato di metodi<br />
della classe. Non ci siamo mai chiesti, però, in quale amb<strong>it</strong>o sia conveniente che<br />
tali funzioni vengano defin<strong>it</strong>e e, per semplic<strong>it</strong>à, negli esempi (ed esercizi) finora<br />
riportati abbiamo sempre defin<strong>it</strong>o le funzioni nel namespace globale.<br />
Questo non è, tuttavia, il modo più corretto di procedere. Come abbiamo detto più<br />
volte, un affollamento eccessivo del namespace globale può essere fonte di<br />
confusione e di errori, specialmente in programmi di grosse dimensioni e con<br />
diversi programmatori che lavorano ad un unico progetto.<br />
E' pertanto preferibile "racchiudere" la classe e le funzioni esterne che<br />
implementano gli operatori della classe in un namespace defin<strong>it</strong>o con un<br />
nome. In questo modo non si "inquina" il namespace globale e, nel contempo,<br />
si può mantenere la notazione simbolica nella chiamata delle operazioni.<br />
Infatti, a differenza dai metodi statici, che devono essere sempre qualificati<br />
con il nome della classe, una funzione appartenente a un namespace non ha<br />
bisogno di essere qualificata con il nome del namespace, se appartiene allo<br />
stesso namespace almeno uno dei suoi argomenti.<br />
In generale, data una generica operazione (usiamo l'operatore @, che in realtà<br />
non esiste, proprio per indicare un'operazione qualsiasi):<br />
a @<br />
b<br />
(dove a è un'istanza di una classe A e b è un' istanza di una<br />
classe B)<br />
il compilatore esegue la ricerca della funzione operator@ nel seguente modo:<br />
• cerca operator@ come metodo della classe A;<br />
• cerca una definizione di operator@ nell'amb<strong>it</strong>o della chiamata (o in<br />
amb<strong>it</strong>i superiori, fino al namespace globale);<br />
• se la classe A è defin<strong>it</strong>a in un namespace M, cerca una definizione di<br />
operator@ in M;
• se la classe B è defin<strong>it</strong>a in un namespace N, cerca una definizione di<br />
operator@ in N<br />
Non sono fissati cr<strong>it</strong>eri di preferenza: se sono trovate più definizioni di<br />
operator@, il compilatore, se può, sceglie la "migliore" (per esempio, quella in<br />
cui i tipi degli operandi corrispondono esattamente, rispetto ad altre in cui la<br />
corrispondenza è ottenuta dopo una conversione implic<strong>it</strong>a), altrimenti segnala<br />
l'ambigu<strong>it</strong>à.<br />
Nel caso che operator@ sia trovata nel namespace in cui è defin<strong>it</strong>a una delle<br />
due classi, la funzione deve essere comunque dichiarata friend in entrambe<br />
le classi (se in entrambe accede a membri privati); ciò potrebbe far sorgere un<br />
problema di dipendenza circolare, problema che peraltro si risolve mediante<br />
dichiarazione anticipata di una delle classi (per fortuna un namespace si<br />
può spezzare in più parti!)<br />
Oggetti-array e array associativi<br />
Tratteremo ora di alcuni overload di operatori binari, da implementare obbligatoriamente<br />
come metodi, in quanto il loro primo operando è oggetto della classe e l-value<br />
modificabile. Fermo restando il fatto che la ridefinizione del significato di un operatore in<br />
overload è assolutamente libera, questi operatori vengono comunemente ridefin<strong>it</strong>i con<br />
significati specifici.<br />
Oggetti-array<br />
Il primo overload che esaminiamo è quello dell'operatore indice [], che<br />
potrebbe servire, per esempio, se un membro della classe è un array. In tal<br />
caso, rinunciando, per non avere ambigu<strong>it</strong>à, a trattare array di oggetti, ma solo<br />
il membro array di ogni oggetto, l'overload dell'operatore indice potrebbe<br />
essere defin<strong>it</strong>o come nel seguente esempio:<br />
data una classe A : class A { int m[10] ; ........ } ;<br />
e una sua istanza a, vogliamo che l'operazione: a[i] non indichi l'oggetto di<br />
indice i di un array di oggetti a (come sarebbe senza overload di []), ma<br />
l'elemento di indice i del membro-array m dell'oggetto a. Per ottenere<br />
questo, basta definire in A il seguente metodo:<br />
int& A::operator[] (const int& i) { return m[i]; }<br />
da notare che il valore di r<strong>it</strong>orno è un riferimento, e questo fa sì che<br />
l'operatore [] funzioni come un l-value, rendendo possibili, non solo<br />
operazioni di estrazione, come:<br />
num = a[i];<br />
ma anche operazioni di inserimento, come:<br />
a[i] = num;
Gli oggetti cost<strong>it</strong>u<strong>it</strong>i da un solo membro-array (o in cui il membro-array è<br />
predominante) sono talvolta detti: oggetti-array. Rispetto ai normali array,<br />
presentano il vantaggio di poter disporre delle funzional<strong>it</strong>à in più offerte dalla<br />
classe di appartenenza; per esempio possono controllare il valore dell'indice,<br />
sollevando eccezione in caso di overflow, oppure modificare la dimensione<br />
dell'array (se il membro-array è dichiarato come puntatore) ecc...<br />
Array associativi<br />
L' operatore indice ha un campo di applicazione molto più vasto e generalizzato<br />
di un normale array. Infatti non esiste nessuna regola che obblighi il secondo<br />
operando a essere un intero, come è l'indice di un array; al contrario, lo si può<br />
definire di un qualsiasi tipo, anche astratto, e ciò permette di stabilire una<br />
corrispondenza (o, come talvolta si dice, un'associazione) fra oggetti di due<br />
classi. Un array associativo, spesso chiamato mappa o anche dizionario,<br />
memorizza coppie di valori: dato un valore, la chiave, si può accedere all'altro, il<br />
valore mappato. La funzione che implementa l'overload dell' operatore<br />
indice fornisce l'algor<strong>it</strong>mo di mappatura, che associa un oggetto della<br />
classe (primo operando) a ogni valore della chiave (secondo operando).<br />
Oggetti-funzione<br />
Anche l'operatore di chiamata di una funzione può essere ridefin<strong>it</strong>o. In<br />
questo caso il primo operando deve essere un oggetto della classe (nascosto<br />
da this) e il secondo operando è una lista di espressioni, che viene valutata<br />
e trattata secondo le normali regole di passaggio degli argomenti di una<br />
funzione. Il metodo che implementa l'overload di questo operatore deve<br />
essere defin<strong>it</strong>o nel seguente modo (supponiamo che il nome della classe sia<br />
A):<br />
tipo del valore di r<strong>it</strong>orno A::operator() (lista di argomenti) { ........ }<br />
L'uso più frequente dell'operatore () si ha quando si vuole fornire la normale<br />
sintassi della chiamata di una funzione a oggetti che in qualche modo si<br />
comportano come funzioni (cioè che utilizzano in modo predominante un loro<br />
metodo). Tali oggetti sono spesso chiamati oggetti-funzione. Rispetto a una<br />
normale funzione, un oggetto-funzione ha il vantaggio di potersi "appoggiare"<br />
a una classe, e quindi di utilizzare le informazioni già memorizzate nei suoi<br />
membri, senza bisogno di dover trasmettere ogni volta queste informazioni come<br />
argomenti aggiuntivi nella chiamata.
Puntatori intelligenti<br />
Abbiamo detto all'inizio che non tutti gli operatori possono essere ridefin<strong>it</strong>i in<br />
overload e in particolare non è ammesso ridefinire quegli operatori i cui<br />
operandi sono nomi non "parametrizzabili"; c<strong>it</strong>iamo, a questo propos<strong>it</strong>o,<br />
l'operatore di risoluzione di visibil<strong>it</strong>à (::), in cui il left-operand è il nome di<br />
una classe o di un namespace, e gli operatori di selezione di un membro (.<br />
e ->), in cui il right-operand è il nome di un membro di una classe.<br />
A questa regola fa eccezione l'operatore ->, che può essere ridefin<strong>it</strong>o; ma,<br />
proprio perchè il suo right-operand non può essere trasmesso come<br />
argomento di una funzione, l'operatore -> in overload è "declassato" da<br />
operatore binario a operatore unario suffisso e mantiene, come unico<br />
operando, il suo originario left-operand, cioè l'indirizzo di un oggetto. La<br />
funzione che implementa questo (strano) overload deve essere un metodo di<br />
una classe, dal che si deduce che gli oggetti di tale classe possono essere usati<br />
come puntatori per accedere ai membri di un'altra classe. Per esempio, data<br />
una classe Ptr_to_A:<br />
class Ptr_to_A { ........ public: A* operator->( ); ........ } ;<br />
le sue istanze possono essere utilizzate per accedere a istanze della classe A,<br />
in una maniera molto simile a quella in cui sono utilizzati i normali puntatori.<br />
Se il metodo viene chiamato come una normale funzione, il suo valore di<br />
r<strong>it</strong>orno può essere usato come puntatore ad un oggetto di A; se invece si<br />
adotta la notazione simbolica dell'operazione, le regole di sintassi pretendono<br />
che il nome di un membro di A venga comunque aggiunto. Per chiarire,<br />
continuiamo nell'esempio precedente:<br />
Ptr_to_A p ;<br />
A* pa = p.operator->( ); OK<br />
A* pa = p->; errore di sintassi<br />
int num = p->ma; OK (ma è un membro di A di tipo int)<br />
p->ma = 7 ; OK (può anche essere un l-value)<br />
L'overload di -> è utile principalmente per creare puntatori "intelligenti", cioè<br />
oggetti che si comportano come puntatori, ma con il vantaggio di poter<br />
disporre delle funzional<strong>it</strong>à in più offerte dalla classe di appartenenza<br />
(esattamente come gli oggetti-array e gli oggetti-funzione).<br />
C'è da sottolineare infine che, come di regola, la definizione dell' overload di -><br />
non implica che siano automaticamente defin<strong>it</strong>e le operazioni equivalenti. Infatti,<br />
mentre per i normali puntatori valgono le seguenti uguaglianze:<br />
p->ma = = (*p).ma = = p[0].ma<br />
le stesse continuano a valere per gli operatori in overload solo se tutti gli<br />
operatori sono defin<strong>it</strong>i in modo tale da produrre volutamente tale risultato.
Operatore di assegnazione<br />
Abbiamo lasciato per ultimo di questo gruppo l'overload dell'operatore di<br />
assegnazione (=), non perchè fosse il meno importante (anzi ...), ma<br />
semplicemente perchè, negli esempi (e negli esercizi) finora riportati, non ne<br />
abbiamo avuto bisogno. Infatti, come già per il costruttore senza argomenti e<br />
per il costruttore di copia, il <strong>C++</strong> fornisce un operatore di assegnazione di<br />
default, che copia membro a membro l'oggetto right-operand nell'oggetto<br />
left-operand.<br />
Nota<br />
In alcune circostanze si potrebbe non desiderare che un oggetto venga<br />
costru<strong>it</strong>o per copia o assegnato. Ma, se non si definiscono overload, il <strong>C++</strong><br />
inserirà quelli di default, e se invece li si definiscono, il programma li userà<br />
direttamente. Come fare allora? La soluzione è semplice: definire degli overload<br />
f<strong>it</strong>tizi e collocarli nella sezione privata della classe; in questo modo gli overload<br />
ridefin<strong>it</strong>i "nasconderanno" quelli di default, ma a loro volta saranno inaccessibili<br />
in quanto metodi non pubblici.<br />
L'assegnazione mediante copia membro a membro può essere esattamente<br />
ciò che si vuole nella maggioranza dei casi, e quindi non ha senso ridefinire<br />
l'operatore. Ma, se la classe possiede membri puntatori, la semplice copia di<br />
un puntatore può generare due problemi:<br />
• dopo la copia, l'area precedentemente puntata dal membro puntatore<br />
del left-operand resta ancora, cioè occupa spazio, ma non è più<br />
accessibile (errore di memory leak);<br />
• il fatto che due oggetti puntino alla stessa area è pericoloso, perché, se<br />
viene chiamato il distruttore di uno dei due oggetti, il membro<br />
puntatore dell'altro, che esiste ancora, punta a un'area che non esiste più<br />
(errore di dangling references).<br />
Come si può notare, il secondo problema è identico a quello che si presenterebbe<br />
usando il costruttore di copia di default, mentre il primo è specifico<br />
dell'operatore di assegnazione (in quanto la copia viene esegu<strong>it</strong>a su un<br />
oggetto già esistente).<br />
Anche in questo caso, è perciò necessario che l'operatore di assegnazione<br />
esegua la copia, non del puntatore, ma dell'area puntata. Per evidenziare<br />
analogie e differenze, riprendiamo l'esempio del costruttore di copia del cap<strong>it</strong>olo<br />
precedente (complicandolo un po', cioè supponendo che l'area puntata sia un<br />
array con dimensioni defin<strong>it</strong>e in un ulteriore membro della classe), e gli<br />
affianchiamo un esempio di corretto metodo di implementazione dell'operatore<br />
di assegnazione:<br />
COSTRUTTORE DI<br />
COPIA<br />
OPERATORE DI<br />
ASSEGNAZIONE
CLASSE<br />
class A {<br />
public:<br />
A( );<br />
int* pa;<br />
int dim;<br />
A(const A&);<br />
A& operator=<br />
(const A&);<br />
........ };<br />
Notare:<br />
operazioni : A a1 ; ........ A a2 = a1 ; A a1 , a2 ; ........ a2 = a1 ;<br />
A::A(const A& a) A& A::operator=(const A& a)<br />
{ {<br />
dim = a.dim ; if (this == &a) return *this;<br />
pa = new int [dim] ; if (dim != a.dim)<br />
for(int i=0; i < dim;<br />
i++)<br />
*(pa+i) = *(a.pa+i) ;<br />
} delete [] pa;<br />
}<br />
{<br />
}<br />
dim = a.dim ;<br />
pa = new int [dim] ;<br />
for(int i=0; i < dim; i++)<br />
*(pa+i) = *(a.pa+i) ;<br />
return *this;<br />
1. la prima istruzione: if (this == &a) return *this; serve a proteggersi<br />
dalla cosidetta auto-assegnazione (a1 = a1); in questo caso la<br />
funzione deve rest<strong>it</strong>uire l'oggetto stesso senza fare altro;<br />
2. il metodo che implementa l'operatore di assegnazione è un po' più<br />
complicato del costruttore di copia, in quanto deve deallocare (con<br />
delete) l'area precedentemente puntata dal membro pa di a2 prima di<br />
allocare (con new) la nuova area; tuttavia, se le aree puntate dai<br />
membri pa di a2 e a1 sono di uguali dimensioni, non è necessario<br />
deallocare e riallocare, ma si può semplicemente riutilizzare l'area già<br />
esistente di a2 per copiarvi i nuovi dati;<br />
3. entrambi i metodi eseguono la copia (tram<strong>it</strong>e un ciclo for) dell'area<br />
puntata e non del puntatore, come avverrebbe se si lasciasse fare ai<br />
metodi di default;<br />
4. la classe dovrà contenere altri metodi (o altri costruttori) che si<br />
occupano dell'allocazione iniziale dell'area e dell'inserimento dei dati; per<br />
semplic<strong>it</strong>à li abbiamo omessi.<br />
Ottimizzazione delle copie<br />
Tanto per ribadire il vecchio detto che "non è saggio chi non si contraddice mai",<br />
ci contraddiciamo sub<strong>it</strong>o: a volte può essere preferibile copiare i puntatori e<br />
non le aree puntate! Anzi, in certi casi può essere utile creare ad-hoc un
puntatore a un oggetto (apparentemente non necessario), proprio allo scopo di<br />
copiare il puntatore al posto dell'oggetto.<br />
Supponiamo, per esempio, che un certo oggetto a1 sia di "grosse dimensioni" e<br />
che, a un certo punto del programma, a1 debba essere assegnato a un altro<br />
oggetto a2, oppure un altro oggetto a2 debba essere costru<strong>it</strong>o e<br />
inizializzato con a1. In entrambi i casi sappiamo che a1 viene copiato in a2.<br />
Ma la copia di un "grosso" oggetto può essere particolarmente onerosa, specie<br />
se effettuata parecchie volte nel programma. Aggiungasi il fatto che spesso<br />
vengono creati e immediatamente distrutti oggetti temporanei, che<br />
moltiplicano il numero delle copie, come si evince dal seguente esempio:<br />
a2 = f(a1);<br />
in questa istruzione vengono esegu<strong>it</strong>e ben 3 copie!<br />
Ci chiediamo a questo punto: ma se, nel corso del programma, a1 e a2 non<br />
vengono modificati, che senso ha eseguire materialmente la copia? Solo la<br />
modifica di almeno uno dei due creerebbe di fatto due oggetti distinti, ma finchè<br />
ciò non avviene, la duplicazione "prematura" sarebbe un'operazione inutilmente<br />
costosa. In base a questo ragionamento, se si riuscisse a creare un meccanismo,<br />
che, di fronte a una richiesta di copia, si lim<strong>it</strong>i a "prenotarla", ma ne rimandi<br />
l'esecuzione al momento dell'eventuale modifica di uno dei due oggetti (copy on<br />
wr<strong>it</strong>e), si otterrebbe lo scopo di ottimizzare il numero di copie, eliminando<br />
tutte quelle che, alla fine, sarebbero risultate inutili.<br />
Puntualmente, è il <strong>C++</strong> che mette a disposizione questo meccanismo. L'idea base<br />
è quella di "svuotare" la classe (che chiamiamo A) di tutti i suoi dati-membro,<br />
lasciandovi solo i metodi (compresi gli eventuali metodi che implementano gli<br />
operatori in overload) e al loro posto inserire un unico membro, puntatore a<br />
un'altra classe (che chiamiamo Arep). Questa seconda classe, che viene<br />
preferibilmente defin<strong>it</strong>a come struttura, è detta "rappresentazione" della<br />
classe A, e in essa vengono inser<strong>it</strong>i tutti i dati-membro che avrebbero dovuto<br />
essere di A. In questa s<strong>it</strong>uazione, si dice che A è implementata come handle<br />
(aggancio) alla sua rappresentazione, ma è la stessa rappresentazione (cioè<br />
la struttura Arep) che contiene realmente i dati.<br />
Più oggetti di A possono "condividere" la stessa rappresentazione (cioè<br />
puntare allo stesso di oggetto di Arep). Per tenere memoria di ciò, Arep deve<br />
contenere un ulteriore membro, di tipo int, in cui contare il numero di oggetti<br />
di A agganciati; questo numero, inizializzato con 1, viene incrementato ogni<br />
volta che è "prenotata" una copia, e decrementato ogni volta che uno degli<br />
oggetti di A agganciati subisce una modifica: nel primo caso, la copia viene<br />
esegu<strong>it</strong>a solo fra i membri puntatori dei due oggetti di A (in modo che puntino<br />
allo stesso oggetto di Arep); nel secondo caso, uno speciale metodo di Arep fa<br />
sì che l'oggetto di Arep "si cloni", cioè crei un nuovo oggetto copia di se stesso,<br />
su questo esegua le modifiche richeste, e infine ne assegni l'indirizzo al membro<br />
puntatore dell'oggetto di A da cui è provenuta la richiesta di modifica.<br />
Ovviamente spetta ai metodi di A individuare quali operazioni comportino la<br />
modifica di un suo oggetto e attivare le azioni conseguenti che abbiamo<br />
descr<strong>it</strong>to. Per concludere, il distruttore di un oggetto di A deve decrementare<br />
il contatore di agganci nel corrispondente oggetto di Arep, e poi procedere alla<br />
distruzione di detto oggetto solo se il contatore è diventato zero.
Da notare che una rappresentazione è sempre creata nella memoria heap e<br />
quindi non ha problemi di lifetime, anche se gli oggetti che l'agganciano sono<br />
automatici: questo è particolarmente utile, per esempio, nel passaggio by value<br />
degli argomenti e del valore di r<strong>it</strong>orno fra chiamante e funzione (e<br />
viceversa): la copia viene esegu<strong>it</strong>a solo apparentemente, in quanto permane la<br />
stessa unica rappresentazione, che sopravvive anche in amb<strong>it</strong>i di visibil<strong>it</strong>à<br />
diversi da quello in cui è stata creata. Per esempio, tornando alla nostra<br />
istruzione:<br />
a2 = f(a1);<br />
almeno 2 delle 3 copie previste non vengono esegu<strong>it</strong>e, in quanto l'oggetto a2 si<br />
aggancia direttamente alla rappresentazione creata dall'oggetto locale di f,<br />
passato come valore di r<strong>it</strong>orno (prima copia "risparmiata") e successivamente<br />
assegnato ad a2 (seconda copia "risparmiata"); per quello che riguarda la terza<br />
copia (passaggio di a1 dal chiamante alla funzione), questa è realmente<br />
esegu<strong>it</strong>a solo se il valore locale di a1 è modificato in f, altrimenti entrambi gli<br />
oggetti continuano a puntare alla stessa rappresentazione creata nel<br />
chiamante, fino a quando f termina e quindi l'a1 locale "muore" senza che la<br />
copia sia mai stata esegu<strong>it</strong>a.<br />
E' preferibile che Arep sia una struttura perchè così tutti i suoi membri sono<br />
pubblici di default. D'altra parte una rappresentazione di una classe deve<br />
essere accessibile solo dalla classe stessa. Pertanto Arep deve essere pubblica<br />
per A e privata per il "mondo esterno". Per ottenere questo, bisogna definire<br />
Arep "dentro" A (struttura-membro o struttura annidata), nella sua sezione<br />
privata (in questo modo non può essere istanziata se non da un metodo di A).<br />
Più elegantemente si può inserire in A la semplice dichiarazione di Arep e<br />
collocare esternamente la sua definizione; in questo caso, però, il suo nome<br />
deve essere qualificato:<br />
struct A::Arep { ........ };<br />
Nell'esercizio che riportiamo come esempio tentiamo una "rudimentale"<br />
implementazione di una classe "stringa", al solo scopo di fornire ulteriori<br />
chiarimenti su quanto detto (l'esercizio è eccezionalmente molto commentato).<br />
Non va utilizzato nella pratica, in quanto la Libreria Standard fornisce una<br />
classe per la gestione delle stringhe molto più completa.<br />
Nel prossimo esercizio consideriamo i tempi delle copie di oggetti del tipo<br />
"stringa" implementato come nell'esercizio precedente (cioè come handle a una<br />
rappresentazione), e li confrontiamo con i tempi ottenuti copiando le<br />
stringhe direttamente.<br />
Espressioni-operazione<br />
Quando si ha a che fare con espressioni che contengono varie operazioni,<br />
sappiamo che ogni operazione crea un oggetto temporaneo, che è usato<br />
come operando per l'operazione successiva, secondo l'ordine fissato dai cr<strong>it</strong>eri
di precedenza e associativ<strong>it</strong>à fra gli operatori. Quando tutte le operazioni di<br />
un'espressione sono state esegu<strong>it</strong>e (cioè, come si dice, l'espressione è stata<br />
valutata), tutti gli oggetti temporanei creati durante la valutazione<br />
dell'espressione vengono distrutti. Pertanto ogni oggetto temporaneo vien<br />
costru<strong>it</strong>o, passato come operando, e alla fine, distrutto, senza svolgere altra<br />
funzione.<br />
Normalmente ogni operazione viene esegu<strong>it</strong>a mediante chiamata della<br />
funzione che implementa l'overload del corrispondente operatore: questa<br />
funzione di sol<strong>it</strong>o costruisce un oggetto locale, che poi r<strong>it</strong>orna per copia al<br />
chiamante (salvo i casi in cui l'oggetto del valore di r<strong>it</strong>orno coincida con uno<br />
degli operandi, il passaggio non può essere esegu<strong>it</strong>o by reference, perchè<br />
l'oggetto locale passato non sopravvive alla funzione). E quindi, in ogni<br />
operazione, viene non solo costru<strong>it</strong>o ma anche copiato un oggetto<br />
temporaneo!<br />
Se gli oggetti coinvolti nelle operazioni sono di "grosse dimensioni" (e<br />
soprattutto se le operazioni sono molte), il costo computazionale per la<br />
costruzione e la copia degli oggetti temporanei potrebbe essere troppo<br />
elevato, e quindi bisogna trovare il modo di ottimizzare le prestazioni del<br />
programma minimizzando tale costo. In pratica bisogna ridurre al minimo:<br />
• il numero degli oggetti temporanei creati;<br />
• il numero di copie;<br />
• il numero di cicli di operazioni native in cui ogni operazione viene<br />
tradotta.<br />
La tecnica, anche in questo caso, consiste nella semplice "impostazione" di ogni<br />
operazione (senza eseguirla), tram<strong>it</strong>e un handle a una struttura, che funge<br />
da "rappresentazione" dell'operazione stessa; solo alla fine, l'intera espressione<br />
viene esegu<strong>it</strong>a tutta in una volta, senza creazione di oggetti temporanei, con il<br />
minimo numero possibile di cicli, e senza copie di passaggio. Questa tecnica<br />
sostanzialmente tratta un'espressione come unica operazione, traducendo n<br />
operatori binari in un solo operatore con n+1 operandi.<br />
Supponiamo, per esempio, di avere la seguente espressione:<br />
a = b * c + d ;<br />
e supponiamo per semplic<strong>it</strong>à (anche se non è obbligatorio) che gli oggetti: a, b,<br />
c e d appartengano tutti alla stessa classe A. Siamo in presenza di tre<br />
operazioni binarie (che, nell'ordine di esecuzione sono: moltiplicazione,<br />
somma e assegnazione), ma vogliamo, per l'occasione, trasformarle in un'unica<br />
operazione "quaternaria" che esegua, in un sol colpo, l'intera espressione.<br />
Per ottenere questo, procediamo nel seguente modo:<br />
1. definiamo un overload della moltiplicazione fra due oggetti di A, che,<br />
anzichè eseguire l'operazione, si lim<strong>it</strong>a a istanziare una struttura di<br />
appoggio (che chiamiamo M), la quale non fa altro che memorizzare i<br />
riferimenti ai due operandi (in altre parole, il suo costruttore<br />
inizializza due suoi membri, dichiarati come riferimenti ad A, come<br />
alias di b e c); a sua volta, M contiene un metodo di casting ad A, che<br />
esegue materialmente la moltiplicazione, ma che viene chiamato solo
se l'operazione rientra in un altro contesto (ricordiamo che, nella scelta<br />
dell'overload più appropriato, il compilatore cerca prima fra quelli in cui i<br />
tipi degli operandi coincidono esattamente, e poi fra quelli in cui la<br />
coincidenza si ha tram<strong>it</strong>e una conversione di tipo);<br />
2. definiamo un overload della somma fra un oggetto di M e un oggetto<br />
di A, che, anche in questo caso, si lim<strong>it</strong>a a istanziare una struttura di<br />
appoggio (che chiamiamo MS), la quale, esattamente come M, memorizza<br />
i riferimenti ai due operandi e contiene un metodo di casting ad A;<br />
3. infine, definiamo un overload del costruttore e dell'operatore di<br />
assegnazione di A, entrambi con un oggetto di MS come argomento,<br />
ed entrambi che chiamano un metodo privato di A, il quale è proprio<br />
quello deputato ad eseguire, in modo ottimizzato, l'intera operazione.
Ered<strong>it</strong>a'<br />
L'ered<strong>it</strong>à in <strong>C++</strong><br />
L'ered<strong>it</strong>à domina e governa tutti gli aspetti della v<strong>it</strong>a. Non solo nel campo della<br />
genetica, ma anche nello stesso pensiero umano, i concetti si aggregano e si<br />
trasmettono secondo relazioni di tipo "gen<strong>it</strong>ore-figlio": ogni concetto complesso<br />
non si crea ex-novo, ma deriva da concetti più semplici, che vengono "ered<strong>it</strong>ati"<br />
e integrati con ulteriori approfondimenti. Per esempio, alle elementari si impara<br />
l'ar<strong>it</strong>metica usando "mele e arance", alle medie si applicano le nozioni<br />
dell'ar<strong>it</strong>metica per studiare l'algebra, al liceo si descrivono le formule chimiche<br />
con espressioni algebriche; ma un professore di chimica non penserebbe mai di<br />
insegnare la sua materia ripartendo dalle mele e dalle arance!<br />
E quindi è lo stesso processo conosc<strong>it</strong>ivo che si sviluppa e si evolve attraverso<br />
l'ered<strong>it</strong>à. Eppure, esisteva, fino a pochi anni fa, un campo in cui questo principio<br />
generale non veniva applicato: quello dello sviluppo del software (!), che, pur<br />
utilizzando strumenti tecnologici "nuovi" e "avanzati", era in realtà in "r<strong>it</strong>ardo"<br />
rispetto a tutti gli altri aspetti della v<strong>it</strong>a: i programmatori continuavano a scrivere<br />
programmi da zero, cioè ripartivano proprio, ogni volta, dalle mele e dalle arance!<br />
In realtà le cose non stanno proprio così: anche i linguaggi di programmazione<br />
precedenti al <strong>C++</strong> (compreso il C) applicano una "specie" di ered<strong>it</strong>à nel<br />
momento in cui mettono a disposizione le loro librerie di funzioni: un<br />
programmatore può utilizzarle se soddisfano esattamente le esigenze del suo<br />
problema specifico; ma, quando ciò non avviene (come spesso cap<strong>it</strong>a), non esiste<br />
altro modo che ricopiare le funzioni e modificarle per adattarle alle proprie<br />
esigenze; questa operazione comporta il rischio di introdurre errori, che a volte<br />
sono ancora più difficili da localizzare di quando si riscrive il programma da zero!<br />
Il <strong>C++</strong> consente invece di applicare lo stesso concetto di ered<strong>it</strong>à che è nella v<strong>it</strong>a<br />
reale: gli oggetti possono assumere, per ered<strong>it</strong>à, le caratteristiche di altri<br />
oggetti e aggiungere caratteristiche proprie, esattamente come avviene<br />
nell'evoluzione del processo conosc<strong>it</strong>ivo. Ed è questa capac<strong>it</strong>à di uniformarsi alla<br />
v<strong>it</strong>a reale che rende il <strong>C++</strong> più potente degli altri linguaggi: il <strong>C++</strong> vanta<br />
caratteristiche peculiari di estendibil<strong>it</strong>à, riusabil<strong>it</strong>à, modular<strong>it</strong>à, e<br />
manutenibil<strong>it</strong>à, proprio grazie ai suoi meccanismi di uniformizzazione alla v<strong>it</strong>a<br />
reale, quali il data hiding, il polimorfismo, l'overload e, ora, l'ered<strong>it</strong>à.<br />
Classi base e derivata<br />
In <strong>C++</strong> con il termine "ered<strong>it</strong>à" si intende quel meccanismo per cui si può creare<br />
una nuova classe, detta classe figlia o derivata, trasferendo in essa tutti i<br />
membri di una classe esistente, detta classe gen<strong>it</strong>rice o base.
La relazione di ered<strong>it</strong>à si specifica nella definizione della classe derivata<br />
(supponendo che la classe base sia già stata defin<strong>it</strong>a), inserendo, dopo il nome<br />
della classe e prima della parentesi graffa di apertura, il simbolo ":" segu<strong>it</strong>o dal<br />
nome della classe base, come nel seguente esempio:<br />
class B : A { ........ } ;<br />
questa scr<strong>it</strong>tura significa che la nuova classe B possiede, oltre ai membri<br />
elencati nella propria definizione, anche quelli ered<strong>it</strong>ati dalla classe esistente<br />
A.<br />
L'ered<strong>it</strong>à procede con struttura gerarchica, o ad albero (come le<br />
subdirectories nell'organizzazione dei files) e quindi una stessa classe può<br />
essere derivata da una classe base e contemporaneamente gen<strong>it</strong>rice di una o<br />
più classi figlie. Quando ogni classe figlia ha una sola gen<strong>it</strong>rice si dice che<br />
l'ered<strong>it</strong>à è "singola", come nel seguente grafico:<br />
Se una classe figlia ha più classi gen<strong>it</strong>rici, si dice che l'ered<strong>it</strong>à è "multipla",<br />
come nel seguente grafico, dove la classe AB è figlia delle classi A3 e B4, e la<br />
classe B23 è figlia delle classi B2 e B3:
Nella definizione di una classe derivata per ered<strong>it</strong>à multipla, le due classi<br />
gen<strong>it</strong>rici vanno indicate entrambe, separate da una virgola:<br />
class AB : A3, B4 { ........ } ;<br />
Accesso ai membri della classe base<br />
Introducendo le classi, abbiamo illustrato il significato degli specificatori di<br />
accesso private: e public:, e abbiamo soltanto accennato all'esistenza di un<br />
terzo specificatore: protected:. Ora, in relazione all'ered<strong>it</strong>à, siamo in grado di<br />
descrivere completamente i tre specificatori:<br />
• private: (default) indica che tutti i membri seguenti sono privati, e non<br />
possono essere ered<strong>it</strong>ati;<br />
• public: indica che tutti i membri seguenti sono pubblici, e possono<br />
essere ered<strong>it</strong>ati;<br />
• protected: indica che tutti i membri seguenti sono protetti, nel senso<br />
che sono privati, ma possono essere ered<strong>it</strong>ati;<br />
Quindi, un membro protetto è inaccesibile dall'esterno, come i membri<br />
privati, ma può essere ered<strong>it</strong>ato, come i membri pubblici.<br />
In realtà, esiste un'ulteriore restrizione, che ha lo scopo di rendere il data-hiding<br />
ancora più profondo: l'accessibil<strong>it</strong>à dei membri ered<strong>it</strong>ati da una classe base<br />
dipende anche dallo "specificatore di accesso alla classe base", che deve<br />
essere indicato come nel seguente esempio:<br />
class B : spec.di accesso A { ........ } ;<br />
dove spec.di accesso può essere: private (default), protected o public<br />
(notare l'assenza dei due punti). Ogni membro ered<strong>it</strong>ato avrà l'accesso più<br />
"restr<strong>it</strong>tivo" fra il proprio originario e quello indicato dallo specificatore di<br />
accesso alla classe base, come è chiar<strong>it</strong>o dalla seguente tabella:<br />
Specificatori di accesso alla classe base<br />
Accesso dei membri private protected public<br />
nella classe base Accessibil<strong>it</strong>à dei membri ered<strong>it</strong>ati<br />
private: inaccessibili inaccessibili inaccessibili<br />
protected: privati protetti protetti<br />
public: privati protetti pubblici<br />
e quindi un membro ered<strong>it</strong>ato è pubblico solo se è public: nella classe base<br />
e l'accesso della classe derivata alla classe base è public.
Se una classe derivata è a sua volta gen<strong>it</strong>rice di una nuova classe, in<br />
quest'ultima l'accesso ai membri ered<strong>it</strong>ati è governato dalle stesse regole, che<br />
vengono però applicate esclusivamente ai membri della classe "intermedia",<br />
indipendentemente da come questi erano nella classe base. In altre parole, ogni<br />
classe "vede" la sua diretta gen<strong>it</strong>rice, e non si preoccupa degli altri eventuali<br />
"ascendenti".<br />
Normalmente l'accesso alla classe base è public. In alcune circostanze,<br />
tuttavia, si può volere che i suoi membri pubblici e protetti, ered<strong>it</strong>ati nella<br />
classe derivata, siano accessibili unicamente da funzioni membro e friend<br />
della classe derivata stessa: in questo caso, occorre che lo specificatore di<br />
accesso alla classe base sia private; analogamente, se si vuole che i membri<br />
pubblici e protetti di una classe base siano accessibili unicamente da funzioni<br />
membro e friend della classe derivata e di altre eventuali classi derivate da<br />
questa, occorre che lo specificatore di accesso alla classe base sia<br />
protected.<br />
Conversioni fra classi base e derivata<br />
Si dice che l'ered<strong>it</strong>à è una relazione di tipo "is a" (un cane è un mammifero, con<br />
caratteristiche in più che lo specializzano). Quindi, se due classi, A e B, sono<br />
rispettivamente base e derivata, gli oggetti di B sono (anche) oggetti di A, ma<br />
non viceversa.<br />
Ne consegue che le conversioni implic<strong>it</strong>e di tipo da B ad A (cioè da classe<br />
derivata a classe base) sono sempre ammesse (con il mantenimento dei soli i<br />
membri comuni), e in particolare ogni puntatore (o riferimento) ad A può<br />
essere assegnato o inizializzato con l'indirizzo (o il nome) di un oggetto di<br />
B. Questo permette, quando si ha a che fare con una gerarchia di classi, di<br />
definire all'inizio un puntatore generico alla classe base "capostip<strong>it</strong>e", e di<br />
assegnargli in segu<strong>it</strong>o (in base al flusso del programma) l'indirizzo di un<br />
oggetto appartenente a una qualunque classe della gerarchia. Ciò è<br />
particolarmente efficace quando si utilizzano le "funzioni virtuali", di cui<br />
parleremo nel prossimo cap<strong>it</strong>olo.<br />
La conversione opposta, da A a B, non è ammessa (a meno che B non abbia un<br />
costruttore con un argomento, di tipo A); fra puntatori (o fra riferimenti)<br />
la conversione è ammessa solo se è esplic<strong>it</strong>a, tram<strong>it</strong>e casting. Non è comunque<br />
un'operazione che abbia molto senso, tantopiù che possono insorgere errori che<br />
sfuggono al controllo del compilatore. Per esempio, supponiamo che mb sia un<br />
membro di B (e non di A):<br />
A a;<br />
B& b = (B&)a; b è un alias di a, convert<strong>it</strong>o a tipo B& - il compilatore lo accetta<br />
b.mb = ....... per il compilatore va bene (mb è membro di B), ma in realtà b è un<br />
alias di a e mb non è membro di A - access violation ?
Tornando alle conversioni implic<strong>it</strong>e da classe derivata a classe base, c'è da<br />
aggiungere che si tratta di conversioni di "grado" molto alto (altrimenti dette<br />
"conversioni banali"), cioè accettate da tutti i costrutti (come le conversioni da<br />
variabile a costante). Per esempio, il costrutto catch con tipo di argomento<br />
X "cattura" le eccezioni di tipo Y (con Y diverso da X), cioè accetta conversioni<br />
da Y a X, solo se:<br />
1. X è const Y (o viceversa, solo se l'argomento è passato by value)<br />
2. Y è una classe derivata da X<br />
mentre, per esempio, non accetta conversioni da int a long (o viceversa).<br />
Costruzione della classe base<br />
Una classe derivata non ered<strong>it</strong>a i costruttori e il distruttore della sua classe<br />
base. In altre parole ogni classe deve fornire i propri costruttori e il<br />
distruttore (oppure utilizzare quelli di default). Quanto detto vale anche per<br />
l'operatore di assegnazione, nel senso che, in sua assenza, la classe derivata<br />
usa l'operatore di default anzichè ered<strong>it</strong>are quello eventualmente presente nella<br />
classe base.<br />
Ogni volta che una classe derivata è istanziata, entrano in azione<br />
automaticamente i costruttori di tutte le classi gerarchicamente superiori,<br />
secondo lo stesso ordine gerarchico (prima la classe base "capostip<strong>it</strong>e", poi<br />
tutte le altre, e per ultima la classe che deve creare l'oggetto). Analogamente,<br />
quando l'oggetto "muore", entrano in azione automaticamente i distruttori delle<br />
stesse classi, ma procedendo in ordine inverso (per primo il distruttore<br />
dell'oggetto e per ultimo il distruttore della classe base "capostip<strong>it</strong>e").<br />
Per quello che riguarda i costruttori, il fatto che entrino in azione<br />
automaticamente comporta il sol<strong>it</strong>o problema (vedere il cap<strong>it</strong>olo sui Costruttori e<br />
Distruttori degli oggetti), che insorge ogni volta che un oggetto non è<br />
costru<strong>it</strong>o con una chiamata esplic<strong>it</strong>a: se è esegu<strong>it</strong>o il costruttore di default,<br />
tutto bene, ma come fare se si vuole (o si deve) eseguire un costruttore con<br />
argomenti?<br />
Abbiamo visto che questo problema ha una soluzione diversa per ogni circostanza:<br />
in pratica ci deve sempre essere "qualcun altro" che si occupi di chiamare il<br />
costruttore e fornigli i valori degli argomenti richiesti. Nel caso delle classi<br />
ered<strong>it</strong>ate il "qualcun altro" è rappresentato dai costruttori delle classi<br />
derivate, ciascuno dei quali deve provvedere ad attivare il costruttore della<br />
propria diretta gen<strong>it</strong>rice (non preoccupandosi invece delle eventuali altre classi<br />
gerarchicamente superiori). Come già abbiamo visto nel caso di una classe<br />
composta, il cui costruttore deve includere le chiamate dei costruttori dei<br />
membri-oggetto nella propria lista di inizializzazione, così vale anche per le<br />
classi ered<strong>it</strong>ate: ogni costruttore di una classe derivata deve includere nella
lista di inizializzazione la chiamata del costruttore della propria gen<strong>it</strong>rice.<br />
Questa operazione si chiama: costruzione della classe base.<br />
Per chiarire quanto detto, consideriamo per esempio una classe A che disponga<br />
di un costruttore con due argomenti:<br />
class A { DEFINIZIONE DEL COSTRUTTORE DI A<br />
protected: A::A(int p, float q) : m1(q), m2(p)<br />
float m1; { .... eventuali altre operazioni del<br />
int m2; costruttore di A }<br />
public: A(int,float);<br />
.... altri membri .... };<br />
Vediamo ora come si deve comportare il costruttore di una classe B, derivata<br />
di A:<br />
class B : public A { DEFINIZIONE DEL COSTRUTTORE DI B<br />
int n; B::B(int a, int b, float c) : n(b), A(a,c)<br />
public: B(int,int,float); { .... eventuali altre operazioni del<br />
.... altri membri .... }; costruttore di B }<br />
Come si può notare, il costruttore di B deve inserire la chiamata di quello di A<br />
nella propria lista di inizializzazione (se non lo fa, e il costruttore di A esiste,<br />
cioè non è chiamato di default, il <strong>C++</strong> dà errore); ovviamente l'ordine<br />
originario degli argomenti del costruttore di A va rigorosamente mantenuto.<br />
Nel caso che B sia a sua volta gen<strong>it</strong>rice di un'altra classe C, il costruttore di C<br />
deve includere nella propria lista di inizializzazione il termine: B(a,b,c), cioè la<br />
chiamata del costruttore di B, ma non il termine A(a,c), chiamata del<br />
costruttore di A.<br />
Il costruttore di una classe derivata non può inizializzare direttamente i<br />
membri ered<strong>it</strong>ati dalla classe base: rifacendoci all'esempio, il costruttore di<br />
B non può inizializzare i membri m1 e m2 ered<strong>it</strong>ati da A, ma lo può fare solo<br />
indirettamente, invocando il costruttore di A.<br />
Notiamo infine che il costruttore di A è dichiarato public: ciò significa che la<br />
classe A può essere anche istanziata indipendentemente. Se però fosse<br />
dichiarato protected, il costruttore di B lo "vedrebbe" ancora e quindi<br />
potrebbe invocarlo ugualmente nella propria lista di inizializzazione, ma gli<br />
utenti esterni non potrebbero accedervi. Un modo per occultare una classe<br />
base (rendendola disponibile solo per le sue classi derivate) è pertanto quello<br />
di dichiarare tutti i suoi costruttori nella sezione protetta.
Regola della dominanza<br />
Finora, negli esempi abbiamo attribu<strong>it</strong>o sempre (e deliberatamente) nomi diversi<br />
ai membri delle classi. Ci chiediamo adesso: cosa succede nel caso che esista un<br />
membro della classe derivata con lo stesso nome di un membro della sua<br />
classe base? Può insorgere un confl<strong>it</strong>to fra i nomi, oppure (nel caso che il<br />
membro sia un metodo) si applicano le regole dell'overload? La risposta ad<br />
entrambe le domande è: NO. In realtà si applica una regola diversa, detta regola<br />
della "dominanza": viene sempre scelto il membro che appartiene alla stessa<br />
classe a cui appartiene l'oggetto.<br />
Per esempio, se due classi, A e B, sono rispettivamente base e derivata e<br />
possiedono entrambe un membro di nome mem, l'operazione:<br />
ogg.mem<br />
seleziona il membro mem di A se ogg è istanza di A, oppure il membro mem<br />
di B se ogg è istanza di B.<br />
Volendo invece selezionare forzatamente uno dei due, bisogna qualificare il<br />
nome del membro comune mediante il sol<strong>it</strong>o operatore di risoluzione della<br />
visibil<strong>it</strong>à. Per esempio:<br />
ogg.A::mem<br />
seleziona sempre il membro mem di A, anche se ogg è istanza di B.<br />
La regola della dominanza può essere sfruttata per modificare i membri<br />
ered<strong>it</strong>ati (soprattutto per quello che riguarda i metodi): l'unico sistema è quello<br />
di ridichiararli con lo stesso nome, garantendosi così che saranno i nuovi<br />
membri, e non gli originari, ad essere utilizzati in tutti gli oggetti della classe<br />
derivata. Non è comunque possibile diminuire il numero dei membri ered<strong>it</strong>ati:<br />
le funzioni "indesiderate" potrebbero essere ridefin<strong>it</strong>e con "corpo nullo", ma non<br />
si può fare di più.<br />
Ered<strong>it</strong>à e overload<br />
Se vi sono due metodi con lo stesso nome, uno della classe base e l'altro della<br />
classe derivata, abbiamo visto che vale la regola della dominanza e non quella<br />
dell'overload. Ciò è vero anche se le due funzioni hanno tipi di argomenti<br />
diversi e, in base all'overload, verrebbe selezionata la funzione che appartiene<br />
alla classe a cui non appartiene l'oggetto.<br />
Per fare un esempio (riprendendo quello precedente), supponiamo che ogg sia<br />
un'istanza della classe derivata B, e che entrambe le classi possiedano un<br />
metodo, di nome fun, con un argomento di tipo double nella classe A e di<br />
tipo int nella classe B:<br />
A::fun(double) B::fun(int)<br />
in esecuzione, la chiamata: ogg.fun(10.7)
non considera l'overload e seleziona comunque la fun di B con argomento int,<br />
operando una conversione implic<strong>it</strong>a da 10.7 a 10<br />
Questo comportamento deriva in realtà da una regola più generale: l'overload<br />
non si applica mai fra funzioni che appartengono a due diversi amb<strong>it</strong>i di<br />
visibil<strong>it</strong>à, anche se i due amb<strong>it</strong>i corrispondono a una classe base e alla sua<br />
classe derivata e quindi la funzione della classe base è accessibile nella<br />
classe derivata per ered<strong>it</strong>à.<br />
La dichiarazione using<br />
Abbiamo già incontrato l'istruzione di "using-declaration", parlando dei<br />
namespace, e sappiamo che serve a rendere accessibile un membro di un<br />
namespace nello stesso amb<strong>it</strong>o in cui è inser<strong>it</strong>a l'istruzione stessa.<br />
Analogamente, una using-declaration si può inserire nella definizione di una<br />
classe derivata per trasferire nel suo amb<strong>it</strong>o un membro della classe base.<br />
Riprendendo il sol<strong>it</strong>o esempio, supponiamo ora di inserire nella definizione di B<br />
l'istruzione:<br />
using A::fun;<br />
(notare che il nome fun appare da solo, senza argomenti e senza parentesi).<br />
Adesso sì che entrambe le funzioni sono nello stesso amb<strong>it</strong>o di visibil<strong>it</strong>à e<br />
quindi si può applicare l'overload. Pertanto la chiamata:<br />
ogg.fun(10.7)<br />
selezionerà correttamente la funzione con argomento double, cioè la fun di A.<br />
Una using-declaration, se non si riferisce a un namespace, può essere<br />
inser<strong>it</strong>a esclusivamente nella definizione di una classe derivata e può riferirsi<br />
esclusivamente a un membro della classe base. Non sono ammessi altri usi.<br />
Una using-directive può essere usata solo con i namespace.<br />
Una using-declaration, inser<strong>it</strong>a nella definizione di una classe derivata, può<br />
avere un altro effetto, oltre a quello di rendere possibile l'overload: permette di<br />
modificare l'accesso ai membri della classe base. Infatti, se un membro della<br />
classe base è protetto (non se è privato), oppure se lo specificatore di<br />
accesso alla classe base è protected o private, e la using-declaration è<br />
inser<strong>it</strong>a nella sezione pubblica della classe derivata, quel membro diventa<br />
pubblico. Questo fatto può essere utilizzato per specificare interfacce che<br />
mettono a disposizione degli utenti parti selezionate di una classe.
Ered<strong>it</strong>à multipla e classi basi virtuali<br />
Supponiamo che una certa classe C derivi, per ered<strong>it</strong>à multipla, da due classi<br />
gen<strong>it</strong>rici B1 e B2. Nella definizione di C, il nome di ognuna delle due classi<br />
base deve essere preceduto dal rispettivo specificatore di accesso (se non è<br />
private, che, ricordiamo, è lo specificatore di default). Per esempio:<br />
class C : protected B1, public B2 { ........ } ;<br />
in questo caso, nella classe C, i membri ered<strong>it</strong>ati da B1 sono tutti protetti,<br />
mentre quelli ered<strong>it</strong>ati da B2 rimangono come erano nella classe base<br />
(protetti o pubblici).<br />
Il costruttore di C deve costruire entrambe le classi gen<strong>it</strong>rici, cioè deve<br />
includere, nella propria lista di inizializzazione, entrambe le chiamate dei<br />
costruttori di B1 e di B2, o meglio, deve includere quei costruttori di B1 o di<br />
B2 che non sono di default, considerati indipendentemente (e quindi, a secondo<br />
delle circostanze, deve includerli entrambi, o uno solo, o nessuno). Anche nel caso<br />
che la classe C non abbia costruttori, è obbligatorio definire esplic<strong>it</strong>amente il<br />
costruttore di default di C (anche con "corpo nullo"), con il solo comp<strong>it</strong>o di<br />
costruire le classi gen<strong>it</strong>rici (questa operazione non è richiesta solo se anche le<br />
classi gen<strong>it</strong>rici sono entrambe istanziate mediante i loro rispettivi costruttori<br />
di default).<br />
Supponiamo ora che le classi B1 e B2 derivino a loro volta da un'unica classe<br />
base A. Siccome ogni classe derivata si deve occupare solo della sua diretta<br />
gen<strong>it</strong>rice, il comp<strong>it</strong>o di costruire la classe A è delegato sia a B1 che a B2, ma<br />
non a C. Per cui, quando viene istanziata C, sono costru<strong>it</strong>e direttamente<br />
soltanto le sue dirette gen<strong>it</strong>rici B1 e B2, ma ciascuna di queste costruisce a<br />
sua volta (e separatamente) A; in altre parole, ogni volta che è istanziata C, la<br />
sua classe "nonna" A viene costru<strong>it</strong>a due volte (classi base "replicate"),<br />
come è illustrato dalla seguente figura:<br />
La replicazione di una classe base può causare due generi di problemi:
• occupazione doppia di memoria, che può essere poco "piacevole",<br />
soprattutto se gli oggetti di C sono molti e il sizeof(A) è grande;<br />
• errore di ambigu<strong>it</strong>à: se gli oggetti di C non accedono mai direttamente ai<br />
membri ered<strong>it</strong>ati da A, tutto bene; ma, se dovesse cap<strong>it</strong>are il contrario, il<br />
compilatore darebbe errore, non sapendo se accedere ai membri<br />
ered<strong>it</strong>ati tram<strong>it</strong>e B1 o tram<strong>it</strong>e B2.<br />
Il secondo problema può essere risolto (in un modo però poco "brillante")<br />
qualificando ogni volta i membri ered<strong>it</strong>ati da A. Per esempio, se ogg è<br />
un'istanza di C e ma è un membro ered<strong>it</strong>ato da A:<br />
ogg.B1::ma indica che ma è ered<strong>it</strong>ato tram<strong>it</strong>e B1<br />
ogg.B2::ma indica che ma è ered<strong>it</strong>ato tram<strong>it</strong>e B2<br />
Entrambi i problemi, invece, si possono risolvere definendo A come classe base<br />
"virtuale": questo si ottiene inserendo, nelle definizioni di tutte le classi<br />
derivate, la parola-chiave virtual accanto allo specificatore di accesso alla<br />
classe base. Esempio:<br />
class B1 : virtual protected A { ........ } ;<br />
class B2 : virtual public A { ........ } ;<br />
La parola-chiave virtual non ha alcun effetto sulle istanze dirette di B1 e di<br />
B2: ciascuna di esse costruisce la propria classe base normalmente, come se<br />
virtual non fosse specificata. Ma, se viene istanziata la classe C, derivata da<br />
B1 e da B2 per ered<strong>it</strong>à multipla, viene creata una sola copia dei membri<br />
ered<strong>it</strong>ati da A, della cui inizializzazione deve essere lo stesso costruttore di<br />
C ad occuparsene (contravvenendo alla regola generale che vuole che ogni figlia<br />
si occupi solo delle sue immediate gen<strong>it</strong>rici); in altre parole, nella lista di<br />
inizializzazione del costruttore di C devono essere incluse le chiamate, non<br />
solo dei costruttori di B1 e di B2, ma anche del costruttore di A. In sostanza<br />
la parola-chiave virtual dice a B1 e B2 di non prendersi cura di A quando<br />
viene creato un oggetto di C, perchè sarà la stessa classe "nipote" C ad<br />
occuparsi della sua "nonna".<br />
Pertanto, se una classe base è defin<strong>it</strong>a virtuale da tutte le sue classi<br />
derivate, viene ev<strong>it</strong>ata la replicazione e si realizza la cosidetta ered<strong>it</strong>à a<br />
diamante, rappresentata dal seguente grafico:
Sulla reale efficacia dell'ered<strong>it</strong>à multipla esistono a tutt'oggi pareri discordanti:<br />
qualcuno sostiene che bisognerebbe usarla il meno possibile, perchè raramente<br />
può essere utile ed è meno sicura e più restr<strong>it</strong>tiva dell'ered<strong>it</strong>à singola (per<br />
esempio non si può convertire un puntatore da classe base virtuale a classe<br />
derivata); altri r<strong>it</strong>engono al contrario che l'ered<strong>it</strong>à multipla possa essere<br />
necessaria per la risoluzione di molti problemi progettuali, fornendo la possibil<strong>it</strong>à<br />
di associare due classi altrimenti non correlate come parti dell'implementazione di<br />
una terza classe. Questo fatto è evidente in modo particolare quando le due<br />
classi giocano ruoli logicamente distinti, come vedremo in un esempio riportato<br />
nel prossimo cap<strong>it</strong>olo, a propos<strong>it</strong>o delle classi base astratte.
Polimorfismo<br />
Late binding e polimorfismo<br />
Abbiamo già sent<strong>it</strong>o parlare di late binding trattando dei puntatori a funzione:<br />
l'aggancio fra il programma chiamante e la funzione chiamata é r<strong>it</strong>ardato<br />
dal momento dalla compilazione a quello dell'esecuzione, perché solo in quella<br />
fase il <strong>C++</strong> può conoscere la funzione selezionata, in base ai dati che<br />
condizionano il flusso del programma. La scelta, tuttavia, avviene all'interno di un<br />
insieme ben defin<strong>it</strong>o di funzioni, diverse l'una dall'altra non solo nel contenuto<br />
ma anche nel nome.<br />
Conosciamo anche il significato di polimorfismo: funzioni-membro con lo<br />
stesso nome e gli stessi argomenti, ma appartenenti a oggetti di classi<br />
diverse. Nella terminologia del <strong>C++</strong>, polimorfismo significa: mandare agli<br />
oggetti lo stesso messaggio ed ottenere da essi comportamenti diversi, sul<br />
modello della v<strong>it</strong>a reale, in cui termini simili determinano azioni diverse, in base al<br />
contesto in cui vengono utilizzati.<br />
Tuttavia il polimorfismo che abbiamo esaminato finora é solo apparente: il<br />
puntatore "nascosto" this, introdotto dal compilatore, differenzia gli<br />
argomenti delle funzioni, e quindi non si tratta realmente di polimorfismo, ma<br />
soltanto di overload, cioè di un meccanismo che, come sappiamo, permette al<br />
<strong>C++</strong> di riconoscere e selezionare la funzione già in fase di compilazione<br />
(early binding).<br />
Il "vero" polimorfismo, nella pienezza del suo significato "filosofico", deve essere<br />
associato al late binding: la differenziazione di comportamento degli oggetti in<br />
risposta allo stesso messaggio non deve essere statica e predefin<strong>it</strong>a, ma<br />
dinamica, cioè deve essere determinata dal contesto del programma in fase di<br />
esecuzione. Vedremo che ciò é realizzabile solo nell'amb<strong>it</strong>o di una stessa famiglia<br />
di classi, e quindi il "vero" polimorfismo non può prescindere dall'ered<strong>it</strong>à e si<br />
applica a funzioni-membro, con lo stesso nome e gli stessi argomenti, che<br />
appartengono sia alla classe base che alle sue derivate.<br />
Ambigu<strong>it</strong>à dei puntatori alla classe base<br />
Prendiamo il caso di due classi, di nome A e B, dove A é la classe base e B una<br />
sua derivata. Consideriamo due istanze, a e b, rispettivamente di A e di B.<br />
Supponiamo inoltre che entrambe le classi contengano una funzione-membro,<br />
di nome display(), non ered<strong>it</strong>ata da A a B, ma ridefin<strong>it</strong>a in B (traducendo<br />
letteralmente il termine inglese "overridden", si suole dire, in questi casi, che la
funzione display() di A é "scavalcata" nella classe B, ma è un termine<br />
"orrendo", che non useremo mai).<br />
Sappiamo che, per la regola della dominanza, ogni volta il compilatore seleziona<br />
la funzione che appartiene alla stessa classe a cui appartiene l'oggetto (cioè la<br />
classe indicata nell'istruzione di definizione dell'oggetto), e quindi:<br />
a.display() seleziona la funzione-membro di A<br />
b.display() seleziona la funzione-membro di B<br />
Supponiamo ora di definire un puntatore ptr alla classe A e di inizializzarlo<br />
con l'indirizzo dell'oggetto a:<br />
A* ptr = &a;<br />
anche in questo caso la funzione può essere selezionata senza ambigu<strong>it</strong>à e<br />
quindi l'istruzione:<br />
ptr->display()<br />
accede alla funzione display() della classe A.<br />
Abbiamo visto, tuttavia, che a un puntatore defin<strong>it</strong>o per una classe base,<br />
possono essere assegnati indirizzi di oggetti di classi derivate, e quindi il<br />
seguente codice é valido:<br />
if(.......) ptr = &a;<br />
else ptr = &b;<br />
in questo caso, dinanzi all'eventuale istruzione:<br />
ptr->display()<br />
come si regola il compilatore, visto che l'oggetto a cui punta ptr é determinato in<br />
fase di esecuzione? Di default, vale ancora la regola della dominanza e quindi,<br />
essendo ptr defin<strong>it</strong>o come puntatore alla classe A, viene selezionata la<br />
funzione display() della classe A, anche se in esecuzione l'oggetto puntato<br />
dovesse appartenere alla classe B.<br />
Funzioni virtuali<br />
Negli esempi esaminati finora, la funzione-membro display() é selezionata in<br />
fase di compilazione (early binding); ciò avviene anche nell'ultimo caso,<br />
sebbene l'oggetto associato alla funzione sia determinato solo in fase di<br />
esecuzione.<br />
Se però, nella definizione della classe A, la funzione display() é dichiarata<br />
con lo specificatore "virtual", il <strong>C++</strong> rinvia la scelta della funzione appropriata<br />
alla fase di esecuzione (late binding). In questo modo si realizza il<br />
polimorfismo: lo stesso messaggio (display), inviato a oggetti di classi<br />
diverse, induce a diversi comportamenti, in funzione dei dati del programma.
Un tipo dotato di funzioni virtuali è detto: tipo polimorfo. Per ottenere un<br />
comportamento polimorfo in <strong>C++</strong>, bisogna esclusivamente operare all'interno di<br />
una gerarchia di classi e alle seguenti condizioni:<br />
1. la dichiarazione delle funzioni-membro della classe base (interessate<br />
al polimorfismo) deve essere specificata con la parola-chiave virtual;<br />
non è obbligatorio (ma neppure vietato) ripetere la stessa parola-chiave<br />
nelle dichiarazioni delle funzioni-membro delle classi derivate (di<br />
sol<strong>it</strong>o lo si fa per migliorare la leggibil<strong>it</strong>à del programma);<br />
2. una funzione dichiarata virtual deve essere sempre anche defin<strong>it</strong>a<br />
(senza virtual) nella classe base (al contrario delle normali funzioni che<br />
possono essere dichiarate senza essere defin<strong>it</strong>e, quando non si usano);<br />
invece, una classe derivata non ha l'obbligo di ridichiarare (e<br />
ridefinire) tutte le funzioni virtuali della classe base, ma solo quelle<br />
che le servono (quelle non ridefin<strong>it</strong>e vengono ered<strong>it</strong>ate);<br />
3. gli oggetti devono essere manipolati soltanto attraverso puntatori (o<br />
riferimenti); quando invece si accede a un oggetto direttamente, il suo<br />
tipo è già noto al compilatore e quindi il polimorfismo in esecuzione<br />
non si attua.<br />
Si può anche aggirare la virtualizzazione, qualificando il nome della<br />
funzione con il sol<strong>it</strong>o operatore di risoluzione della visibil<strong>it</strong>à. Esempio:<br />
ptr->A::display();<br />
in questo caso esegue la funzione della classe base A, anche se questa è stata<br />
dichiarata virtual e ptr punta a un oggetto di B.<br />
Tabelle delle funzioni virtuali<br />
Riprendiamo l'esempio precedente, aggiungendo una nuova classe derivata da<br />
A, che chiamiamo C; questa classe non ridefinisce la funzione display() ma<br />
la ered<strong>it</strong>a da A (come appare nella seguente tabella, dove il termine fra parentesi<br />
quadre è facoltativo):<br />
class A { class B : public A { class C : public A {<br />
........ public: ...... ......... public: ...... ..............<br />
virtual void display(); [virtual] void display(); };<br />
}; };<br />
Se ora assegniamo a ptr l'indirizzo di un oggetto che, in base al flusso dei<br />
dati in esecuzione, può essere indifferentemente di A, di B o di C, dinanzi a<br />
istruzioni del tipo:
ptr->display()<br />
il <strong>C++</strong> seleziona in esecuzione la funzione giusta, cioè quella di A se l'oggetto<br />
appartiene ad A o a C, quella di B se l'oggetto appartiene a B.<br />
Infatti il <strong>C++</strong> prepara, in fase di compilazione, delle tabelle, dette "Tabelle<br />
virtuali" o vtables, una per la classe base e una per ciascuna classe<br />
derivata, in cui sistema gli indirizzi di tutte le funzioni dichiarate virtuali<br />
nella classe base; aggiunge inoltre un nuovo membro in ogni classe, detto<br />
vptr, che punta alla corrispondente vtable.<br />
Il seguente diagramma chiarisce quanto detto, nel caso del nostro esempio:<br />
In questo modo, in fase di esecuzione il <strong>C++</strong> può risalire, dall'indirizzo<br />
contenuto nel membro vptr dell'oggetto puntato da ptr (vptr è un datomembro<br />
e quindi è realmente replicato in ogni oggetto), all'indirizzo della<br />
corretta funzione da selezionare.<br />
Costruttori e distruttori virtuali<br />
I distruttori possono essere virtualizzati, anzi, in certe condizioni è<br />
praticamente indispensabile che lo siano, se si vuole assicurare una corretta<br />
ripul<strong>it</strong>ura della memoria. Infatti, proseguendo con il nostro esempio e supponendo<br />
stavolta che gli oggetti siano allocati nell'area heap, l'istruzione:<br />
delete ptr;<br />
assicura che sia invocato il distruttore dell'oggetto realmente puntato da ptr<br />
solo se il distruttore della classe base A è stato dichiarato virtual; altrimenti<br />
chiamerebbe comunque il distruttore di A, anche quando, in esecuzione, è<br />
stato assegnato a ptr l'indirizzo di un oggetto di una classe derivata.<br />
Viceversa i costruttori non possono essere virtualizzati, per il semplice motivo<br />
che, quando è invocato un costruttore, l'oggetto non esiste ancora e quindi non<br />
può neppure esistere un puntatore con il suo indirizzo. In altre parole, la<br />
nozione di "puntatore a costruttore" è una contraddizione in termini.
Tuttavia è possibile aggirare questo ostacolo virtualizzando, non il costruttore,<br />
ma un altro metodo della classe, defin<strong>it</strong>o in modo che crei un nuovo oggetto<br />
della stessa classe (si deve comunque partire da un oggetto già esistente) e si<br />
comporti quindi come un "costruttore polimorfo", in cui il tipo dell' oggetto<br />
costru<strong>it</strong>o è determinato in fase di esecuzione.<br />
Vediamo ora un'applicazione pratica di quanto detto. Riprendendo il nostro sol<strong>it</strong>o<br />
esempio, supponiamo che la classe base A sia provvista di un metodo<br />
pubblico così defin<strong>it</strong>o:<br />
A* A::clone( ) { return new A(*this); }<br />
come si può notare, la funzione-membro clone crea un nuovo oggetto<br />
nell'area heap, invocando il costruttore di copia di A (oppure quello di default<br />
se la classe ne è sprovvista) con argomento *this, e ne rest<strong>it</strong>uisce l'indirizzo.<br />
Ogni oggetto può pertanto generare una copia di se stesso chiamando la<br />
clone. Analogamente definiamo una funzione-membro clone della classe<br />
derivata B:<br />
A* B::clone( ) { return new B(*this); }<br />
Se ora virtualizziamo la funzione clone, inserendo nella definizione della<br />
classe base A la dichiarazione:<br />
virtual A* clone();<br />
troviamo in B la ridefinizione di una funzione virtuale, in quanto sono<br />
coincidenti il nome (clone), la lista degli argomenti (void) e il tipo del valore<br />
di r<strong>it</strong>orno (A*), e quindi possiamo ottenere da tale funzione un comportamento<br />
polimorfo. In particolare l'istruzione:<br />
A* pnew = ptr->clone();<br />
crea un nuovo oggetto nell'area heap e inizializza pnew con l'indirizzo di tale<br />
oggetto; il tipo di questo nuovo oggetto è però deciso solo in fase di<br />
esecuzione (comportamento polimorfo della funzione clone) e coincide con il<br />
tipo puntato da ptr.<br />
Scelta fra veloc<strong>it</strong>à e polimorfismo<br />
Il processo early binding è più veloce del late binding, in quanto impegna il<br />
<strong>C++</strong> solo in compilazione e non crea nuove tabelle o nuovi puntatori; per<br />
questo motivo la specifica virtual non è di default. Tuttavia è spesso utile<br />
rinunciare a un po' di veloc<strong>it</strong>à in cambio di altri vantaggi, come il polimorfismo,<br />
grazie al quale è il <strong>C++</strong> e non il programmatore a doversi preoccupare di<br />
selezionare ogni volta il comportamento appropriato in risposta allo stesso<br />
messaggio.
Classi astratte<br />
Nel cap<strong>it</strong>olo "Tipi defin<strong>it</strong>i dall'utente" abbiamo ammesso di utilizzare una<br />
nomenclatura "vecchia" identificando indiscriminatamente con il termine "tipo<br />
astratto" qualunque tipo non nativo del linguaggio. E' giunto il momento di<br />
precisare meglio cosa si intenda in <strong>C++</strong> per "tipo astratto".<br />
Una classe base, se defin<strong>it</strong>a con funzioni virtuali, "spiega" cosa sono in grado<br />
di fare gli oggetti delle sue classi derivate. Nel nostro esempio, la classe base<br />
A "spiega" che tutti gli oggetti del programma possono essere visualizzati,<br />
ognuno attraverso la propria funzione display(). In sostanza la classe base<br />
fornisce, oltre alle funzioni, anche uno "schema di comportamento" per le<br />
classi derivate.<br />
Estremizzando questo concetto, si può creare una classe base con funzioni<br />
virtuali senza codice, dette funzioni virtuali pure. Non avendo codice, queste<br />
funzioni servono solo da "schema di comportamento" per le classi derivate<br />
e vanno dichiarate nel seguente modo:<br />
virtual void display() = 0;<br />
(nota: questo è l'unico caso in <strong>C++</strong> di una dichiarazione con inizializzazione!)<br />
in questo esempio, si definisce che ogni classe derivata avrà una sua funzione<br />
di visualizzazione, chiamata sempre con lo stesso nome, e selezionata ogni volta<br />
correttamente grazie al polimorfismo.<br />
Una classe base con almeno una funzione virtuale pura è detta classe base<br />
astratta, perché definisce la struttura di una gerarchia di classi, ma non può<br />
essere istanziata direttamente.<br />
A differenza dalle normali funzioni virtuali, le funzioni virtuali pure devono<br />
essere ridefin<strong>it</strong>e tutte nelle classi derivate (anche con "corpo nullo", quando<br />
non servono). Se una classe derivata non ridefinisce anche una sola funzione<br />
virtuale pura della classe base, rimane una classe astratta e non può ancora<br />
essere istanziata (a questo punto, una sua eventuale classe derivata, per<br />
diventare "concreta", è sufficiente che ridefinisca l'unica funzione virtuale<br />
pura rimasta).<br />
Le classi astratte sono di importanza fondamentale nella programmazione in<br />
<strong>C++</strong> ad alto livello, orientata a oggetti. Esse presentano agli utenti delle<br />
interfacce "pure", senza il vincolo degli aspetti implementativi, che sono invece<br />
forn<strong>it</strong>i dalle loro classi derivate. Una gerarchia di classi, che deriva da una o<br />
più classi astratte, può essere costru<strong>it</strong>a in modo "incrementale", nel senso di<br />
permettere il "raffinamento" di un progetto, aggiungendo via via nuove classi<br />
senza la necess<strong>it</strong>à di modificare la parte preesistente. Gli utenti non sono coinvolti,<br />
se non vogliono, in questo processo di "raffinamento incrementale", in quanto
vedono sempre la stessa interfaccia e utilizzano sempre le stesse funzioni (che,<br />
grazie al polimorfismo, saranno sempre selezionate sull'oggetto appropriato).<br />
Un rudimentale sistema di figure geometriche<br />
A puro t<strong>it</strong>olo esemplificativo dei concetti finora esposti, si è tentato di progettare<br />
l'implementazione di un sistema (molto "rudimentale") di figure geometriche<br />
piane. Abbiamo scelto 6 figure, a ciascuna delle quali abbiamo fatto corrispondere<br />
una classe:<br />
punto classe Dot<br />
linea classe Line<br />
triangolo classe Triangle<br />
rettangolo classe Rect<br />
quadrato classe Square<br />
cerchio classe Circle<br />
Tutte queste classi fanno parte di una gerarchia, al cui vertice si trova un'unica<br />
classe base astratta, di nome Shape, che contiene esclusivamente un<br />
distruttore virtuale (con "corpo nullo") e alcune funzioni virtuali pure. La<br />
classe Shape presenta, quindi, una pura interfaccia, non possedendo datimembro<br />
nè funzioni-membro implementate, e non può essere istanziata (il<br />
compilatore darebbe errore).<br />
Dalla classe Shape derivano due classi, anch'esse astratte, di nome<br />
Polygon e Regular (per la precisione, Polygon non è astratta, ma il suo<br />
costruttore è inser<strong>it</strong>o nella sezione protetta e quindi non può essere<br />
istanziata dall'esterno; Regular, invece, è astratta, in quanto non ridefinisce<br />
tutte le funzioni virtuali pure di Shape).<br />
Finalmente, le classi "concrete" derivano tutte da Polygon e Regular: Dot,<br />
Line, Triangle e Rect derivano da Polygon; Circle deriva da Regular;<br />
Square deriva da Polygon e Regular, per ered<strong>it</strong>à multipla. Si configura così<br />
il seguente schema:
A queste classi si aggiungono due strutture di appoggio: Point, che fornisce le<br />
coordinate dei punti sul piano, e Shape_Error, per la gestione delle eccezioni. Il<br />
tutto è racchiuso in un unico namespace, di nome mini_graphics, che<br />
contiene anche alcune costanti e alcune funzioni esterne alle classi, fra cui due<br />
operatori in overload per la lettura e scr<strong>it</strong>tura di oggetti di Point. Il fatto che<br />
tutte le componenti del sistema appartengano a un namespace permette di<br />
ev<strong>it</strong>are i potenziali confl<strong>it</strong>ti di nomi, in ver<strong>it</strong>à molto comuni, come Line e Rect,<br />
con nomi uguali forn<strong>it</strong>i da altre librerie ed eventualmente messi a disposizione da<br />
queste tram<strong>it</strong>e using-directives. Volendo, l'utente provvederà ad inserire, negli<br />
amb<strong>it</strong>i locali del main e delle sue funzioni, le using-declarations necessarie;<br />
a questo propos<strong>it</strong>o viene forn<strong>it</strong>o un header-file contenente tutte le usingdeclarations<br />
dei nomi del namespace che possono essere visti dall'utente.<br />
Il sistema è accessibile dall'esterno esclusivamente attraverso le funzioni virtuali<br />
pure di Shape, ridefin<strong>it</strong>e nelle classi "concrete"; per cui, defin<strong>it</strong>o un<br />
puntatore a Shape, è possibile tram<strong>it</strong>e questo sfruttare il polimorfismo e<br />
chiamare ogni volta la funzione-membro dell'oggetto "reale" selezionato in<br />
fase di esecuzione. Non tutte le funzioni, però, sono compatibili con tutti gli<br />
oggetti (per esempio una funzione che fornisce due punti può essere usata per<br />
definire una linea o un rettangolo, ma non per definire un triangolo); d'altra<br />
parte, in ogni classe "concreta", tutte le funzioni virtuali pure vanno<br />
ridefin<strong>it</strong>e, e ciò ha cost<strong>it</strong>u<strong>it</strong>o un problema, che poteva essere risolto in due modi:<br />
1. in ogni classe, ridefinire con "corpo nullo" tutte le funzioni incompatibili<br />
(ma in questo modo l'utente non sarebbe stato informato del suo errore);<br />
2. oppure ridefinire tali funzioni in modo da sollevare un'eccezione (ed è<br />
quello che è stato fatto): le funzioni di questo tipo sono state collocate<br />
nelle classi "intermedie" Polygon e Regular, e quindi non hanno avuto<br />
bisogno di essere ridefin<strong>it</strong>e nelle classi "concrete" (dove sono ridefin<strong>it</strong>e<br />
solo le funzioni "compatibili").<br />
Le funzioni virtuali di Shape sono in tutto 11, divise in 4 gruppi e<br />
precisamente:<br />
• funzioni set (con 5 overloads) per impostare i parametri caratteristici di<br />
ogni figura (per esempio, le coordinate del bottom-left-corner e del topright-corner<br />
di un rettangolo); all'inizo, i costruttori (di default) delle<br />
classi "concrete" generano figure precost<strong>it</strong>u<strong>it</strong>e;
• 4 funzioni get... per estrarre informazioni dalle figure (per esempio, le<br />
coordinate di un vertice di un poligono, oppure la lunghezza del diametro<br />
di un cerchio ecc...);<br />
• una funzione display per la visualizzazione (non grafica) dei parametri di<br />
una figura (per esempio le coordinate dei punti estremi di una linea o dei<br />
quattro vertici di un quadrato ecc...);<br />
• una funzione copy_from per copiare una figura da un'altra; se si tenta<br />
la copia fra due figure diverse è sollevata un'eccezione, salvo in questi<br />
casi:<br />
1. copia da quadrato a rettangolo (ammessa in quanto il quadrato<br />
è un caso particolare di rettangolo);<br />
2. copia da quadrato a cerchio (ricava il cerchio iscr<strong>it</strong>to al<br />
quadrato);<br />
3. copia da cerchio a quadrato (ricava il quadrato circoscr<strong>it</strong>to al<br />
cerchio)<br />
In effetti, si tratta di un sistema assolutamente "minimale". Ma il nostro scopo non<br />
era quello di generare un prodotto fin<strong>it</strong>o, bensì di mostrare "come si pone il primo<br />
mattone di una casa". Infatti (e questa è la caratteristica principale della<br />
programmazione a oggetti che sfrutta il polimorfismo) il sistema si presta ad<br />
essere agevolmente incrementato in maniera modulare, in tre direzioni:<br />
• si possono aggiungere nuove figure (e cioè nuove classi) che<br />
ridefiniscono le stesse funzioni virtuali pure di Shape, e quindi si può<br />
ampliare la gerarchia senza modificare nulla dell'esistente;<br />
• si possono aggiungere nuove funzional<strong>it</strong>à (per esempio, trasformazioni di<br />
coordinate, traslazioni, rotazioni, variazioni della scala ecc...); in questo<br />
caso bisogna apportare qualche modifica al progetto, ma pur sempre in<br />
maniera "incrementale";<br />
• si possono creare infine altre gerarchie di classi, che eseguono operazioni<br />
"specializzate", come per esempio la visualizzazione grafica delle figure su<br />
un dato dispos<strong>it</strong>ivo (come vedremo nell'esercizio della prossima sezione); il<br />
fatto importante è che l'introduzione delle nuove gerarchie non comporta<br />
alcuna modifica della gerarchia Shape, ma si lim<strong>it</strong>a a creare degli<br />
"agganci" ad essa, preservando il requis<strong>it</strong>o fondamentale di minimizzare<br />
le dipendenze fra i moduli, che è alla base di una corretta<br />
programmazione. Infatti, per come è stata progettata, la gerarchia Shape<br />
è "device-independent" è può essere visualizzata su qualunque<br />
dispos<strong>it</strong>ivo grafico.<br />
Particolare cura è stata dedicata alla gestione delle eccezioni. Sono stati<br />
individuati quattro tipi di errori possibili:<br />
• errori di input nell'inserimento dei dati (per esempio, dig<strong>it</strong>azione di caratteri<br />
diversi quando sono richieste cifre numeriche);<br />
• tentativi di generare figure geometriche "improprie" (per esempio un<br />
triangolo con i tre vertici allineati);<br />
• tentativi di eseguire funzioni incompatibili con la figura selezionata;<br />
• tentativi di eseguire copie fra figure diverse (salvo nei casi sopraelencati).<br />
Osserviamo, per concludere, che l'introduzione di una classe derivata per<br />
ered<strong>it</strong>à multipla (Square) ha generato qualche piccolo problema aggiuntivo e<br />
richiesto una particolare attenzione:
• anz<strong>it</strong>utto, per ev<strong>it</strong>are la replicazione della classe base, si è dovuto<br />
inserire virtual nelle specifiche di accesso a Shape di Polygon e<br />
Regular (e quindi Shape, oltre a essere astratta è anche virtuale ....<br />
più "irreale" di così....!);<br />
• in secondo luogo si sono dovute rifedinire in Square tutte le funzioni<br />
virtuali pure di Shape (comprese quelle "incompatibili"); altrimenti, il<br />
"doppio percorso" da Square a Shape avrebbe generato messaggi di<br />
errore per ambigu<strong>it</strong>à (infatti, se una funzione non è ridefin<strong>it</strong>a è<br />
ered<strong>it</strong>ata: ma allora, in questo caso, sarebbe ered<strong>it</strong>ata da Polygon o da<br />
Regular?).<br />
Un rudimentale sistema di visualizzazione delle figure<br />
Proseguendo nell'esempio precedente, costruiamo ora una nuova gerarchia di<br />
classi, con lo scopo di visualizzare su un dispos<strong>it</strong>ivo grafico le figure defin<strong>it</strong>e dalla<br />
gerarchia Shape. Non avendo niente di meglio a disposizione, abbiamo scelto<br />
una ("rudimentalissima") implementazione grafica cost<strong>it</strong>u<strong>it</strong>a da caratteri ASCII,<br />
nella quale ogni punto del piano immagine è rappresentato da un carattere ("big<br />
pixel") e quindi "disegnare" un punto significa collocare nella posizione<br />
corrispondente un carattere adeguato (per esempio un asterisco). La bassissima<br />
risoluzione di un simile sistema "grafico" produrrà figure sicuramente distorte e<br />
poco defin<strong>it</strong>e, ma che quello che ci preme sottolineare non è l'efficacia del<br />
prodotto, bensì il metodo utilizzato per la sua implementazione. Il lettore potrà<br />
immaginarsi, al posto di questo sistema, una libreria grafica dotata delle più<br />
svariate funzional<strong>it</strong>à e atta a lavorare su dispos<strong>it</strong>ivi ad alta risoluzione; ma il<br />
"metodo" per implementare tale libreria, mettendola in relazione con le figure di<br />
Shape, sarebbe esattamente lo stesso.<br />
La classe base della nostra nuova gerarchia si chiama ASC_Screen: è una<br />
classe astratta, in quanto possiede una funzione virtuale pura, di nome<br />
draw, così dichiarata:<br />
virtual void draw() = 0;<br />
Tuttavia, a differenza da Shape che presenta una pura interfaccia,<br />
ASC_Screen deve fornire gli strumenti per l'implementazione della grafica su un<br />
dispos<strong>it</strong>ivo "concreto" e quindi è stata dotata di tutte le proprietà e i metodi<br />
adeguati allo scopo. Poichè d'altra parte lo schermo è "unico" indipendentemente<br />
dal numero degli oggetti (cioè delle figure) presenti, tutti i dati-membro e le<br />
funzioni-membro di ASC_Screen (fuorchè draw) sono stati defin<strong>it</strong>i static.<br />
Persino il costruttore e il distruttore (che ovviamente non possono essere<br />
defin<strong>it</strong>i static) si comportano in realtà come metodi statici: il primo alloca la<br />
memoria "grafica" solo in occasione del primo oggetto creato, il secondo libera<br />
tale memoria solo quando tutti gli oggetti sono stati distrutti (per riconoscere tali<br />
condizioni è usato un membro statico "contatore" degli oggetti, incrementato<br />
dal costruttore e decrementato dal distruttore).
Alcuni metodi di ASC_Screen sono accessibili dall'utente e quindi sono<br />
pubblici; altri sono protetti, in quanto accessibili solo dalle classi derivate, e<br />
altri sono privati, per solo uso interno. Tutti i dati-membro sono privati.<br />
La classe ASC_Screen ha quindi una duplice funzione: quella di essere una<br />
base astratta per gli oggetti delle sue classi derivate, che ridefiniscono la<br />
funzione virtuale pura draw per eseguire i disegni; e quella di fornire, a livello<br />
della classe e non del singolo oggetto, tutte le funzional<strong>it</strong>à e i dati necessari per<br />
l'implementazione del sistema.<br />
Ed è a questo punto che entra in gioco l'ered<strong>it</strong>à multipla, la quale permette una<br />
soluzione semplice, pul<strong>it</strong>a ed efficace al tempo stesso: ogni classe derivata da<br />
ASC_Screen, che rappresenta una figura da graficare, deriva anche dalla<br />
corrispondente classe di Shape: in questo modo, da una parte si ered<strong>it</strong>ano le<br />
caratteristiche generali di una figura, che sono "device-independent", e<br />
dall'altra le funzional<strong>it</strong>à necessarie per il disegno della stessa figura su un<br />
particolare device.<br />
Le classi derivate da ASC_Screen hanno gli stessi nomi delle corrispondenti di<br />
Shape, con il prefisso ASC_ (e quindi: ASC_Dot, ASC_Line, ecc...). Ogni<br />
classe possiede un unico membro, che ridefinisce la funzione virtuale pura<br />
draw. Non serve nient'altro, in quanto tutto il resto è ered<strong>it</strong>ato dalle rispettive<br />
gen<strong>it</strong>rici.<br />
La s<strong>it</strong>uazione complessiva è adesso rappresentata dal seguente disegno (la<br />
gerarchia ASC_Screen è "a testa in giù", per ragioni di spazio):<br />
Nell'esercizio che segue viene visualizzato il disegno di una casa "in stile infantile",<br />
in cui ogni componente (pareti, tetto, porte, finestre ecc...) è cost<strong>it</strong>u<strong>it</strong>o da una<br />
figura geometrica elementare. In tutto sono defin<strong>it</strong>i 24 oggetti e due array di<br />
24 puntatori, uno a Shape e l'altro a ASC_Screen. L'indirizzo di ogni<br />
oggetto è assegnato al corrispondente puntatore (in entrambi gli array), così<br />
che è possibile, per ogni figura, chiamare in modo polimorfo sia le funzioni di<br />
Shape che la draw di ASC_Screen. Quest'ultima non esegue materialmente la<br />
visualizzazione, ma si lim<strong>it</strong>a ad inserire degli asterischi (nelle posizioni che
definiscono il contorno della figura) in una matrice bidimensionale di caratteri<br />
(allocata e inizializzata dal costruttore del primo oggetto); poichè ogni riga<br />
della matrice è terminata con un null, si vengono così a cost<strong>it</strong>uire tante<br />
stringhe quante sono le righe. Alla fine, per visualizzare il tutto, il programma<br />
può chiamare il metodo pubblico ASC_Screen::OnScreen(), il quale non fa<br />
altro che scrivere le stringhe sullo schermo, l'una sotto l'altra.<br />
Il sistema è pure dotato ("sorprendentemente") di alcune funzional<strong>it</strong>à più<br />
"avanzate", quali il clipping (lo schermo funge da "finestra" che visualizza solo<br />
una parte dell'immagine, se questa ha un'estensione maggiore), il moving<br />
(possibil<strong>it</strong>à di spostare il centro della "finestra" su un qualunque punto<br />
dell'immagine) e lo zoomming (possibil<strong>it</strong>à di ingrandire o rimpicciolire l'immagine<br />
intorno al centro della "finestra"). Tutte queste operazioni vengono esegu<strong>it</strong>e<br />
chiamando degli opportuni metodi pubblici di ASC_Screen.
Template<br />
Programmazione generica<br />
Nello sviluppo di questo corso, siamo "passati" attraverso vari tipi di<br />
"programmazione", che in realtà perseguono sempre lo stesso obiettivo<br />
(suddivisione di un progetto in porzioni indipendenti, allo scopo di minimizzare il<br />
rapporto costi/benefici nella produzione e manutenzione del software), ma che via<br />
via tendono a realizzare tale obiettivo a livelli sempre più profondi:<br />
• programmazione procedurale: è la programmazione caratteristica del<br />
linguaggio C (e di tutti gli altri linguaggi precedenti al <strong>C++</strong>). L'interesse<br />
principale è focalizzato sull'elaborazione e sulla scelta degli algor<strong>it</strong>mi più<br />
idonei a massimizzarne l'efficienza. Ogni algor<strong>it</strong>mo lavora in una<br />
funzione, a cui si passano argomenti e da cui si ottiene un valore di<br />
r<strong>it</strong>orno. Le funzioni sono implementate con gli strumenti tipici del<br />
linguaggio (tipi, variabili, puntatori, costrutti vari ecc...). Dal punto di<br />
vista dell'utente ogni funzione è una "scatola nera" e i suoi argomenti e<br />
valore di r<strong>it</strong>orno sono gli unici canali di comunicazione.<br />
• programmazione modulare: l'attenzione si sposta dal progetto delle<br />
procedure all'organizzazione dei dati. Ogni gruppo formato da dati<br />
logicamente correlati e dalle procedure che li utilizzano cost<strong>it</strong>uisce un<br />
modulo, in cui i dati sono "occultati" (data hiding). I moduli sono il più<br />
possibile indipendenti. Le interfacce cost<strong>it</strong>uiscono l'unico canale di<br />
comunicazione fra i moduli e i loro utenti. I namespace sono gli<br />
strumenti che il <strong>C++</strong> mette a disposizione per realizzare questo tipo di<br />
programmazione.<br />
• programmazione a oggetti: l'attenzione si sposta ulteriormente dai<br />
moduli ai singoli oggetti. Attraverso le classi, esiste la possibil<strong>it</strong>à di<br />
definire nuovi tipi. I membri di ogni classe possono essere sia dati che<br />
funzioni e solo alcuni di essi possono essere accessibili dall'esterno. Il<br />
data hiding si trasferisce dentro gli oggetti, che diventano ent<strong>it</strong>à attive e<br />
autosufficienti e comunicano con gli utenti solo attraverso i propri membri<br />
pubblici. Ogni nuovo tipo può essere corredato di un insieme di<br />
operazioni (overload degli operatori) e ulteriormente espanso e<br />
specializzato in modo incrementale e indipendente dal codice già scr<strong>it</strong>to,<br />
grazie all'ered<strong>it</strong>à e al polimorfismo.<br />
Un ulteriore "salto di qual<strong>it</strong>à" è rappresentato dalla cosidetta "programmazione<br />
generica", la quale consente di applicare lo stesso codice a tipi diversi, cioè di<br />
definire template (modelli) di classi e funzioni parametrizzando i tipi<br />
utilizzati: nelle classi, si possono parametrizzare i tipi dei dati-membro; nelle<br />
funzioni (e nelle funzioni-membro delle classi) si possono parametrizzare i<br />
tipi degli argomenti e del valore di r<strong>it</strong>orno. In questo modo si raggiunge il<br />
massimo di indipendenza degli algor<strong>it</strong>mi dai dati a cui si applicano: per esempio,<br />
un algor<strong>it</strong>mo di ordinamento può essere scr<strong>it</strong>to una sola volta, qualunque sia il<br />
tipo dei dati da ordinare.<br />
I template sono risolti staticamente (cioè a livello di compilazione) e<br />
pertanto non comportano alcun costo aggiuntivo in fase di esecuzione; sono
invece di enorme util<strong>it</strong>à per il programmatore, che può scrivere del codice<br />
"generico", senza doversi preoccupare di differenziarlo in ragione della varietà<br />
dei tipi a cui tale codice va applicato. Ciò è particolarmente vantaggioso quando<br />
si possono creare classi strutturate identicamente, ma differenti solo per i tipi dei<br />
membri e/o per i tipi degli argomenti delle funzioni-membro.<br />
La stessa Libreria Standard del <strong>C++</strong> mette a disposizione strutture<br />
precost<strong>it</strong>u<strong>it</strong>e di classi template, dette classi conten<strong>it</strong>ore (liste concatenate,<br />
mappe, vettori ecc...) che possono essere utilizzate specificando, nella creazione<br />
degli oggetti, i valori reali da sost<strong>it</strong>uire ai tipi parametrizzati.<br />
Definizione di una classe template<br />
Una classe (o struttura) template è identificata dalla presenza, davanti alla<br />
definizione della classe, dell'espressione:<br />
template<br />
dove T (che è un nome e segue le normali regola di specifica degli<br />
identificatori) rappresenta il parametro di un tipo generico che verrà<br />
utilizzato nella dichiarazione di uno o più membri della classe. In questo<br />
contesto la parola-chiave class non ha il sol<strong>it</strong>o significato: indica che T è il<br />
nome di un tipo (anche nativo), non necessariamente di una classe. L'amb<strong>it</strong>o<br />
di visibil<strong>it</strong>à di T coincide con quello della classe. Se però una funzionemembro<br />
non è defin<strong>it</strong>a inline ma esternamente, bisogna, al sol<strong>it</strong>o, qualificare<br />
il suo nome: in questo caso la qualificazione completa consiste nel ripetere il<br />
prefisso template ancora prima del tipo di r<strong>it</strong>orno (che in particolare<br />
può anche dipendere da T) e inserire dopo il nome della classe. Esempio:<br />
template class A {<br />
T mem ;<br />
public:<br />
Definizione della classe template A<br />
dato-membro di tipo<br />
parametrizzato<br />
A(const T& m) : mem(m) { } costruttore inline con un<br />
argomento di<br />
tipo parametrizzato<br />
T get( ); dichiarazione di funzione-membro<br />
con<br />
valore di r<strong>it</strong>orno di tipo<br />
parametrizzato<br />
........ };
template par<br />
A::get( )<br />
{<br />
}<br />
NOTA<br />
Definizione esterna della funzione-membro get( )<br />
può<br />
notare che il nome del parametro<br />
anche essere diverso da quello usato<br />
nella<br />
return mem ; definizione della classe<br />
Nella definizione della funzione get la ripetizione del parametro par nelle<br />
espressioni template e A potrebbe sembrare ridondante. In<br />
realtà le due espressioni hanno significato è diverso:<br />
• template introduce, nel corrente amb<strong>it</strong>o di visibil<strong>it</strong>à (in<br />
questo caso della funzione get), il nome par come parametro di<br />
template;<br />
• A indica che la classe A è un template con parametro par.<br />
In generale, ogni volta che una classe template è rifer<strong>it</strong>a al di fuori del proprio<br />
amb<strong>it</strong>o (per esempio come argomento di una funzione), è obbligatorio<br />
specificarla segu<strong>it</strong>a dal proprio parametro fra parentesi angolari.<br />
I parametri di un template possono anche essere più di uno, nel qual caso,<br />
nella definizione della classe e nelle definizioni esterne delle sue funzionimembro,<br />
tutti i parametri vanno specificati con il prefisso class e separati da<br />
virgole. Esempio:<br />
template<br />
I template vanno sempre defin<strong>it</strong>i in un namespace, o nel namespace<br />
globale o anche nell'amb<strong>it</strong>o di un'altra classe (template o no). Non possono<br />
essere defin<strong>it</strong>i nell'amb<strong>it</strong>o di un blocco. Non è inoltre ammesso definire nello<br />
stesso amb<strong>it</strong>o due classi con lo stesso nome, anche se hanno diverso numero<br />
di parametri oppure se una classe è template e l'altra no (in altre parole<br />
l'overload è ammesso fra le funzioni, non fra le classi).<br />
Istanza di un template<br />
Un template è un semplice modello (come dice la parola stessa in inglese) e<br />
non può essere usato direttamente. Bisogna prima sost<strong>it</strong>uirne i parametri con<br />
tipi già precedentemente defin<strong>it</strong>i (che vengono detti argomenti). Solo dopo che
è stata fatta questa operazione si crea una nuova classe (cioè un nuovo tipo)<br />
che può essere a sua volta istanziata per la creazione di oggetti.<br />
Il processo di generazione di una classe "reale" partendo da una classe<br />
template e da un argomento è detto: istanziazione di un template (notare<br />
l'analogia: come un oggetto si crea istanziando un tipo, così un tipo si crea<br />
istanziando un template). Se una stessa classe template viene istanziata<br />
più volte con argomenti diversi, si dice che vengono create diverse<br />
specializzazioni dello stesso template. La sintassi per l'istanziazione di un<br />
template è la seguente (riprendiamo l'esempio della classe template A):<br />
A<br />
dove tipo è il nome di un tipo (nativo o defin<strong>it</strong>o dall'utente), da sost<strong>it</strong>uire al<br />
parametro della classe template A nelle dichiarazioni (e definizioni) di tutti<br />
i membri di A in cui tale parametro compare. Quindi la classe "reale" non è A,<br />
ma A, cioè la specializzazione di A con argomento tipo. Ciò rende<br />
possibili istruzioni, come per esempio la seguente:<br />
A ai(5);<br />
che costruisce (mediante chiamata del costruttore con un argomento, di<br />
valore 5) un oggetto ai della classe template A, specializzata con<br />
argomento int.<br />
Parametri di default<br />
Come gli argomenti delle funzioni, anche i parametri dei template possono<br />
essere impostati di default. Riprendendo l'esempio precedente, modifichiamo il<br />
prefisso della definizione della classe A in:<br />
template<br />
ciò comporta che, se nelle istanziazioni di A si omette l'argomento, questo è<br />
sottinteso double; per esempio:<br />
A ad(3.7); equivale a A ad(3.7);<br />
(notare che le parentesi angolari vanno specificate comunque).<br />
Se una classe template ha più parametri, quelli di default possono anche<br />
essere espressi in funzione di altri parametri. Supponiamo per esempio di<br />
definire una classe template B nel seguente modo:<br />
template class B { ........ };
in questa classe i parametri sono due: T e U; ma, mentre l'argomento<br />
corrispondente a T deve essere sempre specificato, quello corrispondente a U può<br />
essere omesso, nel qual caso viene sost<strong>it</strong>u<strong>it</strong>o con il tipo generato dalla classe A<br />
specializzata con l'argomento corrispondente a T. Così:<br />
B crea la specializzazione di B con argomenti double e int,<br />
mentre:<br />
B crea la specializzazione di B con argomenti int e A<br />
Funzioni template<br />
Analogamente alle funzioni-membro di una classe, anche le funzioni non<br />
appartenenti a una classe possono essere dichiarate (e defin<strong>it</strong>e) template.<br />
Esempio di dichiarazione di una funzione template:<br />
template void sort(int n, T* p);<br />
Come si può notare, uno degli argomenti della funzione sort è di tipo<br />
parametrizzato. La funzione ha lo scopo di ordinare un array p di n elementi<br />
di tipo T, e dovrà essere istanziata con argomenti di tipi "reali" da sost<strong>it</strong>uire al<br />
parametro T (vedremo più avanti come si fa). Se un argomento è di tipo<br />
defin<strong>it</strong>o dall'utente, la classe che corrisponde a T dovrà anche contenere tutti<br />
gli overload degli operatori necessari per eseguire i confronti e gli scambi fra<br />
gli elementi dell'array.<br />
Segu<strong>it</strong>ando nell'esempio, allo scopo di evidenziare tutta la "potenza" dei<br />
template confrontiamo ora la nostra funzione con un'analoga funzione di<br />
ordinamento, tratta dalla Run Time Library (che è la libreria standard del<br />
C). Il linguaggio C, che ovviamente non conosce i template nè l'overload degli<br />
operatori, può rendere applicabile lo stesso algor<strong>it</strong>mo di ordinamento a<br />
diversi tipi facendo ricorso agli "strumenti" che ha, e cioè ai puntatori a void<br />
(per generalizzare il tipo dell'array) e ai puntatori a funzione (per dar modo<br />
all'utente di fornire la funzione di confronto fra gli elementi dell'array). Inoltre,<br />
nel codice della funzione, dovrà eseguire il casting da puntatori a void (che<br />
non sono direttamente utilizzabili) a puntatori a byte (cioè a char) e quindi, non<br />
potendo usare direttamente l'ar<strong>it</strong>metica dei puntatori, dovrà anche conoscere<br />
il size del tipo utilizzato (come ulteriore argomento della funzione, che si<br />
aggiunge al puntatore a funzione da usarsi per i confronti). In defin<strong>it</strong>iva, la<br />
funzione "generica" sort del C dovrebbe essere dichiarata nel seguente<br />
modo:<br />
typedef int (*CMP)(const void*, const void*);<br />
void sort(int n, void* p, int size, CMP cmp);
l'utente dovrà provvedere a fornire la funzione di confronto "vera" da sost<strong>it</strong>uire a<br />
cmp, e dovrà pure preoccuparsi di eseguire, in detta funzione, tutti i necessari<br />
casting da puntatore a void a puntatore al tipo utilizzato nella chiamata.<br />
Risulta evidente che la soluzione con i template è di gran lunga preferibile: è<br />
molto più semplice e concisa (sia dal punto di vista del programmatore che da<br />
quello dell'utente) ed è anche più veloce in esecuzione, in quanto non usa<br />
puntatori a funzione, ma solo chiamate dirette (di overload di<br />
operatori che, oltretutto, si possono spesso realizzare inline).<br />
Differenze fra funzioni e classi template<br />
Le funzioni template differiscono dalle classi template principalmente sotto<br />
tre aspetti:<br />
1. Le funzioni template non ammettono parametri di default .<br />
2. Come le classi, anche le funzioni template sono utilizzabili soltanto dopo<br />
che sono state istanziate; ma, mentre nelle classi le istanze devono<br />
essere sempre esplic<strong>it</strong>e (cioè gli argomenti non di default devono essere<br />
sempre specificati), nelle funzioni gli argomenti possono essere spesso<br />
dedotti implic<strong>it</strong>amente dal contesto della chiamata. Riprendendo<br />
l'esempio della funzione sort, la sequenza:<br />
double a[10] = { .........};<br />
sort(10, a);<br />
3. crea automaticamente un'istanza della funzione template sort, con<br />
argomento double dedotto dalla stessa chiamata della funzione.<br />
Quando invece un argomento non può essere dedotto dal contesto, deve<br />
essere specificato esplic<strong>it</strong>amente, nello stesso modo in cui lo si fa con le<br />
classi. Esempio:<br />
template T* create( ) { .........}<br />
int* p = create( ) ;<br />
4. In generale un argomento può essere dedotto quando corrisponde al<br />
tipo di un argomento della funzione e non può esserlo quando<br />
corrisponde al tipo del valore di r<strong>it</strong>orno.<br />
Se una funzione template ha più parametri, dei quali corrispondenti<br />
argomenti alcuni possono essere dedotti e altri no, gli argomenti<br />
deducibili possono essere omessi solo se sono gli ultimi nella lista
(esattamente come avviene per gli argomenti di default di una<br />
funzione). Esempio (supponiamo che la variabile d sia stata defin<strong>it</strong>a<br />
double):<br />
FUNZIONE CHIAMATA NOTE<br />
template<br />
T fun1(U);<br />
template<br />
U fun2(T);<br />
int m =<br />
fun1(d);<br />
int m =<br />
fun2(d);<br />
Il secondo argomento è<br />
dedotto di tipo double<br />
Il primo argomento non si<br />
può omettere,<br />
anche se è deducibile<br />
5. Analogamente alle funzioni tradizionali, e a differenza dalle classi, anche<br />
le funzioni template ammettono l'overload (compresi overload di tipo<br />
"misto", cioè fra una funzione tradizionale e una funzione template).<br />
Nel momento della "scelta" (cioè quando una funzione in overload viene<br />
chiamata), il compilatore applica le normali regole di risoluzione degli<br />
overload, alle quali si aggiungono le regole per la scelta della<br />
specializzazione che meglio si adatta agli argomenti di chiamata della<br />
funzione. Va precisato, tuttavia, che tali regole dipendono dal tipo di<br />
compilatore usato, in quanto i template rappresentano un aspetto dello<br />
standard <strong>C++</strong> ancora in "evoluzione". Nel segu<strong>it</strong>o, ci riferiremo ai cr<strong>it</strong>eri<br />
applicati dal compilatore gcc 3.3 (che è il più "moderno" che<br />
conosciamo):<br />
a)<br />
fra due funzioni template con lo stesso nome viene scelta quella "più<br />
specializzata" (cioè quella che corrisponde più esattamente agli argomenti<br />
della chiamata); per esempio, date due funzioni:<br />
template void fun(T); e template void<br />
fun(A);<br />
(dove A è la classe del nostro esempio iniziale), la chiamata:<br />
fun(5); selezionerà la prima funzione, mentre la chiamata:<br />
fun(A(5)); selezionerà la seconda funzione;<br />
b) se un argomento è dedotto, non sono ammesse conversioni implic<strong>it</strong>e di<br />
tipo, salvo quelle "banali", cioè le conversioni fra variabile e costante e<br />
quelle da classe derivata a classe base; in altre parole, se uno stesso<br />
argomento è ripetuto più volte, tutti i tipi dei corrispondenti argomenti<br />
nella chiamata devono essere identici (a parte i casi di convertibil<strong>it</strong>à sopra<br />
menzionati);<br />
c) come per l'overload fra funzioni tradizionali, le funzioni in cui la<br />
corrispondenza fra i tipi è esatta sono prefer<strong>it</strong>e a quelle in cui la<br />
corrispondenza si ottiene solo dopo una conversione implic<strong>it</strong>a;<br />
d) a par<strong>it</strong>à di tutte le altre condizioni, le funzioni tradizionali sono prefer<strong>it</strong>e alle<br />
funzioni template;<br />
e) il compilatore segnala errore se, malgrado tutti gli "sforzi", non trova<br />
nessuna corrispondenza soddisfacente; come pure segnala errore in caso di<br />
ambigu<strong>it</strong>à, cioè se trova due diverse soluzioni allo stesso livello di<br />
preferenza.
6. Per maggior chiarimento, vediamo ora alcuni esempi di chiamate di<br />
funzioni e di scelte conseguenti operate dal compilatore, date queste<br />
due funzioni in overload, una tradizionale e l'altra template:<br />
void fun(double,double); e template void<br />
fun(T,T);<br />
CHIAMATA RISOLUZIONE NOTE<br />
fun(1,2); fun(1,2); argomento<br />
dedotto,<br />
corrispondenza<br />
esatta<br />
fun(1.1,2.3); fun(1.1,2.3); funzione<br />
tradizionale,<br />
prefer<strong>it</strong>a<br />
fun('A',2); fun(double('A'),double(2)); funzione<br />
tradizionale, unica<br />
possibile<br />
fun(69,71.2); fun(char(69),char(71.2)); argomento<br />
esplic<strong>it</strong>o,<br />
conversioni<br />
ammesse<br />
defin<strong>it</strong>e le seguenti variabili: int a = ...; const int c = ...; int* p = ...;<br />
fun(a,c); fun(a,c); argomento<br />
dedotto,<br />
conversione<br />
"banale"<br />
fun(a,p); ERRORE conversione non<br />
ammessa da int*<br />
a double<br />
Template e modular<strong>it</strong>à<br />
In relazione alla ODR (One-Defin<strong>it</strong>ion-Rule), le funzioni template (e le<br />
funzioni-membro delle classi template) appartengono alla stessa categoria<br />
delle funzioni inline e delle classi (vedere cap<strong>it</strong>olo: Tipi defin<strong>it</strong>i dall'utente,<br />
sezione: Strutture), cioè in pratica la definizione di una funzione template<br />
può essere ripetuta identica in più translation un<strong>it</strong>s del programma.<br />
Nè potrebbe essere diversamente. Infatti, come si è detto, i template sono<br />
istanziati staticamente, cioè a livello di compilazione, e quindi il codice che
utilizza un template deve essere nella stessa translation un<strong>it</strong> del codice che lo<br />
definisce. In particolare, se un stesso template è usato in più translation<br />
un<strong>it</strong>s, la sua definizione, non solo può, ma deve essere inclusa in tutte (in altre<br />
parole, non sono ammesse librerie di template già direttamente in codice binario,<br />
ma solo header-files che includano anche il codice di implementazione in forma<br />
sorgente).<br />
Queste regole, però, contraddicono il principio fondamentale della<br />
programmazione modulare, che stabilisce la separazione e l'indipendenza del<br />
codice dell'utente da quello delle procedure utilizzate: l'interfaccia comune non<br />
dovrebbe contenere le definizioni, ma solo le dichiarazioni delle funzioni (e<br />
delle funzioni-membro delle classi) coinvolte, per modo che qualunque<br />
modifica venga apportata al codice di implementazione di dette funzioni, quello<br />
dell'utente non ne venga influenzato. Con le funzioni template questo non è più<br />
possibile.<br />
Per ovviare a tale grave carenza, e far sì che la programmazione generica<br />
cost<strong>it</strong>uisca realmente "un passo avanti" nella direzione dell'indipendenza fra le<br />
varie parti di un programma, mantenendo nel contempo tutte le "posizioni"<br />
acquis<strong>it</strong>e dagli altri livelli di programmazione, è stata recentemente introdotta<br />
nello standard una nuova parola-chiave: "export", che, usata come prefisso<br />
nella definizione di una funzione template, indica che la stessa definizione è<br />
accessibile anche da altre translation un<strong>it</strong>s. Spetterà poi al linker, e non al<br />
compilatore, generare le eventuali istanze richieste dall'utente. In questo modo<br />
"tutto si rimette a posto", e in particolare:<br />
• le funzioni template possono essere compilate separatamente;<br />
• nell'interfaccia comune si possono includere solo le dichiarazioni, come<br />
per le funzioni tradizionali.<br />
Tutto ciò sarebbe molto "bello", se non fosse che ... putroppo (secondo quello che<br />
ci risulta) nessun compilatore a tutt'oggi implementa la parola-chiave export!<br />
E quindi, per il momento, bisogna ancora includere le definizioni delle funzioni<br />
template nell'interfaccia comune.
General<strong>it</strong>à sulla Libreria Standard del <strong>C++</strong><br />
Campi di applicazione<br />
La Libreria Standard del <strong>C++</strong> è cost<strong>it</strong>u<strong>it</strong>a da un vasto numero di classi e<br />
funzioni che trattano principalmente di:<br />
• Input-Output;<br />
• gestione delle stringhe;<br />
• gestione degli oggetti "conten<strong>it</strong>ori" di altri oggetti (detti: elementi),<br />
quali: gli array, le liste, le code, le mappe, gli insiemi ecc...;<br />
• utilizzo degli "<strong>it</strong>eratori", per "navigare" attraverso gli elementi di un<br />
conten<strong>it</strong>ore o i caratteri di una stringa;<br />
• utilizzo degli "algor<strong>it</strong>mi", per eseguire operazioni sui conten<strong>it</strong>ori e sui<br />
loro elementi, quali: ricerca, conteggio, inserimento, sost<strong>it</strong>uzione,<br />
ordinamento, merging ecc...; sono previste anche operazioni specifiche,<br />
esegu<strong>it</strong>e tram<strong>it</strong>e oggetti-funzione forn<strong>it</strong>i dall'utente o dalla stessa<br />
Libreria;<br />
• operazioni numeriche e matematiche su numeri reali o complessi;<br />
• informazioni riguardanti aspetti del linguaggio che dipendono<br />
dall'implementazione (per esempio: il massimo valore di un float).<br />
La programmazione generica è largamente applicata nella Libreria: infatti,<br />
nella grande maggioranza le sue classi e funzioni sono template (o<br />
specializzazioni di template). Questo fa sì che le stesse operazioni siano<br />
applicabili a una vasta varietà di tipi, sia nativi che defin<strong>it</strong>i dall'utente.<br />
In aggiunta alla Libreria Standard del <strong>C++</strong>, la maggior parte delle<br />
implementazioni offre librerie di "interfacce grafiche", spesso chiamate anche GUI<br />
(graphical user interface), con sistemi a "finestre" per l'interazione fra utente e<br />
programma. Inoltre, la maggior parte degli ambienti di sviluppo integrati fornisce<br />
librerie dette FL (foundation libraries), che supportano lo sviluppo di<br />
applicazioni in accordo con l'ambiente specifico in cui lavorano (per esempio, la<br />
MFC del Visual <strong>C++</strong>). Sia le GUI che (ovviamente) le FL non fanno parte dello<br />
standard <strong>C++</strong> e quindi non verranno trattate in questo corso. La stessa Libreria<br />
Standard sarà considerata perlopiù "dal punto di vista dell'utente", cioè<br />
l'attenzione sarà focalizzata sul suo utilizzo, più che sulla descrizione del suo<br />
contenuto.<br />
Header files
Le classi e le funzioni della Libreria Standard sono raggruppate in una<br />
cinquantina di header files, i cui nomi seguono una particolare convenzione:<br />
non hanno estensione (cioè non hanno .h). Per esempio, il principale header file<br />
per le operazioni di input-output è (al posto di <br />
della "vecchia" libreria).<br />
In ogni header file si trova di sol<strong>it</strong>o una classe (con le eventuali classi<br />
derivate se è presente una gerarchia di classi), e varie funzioni esterne di<br />
appoggio, soprattutto per la definizione di operatori in overload.<br />
A volte un header file include altri header files. Tuttavia, all'inizio di ognuno di<br />
essi, sono inser<strong>it</strong>e alcune direttive al preprocessore che, interrogando<br />
opportune costanti predefin<strong>it</strong>e, controllano l'effettiva inclusione del file (cioè<br />
non lo includono se è già stato incluso precedentemente). Questo permette<br />
all'utente di inserire tutte le direttive #include che r<strong>it</strong>iene necessarie, senza<br />
preoccuparsi di generare eventuali duplicazioni di nomi.<br />
La Libreria Standard del <strong>C++</strong> ingloba la Run Time Library del C, i cui<br />
header files possono essere specificati in due modi:<br />
• con il loro nome tradizionale, per esempio ;<br />
• con i nomi della convenzione <strong>C++</strong>, senza .h, ma con la lettera c davanti,<br />
per esempio <br />
Il namespace std<br />
Tutta la Libreria Standard del <strong>C++</strong> è defin<strong>it</strong>a in un unico namespace, che si<br />
chiama: std.<br />
Pertanto i nomi delle classi, delle funzioni e degli oggetti defin<strong>it</strong>i nella<br />
Libreria devono essere qualificati con il prefisso std::. Per esempio, le<br />
operazioni di ouput sul dispost<strong>it</strong>ivo standard vanno scr<strong>it</strong>te:<br />
std::cout
la quale rende disponibili tutti i nomi della Libreria senza bisogno di<br />
qualificarli.<br />
Per semplic<strong>it</strong>à, visto che i nostri programmi di esempio sono in genere molto<br />
brevi, e quindi il pericolo di confl<strong>it</strong>to fra i nomi è praticamente inesistente,<br />
adotteremo questa soluzione.<br />
La Standard Template Library<br />
Un'importante sottinsieme della Libreria Standard del <strong>C++</strong> è la cosidetta<br />
Standard Template Library (STL), che mette a disposizione degli utenti classi<br />
e funzioni template per la gestione dei conten<strong>it</strong>ori e degli associati <strong>it</strong>eratori<br />
e algor<strong>it</strong>mi.<br />
La principale caratteristica della STL è quella di fornire la massima generic<strong>it</strong>à: i<br />
template della STL permettono all'utente di generare la specializzazione che<br />
desidera (fatte salve certe premesse), cioè di utilizzare la libreria con dati di<br />
qualunque tipo.<br />
Fuori dalla STL, si r<strong>it</strong>rovano ancora classi e funzioni template, ma in generale<br />
la scelta delle possibili specializzazioni si esaurisce in amb<strong>it</strong>i più ristretti. Per<br />
esempio, i template che gestiscono le stringhe e l'input-output lim<strong>it</strong>ano la<br />
loro generic<strong>it</strong>à alla scelta della codifica dei caratteri utilizzati. Noi abbiamo<br />
sempre trattato (e tratteremo) soltanto di caratteri ASCII di un byte (il tipo<br />
char), ma è bene sapere che sono possibili anche caratteri con codifiche diverse<br />
(per esempio caratteri giapponesi), che occupano più di un byte (i cosidetti<br />
wide-characters, o caratteri estesi). Poichè noi "conosciamo" solo il tipo char,<br />
quando parleremo di stringhe e di input-output ignoreremo il fatto che siano<br />
template, perchè in realtà tratteremo con template già specializzati con<br />
argomento .<br />
Un altro esempio: la classe dei numeri complessi è un template solo per il<br />
fatto che i tipi delle parti reale e immaginaria possono essere specializzati<br />
con float, double o long double.<br />
Nelle classi e funzioni della STL, invece, la scelta dei tipi degli argomenti è<br />
completamente libera: l'unica condizione, per i tipi defin<strong>it</strong>i dall'utente, è che<br />
questi siano forn<strong>it</strong>i di tutti gli operatori in overload necessari per eseguire le<br />
operazioni previste.<br />
Nel segu<strong>it</strong>o riportiamo, per completezza, l'elenco (in ordine alfabetico) degli<br />
header files che fanno capo alla STL. Tratteremo solo di alcuni.<br />
algor<strong>it</strong>mi<br />
conten<strong>it</strong>ore: coda "bifronte"
oggetti-funzione<br />
<strong>it</strong>eratori<br />
conten<strong>it</strong>ore: double-linked list<br />
conten<strong>it</strong>ore: array associativo<br />
allocazione di memoria per conten<strong>it</strong>ori<br />
operazioni numeriche<br />
conten<strong>it</strong>ore: coda (FIFO)<br />
conten<strong>it</strong>ore: insieme<br />
conten<strong>it</strong>ore: pila (LIFO)<br />
coppie di dati e operatori relazionali<br />
conten<strong>it</strong>ore: array monodimensionale
La Standard Template Library<br />
General<strong>it</strong>à<br />
Una classe che memorizza una collezione di oggetti (chiamati elementi), tutti<br />
di un certo tipo (parametrizzato), e detta: "conten<strong>it</strong>ore".<br />
I conten<strong>it</strong>ori della STL sono stati progettati in modo da ottenere il massimo<br />
dell'efficienza accompagnata al massimo della generic<strong>it</strong>à. L'obiettivo<br />
dell'efficienza ha escluso dal progetto l'utilizzo delle funzioni virtuali, che<br />
comportano un costo aggiuntivo in fase di esecuzione; e quindi non esiste<br />
un'interfaccia standard per i conten<strong>it</strong>ori, nella forma di classe base astratta.<br />
Ogni conten<strong>it</strong>ore non deriva da un altro, né da una base comune, ma ripete<br />
l'implementazione di una serie di operazioni standard, ognuna delle quali ha, nei<br />
diversi conten<strong>it</strong>ori, lo stesso nome e significato. Qualche conten<strong>it</strong>ore aggiunge<br />
operazioni specifiche, altri eliminano operazioni inefficienti per le loro<br />
particolari caratteristiche, ma resta un nutr<strong>it</strong>o sottoinsieme di operazioni comuni<br />
a tutti i conten<strong>it</strong>ori. Quanto detto vale non solo per le funzioni che sono<br />
metodi delle classi, ma anche per quelle (dette "algor<strong>it</strong>mi") che lavorano sui<br />
conten<strong>it</strong>ori dall'esterno.<br />
Gli <strong>it</strong>eratori permettono di scorrere su un conten<strong>it</strong>ore, accedendo a ogni<br />
elemento singolarmente. Un <strong>it</strong>eratore astrae e generalizza il concetto di<br />
puntatore a una sequenza di oggetti e può essere implementato in tanti modi<br />
diversi (per esempio, nel caso di un array sarà effettivamente un puntatore,<br />
mentre nel caso di una lista sarà un link ecc...). In realtà la particolare<br />
implementazione di un <strong>it</strong>eratore non interessa all'utente, in quanto le<br />
definizioni che riguardano gli <strong>it</strong>eratori sono identiche, nel nome e nel<br />
significato, in tutti i conten<strong>it</strong>ori.<br />
Riassumendo, "dal punto di vista dell'utente", sia le operazioni (metodi<br />
e algor<strong>it</strong>mi) che gli <strong>it</strong>eratori cost<strong>it</strong>uiscono, salvo qualche eccezione, un insieme<br />
standard, indipendente dai conten<strong>it</strong>ori a cui vengono applicati. In questo modo è<br />
possibile scrivere funzioni template con il massimo della generic<strong>it</strong>à<br />
(parametrizzando non solo il tipo dei dati, ma anche la stessa scelta del<br />
conten<strong>it</strong>ore), senza nulla togliere all'efficienza in fase di esecuzione.<br />
Tutte le classi template dei conten<strong>it</strong>ori hanno almeno due parametri, ma il<br />
secondo (che normalmente riguarda l'allocazione della memoria) può essere<br />
omesso in quanto il tipo normalmente utilizzato è forn<strong>it</strong>o di default. Non<br />
approfondiremo questo argomento e quindi descriveremo sempre le classi<br />
template della STL come se avessero solo il parametro che si riferisce al tipo<br />
degli elementi. In generale, allo scopo di "semplificare" una trattazione che già<br />
così è abbastanza complessa, trascureremo il più delle volte sia i parametri di<br />
default dei template che gli argomenti di default delle funzioni.
Iteratori<br />
Abbiamo detto che un <strong>it</strong>eratore è un'astrazione pura, che generalizza il concetto di<br />
puntatore a un elemento di una sequenza.<br />
Sequenze<br />
Anche il concetto di sequenza è un'astrazione, che significa: "qualcosa in cui si<br />
può andare dall'inizio alla fine tram<strong>it</strong>e l'operazione prossimo-elemento", come<br />
è esemplificato dalla seguente rappresentazione grafica:<br />
Un <strong>it</strong>eratore "punta" a un elemento e fornisce un'operazione per far sì che<br />
l'<strong>it</strong>eratore stesso possa puntare all'elemento successivo della sequenza. La<br />
fine di una sequenza corrisponde a un <strong>it</strong>eratore che "punta" all'ipotetico<br />
elemento che segue immediatamente l'ultimo elemento della sequenza (non<br />
esiste un <strong>it</strong>eratore NULL, come nei normali puntatori).<br />
Operazioni basilari sugli <strong>it</strong>eratori<br />
Le operazioni basilari sugli <strong>it</strong>eratori sono 3 e precisamente:<br />
1. "accedi all'elemento puntato" (dereferenziazione, rappresentata dagli<br />
operatori * e ->)<br />
NOTA: a questo propos<strong>it</strong>o un <strong>it</strong>eratore viene detto valido se punta<br />
realmente a un elemento, cioè se può essere dereferenziato; un<br />
<strong>it</strong>eratore non è valido se non è stato inizializzato, oppure se puntava a<br />
un conten<strong>it</strong>ore che è stato ridimensionato (vedere più avanti) o<br />
distrutto, oppure se punta alla fine di una sequenza<br />
2. "punta al prossimo elemento" (incremento, prefisso o suffisso,<br />
rappresentata dall'operatore ++)<br />
3. "esegui il test di uguaglianza o disuguaglianza" (rappresentate dagli<br />
operatori == e !=)<br />
(notare la perfetta coincidenza, simbolica e semantica, con le rispettive<br />
operazioni sui normali puntatori)<br />
L'esistenza di queste operazioni basilari ci permette di scrivere codice generico<br />
che si può applicare a qualsiasi conten<strong>it</strong>ore, come nell'esempio della seguente
funzione template, che copia una qualunque sequenza in un'altra (purchè in<br />
entrambe siano defin<strong>it</strong>i i rispettivi <strong>it</strong>eratori):<br />
template void copy(In from, In endseq, Out to)<br />
{<br />
while(from !=<br />
endseq)<br />
}<br />
{<br />
}<br />
*to = *from;<br />
++from;<br />
++to;<br />
// cicla da from a endseq (escluso)<br />
// copia l'elemento puntato da from in quello<br />
puntato da to<br />
// punta all'elemento successivo della sequenza di<br />
input<br />
// punta all'elemento successivo della sequenza di<br />
output<br />
il parametro In corrisponde a un tipo <strong>it</strong>eratore defin<strong>it</strong>o nella sequenza di<br />
input; il parametro Out corrisponde a un tipo <strong>it</strong>eratore defin<strong>it</strong>o nella<br />
sequenza di output (i parametri sono due anzichè uno per permettere la copia<br />
anche fra conten<strong>it</strong>ori diversi).<br />
Notare che la nostra copy funziona benissimo anche per i normali puntatori. Per<br />
esempio, dati due array di char, così defin<strong>it</strong>i:<br />
char vi[100], vo[100];<br />
la funzione copy ottiene il risultato voluto se è chiamata nel modo seguente:<br />
copy(vi, vi+100, vo);<br />
in questo punto la copy viene istanziata con gli argomenti char* e char*,<br />
dedotti implic<strong>it</strong>amente dal contesto della chiamata, e quindi si crea la<br />
specializzazione:<br />
copy<br />
cioè una funzione che non è più template ma "reale", e ottiene come risultato<br />
la copia dell'array vi nell'array vo.<br />
Gli <strong>it</strong>eratori sono tipi<br />
Come già anticipato nell'esempio che abbiamo visto, gli <strong>it</strong>eratori sono tipi. Ogni<br />
tipo <strong>it</strong>eratore è defin<strong>it</strong>o nell'amb<strong>it</strong>o della classe conten<strong>it</strong>ore a cui si<br />
riferisce. Ci sono perciò molti tipi intrinsecamente diversi di <strong>it</strong>eratori, dal<br />
momento che ogni <strong>it</strong>eratore deve essere in grado di svolgere la propria funzione<br />
per un particolare tipo di conten<strong>it</strong>ore. Tuttavia l'utente quasi mai ha bisogno di<br />
conoscere il tipo di uno specifico <strong>it</strong>eratore: ogni conten<strong>it</strong>ore "conosce" i suoi<br />
tipi <strong>it</strong>eratori e li rende disponibili con nomi convenzionali, uguali in tutti i<br />
conten<strong>it</strong>ori.
Il più comune tipo <strong>it</strong>eratore è:<br />
<strong>it</strong>erator<br />
che punta a un elemento modificabile del conten<strong>it</strong>ore a cui si riferisce.<br />
Gli altri tipi <strong>it</strong>eratori defin<strong>it</strong>i nelle classi conten<strong>it</strong>ore sono:<br />
const_<strong>it</strong>erator<br />
reverse_<strong>it</strong>erator<br />
const_reverse_<strong>it</strong>erator<br />
punta a elementi non modificabili (analogo di puntatore<br />
a costante)<br />
percorre la sequenza in ordine inverso (gli elementi<br />
puntati sono modificabili)<br />
percorre la sequenza in ordine inverso (gli elementi<br />
puntati non sono modificabili)<br />
NOTA: gli <strong>it</strong>eratori diretti e inversi non si possono mescolare (cioè non sono<br />
amesse conversioni di tipo fra <strong>it</strong>erator e reverse_<strong>it</strong>erator).<br />
Un oggetto <strong>it</strong>eratore si ottiene (come sempre succede quando si tratta con i<br />
tipi) istanziando un tipo <strong>it</strong>eratore. Poichè ogni tipo <strong>it</strong>eratore è defin<strong>it</strong>o<br />
nell'amb<strong>it</strong>o di una classe, il suo nome può essere rappresentato all'esterno solo<br />
se è qualificato con il nome della classe di appartenenza (esattamente come<br />
per i membri statici). Per esempio, consideriamo il conten<strong>it</strong>ore vector,<br />
specializzato con argomento int; l'istruzione:<br />
vector::<strong>it</strong>erator <strong>it</strong>;<br />
definisce l'oggetto <strong>it</strong>eratore <strong>it</strong>, istanza del tipo <strong>it</strong>erator della classe<br />
vector.<br />
Inizializzazione degli <strong>it</strong>eratori e funzioni-membro che rest<strong>it</strong>uiscono <strong>it</strong>eratori<br />
L'oggetto <strong>it</strong> non è ancora un <strong>it</strong>eratore valido, in quanto è stato defin<strong>it</strong>o ma<br />
non inizializzato (è esattamente lo stesso discorso che si fa per i puntatori).<br />
Per permettere l'inizializzazione di un <strong>it</strong>eratore, ogni conten<strong>it</strong>ore mette a<br />
disposizione un certo numero di funzioni-membro, che danno accesso agli<br />
estremi della sequenza (come al sol<strong>it</strong>o, i nomi di queste funzioni sono gli stessi<br />
in tutti i conten<strong>it</strong>ori):<br />
<strong>it</strong>erator begin(); rest<strong>it</strong>uisce un oggetto <strong>it</strong>eratore che punta<br />
all'inizio della sequenza<br />
const_<strong>it</strong>erator begin() const; come sopra (elementi costanti)<br />
<strong>it</strong>erator end(); rest<strong>it</strong>uisce un oggetto <strong>it</strong>eratore che punta alla<br />
fine della sequenza<br />
const_<strong>it</strong>erator end() const; come sopra (elementi costanti)<br />
reverse_<strong>it</strong>erator rbegin(); rest<strong>it</strong>uisce un oggetto <strong>it</strong>eratore che punta<br />
all'inizio della sequenza inversa<br />
const_reverse_<strong>it</strong>erator rbegin()<br />
const;<br />
come sopra (elementi costanti)<br />
reverse_<strong>it</strong>erator rend(); rest<strong>it</strong>uisce un oggetto <strong>it</strong>eratore che punta alla
const_reverse_<strong>it</strong>erator rend()<br />
const;<br />
fine della sequenza inversa<br />
come sopra (elementi costanti)<br />
Per esempio, dato un array di n elementi, il valore di r<strong>it</strong>orno ....<br />
di... punta all'elemento di indice ...<br />
begin() 0<br />
end() n (che non esiste)<br />
rbegin() n-1<br />
rend() -1 (che non esiste)<br />
In aggiunta, esiste una funzione-membro (non dei conten<strong>it</strong>ori, ma di<br />
reverse_<strong>it</strong>erator) che fornisce l'unico modo per passare dal tipo<br />
reverse_<strong>it</strong>erator al tipo <strong>it</strong>erator. Questa funzione si chiama base():<br />
applicata a un oggetto reverse_<strong>it</strong>erator che punta a un certo elemento,<br />
rest<strong>it</strong>uisce un oggetto <strong>it</strong>erator che punta all'elemento successivo.<br />
Infine, un oggetto <strong>it</strong>eratore può essere inizializzato (o assegnato) per copia<br />
da un altro oggetto <strong>it</strong>eratore dello stesso tipo. Questo permette di scrivere<br />
funzioni con argomenti <strong>it</strong>eratori passati by value (come la copy del nostro<br />
esempio precedente).<br />
Dichiarazione esplic<strong>it</strong>a di tipo<br />
Nell'esempio di definizione dell'oggetto <strong>it</strong>eratore <strong>it</strong>, l'espressione:<br />
vector::<strong>it</strong>erator rappresenta un tipo; il compilatore lo sa, in quanto<br />
riconosce il conten<strong>it</strong>ore vector. Ma se noi volessimo parametrizzare proprio il<br />
conten<strong>it</strong>ore, per esempio passandolo come argomento a una funzione<br />
template:<br />
template void fun(Cont& c)<br />
e poi definendo e inizializzando all'interno della funzione un oggetto<br />
<strong>it</strong>eratore, con l'istruzione:<br />
Cont::<strong>it</strong>erator <strong>it</strong> = c.begin();<br />
il compilatore non l'accetterebbe, non essendo in grado di riconoscere che<br />
l'espressione Cont::<strong>it</strong>erator rappresenta un tipo. Perché l'espressione sia valida,<br />
occorre in questo caso premettere la parola-chiave typename:<br />
typename Cont::<strong>it</strong>erator <strong>it</strong> = c.begin();<br />
e questo fa sì che il compilatore accetti provvisoriamente Cont::<strong>it</strong>erator come<br />
tipo, rinviando il controllo defin<strong>it</strong>ivo al momento dell'istanziazione della<br />
funzione.<br />
In generale la parola-chiave typename davanti a un identificatore dichiara<br />
esplic<strong>it</strong>amente che quell'identificatore è un tipo (può anche essere usata al<br />
posto di class nella definizione di un template). E' obbligatoria (almeno nelle<br />
versioni più avanzate dello standard) ogni volta che un tipo dipende da un<br />
parametro di template.<br />
Categorie di <strong>it</strong>eratori e altre operazioni
Senza entrare nei dettagli sull'argomento, che esula dagli intendimenti di questo<br />
corso, vogliamo accennare al fatto che gli <strong>it</strong>eratori sono classificati in varie<br />
categorie, a seconda delle operazioni che si possono eseguire su di essi. Infatti,<br />
oltre alle 3 operazioni basilari che abbiamo visto (comuni a tutti gli <strong>it</strong>eratori),<br />
sono possibili altre operazioni, che però si applicano soltanto ad alcune<br />
categorie di <strong>it</strong>eratori. A loro volta le categorie dipendono sostanzialmente dai<br />
particolari conten<strong>it</strong>ori in cui gli <strong>it</strong>eratori sono defin<strong>it</strong>i (per esempio: gli<br />
<strong>it</strong>eratori defin<strong>it</strong>i in vector e in deque appartengono alla categoria: "ad<br />
accesso casuale", mentre gli <strong>it</strong>eratori defin<strong>it</strong>i in list e in altri conten<strong>it</strong>ori<br />
appartengono alla categoria: "bidirezionale").<br />
Le categorie sono organizzate gerarchicamente, nel senso che le operazioni<br />
ammesse per gli <strong>it</strong>eratori di una certa categoria lo sono anche per gli <strong>it</strong>eratori<br />
di categoria superiore, ma non viceversa. Gli stessi algor<strong>it</strong>mi, che (come<br />
vedremo) hanno sempre argomenti <strong>it</strong>eratori, pretendono di operare, ognuno,<br />
su una precisa categoria di <strong>it</strong>eratori (e su quelle gerarchicamente superiori).<br />
Al vertice della gerarchia si trovano gli <strong>it</strong>eratori ad accesso casuale, segu<strong>it</strong>i<br />
dagli <strong>it</strong>eratori bidirezionali (e da altri che non menzioneremo).<br />
Gli <strong>it</strong>eratori bidirezionali e ad accesso casuale ammettono l'operazione di<br />
decremento (--), che sposta il puntamento sull'elemento precedente della<br />
sequenza, mentre soltanto agli <strong>it</strong>eratori ad accesso casuale sono riservate<br />
alcune operazioni aggiuntive, quali:<br />
• indicizzazione [ ], per esempio <strong>it</strong>[3] : punta al terzo elemento<br />
successivo<br />
• operazioni di confronto: < , , >=<br />
• tutte le operazioni con interi che forniscono un'ar<strong>it</strong>metica analoga a<br />
quella dei puntatori: + , += , - , -=<br />
a questo propos<strong>it</strong>o: agli <strong>it</strong>eratori delle altre categorie, per i quali le<br />
suddette operazioni non sono ammesse, la Libreria fornisce due<br />
funzioni (supponiamo che Iter denoti un tipo <strong>it</strong>eratore):<br />
void advance(Iter& <strong>it</strong>, int n) al posto di : <strong>it</strong> += n e ....<br />
difference_type distance(Iter first, Iter last) al posto di : last -<br />
first<br />
dove difference_type è un tipo (di sol<strong>it</strong>o coincidente con int) defin<strong>it</strong>o<br />
(come <strong>it</strong>erator) nel conten<strong>it</strong>ore.<br />
Classificazione dei conten<strong>it</strong>ori<br />
Conten<strong>it</strong>ori Standard<br />
I conten<strong>it</strong>ori della STL sono suddivisi in 2 categorie:<br />
• le sequenze (in senso stretto)
• i conten<strong>it</strong>ori associativi<br />
A loro volta le sequenze sono classificate in sequenze principali e adattatori.<br />
Questi ultimi sono delle interfacce ridotte di sequenze principali, specializzate<br />
per eseguire un insieme molto lim<strong>it</strong>ato di operazioni, e non dispongono di<br />
<strong>it</strong>eratori.<br />
Nei conten<strong>it</strong>ori associativi gli elementi sono coppie di valori. Dato un valore,<br />
la chiave, si può (rapidamente) accedere all'altro, il valore mappato. Si può<br />
pensare a un conten<strong>it</strong>ore associativo come a un array, in cui l'indice (la<br />
chiave) non deve necessariamente essere un intero. Tutti i conten<strong>it</strong>ori<br />
associativi dispongono di <strong>it</strong>eratori bidirezionali, che percorrono gli elementi<br />
ordinati per chiave (e quindi anche i conten<strong>it</strong>ori associativi possono essere<br />
considerati delle sequenze, in senso lato).<br />
Tipi defin<strong>it</strong>i nei conten<strong>it</strong>ori<br />
Tutti i conten<strong>it</strong>ori mettono a disposizione nomi convenzionali di tipi, defin<strong>it</strong>i<br />
nel proprio amb<strong>it</strong>o. Abbiamo appena visto i 4 tipi <strong>it</strong>eratori e il tipo<br />
difference_type. Ve ne sono altri, dei quali elenchiamo i più importanti:<br />
value_type tipo degli elementi<br />
size_type<br />
tipo degli indici e delle dimensioni (normalmente coincide con<br />
unsigned int)<br />
reference equivale a value_type&<br />
const_reference equivale a const value_type&<br />
key_type tipo della chiave nei conten<strong>it</strong>ori associativi<br />
mapped_type tipo del valore mappato nei conten<strong>it</strong>ori associativi<br />
Costo delle operazioni<br />
Nonostante tutti i tipi defin<strong>it</strong>i nei conten<strong>it</strong>ori e molte funzioni-membro<br />
abbiano nomi standardizzati, per permettere la creazione di funzioni generiche<br />
in cui i conten<strong>it</strong>ori stessi figurino come parametri, non sempre è conveniente<br />
sfruttare questa possibil<strong>it</strong>à. In certi casi, infatti, ci sono operazioni che risultano<br />
più efficienti usando un conten<strong>it</strong>ore piuttosto che un altro, e quindi tali<br />
operazioni, pur essendo disponibili in tutti i conten<strong>it</strong>ori, non dovrebbero essere<br />
inser<strong>it</strong>e in funzioni generiche. In altri casi certe operazioni in alcuni<br />
conten<strong>it</strong>ori non sono neppure disponibili, talmente sarebbero inefficienti, e<br />
quindi un tentativo di inserirle in funzioni generiche produrrebbe un messaggio<br />
di errore. Ogni operazione ha un "costo computazionale", che spesso dipende<br />
dal conten<strong>it</strong>ore in cui è esegu<strong>it</strong>a, e quindi a volte non conviene parametrizzare<br />
il conten<strong>it</strong>ore, ma piuttosto selezionare il conten<strong>it</strong>ore più appropriato. La scelta<br />
deve indirizzarsi a operare il più possibile a "costo costante", cioè indipendente dal<br />
numero di elementi (per esempio, l'accesso a un elemento, data la sua<br />
posizione, è a "costo costante" usando vector, e non lo è usando list, mentre per<br />
l'inserimento di un elemento "in mezzo" è esattamente il contrario).<br />
Sommario dei conten<strong>it</strong>ori
I conten<strong>it</strong>ori della STL sono 10 (3 sequenze principali, 3 adattatori e 4<br />
conten<strong>it</strong>ori associativi) e precisamente:<br />
vector è il conten<strong>it</strong>ore più completo; memorizza un array<br />
monodimensionale, ai cui elementi può accedere in modo<br />
"randomatico", tram<strong>it</strong>e <strong>it</strong>eratori ad accesso casuale e indici; può<br />
modificare le sue dimensioni, espandendosi in base alle necess<strong>it</strong>à<br />
list rispetto a vector manca dell'accesso tram<strong>it</strong>e indice e di varie<br />
operazioni sugli <strong>it</strong>eratori, che non sono ad accesso casuale ma<br />
bidirezionali; è più efficiente di vector nelle operazioni di<br />
inserimento e cancellazione di elementi<br />
deque è una "coda bifronte" cioè è una sequenza ottimizzata per rendere<br />
le operazioni alle due estrem<strong>it</strong>à efficienti come in list, mentre<br />
mantiene gli <strong>it</strong>eratori ad accesso casuale e l'accesso tram<strong>it</strong>e indice<br />
come in vector (di cui però non mantiene certe funzioni di gestione<br />
delle dimensioni)<br />
stack è un adattatore di deque per operazioni di accesso (top),<br />
inserimento (push) e cancellazione (pop) dell'elemento in coda<br />
alla sequenza<br />
queue è un adattatore di deque per operazioni di inserimento in coda<br />
(push) e cancellazione in testa (pop); l'accesso è consent<strong>it</strong>o sia in<br />
coda (back) che in testa (front)<br />
prior<strong>it</strong>y_queue è defin<strong>it</strong>o nell'header-file ; è un adattatore di vector<br />
per operazioni di inserimento (push) "ordinato" (cioè fatto in modo<br />
che gli elementi della sequenza siano sempre in ordine<br />
decrescente), e per operazioni di cancellazione in testa (pop) e di<br />
accesso in testa (top); il mantenimento degli elementi in ordine<br />
comporta che le operazioni non siano esegu<strong>it</strong>e "a costo costante" (se<br />
l'implementazione è "fatta bene" il costo dovrebbe essere<br />
proporzionale al logar<strong>it</strong>mo del numero di elementi)<br />
map è il più importante dei conten<strong>it</strong>ori associativi; memorizza una<br />
sequenza di coppie (chiave e valore mappato, entrambi<br />
parametri di map) e fornisce un'accesso rapido a ogni elemento<br />
tram<strong>it</strong>e la sua chiave (ogni chiave deve essere unica all'interno di un<br />
map); mantiene i propri elementi in ordine crescente di chiave;<br />
riguardo al "costo" delle operazioni, valgono le stesse considerazioni<br />
fatte per prior<strong>it</strong>y_queue;<br />
La sua operazione caratteristica è l'accesso tram<strong>it</strong>e indice<br />
(chiamiamo m un oggetto di map):<br />
valore mappato = m[chiave] oppure m[chiave] = valore<br />
mappato<br />
che funziona sia in estrazione che in inserimento; in ogni caso<br />
cerca l'elemento con quella chiave: se lo trova, estrae (o inserisce)<br />
il valore mappato; se non lo trova, lo crea e inizializza il valore<br />
mappato con il "valore base" del suo tipo (dato da<br />
mapped_type); il valore base è zero (in modo appropriato al tipo),<br />
se il tipo è nativo, altrimenti è un oggetto creato dal costruttore di<br />
default (che in questo caso è obbligatorio)<br />
multimap è defin<strong>it</strong>o nell'header-file ; è un conten<strong>it</strong>ore associativo
analogo a map, con la differenza che la chiave può essere duplicata;<br />
non dispone dell'accesso tram<strong>it</strong>e indice<br />
set è un conten<strong>it</strong>ore associativo analogo a map, con la differenza che<br />
possiede solo la chiave (e quindi ha un solo parametro); non<br />
dispone dell'accesso tram<strong>it</strong>e indice; in pratica è una sequenza<br />
ordinata di valori unici e crescenti<br />
multiset è defin<strong>it</strong>o nell'header-file ; è un conten<strong>it</strong>ore associativo<br />
analogo a set, con la differenza che la chiave può essere duplicata; in<br />
pratica è una sequenza ordinata di valori non decrescenti;<br />
A queste classi si aggiunge la struttura template pair, defin<strong>it</strong>a in e<br />
utilizzata dai conten<strong>it</strong>ori associativi:<br />
template struct pair {........};<br />
un oggetto pair è cost<strong>it</strong>u<strong>it</strong>o da una coppia di valori, di cui il primo, di tipo T, è<br />
memorizzato nel membro first e il secondo, di tipo U, è memorizzato nel<br />
membro second. La struttura possiede un costruttore di default, che<br />
inizializza first e second ai valori base dei loro tipi, e un costruttore con un<br />
2 argomenti, per fornire valori iniziali specifici a first e second. Esiste anche la<br />
funzione di Libreria make_pair, che rest<strong>it</strong>uisce un oggetto pair, data una<br />
coppia di valori. Gli elementi di map e multimap sono oggetti di pair<br />
Requis<strong>it</strong>i degli elementi e relazioni d'ordine<br />
Abbiamo detto che i template della STL possono essere istanziati con qualsiasi<br />
tipo di elementi, a libera scelta dell'utente. Se il tipo prescelto è nativo (non<br />
puntatore!) non ci sono problemi. Ma se il tipo è defin<strong>it</strong>o dall'utente, esistono<br />
alcuni requis<strong>it</strong>i a cui deve soddisfare, se si vuole che le operazioni forn<strong>it</strong>e dalla<br />
Libreria funzionino correttamente.<br />
Anz<strong>it</strong>utto le copie: gli elementi sono inser<strong>it</strong>i nel conten<strong>it</strong>ore tram<strong>it</strong>e copia di<br />
oggetti esistenti, e quindi il nostro tipo deve essere provvisto di un costruttore<br />
di copia e di un operatore di assegnazione adeguati (per esempio non devono<br />
eseguire le copie dei membri puntatori ma delle aree puntate ecc...). Se<br />
necessario, deve essere presente anche un corretto distruttore, poichè, quando<br />
un conten<strong>it</strong>ore è distrutto, sono automaticamente distrutti anche i suoi<br />
elementi.<br />
In secondo luogo, l'ordinamento: i conten<strong>it</strong>ori associativi e prior<strong>it</strong>y_queue<br />
ordinano gli elementi (nel momento stesso in cui li inseriscono), e la stessa cosa<br />
viene fatta da alcuni algor<strong>it</strong>mi che operano sui conten<strong>it</strong>ori. E' pertanto<br />
indispensabile che il nostro tipo sia provvisto delle funzional<strong>it</strong>à necessarie per<br />
l'ordinamento dei suoi oggetti. A volte queste funzional<strong>it</strong>à possono essere forn<strong>it</strong>e<br />
da oggetti-funzione specifici (di cui parleremo più avanti, anticipiamo solo che<br />
questi sono indispensabili nel caso che gli elementi siano puntatori a tipo<br />
nativo), ma di default esse vengono cercate fra gli operatori in overload<br />
defin<strong>it</strong>i nel tipo stesso. Fortunatamente non è necessario attrezzare il nostro tipo<br />
con tutti gli operatori relazionali possibili, ma è sufficiente che ce ne sia solo<br />
uno: operator
• X < X è falso (ordine stretto)<br />
• è ammessa la possibil<strong>it</strong>à che X < Y e Y < X siano entrambi falsi (ordine<br />
debole); in questo caso si dice che X e Y hanno ordine equivalente<br />
(cioè in pratica sono uguali, ma non è necessario definire operator==)<br />
• devono vale le proprietà trans<strong>it</strong>ive:<br />
o se X < Y e Y < Z allora X < Z<br />
o se X e Y hanno ordine equivalente e Y e Z hanno ordine<br />
equivalente, allora anche X e Z hanno ordine equivalente<br />
Passiamo ora alla descrizione delle principali funzioni-membro dei conten<strong>it</strong>ori. A parte gli<br />
adattatori, che possiedono poche funzioni specifiche, gli altri conten<strong>it</strong>ori hanno molte<br />
funzioni in comune, con lo stesso nome e lo stesso significato. Pertanto, nella trattazione che<br />
segue, raggruperemo le funzioni non per conten<strong>it</strong>ore, ma per "tematiche", indicando con<br />
Cont il nome di una generica classe conten<strong>it</strong>ore e precisando eventualmente in quale<br />
conten<strong>it</strong>ore un certa funzione è o non è defin<strong>it</strong>a, o è defin<strong>it</strong>a ma inefficiente; se non<br />
altrimenti specificato, si intende che la funzione è defin<strong>it</strong>a nelle sequenze principali e nei<br />
conten<strong>it</strong>ori associativi; indicheremo inoltre con Iter il nome di un generico tipo <strong>it</strong>eratore.<br />
Dimensioni e capac<strong>it</strong>à<br />
Di default lo spazio di memoria per gli elementi di un conten<strong>it</strong>ore è allocato<br />
nell'area heap, ma di questo l'utente non deve normalmente preoccuparsi, in<br />
quanto ogni conten<strong>it</strong>ore possiede un distruttore che libera automaticamente<br />
l'area allocata.<br />
La dimensione di un conten<strong>it</strong>ore (cioè il numero dei suoi elementi) non è<br />
prefissata e immodificabile (come negli array del C). Un oggetto conten<strong>it</strong>ore<br />
"nasce" con una certa dimensione, ma esistono diversi metodi che possono<br />
modificarla (direttamente o implic<strong>it</strong>amente). La funzione-membro che modifica<br />
direttamente una dimensione è:<br />
void Cont::resize(size_type n, value_type<br />
val=value_type())<br />
dove n è la nuova dimensione: se è minore della dimensione corrente,<br />
vengono mantenuti solo i primi n elementi (con i loro valori); se è maggiore,<br />
vengono inser<strong>it</strong>i i nuovi elementi con valori tutti uguali a val, inizializzato di<br />
default al valore base del loro tipo (value_type); la specifica dell'argomento<br />
opzionale val è obbligatoria nel caso che value_type non abbia un<br />
costruttore di default. Il metodo resize è defin<strong>it</strong>o soltanto nelle sequenze<br />
principali.<br />
Altri metodi, che aggiungono, inseriscono o rimuovono elementi in un<br />
conten<strong>it</strong>ore, ne modificano la dimensione implic<strong>it</strong>amente (li vedremo fra poco).<br />
In ogni caso, quando la dimensione cambia, gli <strong>it</strong>eratori precedentemente<br />
defin<strong>it</strong>i potrebbero non essere più validi (conviene ridefinirli o, almeno,<br />
riinizializzarli).<br />
I seguenti metodi in sola lettura rest<strong>it</strong>uiscono informazioni sulla dimensione di<br />
un conten<strong>it</strong>ore:<br />
size_type Cont::size() const rest<strong>it</strong>uisce la dimensione corrente dell'oggetto<br />
*this; è defin<strong>it</strong>o anche negli adattatori<br />
bool Cont::empty() const rest<strong>it</strong>uisce true se *this è vuoto; è defin<strong>it</strong>o anche<br />
negli adattatori
size_type Cont::max_size()<br />
const<br />
rest<strong>it</strong>uisce la dimensione massima che un oggetto<br />
di Cont può raggiungere (è un numero<br />
normalmente molto grande, che dipende dalla<br />
stessa dimensione di value_type e<br />
dall'implementazione)<br />
Se definiamo "capac<strong>it</strong>à" di un oggetto conten<strong>it</strong>ore la quant<strong>it</strong>à di memoria<br />
correntemente allocata (in termini di numero di elementi), è valida la seguente<br />
diseguaglianza:<br />
capac<strong>it</strong>à >= dimensione<br />
questo significa che, se la dimensione aumenta, ma resta inferiore alla<br />
capac<strong>it</strong>à, non viene allocata nuova memoria; appena la dimensione tende a<br />
superare la capac<strong>it</strong>à, si ha una riallocazione della memoria in modo da<br />
ripristinare la diseguaglianza di cui sopra. In altri termini, la differenza:<br />
capac<strong>it</strong>à - dimensione<br />
rappresenta il numero di elementi che si possono inserire senza causare<br />
riallocazione di memoria.<br />
In realtà, in tutti i conten<strong>it</strong>ori, salvo vector, capac<strong>it</strong>à e dimensione sono<br />
coincidenti, cioè ogni operazione che comporta l'aumento della dimensione<br />
produce contestualmente anche una nuova allocazione di memoria. Per ev<strong>it</strong>are<br />
che ciò avvenga troppo spesso e che il "costo" di tali operazioni diventi troppo<br />
elevato, vector mette a disposizione il seguente metodo, che consente di<br />
aumentare la capac<strong>it</strong>à senza modificare la dimensione, cioè in pratica di ev<strong>it</strong>are<br />
continue riallocazioni, riservando uno spazio di memoria "preventivo", ma senza<br />
inserirvi nuovi elementi:<br />
void vector::reserve(size_type n)<br />
dove n è la nuova capac<strong>it</strong>à: se è minore della capac<strong>it</strong>à corrente, la funzione<br />
non ha effetto; se è maggiore, alloca spazio per (n - capac<strong>it</strong>à corrente) "futuri"<br />
nuovi elementi. Si deduce che, con reserve, la capac<strong>it</strong>à di un conten<strong>it</strong>ore<br />
può soltanto aumentare; e la stessa cosa succede a segu<strong>it</strong>o di resize e delle altre<br />
operazioni che modificano la dimensione: la capac<strong>it</strong>à o aumenta (quando<br />
tende a essere superata dalla dimensione), o resta invariata, anche se la<br />
dimensione diminuisce; pertanto non esiste modo di "rest<strong>it</strong>uire" memoria al<br />
sistema prima che lo stesso conten<strong>it</strong>ore venga distrutto (in realtà un modo<br />
esiste, ma lo vedremo più avanti, quando parleremo della funzione-membro<br />
swap).<br />
Per ottenere informazioni sulla capac<strong>it</strong>à, è disponibile il seguente metodo:<br />
size_type vector::capac<strong>it</strong>y() const<br />
che rest<strong>it</strong>uisce la quant<strong>it</strong>à di memoria correntemente allocata, in termini di<br />
numero di elementi.<br />
Costruttori e operatori di copia<br />
Tutti i conten<strong>it</strong>ori dispongono di un certo numero di costruttori, e di operatori<br />
e funzioni per eseguire le copie.<br />
Anz<strong>it</strong>utto, il costruttore di default, il costruttore di copia e l'operatore di<br />
assegnazione sono defin<strong>it</strong>i in tutti i conten<strong>it</strong>ori (adattatori compresi):<br />
Cont::Cont() crea un oggetto di Cont con dimensione<br />
nulla
Cont::Cont(const Cont& c) crea un oggetto di Cont copiandolo<br />
dall'oggetto esistente c<br />
Cont& Cont::operator=(const<br />
Cont& c)<br />
NOTE:<br />
assegna un oggetto esistente c a *this<br />
1. il costruttore di copia e l'operatore di assegnazione non ammettono<br />
conversioni implic<strong>it</strong>e, né fra i tipi dei conten<strong>it</strong>ori, né fra i tipi degli<br />
elementi (in altre parole, non si può copiare un list in un vector, e<br />
neppure un vector in un vector)<br />
2. il nuovo oggetto creato dal costruttore di copia assume la dimensione<br />
di c, ma non la sua capac<strong>it</strong>à, che viene invece fatta coincidere con la<br />
dimensione (cioé è allocata memoria solo per gli elementi copiati)<br />
3. dopo l'assegnazione, *this assume la dimensione di c (gli elementi<br />
preesistenti vengono eliminati), ma non riduce la sua capac<strong>it</strong>à originaria<br />
(può solo aumentarla nel caso che venga superata dalla nuova<br />
dimensione)<br />
4. come è noto, i costruttori di copia entrano in azione anche nel passaggio<br />
by value di argomenti a una funzione. Nel caso che tali argomenti<br />
siano oggetti di un conten<strong>it</strong>ore, l'operazione potrebbe essere<br />
"costosa", se la dimensione del conten<strong>it</strong>ore è molto grande. Pertanto si<br />
consiglia, quando non è necessario altrimenti per motivi particolari, di<br />
passare sempre gli argomenti-conten<strong>it</strong>ore by reference.<br />
Nelle sole sequenze principali sono inoltre defin<strong>it</strong>e le due seguenti funzioni:<br />
• un costruttore con un 1 argomento (più altri di default, di cui a noi<br />
interessa solo il primo):<br />
Cont::Cont(size_type n, const_reference val=value_type())<br />
che crea un oggetto di Cont con dimensione n e inizializza gli<br />
elementi con val (riguardo all'argomento di default vedere le<br />
considerazioni fatte a propos<strong>it</strong>o di resize); nella definizione della classe<br />
Cont questa funzione-membro è dichiarata explic<strong>it</strong>, per ev<strong>it</strong>are<br />
"accidentali" conversioni implic<strong>it</strong>e da size_type a Cont;<br />
• il metodo assign, che è una specie di "estensione" dell'operatore di<br />
assegnazione (non si può usare un operatore in overload perchè<br />
avrebbe "troppi" argomenti):<br />
void Cont::assign(size_type n, const_reference val)<br />
esegue la stessa operazione del costruttore di cui sopra, ma su un<br />
oggetto di Cont già esistente (altra differenza: il secondo argomento<br />
non è di default); come in tutte le operazioni di assegnazione, i<br />
"vecchi" elementi vengono eliminati, la dimensione diventa n, ma la<br />
capac<strong>it</strong>à resta invariata (o aumenta, se era minore di n)<br />
Finora abbiamo esaminato vari casi di operazioni di copia fra conten<strong>it</strong>ori<br />
vincolati a essere dello stesso tipo. Esiste però un costruttore che permette la<br />
creazione degli oggetti di un conten<strong>it</strong>ore mediante copia da un qualunque<br />
altro conten<strong>it</strong>ore, anche di tipo diverso (anche i tipi degli elementi possono<br />
essere diversi, purché convertibili implic<strong>it</strong>amente gli uni negli altri):<br />
Cont::Cont(Iter first, Iter last)<br />
(dove Iter è un tipo <strong>it</strong>eratore defin<strong>it</strong>o in Cont o in un altro conten<strong>it</strong>ore);
questo metodo crea un oggetto di Cont, i cui elementi vengono generati<br />
mediante copia a partire dall'elemento puntato da first fino all'elemento<br />
puntato da last (escluso).<br />
Per esempio, se lst è un oggetto di list (già defin<strong>it</strong>o e inizializzato), è<br />
possibile creare un oggetto vec di vector copiandovi tutti gli<br />
elementi di lst (e convertendoli da int a double) con l'operazione:<br />
vector vec(lst.begin(),lst.end());<br />
E' anche possibile eseguire un'assegnazione, con operazione analoga su un<br />
oggetto di Cont già esistente, mediante un overload del metodo assign<br />
(defin<strong>it</strong>o solo nelle sequenze principali):<br />
void Cont::assign(Iter first, Iter last)<br />
Riprendendo l'esempio precedente, l'operazione:<br />
vec.assign(lst.begin(),lst.end());<br />
elimina in vec i suoi "vecchi" elementi e li sost<strong>it</strong>uisce con quelli di lst (che<br />
converte da int a double)<br />
Infine, nel numero delle funzioni che eseguono copie di conten<strong>it</strong>ori, si può<br />
includere anche il metodo swap:<br />
void Cont::swap(Cont& c)<br />
che scambia gli elementi, la dimensione e la capac<strong>it</strong>à fra *this e c; i tipi, sia<br />
dei conten<strong>it</strong>ori che degli elementi, devono essere gli stessi nei due oggetti.<br />
Per ogni conten<strong>it</strong>ore è disponibile, oltre al metodo swap, anche una funzione<br />
esterna, con lo stesso nome:<br />
void swap(Cont& c1,Cont& c2)<br />
che scambia c1 con c2<br />
Notare che la peculiar<strong>it</strong>à di swap di scambiare anche le capac<strong>it</strong>à, fornisce un<br />
"trucco" che permette di ridurre la memoria allocata a un oggetto conten<strong>it</strong>ore.<br />
Infatti, supponiamo per esempio di avere un oggetto vec di un conten<strong>it</strong>ore<br />
vector, con dimensione n e capac<strong>it</strong>à m > n; con l'istruzione:<br />
vector* ptmp = new vector (vec);<br />
costruiamo un oggetto nell'area heap (puntato da ptmp) che, essendo una<br />
copia di vec, ha dimensione n e capac<strong>it</strong>à n; quindi, con l'istruzione:<br />
vec.swap(*ptmp);<br />
otteniamo che l'oggetto vec si "scambia" con *ptmp (ma gli elementi sono gli<br />
stessi!) e quindi, in particolare, la sua capac<strong>it</strong>à si riduce a n (mentre quella di<br />
*ptmp diventa m); infine, con l'istruzione:<br />
delete ptmp;<br />
liberiamo la memoria allocata per *ptmp (e per i suoi m elementi). In totale<br />
rimane l'oggetto originario vec con tutto come prima, salvo il fatto che la<br />
memoria in eccesso è stata deallocata.<br />
Accesso agli elementi<br />
Tutte le operazioni di accesso agli elementi possono funzionare sia in lettura<br />
che in scr<strong>it</strong>tura, cioè possono rest<strong>it</strong>uire sia un r-value (lettura) che un lvalue(scr<strong>it</strong>tura).<br />
La più generale operazione di accesso è la dereferenziazione di un <strong>it</strong>eratore<br />
(che abbiamo già visto nella sezione dedicata agli <strong>it</strong>eratori).<br />
I conten<strong>it</strong>ori: vector, deque, e map possono accedere ai propri elementi<br />
anche tram<strong>it</strong>e operatori di indicizzazione:
• reference Cont::operator[](size_type i)<br />
per vector e deque; l'argomento i rappresenta l'indice;<br />
• const_reference Cont::operator[](size_type i) const<br />
come il precedente, salvo che accede in sola lettura;<br />
• mapped_type Cont::operator[](const key_type& k)<br />
per map (vedere la descrizione nella tabella sommaria dei conten<strong>it</strong>ori);<br />
l'argomento k rappresenta la chiave, che funge da indice.<br />
A parte l'ovvia differenza fra i tipi degli indici, c'è un'altra fondamentale<br />
differenza fra l'indicizzazione in map e quella in vector e deque: mentre la<br />
prima va sempre "a buon fine" (nel senso che, se un elemento con chiave k<br />
non esiste, l'elemento viene aggiunto), la seconda può generare un errore (non<br />
segnalato) di valore indefin<strong>it</strong>o (se in lettura) o di access violation (se in<br />
scr<strong>it</strong>tura), nel caso che l'elemento con indice i non esista. In altri termini, i<br />
deve essere sempre compreso nel range fra 0 e size() (escluso). Il fatto che<br />
l'accesso via indice non sia controllato è una "scelta" di progetto, che permette<br />
di ev<strong>it</strong>are operazioni "costose" quando il controllo non è necessario. Per<br />
esempio, consideriamo il seguente codice:<br />
vector vec(100000); (crea un oggetto vec con 100000 elementi<br />
vuoti)<br />
for(size_type i=0; i < vec.size(); i++) ( li riempie ....)<br />
{ ................. vec[i] = ................. }<br />
sarebbe oltremodo "costoso" (oltre che sciocco) controllare 100000 volte che i<br />
sia nel range!<br />
A volte invece il controllo è proprio necessario, specie nei casi in cui il valore di i<br />
risulta da operazioni precedenti e quindi non è possibile conoscerlo a priori.<br />
L'accesso via indice "controllato" è forn<strong>it</strong>o dal metodo at (defin<strong>it</strong>o in vector e<br />
deque):<br />
reference Cont::at(size_type i)<br />
const_eference Cont::at(size_type i) const (per la sola lettura)<br />
che, in caso di errore, genera un'eccezione di tipo out_of_range.<br />
Ci chiedamo a questo punto quale relazione intercorra fra gli indici e gli<br />
<strong>it</strong>eratori. E' chiaro che (indicando con c un oggetto di vector o di deque e<br />
con <strong>it</strong> un oggetto <strong>it</strong>eratore (diretto) che inizializziamo con begin()), è<br />
sempre vera l'uguaglianza:<br />
c[0] == *<strong>it</strong><br />
e quindi, per analogia con i puntatori, siamo portati a pensare che sia vera<br />
anche la seguente:<br />
c[i] == *(<strong>it</strong>+i)<br />
in realtà lo è, ma solo perchè abbiamo supposto che c sia un oggetto di vector<br />
o di deque, i cui <strong>it</strong>eratori sono ad accesso casuale e quindi ammettono<br />
l'operazione + con valori interi; mentre non è valida la relazione:<br />
&c[0] == <strong>it</strong><br />
in quanto puntatori e <strong>it</strong>eratori sono tipi differenti.<br />
Le operazioni di accesso in testa e in coda possono anche essere esegu<strong>it</strong>e da<br />
particolari metodi (defin<strong>it</strong>i nelle sequenze principali e nell'adattatore<br />
queue):<br />
reference Cont::front() (accede al primo elemento)<br />
const_reference Cont::front() const (come sopra, in sola lettura)<br />
reference Cont::back() (accede al l'ultimo elemento)
const_reference Cont::back() const (come sopra, in sola lettura)<br />
Gli adattatori stack e prior<strong>it</strong>y_queue possono accedere soltanto al primo<br />
elemento (prior<strong>it</strong>y_queue) o all'ultimo (stack); entrambe le operazioni<br />
vengono esegu<strong>it</strong>e dal metodo top(), il quale non fa altro che chiamare front()<br />
(in prior<strong>it</strong>y_queue) o back() (in stack).<br />
I metodi front, back e top possono generare un errore (incontrollato) se<br />
tentano di accedere a un conten<strong>it</strong>ore vuoto.<br />
Inserimento e cancellazione di elementi<br />
Le operazioni di inserimento e cancellazione di elementi sono presenti in<br />
tutti i conten<strong>it</strong>ori. Tuttavia, in alcuni di essi sono poco efficienti e quindi è<br />
necessario capire in quali conten<strong>it</strong>ori conviene eseguire certe operazioni e in<br />
quali no. A questo scopo, presentiamo nella tabella che segue la relazione che<br />
intercorre, in termini di efficienza, fra ogni conten<strong>it</strong>ore e le sue operazioni di<br />
inserimento e cancellazione, che suddividiamo in tre categorie: operazioni in<br />
testa, in "mezzo" e in coda:<br />
inserimento/<br />
cancellazione<br />
vector deque list queue prior<strong>it</strong>y_queue stack<br />
in testa non defin<strong>it</strong>a efficiente efficiente<br />
efficiente<br />
(solo<br />
canc.)<br />
in "mezzo" inefficiente inefficiente efficiente non<br />
defin<strong>it</strong>a<br />
in coda efficiente efficiente efficiente<br />
efficiente<br />
(solo<br />
ins.)<br />
vedere nota<br />
non defin<strong>it</strong>a<br />
NOTA: ricordiamo che nei conten<strong>it</strong>ori associativi gli inserimenti le<br />
cancellazioni sono sempre, come l'accesso, a "costo logar<strong>it</strong>mico"; in<br />
prior<strong>it</strong>y_queue l'inserimento è a "costo logar<strong>it</strong>mico" (perchè deve<br />
"ordinare"), mentre la cancellazione è a "costo costante".<br />
non<br />
defin<strong>it</strong>a<br />
non<br />
defin<strong>it</strong>a<br />
conten<strong>it</strong><br />
associat<br />
non defin<br />
vedere not<br />
non defin<strong>it</strong>a efficiente non defin<br />
Ciò premesso, vediamo i metodi disponibili per queste operazioni (ricordiamo<br />
che esse modificano implic<strong>it</strong>amente la dimensione e quindi rendono invalidi gli<br />
<strong>it</strong>eratori defin<strong>it</strong>i precedentemente); indicheremo con val l'elemento da inserire<br />
e con <strong>it</strong> l'<strong>it</strong>eratore che punta all'elemento da cancellare o all'elemento prima<br />
del quale il nuovo elemento deve essere inser<strong>it</strong>o:<br />
inserimento in testa void Cont::push_front(const_reference val)<br />
(in prior<strong>it</strong>y_queue cambia nome in push)<br />
cancellazione in testa void Cont::pop_front()<br />
(in queue e in prior<strong>it</strong>y_queue cambia nome in pop)<br />
inserimento in "mezzo"<br />
(vedere nota)<br />
<strong>it</strong>erator Cont::insert(<strong>it</strong>erator <strong>it</strong>,const_reference val)<br />
(r<strong>it</strong>orna un <strong>it</strong>eratore che punta al nuovo elemento)<br />
void Cont::insert(<strong>it</strong>erator <strong>it</strong>,size_type<br />
n,const_reference val)<br />
(inserisce n volte val)<br />
void Cont::insert(<strong>it</strong>erator <strong>it</strong>,Iter first, Iter last)
(dove Iter è un tipo <strong>it</strong>eratore defin<strong>it</strong>o in Cont o in un altro<br />
conten<strong>it</strong>ore; inserisce elementi generati mediante copia a<br />
partire dall'elemento puntato da first fino all'elemento<br />
puntato da last escluso)<br />
cancellazione in "mezzo" <strong>it</strong>erator Cont::erase(<strong>it</strong>erator <strong>it</strong>)<br />
(r<strong>it</strong>orna un <strong>it</strong>eratore che punta all'elemento successivo a<br />
quello cancellato, oppure r<strong>it</strong>orna end() se l'elemento<br />
cancellato era l'ultimo)<br />
<strong>it</strong>erator Cont::erase(<strong>it</strong>erator first, <strong>it</strong>erator last)<br />
(cancella una serie di elementi contigui, a partire<br />
dall'elemento puntato da first fino all'elemento puntato da<br />
last escluso; r<strong>it</strong>orna come sopra)<br />
void Cont::clear()<br />
(elimina tutti gli elementi; equivale a erase con argomenti<br />
begin() e end(), ma è molto più veloce)<br />
inserimento in coda void Cont::push_back(const_reference val)<br />
(in queue e stack cambia nome in push)<br />
cancellazione in coda void Cont::pop_back()<br />
(in stack cambia nome in pop)<br />
NOTA: gli overloads del metodo insert elencati nella tabella riguardano solo le<br />
sequenze principali; nei conten<strong>it</strong>ori associativi insert è defin<strong>it</strong>o con<br />
overloads diversi (vedere più avanti).<br />
Tabella riassuntiva delle funzioni comuni<br />
Abbiamo esaur<strong>it</strong>o la trattazione degli adattatori e delle funzionimembro<br />
comuni a più conten<strong>it</strong>ori. Prima di passare alla descrizione dei metodi<br />
specifici di singoli conten<strong>it</strong>ori, presentiamo, nella seguente tabella l'elenco delle<br />
funzioni esaminate finora. La legenda dei simboli usati è:<br />
ogni conten<strong>it</strong>ore è indicato dalla sua iniziale (es.: v = vector)<br />
a = conten<strong>it</strong>ore associativo (escluso map)<br />
C = "costo costante", L = "costo logar<strong>it</strong>mico", N = "non defin<strong>it</strong>a"<br />
I = "inefficiente" (costo proporzionale al numero di elementi)<br />
v d l m a q p s<br />
dereferenziazione di un <strong>it</strong>eratore C C C C C N N N<br />
begin end rbegin rend C C C C C N N N<br />
resize C C C N N N N N<br />
size empty C C C C C C C C<br />
max_size C C C C C N N N<br />
reserve capac<strong>it</strong>y C N N N N N N N<br />
costruttore di default C C C C C C C C<br />
costruttore di copia operator= I I I I I I I I<br />
costruttore con dimensione assign I I I N N N N N
Metodi specifici di list<br />
costruttore tram<strong>it</strong>e <strong>it</strong>eratori I I I I I N N N<br />
swap C C C C C N N N<br />
operator[] C C N L N N N N<br />
at C C N N N N N N<br />
front back C C C N N C N N<br />
top N N N N N N C C<br />
push_front pop_front N C C N N N N N<br />
push_back pop_back C C C N N N N N<br />
push N N N N N C L C<br />
pop N N N N N C C C<br />
insert erase I I C L L N N N<br />
clear C C C C C N N N<br />
Come si desume dalla tabella, il conten<strong>it</strong>ore list possiede tutte le funzional<strong>it</strong>à di<br />
vector, escluse la "riserva" di memoria (reserve e capac<strong>it</strong>y) e l'accesso via<br />
indice (operator[] e at); in più, può eseguire, come deque, operazioni di<br />
inserimento e cancellazione in testa (push_front e pop_front) ed è più<br />
efficiente di vector e deque nelle operazioni di inserimento e<br />
cancellazione in "mezzo" (insert e erase).<br />
In aggiunta, sono defin<strong>it</strong>i in list alcuni metodi specifici, che forniscono<br />
operazioni particolarmente adatte alla manipolazione delle liste:<br />
• metodo splice, in 3 overloads:<br />
void list::splice(<strong>it</strong>erator <strong>it</strong>, list& lst)<br />
void list::splice(<strong>it</strong>erator <strong>it</strong>, list& lst, <strong>it</strong>erator first)<br />
void list::splice(<strong>it</strong>erator <strong>it</strong>, list& lst, <strong>it</strong>erator first, <strong>it</strong>erator last)<br />
il metodo splice "muove" degli elementi (cioè li copia, cancellando gli<br />
originari) dall'oggetto lst in *this, inserendoli prima dell'elemento di<br />
*this puntato da <strong>it</strong>; nel primo overload vengono mossi tutti gli elementi<br />
di lst (che resta vuoto); nel secondo, viene mosso solo l'elemento di lst<br />
puntato da first; nel terzo, vengono mossi gli elementi contigui di lst,<br />
puntati a partire da first fino a last escluso; è ammesso che *this e lst<br />
coincidano solo a condizione che il range degli elementi da muovere<br />
non contenga <strong>it</strong> (e quindi non è mai ammesso nel primo caso)<br />
• void list::reverse()<br />
inverte gli elementi (cioè scambia il primo con l'ultimo, il secondo con il<br />
penultimo ecc...)<br />
• void list::sort()<br />
ordina gli elementi in senso ascendente (esiste anche un overload in cui<br />
si può imporre la condizione d'ordine tram<strong>it</strong>e un oggetto-funzione, ma ne<br />
parleremo in generale quando tratteremo degli algor<strong>it</strong>mi; la stessa<br />
considerazione vale anche riguardo ai successivi metodi di questo elenco)<br />
• void list::remove(const_reference val)<br />
elimina tutti gli elementi che trova uguali a val
• void list::merge(list& lst)<br />
muove in *this tutti gli elementi di lst (che resta vuoto); se in entrambe<br />
le liste gli elementi erano in ordine, si mantengono in ordine anche nella<br />
lista risultante, altrimenti gli elementi vengono mescolati senza un ordine<br />
defin<strong>it</strong>o<br />
• void list::unique()<br />
elimina tutti gli elementi duplicati contigui (l'operazione ha senso solo se<br />
la lista è gia in ordine)<br />
Metodi specifici dei conten<strong>it</strong>ori associativi<br />
Abbiamo visto che le classi template map e multimap hanno (almeno) due<br />
parametri: la chiave (tipo key_type) e il valore mappato (tipo<br />
mapped_type), defin<strong>it</strong>i in quest'ordine. I loro elementi (tipo value_type)<br />
sono invece specializzazioni della struttura template pair, con argomenti:<br />
const key_type e value_type.<br />
Le classi template set e multiset possono considerarsi dei conten<strong>it</strong>ori<br />
associativi "degeneri" con un solo parametro: la chiave (gli elementi sono<br />
cost<strong>it</strong>u<strong>it</strong>i dalla chiave stessa, e quindi i tipi key_type e value_type sono<br />
coincidenti, mentre mapped_type non esiste).<br />
Tutti i conten<strong>it</strong>ori associativi possiedono <strong>it</strong>eratori bidirezionali, che (di<br />
default) percorrono gli elementi in ordine crescente di chiave.<br />
Dell'operatore di indicizzazione (defin<strong>it</strong>o solo in map) abbiamo già detto;<br />
aggiungiamo solo che non può lavorare su mappe costanti, in quanto, se non<br />
trova un elemento, lo crea. Per eseguire una ricerca senza modificare la mappa,<br />
bisogna usare il metodo find (vedere più avanti).<br />
Per quello che riguarda l'operazione di inserimento di nuovi elementi, fermo<br />
restando che in map il modo più semplice e comune è quello di usare<br />
l'operatore di indicizzazione come l-value (con un nuovo valore della<br />
chiave), in tutti i conten<strong>it</strong>ori associativi si può usare il metodo insert, i cui<br />
overloads sono però diversi da quelli elencati nella tabella generale (al sol<strong>it</strong>o,<br />
indicheremo con val l'elemento da inserire):<br />
• pair Cont::insert(const_reference val)<br />
è defin<strong>it</strong>o solo in map e set; "tenta" di inserire val, cercando se esiste<br />
già una chiave uguale a val.first (se è in map), oppure uguale a val (se<br />
è in set); se la trova, non esegue l'inserimento; rest<strong>it</strong>uisce un oggetto di<br />
pair, in cui first è un <strong>it</strong>eratore che punta all'elemento (vecchio o nuovo)<br />
con chiave val.first (o val se è in set), e second è true nel caso che<br />
val sia stato effettivamente inser<strong>it</strong>o<br />
• <strong>it</strong>erator Cont::insert(const_reference val)<br />
come il precedente, salvo che inserisce val comunque e rest<strong>it</strong>uisce un<br />
<strong>it</strong>eratore che punta al nuovo elemento inser<strong>it</strong>o; è defin<strong>it</strong>o solo in<br />
multimap e multiset<br />
• <strong>it</strong>erator Cont::insert(<strong>it</strong>erator <strong>it</strong>,const_reference val)<br />
è identico nella forma all'overload defin<strong>it</strong>o nelle sequenze principali;<br />
se ne differisce per il significato dell'argomento <strong>it</strong>, che non rappresenta<br />
più il punto dove inserire val (nei conten<strong>it</strong>ori associativi ogni elemento<br />
è sempre inser<strong>it</strong>o nella posizione d'ordine che gli compete), ma piuttosto il
punto dal quale iniziare la ricerca: se risulta che val deve essere inser<strong>it</strong>o<br />
immediatamente dopo <strong>it</strong>, l'operazione non è più a "costo logar<strong>it</strong>mico"<br />
ma a "costo costante" (questo overload può servire per inserire<br />
rapidamente una sequenza di elementi già ordinati, utilizzando in ogni<br />
step il valore di r<strong>it</strong>orno come argomento <strong>it</strong> per lo step successivo)<br />
• void Cont::insert(Iter first, Iter last)<br />
dove Iter è un tipo <strong>it</strong>eratore defin<strong>it</strong>o in Cont o in un altro conten<strong>it</strong>ore;<br />
inserisce elementi generati mediante copia a partire dall'elemento<br />
puntato da first fino all'elemento puntato da last escluso<br />
Anche il metodo erase è un pò diverso, nel senso che fornisce un overload in<br />
più rispetto a quelli già visti:<br />
size_type Cont::erase(const key_type& k)<br />
esegue la ricerca degli elementi con chiave k e, se li trova, li cancella;<br />
rest<strong>it</strong>uisce il numero degli elementi cancellati (che può essere 0 se non ne ha<br />
trovato nessuno, e può essere maggiore di 1 solo in multimap e multiset)<br />
Infine, esistono alcuni metodi defin<strong>it</strong>i solo nei conten<strong>it</strong>ori associativi (per<br />
ognuno di essi esiste anche, ma tralasciamo di indicarla, la versione per gli<br />
oggetti const):<br />
• <strong>it</strong>erator Cont::find(const key_type& k)<br />
rest<strong>it</strong>uisce un <strong>it</strong>eratore che punta al primo elemento con chiave k; se<br />
non ne trova, rest<strong>it</strong>uisce end()<br />
• <strong>it</strong>erator Cont::lower_bound(const key_type& k)<br />
esegue in pratica la stessa operazione di find<br />
• <strong>it</strong>erator Cont::upper_bound(const key_type& k)<br />
rest<strong>it</strong>uisce un <strong>it</strong>eratore che punta al primo elemento con chiave<br />
maggiore di k; se non ne trova, rest<strong>it</strong>uisce end()<br />
• pair Cont::equal_range(const key_type& k)<br />
rest<strong>it</strong>uisce una coppia di <strong>it</strong>eratori in cui first è uguale al valore di<br />
r<strong>it</strong>orno di lower_bound e second è uguale al valore di r<strong>it</strong>orno di<br />
upper_bound<br />
• size_type Cont::count(const key_type& k)<br />
rest<strong>it</strong>uisce il numero degli elementi con la stessa chiave k<br />
Il metodo find è usato preferibilmente in map e set; gli altri hanno senso solo<br />
se usati in conten<strong>it</strong>ori con chiave duplicata (cioè in multimap e multiset)<br />
Funzioni esterne<br />
In tutti gli header-files in cui sono defin<strong>it</strong>e le classi dei conten<strong>it</strong>ori, è anche<br />
defin<strong>it</strong>o un insieme (sempre uguale) di funzioni esterne di "appoggio". Abbiamo<br />
già visto la funzione swap. Le altre sono cost<strong>it</strong>u<strong>it</strong>e dal set completo degli<br />
operatori relazionali, che servono per confrontare fra loro oggetti<br />
conten<strong>it</strong>ori. Le regole applicate sono le seguenti:<br />
• due oggetti conten<strong>it</strong>ori sono uguali (operator==) se hanno la stessa<br />
dimensione e tutti gli elementi corrispondenti sono uguali (e quindi è<br />
necessario che anche nel tipo degli elementi sia defin<strong>it</strong>o operator==);<br />
• dati due oggetti conten<strong>it</strong>ori, a e b, si definisce a minore di b<br />
(operator
1. tutti gli elementi corrispondenti sono uguali e la dimensione di a<br />
è minore della dimensione di b, oppure<br />
2. indipendentemente dalla dimensione di a e di b, il primo<br />
elemento di a non uguale al corrispondente elemento di b è<br />
minore del corrispondente elemento di b (e quindi è necessario<br />
che anche nel tipo degli elementi sia defin<strong>it</strong>o operator
Nella chiamata di un algor<strong>it</strong>mo (che normalmente coincide con la sua<br />
istanziazione, con deduzione implic<strong>it</strong>a degli argomenti del template) gli<br />
argomenti che esprimono i due <strong>it</strong>eratori devono essere dello stesso tipo<br />
(diversamente il compilatore produre un messaggio di errore). A parte questa<br />
lim<strong>it</strong>azione (peraltro ovvia), gli algor<strong>it</strong>mi sono perfettamente generici, nel senso<br />
che possono operare su qualsiasi tipo di conten<strong>it</strong>ore (e su qualsiasi tipo degli<br />
elementi), purché provvisto di <strong>it</strong>eratori; anzi, proprio perché agiscono<br />
attraverso gli <strong>it</strong>eratori, alcuni algor<strong>it</strong>mi possono funzionare altrettanto bene su<br />
classi di dati, come le stringhe e le classi di input-output, che non sono<br />
propriamente conten<strong>it</strong>ori, ma che hanno in comune la proprietà di definire<br />
sequenze espresse in termini di <strong>it</strong>eratori. Inoltre, la maggior parte degli<br />
algor<strong>it</strong>mi funziona anche su normali array (in questo caso, al posto degli<br />
<strong>it</strong>eratori, bisogna mettere i puntatori, mantenendo però la regola della<br />
sequenza semi-aperta).<br />
Pertanto, la definizione più comune di un algor<strong>it</strong>mo (che indichiamo<br />
genericamente con fun) è:<br />
template (tipo di r<strong>it</strong>orno) fun(Iter first, Iter last,<br />
......)<br />
dove Iter è il tipo dell'<strong>it</strong>eratore associato alla sequenza di ingresso e first e<br />
last rappresentano gli estremi della sequenza. Gli altri parametri del<br />
template e gli altri argomenti dell'algor<strong>it</strong>mo sono cost<strong>it</strong>u<strong>it</strong>i di sol<strong>it</strong>o da altri<br />
<strong>it</strong>eratori (di ingresso o di usc<strong>it</strong>a), da valori di dati o da oggetti-funzione. Se un<br />
algor<strong>it</strong>mo coinvolge due sequenze, i cui corrispondenti tipi <strong>it</strong>eratori sono<br />
individuati da due parametri distinti, i tipi delle due sequenze non devono<br />
essere necessariamente gli stessi, purchè coincidano i tipi degli elementi (o uno<br />
dei due sia convertibile implic<strong>it</strong>amente nell'altro).<br />
Oggetti-funzione<br />
Abbiamo già introdotto il concetto di oggetto-funzione trattando degli<br />
operatori in overload: gli oggetti-funzione appartengono a classi che hanno<br />
la particolare caratteristica di utilizzare in modo predominante un loro metodo,<br />
defin<strong>it</strong>o come operatore di chiamata di una funzione:<br />
operator() (lista di argomenti)<br />
il che permette di fornire la normale sintassi della chiamata di una funzione a<br />
oggetti di una classe.<br />
Consideriamo ora il caso di una funzione (la chiamiamo fun) che preveda di<br />
eseguire un certo numero di operazioni, non defin<strong>it</strong>e a priori, ma da selezionare<br />
fra diverse operazioni possibili. Occorre pertanto che tali operazioni siano<br />
trasmesse come argomenti di chiamata di fun. Il C risolve il problema<br />
utilizzando i puntatori a funzione: fun definisce fra i suoi argomenti un<br />
puntatore a funzione; questo viene sost<strong>it</strong>u<strong>it</strong>o, in ogni chiamata di fun, con la<br />
funzione "vera" che esegue le operazioni volute. Ma il <strong>C++</strong> "può fare di<br />
meglio"! Infatti i puntatori a funzione potrebbero, in certi casi, rivelarsi<br />
inadeguati, per i seguenti motivi:<br />
• la risoluzione di un puntatore a funzione è un'operazione "costosa", in<br />
quanto il programma deve ogni volta accedere a una tabella di puntatori;<br />
• se una funzione è chiamata più volte, potrebbero esserci informazioni da<br />
conservare o aggiornare; per cui, o si includono tutte queste informazioni
For_each<br />
nella lista degli argomenti, o si definiscono allo scopo delle variabili<br />
globali ("brutto", in entrambi i casi!);<br />
• la scelta è comunque confinata entro un insieme di funzioni predefin<strong>it</strong>e.<br />
Il <strong>C++</strong> consente di ev<strong>it</strong>are questi inconvenienti, se, al posto di un puntatore a<br />
funzione, si inserisce, come argomento di fun, un oggetto-funzione di tipo<br />
parametrizzato. Infatti:<br />
• la chiamata della funzione (attraverso il metodo operator(), defin<strong>it</strong>o<br />
nella classe dell'oggetto-funzione) è esegu<strong>it</strong>a più velocemente, in<br />
quanto non deve accedere a tabelle (oltretutto operator() può, in certi<br />
casi, essere defin<strong>it</strong>o inline);<br />
• le informazioni aggiuntive, da conservare o aggiornare, possono essere<br />
memorizzate nei membri defin<strong>it</strong>i nella stessa classe dell'oggettofunzione;<br />
• poichè la suddetta classe è un parametro di template, non esiste<br />
nessun vincolo predefin<strong>it</strong>o sulla scelta della funzione da eseguire (purchè<br />
il numero e il tipo dei suoi argomenti sia quello previsto).<br />
Molti algor<strong>it</strong>mi utilizzano gli oggetti-funzione come argomenti (e le<br />
corrispondenti classi come parametri). L'utente può chiamare questi<br />
algor<strong>it</strong>mi fornendo una propria classe come argomento del template; tale<br />
classe deve contenere il metodo operator() (con al massimo due argomenti),<br />
che ha il comp<strong>it</strong>o di eseguire le operazioni desiderate sugli elementi di una data<br />
sequenza.<br />
In aggiunta a quelli defin<strong>it</strong>i dall'utente, la STL mette a disposizione un nutr<strong>it</strong>o<br />
numero di oggetti-funzione, le cui classi sono defin<strong>it</strong>e nell'header-file<br />
. Molte di queste classi trasformano sostanzialmente operazioni<br />
in funzioni, in modo da renderle utilizzabili come argomenti negli algor<strong>it</strong>mi (è<br />
il processo logico inverso a quello che porta alla definizione degli operatori in<br />
overload). Nello stesso header-file sono anche defin<strong>it</strong>e alcune classi e<br />
funzioni (dette adattatori) che trasformano oggetti-funzione in altri oggettifunzione,<br />
sempre allo scopo di renderli utilizzabili negli algor<strong>it</strong>mi. Non<br />
approfondiremo oltre questo argomento, la cui trattazione, piuttosto complessa,<br />
esula dagli intendimenti di questo corso; ci lim<strong>it</strong>eremo a c<strong>it</strong>are alcuni casi<br />
particolari, quando se ne presenterà l'occasione.<br />
Un vantaggio chiave nell'uso degli algor<strong>it</strong>mi e degli oggetti-funzione consiste<br />
nella possibil<strong>it</strong>à offerta al programmatore di "risparmiare codice" (e quindi di<br />
"risparmiare errori"!), ev<strong>it</strong>andogli la necess<strong>it</strong>à di scrivere cicli esplic<strong>it</strong>i, che sono<br />
invece esegu<strong>it</strong>i automaticamente con una sola istruzione. Per comprendere bene<br />
tale vantaggio, consideriamo l'algor<strong>it</strong>mo "più generico che esista", for_each, il<br />
quale non fa altro che eseguire "qualcosa" su ogni elemento di una sequenza<br />
(e il "qualcosa" è deciso dall'utente). Il codice di implementazione di questo<br />
algor<strong>it</strong>mo è il seguente:<br />
template Op for_each(Iter first, Iter last, Op<br />
oggf)<br />
{
Predicati<br />
}<br />
while (first != last) oggf (*first++);<br />
return oggf;<br />
notare che for_each non si interessa di sapere cosa sia realmente il suo terzo<br />
argomento, ma si lim<strong>it</strong>a ad applicargli l'operatore (); spetterà poi al<br />
compilatore controllare, in ogni punto di istanziazione di for_each, che:<br />
1. nella classe che sost<strong>it</strong>uisce il parametro Op sia defin<strong>it</strong>o il<br />
metodo operator();<br />
2. operator() abbia un solo argomento;<br />
3. il tipo dell'argomento di operator() coincida con il tipo dell'elemento<br />
puntato dal tipo <strong>it</strong>eratore che sost<strong>it</strong>uisce il parametro Iter.<br />
Inoltre, notare che:<br />
• for_each r<strong>it</strong>orna lo stesso oggetto-funzione, per permettere al<br />
chiamante di accedere alle eventuali altre informazioni memorizzate nei<br />
suoi membri;<br />
• il terzo argomento può anche essere una normale funzione, nel qual<br />
caso il valore di r<strong>it</strong>orno di for_each non ha significato.<br />
Un "predicato" è un oggetto-funzione che r<strong>it</strong>orna un valore di tipo bool. Gli<br />
algor<strong>it</strong>mi fanno molto uso dei predicati, il cui comp<strong>it</strong>o è spesso di definire cr<strong>it</strong>eri<br />
d'ordine alternativi a operator
specializzazione della prima, dove il predicato è:<br />
elemento == valore<br />
A volte le due versioni hanno lo stesso nome e a volte no. Hanno lo stesso nome<br />
solo quando il numero degli argomenti è diverso e quindi la risoluzione<br />
dell'overload non può generare ambigu<strong>it</strong>à (non dimentichiamo che i tipi degli<br />
argomenti sono parametri di template e quindi potrebbero esserci delle<br />
specializzazioni con i rispettivi tipi coincidenti, generando ambigu<strong>it</strong>à nel caso<br />
che il numero degli argomenti sia uguale). Quando le due versioni non hanno lo<br />
stesso nome, quella con predicato prende il nome dell'altra segu<strong>it</strong>o dal suffisso<br />
_if<br />
Nell'esposizione che segue useremo le seguenti convenzioni:<br />
• siccome tutti gli algor<strong>it</strong>mi sono funzioni template, ometteremo il<br />
prefisso (sempre presente):<br />
template <br />
nella definizione di ogni algor<strong>it</strong>mo; per capire quali siano i suoi<br />
parametri, indicheremo i loro nomi con il colore viola, e in particolare:<br />
o Iter, Iter1, Iter2 saranno parametri di tipi <strong>it</strong>eratori;<br />
o T sarà il parametro del tipo degli elementi;<br />
o Pred sarà il parametro di un tipo predicato<br />
• nella descrizione di ogni algor<strong>it</strong>mo adotteremo la notazione della<br />
sequenza semi-aperta:<br />
[primo estremo, secondo estremo)<br />
e useremo le operazioni ar<strong>it</strong>metiche + e - sugli <strong>it</strong>eratori (lo faremo per<br />
comod<strong>it</strong>à di esposizione, anche se sappiamo che tali operazioni sono<br />
applicabili solo alla categoria degli <strong>it</strong>eratori ad accesso casuale, che non<br />
sono in genere quelli utilizzati dagli algor<strong>it</strong>mi)<br />
Gli algor<strong>it</strong>mi della "famiglia" find scorrono una sequenza, o una coppia di<br />
sequenze, cercando un valore che verifichi una determinata condizione:<br />
Iter find(Iter first, Iter last, const T& val)<br />
Iter find_if(Iter first, Iter last, Pred pr)<br />
cerca il primo valore di un <strong>it</strong>eratore <strong>it</strong> nel range [first, last) tale che risulti true:<br />
*<strong>it</strong> == val nel primo caso e ...<br />
pr(*<strong>it</strong>) nel secondo caso;<br />
r<strong>it</strong>orna <strong>it</strong> se lo trova, oppure last se non lo trova.<br />
Iter find_first_of(Iter1 first1, Iter1 last1, Iter2 first2, Iter2 last2)<br />
Iter find_first_of(Iter1 first1, Iter1 last1, Iter2 first2, Iter2 last2, Pred pr)<br />
cerca il primo valore di un <strong>it</strong>eratore <strong>it</strong>1 nel range [first1, last1) tale che risulti true:<br />
*<strong>it</strong>1 == *<strong>it</strong>2 nel primo caso e ...<br />
pr(*<strong>it</strong>1, *<strong>it</strong>2) nel secondo caso<br />
dove <strong>it</strong>2 è un qualunque valore di un <strong>it</strong>eratore nel range [first2, last2);<br />
r<strong>it</strong>orna <strong>it</strong>1 se lo trova, oppure last1 se non lo trova.<br />
Iter adjacent_find(Iter first, Iter last)<br />
Iter adjacent_find(Iter first, Iter last, Pred pr)<br />
cerca il primo valore di un <strong>it</strong>eratore <strong>it</strong> nel range [first, last-1) tale che risulti true:<br />
*<strong>it</strong> == *(<strong>it</strong>+1) nel primo caso e ...<br />
pr(*<strong>it</strong>, *(<strong>it</strong>+1)) nel secondo caso;<br />
r<strong>it</strong>orna <strong>it</strong> se lo trova, oppure last se non lo trova.
Gli algor<strong>it</strong>mi count e count_if contano le occorrenze di un valore in una<br />
sequenza:<br />
unsigned int count(Iter first, Iter last, const T& val)<br />
unsigned int count_if(Iter first, Iter last, Pred pr)<br />
incrementa un contatore n (inizialmente zero) per ogni valore di un <strong>it</strong>eratore <strong>it</strong> nel<br />
range [first, last) tale che risulti true:<br />
*<strong>it</strong> == val nel primo caso e ...<br />
pr(*<strong>it</strong>) nel secondo caso;<br />
r<strong>it</strong>orna n.<br />
Gli algor<strong>it</strong>mi equal e mismatch confrontano due sequenze:<br />
bool equal(Iter1 first1, Iter1 last1, Iter2 first2)<br />
bool equal(Iter1 first1, Iter1 last1, Iter2 first2, Pred pr)<br />
r<strong>it</strong>orna true solo se, per ogni valore dell'intero N nel range [0, last1-first1) risulta<br />
true:<br />
*(first1+N) == *(first2+N) nel primo caso e ...<br />
pr(*(first1+N), *(first2+N)) nel secondo caso.<br />
pair mismatch(Iter1 first1, Iter1 last1, Iter2 first2)<br />
pair mismatch(Iter1 first1, Iter1 last1, Iter2 first2, Pred pr)<br />
cerca il più piccolo valore dell'intero N nel range [0, last1-first1) tale che risulti<br />
false:<br />
*(first1+N) == *(first2+N) nel primo caso e ...<br />
pr(*(first1+N), *(first2+N)) nel secondo caso;<br />
se non lo trova pone N = last1-first1;<br />
r<strong>it</strong>orna pair(first1+N,first2+N).<br />
NOTA<br />
la seconda sequenza è specificata solo dal primo estremo: ciò significa che il numero<br />
dei suoi elementi deve essere almeno uguale al numero degli elementi della prima<br />
sequenza; questa tecnica è usata in tutti gli algor<strong>it</strong>mi in cui si utlizzano due sequenze<br />
con operazioni che coinvolgono le coppie degli elementi corrispondenti.
Una classe <strong>C++</strong> per le stringhe<br />
La classe string<br />
La Libreria Standard del <strong>C++</strong> mette a disposizione una classe per la gestione<br />
delle stringhe, non come array di caratteri (come le stringhe del C), ma come<br />
normali oggetti (e quindi, per esempio, trasferibili per copia, a differenza delle<br />
stringhe del C, nelle chiamate delle funzioni). Questa classe si chiama<br />
string ed è defin<strong>it</strong>a nell'header file .<br />
Per la ver<strong>it</strong>à, il nome string non è altro che un sinonimo (defin<strong>it</strong>o con<br />
typedef) di:<br />
basic_string<br />
dove basic_string è una classe template con tipo di carattere generico, e<br />
quindi string è una specializzazione di basic_string con argomento char.<br />
Ma poiché, come abbiamo già detto nel cap<strong>it</strong>olo di introduzione alla Libreria, a<br />
noi interessano solo i caratteri di tipo char, ignoreremo la classe template da<br />
cui string proviene e tratteremo string come una classe specifica (non<br />
template).<br />
Da un altro punto di vista, più vicino agli interessi dell'utente, string può essere<br />
considerata come un "conten<strong>it</strong>ore specializzato", e in particolare "somiglia"<br />
molto a vector. Possiede quasi tutte le funzional<strong>it</strong>à di vector, con alcune<br />
(poche) caratteristiche in meno e altre (molte) caratteristiche in più; quest'ultime<br />
servono soprattutto per eseguire le operazioni specifiche di manipolazione delle<br />
stringhe (come per esempio la concatenazione).<br />
In particolare, come gli elementi di vector, anche i caratteri di string possono<br />
essere considerati come facenti parte di una sequenza, e quindi string definisce<br />
gli stessi <strong>it</strong>eratori di vector e della stessa categoria (ad accesso casuale).<br />
Ciò rende possibile l'applicazione di tutti gli algor<strong>it</strong>mi generici della STL anche a<br />
string, tram<strong>it</strong>e i suoi <strong>it</strong>eratori. Questo fatto è indubbiamente un vantaggio, ma<br />
non così grande come potrebbe sembrare. Infatti gli algor<strong>it</strong>mi generici sono<br />
pensati principalmente per strutture i cui elementi sono significativi anche se<br />
presi singolarmente, il che non è generalmente vero per le stringhe. Per<br />
esempio, ordinare una stringa non ha senso (e quindi gli algor<strong>it</strong>mi di<br />
ordinamento o di manipolazione di sequenze ordinate sono poco utili se<br />
applicati alle stringhe). L'attenzione maggiore va invece concentrata sui metodi<br />
di string, alcuni dei quali sono implementati in modo da ottenere<br />
un'ottimizzazione più spinta di quanto non sia possibile nel caso generale.<br />
Confronto fra string e vector
In questa sezione elencheremo le funzional<strong>it</strong>à comuni a string e vector, e, separatamente, i<br />
metodi di vector non presenti in string. Nelle sezioni successive tratteremo esclusivamente<br />
delle funzioni-membro e delle funzioni esterne specifiche di string. Per il significato dei<br />
nomi, e per la descrizione dei tipi e delle funzioni, vedere il cap<strong>it</strong>olo: La Standard<br />
Template Library, sezione: Conten<strong>it</strong>ori Standard.<br />
Tipi defin<strong>it</strong>i in string<br />
Nell'amb<strong>it</strong>o della classe string sono defin<strong>it</strong>i gli stessi tipi defin<strong>it</strong>i in vector e<br />
in particolare (c<strong>it</strong>iamo i più importanti): <strong>it</strong>erator, const_<strong>it</strong>erator,<br />
reverse_<strong>it</strong>erator, const_reverse_<strong>it</strong>erator, difference_type, value_type,<br />
size_type, reference, const_reference<br />
Funzioni-membro comuni<br />
I seguenti metodi, già descr<strong>it</strong>ti nella trattazione dei conten<strong>it</strong>ori, sono defin<strong>it</strong>i<br />
sia in vector che in string, hanno la stessa sintassi di chiamata e svolgono le<br />
medesime operazioni (se vector è specializzato con con argomento char):<br />
Note:<br />
dereferenziazione di un <strong>it</strong>eratore<br />
begin end rbegin rend<br />
resize<br />
size empty<br />
max_size<br />
reserve capac<strong>it</strong>y<br />
costruttore di default<br />
costruttore di copia operator= assign<br />
costruttore e assign tram<strong>it</strong>e <strong>it</strong>eratori<br />
swap<br />
operator[] (accesso non controllato)<br />
at (accesso controllato)<br />
insert erase<br />
• come gli oggetti di vector, anche quelli di string possono utilizzare i<br />
metodi operator[] e at per accedere ai propri elementi (i singoli<br />
caratteri) tram<strong>it</strong>e indice;<br />
• c'è una piccola differenza fra i due metodi assign di vector e quelli di<br />
string: i primi r<strong>it</strong>ornano void, mentre i secondi r<strong>it</strong>ornano string&;<br />
• a propos<strong>it</strong>o del metodo size, è defin<strong>it</strong>o in string anche il metodo<br />
length, che fa esattamente la stessa cosa.<br />
Funzioni esterne comuni
Tutte le funzioni esterne di "appoggio" defin<strong>it</strong>e nell'header file <br />
sono anche defin<strong>it</strong>e nell'header file ; ricordiamo che queste funzioni<br />
sono: swap, operator==, operator!=, operator=. Ognuna di esse ha due argomenti, che nelle<br />
funzioni defin<strong>it</strong>e nell'header file sono ovviamente di tipo string.<br />
Funzioni-membro di vector non presenti in string<br />
Un numero molto ridotto di metodi di vector non è ridefin<strong>it</strong>o in string:<br />
• Costruttore con un 1 argomento<br />
Non è ammesso inizializzare una stringa fornendole solo la<br />
dimensione. Per esempio:<br />
string str(7); è un'istruzione errata;<br />
invece è possibile inizializzare una stringa fornendole la dimensione e<br />
il carattere di "riempimento". Per esempio:<br />
string str(7,'a'); ok, genera: "aaaaaaa";<br />
in pratica il secondo argomento, che in vector è opzionale, in string è<br />
obbligatorio<br />
• Operazioni in testa e in coda<br />
i seguenti metodi di vector non esistono in string: front,<br />
back, push_back, pop_back<br />
• Metodo clear<br />
in compenso esiste un ulteriore overload del metodo erase che esegue<br />
la stessa operazione<br />
Il membro statico npos<br />
La classe string dichiara il seguente dato-membro "atipico":<br />
static const size_type npos;<br />
che è inizializzato con il valore -1. Poiché d'altra parte il tipo size_type è<br />
sempre unsigned, la costante string::npos contiene in realtà il massimo<br />
numero pos<strong>it</strong>ivo possibile. Viene usato come argomento di default di alcune<br />
funzioni-membro o come valore di r<strong>it</strong>orno "speciale" (per esempio per<br />
indicare che un certo elemento non è stato trovato). In pratica npos<br />
rappresenta un indice che "non può esistere", in quanto è maggiore di tutti gli<br />
indici possibili. In un certo senso svolge le stesse funzioni del terminatore nelle<br />
stringhe del C, che non esiste negli oggetti di string (il carattere '\0' può<br />
essere un elemento di string come tutti gli altri).<br />
Come vedremo, i metodi di string che utilizzano gli indici come argomenti<br />
spesso fanno uso di npos per indicare la fine della stringa.
Costruttori e operazioni di copia<br />
Oltre ai 4 costruttori già visti (default, copia da oggetto string, copia<br />
tram<strong>it</strong>e <strong>it</strong>eratori e inizializzazione con carattere di "riempimento"), string<br />
definisce i seguenti costruttori specifici:<br />
string::string(const string& str, size_type ind, size_type n=npos)<br />
copia da str, a partire dall'elemento con indice ind, per n elementi o fino al termine<br />
di str (quello che "arriva prima")<br />
string::string(const char* s)<br />
copia caratteri, a partire da quello puntato da s e fino a quando incontra il carattere<br />
'\0' (escluso); in pratica copia una stringa del C (che può anche essere una costante<br />
l<strong>it</strong>eral)<br />
string::string(const char* s, size_type n)<br />
come sopra, salvo che copia solo n caratteri (se prima non incontra '\0')<br />
Per quello che riguarda le copie in oggetti di string già esistenti, oltre<br />
all'operatore di assegnazione standard e alle due versioni del metodo assign<br />
(copia tram<strong>it</strong>e <strong>it</strong>eratori e copia con carattere di "riempimento") presenti anche<br />
in vector, string definisce ulteriori overloads (in tutte le seguenti operazioni<br />
l'oggetto esistente viene cancellato e sost<strong>it</strong>u<strong>it</strong>o da quello ottenuto per copia):<br />
string& string::operator=(const char* s)<br />
copia una stringa del C<br />
string& string::operator=(char c)<br />
copia un singolo carattere; nota: l'assegnazione di un singolo carattere è ammessa,<br />
mentre l'inizializzazione non lo è<br />
string& string::assign(const string& str)<br />
esegue le stesse operazioni dell'operatore di assegnazione standard<br />
string& string::assign(const string& str, size_type ind, size_type n)<br />
esegue le stesse operazioni del costruttore con uguali argomenti<br />
string& string::assign(const char* s)<br />
esegue le stesse operazioni dell'operatore di assegnazione con uguale argomento<br />
string& string::assign(const char* s, size_type n)<br />
esegue le stesse operazioni del costruttore con uguali argomenti
Gestione degli errori<br />
Abbiamo detto che, come in vector, operator[] non controlla che l'argomento<br />
indice sia compreso nel range [0,size()), mentre il metodo at effettua il<br />
controllo e genera un'eccezione di tipo out_of_range in caso di errore.<br />
Molti altri metodi di string hanno, fra gli argomenti, due tipi size_type<br />
consecutivi, di cui il primo rappresenta un indice (che ha il significato di<br />
"posizione iniziale"), mentre il secondo rappresenta il numero di caratteri "da<br />
quel punto in poi" (abbiamo già visto così fatti un costruttore e un metodo<br />
assign). In tutti i casi il primo argomento è sempre controllato (generando la<br />
sol<strong>it</strong>a eccezione se l'indice non è nel range), mentre il secondo non lo è mai e<br />
quindi un numero di caratteri troppo alto viene semplicemente interpretato come<br />
"il resto della stringa" (che in particolare è l'unica interpretazione possibile se il<br />
valore del secondo argomento è npos). Notare che, se la "posizione iniziale" e/o<br />
il numero di caratteri sono dati come numeri negativi, questi vengono convert<strong>it</strong>i<br />
in valori pos<strong>it</strong>ivi molto grandi (essendo size_type un tipo unsigned), e quindi,<br />
per esempio:<br />
string(str,-<br />
2,3);<br />
string(str,3,-<br />
2);<br />
genera out_of_range<br />
va bene: costruisce un oggetto string per copia da str, a<br />
partire dal quarto carattere fino al termine<br />
I metodi per la ricerca di sotto-stringhe (che vedremo più avanti) rest<strong>it</strong>uiscono<br />
npos in caso di insuccesso, ma non generano eccezioni; se però il programma<br />
dell'utente non controlla il valore di r<strong>it</strong>orno e lo usa direttamente come<br />
argomento di "posizione" nella chiamata di un'altra funzione, allora sì che, in<br />
caso di insuccesso nella ricerca, si genera un'eccezione out_of_range.<br />
I metodi che usano una coppia di <strong>it</strong>eratori al posto della coppia "posizionenumero"<br />
non effettuano nessun controllo (e lo stesso discorso vale per gli<br />
algor<strong>it</strong>mi, come sappiamo) e quindi spetta al programma dell'utente assicurare<br />
che i lim<strong>it</strong>i del range non vengano oltrepassati.<br />
La stessa cosa dicasi quando la coppia di argomenti "posizione-numero" si<br />
riferisce a una stringa del C: anche qui non viene esegu<strong>it</strong>o nessun controllo (a<br />
parte il controllo sul terminator che viene riconosciuto come fine della stringa)<br />
e quindi bisogna porre la massima attenzione sull'argomento che rappresenta la<br />
"posizione iniziale" (che in questo caso è un puntatore a char): anz<strong>it</strong>utto deve<br />
essere diverso da NULL (altrimenti il programma abortisce) e in secondo luogo<br />
deve realmente puntare a un carattere interno alla stringa.<br />
Un altro tipo di errore (comune anche a vector), molto raro, che genera<br />
un'eccezione di tipo length_error, avviene quando si tenta di costruire una<br />
stringa più lunga del massimo consent<strong>it</strong>o (dato da max_size). Lo stesso errore<br />
è generato se si tenta di superare max_size chiamando un metodo che modifica<br />
la dimensione direttamente (resize) o implic<strong>it</strong>amente (insert, append,<br />
replace, operator+=), oppure che modifica la capac<strong>it</strong>à (reserve).
Conversioni fra oggetti string e stringhe del C<br />
La conversione da una stringa del C (che indichiamo con s) a un oggetto string<br />
(che indichiamo con str) si ottiene semplicemente assegnando s a str (con<br />
operator= o con il metodo assign), oppure costruendo str per copia da s.<br />
Ovviamente, se si vuole eseguire la conversione inversa, da oggetto string a<br />
stringa del C, non si può semplicemente invertire gli operandi<br />
nell'assegnazione, in quanto il tipo nativo char* non consente assegnazioni<br />
da oggetti string. Bisogna invece ricorrere ad alcuni metodi defin<strong>it</strong>i nella<br />
stessa classe string. Questi metodi sono 3 e precisamente:<br />
1. const char* string::data() const<br />
scrive i caratteri di *this in un array di cui rest<strong>it</strong>uisce il puntatore.<br />
L'array è gest<strong>it</strong>o internamente a string e perciò non va preallocato nè<br />
cancellato. L'oggetto *this non può essere modificato, nel senso che una<br />
sua successiva modifica invalida l'array, nè possono essere modificati i<br />
caratteri dello stesso array (in pratica il metodo data può operare solo<br />
su oggetti costanti). Non viene aggiunto il terminator alla fine<br />
dell'array e quindi non è possibile utilizzare l'array come argomento<br />
nelle funzioni che operano sulle stringhe (in sostanza è proprio un array<br />
di caratteri , non una stringa!)<br />
2. const char* string::c_str() const<br />
è identico a data, salvo il fatto che aggiunge il terminator alla fine,<br />
creando così un array di caratteri null terminated, cioè una "vera"<br />
stringa del C<br />
3. size_type copy(char* s, size_type n, size_type pos = 0) const<br />
copia n caratteri di *this, a partire dal carattere con indice pos,<br />
nell'array s, preallocato dal chiamante. Rest<strong>it</strong>uisce il numero di caratteri<br />
effettivamente copiati. Non aggiunge il terminator alla fine dell'array.<br />
Per copiare tutti i caratteri di *this si può usare string::npos come<br />
secondo argomento e omettere il terzo.<br />
Da un esame cr<strong>it</strong>ico dei tre metodi soprac<strong>it</strong>ati, si può osservare che:<br />
1. data è "quasi" inutilizzabile (può servire solo quando si trattano array di<br />
caratteri e non stringhe)<br />
2. c_str è invece molto utile, perchè permette di inserire il suo valore di<br />
r<strong>it</strong>orno come argomento nelle funzioni di Libreria del C che operano<br />
sulle stringhe. Per esempio:<br />
int m = atoi(str.c_str());<br />
(nota: non esistono funzioni <strong>C++</strong> che convertono stringhe di caratteri<br />
decimali in numeri).<br />
Tuttavia può operare solo su oggetti costanti
3. copy ha il vantaggio di permettere la modifica dell'array copiato. Bisogna<br />
però ricordarsi di aggiungere un carattere '\0' in fondo (e bisogna anche<br />
ev<strong>it</strong>are che lo stesso carattere sia presente all'interno della stringa da<br />
copiare)<br />
Confronti fra stringhe<br />
Per confrontare due oggetti string, o un oggetto string e una stringa del C,<br />
la classe string fornisce il metodo compare, con vari overloads. Il valore di<br />
r<strong>it</strong>orno è sempre di tipo int ed ha il seguente significato:<br />
• 0, se le due stringhe sono identiche;<br />
• un numero negativo se *this precede lessicograficamente la stringaargomento;<br />
• un numero pos<strong>it</strong>vo se *this segue lessicograficamente la stringaargomento.<br />
Rispetto agli operatori relazionali, il metodo compare ha quindi il vantaggio<br />
di rest<strong>it</strong>uire il risultato di con una sola chiamata. I suoi overloads<br />
sono:<br />
• int compare(const string& str) const<br />
confronta *this con l'oggetto string str<br />
• int compare(const char* s) const<br />
confronta *this con la stringa del C s<br />
• int compare(size_type ind, size_type n, const string& str) const<br />
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"<br />
ind-n, con l'oggetto string str<br />
• int compare(size_type ind, size_type n, const string& str,<br />
size_type ind1, size_type n1) const<br />
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"<br />
ind-n, con la sotto-stringa dell'oggetto string str, data dalla coppia<br />
"posizione-numero" ind1-n1<br />
• int compare(size_type ind, size_type n, const char* s, size_type<br />
n1=npos) const<br />
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"<br />
ind-n, con i primi n1 caratteri della stringa del C s<br />
L'utente non può fornire un cr<strong>it</strong>erio di confronto specifico; se lo vuol fare, non<br />
deve usare compare, ma l'algor<strong>it</strong>mo lexicographical_compare con un<br />
predicato. Per esempio:<br />
lexicographical_compare(s1.begin(),s1.end(),s2.begin(),s2.end(),nocas<br />
e);
est<strong>it</strong>uisce true se la stringa s1 precede la stringa s2 in base al cr<strong>it</strong>erio di<br />
confronto dato dalla funzione nocase (forn<strong>it</strong>a dall'utente).<br />
Nell'header-file si trovano varie funzioni esterne di "appoggio" che<br />
implementano diversi overloads degli operatori relazionali: =; per ognuno di essi esistono tre versioni: quella presente anche in<br />
e negli header-files degli altri conten<strong>it</strong>ori, in cui entrambi gli<br />
operandi sono della stessa classe conten<strong>it</strong>ore (in questo caso string) e quelle<br />
in cui rispettivamente il primo o il secondo operando è di tipo const char* (cioè<br />
una stringa del C). Questo permette di confrontare indifferentemente due<br />
oggetti string, o un oggetto string e una stringa del C, o una stringa del C<br />
e un oggetto string. In particolare la stringa del C può essere una costante<br />
l<strong>it</strong>eral. Esempio:<br />
if (str == "Hello") .....<br />
Concatenazioni e inserimenti<br />
Concatenare due stringhe significa scrivere le due stringhe l'una di segu<strong>it</strong>o<br />
all'altra in una terza stringa.<br />
Nell'header-file si trovano varie funzioni esterne di "appoggio" che<br />
implementano diversi overloads dell'operatore +, il quale fornisce la stringa<br />
concatenata, date due stringhe come operandi; di queste, una è sempre di<br />
tipo const string&, mentre l'altra può essere ancora di tipo const string&,<br />
oppure di tipo const char* (cioè una stringa del C), oppure di tipo char (cioè<br />
un singolo carattere). Mantenendo la convenzione simbolica che abbiamo usato<br />
finora, r<strong>it</strong>eniamo a questo punto che la descrizione delle funzioni possa essere<br />
omessa (quando è autoesplicativa già in base ai tipi e ai nomi convenzionali degli<br />
argomenti):<br />
• string operator+(const string& str1, const string& str2)<br />
• string operator+(const string& str, const char* s)<br />
• string operator+(const char* s, const string& str)<br />
• string operator+(const string& str, char c)<br />
• string operator+(char c, const string& str)<br />
Per l'operazione di somma e assegnazione in notazione compatta, sono<br />
disponibili tre metodi che implementano altrettanti overloads dell'operatore<br />
+=. In questo caso la stringa concatenata è la stessa di partenza (*this) a cui<br />
viene aggiunta in coda la stringa-argomento:<br />
• string& string::operator+=(const string& str)<br />
• string& string::operator+=(const char* s)<br />
• string& string::operator+=(char c)
Il metodo append esegue la stessa operazione di operator+=, con il<br />
vantaggio che gli argomenti possono essere più di uno. Ne sono forn<strong>it</strong>i vari<br />
overloads:<br />
• string& string::append(const string& str)<br />
• string& string::append(const string& str, size_type ind, size_type<br />
n)<br />
• string& string::append(const char* s)<br />
• string& string::append(const char* s, size_type n)<br />
• string& string::append(size_type n, char c)<br />
appende n volte il carattere c<br />
• string& string::append(Iter first, Iter last)<br />
Per quello che riguarda l'inserimento di caratteri "in mezzo" a una stringa<br />
(operazione di bassa efficienza, come in vector), sono disponibili ulteriori<br />
overloads del metodo insert (oltre a quelli comuni con vector); tutti<br />
inseriscono caratteri prima dell'elemento di *this con indice pos e<br />
rest<strong>it</strong>uiscono by reference lo stesso *this:<br />
• string& string::insert(size_type pos, const string& str)<br />
• string& string::insert(size_type pos, const string& str, size_type<br />
ind, size_type n)<br />
• string& string::insert(size_type pos, const char* s)<br />
• string& string::insert(size_type pos, const char* s, size_type n)<br />
• string& string::insert(size_type pos, size_type n, char c)<br />
inserisce n volte il carattere c<br />
Ricerca di sotto-stringhe<br />
Nella classe string sono defin<strong>it</strong>i molti metodi che ricercano la stringaargomento<br />
come sotto-stringa di *this. Tutti rest<strong>it</strong>uiscono un valore di tipo<br />
size_type, che, se la sotto-stringa è trovata, rappresenta l'indice del suo<br />
primo carattere; se invece la ricerca fallisce il valore rest<strong>it</strong>u<strong>it</strong>o è npos. Tutti i<br />
metodi sono defin<strong>it</strong>i const in quanto eseguono la ricerca senza modificare<br />
l'oggetto.<br />
Nell'elenco che segue, suddiviso in vari gruppi, l'argomento di nome pos<br />
rappresenta l'indice dell'elemento di *this da cui iniziare la ricerca, mentre<br />
l'argomento di nome n rappresenta il numero di caratteri della stringaargomento<br />
da utilizzare per la ricerca.<br />
Cerca una sotto-stringa:<br />
• size_type string::find(const string& str, size_type pos=0) const<br />
• size_type string::find(const char* s, size_type pos=0) const
• size_type string::find(const char* s, size_type pos, size_type n)<br />
const<br />
• size_type string::find(char c, size_type pos=0) const<br />
Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:<br />
• size_type string::rfind(const string& str, size_type pos=npos)<br />
const<br />
• size_type string::rfind(const char* s, size_type pos=npos) const<br />
• size_type string::rfind(const char* s, size_type pos, size_type n)<br />
const<br />
• size_type string::rfind(char c, size_type pos=npos) const<br />
Cerca il primo carattere di *this che si trova nella stringa-argomento:<br />
• size_type string::find_first_of(const string& str, size_type pos=0)<br />
const<br />
• size_type string::find_first_of(const char* s, size_type pos=0)<br />
const<br />
• size_type string::find_first_of(const char* s, size_type pos,<br />
size_type n) const<br />
• size_type string::find_first_of(char c, size_type pos=0) const<br />
Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:<br />
• size_type string::find_last_of(const string& str, size_type<br />
pos=npos) const<br />
• size_type string::find_last_of(const char* s, size_type pos=npos)<br />
const<br />
• size_type string::find_last_of(const char* s, size_type pos,<br />
size_type n) const<br />
• size_type string::find_last_of(char c, size_type pos=npos) const<br />
Cerca il primo carattere di *this che non si trova nella stringa-argomento:<br />
• size_type string::find_first_not_of(const string& str, size_type<br />
pos=0) const<br />
• size_type string::find_first_not_of(const char* s, size_type pos=0)<br />
const<br />
• size_type string::find_first_not_of(const char* s, size_type pos,<br />
size_type n) const<br />
• size_type string::find_first_not_of(char c, size_type pos=0) const<br />
Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:<br />
• size_type string::find_last_not_of(const string& str, size_type<br />
pos=npos) const<br />
• size_type string::find_last_not_of(const char* s, size_type<br />
pos=npos) const<br />
• size_type string::find_last_not_of(const char* s, size_type pos,<br />
size_type n) const<br />
• size_type string::find_last_not_of(char c, size_type pos=npos)<br />
const
Estrazione e sost<strong>it</strong>uzione di sotto-stringhe<br />
Il metodo substr crea una stringa estraendola da *this e la rest<strong>it</strong>uisce per<br />
copia:<br />
string string::substr(size_type pos=0, size_type n=npos) const<br />
la stringa originaria non è modificata; la nuova stringa coincide con la sottostringa<br />
di *this che parte dall'elemento con indice pos e contiene n caratteri.<br />
Il metodo replace, defin<strong>it</strong>o con vari overloads, sot<strong>it</strong>uisce una sotto-stringa<br />
di *this con la stringa-argomento (o una sua sotto-stringa) e rest<strong>it</strong>uisce by<br />
reference lo stesso *this. Il numero dei nuovi caratteri non deve<br />
necessariamente coincidere con quello preesistente (la nuova sotto-stringa può<br />
essere più lunga o più corta di quella sost<strong>it</strong>u<strong>it</strong>a) e quindi il metodo replace, oltre<br />
a modificare l'oggetto, può anche modificarne la dimensione.<br />
Nell'elenco che segue, i nomi degli argomenti hanno il seguente significato:<br />
• pos : "posizione iniziale" in *this<br />
• m : "numero di caratteri" in *this<br />
• ind : "posizione iniziale" nella stringa-argomento<br />
• n : "numero di caratteri" nella stringa-argomento<br />
• ib,ie : <strong>it</strong>eratori che delim<strong>it</strong>ano la sotto-stringa in *this<br />
• n,c: carattere c ripetuto n volte<br />
Metodi che definiscono la sotto-stringa da sost<strong>it</strong>uire mediante la coppia<br />
"posizione-numero":<br />
• string& string::replace(size_type pos, size_type m, const string&<br />
str)<br />
• string& string::replace(size_type pos, size_type m, const string&<br />
str, size_type ind, size_type n)<br />
• string& string::replace(size_type pos, size_type m, const char* s)<br />
• string& string::replace(size_type pos, size_type m, const char* s,<br />
size_type n)<br />
• string& string::replace(size_type pos, size_type m, size_type n,<br />
char c)<br />
Metodi che definiscono la sotto-stringa da sost<strong>it</strong>uire mediante una coppia di<br />
<strong>it</strong>eratori:<br />
• string& string::replace(<strong>it</strong>erator ib, <strong>it</strong>erator ie, const string& str)<br />
• string& string::replace(<strong>it</strong>erator ib, <strong>it</strong>erator ie, const char* s)
• string& string::replace(<strong>it</strong>erator ib, <strong>it</strong>erator ie, const char* s,<br />
size_type n)<br />
• string& string::replace(<strong>it</strong>erator ib, <strong>it</strong>erator ie, size_type n, char c)<br />
• string& string::replace(<strong>it</strong>erator ib, <strong>it</strong>erator ie, Iter first, Iter last)<br />
Per cancellare una sotto-stringa è disponibile un ulteriore overload del<br />
metodo erase (oltre a quelli comuni con vector):<br />
string& string::erase(size_type pos=0, size_type n=npos)<br />
notare che la chiamata di erase senza argomenti equivale alla chiamata di<br />
clear in vector.<br />
Operazioni di input-output<br />
Nell'header-file si trovano due funzioni esterne di "appoggio" che<br />
implementano due ulteriori overloads degli operatori di flusso "" (estrazione), con right-operand di tipo string.<br />
Pertanto, la lettura e la scr<strong>it</strong>tura di un oggetto string si possono eseguire<br />
semplicemente utilizzando gli operatori di flusso come per le stringhe del C.<br />
In particolare la lettura "salta" (cioè non inserisce nella stringa) i caratteri<br />
bianchi e i caratteri speciali (che anzi usa come separatori fra una stringa e<br />
l'altra). I caratteri "buoni" vengono invece immessi nella stringa l'uno dopo<br />
l'altro a partire dalla "posizione" 0 e fino all'incontro di un separatore; la stringa<br />
letta sost<strong>it</strong>uisce quella memorizzata precedentemente, assumendo (in più o in<br />
meno) anche una nuova dimensione.<br />
Per la lettura di una stringa che includa anche i caratteri bianchi e i caratteri<br />
speciali, in è defin<strong>it</strong>a anche la funzione getline:<br />
istream& getline(istream&, string& str, char eol='\n')<br />
che estrae caratteri dal flusso di input e li memorizza in str; l'estrazione<br />
termina quando è incontrato il carattere eol, che viene rimosso dal flusso di<br />
input ma non inser<strong>it</strong>o in str. Omettendo il terzo argomento si ottiene<br />
effettivamente la lettura di una intera "linea" di testo.<br />
Il valore di r<strong>it</strong>ono, di tipo riferimento a istream, permette di utilizzare la<br />
chiamata di getline come left-operand di una o più operazioni di<br />
estrazione. Esempio:<br />
getline(cin,str1,'\t') >> str2 >> str3 ;<br />
legge tutti i caratteri fino al primo tabulatore (escluso), memorizzandoli in<br />
str1, e poi legge due sequenze di caratteri delim<strong>it</strong>ate da separatori e li<br />
memorizza in str2 e str3 .
Librerie statiche e dinamiche in Linux<br />
Introduzione<br />
Un problema che si presenta comunemente nello sviluppo dei programmi è che<br />
questi tendono a diventare sempre più complessi, il tempo richiesto per la loro<br />
compilazione cresce di conseguenza, e la directory di lavoro è sempre più<br />
affollata. E' proprio in questa fase che incominciamo a chiederci se non esista un<br />
modo più efficiente per organizzare i nostri progetti. Una possibil<strong>it</strong>à che ci viene<br />
offerta dai compilatori sono le librerie.<br />
Librerie in ambiente Linux<br />
Una libreria è semplicemente un file contenente codice compilato che può essere<br />
successivamente incorporato come una unica ent<strong>it</strong>à in un nostro programma in<br />
fase di linking; l'utilizzo delle librerie ci permettere di realizzare programmi più<br />
facili da compilare e mantenere. Di norma le librerie sono indicizzate, così risulta<br />
più facile localizzare simboli (funzioni, variabili, classi, etc...) al loro interno. Per<br />
questa ragione il link ad una libreria è più veloce rispetto al caso in cui i moduli<br />
oggetto siano separati nel disco. Inoltre, quando usiamo una libreria abbiamo<br />
meno files da aprire e controllare, e questo comporta un ulteriore aumento della<br />
veloc<strong>it</strong>à del processo di link.<br />
Nell'ambiente Linux (come nella maggior parte dei sistemi moderni) le librerie si<br />
suddividono in due famiglie principali:<br />
• librerie statiche (static libraries)<br />
• librerie dinamiche o condivise (shared libraries)<br />
Ognuna presenta vantaggi e svantaggi, ma tutte hanno una cosa in comune:<br />
cost<strong>it</strong>uiscono un catalogo di funzioni, classi, etc..., che ogni programmatore può<br />
riutilizzare.<br />
Un programma di prova<br />
Prima di vedere come si costruiscono e si usano questi due tipi di librerie,<br />
presentiamo un piccolo programma di prova che ci servirà da esempio.<br />
Il programma comprende una collezione di funzioni matematiche (myfuncs) ed<br />
un gestore di errori (la classe ErrMsg):<br />
• main.cpp<br />
• myfuncs.h<br />
• myfuncs.cpp<br />
• errmsg.h<br />
• errmsg.cpp
Le funzioni ''div'' e ''log'' in sostanza ridefiniscono le operazioni di divisione e il<br />
logar<strong>it</strong>mo decimale ma in aggiunta permettono una gestione delle eccezioni<br />
tram<strong>it</strong>e il meccanismo di throw-catch.<br />
Il programma può essere compilato in maniera ''convenzionale'' tram<strong>it</strong>e<br />
l'istruzione:<br />
g++ -o prova main.cpp myfuncs.cpp<br />
errmsg.cpp<br />
L'eseguibile prova si aspetta sulla linea di comando due numeri e calcola in<br />
sequenza il loro rapporto ed il logar<strong>it</strong>mo del primo:<br />
./prova 10 3<br />
3.33333<br />
1<br />
Queste operazioni vengono esegu<strong>it</strong>e nel main del programma in un blocco try; se si verifica<br />
una eccezione (nella fattispecie una divisione per zero o il logar<strong>it</strong>mo di un numero negativo) il<br />
blocco catch invoca la funzione membro ErrMsg.print_message() ed il programma termina<br />
con un messaggio di errore:<br />
./prova -10 3<br />
-3.33333<br />
**Severe Error in "double log(double)":Invalid<br />
argument.<br />
Qu<strong>it</strong>ting now.<br />
Librerie statiche<br />
Le librerie statiche vengono installate nell'eseguibile del programma prima che<br />
questo possa essere lanciato. Esse sono semplicemente cataloghi di moduli<br />
oggetto che sono stati collezionati in un unico file conten<strong>it</strong>ore. Le librerie statiche<br />
ci permettono di effettuare dei link di programmi senza dover ricompilare il loro<br />
codice sorgente. Per far girare il nostro programma abbiamo bisogno solo del suo<br />
file eseguibile.<br />
Come costruire una libreria statica<br />
Per costruire una libreria statica bisogna partire dai moduli oggetto dei nostri<br />
sorgenti.<br />
g++ -c myfuncs.cpp errmsg.gcc
Una volta compilati i moduli myfuncs.o e errmsg.o, costruiamo la libreria statica<br />
libmath_util.a con il programma di archiviazione ar:<br />
ar r libmath_util.a myfuncs.o errmsg.o<br />
Il comando ar invocato con la flag ''r'' crea la libreria (se ancora non esiste) e vi<br />
inserisce (eventualmente rimpiazzandoli) i moduli oggetto. Nel scegliere il nome di<br />
una libreria statica è stata utilizzata la seguente convenzione: il nome del file della<br />
libreria inizia con il prefisso ''lib'' e termina con il suffisso ".a".<br />
Per verificare il contenuto della libreria possiamo usare<br />
ar tv libmath_util.a<br />
rw-r--r-- 223/100 18256 Dec 10 14:24 2003 errmsg.o<br />
rw-r--r-- 223/100 23476 Dec 10 14:23 2003 myfuncs.o<br />
Link con una libreria statica<br />
Una volta creato il nostro archivio, vogliamo utilizzarlo in un programma. Per poter<br />
effettuare il link ad una libreria statica, il compilatore g++ deve essere utilizzato in<br />
questo modo:<br />
g++ -o prova_s main.cpp -L. -lmath_util<br />
Dove abbiamo chiamato l'eseguibile prova_s per ricordarci che è stato ottenuto<br />
tram<strong>it</strong>e il link alla libreria statica. Notate che abbiamo omesso il prefisso ''lib'' e il<br />
suffisso ''.a'' quando abbiamo immesso il nome della libreria nella linea di<br />
comando con la flag "-l". Ci pensa il linker ad attaccare queste parti alla fine e<br />
all'inizio del nome di libreria. Notate inoltre l'uso della flag ''-L.'' che dice al<br />
compilatore di cercare la libreria anche nella directory in uso e non solo nelle<br />
directory standard dove risiedono le librerie di sistema (per es. /usr/lib/).<br />
Il processo di link inizia con il caricamento del modulo main.o in cui viene defin<strong>it</strong>a<br />
la funzione main(). A questo punto il linker si accorge della presenza dei nomi di<br />
funzioni div e log e della classe ErrMsg, utilizzate dalla funzione main() ma non<br />
defin<strong>it</strong>e. Siccome viene forn<strong>it</strong>o al linker il nome della libreria libmath_util.a,<br />
viene fatta una ricerca nei moduli all'interno di questa libreria per cercare quelli in<br />
cui sono defin<strong>it</strong>e queste ent<strong>it</strong>à. Una volta localizzati, questi moduli vengono<br />
estratti dalla libreria ed inclusi nell'eseguibile del programma.<br />
L'eseguibile prova_s contiene così tutto il codice necessario al suo funzionamento<br />
ed è pronto per essere lanciato.<br />
I lim<strong>it</strong>i del meccanismo del link statico
Si deve precisare che il linker estrae dalla libreria statica solo i moduli<br />
strettamente necessari alla compilazione del programma. Questo dimostra una<br />
certa capac<strong>it</strong>à di economizzare le risorse delle librerie. Pensiamo però a più<br />
programmi che utilizzano, magari per altri scopi, la stessa libreria statica. I<br />
programmi utilizzano la libreria statica distintamente, cioè ognuno ne possiede<br />
una copia. Se questi devono essere esegu<strong>it</strong>i contemporaneamente nello stesso<br />
sistema, i requis<strong>it</strong>i di memoria si moltiplicano di conseguenza solo per osp<strong>it</strong>are<br />
funzioni assolutamente identiche.<br />
Le librerie condivise forniscono un meccanismo che permette a una singola copia<br />
di un modulo di codice di essere condivisa tra diversi programmi nello stesso<br />
sistema operativo. Ciò permette di tenere solo una copia di una data libreria in<br />
memoria ad un certo istante.<br />
Librerie condivise<br />
Le librerie condivise (dette anche dinamiche) vengono collegate ad un programma<br />
in due passaggi. In un primo momento, durante la fase di compilazione (Compile<br />
Time), il linker verifica che tutti i simboli (funzioni, variabili, classi, e simili ...)<br />
richieste dal programma siano effettivamente collegate o al programma o ad una<br />
delle sue librerie condivise. In ogni caso i moduli oggetto della libreria<br />
dinamica non vengono inser<strong>it</strong>i direttamente nel file eseguibile. In un<br />
secondo momento, quando l'eseguibile viene lanciato (Run Time), un programma<br />
di sistema (dynamic loader) controlla quali librerie dinamiche sono state collegate<br />
al nostro programma, le carica in memoria, e le attacca alla copia del programma<br />
in memoria.<br />
La fase di caricamento dinamico rallenta leggermente il lancio del programma, ma<br />
si ottiene il notevole vantaggio che, se un secondo programma collegato alla<br />
stessa libreria condivisa viene lanciato, questo può utilizzare la stessa copia della<br />
libreria dinamica già in memoria, con un prezioso risparmio delle risorse del<br />
sistema. Per esempio, le librerie standard del C e del <strong>C++</strong> sono delle librerie<br />
condivise utilizzate da tutti i programmi C/<strong>C++</strong>.<br />
L'uso di librerie condivise ci permette quindi di utilizzare meno memoria per far<br />
girare i nostri programmi e di avere eseguibili molto più snelli, risparmiando così<br />
spazio disco.<br />
Come costruire una libreria condivisa<br />
La creazione di una libreria condivisa è molto simile alla creazione di una libreria<br />
statica. Si compila una lista di oggetti e li si colleziona in un unico file. Ci sono<br />
però due differenze importanti:<br />
1. Dobbiamo compilare per "Pos<strong>it</strong>ion Independent Code" (PIC). Visto che al<br />
momento della creazione dei moduli oggetto non sappiamo in quale<br />
posizione della memoria saranno inser<strong>it</strong>i nei programmi che li useranno,<br />
tutte le chiamate alle funzioni devono usare indirizzi relativi e non assoluti.
Per generare questo tipo di codice si passa al compilatore la flag "-fpic" o<br />
"-fPIC" nella fase di compilazione dei moduli oggetto.<br />
2. Contrariamente alle librerie statiche, quelle dinamiche non sono file di<br />
archivio. Una libreria condivisa ha un formato specifico che dipende<br />
dall'arch<strong>it</strong>ettura per la quale è stata creata. Per generarla di usa o il<br />
compilatore stesso con la flag "-shared" o il suo linker.<br />
Consideriamo ancora una volta il nostro programma di prova. I comandi per la<br />
creazione di una libreria condivisa possono presentarsi come segue:<br />
g++ -fPIC -c myfuncs.cpp<br />
g++ -fPIC -c errmsg.cpp<br />
g++ -shared -o libmath_util.so myfuncs.o errmsg.o<br />
Nel scegliere il nome di una libreria condivisa è stata utilizzata la convenzione<br />
secondo cui il nome del file della libreria inizia con il prefisso ''lib'' e termina con il<br />
suffisso ".so''.<br />
I primi due comandi compilano i moduli oggetto con l'opzione (fPIC) in maniera<br />
tale che essi siano utilizzabili per una libreria condivisa (possiamo comunque<br />
utilizzarli in un programma normale anche se sono stati compilati con PIC).<br />
L'ultimo comando chiede al compilatore di generare la libreria dinamica.<br />
Link con una libreria condivisa<br />
Come abbiamo già preannunciato l'uso di una libreria condivisa si articola in due<br />
momenti: Compile time e Run Time. La parte di compilazione e semplice. Il link ad<br />
una libreria condivisa avviene in maniera del tutto simile al caso di una libreria<br />
statica<br />
g++ -o prova_d main.cpp -L. -lmath_util<br />
Dove abbiamo chiamato l'eseguibile prova_d per ricordarci che è stato ottenuto<br />
tram<strong>it</strong>e il link alla libreria dinamica.<br />
Se però proviamo a lanciare l'eseguibile otteniamo una sgrad<strong>it</strong>a sorpresa:<br />
./prova_d -10 3<br />
./prova_d: error while loading shared libraries:<br />
libmath_util.so:<br />
cannot open shared object file: No such file or<br />
directory<br />
Il dynamic loader non è in grado di localizzare la nostra libreria!
Possiamo infatti usare il comando ldd per verificare le dipendenze delle librerie<br />
condivise e scoprire che la nostra libreria non viene localizzata dal loader<br />
dinamico:<br />
ldd ./prova_d<br />
libmath_util.so => not found<br />
libstdc++.so.5 =><br />
/usr/lib/libstdc++.so.5 (0x40030000)<br />
libm.so.6 => /lib/tls/libm.so.6<br />
(0x400e3000)<br />
libgcc_s.so.1 => /lib/libgcc_s.so.1<br />
(0x40106000)<br />
libc.so.6 => /lib/tls/libc.so.6<br />
(0x42000000)<br />
/lib/ld-linux.so.2 => /lib/ld-linux.so.2<br />
(0x40000000)<br />
Ciò avviene perché la nostra libreria non risiede in una directory standard.<br />
La variabile ambiente LD_LIBRARY_PATH<br />
Ci sono diversi modi per specificare la posizione delle librerie condivise<br />
nell'ambiente linux. Se avete i privilegi di root, una possibil<strong>it</strong>à è quella di<br />
aggiungere il path della nostra libreria al file /etc/ld.so.conf per poi lanciare<br />
/sbin/ldconfig . Ma se non avete l'accesso all'utente root, potete sfruttare la<br />
variabile ambiente LD_LIBRARY_PATH per dire al dynamic loader dove cercare<br />
la nostra libreria:<br />
setenv LD_LIBRARY_PATH<br />
/home/murgia/<strong>C++</strong>/<br />
ldd ./prova_d<br />
libmath_util.so =><br />
/home/murgia/<strong>C++</strong>/libmath_util.so<br />
(0x40017000)<br />
libstdc++.so.5 =><br />
/usr/lib/libstdc++.so.5 (0x40030000)<br />
libm.so.6 => /lib/tls/libm.so.6<br />
(0x400e3000)<br />
libgcc_s.so.1 => /lib/libgcc_s.so.1<br />
(0x40106000)<br />
libc.so.6 => /lib/tls/libc.so.6
(0x42000000)<br />
/lib/ld-linux.so.2 => /lib/ld-linux.so.2<br />
(0x40000000)<br />
In questo caso il programma ldd ci informa che ora il dynamic loader è in grado di<br />
localizzare libmath_util.so, ed il programma sarà esegu<strong>it</strong>o con successo.<br />
La flag -rpath<br />
Esiste anche la possibil<strong>it</strong>à di passare al linker la locazione della nostra librerie con<br />
l'opzione -rpath in questa maniera<br />
g++ -o prova_d main.cpp -Wl,rpath,/home/murgia/<strong>C++</strong>/<br />
-L. -<br />
lmath_util<br />
in questo caso non sarà necessario preoccuparsi di definire la variabile ambiente<br />
LD_LIBRARY_PATH.<br />
Si faccia però attenzione al fatto che il linker da' la precedenza al path specificato<br />
con -rpath, se questo non è specificato allora usa il valore di LD_LIBRARY_PATH,<br />
e solo infine verifica il contenuto del file /etc/ld.so.conf.<br />
Che tipo di libreria sto usando?<br />
Se nella stessa directory sono presenti sia libmath_util.so che libmath_util.a il<br />
linker preferirà la prima. Per forzare il linker ad utilizzare la libreria statica si può<br />
usare la flag -static.<br />
Un aspetto pos<strong>it</strong>ivo dell'utilizzo delle librerie condivise<br />
Diversi programmi che fanno uso di librerie comuni possono essere corretti<br />
contemporaneamente intervenendo sulla libreria che è fonte di errore. La sola<br />
ricompilazione e sost<strong>it</strong>uzione della libreria risolve un problema comune.<br />
Per riassumere:<br />
Librerie statiche:<br />
Librerie statiche vs librerie condivise<br />
• Ogni processo ha la sua copia della libreria statica che sta usando, caricata<br />
in memoria.<br />
• Gli eseguibili collegati con librerie statiche sono più grandi.
Librerie condivise:<br />
• Solo una copia della libreria viene conservata in memoria ad un dato istante<br />
(sfruttiamo meno memoria per far girare i nostri programmi e gli eseguibili<br />
sono più snelli).<br />
• I programmi partono più lentamente.
Le operazioni di input-ouput in <strong>C++</strong><br />
La gerarchia di classi stream<br />
La Libreria Standard del <strong>C++</strong> mette a disposizione, per l'esecuzione delle<br />
operazioni di input-output, un insieme di classi, funzioni e oggetti globali<br />
(tutti defin<strong>it</strong>i, come sempre, nel namespace std). Fra questi, conosciamo già gli<br />
oggetti cin, cout e cerr (a cui bisogna aggiungere, per completezza, clog, che<br />
differisce cerr da in quanto opera con output bufferizzato), collegati ai<br />
dispos<strong>it</strong>ivi standard stdin, stdout e stderr; e conosciamo anche l'esistenza di<br />
varie funzioni che implementano gli overloads degli operatori di flusso "" (estrazione), rispettivemente per la scr<strong>it</strong>tura dei dati su<br />
cout o cerr, e per la lettura dei dati da cin.<br />
Tutte le funzional<strong>it</strong>à di I/O del <strong>C++</strong> sono defin<strong>it</strong>e in una decina di headerfiles.<br />
Il principale è , che va sempre incluso. Alcuni altri sono inclusi<br />
dallo stesso , per cui c<strong>it</strong>eremo di volta in volta solo quelli necessari.<br />
Alcune classi della Libreria gestiscono operazioni di I/O "ad alto livello", cioè<br />
indipendenti dal dispos<strong>it</strong>ivo, che può essere un'un<strong>it</strong>à esterna (come i dispos<strong>it</strong>ivi<br />
standard a noi noti), un file, o anche un'area di memoria (in particolare una<br />
stringa); queste classi sono strutturate in un'organizzazione gerarchica: da<br />
un'unica classe base discendono, per ered<strong>it</strong>à, tutte le altre. Ogni loro istanza è<br />
detta genericamente "stream" (flusso). Il concetto di stream è un'astrazione,<br />
che rappresenta un "qualcosa" da o verso cui "fluisce" una sequenza di bytes;<br />
in sostanza un oggetto stream può essere interpretato come un "file<br />
intelligente" (con proprietà e metodi, come tutti gli oggetti), che agisce come<br />
"sorgente" da cui estrarre (input), o "destinazione" in cui inserire (output) i<br />
dati.<br />
Un altro concetto importante è quello della "posizione corrente" in un oggetto<br />
stream (file pos<strong>it</strong>ion indicator), che coincide con l'indice (paragonando lo<br />
stream a un array) del prossimo byte che deve essere letto o scr<strong>it</strong>to. Ogni<br />
operazione di I/O modifica la posizione corrente, la quale può essere anche<br />
ricavata o impostata direttamente usando particolari metodi (come vedremo). A<br />
questo propos<strong>it</strong>o precisiamo che la parola "inserimento", usata come sinonimo<br />
di operazione di scr<strong>it</strong>tura, ha diverso significato in base al valore della<br />
posizione corrente: se questa è interna allo stream, i dati non vengono<br />
"inser<strong>it</strong>i", ma sovrascr<strong>it</strong>ti; se invece la posizione corrente è alla fine dello<br />
stream (cioè una posizione oltre l'ultimo byte), i nuovi dati vengono<br />
effettivamente inser<strong>it</strong>i.<br />
La gerarchia di classi stream è illustrata dalla seguente figura:
Tutte le classi della gerarchia, salvo ios_base, sono specializzazioni di<br />
template: il nome di ognuna è in realtà un sinonimo del nome (con prefisso<br />
basic_) di una classe template specializzata con argomento . Per<br />
esempio:<br />
ifstream è un sinonimo di: basic_ifstream<br />
ma, come già detto a propos<strong>it</strong>o della classe string, noi siamo interessati solo al<br />
tipo char e quindi tratteremo direttamente delle classi specializzate e non dei<br />
template da cui provengono.<br />
Le classi ios_base e ios<br />
La classe base della gerarchia, ios_base, contiene proprietà e metodi che<br />
sono comuni sia alle operazioni di input che a quelle di output e non<br />
dipendono da parametri di template. Le stesse caratteristiche sono presenti<br />
nella sua classe derivata, ios, con la differenza che questa è una<br />
specializzazione con argomento char di<br />
template class basic_ios,<br />
le cui funzional<strong>it</strong>à dipendono dal parametro T. Dal nostro punto di vista, però,<br />
non ci sono parametri di template (assumendo sempre T=char), e quindi le<br />
due classi si possono considerare insieme come se fossero un'unica classe.<br />
Entrambe forniscono strumenti di uso generale per le operazioni di I/O, come<br />
ad esempio le funzioni di controllo degli errori, i flags per l'impostazione dei<br />
formati di lettura e/o scr<strong>it</strong>tura, i modi di apertura dei files ecc... (molti di<br />
questi dati-membro sono enumeratori cost<strong>it</strong>u<strong>it</strong>i da un singolo b<strong>it</strong> in una<br />
posizione specifica, e si possono combinare insieme con operazioni logiche b<strong>it</strong><br />
a b<strong>it</strong>). Entrambe le classi, inoltre, dichiarano i loro costruttori nella sezione<br />
protetta, e quindi non è possibile istanziarle direttamente; si devono invece<br />
utilizzare le classi derivate da ios, a partire da istream (per l'input) e<br />
ostream (per l'output), che contengono, per ered<strong>it</strong>à, anche i membri defin<strong>it</strong>i<br />
in ios e ios_base.<br />
Le classi istream, ostream e iostream<br />
La classe istream, derivata diretta di ios, contiene le funzional<strong>it</strong>à necessarie<br />
per le operazioni di input; in particolare la classe definisce un overload<br />
dell'operatore di flusso ">>" (estrazione), che determina il trasferimento di<br />
dati da un oggetto istream alla memoria. Sebbene non sia escluso che si
possano costruire delle sue istanze nel programma, anche la classe istream,<br />
come già la sua gen<strong>it</strong>rice ios, serve quasi esclusivamente per fornire proprietà<br />
e metodi alle classi derivate. Alla classe istream appartiene, come sappiamo,<br />
l'oggetto globale cin.<br />
La classe ostream, derivata diretta di ios, contiene le funzional<strong>it</strong>à necessarie<br />
per le operazioni di output; in particolare la classe definisce un overload<br />
dell'operatore di flusso "
La classe istringstream serve per le operazioni di input. Un oggetto<br />
istringstream è sostanzialmente una stringa, dalla quale però si possono<br />
estrarre dati, come se fosse un dispos<strong>it</strong>ivo periferico o un file. Analogamente ai<br />
files, se il risultato di un'operazione di estrazione è NULL, significa che si è<br />
raggiunta la fine della stringa (eos).<br />
La classe ostringstream serve per le operazioni di output. Un oggetto<br />
ostringstream è sostanzialmente una stringa, nella quale però si possono<br />
inserire dati, come se fosse un dispos<strong>it</strong>ivo periferico o un file. Le operazioni di<br />
inserimento possono anche modificare la dimensione della stringa, e quindi<br />
non è necessario effettuare controlli sul range. Questo fatto può essere di grande<br />
util<strong>it</strong>à perchè permette di espandere una stringa liberamente, in base alle<br />
necess<strong>it</strong>à (per esempio, per preparare un output "formattato").<br />
Infine la classe stringstream serve sia per le operazioni di input che di<br />
output.<br />
Tipi defin<strong>it</strong>i nella Libreria<br />
La Libreria di I/O definisce alcuni tipi specifici (molti dei quali sono in realtà<br />
sinonimi, creati con typedef, di altri tipi, che a loro volta dipendono<br />
dall'implementazione). I principali sono (per ognuno di essi indichiamo, fra<br />
parentesi tonde, l'amb<strong>it</strong>o o la classe in cui è defin<strong>it</strong>o, e, fra parentesi quadre,<br />
"normalmente implementato come ..."):<br />
• streamsize (namespace std) [sinonimo di int]<br />
indica un numero di bytes consecutivi in un oggetto stream; questo tipo<br />
(come pure i successivi) è utilizzato come argomento in varie funzioni di<br />
I/O<br />
• streamoff (namespace std) [sinonimo di long]<br />
indica lo spostamento in byte da una certa posizione in un oggetto<br />
stream a un'altra<br />
• fmtflags (ios_base) [tipo enumerato]<br />
i suoi enumeratori controllano l'impostazione del formato di lettura o<br />
scr<strong>it</strong>tura (vedere più avanti)<br />
• iostate (ios_base) [tipo enumerato]<br />
i suoi enumeratori controllano lo stato dell'oggetto stream dopo<br />
un'operazione (vedere più avanti)<br />
• openmode (ios_base) [tipo enumerato]<br />
i suoi enumeratori controllano il modo di apertura di un file (vedere<br />
prossimo paragrafo)<br />
• seekdir (ios_base) [tipo enumerato]<br />
i suoi enumeratori si riferiscono a particolari posizioni nell'oggetto<br />
stream, e sono:<br />
ios_base::beg (posizione iniziale)<br />
ios_base::cur (posizione corrente)<br />
ios_base::end (posizione finale)<br />
• pos_type (ios) [sinonimo di long]<br />
è il tipo della posizione corrente nell'oggetto stream<br />
• off_type (ios) [sinonimo di long]<br />
è sostanzialmente un sinonimo di streamoff, con la sola differenza che è<br />
defin<strong>it</strong>o nella classe ios anzichè nel namespace std
Modi di apertura di un file<br />
Relativamente alle operazioni di I/O su file, bisogna precisare anz<strong>it</strong>utto che la<br />
costruzione di un oggetto stream e l'apertura del file associato all'oggetto<br />
sono due operazioni logicamente e cronologicamente distinte (anche se esiste<br />
un costruttore che fa entrambe le cose, come vedremo). Di sol<strong>it</strong>o si usa prima il<br />
costruttore di default dell'oggetto (che non fa nulla) e poi un suo particolare<br />
metodo (la funzione open) che gli associa un file e lo apre. Questo permette<br />
di chiudere il file (tram<strong>it</strong>e un altro metodo, la funzione close) prima che<br />
l'oggetto sia distrutto e quindi riutilizzare l'oggetto stesso associandogli un<br />
altro file. Non possono coesistere due files aperti sullo stesso oggetto. Un file<br />
ancora aperto al momento della distruzione dell'oggetto viene chiuso<br />
automaticamente.<br />
Un file può essere aperto in diversi modi, a seconda di come si impostano i<br />
seguenti flags (che sono enumeratori del tipo enumerato openmode):<br />
• ios_base::in<br />
il file deve essere aperto in lettura<br />
• ios_base::out<br />
il file deve essere aperto in scr<strong>it</strong>tura<br />
• ios_base::ate<br />
il file deve essere aperto con posizione (inizialmente) sull'eof (significa<br />
"at the end"); di default un file è aperto "at the beginning"<br />
• ios_base::app<br />
il file deve essere aperto con posizione (permanentemente) sull'eof<br />
(cioè i dati si potranno scrivere solo in fondo al file)<br />
• ios_base::trunc<br />
il file deve essere aperto con cancellazione del suo contenuto<br />
preesistente; se il file non esiste, viene creato (in tutti gli altri casi deve già<br />
esistere)<br />
• ios_base::binary<br />
il file deve essere aperto in modo "binario", cioè i dati devono essere<br />
scr<strong>it</strong>ti o letti esattamente come sono; di default il file é aperto in<br />
modo "testo", nel qual caso, in output, ogni carattere newline può<br />
(dipende dall'implementazione!) essere trasformato nella coppia di<br />
caratteri carriage-return/line-feed (e viceversa in input)<br />
Ogni flag è rappresentato in una voce memoria da 16 o 32 b<strong>it</strong>, con un solo b<strong>it</strong><br />
diverso da zero e in una posizione diversa da quella dei b<strong>it</strong> degli altri flags.<br />
Questo permette di combinare insieme due modi con un'operazione di OR b<strong>it</strong> a<br />
b<strong>it</strong>, oppure di verificare la presenza di un singolo modo in una combinazione<br />
esistente, estraendolo con un'operazione di AND b<strong>it</strong> a b<strong>it</strong>. Per esempio, la<br />
combinazione:<br />
ios_base::in | ios_base::out<br />
indica che il file può essere aperto sia in lettura che in scr<strong>it</strong>tura. Va precisato,<br />
tuttavia, che il significato di alcune combinazioni dipende dall'implementazione e<br />
quindi va verificato "sperimentalmente", consultando il manuale del proprio<br />
sistema. Per esempio, nelle ultime versioni dello standard, il flag ios_base::out<br />
non può mai stare da solo, ma deve essere combinato con altri.<br />
Per concludere, i flags ios_base::in e ios_base::out sono anche usati dai<br />
costruttori delle classi che gestiscono l'I/O su stringa.
Operazioni di output<br />
Nella classe ostream sono defin<strong>it</strong>e varie funzioni-membro per l'esecuzione<br />
delle operazioni di output. Queste funzioni sono utilizzate direttamente per la<br />
scr<strong>it</strong>tura sui dispos<strong>it</strong>ivi standard stdout e stderr e sono ered<strong>it</strong>ate nelle classi<br />
ofstream, fstream, ostringstream e stringstream per la scr<strong>it</strong>tura su file e<br />
su stringa.<br />
Metodi operator
stringhe di caratteri.<br />
NOTA: questo metodo è particolarmente indicato per scrivere dati di<br />
qualsiasi tipo nativo (per esempio dati binari su file), operando una<br />
conversione di tipo puntatore nella chiamata. Per esempio, supponendo<br />
che out sia il nome dell'oggetto stream e val un valore intero o<br />
floating, si può scrivere val in out con la chiamata:<br />
out.wr<strong>it</strong>e((char*)&val,sizeof(val));<br />
notare il casting, che reintepreta l'indirizzo di val come indirizzo di una<br />
sequenza di sizeof(val) bytes. Nel caso invece che il tipo sia defin<strong>it</strong>o<br />
dall'utente, il discorso è un po' più complicato: la soluzione più "elegante"<br />
è quella della cosidetta "serializzazione", che consiste nel creare (nella<br />
classe dell'oggetto da scrivere) un metodo specifico, che scriva in<br />
successione i diversi membri dell'oggetto.<br />
• pos_type ostream::tellp()<br />
r<strong>it</strong>orna la posizione corrente<br />
• ostream& ostream::seekp(pos_type pos)<br />
sposta la posizione corrente in pos; r<strong>it</strong>orna *this; questo metodo<br />
(come il suo overload che segue) si usa principalmente quando l'output è<br />
su file ad accesso casuale<br />
• ostream& ostream::seekp(off_type off, ios_base::seekdir seek)<br />
sposta la posizione corrente di off bytes a partire dal valore indicato<br />
dall'enumeratore seek; r<strong>it</strong>orna *this; off può anche essere negativo<br />
(deve esserlo quando seek coincide con ios_base::end e deve non<br />
esserlo quando seek coincide con ios_base::beg); in ogni caso se<br />
l'operazione tende a spostare la posizione corrente fuori dal range, la<br />
seekp non viene esegu<strong>it</strong>a e la posizione corrente resta invariata; la<br />
posizione corrispondente alla fine dello stream (cioè eof o eos) è<br />
considerata ancora nel range.<br />
Funzioni virtuali di output<br />
Le funzioni-membro di ostream non sono virtuali, per motivi di efficienza,<br />
dato che in un programma le operazioni di I/O sono in genere molto frequenti.<br />
Tuttavia si può essere talvolta nella necess<strong>it</strong>à di mandare in output un oggetto<br />
di tipo polimorfo, lasciando alla fase di esecuzione del programma la scelta del<br />
tipo "concreto" fra quelli derivati da un'unica classe base astratta. Per<br />
ottenere questo risultato, bisogna procedere nel seguente modo (supponiamo di<br />
chiamare My_base la classe base astratta):<br />
1. dichiarare in My_base la funzione virtuale pura (che chiamiamo ins):<br />
virtual ostream& ins(ostream& out) const = 0; // scrive<br />
*this su out<br />
2. ridefinire ins in tutte le classi derivate da My_base, in modo che ogni<br />
funzione svolga l'operazione di scr<strong>it</strong>tura appropriata per la sua classe<br />
3. definire il seguente overload di operator
generale per fornire operazioni che si comportano come funzioni virtuali, ma<br />
con la selezione dinamica basata sul secondo argomento.<br />
Metodi specifici per l'output su file<br />
Nella classe ofstream, derivata di ostream (e anche nella classe fstream,<br />
derivata di iostream, per le operazioni comuni all'output e all'input), sono<br />
defin<strong>it</strong>i alcuni metodi che, insieme a quelli ered<strong>it</strong>ati dalla classe base,<br />
servono per la scr<strong>it</strong>tura su file. Il più importante di questi è il metodo open:<br />
void ofstream::open(const char* filename, ios_base::openmode mode<br />
= ....)<br />
void fstream::open(const char* filename, ios_base::openmode mode<br />
= ....)<br />
che ha due argomenti: il primo, filename, è il nome del file da aprire (nota: è<br />
una stringa del C, non un oggetto string!), il secondo, mode, rappresenta il<br />
modo di apertura del file ed è di default, con valore che dipende dalla classe<br />
e precisamente:<br />
• in ofstream (sola scr<strong>it</strong>tura):<br />
mode = ios_base::out | ios_base::trunc<br />
(notare: se il file esiste, viene "troncato", se non esiste viene creato)<br />
• in fstream (lettura e scr<strong>it</strong>tura):<br />
mode = ios_base::out | ios_base::in<br />
(notare: il file deve esistere)<br />
Se si verifica un errore, non appaiono messaggi, ma nessuna delle successive<br />
operazioni sul file viene esegu<strong>it</strong>a. Ci si può accorgere dell'errore interrogando lo<br />
stato dell'oggetto (come vedremo).<br />
Fra gli altri metodi defin<strong>it</strong>i in ofstream c<strong>it</strong>iamo:<br />
• void ofstream::close()<br />
chiude il file senza distruggere l'oggetto *this, a cui si può così<br />
associare un altro file (oppure di nuovo lo stesso, per esempio con modi<br />
di apertura diversi)<br />
• costruttore di default<br />
crea l'oggetto senza aprire nessun file; deve ovviamente essere segu<strong>it</strong>o<br />
da una open<br />
• costruttore con esattamente gli stessi argomenti della open (compresi i<br />
defaults)<br />
riunisce insieme le operazioni del costruttore di default e della open (a<br />
cui è ovviamente alternativo); anche se generalmente il file resta aperto<br />
fino alla distruzione dell'oggetto, la "prima" apertura tram<strong>it</strong>e<br />
costruttore al posto della open non preclude la possibil<strong>it</strong>à che il file<br />
venga chiuso "anticipatamente" (con la close) e che poi venga associato<br />
all'oggetto un altro file (con una successiva open)<br />
• bool ofstream::isopen()<br />
r<strong>it</strong>orna true se esiste un file aperto associato all'oggetto
La classe fstream definisce esattamente gli stessi metodi di ofstream (l'unica<br />
differenza è nel modo di apertura di default del file, dato dal secondo<br />
argomento del costruttore come nella open corrispondente).<br />
Metodi specifici per l'output su stringa<br />
Nella classe ostringstream, derivata di ostream (e anche nella classe<br />
stringstream, derivata di iostream, per le operazioni comuni all'output e<br />
all'input), sono defin<strong>it</strong>i alcuni metodi che, insieme a quelli ered<strong>it</strong>ati dalla<br />
classe base, servono per la scr<strong>it</strong>tura su stringa. I più importanti sono:<br />
• ostringstream::ostringstream(ios_base::openmode mode =<br />
ios_base::out)<br />
costruttore di default (con un argomento di default )<br />
• ostringstream::ostringstream(const string& str, ios_base ..come<br />
sopra.. )<br />
costruttore per copia da un oggetto string (con il secondo<br />
argomento di default )<br />
• string ostringstream::str()<br />
crea una copia di *this e la r<strong>it</strong>orna convert<strong>it</strong>a in un oggetto string.<br />
Questo metodo è molto utile, in quanto gli oggetti di ostringstream (e<br />
delle altre classi della gerarchia stream) non possiedono le funzional<strong>it</strong>à<br />
delle stringhe; per poterli utilizzare come stringhe è prima necessario<br />
convertirli in oggetti string.<br />
• void ostringstream::str(const string& str)<br />
questo secondo overload di str esegue l'operazione inversa del<br />
precedente: sost<strong>it</strong>uisce in *this una copia di un oggetto string<br />
La classe stringstream definisce esattamente gli stessi metodi di<br />
ostringstream, con la differenza che l'argomento di default dei costruttori<br />
(mode) è:<br />
mode = ios_base::out | ios_base::in<br />
Operazioni di input<br />
Nella classe istream sono defin<strong>it</strong>e varie funzioni-membro per l'esecuzione<br />
delle operazioni di input. Queste funzioni sono utilizzate direttamente per la<br />
lettura dal dispos<strong>it</strong>ivo standard stdin e sono ered<strong>it</strong>ate nelle classi ifstream,<br />
fstream, istringstream e stringstream per la lettura da file e da stringa.<br />
Metodi operator>><br />
Alcuni metodi di istream definiscono tutti i possibili overloads di<br />
operator>> con argomento di tipo nativo (compresi i tipi ottenuti mediante i<br />
prefissi short, long, signed e unsigned). Da *this vengono estratte
stringhe di caratteri, che sono interpretate secondo un certo formato e poi<br />
convert<strong>it</strong>e nel tipo rappresentato dall'argomento, in cui vengono infine<br />
memorizzate. Ognuna di queste stringhe (che chiamiamo "stringhe di input") è<br />
delim<strong>it</strong>ata da uno o più "spazi bianchi" (così sono defin<strong>it</strong>i i caratteri: spazio,<br />
tabulazione, fine riga, fine pagina e r<strong>it</strong>orno carrello); tutti gli spazi<br />
bianchi che precedono e seguono una stringa di input vengono "scartati", cioè<br />
eliminati dallo stream e non trasfer<strong>it</strong>i in memoria (anche quando l'argomento è<br />
di tipo char, nel qual caso non viene estratta una stringa, ma un singolo<br />
carattere, pur sempre tuttavia dopo avere "scartato" tutti gli eventuali spazi<br />
bianchi che lo precedono). Pertanto ogni singola esecuzione di operator>><br />
converte e trasferisce in memoria una e una sola stringa di input alla volta,<br />
qualunque sia la dimensione dello stream. I caratteri della stringa di input,<br />
inoltre, devono essere tutti validi, in relazione al tipo dell' argomento. Per<br />
esempio, se il dato da leggere è di tipo int e la stringa di input contiene un<br />
"punto", questa viene troncata in modo da lasciare il "punto" come primo<br />
carattere della prossima stringa di input da estrarre (vedere la gestione degli<br />
errori nella prossima sezione).<br />
Per quello che riguarda i puntatori (a qualunque tipo), è defin<strong>it</strong>o un overload<br />
di operator>> con argomento void*, che converte la stringa di input in un<br />
numero intero e lo memorizza nell'argomento (la cosa ha però scarso interesse,<br />
in quanto non si possono mai assegnare valori agli indirizzi). E' importante<br />
invece il caso di puntatore a carattere, per il quale è defin<strong>it</strong>o un overload<br />
specifico con argomento char*: in questo caso la stringa di input non viene<br />
convert<strong>it</strong>a, ma trasfer<strong>it</strong>a così com'è nell'area di memoria puntata dall'argomento;<br />
alla fine viene aggiunto automaticamente il carattere '\0' come terminatore<br />
della stringa memorizzata.<br />
Per ciò che concerne la definizione di ulteriori overloads di operator>> con<br />
argomento di tipo defin<strong>it</strong>o dall'utente, e la scelta fra i metodi e le funzioni<br />
esterne, vedere le considerazioni fatte a propos<strong>it</strong>o di operator e gli altri metodi di<br />
istream che eseguono operazioni di lettura consiste nel fatto che i primi<br />
estraggono stringhe di input, senza spazi bianchi e interpretate secondo un<br />
certo formato (formatted input functions), mentre gli altri metodi<br />
estraggono singoli bytes (o sequenze di bytes) senza applicare nessun<br />
formato (unformatted input functions) e senza escludere gli spazi bianchi.<br />
Vediamone i principali:<br />
• int istream::get()<br />
estrae un byte e lo r<strong>it</strong>orna al chiamante. Nota: il valore di r<strong>it</strong>orno è<br />
sempre pos<strong>it</strong>ivo (in quanto è defin<strong>it</strong>o int e contiene un solo byte, cioè al<br />
massimo il numero 255; pertanto un valore di r<strong>it</strong>orno negativo indica<br />
convenzionalmente che si è verificato un errore, oppure che la posizione<br />
corrente era già sulla fine dello stream (cioè su eof o eos)<br />
• istream& istream::get(char& c)<br />
estrae un byte e lo memorizza in c; r<strong>it</strong>orna *this<br />
• istream& istream::get(char* p, streamsize n, char delim='\n')<br />
estrae n-1 bytes e li memorizza nell'area puntata da p (facendo seguire il<br />
carattere '\0' come terminatore della stringa memorizzata); r<strong>it</strong>orna
*this; il processo di estrazione può essere interrotto in anticipo, per uno<br />
dei seguenti motivi:<br />
1. è stata raggiunta la fine dello stream;<br />
2. è stato incontrato il carattere delim; in questo caso delim non<br />
viene estratto e la posizione corrente si attesta sullo stesso<br />
delim<br />
• istream& istream::getline(char* p, streamsize n, char delim='\n')<br />
è identica alla get precedente, con due differenze:<br />
1. se incontra il carattere delim non lo estrae (come nella get), ma<br />
la posizione corrente si attesta dopo delim (cioè delim viene<br />
"saltato")<br />
2. se completa l'estrazione di n-1 bytes senza incontrare delim,<br />
viene impostata una condizione di errore; in pratica ciò vuol dire che<br />
l'argomento n serve per imporre la condizione:<br />
posizione di delim - posizione corrente < n<br />
• istream& istream::read(char* p, streamsize n)<br />
differisce dalle funzioni precedenti per il fatto che non ha delim<strong>it</strong>atori (a<br />
parte la fine dello stream) e estrae n bytes (senza aggiungere il<br />
carattere '\0' in fondo); e quindi non legge stringhe di caratteri, ma<br />
dati binari di qualsiasi tipo (vedere la NOTA a propos<strong>it</strong>o del metodo<br />
wr<strong>it</strong>e di ostream)<br />
• streamsize istream::readsome(char* p, streamsize n)<br />
come la read, salvo il fatto che r<strong>it</strong>orna il numero di bytes effettivamente<br />
letti<br />
• istream& istream::ignore(streamsize n=1, int delim=EOF)<br />
"salta" i prossimi n bytes, oppure i prossimi bytes fino a delim<br />
(compreso); il default di delim (EOF) è una costante predefin<strong>it</strong>a che<br />
indica la fine dello stream (normalmente implementata con il valore -1);<br />
ignore serve soprattutto per "saltare" caratteri invalidi nella lettura<br />
formattata da una stringa di input<br />
I metodi di interrogazione e modifica diretta della posizione corrente sono:<br />
tellg e seekg (in 2 overloads): hanno gli stessi argomenti e svolgono le stesse<br />
operazioni dei corrispondenti tellp e seekp defin<strong>it</strong>i in ostream.<br />
Metodi specifici per input da file e da stringa<br />
Nelle classi ifstream e istringstream, derivate di istream, sono defin<strong>it</strong>i<br />
esattamente gli stessi metodi che si trovano rispettivamente in ofstream e<br />
ostringstream. L'unica differenza sta nel default dell'argomento mode della<br />
open e dei costruttori, che in questo caso è:<br />
mode = ios_base::in<br />
Stato dell'oggetto stream e gestione degli errori
A ogni oggetto stream è associato uno "stato", impostando e controllando il<br />
quale è possibile gestire gli errori e le condizioni anomale nelle operazioni di<br />
input-output.<br />
Lo stato dell'oggetto è rappresentato da un insieme di flags (che sono<br />
enumeratori del tipo enumerato iostate, defin<strong>it</strong>o nella classe ios_base),<br />
ciascuno dei quali (come gli enumeratori del tipo openmode) può essere<br />
combinato con gli altri con un'operazione di OR b<strong>it</strong> a b<strong>it</strong> e separato dagli altri<br />
con un'operazione di AND b<strong>it</strong> a b<strong>it</strong>. I flags sono i seguenti:<br />
• ios_base::goodb<strong>it</strong><br />
finora tutto bene e la posizione corrente non è sulla fine dello stream;<br />
nessun b<strong>it</strong> è "settato" (valore 0)<br />
• ios_base::failb<strong>it</strong><br />
si è verificato un errore di I/O, oppure si è tentato di eseguire<br />
un'operazione non consent<strong>it</strong>a (per esempio la open di un file che non<br />
esiste)<br />
• ios_base::badb<strong>it</strong><br />
si è verificato un errore di I/O irrecuperabile<br />
• ios_base::eofb<strong>it</strong><br />
la posizione corrente è sulla fine dello stream; un successivo tentativo<br />
di lettura imposta anche failb<strong>it</strong><br />
La classe ios, derivata di ios_base, fornisce alcuni metodi per la gestione e il<br />
controllo dello stato:<br />
• ios_base::iostate ios::rdstate() const<br />
r<strong>it</strong>orna lo stato che risulta dall'ultima operazione<br />
• void ios::clear(iostate st=goodb<strong>it</strong>)<br />
imposta lo stato con st (cancellando il valore precedente); chiamando<br />
clear() senza argomenti si imposta goodb<strong>it</strong>, cioè si "resettano" i flags<br />
di errore<br />
• void ios::setstate(iostate st)<br />
aggiunge il flag st allo stato corrente, eseguendo l'istruzione:<br />
clear(rdstate() | st );<br />
• bool ios::good() const<br />
r<strong>it</strong>orna rdstate() == goodb<strong>it</strong><br />
• bool ios::fail() const<br />
r<strong>it</strong>orna bool(rdstate() & failb<strong>it</strong>)<br />
• bool ios::bad() const<br />
r<strong>it</strong>orna bool(rdstate() & badb<strong>it</strong>)<br />
• bool ios::eof() const<br />
r<strong>it</strong>orna bool(rdstate() & eofb<strong>it</strong>)<br />
• ios::operator void*() const<br />
r<strong>it</strong>orna NULL se fail() | bad() è true; altrimenti r<strong>it</strong>orna this (che<br />
però, essendo convert<strong>it</strong>o in un puntatore a void, non può essere<br />
dereferenziato)<br />
NOTA: questo (strano) metodo necess<strong>it</strong>a di un chiarimento: é noto che il<br />
casting a puntatore a void non é mai necessario, in quanto un<br />
puntatore a void può puntare a qualsiasi tipo di oggetto; quindi anche il<br />
semplice nome dell'oggetto può essere reinterpretato come suo casting<br />
a puntatore a void (!!!). In pratica il compilatore, quando incontra<br />
l'oggetto come operando in una posizione che non gli compete, prima di
segnalare l'errore cerca se nella classe a cui appartiene l'oggetto é<br />
defin<strong>it</strong>o un overload del casting a puntatore a void e, se lo trova, lo<br />
applica. Nel nostro caso il metodo r<strong>it</strong>orna normalmente this e quindi un<br />
espressione del tipo:<br />
cout > .... )<br />
infatti l'operazione >> r<strong>it</strong>orna cin, che viene convert<strong>it</strong>o da operator<br />
void*: questo a sua volta r<strong>it</strong>orna l'indirizzo di cin finchè non ci sono<br />
errori (e quindi true, essendo un indirizzo sempre diverso da zero) e il<br />
ciclo prosegue; ma quando il programma tenta di leggere la fine dello<br />
stream, si imposta il flag failb<strong>it</strong> e quindi operator void* r<strong>it</strong>orna NULL<br />
interrompendo il ciclo.<br />
• bool ios::operator !() const<br />
r<strong>it</strong>orna bool(fail() | bad())<br />
NOTA: le espressioni: if(cin) e if(!!cin) sono equivalenti (!),<br />
mentre le espressioni: if(cin) e if(cin.good()) non sono equivalenti,<br />
in quanto la prima non controlla il flag eofb<strong>it</strong><br />
Quando è impostato un qualunque flag diverso da goodb<strong>it</strong>, nessuna funzione<br />
non const defin<strong>it</strong>a nell'oggetto stream può essere esegu<strong>it</strong>a (senza messaggi di<br />
errore: semplicemente le successive istruzioni con operazioni di I/O non hanno<br />
alcun effetto); tuttavia lo stato può essere "resettato" chiamando la clear<br />
(successivamente, però, bisogna rimuovere la causa dell'errore se si vuole che le<br />
operazioni di I/O riprendano a essere regolarmente esegu<strong>it</strong>e).<br />
Se, durante un'operazione di lettura formattata da una stringa di input, si<br />
incontra un carattere non ammissibile in relazione al tipo di dato da leggere,<br />
abbiamo già detto che la stringa di input viene "spezzata" in due: la prima, su<br />
cui viene normalmente esegu<strong>it</strong>a la lettura, termina lasciando fuori il carattere<br />
invalido; la seconda comincia con il carattere invalido (che, se è tale anche in<br />
relazione al tipo del successivo dato da leggere, deve essere "saltato"<br />
chiamando la ignore). Per quanto riguarda lo stato, il comportamento è diverso<br />
a seconda che il carattere invalido sia o meno il primo carattere della stringa<br />
di input:<br />
• se non è il primo, lo stato resta defin<strong>it</strong>o dal flag goodb<strong>it</strong> (per la<br />
successiva operazione si può chiamare la ignore senza la clear);<br />
• se è il primo, è impostato il flag failb<strong>it</strong> (bisogna chiamare la clear prima<br />
della ignore se si vuole che questa abbia effetto)<br />
Errori gest<strong>it</strong>i dalle eccezioni<br />
Per una gestione corretta degli errori, sarebbe opportuno controllare lo stato<br />
dopo ogni operazione di I/O. Se però le operazioni sono molte, la cosa non<br />
risulta molto comoda, anche in considerazione del fatto che gli errori sono in<br />
generale poco frequenti. In particolare le operazioni di output sono controllate<br />
assai raramente (benchè ogni tanto anche loro falliscano): di sol<strong>it</strong>o si verifica che,<br />
dopo una open, il file sia stato aperto correttamente, e niente di più.
Diverso è il discorso se si riferisce alle operazioni di input: qui i possibili errori<br />
sono vari e diversi: formati sbagliati, errori umani nella immissione dei dati ecc...,<br />
senza contare il fatto che bisogna sempre controllare il raggiungimento della fine<br />
dello stream. Pertanto l'esame dello stato dopo un'operazione di lettura è<br />
quasi sempre necessario.<br />
Tuttavia, come alternativa alla disseminazione di istruzioni if e sw<strong>it</strong>ch nel<br />
programma, è possibile gestire gli errori di input-output anche mediante le<br />
eccezioni. A questo scopo è defin<strong>it</strong>o nella classe ios_base un oggetto del<br />
tipo enumerato iostate (exception mask), che contiene un insieme di flags<br />
di stato: quando un'operazione di I/O imposta uno di questi flags, viene<br />
generata un'eccezione di tipo ios_base::failure (failure è una classe<br />
"annidata" in ios_base) che può essere catturata e gest<strong>it</strong>a da un blocco<br />
catch:<br />
catch(ios_base::failure) { ..... }<br />
Di default l'exception mask è vuoto (cioè di default gli errori di I/O non<br />
generano eccezioni), ma è possibile cambiarne il contenuto chiamando il<br />
metodo exceptions di ios:<br />
void ios::exceptions(iostate em) che imposta l'exception mask con<br />
em.<br />
Esiste anche un overload di exceptions senza argomenti che r<strong>it</strong>orna<br />
l'exception mask corrente:<br />
ios_base::iostate ios::exceptions() const<br />
Con i due overloads di exceptions è possibile circoscrivere l'uso delle<br />
eccezioni in aree precise del programma; per esempio:<br />
ios_base::iostate em = cin.exceptions(); salva l'exception mask<br />
corrente (no eccezioni)<br />
in em<br />
cin.exceptions(ios_base::badb<strong>it</strong>|ios_base::failb<strong>it</strong>); imposta l'exception<br />
mask con badb<strong>it</strong> e<br />
failb<strong>it</strong><br />
try { ... cin >> ...} blocco delle istruzioni di<br />
I/O che possono<br />
generare eccezioni<br />
catch(ios_base::failure) { ..... } blocco di gestione delle<br />
eccezioni<br />
cin.exceptions(em); ripristina l'exception<br />
mask precedente (no<br />
eccezioni)<br />
Formattazione e manipolatori di formato
Abbiamo detto che le funzioni operator>> (in istream) e operator> converte<br />
una stringa di input (delim<strong>it</strong>ata da spazi bianchi) nel dato da memorizzare,<br />
mentre operator
1. trova che l'overload di operator
Abbiamo visto che un manipolatore è una funzione che viene esegu<strong>it</strong>a al posto<br />
di un puntatore a funzione e quindi il suo nome va specificato, come<br />
operando in un'operazione di flusso, senza parentesi e senza argomenti.<br />
Esistono tuttavia manipolatori che accettano un argomento, cioè che vanno<br />
specificati con un valore fra parentesi. In questi casi (consideriamo al sol<strong>it</strong>o solo<br />
l'output) l'overload di operator
destra (oppure a sinistra se è stato specificato il manipolatore left); nella<br />
posizione che compete ai caratteri rimanenti, viene scr<strong>it</strong>to il cosidetto<br />
"carattere di riempimento", che di default è uno spazio (codice 32), ma<br />
che può anche essere modificato con setfill. Il manipolatore setw è<br />
l'unico che non ha effetto permanente, ma modifica il formato solo<br />
relativamente alla prossima operazione (dalla successiva il formato<br />
tornerà com'era prima di setw)<br />
• setfill(char c)<br />
stabilisce che il "carattere di riempimento" d'ora in poi sarà c<br />
• setprecision(int p)<br />
(p deve essere non negativo, altrimenti il manipolatore non ha effetto)<br />
influenza esclusivamente l'output di numeri floating e il suo effetto è<br />
diverso, a seconda di come è impostato il formato floating; questo può<br />
assumere tre diverse configurazioni:<br />
1. fixed: è impostato dal manipolatore fixed; utilizza la<br />
rappresentazione:<br />
[parte intera].[parte decimale]<br />
(corrisponde allo specificatore di formato %f del C); p indica il<br />
numero esatto di cifre della parte decimale (compresi eventuali zeri<br />
a destra); l'ultima cifra decimale è arrotondata; se p è zero, è<br />
arrotondata la cifra delle un<strong>it</strong>à e il punto decimale non è scr<strong>it</strong>to (a<br />
meno che non sia stato specificato il manipolatore showpoint)<br />
2. scientific: è impostato dal manipolatore scientific; utilizza la<br />
rappresentazione:<br />
[cifra intera].[parte decimale]e[esponente]<br />
(corrisponde allo specificatore di formato %e del C); come<br />
fixed, p indica il numero esatto di cifre della parte decimale e<br />
l'ultima cifra decimale è arrotondata; scrive E al posto di e se è stato<br />
specificato il manipolatore uppercase; l'esponente è cost<strong>it</strong>u<strong>it</strong>o dal<br />
segno, segu<strong>it</strong>o da 2 o 3 (dipende dall'implementazione) cifre intere<br />
3. general: è impostato di default; sceglie, fra le rappresentazioni di<br />
fixed e di scientific, quella più conveniente (corrisponde allo<br />
specificatore di formato %g del C); p indica il numero massimo<br />
di cifre significative; l'ultima cifra significativa è arrotondata; gli zeri<br />
non significativi della parte decimale non sono scr<strong>it</strong>ti; se il numero è<br />
arrotondato a intero non è scr<strong>it</strong>to neppure il punto decimale (a meno<br />
che non sia stato specificato il manipolatore showpoint).<br />
NOTA: questo è l'unico caso in cui non esiste un manipolatore per<br />
ripristinare il default. Per tornare al formato general dopo che è<br />
stato impostato fixed o scientific, bisogna usare il metodo setf<br />
(defin<strong>it</strong>o in ios_base), nel seguente modo (supponiamo per<br />
esempio che l'oggetto stream sia cout):<br />
cout.setf(ios_base::fmtflags(0),ios_base::floatfield);<br />
Manipolatori defin<strong>it</strong>i dall'utente<br />
Applicando gli schemi riportati negli esempi di manipolatori con e senza<br />
argomenti, un programmatore può definire nuovi manipolatori, per il suo uso<br />
specifico.<br />
Nell'esercizio che segue è defin<strong>it</strong>o un manipolatore, chiamato format (con 2<br />
argomenti!), che permette la scr<strong>it</strong>tura di un dato, di tipo double, specificando<br />
insieme, in un'unica stringa, il formato floating, il campo e la precisione. Il
manipolatore deve essere usato nel modo seguente (supponiamo al sol<strong>it</strong>o che<br />
l'oggetto stream sia cout):<br />
cout
cout
Gestione del buffer di input<br />
Oltre ai metodi di istream, tellg e seekg, già visti, consideriamo i seguenti:<br />
• istream& istream::putback(char c)<br />
inserisce c nel buffer prima della posizione corrente e arretra la<br />
posizione corrente di 1; l'operazione è valida solo se è preceduta da<br />
almeno una normale lettura (cioè non si può inserire un carattere prima<br />
dell'inizio dell'oggetto); r<strong>it</strong>orna *this<br />
• istream& istream::unget()<br />
come putback, con la differenza che rimette nel buffer l'ultimo carattere<br />
letto<br />
• int istream::peek()<br />
r<strong>it</strong>orna il prossimo carattere da leggere (senza toglierlo dal buffer e<br />
senza spostare la posizione corrente); questo metodo (come anche i<br />
precedenti) può essere usato per riconoscere il tipo del prossimo dato<br />
prima di leggerlo effettivamente (vedere esercizio).
Conclusioni<br />
La programmazione modulare, la programmazione a oggetti e la<br />
programmazione generica forniscono strumenti formidabili per scrivere codice<br />
ad alto livello. La possibil<strong>it</strong>à di suddivere un programma in porzioni (quasi)<br />
indipendenti rende l'attiv<strong>it</strong>à dei programmatori più facile, piacevole ed efficace e<br />
rende il programma stesso più flessibile, riutilizzabile, estendibile e di più facile<br />
manutenzione.<br />
Fra tutti i linguaggi, il <strong>C++</strong> è quello che maggiormente permette di realizzare<br />
questi obiettivi, grazie ai suoi potenti strumenti concettuali: data hiding,<br />
namespace, classe, overload di funzioni e di operatori, ered<strong>it</strong>à,<br />
polimorfismo e template. Tuttavia, a differenza da altri linguaggi "puri" di<br />
programmazione orientata a oggetti, il <strong>C++</strong> non "rinnega" la "cultura" del C,<br />
da cui ered<strong>it</strong>a intatta la potenza e verso cui mantiene la compatibil<strong>it</strong>à,<br />
preservando un "patrimonio" di conoscenze e realizzazioni che non sarebbe stato<br />
conveniente disperdere.<br />
Pertanto il <strong>C++</strong> è un linguaggio insieme completo e in continua evoluzione: sul<br />
solido impianto del C ha costru<strong>it</strong>o una nuova "filosofia" che gli permette di<br />
espandersi nel tempo. A tutt'oggi il <strong>C++</strong> si utilizza praticamente in qualsiasi<br />
dominio applicativo, inclusi quelli (a noi vicini) dell'insegnamento e della ricerca.<br />
Terminiamo questo corso con una serie di consigli utili per un programmatore<br />
<strong>C++</strong> non ancora "esperto":<br />
• usa "poco" la direttiva #define; al suo posto usa:<br />
o const e enum, per definire valori costanti;<br />
o inline, per ev<strong>it</strong>are la perd<strong>it</strong>a di efficienza dovuta alle chiamate di<br />
funzioni;<br />
o template, per specificare famiglie di funzioni o di tipi;<br />
o namespace, per ev<strong>it</strong>are confl<strong>it</strong>ti nei nomi.<br />
• non dichiarare una variabile locale molto prima di usarla; una<br />
dichiarazione può apparire ovunque possa apparire un'istruzione<br />
• non definire mai variabili globali; le variabili non locali siano sempre<br />
defin<strong>it</strong>e in un namespace<br />
• ev<strong>it</strong>a le copie inutili: passa il più possibile gli argomenti per riferimento;<br />
se non vuoi che gli argomenti vengano modificati, dichiarali const<br />
• dimentica le funzioni del C di gestione della memoria dinamica<br />
(malloc, free e compagnia) e al loro posto usa gli operatori new e<br />
delete; per riallocare memoria, non usare la realloc del C, ma i metodi<br />
resize o reserve di vector<br />
• suddividi il tuo programma in moduli indipendenti, usando i namespace;<br />
se sei coinvolto in un grosso progetto, potrai sviluppare il software in modo<br />
più efficiente<br />
• ragguppa il più possibile variabili e funzioni in classi, e usa gli oggetti,<br />
istanze delle classi
• gli oggetti sono componenti attive, con proprietà e metodi; realizza il<br />
data hiding, rendendo in generale private tutte le proprietà e pubblici<br />
solo i metodi che vengono chiamati dall'esterno<br />
• se una funzione agisce su un oggetto di una classe, rendila metodo di<br />
quella classe; se non è possibile, dichiarala friend (solo però se accede<br />
a membri privati)<br />
• sfrutta l'overload degli operatori per definire operazioni fra gli oggetti<br />
• associa costruttori e distruttori alle classi che definisci<br />
• non ricominciare sempre "da zero": usa l'ered<strong>it</strong>à quando vuoi espandere<br />
un concetto, e la composizione quando vuoi riunire concetti esistenti<br />
• struttura la tua gerarchia di classi applicando il polimorfismo: potrai<br />
aggiungere nuove classi senza modificare il codice esistente; non<br />
dimenticare di dichiarare virtual il distruttore della classe base<br />
• usa i template quando devi progettare una funzione o una classe da<br />
applicare a tipi diversi di oggetti<br />
• minimizza l'uso degli array e delle stringhe del C; la Libreria Standard<br />
del <strong>C++</strong> mette a disposizione le classi vector e string, che sono più<br />
versatili e più efficienti; in generale, non tentare di costruire da solo quello<br />
che è già forn<strong>it</strong>o dalla Libreria (difficilmente potresti raggiungere il suo<br />
livello di ottimizzazione)<br />
• la Standard Template Library fornisce un insieme di classi<br />
(conten<strong>it</strong>ori) e funzioni (algor<strong>it</strong>mi) che, in quanto template, si<br />
possono applicare a una gamma molto vasta di problemi applicativi: non<br />
farti mai sfuggire l'occasione di utilizzarla!<br />
• ev<strong>it</strong>a le funzioni di I/O del C; usa le classi di flusso e i relativi<br />
operatori: sono più facili, più eleganti e possono avere overload