02.06.2013 Views

CORSO C++ STANDARD - Didattica.it

CORSO C++ STANDARD - Didattica.it

CORSO C++ STANDARD - Didattica.it

SHOW MORE
SHOW LESS

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

Hooray! Your file is uploaded and ready to be published.

Saved successfully!

Ooh no, something went wrong!