Vous êtes sur la page 1sur 296

CORSO C++ STANDARD

Indice degli argomenti trattati

Introduzione
Obiettivi e Prerequisiti
Contenuto generale del Corso
Nota storica
Caratteristiche generali del linguaggio
"Case sensitivity"
Moduli funzione
Entry-point del programma: la funzione main
Le tre parti di una funzione
Aree di commento
Primo programma di esempio (con tabella esplicativa di ogni
simbolo usato)
Cominciamo dalla funzione printf
Perch una funzione di I/O del C ?
Operazioni della funzione printf
Argomenti della funzione printf
Scrittura della control string sullo schermo
Definizione di sequenza di escape
Principali sequenze di escape
La funzione printf con pi argomenti
Definizione di specificatore di formato
Principali specificatori di formato in free-format
Specificatori di formato con ampiezza di campo e precisione
Altri campi degli specificatori di formato
Tipi, Variabili, Costanti
Tipi delle variabili
Tipi intrinseci del linguaggio
Dichiarazione e definizione degli identificatori
Qualificatori e specificatori di tipo
Tabella di occupazione della memoria dei vari tipi di dati
L'operatore sizeof
Il tipo "booleano"
Definizione con Inizializzazione
Le Costanti in C++
Specificatore const
Visibilit e tempo di vita
Visibilit di una variabile
Tempo di vita di una variabile
Visibilit globale
Operatori e operandi
Definizione di operatore e regole generali
Operatore di assegnazione
Operatori matematici
Operatori a livello del bit
Operatori binari in notazione compatta
Operatori relazionali

Operatori logici
Operatori di incremento e decremento
Operatore condizionale
Conversioni di tipo
Precedenza fra operatori (tabella)
Ordine di valutazione
Introduzione all'I/O sui dispositivi standard
Dispositivi standard di I/O
Oggetti globali di I/O
Operatori di flusso di I/O
Output tramite l'operatore di inserimento
Input tramite l'operatore di estrazione
Memorizzazione dei dati introdotti da tastiera
Comportamento in caso di errore in lettura
Il Compilatore GNU gcc in ambiente Linux
Un compilatore integrato C/C++
Il progetto GNU
Quale versione di gcc sto usando?
I passi della compilazione
Estensioni
L'input/output di gcc
Il valore restituito al sistema
Passaggi intermedi di compilazione
I messaggi del compilatore
Controlliamo i livelli di warning
Compilare per effetture il debug
Autopsia di un programma defunto
Ottimizzazione
Compilazione di un programma modulare
Inclusione di librerie in fase di compilazione
Il Comando 'make' in ambiente Linux
Perche' utilizzare il comando make?
Il Makefile ed i target del make
Dipendenze
Macro e variabili ambiente
Compiliamo con make
Alcuni target standard
Istruzioni di controllo
Istruzione di controllo if
Istruzione di controllo while
Istruzione di controllo do ... while
Istruzione di controllo for
Istruzioni continue, break e goto
Istruzione di controllo switch ... case
Array
Cos' un array ?
Definizione e inizializzazione di un array
L'operatore [ ]
Array multidimensionali
L'operatore sizeof e gli array
Gli array in C++
Stringhe di caratteri

Le stringhe come particolari array di caratteri


Definizione di variabili stringa
Inizializzazione di variabili stringa
Funzioni di libreria gets e puts
Conversioni fra stringhe e numeri
Le stringhe in C++
Funzioni
Definizione di una funzione
Dichiarazione di una funzione
Istruzione return
Comunicazioni fra programma chiamante e funzione
Argomenti di default
Funzioni con overload
Funzioni inline
Trasmissione dei parametri tramite l'area stack
Ricorsivit delle funzioni
Funzioni con numero variabile di argomenti
Cenni sulla Run Time Library
Riferimenti
Costruzione di una variabile mediante copia
Cosa sono i riferimenti ?
Comunicazione per "riferimento" fra programma e funzione
Direttive al Preprocessore
Cos' il preprocessore ?
Direttiva #include
Direttiva #define di una costante
Confronto fra la direttiva #define e lo specificatore const
Direttiva #define di una macro
Confronto fra la direttiva #define e lo specificatore inline
Direttive condizionali
Direttiva #undef
Sviluppo delle applicazioni in ambiente Windows
Definizioni di IDE e di "progetto"
Gestione di files e progetti
Editor di testo
Gestione delle finestre
Costruzione dell'applicazione eseguibile
Debug del programma
Utilizzo dell'help in linea
Indirizzi e Puntatori
Operatore di indirizzo &
Cosa sono i puntatori ?
Dichiarazione di una variabile di tipo puntatore
Assegnazione di un valore a un puntatore
Aritmetica dei puntatori
Operatore di dereferenziazione *
Puntatori a void
Errori di dangling references
Funzioni con argomenti puntatori
Puntatori ed Array
Analogia fra puntatori ed array
Combinazione fra operazioni di deref. e di incremento

Confronto fra operatore [ ] e deref. del puntatore "offsettato"


Funzioni con argomenti array
Funzioni con argomenti puntatori passati by reference
Array di puntatori
Elaborazione della riga di comando
Esecuzione di un programma tramite riga di comando
Argomenti passati alla funzione main
Puntatori e Funzioni
Funzioni che restituiscono puntatori
Puntatori a Funzione
Array di puntatori a funzione
Funzioni con argomenti puntatori a funzione
Puntatori e Costanti
Puntatori a costante
Puntatori costanti
Puntatori costanti a costante
Funzioni con argomenti costanti trasmessi by value
Funzioni con argomenti costanti trasmessi by reference
Tipi definiti dall'utente
Concetti di oggetto e istanza
Typedef
Strutture
Operatore .
Puntatori a strutture - Operatore ->
Unioni
Dichiarazione di strutture e membri di tipo struttura
Strutture di tipo bit field
Tipi enumerati
Allocazione dinamica della memoria
Memoria stack e memoria heap
Operatore new
Operatore delete
Namespace
Programmazione modulare e compilazione separata
Definizione di namespace
Risoluzione della visibilit
Membri di un namespace definiti esternamente
Namespace annidati
Namespace sinonimi
Namespace anonimi
Estendibilit della definizione di un namespace
Parola-chiave using
Precedenze e conflitti fra i nomi
Collegamento fra namespace definiti in files diversi
Eccezioni
Segnalazione e gestione degli errori
Il costrutto try
L'istruzione throw
Il gestore delle eccezioni: costrutto catch
Riconoscimento di un'eccezione fra diverse alternative
Blocchi innestati
Eccezioni che non sono errori

Classi e data hiding


Analogia fra classi e strutture
Specificatori di accesso
Data hiding
Funzioni membro
Risoluzione della visibilit
Funzioni-membro di sola lettura
Classi membro
Polimorfismo
Puntatore nascosto this
Membri a livello di classe e accesso "friend"
Membri di tipo enumerato
Dati-membro statici
Funzioni-membro statiche
Funzioni friend
Classi friend
Costruttori e distruttori degli oggetti
Costruzione e distruzione di un oggetto
Costruttori
Costruttori e conversione implicita
Distruttori
Oggetti allocati dinamicamente
Membri puntatori
Costruttori di copia
Liste di inizializzazione
Membri oggetto
Array di oggetti
Oggetti non locali
Oggetti temporanei
Utilit dei costruttori e distruttori
Overload degli operatori
Estendibilit del C++
Ridefinizione degli operatori
Metodi della classe o funzioni esterne ?
Il ruolo del puntatore nascosto this
Overload degli operatori di flusso di I/O
Operatori binari e conversioni
Operatori unari e casting a tipo nativo
Operatori in namespace
Oggetti-array e array associativi
Oggetti-funzione
Puntatori intelligenti
Operatore di assegnazione
Ottimizzazione delle copie
Espressioni-operazione
Eredita'
L'eredit in C++
Classi base e derivata
Accesso ai membri della classe base
Conversioni fra classi base e derivata
Costruzione della classe base
Regola della dominanza

Eredit e overload
La dichiarazione using
Eredit multipla e classi basi virtuali
Polimorfismo
Late binding e polimorfismo
Ambiguit dei puntatori alla classe base
Funzioni virtuali
Tabelle delle funzioni virtuali
Costruttori e distruttori virtuali
Scelta fra velocit e polimorfismo
Classi astratte
Un rudimentale sistema di figure geometriche
Un rudimentale sistema di visualizzazione delle figure
Template
Programmazione generica
Definizione di una classe template
Istanza di un template
Parametri di default
Funzioni template
Differenze fra funzioni e classi template
Template e modularit
Generalit sulla Libreria Standard del C++
Campi di applicazione
Header files
Il namespace std
La Standard Template Library
La Standard Template Library
Generalit
Iteratori
Contenitori Standard
Algoritmi e oggetti-funzione
Una classe C++ per le stringhe
La classe string
Confronto fra string e vector<char>
Il membro statico npos
Costruttori e operazioni di copia
Gestione degli errori
Conversioni fra oggetti string e stringhe del C
Confronti fra stringhe
Concatenazioni e inserimenti
Ricerca di sotto-stringhe
Estrazione e sostituzione di sotto-stringhe
Operazioni di input-output
Librerie statiche e dinamiche in Linux
Introduzione
Librerie in ambiente Linux
Un programma di prova
Librerie statiche
Come costruire una libreria statica
Link con una libreria statica
I limiti del meccanismo del link statico
Librerie condivise

Come costruire una libreria condivisa


Link con una libreria condivisa
La variabile ambiente LD_LIBRARY_PATH
La flag -rpath
Che tipo di libreria sto usando?
Un aspetto positivo dell'utilizzo delle librerie condivise
Librerie statiche vs librerie condivise
Le operazioni di input-ouput in C++
La gerarchia di classi stream
Operazioni di output
Operazioni di input
Stato dell'oggetto stream e gestione degli errori
Formattazione e manipolatori di formato
Cenni sulla bufferizzazione
Conclusioni

INTRODUZIONE
Obiettivi e Prerequisiti

Obiettivi
Acquisire le conoscenze necessarie per lo sviluppo di applicazioni in linguaggio
C++, usando la tecnica della "Programmazione orientata a Oggetti" o OOP
(Object Oriented Programming").
Un linguaggio di programmazione ha due scopi principali:
1. Fornire i mezzi perch il programmatore possa specificare le azioni da
eseguire;
2. Fornire un insieme di concetti per pensare a quello che pu essere fatto.
Il primo scopo richiede che il linguaggio sia vicino alla macchina (il C fu progettato
con questo scopo); il secondo richiede che il linguaggio sia vicino al problema da
risolvere, in modo che i concetti necessari per la soluzione siano esprimibili
direttamente e in forma concisa. La OOP stata appositamente pensata per
questo scopo e le potenzialit aggiunte al C per creare il C++ ne costituiscono
l'aspetto principale e caratterizzante.

Prerequisiti
Conoscenza dei concetti base e della terminologia informatica (es. : linguaggio,
programma, istruzione di programma, costante, variabile, funzione, operatore,
locazione di memoria, codice sorgente, codice oggetto, compilatore, linker ecc)
Non necessaria la conoscenza del C ! Infatti il C++ anche (ma non solo)
un'estensione del C, che mantiene nel suo ambito come sottoinsieme. E quindi un
corso, base ed essenziale ma completo, di C++, anche un corso di C.

Contenuto generale del Corso

Livello di partenza
Concetti fondamentali di programmazione in C e C++, tenendo presente gli
obiettivi, e quindi:

il C verr trattato solo negli aspetti che si mantengono inalterati in C++


Esempi: le istruzioni di controllo if .. else, i costrutti while, do...while,
for ecc...
il C non verr trattato dove stato sostituito dal C++
Esempio : le funzioni C di allocazione di memoria malloc e free, sostituiti
in C++ dagli operatori new e delete
il Corso riguarder soltanto il C++ standard, indipendentemente dalla
piattaforma hardware o dal sistema operativo, e quindi non verranno
trattati tutti quegli argomenti legati all'ambiente specifico

Livello avanzato
La Programmazione a Oggetti: concetti di: tipo astratto, classe, istanza,
incapsulamento, overload di funzioni e operatori, costruttore e distruttore,
ereditariet, polimorfismo, funzione virtuale, template ecc...
La libreria standard del C++ : classi iostream (per l'input-output) e string, classi
contenitore (vector, list, queue, stack, map ecc. ), algoritmi e iteratori.

Nota Storica

Il C++ fu "inventato" nel 1980 dal ricercatore informatico danese Bjarne Stroustrup, che
ricav concetti gi presenti in precedenti linguaggi (come il Simula67) per produrre una
verisone modificata del C, che chiam: "C con le classi". Il nuovo linguaggio univa la potenza e
l'efficienza del C con la novit concettuale della programmazione a oggetti, allora ancora in
stato "embrionale" (c'erano gi le classi e l'eredit, ma mancavano l'overload, le funzioni
virtuali, i riferimenti, i template, la libreria e moltre altre cose).
Il nome C++ fu introdotto per la prima volta nel 1983, per suggerire la sua natura evolutiva dal
C, nel quale ++ l'operatore di incremento (taluni volevano chiamarlo D, ma C++ prevalse, per
i motivi detti).
All'inizio, comunque, e per vari anni, il C++ rest un esercizio quasi "privato" dell'Autore e dei
suoi collaboratori, progettato e portato avanti, come egli stesso disse, "per rendere pi facile e
piacevole la scrittura di buoni programmi".
Tuttavia, alla fine degli anni 80, risult chiaro che sempre pi persone apprezzavano ed
utilizzavano il linguaggio e che la sua standardizzazione formale era un obiettivo da perseguire.
Nel 1990 si form un comitato per la standardizzazione del C++, cui ovviamente partecip lo
stesso Autore. Da allora in poi, il comitato, nelle sue varie articolazioni, divenne il luogo
deputato all'evoluzione e al raffinamento del linguaggio.
Finalmente l'approvazione formale dello standard si ebbe alla fine del 1997. In questi ultimi
anni il C++ si ulteriormente evoluto, soprattutto per quello che riguarda l'implementazione di
nuove classi nella libreria standard.

Caratteristiche generali del linguaggio


"Case sensitivity"

Il linguaggio C++ (come il C) distingue i caratteri maiuscoli da quelli minuscoli.


Esempio: i nomi MiaVariabile e miavariabile indicano due variabili diverse

Moduli funzione

In C++ (come in C) ogni modulo di programma una funzione.


Non esistono subroutines o altri tipi di sottoprogramma.
Ogni funzione identificata da un nome

Entry point del programma: la funzione main

Quando si manda in esecuzione un programma, questo inizia sempre dalla


funzione identificata dalla parola chiave main
Il main chiamato dal sistema operativo, che gli pu passare dei parametri; a
sua volta il main pu restituire al sistema un numero intero (di solito analizzato
come possibile codice di errore).

Le tre parti di una funzione

lista degli argomenti passati dal programma chiamante: vanno indicati fra parentesi
tonde dopo il nome della funzione; void indica che non vi sono argomenti (si pu

omettere)

blocco (ambito di azione, ambito di visibilit, scope) delle istruzioni della funzione:
va racchiuso fra parentesi graffe;
ogni istruzione deve terminare con ";" (pu estendersi su pi righe o vi possono essere
pi istruzioni sulla stessa riga);
un'istruzione costituita da una successione di "tokens": un "token" il pi piccolo
elemento di codice individualmente riconosciuto dal compilatore; sono "tokens" : gli
identificatori, le parole-chiave, le costanti letterali o numeriche, gli operatori e
alcuni caratteri di punteggiatura;
i blanks e gli altri caratteri "separatori" (horizontal or vertical tabs, new lines, formfeeds)
fra un token e l'altro o fra un'istruzione e l'altra, sono ignorati; in assenza di "separatori"
il compilatore analizza l'istruzione da sinistra a destra e tende, nei casi di ambiguit, a
separare il token pi lungo possibile.
Es.
l'istruzione
a = i+++j;
pu essere interpretata come:
a = i + ++j;
oppure come: a = i++ + j;
il compilatore sceglie la seconda interpretazione.

tipo del valore di ritorno al programma chiamante: va indicato prima del nome della
funzione ed obbligatorio; se void indica che non c' valore di ritorno

Commenti

I commenti sono brani di programma (che il compilatore ignora) inseriti al solo


scopo di documentazione, cio per spiegare il significato delle istruzioni e cos
migliorare la leggibilit del programma. Sono molto utili anche allo stesso autore,
per ricordargli quello che ha fatto, quando ha necessit di rivisitare il programma
per esigenze di manutenzione o di aggiornamento. Un buon programma si
caratterizza anche per il fatto che fa abbondante uso di commenti.
In C++ ci sono due modi possibili di inserire i commenti:

l'area di commento introdotta dal doppio carattere /* e termina con il doppio carattere
*/ (pu anche estendersi su pi righe)
l'area di commento inizia con il doppio carattere // e termina alla fine della riga

Esempio di programma

Cominciamo dalla funzione "printf"


Perch una funzione di input-output del C ?

La funzione printf importante perch utilizza gli specificatori di formato, che


definiscono il modo di scrivere i dati (formattazione). Tali specificatori sono
usati, con le stesse regole della printf, da tutte le funzioni (anche non di inputoutput), che eseguono conversioni di formato sui dati

Operazioni della funzione printf

La funzione printf formatta e scrive una serie di caratteri e valori sul dispositivo
standard di output (stdout), associato di default allo schermo del video, e
restituisce al programma chiamante il numero di caratteri effettivamente scritti
(oppure un numero negativo in caso di errore).
Quando si usa la funzione printf bisogna prima includere il file header
<stdio.h>

Argomenti della funzione printf

La funzione printf riceve dal programma chiamante uno o pi argomenti. Solo il


primo obbligatorio e deve essere una stringa, che si chiama control string
(stringa di controllo)

Scrittura della control string sullo schermo

Quando printf chiamata con un solo argomento, la control string viene


trasferita sullo schermo, carattere per carattere (compresi gli spazi bianchi), salvo
quando sono incontrati i seguenti caratteri particolari:
"

(termina la control string)

(introduce uno specificatore di formato - da non usare in questo caso)

(introduce una sequenza di escape)

Sequenze di escape

Il carattere \ (backslash) non viene trasferito sullo schermo, ma utilizzato in


combinazione con i caratteri successivi (un solo carattere se si tratta di una
lettera, oppure una sequenza di cifre numeriche); l'insieme viene detto: escape
sequence, e viene interpretato come un unico carattere.
Le sequenze di escape sono usate tipicamente per specificare caratteri speciali
che non hanno il loro equivalente stampabile (come newline, carriage return,
tabulazioni, suoni ecc...), oppure caratteri, che da soli, hanno una funzione
speciale, come le virgolette o lo stesso backslash

Principali sequenze di escape

\a

suona il campanello (bell)

\b

carattere backspace

\f

salta pagina (form-feed)

\n

va a capo (newline)

\t

tabulazione orizzontale

\"

carattere virgolette

\r
\\

ritorno carrello (carriage-

return)

carattere backslash

\nnn carattere con codice ascii nnn (tre cifre in ottale)


\nn

carattere con codice ascii nn (due cifre in esadecimale)

%% carattere "%" - atipico: introdotto da % anzich da \


\

da solo alla fine della riga = continua la control string nella riga successiva

La funzione printf con pi argomenti

Eventuali altri argomenti successivi alla control string, nella chiamata a printf,
rappresentano i dati da formattare e scrivere, e possono essere costituiti da
costanti, variabili, espressioni, o altre funzioni (in questo caso in realt
l'argomento il valore di ritorno della funzione, la quale viene eseguita prima
della printf). Per il momento, dato che le variabili non sono state ancora
introdotte, supponiamo che i dati siano costituiti da costanti o da espressioni
fra costanti

Specificatori di formato

Ad ogni argomento successivo alla control string, deve corrispondere, all'interno


della stessa control string e nello stesso ordine, uno specificatore di formato,
costituito da un gruppo di caratteri introdotto dal carattere "%". Nella sua forma
generale uno specificatore di formato ha la seguente sintassi:
%[flags][width][.precision]type
dove i termini indicati con il colore fuchsia costituiscono i campi dello
specificatore (senza spazi in mezzo), e sono tutti opzionali salvo l'ultimo (type),
che determina come deve essere interpretato il corrispondente argomento della
printf (numero intero, numero floating, carattere, stringa ecc...). I campi
opzionali controllano invece il formato di scrittura. Se sono omessi, cio lo
specificatore assume la forma minima %type, i dati sono scritti in free-format
(cio in modo da occupare lo spazio strettamente necessario). Per esempio, se a
un certo punto della control string compare lo specificatore %d, significa che in
quella posizione deve essere scritto, in free-format, il valore del corrispondente
argomento della printf, espresso come numero intero decimale, come nel
caso della seguente istruzione:
printf("Ci sono %d iscritti a questo corso!\nTemevo fossero solo
%d!",3215+1,2);
che scrive su video la frase:
Ci sono 3216 iscritti a questo corso!
Temevo fossero solo 2!

Principali specificatori di formato in free-format

In uno specificatore di formato il campo obbligatorio type pu assumere uno


dei seguenti valori:
u, o, x

valori interi assoluti, basi: decimale, ottale,


esadecimale

come x ma con le cifre letterali maiuscole

d, i

valori interi relativi, base decimale

f, e

valori floating, notazione normale o esponenziale

come f o e (sceglie il pi comodo)

E, G

come e e g (scrive "E" al posto di "e")

carattere

stringa di caratteri

indirizzo di memoria (in esadecimale)


[p02]

Specificatori di formato con ampiezza di campo e precisione

Anzich in free-format, si possono scrivere i dati in formato definito, tramite gli


specificatori numerici di ampiezza di campo e precisione.
In uno specificatore di formato il campo opzionale width, costituito da un
numero intero positivo, determina l'ampiezza di campo, cio il numero di
caratteri minimo con cui deve essere scritto il dato corrispondente. Se il numero di
caratteri effettivo inferiore, il campo viene riempito (normalmente) a sinistra con
spazi bianchi; se invece il numero superiore, il campo viene espanso fino a
raggiungere la lunghezza effettiva (in altre parole il dato viene sempre scritto per
intero, anche se il valore specificato in width insufficiente). Se al posto di un
numero si specifica nel campo width un asterisco, il valore viene desunto in
esecuzione dalla lista degli argomenti della printf; in questo caso il valore
dell'ampiezza di campo deve precedere immediatamente il dato a cui lo
specificatore in esame si riferisce.
In uno specificatore di formato il campo opzionale precision, se presente,
deve essere sempre preceduto da un punto (che lo separa dal campo width), ed

costituito da un numero intero non negativo, con significato che dipende dal
contenuto del campo obbligatorio type, come si evince dalla seguente tabella:
contenuto
campo type

significato campo precision

default

d,i,u,o,x,X
(valori interi)

La precisione specifica il minimo


numero di cifre che devono essere
scritte. Se il numero di cifre effettive
del dato corrispondente minore
della precisione, vengono scritti
degli zeri sulla sinistra fino a
completare il campo. Se invece il
numero di cifre effettive superiore,
il dato comunque scritto per intero
senza nessun troncamento. Infine, se
la precisione .0 (oppure
semplicemente .) e il dato zero,
non viene scritto nulla.

f,e,E
(valori
floating)

La precisione specifica il numero di


cifre che devono essere scritte dopo il
punto decimale. L'ultima cifra
arrotondata. Se la precisione .0
(oppure semplicemente .), non
scritto neppure il punto decimale (in
questo caso arrotondata la cifra
intera delle unit).

6 cifre decimali

g,G
(valori
floating)

La precisione specifica il massimo


numero di cifre significative che
devono essere scritte. L'ultima cifra
arrotondata. Gli zeri non significativi a
destra non vengono scritti.

6 cifre significative

c
(carattere)

La precisione non ha effetto.

s
(stringa)

La precisione specifica il massimo


numero di caratteri che devono
essere scritti. I caratteri in eccesso
non vengono scritti.

La stringa scritta per


intero

Come per l'ampiezza di campo, anche per la precisione, se al posto di un


numero si specifica un asterisco, il valore viene desunto in esecuzione dalla lista
degli argomenti della printf; anche in questo caso il valore della precisione deve
precedere immediatamente il dato a cui lo specificatore in esame si riferisce.

Altri campi degli specificatori di formato

In uno specificatore di formato il campo opzionale flags costituito da uno o


pi caratteri, ciascuno dei quali svolge una funzione particolare, come descritto
dalla seguente tabella:
flag

significato

default

Allinea la scrittura a sinistra in un campo con


ampiezza specificata da width.

Allineamento a destra

Mette il segno (+ o ) davanti al numero.

Il segno appare solo se il


numero negativo

spazio

Mette uno spazio bianco davanti al numero,


se questo positivo.

Nessuno spazio davanti


ai numeri positivi

Aggiunge zeri sulla sinistra fino a raggiungere


l'ampiezza specificata da width. Se appaiono
insieme 0 e -, 0 ignorato.

Il campo specificato da
width riempito da
spazi bianchi

# (usato
con
o,x,X)

Mette davanti a ogni valore diverso da zero i


prefissi 0, 0x, o 0X, rispettivamente.

Nessun prefisso davanti


ai numeri

# (usato
con
e,E,f,g,G)

Scrive sempre il punto decimale.

Il punto decimale
scritto solo se seguito
da altre cifre

# (usato
con g,G)

Riempie tutto il campo con ampiezza


specificata da width, scrivendo anche gli zeri
non significativi.

Gli zeri non significativi


non vengono scritti

Tipi, Variabili, Costanti


Tipi delle variabili

Classificazione delle variabili in tipi


Si dice che il C++ (come il C) un linguaggio "tipato", per il fatto che pretende
che di ogni variabile venga dichiarato il tipo di appartenenza.
Definizione di tipo di una variabile
Il tipo un termine di classificazione che raggruppa tutte quelle variabili che sono
memorizzate nello stesso modo e a cui si applica lo stesso insieme di operazioni.
Controllo forte sui tipi
Il C++ esercita un forte controllo sui tipi (strong type checking), nel senso che
regola e limita la conversione da un tipo all'altro (casting) e controlla
l'interazione fra variabili di tipo diverso.

Tipi intrinseci del linguaggio

In C++ esistono solo 5 tipi, detti "intrinseci o "nativi" del linguaggio :


int

numero intero di 2 o 4 byte

char

numero intero di 1 byte (interpretabile come codice ascii di un


carattere)

float

numero in virgola mobile con 6-7 cifre significative (4 byte )

double

numero in virgola mobile con 15-16 cifre significative (8 byte )

bool

valore booleano: true o false (1 byte )

In realt il numero di tipi possibili molto pi grande, sia perch ogni tipo nativo
pu essere specializzato mediante i qualificatori di tipo, sia perch il
programma stesso pu creare propri tipi personalizzati (detti "tipi astratti")

Dichiarazione e definizione degli identificatori

Cos' un identificatore ?
Un identificatore un nome simbolico che il programma assegna a un'entit del
linguaggio, per modo che il compilatore sia in grado di riconoscere quell'entit
ogni volta che incontra il nome che le stato assegnato.
Sono pertanto identificatori i nomi delle variabili, delle funzioni, degli array,
dei tipi astratti, delle strutture, delle classi ecc...
Ogni identificatore consiste di una sequenza di lettere (maiuscole o minuscole)
e di cifre numeriche, senza caratteri di altro tipo o spazi bianchi (a parte
l'underscore "_", che considerato una lettera). Il primo carattere deve essere
una lettera.
Non sono validi gli identificatori che coincidono con le parole-chiave del
linguaggio (come da Tabella sotto riportata).
Esempi di identificatori validi:
hello
deep_space9
a123
_7bello
Esempi di identificatori non validi:
un amico

(contiene uno spazio)

un'amica

(contiene un apostrofo)

7bello

(il primo carattere non una lettera)

for

( una parola-chiave del C++)

Tabella delle parole-chiave del C++


auto

bool

break

case

catch

char

class

const

const_class

continue

default

delete

do

double

dynamic_cast

else

enum

explicit

extern

false

float

for

friend

goto

if

inline

int

long

main

mutable

namespace

new

operator

private

protected

public

register

reinterpret_class

return

short

signed

sizeof

static

static_cast

struct

switch

template

this

throw

true

try

typedef

typeid

typename

union

unsigned

using

virtual

void

volatile

wmain

while

Dichiarazione obbligatoria degli identificatori


In C++ tutti gli identificatori di un programma devono essere dichiarati prima
di essere utilizzati (non necessariamente all'inizio del programma), cio deve
essere specificato il loro tipo. Per dichiarare un identificatore bisogna scrivere
un'istruzione apposita in cui l'identificatore preceduto dal tipo di
appartenenza. Es.
int Variabile_Intera;
Pi identificatori dello stesso tipo possono essere dichiarati nella stessa
istruzione e separati l'uno dall'altro da una virgola. Es.
int ore, giorni, mesi;

Definizione obbligatoria degli identificatori


Un'istruzione di dichiarazione si limita ad informare il compilatore del C++ che
un certo identificatore appartiene a un certo tipo, ma pu non essere
considerata in fase di esecuzione del programma. Quando una dichiarazione
comporta anche un'operazione eseguibile, allora si dice che anche una
definizione.
Per esempio, l'istruzione:
extern int error_number;
soltanto una dichiarazione, in quanto (come vedremo pi avanti) con lo
specificatore extern informa il compilatore (o meglio il linker) che la variabile
error_number definita in un altro file del programma (e quindi l'istruzione
serve solo ad identificare il tipo della variabile e a permetterne l'utilizzo);
mentre l'istruzione:
int error_number;
anche una definizione, in quanto non si limita ad informare il compilatore che
la variabile error_number di tipo int, ma crea la variabile stessa, allocando
un'apposita area di memoria.
Per meglio comprendere la differenza fra dichiarazione e definizione, si
considerino le seguenti regole:

tutte le definizioni sono anche dichiarazioni (ma non vero il


contrario);
deve esserci una ed una sola definizione per ogni identificatore che
appare nel programma (o meglio, per ogni identificatore che appare in
uno stesso ambito, altrimenti si tratta di identificatori diversi, pur
avendo lo stesso nome), mentre possono esserci pi dichiarazioni
(purch non in contraddizione fra loro);

un identificatore deve essere dichiarato prima del suo utilizzo, ma pu


essere definito dopo (o altrove, come abbiamo visto nell'esempio
precedente);
la semplice dichiarazione (cio senza specificatore) di una variabile di
tipo nativo sempre anche una definizione, in quanto comporta
l'allocazione di un'area di memoria;

Qualificatori e specificatori di tipo

Definizione di "qualificatore" e "specificatore"


Un qualificatore di tipo una parola-chiave che, in una istruzione di
dichiarazione, si premette a un tipo nativo, per indicare il modo in cui la
variabile dichiarata deve essere immagazzinata in memoria. Se il tipo omesso,
sottointeso int.
Esistono 4 qualificatori: short, long, signed, unsigned.
Uno specificatore una parola-chiave che, in una istruzione di dichiarazione,
si premette al tipo (che pu essere qualsiasi, anche non nativo) e all'eventuale
qualificatore, per definire ulteriori caratteristiche dell'entit dichiarata. Esistono
svariati tipi di specificatori, con funzioni diverse: li introdurremo via via durante
il corso, quando sar necessario.
Qualificatori short e long
I qualificatori short e long si applicano al tipo int. Essi definiscono la
dimensione della memoria occupata dalle rispettive variabili di appartenenza.
Purtroppo lo standard non garantisce che tale dimensione rimanga inalterata
trasportando il programma da una piattaforma all'altra, in quanto essa dipende
esclusivamente dalla piattaforma utilizzata. Possiamo solo dire cos: a tutt'oggi,
nelle implementazioni pi diffuse del C++ , il qualificatore short definisce
variabili di 16 bit (2 byte) e il qualificatore long definisce variabili di 32 bit (4
byte), mentre il tipo int "puro" definisce variabili di 32 bit (cio long e int sono
equivalenti).
Vedremo fra poco che esiste un operatore che permette di conoscere la effettiva
occupazione di memoria dei diversi tipi di variabili.
Per completezza aggiungiamo che il qualificatore long si pu applicare anche al
tipo double (la cosidetta "precisione estesa"), ma, da prove fatte sulle
macchine che generalmente usiamo, risultato che conviene non applicarlo!
Qualificatori signed e unsigned
I qualificatori signed e unsigned si applicano ai tipi "interi" int e char. Essi
determinano se le rispettive variabili di appartenenza possono assumere o meno
valori negativi.

E' noto che i numeri interi negativi sono rappresentati in memoria mediante
l'algoritmo del "complemento a 2" (dato un numero N rappresentato da una
sequenza di bit, -N si rappresenta invertendo tutti i bit e aggiungendo 1). E' pure
noto che, in un'area di memoria di m bit, esistono 2m diverse possibili
configurazioni (cio un numero intero pu assumere 2m valori). Pertanto un
numero con segno ha un range (intervallo) di variabilit da -2m-1 a +2m-1-1,
mentre un numero assoluto va da 0 a +2m-1.
Se il tipo int, i qualificatori signed e unsigned possono essere combinati
con short e long, dando luogo, insieme a signed char e unsigned char, a 6
diversi tipi interi possibili.
E i tipi int e char "puri" ? Il tipo int sempre con segno (e quindi signed int e
int sono equivalenti), mentre, per quello che riguarda il tipo char, ancora una
volta dipende dall'implementazione: "in generale" (ma non sempre) coincide con
signed char.

L'operatore sizeof

L'operatore sizeof(operando) restituisce la lunghezza in byte di identificatori


appartenenti a un dato tipo; operando specifica il tipo in esame o un qualunque
identificatore dichiarato di quel tipo. Per esempio, sizeof(int) pu essere usato
per sapere se il tipo int di 2 o di 4 byte.
[p04]
Confronto dei risultati fra diverse architetture

Lunghezza della voce di memoria in byte


tipo
char

PC (32 bit)

PC (32 bit) DEC ALPHA (64 bit)

con Windows con Linux


1

con Unix
1

short

int

long

float

double

long double

12

16

bool

Definizione con Inizializzazione

Abbiamo visto finora che ogni dichiarazione o definizione di un identificatore


consiste di tre parti:

uno o pi specificatori (opzionali);


il tipo (eventualmente preceduto da uno o pi qualificatori);
l'identificatore.

NOTA
Per completezza aggiungiamo che a sua volta l'identificatore pu essere
preceduto (e/o seguito) da un "operatore di dichiarazione".
I pi comuni operatori di dichiarazione sono:
*

puntatore

prefisso

*const

puntatore costante

prefisso

&

riferimento

prefisso

[]

array

suffisso

()

funzione

suffisso

Ne parleremo al momento opportuno.


Esiste una quarta parte, opzionale, che si chiama inizializzatore (e che si pu
aggiungere solo nel caso della definizione di una variabile): un inizializzatore
un'espressione che definisce il valore iniziale assunto dalla variabile, ed
separato dal resto della definizione dall'operatore "=".
Quindi, ricapitolando (nel caso che l'identificatore sia il nome di una variabile):

la semplice dichiarazione assegna un tipo alla variabile;

la definizione crea la variabile in memoria, ma non il suo contenuto, che


rimane, per il momento, indefinito (forse resta quello che c'era prima nella
stessa locazione fisica di memoria);
la definizione con inizializzazione attribuisce un valore iniziale alla
variabile definita.
Es. unsigned peso = 57;

n.b. un'inizializzazione concettualmente diversa da un'assegnazione


In C++ i valori di inizializzazione possono essere dati non solo da costanti, ma
anche da espressioni che includono variabili definite precedentemente.
Es.

int lordo = 45;

int tara = 23;

int netto = lordo-tara;

Il tipo "booleano"

Il tipo bool non faceva parte inizialmente dei tipi nativi del C e solo
recentemente stato introdotto nello standard del C++.
Una variabile "booleana" (cio dichiarata bool) pu assumere solo due valori:
true e false. Tuttavia, dal punto di vista dell'occupazione di memoria, il tipo
bool identico al tipo char, cio occupa un intero byte (anche se in pratica
utilizza un solo bit).
Nelle espressioni aritmetiche e logiche valori booleani e interi possono essere
mescolati insieme: se un booleano convertito in un intero, per definizione
true corrisponde al valore 1 e false corrisponde al valore 0; viceversa, se un
intero convertito in un booleano, tutti i valori diversi da zero diventano true
e zero diventa false. Esempi:
bool b = 7;

( b inizializzata con true )

int i

= true;

( i inizializzata con 1 )

int i

= 7 < 2;

( espressione falsa: i inizializzata con 0 )

Le Costanti in C++

Costanti intere

Una costante intera un numero decimale (base 10), ottale (base 8) o


esadecimale (base 16) che rappresenta un valore intero positivo o negativo. Un
numero senza prefissi o suffissi interpretato in base decimale e di tipo int (o
unsigned int se la costante specificata maggiore del massimo numero positivo
signed int). La prima cifra del numero non deve essere 0.
Un numero con prefisso 0 interpretato in base ottale e di tipo int (o
unsigned int).
Es.
a = 0100; (in a memorizzato il numero 64)
Un numero con prefisso 0x o 0X interpretato in base esadecimale e di tipo
tipo int (o unsigned int). Le "cifre" a,b,c,d,e,f possono essere scritte sia in
maiuscolo che in minuscolo.
Es.
a = 0x1B; (in a memorizzato il numero 27)
In qualunque caso, la presenza del suffisso L indica che il numero deve essere di
tipo long int, mentre la presenza del suffisso U indica che il numero deve essere
di tipo unsigned int.
Es.
a = 0x1BL;
a = 0x1BU;
Costanti in virgola mobile
Una costante in virgola mobile un numero decimale (base 10), che
rappresenta un valore reale positivo o negativo.
Pu essere specificato in 2 modi:

parte_intera.parte_decimale (il punto obbligatorio)


notazione esponenziale (il punto non obbligatorio)

Esempi:

15.75

-1.5e2

25E-4

10.

In qualunque notazione, se il numero scritto senza suffissi, assunto di tipo


double. Per forzare il tipo float bisogna apporre il suffisso f (o F)
Es.
10.3 di tipo double
1.4e-5f di tipo float
Costanti carattere
Una costante carattere rappresentata inserendo fra singoli apici un carattere
stampabile oppure una sequenza di escape.
Esempi:

'A'

carattere A

'\n'

carattere newline

'\003'

carattere cuoricino

In memoria un carattere rappresentato da un numero intero di 1 byte (il suo


codice ascii). Le conversioni fra tipo char e tipo int sono automatiche (purch il
valore intero da convertire sia compreso nel range del tipo char) e quindi i due
tipi possono essere mescolati insieme nelle espressioni aritmetiche.
Per esempio, l'operazione:
MiaVar = 'A' + 1;
ammessa, se la variabile MiaVar stata dichiarata int oppure char.
Il carattere NULL ha codice ascii 0 e si rappresenta con '\0' (da non
confondere con il carattere 0 decimale che ha codice ascii 48).

Costanti stringa
Una costante stringa rappresentata inserendo un insieme di caratteri (fra cui
anche sequenze di escape) fra doppi apici (virgolette).
Es.
"Ciao Universo\n"
In C++ (come in C) non esistono le stringhe come tipo intrinseco. Infatti esse
sono definite come sequenze (array) di caratteri, con una differenza rispetto ai
normali array: il compilatore, nel creare una costante stringa, aggiunge
automaticamente un NULL dopo l'ultimo carattere (si dice che le stringhe sono
"array di caratteri null terminated"). E quindi, per esempio, 'A' e "A" sono
due costanti diverse :
'A'

un carattere e occupa 1 byte (con il numero 65)

"A"

una stringa e occupa 2 byte (con i numeri 65 e 0)

Per inizializzare una stringa bisogna definirla di tipo char e aggiungere al


nome della variabile l'operatore di dichiarazione [].
Es.
char MiaStr[] = "Sono una stringa";

Specificatore const

Se nella definizione di una variabile, si premette al tipo (e ai suoi eventuali


qualificatori), lo specificatore const, il contenuto della variabile non pu pi
essere modificato. Ovviamente una variabile definita const deve sempre essere
inizializzata.
Es.

const double pigreco = 3.14159265385;

L'uso di const fortemente consigliato rispetto all'alternativa di scrivere pi volte


la stessa costante nelle istruzioni del programma; infatti se il programmatore
decide di cambiarne il valore, e ha usato const, sufficiente che modifichi la sola
istruzione di definizione.
D'ora in poi, quando parleremo di "costanti", intenderemo riferirci a "variabili
definite const" (distinguendole dalle costanti "dirette" che saranno invece
chiamate "costanti letterali" o "literals").

Visibilit e tempo di vita


Visibilit di una variabile

Ambito di azione
Abbiamo visto che, in via del tutto generale, si definisce ambito di azione (o
ambito di visibilit o scope) l'insieme di istruzioni di programma comprese fra
due parentesi graffe: {....}.
Le istruzioni di una funzione devono essere comprese tutte nello stesso ambito;
ci non esclude che si possano definire pi ambiti innestati l'uno dentro l'altro
(ovviamente il numero di parentesi chiuse deve bilanciare quello di parentesi
aperte, e ogni parentesi chiusa termina l'ambito iniziato con la parentesi aperta
pi interna).

Variabili locali
In ogni caso una variabile visibile al programma e utilizzabile solo nello stesso
ambito in cui definita (variabili locali). Se si tenta di utilizzare una variabile in
ambiti diversi da quello in cui definita (o in ambiti superiori in caso di pi
ambiti innestati), il compilatore non la riconosce.
Il C++ ammette che si ridefinisca pi volte la stessa variabile, purch in ambiti
diversi; in questo caso riconosce la variabile definita nel proprio ambito o in
quello superiore pi vicino.

Variabili globali
Una variabile globale, cio visibile in tutto il programma, solo se definita
al di fuori di qualunque ambito (che viene per questo definito: ambito globale).
Le definizioni (con eventuali inizializzazioni) sono le uniche istruzioni del
linguaggio che possono anche risiedere esternamente all'ambito delle funzioni.
In caso di concorrenza fra una variabile globale e una locale viene riconosciuta
la variabile locale; tuttavia la variabile globale prevale se specificata con
prefisso :: (operatore di riferimento globale).

Tempo di vita di una variabile

Variabili automatiche
Una variabile detta automatica (o dinamica), se cessa di esistere non appena
il flusso del programma esce dalla funzione in cui la variabile definita. Se il
flusso del programma torna nella funzione, la variabile viene ricreata ex-novo e, in
particolare, viene reinizializzata sempre con lo stesso valore. Tutte le variabili
locali sono, per default, automatiche ("tempo di vita" limitato all'esecuzione
della funzione).

Variabili statiche
Una variabile detta statica se il suo "tempo di vita" coincide con l'intera
durata del programma: quando il flusso del programma torna nella funzione in
cui definita una variabile statica, ritrova la variabile come l'aveva lasciata (cio
con lo stesso valore); ci significa in particolare che l'istruzione di definizione
(con eventuale annessa inizializzazione) viene eseguita solo la prima volta. Per
ottenere che una variabile sia statica, bisogna preporre lo specificatore static
nella definizione della variabile.
Esiste anche, per le variabili automatiche, lo specificatore auto, ma inutile
in quanto di default (pu essere usato per migliorare la leggibilit del
programma).
A differenza dalle variabili automatiche, (in cui, in assenza di inizializzatore, il
contenuto iniziale indefinito), le variabile statiche sono inizializzate di default a
zero (in modo appropriato al tipo).

Visibilit globale

Variabili globali statiche


Una variabile locale pu essere automatica o statica; una variabile globale
sempre statica (se visibile dall'esterno deve essere anche viva!) e quindi lo
specificatore static non avrebbe significato.
In realt, nella definizione di una variabile globale, lo specificatore static ha
un significato differente: quello di limitare la visibilit della variabile al solo file in
cui definita (file scope). Senza lo specificatore static, la variabile visibile
anche negli altri files, purch in essi venga dichiarata con lo specificatore
extern.

Visibilit di variabili globali


Se una variabile globale, visibile in tutti i files sorgente del programma (cio
definita senza lo specificatore static), non inizializzata, deve esistere una
e una sola dichiarazione senza lo specificatore extern, altrimenti il linker
darebbe errore, con messaggio "unresolved symbol" (se tutte le dichiarazioni
hanno extern), oppure "one or more multiply defined symbols" (se ci sono
due dichiarazioni senza extern); se invece la variabile inizializzata,
l'inizializzazione deve essere presente in un solo file (in questo caso lo
specificatore extern opzionale), mentre negli altri files la variabile deve
essere dichiarata con extern e non deve essere inizializzata.

Visibilit di costanti globali


In C++ le costanti globali (cio le variabili globali definite const, con
inizializzazione obbligatoria), obbediscono a regole differenti e precisamente:

di default le costanti globali hanno file scope;


affinch una costante globale sia visibile dappertutto, necessaria la
presenza dello specificatore extern anche nella dichiarazione in cui la
costante inizializzata (ovviamente, come per le variabili,
l'inizializzazione deve essere presente una sola volta).

Tabella riassuntiva
Visibilit globale
specificatore extern specificatore extern
nel file di definizione
negli altri files
Variabile globale senza
inizializzazione
Variabile globale con
inizializzazione
Costante globale

vietato
opzionale
obbligatorio

obbligatorio

File scope

specificatore static

obbligatorio
specificatore static
senza inizializzazione
obbligatorio
senza inizializzazione

default

Operatori e operandi
Definizione di operatore e regole generali

Un operatore un token che agisce su una coppia di dati (o su un singolo


dato), detti operandi, ottenendo un nuovo dato (risultato dell'operazione).
Ogni operatore identificato da un particolare simbolo grafico, costituito di solito
da un solo carattere (ma talvolta anche da due o pi caratteri). Non tutti gli
operatori possono applicarsi ad ogni tipo di operando, ma, per ogni operatore
esiste un ben definito insieme di tipi di operandi a cui l'operatore applicabile.
Un operatore detto binario se agisce su due operandi, unario se agisce su
un solo operando. Se l'operatore binario, i due operandi sono detti leftoperand e right-operand.
Un'espressione una successione di operazioni in cui il risultato di ogni
operazione diviene operando per le operazioni successive, fino a giungere ad
un unico risultato. L'ordine in cui le operazioni sono eseguite regolato secondo
precisi criteri di precedenza e associativit fra gli operatori.
Es.
a op1 b op2 c
operatori)

(a, b, c sono operandi, op1 e op2 sono

1. op1 ha la precedenza: il risultato di a op1 b diventa left-operand di


op2 c
2. op2 ha la precedenza: il risultato di b op2 c diventa right-operand di a
op1
3. op1 e op2 hanno la stessa precedenza, ma l'associativit procede da
sinistra a destra: come nel caso 1.
4. op1 e op2 hanno la stessa precedenza, ma l'associativit procede da
destra a sinistra: come nel caso 2.
Per ottenere che un'operazione venga comunque eseguita con precedenza
rispetto alle altre, bisogna racchiudere operatore e operandi fra parentesi
tonde.
Es.
a op1 ( b op2 c )
(la seconda operazione viene eseguita per
prima)

Operatore di assegnazione

L'operatore binario di assegnazione = copia il contenuto del right-operand


(detto nello specifico r-value) nel left-operand (detto l-value).
a=b
b (r-value) pu essere una qualunque espressione che restituisce un valore di
tipo nativo;
a (l-value) ha un ambito di scelta molto pi ristretto (tutti gli l-values possono
essere r-values, ma non viceversa); in pratica, salvo poche eccezioni, a deve
essere una variabile.
I tipi di a e b devono coincidere, oppure il tipo di b deve essere convertibile
implicitamente nel tipo di a.

Operatori matematici

Come in tutti i linguaggi di programmazione, le operazioni matematiche


fondamentali (addizione, sottrazione, moltiplicazione, divisione) sono
eseguite rispettivamente dai seguenti operatori binari:
+

Se la divisione fra due numeri interi, il risultato dell'operazione ancora un


numero intero (troncamento).
Es.
27 / 4
da' come risultato 6 (anzich 6.75).
Il resto di una divisione fra numeri interi si calcola con l'operatore binario
%
Es.
27 % 4
da' come risultato 3

Operatori a livello del bit

Il C++ pu, a differenza da altri linguaggi, operare sulle variabili intere a livello
del bit.

L'operatore binario >> produce lo scorrimento a destra (right-shift) dei bit


del left-operand, in quantit pari al right-operand. In pratica esegue una
divisione intera (con divisore uguale a una potenza di 2)
Es.
a >> n
equivale a
a / 2n
Dalla sinistra entrano cifre binarie 0 se il numero a positivo, oppure cifre binarie
1 se il numero a negativo (a causa della notazione a complemento a 2 dei
numeri negativi).

L'operatore binario << produce lo scorrimento a sinistra (left-shift) dei bit


del left-operand, in quantit pari al right-operand. In pratica esegue una
moltiplicazione per una potenza di 2
Es.
a << n
equivale a
a * 2n
Dalla destra entrano sempre cifre binarie 0.

Gli operatori binari &, |, e ^ eseguono operazioni logiche bit a bit fra i due
operandi, e precisamente:
&

esegue l'AND fra i corrispondenti bit dei due operandi

esegue l'OR inclusivo fra i corrispondenti bit dei due operandi

esegue l'OR esclusivo (XOR) fra i corrispondenti bit dei due


operandi

Es., date due variabili char a e b i cui valori sono, in notazione binaria:
a

0 1 0 0 1 1 0 1

(77)

0 0 1 1 1 0 1 0

(58)

i risultati delle tre operazioni sono rispettivamente:


a&b

0 0 0 0 1 0 0 0

(8)

a|b

0 1 1 1 1 1 1 1

(127)

a^b

0 1 1 1 0 1 1 1

(119)

L'operatore unario ~ inverte i bit dell'operando, cio calcola il suo


complemento a uno (in pratica, se il numero signed, lo inverte di segno e
sottrae 1).

Operatori binari in notazione compatta

Data l'espressione:
a = a op b
dove op un'operatore matematico o a livello del bit, b un'espressione
qualsiasi e a una variabile, le due operazioni possono essere sintetizzate in
una tramite l'operatore binario op=
Es.
MiaVariabile*4

MiaVariabile *= 4

equivale a

MiaVariabile =

La notazione compatta conveniente soprattutto quando il nome delle variabili


lungo!

Operatori relazionali

Gli operatori binari relazionali sono:


>

>=

<

<=

==

!=

Questi operatori eseguono il confronto fra i valori dei due operandi (che
possono essere di qualsiasi tipo nativo) e restituiscono un valore booleano:

a>b
a >= b
a<b
a <= b
a == b
a != b

restituisce true se a maggiore di b


restituisce true se a maggiore o uguale a b
restituisce true se a minore di b
restituisce true se a minore o uguale a b
restituisce true se a uguale a b
restituisce true se a diverso da b

Esempi:

bool bvar = 7 > 3;


bool bvar = 7 < 3;

(in bvar viene memorizzato true)


(in bvar viene memorizzato false)

Operatori logici

Gli operatori logici sono:


&&

||

Questi operatori agiscono su operandi booleani e restituiscono un valore


booleano:
L'operatore binario && esegue l'AND logico fra gli operandi:
a && b
risultato: true se entrambi a e b sono true; altrimenti:
false
L'operatore binario || esegue l'OR logico fra gli operandi:
a || b
risultato: false se entrambi a e b sono false;
altrimenti: true
L'operatore unario ! esegue il NOT logico dell'operando:
!a
risultato: true se a false o viceversa

Notare la differenza fra gli operatori logici


confronto bit a bit & e |
Es:

&& e || e gli operatori di

5 && 2

restituisce true in quanto entrambi gli operandi sono true (ogni


intero diverso da zero convertito in true)

5&2

restituisce 0 (e quindi false se convertito in booleano) in quanto i


bit corrispondenti sono tutti diversi

Operatori di incremento e decremento

Gli operatori unari di incremento ++ o decremento -- fanno aumentare o


diminuire di un'unit il valore dell'operando (che deve essere un l-value di
qualunque tipo nativo). Equivalgono alla sintesi di un operatore binario di
addizione o sottrazione, in cui il right-operand 1, con un operatore di
assegnazione, in cui il left-operand coincide con il left-operand
dell'addizione o sottrazione.
Es.
MiaVariabile++;
MiaVariabile+1;

equivale a

MiaVariabile =

la prima forma pi rapida e compatta (specialmente se il nome della variabile


lungo!) .

L'operatore pu seguire (suffisso) o precedere (prefisso) l'operando. Nella


forma prefisso l'incremento (o il decremento) viene eseguito prima che la
variabile sia utilizzata nell'espressione, nella forma suffisso avviene il contrario.

Es:

int a, b, c=5 ;
a = c++;
b = ++c;

alla fine di queste operazioni si trovano, nella variabili a, b e c, rispettivamente i


valori 5, 7 e 7.
Da questo esempio si capisce anche che la forma incremento (o decremento)
conviene non solo perch pi compatta, ma soprattutto perch consente di
ridurre il numero di istruzioni.

Operatore condizionale

L'operatore condizionale l'unico operatore ternario (tre operandi):


condizione ? espressioneA : espressioneB
(dove condizione un'espressione logica) l'operazione
restituisce il valore dell'espressioneA se la condizione e' true o
il valore dell'espressioneB se la condizione e' false
Es:

minimo = a < b ? a : b ;

L'operatore condizionale gode della rara propriet di restituire un ammissibile


l-value (non in tutti i compilatori, per!)
Es.
(m < n ? a : b) = c ;
(memorizza il valore di c in a se m minore di n, altrimenti memorizza il valore di
c in b)
in questo caso per a e b non possono essere n espressioni n costanti, ma
soltanto l-values.

Conversioni di tipo

Conversioni di tipo implicite


Il C++ esercita un forte controllo sui tipi e da' messaggio di errore quando si
tenta di eseguire operazioni fra operandi di tipo non ammesso. Es.
l'operatore % richiede che entrambi gli operandi siano interi.
I quattro operatori matematici si applicano a qualsiasi tipo intrinseco, ma i
tipi dei due operandi devono essere uguali. Tuttavia, nel caso di due tipi diversi,
il compilatore esegue una conversione di tipo implicita su uno dei due
operandi, seguendo la regola di adeguare il tipo pi semplice a quello pi
complesso, secondo la seguente gerarchia (in ordine crescente di
complessit):
bool - char - unsigned char - short - unsigned short - long - unsigned
long - float - double - long double
Es:

nell'operazione 3.4 / 2 il secondo operando trasformato in 2.0 e il risultato


correttamente 1.7
Nelle assegnazioni, il tipo del right-operand viene sempre trasformato
implicitamente nel tipo del left-operand (con un messaggio warning se la
conversione potrebbe implicare loss of data (perdita di dati), trasformando un
tipo pi complesso in un tipo pi semplice).
Es:

date le variabili char c e double d, l'assegnazione c = d


ammessa, ma genera un messaggio warning in fase di compilazione.

Nelle operazioni fra tipi interi, se il valore ottenuto esce dal range (overflow),
l'errore non viene segnalato. La stessa cosa dicasi se l'overflow si verifica a
seguito di una conversione di tipo.
Es:

short n = 32767 ;
n++ ;

(l'errore non viene segnalato, ma in n si ritrova il numero -32768)

Conversioni di tipo esplicite (casting)


Quando si vuole ottenere una conversione di tipo che non verrebbe eseguita
implicitamente, bisogna usare l'operatore binario di casting (o conversione
esplicita), che consiste nell'indicazione del nuovo tipo fra parentesi davanti al
nome della variabile da trasformare.
Es. se la variabile n di tipo int, l'espressione (float)n trasforma il contenuto di
n da int in float.
In C++ si pu usare anche il formato funzione (function-style casting):

float(n) equivalente a (float)n


va detto che il function-style casting non sempre possibile (per esempio con i
puntatori non si pu fare).
Tutti i tipi nativi consentono il casting, fermo restando il fatto che, se la
variabile da trasformare operando di una certa operazione, il tipo risultante
deve essere fra quelli ammissibili (altrimenti viene generato un errore in
compilazione). Per esempio: float(n) % 3 errato in quanto l'operatore %
ammette solo operandi interi.
Vediamo ora un esempio in cui si evidenzia la necessit del casting:
int m=10, n=4;
float r, a=2.7F;
r = m/n+a;
nell'ultima istruzione la divisione fra due numeri interi e quindi, essendo i due
operandi dello stesso tipo, la conversione implicita non viene eseguita e il
risultato della divisione il numero intero 2; solo successivamente questo
numero viene convertito in modo implicito in 2.0 per essere sommato ad a. Se
vogliamo che la conversione a float avvenga prima della divisione, e che
questa fornisca il risultato esatto (cio 2.5), dobbiamo convertire
esplicitamente almeno uno dei due operandi e quindi riscrivere cos la terza
istruzione:
r = (float)m/n+a;

(non servono altre parentesi perch il casting ha la precedenza


sulla divisione)

Il casting che abbiamo esaminato finora quello del C (C-style casting). Il


C++ ha aggiunto altri quattro operatori di casting, suddividendo le conversioni
di tipo in altrettante categorie e riservando un operatore per ciascuna di esse
(per fornire al compilatore strumenti di controllo pi raffinati). D'altra parte il Cstyle casting (che li comprende tutti) ammesso anche in C++, e pertanto non
tratteremo in questo corso degli altri operatori di casting, limitandoci a fornirne
l'elenco:
static_cast<T>(E)
dynamic_cast<T>(E)
reinterpret_cast<T>(E)
const_cast<T>(E)
dove E un'espressione qualsiasi il cui tipo convertito nel tipo T.

Precedenza fra operatori

Nella seguente tabella gli operatori sono in ordine di precedenza decrescente


(nello stesso blocco di righe hanno uguale precedenza):
[Legenda degli operandi: id=identificatore; pid=puntatore a
identificatore; expr=espressione; lv=l-value; ...=operandi opzionali]
CATEGORIA

SIMBOLO E
OPERANDI

risoluzione di visibilit
riferimento globale

binario
unario

id::id
::id

da sinistra a
destra
----

selezione di un membro
selezione di un membro puntato
indicizzazione array
chiamata di funzione
incremento suffisso
decremento suffisso
identificazione di tipo

binario
binario
binario
binario
unario
unario
unario

id.id
pid->id
id[expr]
id(expr)
lv++
lv-typeid(expr)

da sinistra a
destra
da sinistra a
destra
da sinistra a
destra
-------------

dimensione di un oggetto
complemento a 1
NOT logico
incremento prefisso
decremento prefisso
segno - algebrico
segno + algebrico
indirizzo di memoria
dereferenziazione
allocazione di memoria
deallocazione di memoria
casting (conversione di tipo)

unario
unario
unario
unario
unario
unario
unario
unario
unario
ternario
binario
binario

sizeof(expr)
~ expr
! expr
++lv
--lv
- expr
+ expr
&lv
*pid
new tipo ...
delete ... pid
(tipo)expr

---da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
------da destra
sinistra

DESCRIZIONE OPERATORE

ASSOCIATIVITA'

a
a
a
a
a
a
a
a

moltiplicazione
divisione
resto di divisione intera

binario
binario
binario

expr * expr
expr / expr
expr % expr

da sinistra a
destra
da sinistra a
destra
da sinistra a
destra

addizione
sottrazione

binario
binario

expr + expr
expr - expr

da sinistra a
destra

da sinistra a
destra
scorrimento a destra
scorrimento a sinistra

binario
binario

expr >> expr


expr << expr

da sinistra a
destra
da sinistra a
destra

minore
minore o uguale
maggiore
maggiore o uguale

binario
binario
binario
binario

expr < expr


expr <= expr
expr > expr
expr >= expr

da sinistra
destra
da sinistra
destra
da sinistra
destra
da sinistra
destra

a
a
a
a

uguale
diverso

binario
binario

expr == expr
expr != expr

da sinistra a
destra
da sinistra a
destra

AND bit a bit

binario

expr & expr

da sinistra a
destra

XOR bit a bit

binario

expr ^ expr

da sinistra a
destra

OR bit a bit

binario

expr | expr

da sinistra a
destra

AND logico

binario

expr && expr

da sinistra a
destra

OR logico

binario

expr || expr

da sinistra a
destra

espressione condizionale

ternario

expr ? expr :
expr

da destra a
sinistra

binario
binario
binario
binario
binario
binario
binario
binario
binario
binario
binario

lv = expr
lv *= expr
lv /= expr
lv %= expr
lv += expr
lv -= expr
lv >>= expr
lv <<= expr
lv &= expr
lv |= expr
lv ^= expr

da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra
da destra
sinistra

assegnazione
moltiplicazione e assegnazione
divisione e assegnazione
resto e assegnazione
addizione e assegnazione
sottrazione e assegnazione
scorrimento a destra e
assegnazione
scorrimento a sinistra e
assegnazione
AND bit a bit e assegnazione
OR bit a bit e assegnazione
XOR bit a bit e assegnazione

a
a
a
a
a
a
a
a
a

da destra a
sinistra
da destra a
sinistra
serializzazione delle espressioni

binario

expr , expr

da sinistra a
destra

Ordine di valutazione

Le regole di precedenza e associativit fra gli operatori non garantiscono che


l'ordine di valutazione delle sotto-espressioni all'interno di un espressione
sia sempre definito. Per esempio, si consideri l'espressione:
a = fun1(5) + fun2(3);
(dove fun1 e fun2 sono funzioni)
le regole di precedenza assicurano che prima vengano eseguite le chiamate
delle funzioni, poi l'addizione e infine l'assegnazione, ma non definito quale
delle due funzioni venga chiamata per prima.
Un altro caso di ordine di valutazione indefinito si ha fra gli argomenti di
chiamata di una funzione:
Es.
funz(expr1,expr2)
valuta prima expr1 o expr2 ?
All'opposto, in molti casi, l'ordine di valutazione univocamente definito, come
per esempio nelle operazioni logiche:
1.
2.

expr1 && expr2


expr1 || expr2

in entrambi i casi expr1 sempre valutata prima di expr2; in pi, expr2


valutata (e quindi eseguita) solo se necessario. In altre parole:

nel caso 1. expr2 non viene eseguita se expr1 false


nel caso 2. expr2 non viene eseguita se expr1 true

questo tipo di azione si chiama valutazione cortocircuitata ed molto utile


perch consente di ridurre il numero di istruzioni.

Introduzione all'I/O sui dispositivi standard


In questa lezione introdurremo le caratteristiche principali dell'I/O in C++, limitandoci per il
momento all'I/O in free-format sui dispositivi standard di input e di output.
Precisiamo che useremo una libreria (dichiarata nell'header-file: <iostream.h>) che ormai
"superata" dalla Libreria Standard (alcuni compilatori danno un messaggio di warning,
avvisando che si sta usando una "deprecated" (?!) library). Tuttavia questa libreria ancora
integrata nello standard e ci sembra un buon approccio per introdurre l'argomento.

Dispositivi standard di I/O

In C++ (come in C) sono definiti i seguenti dispositivi standard di I/O


(elenchiamo i tre principali):

stdout standard output (di default associato al video)


stderr standard output per i messaggi (associato al video)
stdin standard input (di default associato alla tastiera)

stdin e stdout sono reindirizzabili a files nella linea di comando quando si


lancia il programma eseguibile.

Oggetti globali di I/O

In C++ i dispositivi standard di I/O stdout, stderr e stdin sono "collegati"


rispettivamente agli oggetti globali cout, cerr e cin.
Oggetto (definizione temporanea): variabile appartenente a un tipo astratto,
non nativo del linguaggio.
Globale: visibile sempre e dappertutto.
Un oggetto globale creato appena si lancia il programma, prima che venga
eseguita la prima istruzione del main.

Per definire gli oggetti globali di I/O bisogna includere l'header-file:


<iostream.h>.

Operatori di flusso di I/O

In C++ sono definiti gli operatori di flusso di I/O


<< (inserimento)
e
>> (estrazione)
i cui left-operand sono rispettivamente cout (oppure cerr, che non
menzioneremo pi, in quanto le sue propriet sono identiche a quelle di cout) e
cin.
Il compilatore distingue gli operatori di flusso da quelli di shift dei bit
(identificati dagli stessi simboli) in base al contesto, cio in base al tipo degli
operandi.

Output tramite l'operatore di inserimento

In C++ un'operazione di output si identifica con un'operazione di inserimento


nell'oggetto cout:
cout << dato;
dove dato una qualsiasi variabile o espressione di tipo nativo (oppure una
stringa). L'istruzione significa: il "dato" viene "inserito" nell'oggetto cout (e da
questo automaticamente trasferito su stdout).
A differenza dalla funzione printf non necessario usare specificatori di
formato, in quanto il tipo delle variabili riconosciuto automaticamente (in
realt, come vedremo pi avanti, esistono anche qui degli specificatori, detti
"manipolatori di formato", ma servono soltanto quando la scrittura deve essere
non in free-format).

Esempi:

cout << "Scrive una stringa\n";


cout << Variabile_intera;
cout << Variabile_float;
ecc.....

In ogni operazione viene trasferito un solo dato per volta; per cui, se si devono
scrivere pi dati (specie se di tipo diverso), vanno fatte altrettante operazioni di
inserimento, con istruzioni separate. Alternativamente, in una stessa istruzione
si possono "impilare" pi operazioni di inserimento una di seguito all'altra.
Esempio:

cout << dato1 << dato2 << dato3;


equivale a:

cout << dato1; cout << dato2; cout << dato3;

questo possibile grazie al fatto che l'operatore << restituisce lo stesso


oggetto del left-operand (cio cout) e che l'associativit dell'operazione
procede da sinistra a destra.
Una variabile di tipo char scritta come carattere; per scriverla come numero
occorre fare il casting.
Per esempio, l'istruzione:

cout << 'A' << " ha codice ascii: " << (int)'A' <<
"\n";

visualizza la frase:

A ha codice ascii 65

Input tramite l'operatore di estrazione

In C++ un'operazione di input si identifica con un'operazione di estrazione


dall'oggetto cin:
cin >> dato;
dove dato un l-value di qualsiasi tipo nativo (oppure una variabile stringa).
L'istruzione significa: il valore immesso da stdin (automaticamente trasferito in
cin) viene "estratto" dall'oggetto cin e memorizzato nella variabile "dato".
Come le operazioni di inserimento, anche quelle di estrazione possono essere
"impilate" una di seguito all'altra in un'unica istruzione.
Esempio: cin >> dato1 >> dato2 >> dato3; (i dati dato1, dato2, dato3
devono essere forniti nello stesso ordine).
Il programma interpreta la lettura di un dato come terminata se incontra un
blank, un carattere di tabulazione o un ritorno a capo. Ne consegue che, se

l'input una stringa, non deve contenere blanks (n tabs) e non pu essere
spezzata in due righe. D'altra parte l'esistenza dei terminatori (blank, tab o
CR) consente di immettere pi dati nella stessa riga.
Casi particolari:

i terminatori inseriti ripetutamente o prima del dato da leggere sono


ignorati
se il dato da leggere di tipo numerico, la lettura terminata quando
incontra un carattere non valido (compreso il punto decimale se il
numero intero, cio non esegue conversioni di tipo)
se il dato da leggere di tipo char, legge un solo carattere

Memorizzazione dei dati introdotti da tastiera

Se stdin associato, come di default, alla tastiera, la memorizzazione dei dati


segue delle regole generali, che sono le stesse sia in C++ (lettura tramite
l'oggetto cin) che in C (lettura tramite le funzioni di libreria):

la lettura non avviene direttamente, ma tramite un'area di memoria, detta


buffer di input;
il programma, appena incontra un'istruzione di lettura, si appresta a
memorizzare i dati (che distingue l'uno dall'altro riconoscendo i
terminatori) trasferendoli dal buffer di input, finch questo non resta
vuoto;
se il buffer di input si svuota prima che la lettura sia terminata (oppure se
il buffer gi vuoto all'inizio della lettura, come dovrebbe succedere
sempre), il programma si ferma in attesa di input e il controllo passa
all'operatore, che viene abilitato a introdurre dati da tastiera fino a quando
non invia un enter (indipendentemente dal numero di dati da leggere);
l'intera riga digitata dall'operatore viene poi trasferita nel buffer di input,
al quale il programma riaccede per completare l'operazione di lettura;
se nel buffer di input restano ancora dati dopo che l'operazione di lettura
finita, questi verranno memorizzati durante la lettura successiva.

Come si pu notare, la presenza del buffer di input (molto utile peraltro per
migliorare l'efficienza del programma) crea una specie di "asincronismo" fra
operatore e programma, che pu essere facilmente causa di errore: bisogna fare
attenzione a fornire ogni volta esattamente il numero di dati richiesti.

Comportamento in caso di errore in lettura

Le operazioni di estrazione non restituiscono mai espliciti messaggi di errore,


tuttavia,

se il primo carattere letto non valido (per esempio una lettera se vuole
leggere un numero), il programma non memorizza il dato e imposta una
condizione di errore interna che inibisce anche le successive operazioni di
lettura (nel senso che tutte le istruzioni di lettura, dal punto dell'errore in
poi, vengono "saltate");
se invece il carattere non valido non il primo, il programma accetta il dato
letto fino a quel momento, ma il carattere invalido resta nel buffer,
disponibile per le operazioni di lettura successive.

Per accorgersi di un errore (e per porvi rimedio) bisogna utilizzare alcune


propriet dell'oggetto cin (di cui parleremo pi avanti).

Il Compilatore GNU gcc in ambiente Linux

Un compilatore integrato C/C++


Per Linux e' disponibile un compilatore integrato C/C++: si tratta dei comandi
GNU gcc e g++, rispettivamente.
In realta g++ e' uno script che chiama gcc con opzioni specifiche per riconoscere
il C++.
Il progetto GNU
Il comando gcc, GNU Compiler Collection, fa parte del progetto GNU (web server
www.gnu.org). Il progetto GNU fu lanciato nel 1984 da Richard Stallman con lo
scopo di sviluppare un sistema operativo di tipo Unix che fosse completamente
"free" software.
Cosa GNU/Linux?
Gnu Non Unix!
"GNU, che sta per "Gnu's Not Unix" (Gnu Non Unix), il nome del

sistema
software completo e Unix-compatibile che sto scrivendo per
distribuirlo
liberamente a chiunque lo possa utilizzare. Molti altri volontari mi
stanno aiutando. Abbiamo gran necessit di contributi in tempo,
denaro,
programmi e macchine."

[Richard Stallman, Dal manifesto GNU, http://www.gnu.org/gnu/manifesto.html

Quale versione di gcc sto usando?


Si puo' determinare la versione del compilatore invocando:
gcc -v
gcc version 2.96 20000731 (Red Hat Linux 7.1
2.96-98)

I passi della compilazione


Sia gcc che g++ processano file di input attraverso uno o piu' dei seguenti passi:
1) preprocessing
-rimozione dei commenti
-interpretazioni di speciali direttive per il preprocessore denotate da "#"

come:
#include - include il contenuto di un determinato file, Es.
#include<math.h>
#define -definisce un nome simbolico o una variabile, Es. #define
MAX_ARRAY_SIZE 100
2) compilation
-traduzione del codice sorgente ricevuto dal preprocessore in codice
assembly
3) assembly
-creazione del codice oggetto
4) linking
-combinazione delle funzioni definite in altri file sorgenti o definite in
librerie con la funzione main() per creare il file eseguibile.
Estensioni
Alcuni suffissi di moduli implicati nel processo di compilazione:
.c modulo sorgente C; da preprocessare, compilare e assemblare
.cc modulo sorgente C++; da preprocessare, compilare e assemblare
.cpp modulo sorgente C++; da preprocessare, compilare e assemblare
.h modulo per il preprocessore; di solito non nominato nella riga di commando
.o modulo oggetto; da passare linker
.a sono librerie statiche
.so sono librerie dinamiche
L' input/output di gcc

gcc accetta in input ed effettua la compilazione di codice C o C++ in un solo


colpo.
Consideriamo il seguente codice sorgente C:

/* il codice C pippo.c */
#include <stdio.h>
int main() {
puts("ciao pippo!");
return 0;
}
Per effettuare la compilazione

gcc pippo.c
In questo caso l' output di default e' direttamente l'eseguibile a.out.
Di solito si specifica il nome del file di output utilizzando l' opzione -o :

gcc -o prova pippo.c


L'eseguibile puo' essere lanciato usando semplicemente

./prova
ciao pippo!
Nota. Usare "./" puo' sembrare superfluo. In realta' si dimostra molto utile per
evitare di lanciare involontariamente un programma omonimo, per esempio il
comando "test"!
Consideriamo ora un codice sorgente C++ analogo:

// Il codice C++ pippo.cpp


#include<iostream>
int main() {
cout<<"ciao pippo!"<<'\n';
return 0;
}
Questa volta compiliamo usando

g++ -o prova pippo.cpp


Il valore restituito al sistema

Per verificare il valore restituito dal programma al sistema tramite l'istruzione di


return usiamo

./prova
ciao pippo!
echo $?
0

Passaggi intermedi di compilazione


Per compilare senza effettuare il link usare

g++ -c pippo.cpp
In questo caso viene creato il file oggetto pippo.o .
Per effettuare il link usiamo

g++ -o prova pippo.o

I messaggi del compilatore


Il compilatore invia spesso dei messaggi all'utente. Questi messaggi si possono
classificare in due famiglie: messaggi di avvertimento (warning messagges) e
messaggi di errore (error messagges). I messaggi di avvertimento indicano la
presenza di parti di codice presumibilmente mal scritte o di problemi che
potrebbero avvenire in seguito, durante l'esecuzione del programma. I messaggi
di avvertimento non interrompono comunque la compilazione.I messaggi di errore
invece indicano qualcosa che deve essere necessariamente corretto e causano
l'interruzione della compilazione.
Esempio di un codice C++ che genera un warning:

// example1.cpp
#include<iostream>
float multi(int a, int b) {
return a*b;
};
int main() {
float a=2.5;
int b=1;
cout<<"a="<<a<<", b="<<b<<'\n';
cout<<"a*b="<<multi(a,b)<<'\n';
return 0;
}
In fase di compilazione apparira' il seguente warning:

example1.cpp: In function `int main ()':


example1.cpp:12: warning: passing `float' for
argument passing 1 of
`multi (int, int)'
example1.cpp:12: warning: argument to `int'
from `float'
Il messaggio ci avvisa che alla linea 12 del main() e' stato passato alla funzione
multi un float invece che un int.
Esempio di un codice che genera un messaggio di errore:

// example1.cpp
#include<iostream>
float multi(int a, int b) {
return a*b
};
int main() {
int a=2;
int b=1;
cout<<"a="<<a<<", b="<<b<<'\n';
cout<<"a/b="<<multi(a,b)<<'\n';
return 0;
}
Si noti che l' instruzione di return all' interno della funzione multi non termina con
il ; .
A causa di questo grave errore la compilazione non puo' essere portata a termine:

example1.cpp: In function `float multi (int,


int)':
example1.cpp:5: parse error before `}'
example1.cpp:5: warning: no return statement
in function returning
non-void

Controlliamo i livelli di warning


Per inibire tutti i messaggi di warinig usare l' opzione -w

g++ -w -o prova example1.cpp


Per usare il massimo livello di warning usare l' opzione -Wall

g++ -Wall -o prova example1.cpp

Compilare per effetture il debug


Se siete intenzionati ad effettuare il debug di un programma,
utilizzate sempre l'opzione -g:

g++ -Wall -g -o pippo example1.cpp


L' opzione -g fa in modo che il programma eseguibile contenga
informazioni supplementari che permettono al debugger di collegare
le istruzioni in linguaggio macchina che si trovano nell'eseguibile alle
righe del codice corrispondenti nei sorgenti C/C++.

Autopsia di un programma defunto


Il seguente codice C++, wrong.cpp, genera un errore (nella
fattispecie una divisione per 0) in fase di esecuzione che porta alla
terminazione innaturale del programma

#include<iostream>
int div(int a, int b) {
return a/b;
};
int main() {
int a=2;
int b=0;
cout<<"a="<<a<<", b="<<b<<'\n';
cout<<"a/b="<<div(a,b)<<'\n';
return 0;
}

Compiliamo il file wrong_code.cpp

g++ -Wall -g -o wrong_program


wrong_code.cpp
Il codice e' sintatticamente ineccepibile e verra' compilato senza
problemi. In fase di esecuzione si verifica tuttavia una divisione per
zero che causa la morte del programma.

./wrong_program
a=2, b=0
Floating exception (core dumped)
Linux genera nella directory corrente un file in cui scarica la memoria
memoria assocciata al programma (core dump):

ls -sh
total 132k
100k core 4.0k wrong_code.cpp 28k
wrong_program*
Il file core contiene l 'immagine della memoria (riferita al nostro
programma) al momento dell'errore.
Possiamo effettuare l' autopsia del programma utilizzando il
debugger GNU gdb

gdb wrong_program core


...
Core was generated by `wrong_prog'.
...
#0 0x080486a5 in div (a=2, b=0) at
wrong_code.cpp:4
4
return a/b;
(gdb) where
#0 0x080486a5 in div (a=2, b=0) at
wrong_code.cpp:4
#1 0x08048737 in main () at
wrong_code.cpp:13
#2 0x400b1647 in __libc_start_main

(main=0x80486b0 <main>, argc=1,


ubp_av=0xbfffe614, init=0x80484e8
<_init>, fini=0x80487b0 <_fini>,
rtld_fini=0x4000dcd4 <_dl_fini>,
stack_end=0xbfffe60c)
at ../sysdeps/generic/libc-start.c:129
(gdb) quit

Il comando where di gdb ci informa che l' errore si e' verificato alla
riga 4 del modulo wrong_code.cpp.
Esiste una versione con interfaccia grafica di gdb : kdbg

Ottimizzazione
Il compilatore gcc consente di utilizzare diverse opzioni per ottenere un risultato
pi o meno ottimizzato. L'ottimizzazione richiede una potenza elaborativa
maggiore, al crescere del livello di ottimizzazione richiesto. L' opzione -On
ottimizza il codice, dove n il livello di ottimizzazione. Il massimo livello di
ottimizzazione allo stato attuale il 3, quello generalmente pi usato 2.
Quando non si deve eseguire il debug consigliato ottimizzare il codice.

Opzione

Descrizione

-O, -O1

Ottimizzazione minima

-O2

Ottimizzazione media

-O3

Ottimizzazione massima

-O0

Nessuna ottimizzazione

Esempio di un codice chiaramente inefficiente

int main() {
int a=10;
int b=1;
int c;
for (int i=0; i<1e9; i++) {
c=i+a*b-a/b;
}
return 0;
}

Confronto dei tempi di esecuzione in funzione di livelli di ottimizzazione crescente

Livello

Tempo di esecuzione (secondi)

O0

32.2

O1

5.4

O2

5.2

O3

5.2

Compilazione di un programma modulare


Un programma modulare e' un programma spezzettato in componenti piu' piccole
con funzioni specifiche. La programmazione modulare e' piu' facile da
comprendere e da correggere.
Nel seguito abbiamo un programma C++ composto da
tre moduli: main.cpp, myfunc.cpp e myfunc.h .

// main.cpp
#include<iostream>
#include"myfunc.h"
int main() {
int a=6;
int b=3;
cout<<"a="<<a<<", b="<<b<<'\n';
cout<<"a/b="<<div(a,b)<<'\n';
cout<<"a*b="<<mul(a,b)<<'\n';
return 0;
}

// myfunc.h
int div(int a, int b);
int mul(int a, int b);

// myfunc.cpp
int div(int a, int b) {
return a/b;
};

int mul(int a, int b) {


return a*b;
};
1. Per compilare usiamo
g++ -Wall -g -o prova main.cpp
myfunc.cpp
Si noti che il file myfunc.h non appare nella riga di comando, verra' incluso
dal gcc in fase di precompilazione.

Inclusione di librerie in fase di compilazione


L'opzione -lnome_libreria compila utilizzando la libreria indicata, tenendo
presente che, per questo, verr cercato un file che inizia per lib, continua con il
nome indicato e termina con .a oppure .so.
Modifichiamo i moduli myfunc.h e myfunc.cpp aggiungendo la
funzione pot:

// myfunc.h
int div(int a, int b);
int mul(int a, int b);
float pot(float a, float b);

// myfunc.cpp
int div(int a, int b) {
return a/b;
};
int mul(int a, int b) {
return a*b;
};
float pot(float a, float b) {
return pow(a,b);
}
La compilazione pero' si interrompe
g++ -Wall -g -o prova main.cpp
myfunc.cpp
myfunc.cpp: In function `float pot (float,

float)':
myfunc.cpp:11: `pow' undeclared (first use
this function)
myfunc.cpp:11: (Each undeclared identifier is
reported only once for
each function it appears in.)
La funzione pow e' contenuta nella libreria matematica
math, dobbiamo allora aggiungere l'istruzione include
nel modulo :

// myfunc.cpp
#include<math.h>
int div(int a, int b) {
return a/b;
};
int mul(int a, int b) {
return a*b;
};
float pot(float a, float b) {
return pow(a,b);
}
e compilare con un link alla libreria libm.so

g++ -Wall -g -o prova main.cpp


myfunc.cpp -lm
Di default il compilatore esegue la ricerca della libreria nel direttorio
standard /usr/lib/. Tramite l' opzione -L/nome_dir, e' possibile
aggiunge la directory /nome_dir alla lista di direttori in cui gcc
cerca le librerie in fase di linking.

Il Comando 'make' in ambiente Linux

Perche' utilizzare il comando make?


Immaginate un progetto molto esteso, formato da decine e decine di moduli, e di
voler cambiare solo una piccola parte di codice e di voler poi testare il programma.
Ovviamente, per evitare di ricompilare tutto il codice ad ogni modifica, e'
conveniente compilare solo i moduli appena modificati e poi effettuare il link con
la parte di codice rimasta immutata. Potrebbe pero' essere difficile, o quanto
meno noioso, controllare ripetutamente quali moduli devono essere per forza
ricompilati e quali no. Il comando make fa questo per voi!

Il Makefile ed i target del make


Per funzionare make ha bisogno che voi scriviate un file chiamato Makefile in cui
siano descritte le relazioni fra i vostri files ed i comandi per aggiornarli. Quando il
make viene invocato esegue le istruzioni contenute nel Makefile.
Una idea base che bisogna capire del make e' il concetto di target . Il primo target
in assoluto e' il Makefile stesso. se si lancia il make senza aver preparato un
Makefile si ottiene il seguente risultato
make
make: *** No targets specified and no
makefile found. Stop.
Quello che segue e' un semplice Makefile in cui sono stati definiti tre target e tre azioni
corrispondenti:

# Un esempio di Makefile
one:
@echo UNO!
two:

@echo DUE!

three:
@echo E TRE!
La definizione di un target inizia sempre all'inizio della riga ed seguito da : . Le
azioni (in questo caso degli output su schermo) seguono le definizioni di ogni
target e, anche se in questo esempio sono singole, possono essere molteplici. La
prima riga, che inizia con #, e' un commento.

Per utilizare i target invochiamoli sulla riga di comando del make:


make one
UNO!
make one two three
UNO!
DUE!
E TRE!
Se non si invoca nessun target nella linea di comando, make assume come
default il primo che trova nel Makefile:
make
UNO!
IMPORTANTE: le linee in cui si specificano le azioni corrispondenti ad ogni target
(Es. @echo UNO!) devono iniziare con un separatore <TAB>!
Il seguente Makefile non e' valido perche' la riga seguente la definizione del target non inizia
con un separatore <TAB>:

# Un esempio di Makefile mal scritto


one:
@echo UNO!

make one
Makefile:4: *** missing separator. Stop.
Le righe di azione devo iniziare invariabilmente con un separatore <TAB>, NON
POSSONO ESSERE UITLIZZATI DEGLI SPAZI!

Dipendenze
E' possibile definire delle dipendenze fra i target all' interno del Makefile

# Un esempio di Makefile con dipendenze


one:
@echo UNO!
two:

one

@echo DUE!
three: one two
@echo E TRE!
all:

one two three


@echo TUTTI E TRE!

Si noti come i target vengono elaborati in sequenza:


make three
UNO!
DUE!
E TRE!
make all
UNO!
DUE!
E TRE!
TUTTI E TRE!
Macro e variabili ambiente
E' possibile definere delle Macro all' interno del Makefile

#Definiamo la Macro OBJECT


OBJECT=PIPPO
one:

@echo CIAO $(OBJECT)!

make
CIAO PIPPO!
Possiamo ridefinire il valore della macro OBJECT direttamente sulla riga di
comando, senza alterare il Makefile!

make OBJECT=pippa
CIAO pippa!
Il Makefile puo' accedere alle variabili ambiente:

# Usiamo una variabile ambiente


OBJECT=$(TERM)
one:

@echo CIAO $(OBJECT)!

make
CIAO xterm!
Compiliamo con make (finalmente)
Supponiamo di voler compilare il seguente codice C++ composto da tre moduli
(main.cpp, myfunc.cpp e myfunc.h) usando il comando make.

// main.cpp
#include<iostream>
#include"myfunc.h"
int main() {
int a=6;
int b=3;
cout<<"a="<<a<<", b="<<b<<endl;
cout<<"a/b="<<div(a,b)<<endl;
cout<<"a*b="<<mul(a,b)<<endl;
cout<<"a^b="<<pot(a,b)<<endl;
return 0;
}
// myfunc.cpp
#include<math.h>
int div(int a, int b) {
return a/b;
};
int mul(int a, int b) {
return a*b;
};
float pot(float a, float b) {
return pow(a,b);
}
// myfunc.h

int div(int a, int b);


int mul(int a, int b);
float pot(float a, float b);
Un semplice Makefile si presenta cosi':

OBJECTS=main.o myfunc.o
CFLAGS=-g -Wall
LIBS=-lm
CC=g++
PROGRAM_NAME=prova
$(PROGRAM_NAME):$(OBJECTS)
$(CC) $(CFLAGS) -o
$(PROGRAM_NAME) $(OBJECTS) $(LIBS)
@echo " "
@echo "Compilazione completata!"
@echo " "
Il make ricompilera' il target prova se i files da cui questo dipende (gli OBJECTS
main.o e myfunc.o) sono stati modificati dopo che prova e' stato modificato
l'ultima volta oppure non esistono. Il processo di ricompilazione avverra' secondo
la regola descritta nell' azione del target e usando le Macro definite dall' utente
(CC, CFLAGS, LIBS).
Per compilare usiamo semplicemente

make
g++ -c -o main.o main.cpp
g++ -c -o myfunc.o myfunc.cpp
g++ -g -Wall -o prova main.o myfunc.o -lm
Compilazione completata!
Se modifichiamo solo un modulo, per esempio myfunc.cpp, il make effettuera' la
compilazione di questo file solamente.
make
g++ -c -o myfunc.o myfunc.cpp
g++ -g -Wall -o prova main.o myfunc.o -lm
Compilazione completata!
Alcuni target standard
Esistono alcuni target standard usati da programmatori Linux e GNU. Fra questi:

install, viene utilizzato per installare i file di un progetto e puo'


comprendere la creazione di nuove directory e la assegnazione di diritti di
accesso ai file.
clean, viene utilizzato per rimuovere dal sistema i file oggetto (*.o), i file
core, e altri file tempornei creati in fase di compilazione
all, di solito utilizzato per richiamare altri target con lo scopo di costruire
l'intero progetto.

Aggiungiamo il target clean al nostro Makefile:

OBJECTS=main.o myfunc.o
CC=g++
CFLAGS=-g -Wall
LIBS=-lm
PROGRAM_NAME=prova

$(PROGRAM_NAME):$(OBJECTS)
$(CC) $(CFLAGS) -o
$(PROGRAM_NAME) $(OBJECTS) $(LIBS)
@echo " "
@echo "Compilazione completata!"
@echo " "
clean:
rm -f *.o
rm -f core
Invocare il target clean comporta la cancellazione di tutti i file
oggetto e del file core.

make clean
rm -f *.o
rm -f core

Istruzioni di controllo
Si chiamano "istruzioni di controllo" in C++ (come in C) quelle istruzioni che modificano
l'esecuzione sequenziale di un programma.

Istruzione di controllo if

Sintassi:
if (condizione) istruzione;
(dove condizione un'espressione logica) se la condizione
true il programma esegue l'istruzione, altrimenti passa
direttamente all'istruzione successiva
Nel caso di due scelte alternative, all'istruzione if si pu associare l'istruzione else
:
if (condizione) istruzioneA;
else istruzioneB;
se la condizione true il programma esegue l'istruzioneA,
altrimenti esegue l'istruzioneB

Se le istruzioni da eseguire in base alla condizione sono pi di una, bisogna


creare un ambito, cio raggruppare le istruzioni fra parentesi graffe:
if (condizione) { ....... blocco di istruzioni ...... }
e analogamente:
else { ....... blocco di istruzioni ...... }
[p11]

Se l'istruzione controllata da un if consiste a sua volta in un altro if (sono


possibili pi istruzioni if "innestate"), ogni eventuale else si riferisce sempre all'if
immediatamente superiore (in assenza di parentesi graffe).

Es.

if (cond1) if (cond2) istr1; else istr2;


(istr2 eseguita se cond1 true e cond2 false)

Invece:

if (cond1) { if (cond2) istr1;} else istr2;


(istr2 eseguita se cond1 false, indipendentemente da

cond2).

Per essere sicuri di ottenere quello che si vuole, mettere sempre le parentesi
graffe, anche se sono ridondanti, e quindi il primo caso equivalente (ma pi
chiaro) se si scrive:
if (cond1) { if (cond2) istr1; else istr2; }

Istruzione di controllo while

Sintassi:
while (condizione) istruzione;
(dove condizione un'espressione logica) il programma
esegue ripetutamente l'istruzione finch la condizione true e
passa all'istruzione successiva appena la condizione diventa
false.
Ovviamente, affinch il loop (ciclo) non si ripeta all'infinito,
l'istruzione deve modificare qualche parametro della
condizione.

Se le istruzioni da eseguire in base alla condizione sono pi di una, bisogna


creare un ambito, cio raggruppare le istruzioni fra parentesi graffe:
while (condizione) { ....... blocco di istruzioni ...... }

La condizione viene verificata all'inizio di ogni iterazione del ciclo: pertanto


possibile, se la condizione gi inizialmente false, che il ciclo non venga
eseguito neppure una volta.

E' ammessa anche la forma:


while (condizione) ;
in questo caso, per evitare un loop infinito, la condizione deve essere in grado
di automodificarsi.

Istruzione di controllo do ... while

Sintassi:
do { ... blocco di istruzioni ... } while ( condizione ) ;
(dove condizione un'espressione logica) funziona come l'istruzione
while, con la differenza che la condizione verificata alla fine di ogni
iterazione e pertanto il ciclo sempre eseguito almeno una volta. Se la
condizione true il programma torna all'inizio del ciclo ed esegue una
nuova iterazione, se false, passa all'istruzione successiva. Le parentesi
graffe sono obbligatorie, anche se il blocco costituito da una sola
istruzione.

Istruzione di controllo for

Sintassi:
for (inizializzazione; condizione; modifica) istruzione;
(dove inizializzazione un'espressione eseguita solo la prima volta,
condizione un'espressione logica, modifica un'espressione
eseguita alla fine di ogni iterazione) il programma esegue ripetutamente
l'istruzione finch la condizione true e passa all'istruzione successiva
appena la condizione diventa false.

Se le istruzioni da eseguire in base alla condizione sono pi di una, bisogna


creare un ambito, cio raggruppare le istruzioni fra parentesi graffe:
for (inizializzazione; condizione; modifica)
{ ....... blocco di istruzioni ...... }

L'istruzione for simile all'istruzione while, con le differenze che in while


l'inizializzazione impostata precedentemente e la modifica eseguita
all'interno del blocco di istruzioni del ciclo. Come in while, anche in for la
condizione viene verificata all'inizio di ogni iterazione.

Esempio (confronto fra for e while):

int conta;

int conta=0;

for (conta=0; conta<10;


conta+=2)

while (conta<10)

cout << conta << '\n' ;

{
cout << conta <<
'\n' ;
conta+=2;
}

Tutte le parti di un'istruzione for sono opzionali; al limite anche l'istruzione:


for (;;) ;

(loop infinito)

sintatticamente valida (anche se poco "pratica"!); infatti, se non specificata, la


condizione di default true.

Istruzioni continue, break e goto

Le istruzioni continue e break sono utilizzate all'interno di un blocco di


istruzioni controllate da while, do ... while o for.
L'istruzione continue interrompe l'iterazione corrente del ciclo: il programma
riprende dall'inizio dell'iterazione successiva, previa esecuzione della modifica
(nei cicli for) e verifica della condizione (in tutti i tipi di cicli).
L'istruzione break interrompe completamente un ciclo: il programma riprende
dalla prima istruzione successiva a quelle del ciclo.
Nel caso di pi cicli innestati, le istruzioni continue e break hanno effetto
esclusivamente sul ciclo a cui appartengono e non su quelli pi esterni.

Il C++ mantiene la "vecchia" istruzione goto :


goto identificatore;
............................
............................
identificatore: istruzione;
il flusso del programma "salta" direttamente all'istruzione labellata (etichettata)
identificatore.
L'istruzione goto ha pochi utilizzi nella normale programmazione ad alto livello.
Pu essere importante nei rari casi in cui richiesta la massima efficienza (per
esempio in applicazioni in tempo reale), oppure per uscire direttamente dal pi
interno di diversi cicli innestati.

Istruzione di controllo switch ... case

Sintassi (le parti fra parentesi quadra sono opzionali):


switch( espressione )
{
[case costante1 : [ blocco di istruzioni 1]]
[case costante2 : [ blocco di istruzioni 2]]
..............
[default : [blocco di istruzioni]]
}

L'istruzione switch confronta il valore dell'espressione (che deve restituire un


risultato intero) con le diverse costanti (dello stesso tipo dell'espressione) e,
appena ne trova una uguale, esegue tutte le istruzioni da quel punto in poi
(anche se le istruzioni relative allo stesso case sono pi d'una, non necessario
inserirle fra parentesi graffe). Se nessuna costante uguale al valore
dell'espressione, esegue, se esistono, le istruzioni dopo default:
Per ottenere che le istruzioni selezionate siano eseguite in alternativa alle altre,
bisogna inserire alla fine del corrispondente blocco l'istruzione break
[p15]

Array
Cos' un array ?

Un array un insieme di variabili che occupano locazioni consecutive in memoria


e sono caratterizzate dall'appartenere tutte allo stesso tipo, detto tipo dell'array
(pu anche essere un tipo astratto).
Ogni variabile di tale insieme detta elemento dell'array ed identificata dalla
sua posizione d'ordine nell'array (indice). L'intero array identificato da un
nome (che va specificato secondo le regole generali di specifica degli
identificatori).
Il numero di elementi di un array (detto dimensione dell'array ) predefinito
e invariabile. In C++ (come in C) l'indice pu assumere valori compresi fra zero
e il numero di elementi meno 1.

Definizione e inizializzazione di un array

Per definire un array bisogna specificare prima il tipo e poi il nome dell'array,
seguito dalla sua dimensione fra parentesi quadre (la dimensione deve essere
espressa da una costante).
Es.
int valori[30];
In fase di definizione un array pu essere anche inizializzato. I valori iniziali
dei suoi elementi devono essere specificati fra parentesi graffe e separati l'un
l'altro da una virgola; inoltre la dimensione dell'array, essendo determinata
automaticamente, pu essere omessa (non per le parentesi quadre, che
costituiscono l'operatore di dichiarazione dell'array).
Es.
int valori[] = {32, 53, 28, 85, 21};
nel caso dell'esempio la dimensione 5 automaticamente calcolata.

L'operatore [ ]

L'operatore binario [ ] richiede come left-operand il nome di un array e


come secondo operando (racchiuso fra le due parentesi quadre) una qualunque
espressione con risultato intero (interpretato come indice dell'array).
Il significato dell'operatore [ ] duplice:

usato per restituire un l-value, un operatore di inserimento di dati


nell'array.
Es.
valori[3] = 45;
(il numero 45 viene assegnato alla variabile identificata dall'indice 3
dell'array valori)
usato per restituire un r-value, un operatore di estrazione di dati
dall'array.
Es.
a = valori[4] ;
(il contenuto della variabile identificata dall'indice 4 dell'array valori
viene assegnato alla variabile a)

Array multidimensionali

In C++ (come in C) sono possibili array con qualsivoglia numero di dimensioni;


tali array vanno definiti come nel seguente esempio (array tridimensionale):
float tabella[3][4][2];
NOTA : la formulazione appare un po' "strana", ma chiarisce il fatto che un array
multidimensionale da intendersi come un array di array. Nell'esempio:
tabella un array di 3 elementi, ciascuno dei quali un array di 4 elementi,
ciascuno dei quali un array di 2 elementi di tipo float.
A differenza dal FORTRAN, in C++ (come in C) gli array multidimensionali
sono memorizzati con gli indici meno significativi a destra ("per riga", nel caso di
array bidimensionali).
Per esempio, dato l'array A[2][3], i suoi elementi sono memorizzati nel seguente
ordine:
A[0][0] , A[0][1] , A[0][2] , A[1][0] , A[1][1] , A[1][2]
Per inizializzare un array multidimensionale, bisogna innestare tanti gruppi
di parentesi graffe quante sono le singole porzioni monodimensionali
dell'array, ed elencare gli elementi nello stesso ordine in cui saranno
memorizzati.
Esempio, nel caso bidimensionale:
0} , {-2, 6 } };

int dati[3][2] = { {8, -5} , {4,

L'operatore sizeof e gli array

L'operatore sizeof, se l'operando il nome di un array, restituisce il numero


di bytes complessivi dell'array, che dato dal numero degli elementi
moltiplicato per la lunghezza in byte di ciascun elemento (la quale ovviamente
dipende dal tipo dell'array).
[p16]

Gli array in C++

Gli array descritti finora sono quelli "in stile C". Nei programmi in C++ ad alto
livello sono scarsamente utilizzati. Al loro posto si preferisce usare alcune classi
della Libreria Standard (come vedremo) che offrono flessibilit molto maggiori
(per esempio la dimensione modificabile dinamicamente e inoltre, negli array
multidimensionali, si possono definire singole porzioni monodimensionali con
dimensioni diverse).

Stringhe di caratteri
Le stringhe come particolari array di caratteri

Abbiamo gi visto che le stringhe non costituiscono un tipo intrinseco del C++
e di conseguenza non sono ammesse come operandi dalla maggior parte degli
operatori (compreso l'operatore di assegnazione).
Sono tuttavia riconosciute da alcuni operatori (come per esempio gli operatori di
flusso di I/O del C++) e da numerose funzioni di libreria del C (come per
esempio la printf, insieme a molte altre che hanno il compito specifico di
manipolare le stringhe).
In memoria le stringhe sono degli array di tipo char, con una particolarit in
pi, che le fa riconoscere da operatori e funzioni come stringhe e non come
normali array: l'elemento dell'array che segue l'ultimo carattere della stringa
deve contenere il carattere NULL (detto in questo caso terminatore); si dice
pertanto che una stringa un "array di tipo char null terminated".

Definizione di variabili stringa

Consideriamo il seguente esempio. L'istruzione:


char MiaVar[30];
definisce la variabile MiaVar come array di tipo char con massimo 30
elementi, ma non ancora come stringa. Affinch MiaVar sia identificata da
operatori e funzioni come stringa, dobbiamo non solo definire una variabile
array di tipo char, ma anche inserire nell'array una serie di caratteri terminata
da un NULL.
Per esempio, se vogliamo che MiaVar presenti a operatori e funzioni la
stringa "Ciao", dobbiamo scrivere le istruzioni:
MiaVar[0] = 'C';
MiaVar[1] = 'i';
MiaVar[2] = 'a';
MiaVar[3] = 'o';
MiaVar[4] = '\0';
impegnando cos 5 elementi dell'array dei 30 disponibili (i rimanenti 25 saranno
ignorati).

Inizializzazione di variabili stringa

Bench le stringhe non siano ammesse nelle operazioni di assegnazione, lo


sono in quelle di inizializzazione (il che conferma che si tratta di due operazioni
diverse!):
Sequenza non valida
char Saluto[10];

Sequenza valida
char Saluto[10] = "Ciao";

Saluto = "Ciao";
Nelle inizializzazioni si utilizzano le costanti stringa, i cui caratteri vengono
inseriti nei primi elementi dell'array dichiarato; il terminatore viene aggiunto
automaticamente nell'elemento successivo a quello in cui stato inserito l'ultimo
carattere. La stringa pu essere "allungata" fino a un massimo di caratteri
(terminatore compreso) pari alla dimensione dell'array.
E' anche possibile inizializzare una stringa come un normale array; in questo
caso, per, il terminatore deve essere inserito esplicitamente:
char Saluto[] = { 'C', 'i', 'a', 'o', '\0' };
ovviamente questa seconda forma, inutilmente pi "faticosa", non mai usata!
Se nella inizializzazione si omette la dimensione dell'array, questa viene
automaticamente definita dalla lunghezza della costante stringa aumentata di
uno, per far posto al terminatore (in questo caso la stringa non pu pi essere
"allungata"!):
char Saluto[] = "Ciao";
elementi).

(allocato in memoria array con 5

In caso che si creino delle stringhe con un numero di caratteri (compreso il


terminatore) maggiore di quello dichiarato, il programma non produce
direttamente messaggi di errore, ma invade zone di memoria non di sua
pertinenza, con conseguenze imprevedibili (spesso si verifica un errore fatale a
livello di sistema operativo).

Funzioni di libreria gets e puts

Bench le funzioni gets e puts facciano parte della libreria di I/O del C, il loro
uso abbastanza frequente anche in programmi C++, a causa di alcune
peculiarit che le distinguono da tutte le altre funzioni di I/O.
La funzione gets(argomento) trasferisce l'intero buffer di input di stdin nella
variabile stringa argomento, riconoscendo come unico terminatore il carattere
new-line ('\n') (che sempre l'ultimo carattere del buffer) e sostituendolo con
il carattere NULL ('\0'). Ne consegue che la stringa pu contenere anche blanks
e tabulazioni (a differenza dalle stringhe lette mediante cin o le altre funzioni
di input del C). In pratica, la gets legge da tastiera un'intera riga di testo,
compreso il ritorno a capo che trasforma nel terminatore della stringa.
La funzione puts(argomento) trasferisce in stdout il contenuto della variabile
stringa argomento, sostituendo il terminatore di stringa NULL con il carattere
new-line. In pratica, la puts scrive su video un'intera riga di testo, compreso il
ritorno a capo.
In entrambi i casi la variabile argomento deve essere stata definita (nel
programma chiamante) come array di tipo char.

Conversioni fra stringhe e numeri

Le conversioni fra stringhe (contenenti caratteri numerici) e numeri (e


viceversa) non si possono fare direttamente mediante casting, in quanto le
stringhe sono degli array e, in pi, ogni elemento che le costituisce
convertibile nel corrispondente codice ascii, non nel valore numerico della cifra
rappresentata.
Es.: int('1') da' come risultato il numero 49 (codice ascii del carattere 1) e non
il numero 1
Bisogna invece ricorrere a opportune funzioni di libreria.

Conversioni da stringhe a numeri - Le funzioni atoi e atof


Per convertire una stringa in un numero, la via pi semplice usare le funzioni
di libreria (del C) atoi e atof :

atoi(argomento), dove argomento una stringa contenente la


rappresentazione decimale di un numero intero, esegue la conversione di
argomento e restituisce un valore di tipo int
atof(argomento), dove argomento una stringa contenente la
rappresentazione decimale di un numero floating (in notazione normale o

esponenziale), esegue la conversione di argomento e restituisce un valore


di tipo double
Entrambe le funzioni vanno utilizzate includendo l'header-file: <stdlib.h>
Il processo di conversione si interrompe (con il numero calcolato fino a quel
momento) appena incontrato un carattere non valido (senza messaggi di
errore). Se nessun carattere convertito atoi e atof ritornano rispettivamente 0
e 0.0

Conversioni da numeri a stringhe - La funzione sprintf


Per convertire numeri in stringhe, pi conveniente (rispetto ad altre
possibilit) usare la funzione di libreria (del C) sprintf. Infatti questa funzione,
non solo esegue la conversione, ma permette anche di ottenere una stringa
formattata nel modo desiderato.
All'inizio di questo corso abbiamo trattato della funzione printf, che utilizza gli
specificatori di formato per scrivere dati sul dispositivo standard di output
(stdout). La funzione sprintf identica alla printf salvo il fatto che scrive in una
stringa anzich su stdout. Richiede due argomenti fissi, seguiti da un numero
qualsiasi di argomenti opzionali:

il primo argomento la variabile stringa (definita come array di tipo


char) in cui inserire i dati formattati
il secondo argomento la control-string (come il primo della printf)
il terzo argomento e i successivi sono i dati da formattare (come il
secondo e i successivi della printf)

Le stringhe in C++

Le stringhe descritte finora sono quelle "in stile C". In C++ si usano ancora, ma
si ricorre pi spesso alla classe string della Libreria Standard, che offre
maggiori flessibilit e incapsula tutte le funzioni di manipolazione delle stringhe.

Funzioni
Definizione di una funzione

Una funzione cos definita:


tipo nome(argomenti)
{
... istruzioni ... (dette: codice di implementazione della funzione)
}
(notare che la prima istruzione senza punto e virgola, in quanto completata
dall'ambito che segue)

tipo: il tipo del valore di ritorno della funzione (con eventuali


specificatori e/o qualificatori), detto anche tipo della funzione; se la
funzione non ha valore di ritorno, bisogna specificare void
nome: l'identificatore della funzione; segue le regole generali di
specifica degli identificatori
argomenti: lista degli argomenti passati dal programma chiamante;
se non vi sono argomenti, si pu specificare void (o, pi comodamente,
non scrivere nulla fra le parentesi)

Gli argomenti vanno specificati insieme al loro tipo (come nelle dichiarazioni
delle variabili) e, se pi d'uno, separati con delle virgole.
Es.

char MiaFunz(int dato, float valore)


la funzione MiaFunz riceve dal programma chiamante gli argomenti:
dato (di tipo int), e valore (di tipo float), e ritorna un risultato di tipo
char

Dichiarazione di una funzione

Se in un file di codice sorgente una funzione chiamata prima di essere


definita, bisogna dichiararla prima di chiamarla.
La dichiarazione di una funzione (detta anche prototipo) un'unica
istruzione, formalmente identica alla prima riga della sua definizione, salvo il

fatto che deve terminare con un punto e virgola. Tornando all'esempio precedente
la dichiarazione della funzione MiaFunz :
char MiaFunz(int dato, float valore);
Nella dichiarazione di una funzione i nomi degli argomenti sono fittizi e non
necessario che coincidano con quelli dalla definizione (non neppure necessario
specificarli); invece i tipi sono obbligatori: devono coincidere ed essere nello
stesso ordine di quelli della definizione. Es., un'altra dichiarazione valida della
funzione MiaFunz :
char MiaFunz(int, float);
NOTA IMPORTANTE
La tendenza dei programmatori in C++ di separare le dichiarazioni dalle altre
istruzioni di programma: le prime, che possono riguardare non solo funzioni, ma
anche costanti predefinite o definizioni di tipi astratti, sono sistemate in
header-files (con estensione del nome .h), le seconde in implementation-files
(con estensione .c, .cpp o .cxx); ogni implementation-file che contiene
riferimenti a funzioni (o altro) dichiarate in header-files, deve includere
quest'ultimi mediante la direttiva #include.

Istruzione return

Nel codice di implementazione di una funzione l'istruzione di ritorno al


programma chiamante :
return espressione;
il valore calcolato dell'espressione viene restituito al programma
chiamante come valore di ritorno della funzione (se il suo tipo non
coincide con quello dichiarato della funzione, il compilatore segnala un
errore, oppure, quando pu, esegue una conversione implicita, con
warning se c' pericolo di loss of data)
Non necessario che tale istruzione sia fisicamente l'ultima (e non neppure
necessario che ve ne sia una sola: dipende dalla presenza delle istruzioni di
controllo, che possono interrompere l'esecuzione della funzione in punti
diversi). Se la funzione non ha valore di ritorno (tipo void), bisogna
specificare return; (da solo). Questa istruzione pu essere omessa quando il
punto di ritorno coincide con la fine fisica della funzione.

Comunicazioni fra programma chiamante e funzione

Da programma chiamante a funzione


La chiamata di una funzione non di tipo void pu essere inserita come
operando in qualsiasi espressione o come argomento nella chiamata di
un'altra funzione (in questo caso il compilatore controlla che il tipo della
funzione sia ammissibile): la chiamata viene eseguita con precedenza rispetto
alle altre operazioni e al suo posto viene sostituito il valore di ritorno restituito
dalla funzione.
Il valore di ritorno pu non essere utilizzato dal programma chiamante,
come se la funzione fosse di tipo void; in questi casi (cio se la funzione di
tipo void, oppure il valore di ritorno non interessa), la chiamata non pu
essere inserita in una espressione, ma deve assumere la forma di un'istruzione a
se stante.
Quando esegue la chiamata di una funzione, il programma costruisce una copia
di ogni argomento, creando delle variabili locali nell'ambito della funzione
(passaggio degli argomenti per valore). Ci significa che tutte le modifiche,
fatte dalla funzione al valore di un argomento, hanno effetto soltanto
nell'ambito della funzione stessa.
Es.

funzione:

funz(int a) { ..... a = a+1; .... }

prog. chiamante:

int b = 0 ...... funz(b); .....

il programma, prima di chiamare funz, copia il valore della propria variabile b


nell'argomento a, che diventa una variabile locale nell'ambito di funz; per cui a
"muore" appena il controllo ritorna al programma e il valore di b resta invariato,
qualunque modifica abbia subito a durante l'esecuzione di funz.
A questa regola fa eccezione (per motivi che vedremo in seguito) il caso in cui gli
argomenti sono nomi di array (e quindi in particolare di stringhe). Per
trasmettere un intero array a una funzione (nel caso di singoli elementi non ci
sarebbe eccezione alla regola generale) bisogna inserire nella chiamata il nome
dell'array (senza parentesi quadre) e, corrispondentemente nella funzione la
dichiarazione di una variabile seguita dalla coppia di parentesi quadre. Non
serve specificare la dimensione in quanto la stessa gi stata dichiarata nel
programma chiamante (tuttavia, se l'array multidimensionale l'unico
indice che si pu omettere quello all'estrema sinistra). In questa situazione,
tutte le modifiche fatte ai singoli elementi dell'array vengono riprodotte sull'array
del programma chiamante.

Da funzione a programma chiamante

Quando il controllo torna da una funzione al programma chiamante, tramite


l'istruzione: return espressione;, il programma costruisce una copia del valore
calcolato dell'espressione (che "muore" appena termina la funzione), creando
un valore locale nell'ambito del programma chiamante.
Es.

nel programma chiamante:

..... int a = funz(); .......

nella funzione:

int funz() { ...... return b; .... }

funz restituisce al programma non la variabile b (che, in quanto locale in funz


muore appena funz termina), ma una sua copia, che sopravvive a funz e diventa
un valore locale (temporaneo, cio non identificato da un nome) del
programma chiamante, assegnato alla variabile a.
[p20]

Argomenti di default

In C++ consentito "inizializzare" un argomento: come conseguenza, se


nella chiamata l'argomento omesso, il suo valore assunto, di default,
uguale alla costante (o variabile globale) usata per l'inizializzazione. Questa
deve essere fatta un'unica volta (e quindi in generale nel prototipo della
funzione, ma non nella sua definizione). Es.
prototipo:

void scrive(char [ ] = "Messaggio di saluto");

chiamata:

scrive();

definizione:

void scrive(char ave[ ] ) { ............ }

equivale a: scrive("Messaggio di saluto");

Se una funzione ha diversi argomenti, di cui alcuni required (da specificare) e


altri di default, quelli required devono precedere tutti quelli di default.

Funzioni con overload

A differenza dal C, il C++ consente l'esistenza di pi funzioni con lo stesso


nome, che sono chiamate: "funzioni con overload". Il compilatore distingue

una funzione dall'altra in base alla lista degli argomenti: due funzioni con
overload devono differire per il numero e/o per il tipo dei loro argomenti.
Es.
funz(int); e funz(float);
verranno chiamate con lo stesso
nome funz, ma sono in realt due funzioni diverse, in quanto la prima ha un
argomento int, la seconda un argomento float.
Non sono ammesse funzioni con overload che differiscano solo per il tipo del
valore di ritorno ; n sono ammesse funzioni che differiscano solo per
argomenti di default.
Es.

void funz(int); e

int funz(int);

non sono accettate, in quanto generano ambiguit: infatti, in una chiamata tipo
funz(n), il programma non saprebbe se trasferirsi alla prima oppure alla seconda
funzione (non dimentichiamo che il valore di ritorno pu non essere utilizzato).
Es.

funz(int); e

funz(int, double=0.0);

non sono accettate, in quanto generano ambiguit: infatti, in una chiamata tipo
funz(n), il programma non saprebbe se trasferirsi alla prima funzione (che ha un
solo argomento), oppure alla seconda (che ha due argomenti, ma il secondo
pu essere omesso per default).
La tecnica dell'overload, comune sia alle funzioni che agli operatori, molto
usata in C++, perch permette di programmare in modo semplice ed efficiente:
funzioni che eseguono operazioni concettualmente simili possono essere
chiamate con lo stesso nome, anche se lavorano su dati diversi.
Es., per calcolare il valore assoluto di un numero, qualunque sia il suo tipo, si
potrebbe usare sempre una funzione con lo stesso nome (per esempio abs).

Funzioni inline

In C++ esiste la possibilit di chiedere al compilatore di espandere ogni


chiamata di una funzione con il codice di implementazione della funzione
stessa. Questo si ottiene premettendo alla definizione di una funzione lo
specificatore inline.
Es.

inline double cubo(double x) { return x*x*x ; }


ogni volta che il compilatore trova nel programma la chiamata:
cubo(espressione); la trasforma nell'istruzione : (espressione)
(espressione) * (espressione) ;

L'uso dello specificatore inline molto comune, in quanto permette di eliminare


il sovraccarico di lavoro dovuto alla gestione della comunicazione fra programma e
funzione. Se per il numero di chiamate della funzione molto elevato ed in
punti diversi del programma, il vantaggio potrebbe essere annullato dall'eccessivo
accrescimento della lunghezza del programma (il vantaggio invece evidente
quando vi sono poche chiamate ma inserite in cicli while o for: in questo caso lo
specificatore inline fa crescere di poco la dimensione del programma, ma il
numero delle chiamate in esecuzione pu essere molto elevato).
In ogni caso il compilatore si riserva il diritto di accettare o rifiutare lo
specificatore inline: in pratica, una funzione che consista di pi di 4 o 5 righe
di istruzioni viene compilata come funzione separata, indipendentemente dalla
presenza o meno dello specificatore inline.

Trasmissione dei parametri tramite l'area stack

Cenni sulle liste


In qualsiasi linguaggio di programmazione le liste di dati possono essere
accessibili in vari modi (per esempio in modo randomatico), ma esistono due
particolari categorie di liste caratterizzate da metodi di accesso ben definiti e
utilizzate in numerose circostanze:

le liste di tipo queue (coda), accessibili con il metodo FIFO (first in-first
out): il primo dato che entra nella lista il primo a essere servito; tipiche
queues sono le code davanti agli sportelli, le code di stampa (priorit a
parte) ecc...
le liste di tipo stack (pila), accessibili con il metodo LIFO (last in-first
out): l'ultimo dato che entra nella lista il primo a essere servito.

Uso dell'area stack


Nella trasmissione dei parametri fra programma chiamante e funzione
vengono utilizzate liste di tipo stack: quando una funzione A chiama una
funzione B, sistema in un'area di memoria, detta appunto stack, un pacchetto
di dati, comprendenti:
1. l'area di memoria per tutte le variabili automatiche di B;
2. la lista degli argomenti di B in cui copia i valori trasmessi da A;
3. l'indirizzo di rientro in A (cio il punto di A in cui il programma deve
tornare una volta completata l'esecuzione di B, trasferendovi l'eventuale
valore di ritorno).

La funzione B utilizza tale pacchetto e, se a sua volta chiama un'altra funzione


C, sistema nell'area stack un altro pacchetto, "impilato" sopra il precedente,
come nel seguente schema (tralasciamo le aree riservate alle variabili
automatiche):
Area stack

Commenti

Indirizzo di rientro in B
Argomento 1 passato a
La funzione B chiama la funzione C con due
C
argomenti
Argomento 2 passato a
C
Indirizzo di rientro in A
Argomento 1 passato a
B
La funzione A chiama la funzione B con tre
Argomento 2 passato a
argomenti
B
Argomento 3 passato a
B
Quando il controllo deve tornare da C a B, il programma fa riferimento all'ultimo
pacchetto entrato nello stack per conoscere l'indirizzo di rientro in B e,
eseguita tale operazione, rimuove lo stesso pacchetto dallo stack (cancellando
di conseguenza anche le variabili automatiche di C).
La stessa cosa succede quando il controllo rientra da B in A; dopodich lo stack
rimane vuoto.

Ricorsivit delle funzioni

Tornando all'esempio precedente, la trasmissione dei parametri attraverso lo


stack garantisce che il meccanismo funzioni comunque, sia che A, B e C siano
funzioni diverse, sia che si tratti della stessa funzione (ogni volta va a cercare
nello stack l'indirizzo di rientro nel programma chiamante e quindi non cambia
nulla se tale indirizzo si trova all'interno della stessa funzione).
Ne consegue che in C++ (come in C) le funzioni possono chiamare se stesse
(ricorsivit delle funzioni). Ovviamente tali funzioni devono sempre contenere
un'istruzione di controllo che, se si verificano certe condizioni, ha il compito di
interrompere la successione delle chiamate.
Esempio tipico di una funzione chiamata ricorsivamente quello del calcolo del
fattoriale di un numero intero:
int fact(int n) {

if ( n <= 1 ) return 1;
return n

fact(n-1);

<----- istr. di controllo


}

Fattoriale in pps

alternativamente, cio senza usare la ricorsivit, si produce codice meno


compatto:
int fact(int n) {
int i = 2, m = 1;
while ( i <= n ) m *= i++ ;
return m;

}
[p22]

Funzioni con numero variabile di argomenti

In C++ (come in C), tramite accesso allo stack, possibile gestire funzioni con
numero variabile di argomenti. Caso tipico la nota funzione printf, che ha un
solo argomento fisso (la control-string), seguito eventualmente dagli
argomenti opzionali (i dati da scrivere), il cui numero determinato in fase di
esecuzione, esaminando il contenuto della stessa control-string.
Le funzioni con numero variabile di argomenti vanno dichiarate e definite
con tre puntini (ellipsis) al posto della lista degli argomenti opzionali, che
devono sempre seguire quelli fissi (deve sempre esistere almeno un argomento
fisso).
Es.:
int funzvar(int a, float b, ...)
gli argomenti fissi della funzione funzvar sono due: a e b; a questi possono
seguire altri argomenti (in numero qualsiasi). Normalmente gli argomenti fissi
contengono l'informazione (come nella printf) sull'effettivo numero di argomenti
usati in una chiamata.
La funzione pu accedere al suo pacchetto di chiamata, contenuto nello
stack, per mezzo di alcune funzioni di libreria, i cui prototipi si trovano
nell'header-file <stdarg.h> ; per memorizzare i valori degli argomenti
opzionali trasmessi dal programma chiamante, la funzione deve procedere
nel seguente modo:

1. anzitutto deve definire una variabile, di tipo (astratto) va_list (creato in


<stdarg.h>), che serve per accedere alle singole voci dello stack
Es. :
va_list marker ;
2. poi deve chiamare la funzione di libreria va_start, per posizionarsi
nello stack sull'inizio degli argomenti opzionali.
Es. :
va_start(marker,b) ;
dove b l'ultimo degli
argomenti fissi;
3. poi, per ogni argomento opzionale che si aspetta di trovare, deve
chiamare la funzione di libreria va_arg
Es. :
c = va_arg(marker,int) ;
(notare che il secondo argomento di va_arg definisce il tipo
dell'argomento opzionale, il cui valore sar trasferito in c).
4. infine deve chiamare la funzione di libreria va_end per chiudere le
operazioni
Es. :
va_end(marker) ;

Cenni sulla Run Time Library

La libreria standard del C


La Run Time Library la libreria standard del C, usata anche dal C++, e
contiene diverse centinaia di funzioni.
Il codice di implementazione delle funzioni di libreria fornito in forma gi
compilata e risiede in files binari (.obj o .lib), mentre i prototipi sono disponibili
in formato sorgente e si trovano distribuiti in vari header-files (.h).
Il linker, lanciato da un ambiente di sviluppo, accede in genere automaticamente
ai codici binari della libreria. Il compilatore, invece, richiede che tutte le funzioni
usate in ogni file sorgente di un'applicazione siano espressamente dichiarate,
tramite inclusione dei corrispondenti header-files.

Principali categorie di funzioni della Run-time library


Elenchiamo le principali categorie in cui possono essere classificate le funzioni
della Run Time Library. Per informazioni sulle funzioni individualmente
consultare l'help dell'ambiente di sviluppo disponibile.
Categorie
Operazioni di Input/Output
Funzioni matematiche e statistiche

Header-files
<io.h> , <stdio.h>
<math.h> , <stdlib.h>

Attributi del carattere

<ctype.h>

Conversioni numeri-stringhe

<stdlib.h>

Gestione e manipolazione stringhe

<string.h>

Gestione dell'ambiente

<direct.h> , <stdlib.h>

Gestione degli errori

<stdio.h> , <stdlib.h>

Ricerca e ordinamento dati


Gestione della data e dell'ora
Gest. numero variabile di argomenti

<search.h> , <stdlib.h>
<time.h>
<stdarg.h>

Riferimenti
Costruzione di una variabile mediante copia

Riassumiamo i casi in cui una variabile (o pi in generale un oggetto) viene


costruita (creata) mediante copia di una variabile esistente dello stesso tipo:

una variabile definita e inizializzata con il valore di una costante o di


una variabile esistente;
l'argomento di una funzione passato by value (per valore) dal
programma chiamante alla funzione;
il valore di ritorno di una funzione passato by value dalla funzione
al programma chiamante.

Cosa sono i riferimenti ?

In C++ i riferimenti sono variabili introdotte dall'operatore di dichiarazione :


&
Il loro significato quello di occupare la stessa memoria delle variabili a cui si
riferiscono (in altre parole sono degli alias di altre variabili).
Si definiscono come nel seguente esempio: int & ref = var;
(dove var una variabile di tipo int precedentemente definita, oppure una
qualunque espressione che restituisce un l-value di tipo int) la variabile ref
un riferimento a var: qualsiasi modifica apportata a var si ritrova in ref (e
viceversa). I tipi di ref e var devono coincidere, non ammesso il casting in
nessun caso (anche quando i tipi sono in pratica gli stessi, come int e long).
L'insieme int & assume la connotazione di un nuovo tipo: il tipo di riferimento
a int.
Nota: nelle definizioni multiple & va ripetuto: in altre parole, l'operatore di
dichiarazione & va considerato, dal punto di vista sintattico, un prefisso
dell'identificatore e non un suffisso del tipo.
Va da s che i riferimenti vanno sempre inizializzati. L'inizializzazione, tuttavia,
non comporta la costruzione di una nuova variabile mediante copia, in quanto,
per il programma, si tratta sempre della stessa variabile (la differenza fra i nomi
"scompare" dopo la compilazione).
E' anche possibile dichiarare un riferimento con lo specificatore const :
const int & ref = var;

si pu sempre modificare var (e di conseguenza resta modificato ref), ma non si


pu modificare direttamente ref, che per questo viene anche detto alias di
riferimento a sola lettura.
I riferimenti dichiarati const si possono anche inizializzare con un non lvalue (e non necessariamente dello stesso tipo, purch convertibile
implicitamente). Es.:
int & ref = var+1;

non ammesso: var+1 non un l-value

const int & ref = var+1;

ammesso, anche se var non int

Ci possibile perch in questo caso il programma crea una variabile temporanea


di tipo int (chiamiamola temp) che inizializza con var+1 (dopo aver
convertito, se necessario, il tipo di var in int) e poi definisce:
const int & ref = temp;
La variabile temp persiste nello stesso ambito di ref, ma non accessibile e
quindi in questo caso ref non pu pi cambiare anche se cambia var.

Comunicazione per "riferimento" fra programma e funzione

L'uso pi frequente dei riferimenti si ha nelle comunicazioni fra funzione e programma


chiamante. Infatti, mentre in C il passaggio degli argomenti e del valore di ritorno avviene
sempre e soltanto by value, in C++ pu avvenire anche by reference (per riferimento).

Da programma chiamante a funzione


Un argomento passato a una funzione pu essere dichiarato come
riferimento:
funz(int& num)
in questo caso l'argomento passato by reference, cio non ne viene
costruita una copia, ma la variabile num un alias di riferimento della sua
corrispondente nel programma chiamante.
Ne consegue che ogni modifica apportata a num in funz viene effettuata anche
nel programma chiamante.
Es.:

funzione:

funz(int& a) { ..... a = a+1; .... }

prog. chiamante:

int b = 0; ...... funz(b); .....

alla fine in b si ritrova il valore 1


In base alle regole enunciate nel paragrafo precedente, il valore passato alla
funzione deve essere un l-value ed esattamente dello stesso tipo del
corrispondente argomento dichiarato nella funzione (a meno che non venga
dichiarato const, nel qual caso non ci sono restrizioni, purch sia ammessa la
conversione di tipo implicita).

Da funzione a programma chiamante


Anche il valore di ritorno restituito da una funzione pu essere dichiarato
come riferimento.
Es.:

nel programma chiamante:

..... funz( ); .......

nella funzione:

int& funz( ) { ...... return b; .... }

anche questa volta il valore passato by reference, cio non ne viene


costruita una copia, ma nel programma chiamante viene utilizzato
direttamente il riferimento al valore b restituito da funz (per le note regole b
deve essere un l-value, a meno che il valore di ritorno non sia dichiarato
const) .
In questo caso per, onde evitare errori in esecuzione, necessario che b
sopravviva a funz; ci possibile soltanto in uno dei seguenti tre casi:

b una variabile globale


b una variabile locale di funz, ma dichiarata static
b essa stessa un argomento di funz, a sua volta passato by reference.

Il valore di ritorno un l-value (se non dichiarato const). Questo significa


che la chiamata di una funzione che ritorna un valore by reference pu
essere messa a sinistra di un'operazione di assegnazione !!!
Ci possibile solo in C++ !

Direttive al Preprocessore
Cos' il preprocessore ?

In C++ (come in C), prima che il compilatore inizi a lavorare, viene attivato un
programma, detto preprocessore, che ricerca nel file sorgente speciali
istruzioni, chiamate direttive.
Una direttiva inizia sempre con il carattere # (a colonna 1) e occupa una sola
riga (non ha un terminatore, in quanto finisce alla fine della riga; riconosce per
i commenti, introdotti da // o da /*, e la continuazione alla riga successiva,
definita da \).
Il preprocessore crea una copia del file sorgente (da far leggere al
compilatore) e, ogni volta che incontra una direttiva, la esegue sostituendola
con il risultato dell'operazione. Pertanto il preprocessore, eseguendo le
direttive, non produce codice binario, ma codice sorgente per il compilatore.
Ogni file sorgente, dopo la trasformazione operata dal preprocessore, prende
il nome di translation unit. Ogni translation unit viene poi compilata
separatamente, con la creazione del corrispondente file oggetto, in codice
binario. Spetta al linker, infine, collegare tutti i files oggetto, generando un
unico programma eseguibile.
Nel linguaggio esistono molte direttive (alcune delle quali dipendono dal sistema
operativo). In questo corso tratteremo soltanto delle seguenti: #include ,
#define , #undef e direttive condizionali.

Direttiva #include

Ci gi noto il significato della direttiva #include:


#include <filename>

oppure

#include "filename"

che determina l'inserimento, nel punto in cui si trova la direttiva, dell'intero


contenuto del file con nome filename.
Se si usano le parentesi angolari, si intende che filename vada cercato nella
directory di default del linguaggio; se invece si usano le virgolette, il file si trova
nella directory del programma.

La direttiva #include viene usata quasi esclusivamente per inserire gli headerfiles (.h) ed particolarmente utile quando in uno stesso programma ci sono pi
implementation-files che includono lo stesso header-file.

Direttiva #define di una costante

Quando il preprocessore incontra la seguente direttiva:


#define identificatore valore
dove, identificatore un nome simbolico (che segue le regole generali di
specifica di tutti gli altri identificatori) e valore un'espressione qualsiasi,
delimitata a sinistra da blanks o tabs e a destra da blanks, tabs o new-line (i
blanks e tabs interni fanno parte di valore), sostituisce identificatore con
valore in tutto il file (da quel punto in poi).
Es.

#define bla frase qualsiasi anche con "virgolette"

sostituisce (da quel punto in poi) in tutto il file la parola bla con la frase: frase
qualsiasi anche con "virgolette" (la "stranezza" dell'esempio riportato ha lo
scopo di dimostrare che la sostituzione assolutamente fedele e cieca, qualunque
sia il contenuto dell'espressione che viene sostituita all'identificatore; il
compito di "segnalare gli errori" viene lasciato al compilatore!)
In generale la direttiva #define serve per assegnare un nome a una costante
(che viene detta "costante predefinita").
Es.

#define ID_START 3457

da questo punto in poi, ogni volta che il programma deve usare il numero 3457,
si pu specificare in sua vece ID_START
Esistono principalmente due vantaggi nell'uso di #define:

se il programmatore decide di cambiare valore a una costante, sufficiente


che lo faccia in un solo punto del programma;
molto spesso i nomi sono pi significativi e mnemonici dei numeri (oppure
pi brevi delle stringhe, se rappresentano costanti stringa) e perci
l'uso delle costanti predefinite permette una maggiore leggibilit del
codice e una maggiore efficienza nella programmazione.

In pratica la direttiva #define produce gli stessi risultati dello specificatore di


tipo const; al posto della direttiva dell'esempio precedente si sarebbe potuto
scrivere la dichiarazione:

const int ID_START = 3457;

Confronto fra la direttiva #define e lo specificatore const

Vantaggi nell'uso di const:

il tipo della costante dichiarato; un eventuale errore di dichiarazione


viene segnalato immediatamente;
la costante riconosciuta, e quindi analizzabile, nelle operazioni di debug.

Vantaggi nell'uso di #define:

una costante predefinita a volte pi comoda e immediata ( una


questione sostanzialmente "estetica"!) e pu essere usata anche per altri
scopi (per esempio per sostituire o mascherare nomi).

Direttiva #define di una macro

Quando il preprocessore incontra la seguente direttiva:


#define identificatore(argomenti) espressione
riconosce una macro, che distingue dalla definizione di una costante per la
presenza della parentesi tonda subito dopo identificatore (senza blanks in
mezzo).
Una macro molto simile a una funzione. Il suo uso chiarito dal seguente
esempio:
#define Max(a,b) a > b ? a : b
tutte le volte che il preprocessore trova nel programma una chiamata della
macro, per esempio Max(x,y), la espande, sostituendola con: x > y ? x : y
Come nel caso di definizione di una costante, anche per una macro la
sostituzione avviene in modo assolutamente fedele: a parte i nomi degli
argomenti, che sono ricopiati dalla chiamata e non dalla definizione, tutti gli
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
nella definizione).
Nella chiamata di una macro si possono mettere, al posto degli argomenti,
anche delle espressioni (come nelle chiamate di funzioni); sar compito,
come al solito, del compilatore controllare che l'espressione risultante sia
accettabile. Riprendendo l'esempio precedente, la seguente chiamata:
Max(x+1,y)
espansa in
x+1 > y ? x+1 : y
sar accettata dal compilatore, in istruzioni del tipo :
c = Max(x+1,y);
ma rigettata in istruzioni come:
Max(x+1,y) = c;
in quanto, in questo caso, gli operandi di un operatore condizionale devono
essere l-values.
In altri casi, la sostituzione "cieca" pu causare errori che lo stesso compilatore
non in grado di riconoscere.
Es.
#define quadrato(x) x*x
la chiamata: quadrato(2+3) viene espansa in 2+3*2+3 con risultato,
evidentemente, errato.
Per evitare tale errore si sarebbe dovuto scrivere:
#define quadrato(x)
(x)*(x)
Agli effetti pratici (purch si usino le dovute attenzioni!), la definizione di una
macro produce gli stessi risultati dello specificatore inline di una funzione.

Confronto fra la direttiva #define e lo specificatore inline

Vantaggi nell'uso di inline:

il tipo della funzione dichiarato e controllato ;


la funzione riconosciuta, e quindi analizzabile, nelle operazioni di
debug;
l'espansione di una funzione inline fatta non in modo "cieco", ma in
modo "intelligente" (vantaggio decisivo!).

Vantaggi nell'uso di #define:

Una macro e pi immediata e pi semplice da scrivere di una funzione.

Le macro, usatissime in C, sono raramente utilizzate in C++, se non per


funzioni molto brevi e adoperate a livello locale (cio nello stesso modulo in cui
sono definite). Un uso pi frequente delle macro si ha quando non
corrispondono a funzioni ma a espressioni "parametrizzate" molto lunghe che
compaiono pi volte nel programma.

Direttive condizionali

Il preprocessore dispone di un suo mini-linguaggio di controllo, che consiste


nelle seguenti direttive condizionali:
#if espressione1

oppure

#if defined(identificatore1)

oppure ...

#if !defined(identificatore1)
...... blocco di direttive e/o istruzioni ........
#elif espressione2

oppure

#elif defined(identificatore2) oppure ...


#elif !defined(identificatore2)

...... blocco di direttive e/o istruzioni ........


#else
...... blocco di direttive e/o istruzioni ........
#endif
dove: espressione un espressione logica che pu contenere solo identificatori
di costanti predefinite o costanti literals, ma non variabili e neppure
variabili dichiarate const, che il preprocessore non riconosce
defined(identificatore) restituisce vero se identificatore definito
(cio se stata eseguita la direttiva: #define identificatore); al posto di
#if defined(identificatore) si pu usare la forma: #ifdef identificatore
!defined(identificatore) restituisce vero se identificatore non
definito; al posto di #if !defined(identificatore) si pu usare la forma:
#ifndef identificatore
#elif sta per else if ed opzionale (possono esserci pi blocchi
consecutivi, ciascuno introdotto da un #elif)
#else opzionale (se esiste, deve introdurre l'ultimo blocco prima di
#endif)
#endif (obbligatorio) termina la sequenza iniziata con un #if
non necessario racchiudere i blocchi fra parentesi graffe, perch ogni blocco
terminato da #elif, o da #else, o da #endif
Il preprocessore identifica il blocco (se esiste) che corrisponde alla prima
condizione risultata vera, oppure il blocco relativo alla direttiva #else (se esiste)
nel caso che tutte le condizioni precedenti siano risultate false. Tale blocco pu
contenere sia istruzioni di programma che altre direttive, comprese direttive
condizionali (possono esistere pi blocchi #if "innestati"): il preprocessore
esegue le direttive e presenta al compilatore le istruzioni che si trovano nel

blocco selezionato, scartando sia direttive che istruzioni contenute negli altri
blocchi della sequenza #if ... #endif.

Direttiva #undef

La direttiva:
#undef identificatore
indica al preprocessore di disattivare l'identificatore specificato, cio
rimuovere la corrispondenza fra l'identificatore e una costante,
precedentemente stabilita con la direttiva:
#define identificatore costante
Nelle istruzioni successive alla direttiva #undef, lo stesso nome potr essere
adibito ad altri usi.
Es.

#ifdef EOF
#undef EOF
#endif
char EOF[] = "Ente Opere Filantropiche";

Sviluppo delle applicazioni in ambiente Windows


Definizioni di IDE e di "progetto"

Un IDE (Integrated Development Environment) un programma interattivo che


si lancia da sistema operativo e che aiuta lo sviluppatore di software (cio il
programmatore) a costruire un progetto.
Un progetto un insieme di files, contenenti codice sorgente, che vengono
letti ed elaborati dal compilatore separatamente e poi collegati insieme (tramite
il linker) per costruire un unico file in codice binario, contenente il programma
eseguibile, che pu essere a sua volta lanciato dallo stesso IDE o
autonomamente da sistema operativo.
Lo sviluppatore interagisce con IDE tramite men di tipo pop-up (a tendina); in
genere le voci di men pi significative sono selezionabili anche tramite toolbars
(gruppi di icone) o tramite i cosiddetti acceleratori (tasti della keyboard che
eseguono la stessa funzione della corrispondente voce di men).
Un IDE pu aprire sullo schermo e usare parecchie finestre
contemporaneamente, contenenti i files sorgente (uno per ogni finestra), l'output
del programma, le informazioni acquisite in fase di debug ecc Possono esistere
anche finestre che contengono l'elenco dei files, delle funzioni, o anche delle
singole variabili utilizzate; "cliccando" su queste voci si pu raggiungere
rapidamente la parte di programma che interessa esaminare o modificare.
Nel seguito illustreremo brevemente l'utilizzo del seguente IDE: Microsoft
Visual C++, versione 6, che gira nel sistema operativo Windows. Teniamo a
precisare che il Visual C++ non soltanto un IDE, ma un linguaggio vero e
proprio, essendo dotato di funzionalit e librerie che vanno ben oltre lo standard
C++. Noi ci limiteremo, per, ad illustrare il suo ambiente di sviluppo, nella
versione "ridotta" per applicazioni che utilizzano solo codice standard.

Gestione di files e progetti

creazione nuovo progetto o nuovo file


apertura e chiusura progetto
inserimento di files esistenti nel progetto aperto
apertura, salvataggio (con eventuale cambiamento del nome) e chiusura file
tutte le operazioni di selezione di file o directory sono eseguibili tramite dialog box
oppure direttamente dalla lista dei MRU (Most Recently Used)

Editor di testo

Un IDE normalmente provvisto di tutte le funzionalit standard di un editor di


testo interattivo (cut, copy, paste, delete, find, replace, undo, redo, ecc). In pi,
il suo editor "intelligente", nel senso che in grado di riconoscere ed
interpretare il testo in modo da renderlo di pi facile comprensione (per esempio,
scrive le parole-chiave con un altro colore, "indenta" automaticamente le
istruzioni che continuano nella riga successiva o che appartengono ad un ambito
interno ecc...).

Gestione delle finestre

full screen della finestra attiva

selezione della finestra da porre in primo piano


visione contemporanea di pi finestre (allineate orizzontalmente, verticalmente o in
cascade) ecc

Costruzione dell'applicazione eseguibile

file make: creato e aggiornato automaticamente; contiene tutte le relazioni fra i files
sorgente e le opzioni di compilazione e link del progetto
programma make: legge il file make ed esegue:
o la compilazione di tutti i files del progetto, creando un file binario .obj per ogni
file sorgente incluso nel progetto; inoltre la compilazione di tipo
incrementale, nel senso che ricompila solo i files sorgente che sono stati modificati
dopo la creazione dei rispettivi .obj
o il link di tutti i .obj per la creazione del programma eseguibile, che ha
estensione .exe; anche in questo caso l'operazione di tipo incrementale, cio
viene eseguita solo se almeno un .obj stato modificato (o se il .exe non esiste).

Debug del programma

Eseguendo il programma in modo debug, possibile inserire dei breakpoints


(punti di interruzione del programma) direttamente nel codice sorgente e poi
esaminare il valore corrente delle variabili (con il comando watch, oppure
semplicemente posizionando il cursore del mouse sulla variabile da ispezionare: si
apre una finestrella gialla (tip) che mostra il contenuto della variabile), oppure
eseguire il programma step-by-step (una istruzione alla volta) ecc

Utilizzo dell'help in linea

Ogni buon IDE provvisto di un robusto sistema di documentazione che spiega il


significato e il modo di utilizzo delle parole-chiave del linguaggio, dei simboli,
delle variabili predefinite e, ovviamente, delle funzioni di libreria; di solito
organizzato per topics (argomenti), ma esiste anche la possibilit di eseguire la
ricerca di ogni singolo termine presente del sistema accedendo a un elenco
generale in ordine alfabetico.
Inoltre disponibile il "context sensitive help" che permette di accedere
direttamente all'informazione desiderata posizionando il cursore del mouse
all'interno della finestra di editor del proprio file sorgente, sopra la variabile o
funzione da esaminare, e poi spingendo il tasto F1.
Infine il testo della guida in linea accessibile con la funzionalit copy dell'editor
(ovviamente non con cut o paste, essendo in sola lettura): ci consente di
selezionare e trasferire nel proprio programma brani di codice (per esempio nomi
di funzioni o variabili predefinite) senza possibilit di errore.

Indirizzi e Puntatori
Operatore di indirizzo &

L'operatore unario di indirizzo :


&
restituisce l'indirizzo della locazione di memoria dell'operando.
L'operando deve essere un ammissibile l-value. Il valore restituito
dall'operatore non pu essere usato come l-value (in quanto l'indirizzo di
memoria di una variabile non pu essere assegnato in un'istruzione, ma
predeterminato dal programma).
Esempi (notare l'uso delle parentesi per alterare l'ordine delle precedenze):
&a

ammesso, purch a sia un l-value

&(a+1)

non ammesso, in quanto a+1 non un l-value

&(a>b?a:b)

ammesso, in quanto l'operatore condizionale pu restituire un


l-value,
purch a e b siano l-values

&a = b

non ammesso, in quanto l'operatore & non pu restituire un lvalue

Gli indirizzi di memoria sono rappresentati da numeri interi, in byte, e, nelle


operazioni di output, sono scritti, di default, in forma esadecimale.

Cosa sono i puntatori ?

I puntatori sono particolari tipi del linguaggio. Una variabile di tipo puntatore
designata a contenere l'indirizzo di memoria di un'altra variabile (detta
variabile puntata), la quale a sua volta pu essere di qualunque tipo, anche
non nativo (persino un altro puntatore!).

Dichiarazione di una variabile di tipo puntatore

Bench gli indirizzi siano numeri interi e quindi una variabile puntatore possa
contenere solo valori interi, tuttavia il C++ (come il C) pretende che nella
dichiarazione di un puntatore sia specificato anche il tipo della variabile
puntata (in altre parole un dato puntatore pu puntare solo a un determinato
tipo di variabili, quello specificato nella dichiarazione).
Per ottenere ci, bisogna usare l'operatore di dichiarazione :
Es. :

int * pointer

dichiara (e definisce) la variabile pointer, puntatore a


variabile di tipo int

Nota: nelle definizioni multiple * va ripetuto: in altre parole, l'operatore di


dichiarazione * va considerato, dal punto di vista sintattico, un prefisso
dell'identificatore e non un suffisso del tipo.
Si pu dire pertanto che, a questo punto della nostra conoscenza, il numero dei
tipi del C++ "raddoppiato": esistono tanti tipi di puntatori quanti sono i tipi
delle variabili puntate.
Un puntatore accetta quasi sempre il casting, purch il risultato della
conversione sia ancora un puntatore. Tornando all'esempio precedente,
l'operazione di casting:
(double*)pointer
restituisce un puntatore a una variabile di tipo double.
Nota2: nel casting, invece, l'operatore di dichiarazione * un suffisso del
tipo. (!)
Si pu anche dichiarare un puntatore a puntatore.
Es. :

double** pointer_to_pointer
dichiara (e definisce) la variabile pointer_to_pointer,
puntatore a puntatore a variabile di tipo double

Assegnazione di un valore a un puntatore

Sappiamo che gli indirizzi di memoria non possono essere assegnati da


istruzioni di programma, ma sono determinati automaticamente in fase di

esecuzione; quindi non si possono assegnare valori a un puntatore, salvo che in


questi quattro casi:

a un puntatore assegnato il valore NULL (non punta a "niente");


a un puntatore assegnato l'indirizzo di una variabile esistente,
restituito dall'operatore &
( Es. :
int a;
int* p;
p = &a; );
eseguita un'operazione di allocazione dinamica della memoria (di cui
tratteremo pi avanti);
a un puntatore assegnato il valore che deriva da un'operazione di
aritmetica dei puntatori (vedere prossima sezione).

Quanto detto per le assegnazioni vale anche per le inizializzazioni.


Va precisato, comunque, che ogni tentativo di assegnare valori a un puntatore
in casi diversi da quelli sopraelencati (per esempio l'assegnazione di una
costante) costituisce un errore che non viene segnalato dal compilatore, ma che
pu produrre effetti indesiderabili (o talvolta disastrosi) in fase di esecuzione.

Aritmetica dei puntatori

Abbiamo detto che il valore assunto da un puntatore un numero intero che


rappresenta, in byte, un indirizzo di memoria. Il C++ (come il C) ammette le
operazioni di somma fra un puntatore e un valore intero (con risultato
puntatore), oppure di sottrazione fra due puntatori (con risultato intero).
Tali operazioni vengono per eseguite in modo "intelligente", cio tenendo conto
del tipo della variabile puntata. Per esempio, se si incrementa un puntatore a
float di 3 unit, in realt il suo valore viene incrementato di 12 byte.
Queste regole dell'aritmetica dei puntatori assicurano che il risultato sia
sempre corretto, qualsiasi sia la lunghezza in byte della variabile puntata. Per
esempio, se p punta a un elemento di un array, p++ punter all'elemento
successivo, qualunque sia il tipo (anche non nativo) dell'array.

Operatore di dereferenziazione *

L'operatore unario di dereferenziazione * (che abbrevieremo in deref.) di un


puntatore restituisce il valore della variabile puntata dall'operando ed ha un
duplice significato:

usato come r-value, esegue un'operazione di estrazione.


Es. a = *p ; (assegna ad a il valore della variabile puntata da p)
usato come l-value, esegue un'operazione di inserimento.
Es. *p = a ; (assegna il valore di a alla variabile puntata da p)

In pratica l'operazione di deref. inversa a quella di indirizzo. Infatti, se


assegniamo a un puntatore p l'indirizzo di una variabile a,
p = &a ;
allora la relazione logica: *p == a
risulta vera, cio la deref. di p coincide
con a.
Ovviamente non detto il contrario, cio, se assegniamo alla deref. di p il
valore di a,
p = &b ;
*p = a ;
ci non comporta automaticamente che in p si ritrovi l'indirizzo di a (dove invece
resta l'indirizzo di b), ma semplicemente che il valore della variabile puntata
da p (cio b) coincider con a.

Puntatori a void

Contrariamente all'apparenza un puntatore dichiarato a void,


es.:
void* vptr;
pu puntare a qualsiasi tipo di variabile. Ne consegue che a un puntatore a
void si pu assegnare il valore di qualunque puntatore, ma non viceversa (
necessario operare il casting).
Es.:

definiti:

int* iptr;

void* vptr;

ammessa l'assegnazione:

vptr = iptr;

ma non:

iptr = vptr;

bens:

iptr = (int*)vptr;

I puntatori a void non possono essere dereferenziati n possono essere


inseriti in operazioni di aritmetica dei puntatori. In generale si usano quando il
tipo della variabile puntata non ancora stabilito al momento della
definizione del puntatore, ma determinato successivamente, in base al flusso
di esecuzione del programma.

Errori di dangling references

In C++ (come in C) l'assegnazione dell'indirizzo di una variabile a a un


puntatore p :
p = &a ;
e il successivo accesso ad a tramite deref. di p, possono portare a errori di
dangling references (perdita degli agganci) se puntatore e variabile
puntata non condividono lo stesso ambito d'azione. Infatti, se l'ambito di p
pi esteso di quello di a (per esempio se p una variabile globale) e a va out of
scope mentre p continua ad essere visibile, la deref. di p accede ad un'area della
memoria non pi allocata al programma, con risultati spesso imprevedibili.

Funzioni con argomenti puntatori

Quando, nella chiamata di una funzione, si passa come argomento un


indirizzo (sia che si tratti di una variabile puntatore oppure del risultato di
un'operazione di indirizzo), per esempio (essendo, al solito, p un puntatore e
a una qualsiasi variabile):
funz(.... p ....)

oppure

funz(.... &a ....)

nella definizione (e ovviamente anche nella dichiarazione) della funzione il


corrispondente argomento va dichiarato come puntatore; continuando
l'esempio (se a di tipo int):
void funz(.... int* p ....)
L'argomento , come sempre, passato by value. In C++ anche possibile,
passarlo by reference, nel qual caso bisogna indicare entrambi gli operatori di
dichiarazione * e & :
void funz(.... int*& p ....)
Se il puntatore passato by value, nella funzione viene creata una copia del
puntatore e, qualsiasi modifica venga fatta al suo valore, il corrispondente
valore nel programma chiamante rimane inalterato. In questo caso, tuttavia,
tramite l'operazione di deref., la variabile puntata (che si trova nel
programma chiamante), accessibile e modificabile dall'interno della
funzione.

Es.:

programma chiamante:

int a = 10; ...... funz(&a);

funzione:

void funz( int* p) { ....*p = *p+5; .... }

alla fine, nella variabile a si trova il valore 15 (in questo caso non esistono
problemi di scope, in quanto la variabile a, pur non essendo direttamente visibile
dalla funzione, ancora in vita e quindi accessibile tramite un'operazione di
deref.).
Per i motivi suddetti, quando l'argomento della chiamata un indirizzo, si dice
impropriamente che la variabile puntata trasmessa by address e che, per
questa ragione, modificabile. In realt l'argomento non la variabile
puntata, ma il puntatore, e questo trasmesso, come ogni altra variabile, by
value.

Puntatori ed Array
Analogia fra puntatori ed array

Quando abbiamo trattato gli array, avremmo dovuto fare le seguente riflessione:
"Il C++ un linguaggio tipato (ogni entit del linguaggio deve appartenere a un
tipo); e allora, cosa sono gli array ?".
La risposta :

"Gli array sono dei puntatori!".

Quando si dichiara un array, in realt si dichiara un puntatore, con alcune


caratteristiche in pi:

la dichiarazione di un puntatore comporta allocazione di memoria


per una variabile puntatore, ma non per la variabile puntata.
Es.: int* lista;

alloca memoria per la variabile puntatore lista ma non


per la variabile puntata da lista

la dichiarazione di un array comporta allocazione di memoria non


solo per una variabile puntatore (il nome dell'array), ma anche per
l'area puntata, di cui viene predefinita la lunghezza; inoltre il puntatore
viene dichiarato const e inizializzato con l'indirizzo dell'area puntata
(cio del primo elemento dell'array).
Es.:

int lista[5];

1.

alloca memoria per il puntatore


costante lista;
2.
alloca memoria per 5 valori di tipo int;
3.
inizializza lista con &lista[0]

Il fatto che il puntatore venga assunto const comporta che l'indirizzo


dell'array non modificabile e quindi il nome dell'array non pu essere
usato come l-value (mentre un normale puntatore s).
Esiste un'altra differenza fra la dichiarazione di un'array e quella di un
puntatore: in un array l'area puntata pu essere inizializzata tramite la lista
degli elementi dell'array, mentre in un puntatore ci non ammesso. A questa
regola fa eccezione il caso di un puntatore a char quando l'area puntata
inizializzata mediante una stringa literal (per compatibilit con vecchie
versioni del linguaggio).
Es.:

char saluto[ ] = "Ciao";

ammesso - saluto const

char saluto[ ] = {'C','i','a','o','\0'};

ammesso - saluto const

char* saluto = {'C','i','a','o','\0'};

non ammesso

char* saluto = "Ciao";

ammesso !!! - saluto non const !!!

nell'ultimo caso, tuttavia, non concesso modificare la stringa (anche se


concesso modificare il puntatore!): il programma da' errore in fase di
esecuzione! Per esempio, se poniamo:
saluto[2] = 'c';
la stringa diventa correttamente "Cico" se saluto stato dichiarato array di
char, mentre risulta un errore di "access violation" della memoria (?!) se
saluto stato dichiarato puntatore a char. Conclusioni: non inizializzare mai
un puntatore a char con una stringa literal! (oppure farlo solo se si sicuri
che la stringa non verr mai modificata).

Combinazione fra operazioni di deref. e di incremento

Le operazioni di deref. e di incremento (o decremento) possono applicarsi


contemporaneamente allo stesso operando puntatore.
Es. :
*p++
In questo caso l'incremento opera sul puntatore e non sulla variabile
puntata e, al solito, agisce prima della deref. se prefisso, oppure dopo la
deref. se suffisso. Da notare che l'espressione nel suo complesso pu essere
un l-value, mentre il semplice incremento (o decremento) di una variabile non
lo . Infatti, un'istruzione del tipo:
viene espansa in : *p = c ;
p = p+1 ;
*p++ = c ;
e quindi accettabile perch l'operazione di deref. pu essere un l-value,
mentre l'istruzione:
a++ = c ;
inaccettabile in quanto l'operazione di incremento non un l-value.

Confronto fra operatore [ ] e deref. del puntatore "offsettato"

Poich il nome (usato da solo) di un array ha il significato di puntatore al primo


elemento dell'array, ogni altro elemento accessibile tramite un'operazione
di deref. del puntatore-array "offsettato", cio incrementato di una quantit
pari all'indice dell'elemento. Da questo e dalle note regole di aritmetica dei
puntatori consegue che le espressioni (dato un array A):
A[i]

*(A+i)

conducono ad identico risultato e quindi sono perfettamente intercambiabili e


possono essere entrambe usate sia come r-value che come l-value.

Funzioni con argomenti array

Quando, nella chiamata di una funzione, si passa come argomento un array


(senza indici), in realt si passa un puntatore, cio l'indirizzo del primo
elemento dell'array e pertanto i singoli elementi sono direttamente modificabili
dall'interno della funzione. Questo spiega l'apparente anomalia di
comportamento degli argomenti array (e in particolare delle stringhe), a cui
abbiamo accennato trattando del passaggio degli argomenti by value.
Es. :

nel programma chiamante:

int A[ ] = {0,0,0};

.... funz(.... A,....) ; ....

nella funzione:

void funz(....int A[ ] , ....) { ....A[1] = 5;


....}

il secondo elemento dell'array A risulta modificato, perch in realt nella


funzione viene eseguita l'operazione: *(A+1)= 5 (il valore 5 viene inserito
nella locazione di memoria il cui indirizzo A+1).
Nella dichiarazione (e nella definizione) della funzione, un argomento
array pu essere indifferentemente dichiarato come array o come puntatore
(in questo caso non c' differenza perch la memoria gi allocata nel
programma chiamante). Tornando all'esempio, la funzione funz avrebbe
potuto essere definita nel seguente modo: void funz(....int* A, ....)
Le due dichiarazioni sono perfettamente identiche; di solito si preferisce la
seconda per evidenziare il fatto che il valore dell'argomento un indirizzo (il
puntatore creato per copia non mai assunto const, anche se l'argomento
dichiarato come array: resta comunque valida la regola che ogni modifica del suo
valore fatta sulla copia non si ripercuote sull'originale).

Funzioni con argomenti puntatori passati by reference

Quando un argomento puntatore dichiarato in una funzione come


riferimento,
es.
void funz(....int*& A, ....),
nel programma chiamante il corrispondente argomento non pu essere
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
l-value, a meno che non siano essi stessi dichiarati const nella funzione).

Array di puntatori

In C++ (come in C) i puntatori, come qualsiasi altra variabile, possono essere


raggruppati in array e definiti come nel seguente esempio:
int* A[10];

(definisce un array di 10 puntatori a int)

Come un array equivale a un puntatore, cos un array di puntatori equivale a


un puntatore a puntatore (con in pi l'allocazione della memoria puntata,
come nel caso di array generico). Se questo viene passato come argomento di
una funzione, nella stessa pu essere dichiarato indifferentemente come array
di puntatori o come puntatore a puntatore.
Continuando l'esempio precedente:
programma chiamante:

funz(.... A,....) ;

dichiarazione di funz:

void funz(....int** A, ....);

Il caso pi frequente di array di puntatori quello dell'array di stringhe, che


consente anche l'inizializzazione tramite l'elenco, non dei valori dei puntatori,
ma (atipicamente) delle stesse stringhe che costituiscono l'array.
Es.:

char* colori[3] = {"Blu", "Rosso", "Verde"} ;

Come appare nell'esempio, le stringhe possono anche essere di differente


lunghezza; in memoria sono allocate consecutivamente e, per ciascuna di esse,
sono riservati tanti bytes quant' la rispettiva lunghezza (terminatore
compreso). Da certi compilatori la memoria allocata per ogni stringa
arrotondata per eccesso a un multiplo di un numero prefissato di bytes.

Elaborazione della riga di comando


Esecuzione di un programma tramite riga di comando

Un caso tipico di utilizzo di array di stringhe si ha quando il sistema operativo


passa a un programma una serie di parametri, elencati nella riga di
comando.
Es.:

copy file1 file2

copy il programma
file1 e file2 sono i parametri

Anche un programma scritto in C++ (e trasformato dall'ambiente di sviluppo in


un modulo eseguibile) pu essere lanciato da sistema operativo come se fosse
un comando, e pu essere accompagnato da parametri. Il C++ (come il C) si
incarica di trasformare tali parametri in argomenti trasmessi alla funzione
main, per modo che il programma possa elaborarli.

Argomenti passati alla funzione main

Finora abbia supposto che il main fosse una funzione priva di argomenti. In
realt il sistema operativo passa al main un certo numero di argomenti, di
cui, in questo caso, ci interessano i primi due:
int argc

numero di voci presenti nella riga di comando (compreso lo


stesso nome del programma)

char** argv

array di stringhe, in cui ogni elemento corrisponde a una


voce della riga di comando (in fondo viene aggiunta una
stringa NULL)

Pertanto, se il programma deve utilizzare dei parametri, il main va definito


come segue:
int main(int argc, char** argv)
Per esempio, se la riga di comando contiene: copy file1 file2
argc contiene il numero 3
argv[0] contiene la stringa "copy"
argv[1] contiene la stringa "file1"
argv[2] contiene la stringa "file2"

argv[3] contiene NULL

Puntatori e Funzioni
Funzioni che restituiscono puntatori

Il valore di ritorno restituito da una funzione pu essere di qualsiasi tipo,


compreso il tipo puntatore.
Es.:

int* funz();

dichiara una funzione funz che restituisce un valore


puntatore a int

Come in generale, il valore di ritorno, anche se un puntatore, viene


trasmesso by value e quindi ne viene creata una copia nel programma
chiamante; ci garantisce che il puntatore sopravviva alla funzione anche se
stato creato all'interno del suo ambito.
Tuttavia la variabile puntata potrebbe non sopravvivere alla funzione (se
stata creata nel suo ambito e non dichiarata static). Ci porterebbe a un errore
di dangling references. Notare l'analogia con il tipo di errore generato quando
un valore di ritorno, trasmesso by reference, corrisponde a una variabile che
cessa di esistere: in quel caso tuttavia, il compilatore ha il controllo della
situazione e quindi pu segnalare l'errore (o almeno un warning); nel caso di un
puntatore, invece, il suo contenuto (cio l'indirizzo della variabile puntata)
determinato in esecuzione e quindi l'errore non pu essere segnalato dal
compilatore. Spetta al programmatore fare la massima attenzione a che ci non si
verifichi.
Il pi frequente uso di funzioni che restituiscono puntatori si ha nel caso di
puntatori a char, cio di stringhe. Nella stessa libreria Run-time ci sono molte
funzioni che restituiscono stringhe.
Esempio di funzione di libreria:
char* strcat(char* str1, char* str2);
concatena la stringa str2 alla stringa str1 e restituisce il risultato sia nella
stessa str1 che come valore di ritorno. Notare che in questo caso non c'
pericolo di errore, purch lo spazio di memoria per str1 sia stato adeguatamente
allocato nel programma chiamante.

Puntatori a Funzione

In C++ (come in C) esistono anche i puntatori a funzione! Questi servono


quando il programma deve scegliere quale funzione chiamare fra diverse
possibili, e la scelta non definita a priori ma dipende dai dati del programma
stesso. Questo processo si chiama late binding ("aggancio ritardato"): gli

indirizzi delle funzioni da chiamare non vengono risolti al momento della


compilazione, come avviene normalmente (early binding) ma al momento
dell'esecuzione.
I puntatori a funzione non devono essere definiti, ma solo dichiarati, come
nel seguente esempio:
int* (*pfunz)(double , char* );
dichiara un puntatore a funzione pfunz che restituisce un puntatore a int e
ha due argomenti: il primo di tipo double, il secondo un puntatore a
char. Notare le parentesi intorno al nome della funzione, in assenza delle quali
la dichiarazione sarebbe interpretata in modo diverso (una normale funzione
pfunz che restituisce un puntatore a puntatore a int).
Nel corso del programma il puntatore a funzione deve essere assegnato (o
inizializzato) con il nome di una funzione "vera", che deve essere
precedentemente dichiarata con lo stesso tipo del valore di ritorno e gli stessi
argomenti del puntatore. Continuando l'esempio precedente:
int* funz1(double , char* );
int* funz2(double , char* );
if ( ......... ) pfunz = funz1 ;
else pfunz = funz2;
notare che i nomi delle funzioni e del puntatore vanno indicati da soli, senza i
loro argomenti (e senza le parentesi).
In una chiamata della funzione, tutti i testi di C dicono che il puntatore va
dereferenziato (in realt non necessario):
(*pfunz)(12.3,"Ciao");
... ma va bene anche:
pfunz(12.3,"Ciao");

Array di puntatori a funzione

In C++ (come in C) consentito dichiarare array di puntatori a funzione,


nella forma specificata dal seguente esempio:
double (*apfunz[5])(int);
dichiara l'array apfunz di 5 puntatori a funzione, tutti con valore di ritorno
di tipo double e con un argomento di tipo int.
L'array pu essere inizializzato con un elenco di nomi di funzioni, gi
dichiarate e condividenti tutte le stesso tipo di valore di ritorno e gli stessi
argomenti:
double (*apfunz[5])(int) = {f1, f2, f3, f4, f5} ;

dove f1 ecc... sono tutte funzioni dichiarate: double f1(int); ecc...


I singoli elementi dell'array possono anche essere assegnati tramite
l'operatore [ ], che funziona come l-value nel modo consueto:
apfunz[3]= fn;
dove fn una funzione dichiarata: double fn(int);
Nelle chiamate, si usa ancora l'operatore [ ] per selezionare l'elemento
desiderato:
apfunz[i](n);
(non necessario dereferenziare il
puntatore)
dove l'indice i permette di accedere alla funzione precedentemente assegnata
all'i-esimo elemento dell'array.
Gli array di puntatori a funzione possono essere utili, per esempio, quando la
funzione da eseguire selezionata da un men: in questo caso l'indice i ,
corrispondente a una voce di men, indirizza direttamente la funzione prescelta,
senza bisogno di istruzioni di controllo, come if o switch, per determinarla.

Funzioni con argomenti puntatori a funzione

E' noto che, quando nella chiamata di una funzione compare come argomento
un'altra funzione, questa viene eseguita per prima e il suo valore di ritorno
utilizzato come argomento dalla prima funzione. Quindi il vero argomento
della prima funzione non la seconda funzione, ma un normale valore, che
pu avere qualsiasi origine (variabile, espressione ecc...), e in particolare in
questo caso il risultato dell'esecuzione di un'altra funzione (il cui tipo di valore
di ritorno deve coincidere con il tipo dichiarato dell'argomento).
Quando invece una funzione dichiara fra i suoi argomenti un puntatore a
funzione, allora sono parametrizzate proprio le funzioni e non i loro valori di
ritorno. Nelle chiamate necessario specificare come argomento il nome di
una funzione "vera", precedentemente dichiarata, che viene sostituito a quello
del puntatore.
Es.:

dichiarazioni:

void fsel(int (*)(float));


int funz1(float);
int funz2(float);

chiamate:

fsel(funz1);
fsel(funz2);

definizione fsel:

void fsel(int (*pfunz)(float))


{ .... n = pfunz(r); .....}
(dove n di tipo int e r di tipo float)

l'istruzione n=pfunz(r) viene sostituita la prima volta con n=funz1(r) e la


seconda volta con n=funz2(r) . Notare che, nelle chiamate, l'argomentofunzione deve essere a sua volta specificato senza argomenti e senza le
parentesi tonde.
Nell'esempio abbiamo supposto che la variabile r, argomento della pfunz, sia
creata all'interno della fsel; anche se r fosse passato dal programma
chiamante, la forma: fsel(funz1(r)) sarebbe comunque errata: l'unico modo
per passare r potrebbe essere quello di dichiararlo come ulteriore argomento
della fsel, cio:
void fsel(float, int (*pfunz)(float));
e nelle chiamate
specificare:
fsel(r, funz1);
...oppure...
fsel(r, funz2);

Puntatori e Costanti
Puntatori a costante

Nelle definizioni di una variabile puntatore, lo specificatore di tipo const


indica che deve essere considerata costante la variabile puntata (non il
puntatore!).
Es.:
const float* ptr;
definisce il puntatore variabile ptr a costante float.
In realt a un puntatore a costante si pu anche assegnare l'indirizzo di una
variabile, ma non vero il contrario: l'indirizzo di una costante pu essere
assegnato solo a un puntatore a costante. In altre parole il C++ accetta
conversioni da puntatore a variabile a puntatore a costante, ma non
viceversa.
L'operazione di deref. di un puntatore a costante non mai accettata come lvalue, anche se la variabile puntata non const.
Es.: int datov=50;

(datov una variabile int)

const int datoc=50;

(datoc una costante int)

int* ptv;

(ptv un puntatore a variabile int)

const int* ptc;

(ptc un puntatore a costante int)

ptc = &datov;

(valida, in quanto le conversioni da int* a const


int* sono ammesse)

ptv = &datoc;

(non valida, in quanto le conversioni da const int* a


int* non sono ammesse)

*ptc

(deref. l-value non valida, anche se ptc punta a


una variabile)

= 10;

datov=10; cout << *ptc;

(deref. r-value valida, scrive 10)

Puntatori costanti

I puntatori costanti si definiscono specificando l'operatore di dichiarazione


* const (al posto di *)

Es.:
float* const ptr;
definisce il puntatore costante ptr a variabile float
Un puntatore costante segue la regola di tutte le costanti: deve essere
inizializzato, ma non pu pi essere modificato (non un l-value). Resta lvalue, invece, la deref. di un puntatore costante che punta a una variabile.
Es.:

int dato1,dato2;

(dato1 e dato2 sono due variabili int)

int* const ptr =


&dato1;

(ptr un puntatore costante, inizializzato con


l'indirizzo di dato1)

*ptr

(valida, in quanto ptr punta a una variabile)

= 10;

ptr = &dato2;

(non valida, in quanto ptr costante)

Casi tipici di puntatori costanti sono gli array.

Puntatori costanti a costante

Ripetendo due volte const (come specificatore di tipo e come operatore di


dichiarazione), si pu definire un puntatore costante a costante (di uso
piuttosto raro).
Es.: const char dato='A';
const char* const ptr =
&dato;

(dato una costante char, inizializzata con 'A')


(ptr un puntatore costante a costante ,
inizializzato con l'indirizzo della costante
dato)

Nel caso di un puntatore costante a costante, non sono l-values n il


puntatore n la sua deref.

Funzioni con argomenti costanti trasmessi by value

Le regole di ammissibilit degli argomenti di una funzione, dichiarati const


(nella funzione e/o nel programma chiamante) e trasmessi by value, sono

riconducibili alle regole generali applicate a una normale dichiarazione con


inizializzazione; come noto, infatti, la trasmissione by value comporta una
creazione per copia, che equivale alla dichiarazione dell'argomento come
variabile locale della funzione, inizializzata con il valore passato dal
programma chiamante.
Quindi un argomento pu essere dichiarato const nel programma chiamante
e non nella funzione o viceversa, senza limitazioni (in quanto la creazione per
copia "separa i destini" delle due variabili), salvo in un caso: un puntatore a
costante non pu essere dichiarato tale nel programma chiamante se non lo
anche nella funzione (in quanto non sono ammesse le conversioni da puntatore
a costante a puntatore a variabile).
Es.:

void funz(int*);

void main() { const int* ptr; ... funz(ptr); ... }

(errore : l'argomento dichiarato puntatore a costante nel main e


puntatore a variabile nella funzione)
void funz(const int*);

void main() {int*ptr; ... funz(ptr); ... }

( ok! )

Da quest'ultimo esempio si capisce anche qual' l'uso principale di un puntatore


a costante: come argomento passato a una funzione, se non si desidera che
la variabile puntata subisca modifiche dall'interno della funzione stessa
(tramite operazioni di deref.), anche se ci possibile nel programma
chiamante.

Funzioni con argomenti costanti trasmessi by reference

Sappiamo che, se un argomento passato a una funzione by reference, non


ne viene costruita una copia, ma il suo nome nella funzione assunto come
alias del nome corrispondente nel programma chiamante (cio i due nomi si
riferiscono alla stessa locazione di memoria). Per questo motivo il controllo sui tipi
e pi rigoroso rispetto al caso di passaggio by value: in particolare, qualsiasi sia
l'argomento (puntatore o no), non ammesso dichiararlo const nel
programma chiamante e non nella funzione.
La dichiarazione inversa (const solo nella funzione) invece possibile, in quanto
corrisponde alla definizione di un alias di sola lettura: l'argomento, pur
essendo modificabile nel programma chiamante, non lo dall'interno della
funzione.
Il passaggio by reference di argomenti dichiarati const nella funzione in
uso molto frequente in C++, perch combina insieme due vantaggi: quello di

proteggere i dati del programma da modifiche indesiderate (come nel passaggio


by value), e quello di una migliore efficienza; infatti il passaggio by reference,
non comportando la creazione di nuove variabili, pi veloce del passaggio by
value.
Quando un argomento passato by reference ed dichiarato const nella
funzione, non esiste pi la condizione che nel programma chiamante il
corrispondente argomento sia un l-value (pu anche essere il risultato di
un'espressione).
Se si vuole dichiarare un argomento: puntatore costante passato by
reference, bisogna specificare entrambi gli operatori di dichiarazione *const
e & (nell'ordine)
Es.:

funz(int* const & ptr);


dichiara che l'argomento ptr un puntatore costante a variabile int,
passato by reference

Tipi definiti dall'utente


Il termine "tipo astratto", usato in contrapposizione ai tipi nativi del linguaggio, non molto
appropriato: il C++ consente al programmatore di definire nuovi tipi, estendendo cos le
capacit effettive del linguaggio; ma, una volta definiti, questi tipi sono molto "concreti" e sono
trattati esattamente come i tipi nativi. Per questo motivo, la tendenza "moderna" di
identificare i tipi non nativi con il termine: "tipi definiti dall'utente" e di confinare
l'aggettivo "astratto" a una precisa sottocategoria di questi (di cui parleremo pi avanti).
Tuttavia noi continueremo, per comodit, a usare la "vecchia" terminologia.
In questo capitolo parleremo dei tipi astratti comuni sia al C che al C++, usando per la
nomenclatura (oggetti, istanze ecc...) del C++.

Concetti di oggetto e istanza

Il termine oggetto sostanzialmente sinonimo del termine variabile. Bench


questo termine si usi soprattutto in relazione a tipi astratti (come strutture o
classi), noi possiamo generalizzare il concetto, definendo oggetto una variabile
di qualunque tipo, non solo formalmente definita, ma anche gi creata e
operante.
E' noto infatti che l'istruzione di definizione di una variabile non si limita a
dichiarare il suo tipo, ma crea fisicamente la variabile stessa, allocando la
memoria necessaria (nella terminologia C++ si dice che la variabile viene
"costruita"): pertanto la definizione di una variabile comporta la "costruzione"
di un oggetto.
Il termine istanza quasi simile al termine oggetto; se ne differenzia in quanto
sottolinea l'appartenenza dell'oggetto a un dato tipo (istanza di ... "qualcosa").
Per esempio, la dichiarazione/definizione:
int ivar ;
costruisce l'oggetto ivar, istanza del tipo int.
Esiste anche il verbo: istanziare (o instanziare) un certo tipo, che significa
creare un'istanza di quel tipo.

Typedef

L'istruzione introdotta dalla parola-chiave typedef definisce un sinonimo di un


tipo esistente, cio non crea un nuovo tipo, ma un nuovo identificatore di un
tipo (nativo o astratto) precedentemente definito.
Es.:
typedef unsigned long int* pul ;
definisce il nuovo identificatore di tipo pul, che potr essere usato, nelle
successive dichiarazioni (all'interno dello stesso ambito), per costruire
oggetti di tipo puntatore a unsigned long:
unsigned long a;

pul ogg1 = &a;

pul parray[100];

ecc...

L'uso di typedef permette di semplificare dichiarazioni lunghe di variabili dello


stesso tipo. Per esempio, supponiamo di dover dichiarare molti array, tutti dello
stesso tipo e della stessa dimensione:
double a1[100];

double a2[100];

double a3[100];

ecc...

usando typedef la semplificazione evidente:


typedef double a[100];

a a1;

a a2;

a a3;

Un caso in cui si evidenzia in modo eclatante l'utilit di typedef quello in cui si


devono dichiarare pi funzioni con lo stesso puntatore a funzione come
argomento.
Es.: typedef bool (*tpfunz)(const int&, int&, const char*, int&, char*&,
int&);
in questo caso tpfunz il nome di un tipo puntatore a funzione e pu essere
sostituito nelle dichiarazioni delle funzioni chiamanti al posto dell'intera
stringa di cui sopra:
void fsel1(tpfunz);
int fsel2(tpfunz); double fsel3(tpfunz);
ecc....
infine, nelle definizioni delle funzioni chiamanti bisogna specificare un
argomento di "tipo" tpfunz e usare questo per le chiamate. Es:
void fsel1(tpfunz pfunz) { ... if(pfunz(4,a,"Ciao",b,pc,m)) .... }
Un altro utilizzo di typedef quello di confinare in unico luogo i riferimenti diretti
a un tipo. Per esempio, se il programma lavora in una macchina in cui il tipo int
corrisponde a 32 bit e noi poniamo:
typedef int int32;
avendo cura poi di attribuire il tipo int32 a tutte le variabili intere che vogliamo a
32 bit, possiamo portare il programma su una macchina a 16 bit ridefinendo
solamente int32 :
typedef long int32;

ecc...

Strutture

Come gli array, in C++ (e in C) le strutture sono gruppi di dati; a differenza


dagli array, i singoli componenti di una struttura possono essere di tipo
diverso.
Esempio di definizione di una struttura:
struct anagrafico
{
char nome[20];
int anni;
char indirizzo[30];
};
Dopo la parola-chiave struct segue l'identificatore della struttura, detto
anche marcatore o tag, e, fra parentesi graffe, l'elenco dei componenti della
struttura, detti membri; ogni membro dichiarato come una normale
variabile ( una semplice dichiarazione, non una definizione, e pertanto non
comporta la creazione dell'oggetto corrispondente) e pu essere di qualunque
tipo (anche array o puntatore o una stessa struttura). Dopo la parentesi
graffa di chiusura, obbligatoria la presenza del punto e virgola (diversamente dai
blocchi delle funzioni).
In C++ (e non in C) la definizione di una struttura comporta la creazione di
un nuovo tipo, il cui nome coincide con il tag della struttura. Pertanto,
riprendendo l'esempio, anagrafico a pieno titolo un tipo (come int o double),
con la sola differenza che si tratta di un tipo astratto, non nativo del
linguaggio.
Per questo motivo l'enunciato di una struttura una definizione e non una
semplice dichiarazione: crea un'entit (il nuovo tipo) e ne descrive il
contenuto. Ma, diversamente dalle definizioni delle variabili, non alloca memoria,
cio non crea oggetti. Perch ci avvenga, il nuovo tipo deve essere
istanziato, esattamente come succede per i tipi nativi. Riprendendo l'esempio,
l'istruzione di definizione:
anagrafico ana1, ana2, ana3 ;
costruisce gli oggetti ana1, ana2 e ana3, istanze del tipo anagrafico. Solo
adesso viene allocata memoria, per ogni oggetto in quantit pari alla somma
delle memorie che competono ai singoli membri della struttura (l'operazione
sizeof(anagrafico), oppure sizeof(ana1) ecc..., restituisce il numero dei bytes
allocati ad ogni istanza di anagrafico).
La collocazione ideale della definizione di una struttura in un header-file:
conviene infatti separarla dalle sue istanze, in quanto la definizione deve essere
(di solito) accessibile dappertutto, mentre le istanze sono normalmente locali e

quindi limitate dal loro ambito di visibilit. Potrebbe per sorgere un problema:
se un programma suddiviso in pi files sorgente e tutti includono lo stesso
header-file contenente la definizione di una struttura, dopo l'azione del
preprocessore risulteranno diverse translation unit con la stessa definizione
e quindi sembrerebbe violata la "regola della definizione unica" (o ODR,
dall'inglese one-definition-rule). In realt, per la definizione dei tipi astratti
(e di altre entit del linguaggio, come i template, che vedremo pi avanti), la
ODR si esprime in modo meno restrittivo rispetto al caso della definizione di
variabili e funzioni (non inline): in questi casi, due definizioni sono ancora
ritenute esemplari della stessa, unica, definizione, se e solo se:
1. appaiono in differenti translation units ,
2. sono identiche nei rispettivi elementi lessicali,
3. il significato dei rispettivi elementi lessicali lo stesso in entrambe le

translation units

e tali condizioni sono senz'altro verificate se due files sorgente includono lo


stesso header-file (purch in uno dei due non si alteri il significato dei nomi con
typedef o #define !).

Operatore .

La grande utilit delle strutture consiste nel fatto che i nomi delle sue istanze
possono essere usati direttamente come operandi in molte operazioni o come
argomenti nelle chiamate di funzioni, consentendo un notevole risparmio,
soprattutto quando il numero di membri elevato.
In alcune operazioni, tuttavia, necessario accedere a un membro
individualmente. Ci possibile grazie all'operatore binario . di accesso al
singolo membro: questo operatore ha come left-operand il nome
dell'oggetto e come right-operand quello del membro.
Es.:
ana2.indirizzo
Come altri operatori che svolgono compiti analoghi (per esempio l'operatore [ ]
di accesso al singolo elemento di un array), anche l'operatore . pu
restituire sia un r-value (lettura di un dato) che un l-value (inserimento di un
dato).
Es.:

int a = ana1.anni; inizializza a con il valore del membro anni dell'oggetto


ana1
ana3.anni = 27;

inserisce 27 nel membro anni dell'oggetto ana3

Puntatori a strutture - Operatore ->

Come tutti i tipi del C++ (e del C), anche i tipi astratti, e in particolare le
strutture, hanno i propri puntatori. Per esempio (notare le differenze):
int* p_anni = &ana1.anni;
anagrafico* p_anag = &ana1;
nel primo caso definisce un normale puntatore a int, che inizializza con
l'indirizzo del membro anni dell'oggetto ana1; nel secondo caso definisce
un puntatore al tipo-struttura anagrafico, che inizializza con l'indirizzo
dell'oggetto ana1.
Per accedere a un membro di un oggetto (istanza di una struttura) di cui
dato il puntatore, bisogna eseguire un'operazione di deref. . Riprendendo
l'esempio precedente, si potrebbe pensare che la forma corretta dell'operazione
sia:
*p_anag.anni
e invece non lo , in quanto l'operatore . ha la precedenza sull'operatore di
deref. e quindi il compilatore darebbe messaggio di errore, interpretando
p_anag.anni come un indirizzo da dereferenziare (l'interpretazione sarebbe
giusta se esistesse un oggetto di nome p_anag con un membro di nome anni
definito puntatore a int, e invece esiste un puntatore di nome p_anag a un
oggetto con un membro di nome anni definito int).
Perch il risultato sia corretto bisognerebbe inserire la deref. del puntatore fra
parentesi, cio:
(*p_anag).anni
il C++ (come il C) consente di evitare questa "fatica" mettendo a disposizione un
altro operatore, che restituisce un identico risultato:
p_anag->anni
In generale l'operatore -> permette di accedere a un membro (indicato dal
right-operand) di un oggetto, istanza di una struttura, il cui indirizzo
dato nel left-operand (ovviamente anche questo operatore pu restituire sia un
r-value che un l-value).

Unioni

Le unioni sono identiche alle strutture (sono introdotte dalla parola-chiave


union al posto di struct), eccetto nel fatto che i membri di ogni loro istanza
occupano la stessa area di memoria.
In pratica un'unione consente di utilizzare un solo membro per ogni oggetto
(anche se i membri definiti sono pi d'uno) e servono quando pu essere
comodo selezionare ogni volta il membro pi appropriato, in base alle necessit.
L'occupazione di memoria di un'unione coincide con quella del membro di
dimensioni maggiori.

Array di strutture

Abbiamo visto negli esempi che i membri di una struttura possono essere
array. Anche le istanze di una struttura possono essere array.
Es.:

definizione:
costruzione oggetti:
accesso:

struct tipo_stud { char nome[20]; int voto[50];}


;
tipo_stud studente[40];
studente[5].voto[10] = 30;

(lo studente n.5 ha preso 30 nella prova n.10 !)

Dichiarazione di strutture e membri di tipo struttura

I membri di una struttura possono essere a loro volta di tipo struttura. Esiste
per il problema di fare riconoscere tale struttura al compilatore. Le soluzione
pi semplice definire la struttura a cui appartiene il membro prima della
struttura che contiene il membro (cos il compilatore in grado di riconoscerne
il tipo). Tuttavia capita non di rado che la stessa struttura a cui appartiene il
membro contenga informazioni che la collegano alla struttura principale: in
questi casi viene a determinarsi la cosidetta "dipendenza circolare",
apparentemente senza soluzione.
In realt il C++ offre una soluzione semplicissima: dichiarare la struttura
prima di definirla! La dichiarazione di una struttura consiste in una istruzione

in cui appaiono esclusivamente la parola-chiave struct e l'identificatore della


struttura.
Es.:

struct

data ;

chiaramente si tratta di una dichiarazione-non-definizione (questo il terzo


caso che incontriamo, dopo le dichiarazioni di variabili con le specificatore
extern e le dichiarazioni di funzioni), nel senso che non rende ancora la
struttura utilizzabile, ma sufficiente affinch il compilatore accetti data come
tipo di una struttura definita successivamente.
Allora il problema risolto ? No ! Perch no ? Perch il compilatore ha un'altra
esigenza oltre quella di riconoscere i tipi: deve essere anche in grado di calcolare
le dimensioni di una struttura e non lo pu fare se questa contiene membri di
strutture non definite. Solo nel caso che i membri in questione siano
puntatori questo problema non sussiste, in quanto le dimensioni di un
puntatore sono fisse e indipendenti dal tipo della variabile puntata.
Pertanto, la dipendenza circolare fra membri di strutture diverse pu essere
spezzata solo se almeno in una struttura i membri coinvolti sono puntatori.
Per esempio, una sequenza corretta potrebbe essere:
struct

data ;

dichiarazione anticipata della struttura data

struct persona { char


nome[20]; data* pnascita;} ;

definizione della struttura principale persona


con un membro puntatore a data

struct data { int giorno; int


mese; int anno; persona
caio; } ;

definizione della struttura data con un membro


di tipo persona

in questo modo il membro pnascita della struttura persona riconosciuto


come puntatore al tipo data prima ancora che la struttura data sia definita.
Con lo stesso ragionamento si pu dimostrare che possibile dichiarare dei
membri di una struttura come puntatori alla struttura stessa (per esempio,
quando si devono costruire delle liste concatenate). In questo caso, poi, la
dichiarazione anticipata non serve in quanto il compilatore conosce gi il nome
della struttura che appare all'inizio della sua definizione.
Nota: La dipendenza circolare si pu avere anche fra le funzioni (una funzione A
che chiama una funzione B che chiama una funzione C che a sua volta
chiama la funzione A). Ma in questi casi le dichiarazioni contengono gi tutte
le informazioni necessarie e quindi il problema si risolve semplicemente
dichiarando A prima di definire (nell'ordine) C, B e la stessa A.

Per accedere a un membro di una la struttura al cui tipo appartiene il


membro di un certo oggetto, necessario ripetere due volte l'operazione con
l'operatore . (e/o con l'operatore -> se il membro un puntatore).
Seguitando con lo stesso esempio :

costruzione oggetto:

persona tizio; (da qualche altra parte bisogna anche


creare un oggetto di tipo data e assegnare il suo
indirizzo a tizio.pnascita)

accesso:

tizio.pnascita->anno = 1957;

come si pu notare dall'esempio, il numero 1957 stato inserito nel membro


anno dell'oggetto il cui indirizzo si trova nel membro puntatore pnascita
dell'istanza tizio della struttura persona.

Strutture di tipo bit field

Le strutture di tipo bit field permettono di riservare ad ogni membro un


determinato numero di bit di memoria, consentendo notevoli risparmi; il tipo di
ogni membro deve essere unsigned int.
Es.:

struct bit { unsigned int ma:2; unsigned int mb:1; } ;

la presenza dei due punti, seguita dal numero di bit riservati, identifica la
definizione di una struttura di tipo bit field.

Tipi enumerati

Con la parola-chiave enum si definiscono i tipi enumerati, le cui istanze


possono assumere solo i valori specificati in un elenco.
Es.:

enum feriale { Lun, Mar, Mer, Gio, Ven } ;

dove: feriale il nome del tipo enumerato e le costanti fra parentesi graffe
sono i valori possibili (detti enumeratori).
In realt agli enumeratori sono assegnati numeri interi, a partire da 0 e con
incrementi di 1, come se si usassero le direttive:
#define Lun 0
#define Mar 1
ecc...
Volendo assegnare numeri diversi (comunque sempre interi), bisogna specificarlo.
Es.:

enum dati { primo, secondo=12, terzo } ;

in questo caso alla costante primo assegnato 0, a secondo assegnato 12 e


a terzo assegnato 13. Comunque l'uso degli enumeratori, anzich quello
diretto delle costanti numeriche corrispondenti, utile in quanto permette di
scrivere codice pi chiaro ed pi esplicativo di ci che si vuole fare.
Analogamente al tag di una struttura, il nome di un tipo enumerato
assunto, in C++ come un nuovo tipo del linguaggio.
Es.:
feriale oggi = Mar ;
costruisce l'oggetto oggi, istanza del tipo enumerato feriale e lo
inizializza con il valore dell'enumeratore Mar.
Un oggetto di tipo enumerato pu assumere valori anche diversi da quelli
specificati nella definizione. L'intervallo di validit (detto dominio) di un tipo
enumerato contiene tutti i valori dei propri enumeratori arrotondati alla
minima potenza di 2 maggiore o uguale al massimo enumeratore meno 1. Il
dominio comincia da 0 se il minimio enumeratore non negativo; altrimenti
il valore maggiore tra le potenze di due negative minori o uguali del minimo
enumeratore (si uguagliano poi minimo e massimo scegliendo il pi grande in
valore assoluto). In ogni caso il dominio non pu superare il range del tipo int.
Es.:

enum en1 { bello, brutto } ;

dominio 0:1

enum en2 { a=3, b=10 } ;

dominio 0:15

enum en3 { a=-38, b=850 } ;

dominio -1024:1023

come si pu notare, il numero complessivo degli enumeratori possibili sempre


una potenza di 2.
Per inizializzare un oggetto di tipo enumerato con un valore intero (anche
diverso dalle costanti incluse nella definizione, purch compreso nel dominio)
obbligatorio il casting.
Es.:

en2 oggetto1 = (en2)14 ;

OK, 14 compreso nel dominio

en2 oggetto2 = (en2)20 ;

risultato indefinito, 20 non compreso nel


dominio

en2 oggetto3 = 3 ;

errore: conversione implicita non ammessa

Gli enumeratori sono ammessi nelle operazioni fra numeri interi e, in questi
casi, sono converititi implicitamente in int.

Allocazione dinamica della memoria


Memoria stack e memoria heap

Abbiamo gi sentito parlare dell'area di memoria stack: quella in cui viene


allocato un pacchetto di dati non appena l'esecuzione passa dal programma
chiamante a una funzione. Abbiamo detto che questo pacchetto (il quale
contiene l'indirizzo di rientro nel programma chiamante, la lista degli
argomenti passati alla funzione e tutte le variabili automatiche definite nella
funzione) viene "impilato" sopra il pacchetto precedente (quello del
programma chiamante) e poi automaticamente rimosso dalla memoria appena
l'esecuzione della funzione terminata.
Sappiamo anche che, grazie a questo meccanismo, le funzioni possono essere
chiamate ricorsivamente e inoltre si possono gestire funzioni con numero
variabile di argomenti. Le variabili automatiche definite nella funzione hanno
lifetime limitato all'esecuzione della funzione stessa proprio perch, quando la
funzione termina, il corrispondente pacchetto allocato nell'area stack viene
rimosso.
Un'altra area di memoria quella in cui vengono allocate le variabili non locali e
le variabili locali statiche. A differenza dalla precedente, quest'area viene
mantenuta in vita fino alla fine del programma, anche se ogni variabile visibile
solo all'interno del proprio ambito.
Esiste una terza area di memoria che il programma pu utilizzare. Questa area,
detta heap, soggetta a regole di visibilit e tempo di vita diverse da quelle
che governano le due aree precedenti, e precisamente:

l'area heap non allocata automaticamente, ma pu essere allocata o


rimossa solo su esplicita richiesta del programma (allocazione dinamica
della memoria);
l'area allocata non identificata da un nome, ma accessibile
esclusivamente tramite deref. di un puntatore;
il suo scope coincide con quello del puntatore che contiene il suo
indirizzo;
il suo lifetime coincide con l'intera durata del programma, a meno che non
venga esplicitamente deallocata; se il puntatore va out of scope, l'area
non pi accessibile, ma continua a occupare memoria inutilmente: si
verifica l'errore di memory leak, opposto a quello di dangling
references.

Operatore new

In C++, l'operatore new costruisce uno o pi oggetti nell'area heap e ne


restituisce l'indirizzo. In caso di errore (memoria non disponibile) restituisce
NULL.
Gli operandi di new (tutti alla sua destra) sono tre, di cui solo il primo
obbligatorio (le parentesi quadre nere racchiudono gli operandi opzionali):
new tipo [[dimensione]] [(valore iniziale)]

tipo il tipo (anche astratto) dell'oggetto (o degli oggetti) da creare;


dimensione il numero degli oggetti, che vengono sistemati nella
memoria heap consecutivamente (come gli elementi di un array); se
questo operando omesso, viene costruito un solo oggetto; se
presente, l'indirizzo restituito da new punta al primo oggetto;
valore iniziale il valore con cui l'area allocata viene inizializzata (deve
essere dello stesso tipo di tipo); se omesso l'area non inizializzata.

NOTA: si potuto riscontrare che a volte i due operandi opzionali sono


mutuamente incompatibili (alcuni compilatori pi antichi danno errore): in pratica
(vedremo perch parlando dei costruttori), se il tipo nativo inizializza
comunque tutti i valori con zero, se il tipo astratto funziona bene (a certe
condizioni).
Ovviamente l'operatore new non pu restituire un l-value; pu essere invece
un r-value sia nelle inizializzazioni che nelle assegnazioni, e pu far parte di
operazioni di aritmetica fra puntatori . Esempi:
inizializzazione:
int* punt = new int (7);
assegnazione con operazione aritmetica:
struct anagrafico { ....... } ;
anagrafico* p_anag ;
p_anag = new anagrafico [100] + 9 ;
nel primo esempio alloca un oggetto int (inizializzato con il valore 7) nell'area
heap e usa il suo indirizzo per inizializzare il puntatore punt; nel secondo
esempio definisce la struttura anagrafico e definisce un puntatore a tale
struttura, a cui assegna l'indirizzo del decimo di cento oggetti di tipo
anagrafico, allocati nell'area heap.

Operatore delete

In C++, l'operatore binario delete (con un operando opzionale e l'altro


obbligatorio) dealloca la memoria dell'area heap puntata dall'operando
(obbligatorio). Non restituisce alcun valore e quindi deve essere usato da solo in
un'istruzione (non essendo n un l-value n un r-value non pu essere usato in
un'espressione con altre operazioni).

Es.:

allocazione:
deallocazione:

int* punt = new int ;


delete punt ;

Contrariamente all'apparenza l'operatore delete non cancella il puntatore n


altera il suo contenuto: l'unico effetto di liberare la memoria puntata rendendola
disponibile per ulteriori allocazioni (se l'operando non punta a un'area heap
alcuni compilatori generano un messaggio di errore (o di warning), altri no, ma
in ogni caso l'operatore delete non ha effetto).
Se l'operando punta a un'area in cui stato allocato un array di oggetti,
bisogna inserire dopo delete l'operando opzionale, che consiste in una coppia
di parentesi quadre (senza la dimensione dell'array, che il C++ in grado di
riconoscere automaticamente).
Es.:

float* punt = new float [100] ;

(alloca 100 oggetti float )

delete [ ] punt ;

(libera tutta la memoria allocata)

L'operatore delete costituisce l'unico mezzo per deallocare memoria heap,


che, altrimenti, sopravvive fino alla fine del programma, anche quando non pi
raggiungibile.
Es.: int* punt = new int ;

(alloca un oggetto int nell'area heap e inizializza


punt con il suo indirizzo)

int a ;

(definisce un oggetto int nell'area stack)

punt = &a ;

(assegna a punt un indirizzo dell'area stack;


l'oggetto int dell'area heap non pi raggiungibile)

Namespace
Programmazione modulare e compilazione separata

Nel corso degli anni, l'enfasi nella progettazione dei programmi si spostata dal
progetto delle procedure all'organizzazione dei dati, in ragione anche dei
problemi di sviluppo e manutenzione del software che sono direttamente correlati
all'aumento di dimensione dei programmi. La possibilit di suddividere grossi
programmi in porzioni il pi possibile ridotte e autosufficienti (detti moduli)
pertanto caratteristica di un modo efficiente di produrre software, in quanto
permette di sviluppare programmi pi chiari e pi facili da mantenere ed
aggiornare (specie se i programmatori che lavorano a un stesso progetto sono
molti).
Un modulo costituito da dati logicamente correlati e dalle procedure che li
utilizzano. L'idea-base quella del "data hiding" (occultamento dei dati), in
ragione della quale un programmatore "utente" del modulo non ha bisogno di
conoscere i nomi delle variabili, dei tipi, delle funzioni e in generale delle
caratteristiche di implementazione del modulo stesso, ma sufficiente che sappia
come utilizzarlo, cio come mandargli le informazioni e ottenere le risposte. Un
modulo pertanto paragonabile a un dispositivo (il cui meccanismo interno
sconosciuto), con il quale comunicare attraverso operazioni di input-output. Tali
operazioni sono a loro volta raggruppate in un modulo separato, detto
interfaccia che rappresenta l'unico canale di comunicazione fra il modulo e i
suoi utenti.
La programmazione modulare offre cos un duplice vantaggio: quello di
separare l'interfaccia dal codice di implementazione del modulo, dando la
possibilit al modulo di essere modificato senza che il codice dell'utente ne sia
influenzato; e quello di permettere all'utente di definire i nomi delle variabili, dei
tipi, delle funzioni ecc.. senza doversi preoccupare di eventuali conflitti con i
nomi usati nel modulo e dell'insorgere di errori dovuti a simboli duplicati.
Parallelo al concetto di programmazione modulare quello di compilazione
separata. Per motivi di efficienza la progettazione di un programma (specie se di
grosse dimensioni) dovrebbe prevedere la sistemazione dei moduli in files
separati: in questo modo ogni intervento di modifica o di correzione degli errori di
un singolo modulo comporterebbe la ricompilazione di un solo file. E' utile che
anche l'interfaccia di un modulo risieda in un file separato sia dal codice
dell'utente che da quello di implementazione del modulo stesso. Entrambi questi
files dovrebbero poi contenere la direttiva #include (file dell'interfaccia) cos
che il preprocessore possa creare due translation units indipendenti, ma
collegate entrambe alla stessa interfaccia (questo approccio molto pi
conveniente di quello di creare due soli files entrambi con il codice
dell'interfaccia, in quanto permette al progettista del modulo di modificare
l'interfaccia senza implicare che la stessa modifica venga eseguita anche nel file
dell'utente).

Definizione di namespace

Dal punto di vista sintattico, la definizione di un namespace somiglia molto a


quella di una struttura (cambia la parola-chiave e inoltre il punto e virgola in
fondo non obbligatorio). Esempio:
namespace Stack
{
const int max_size = 100;
char v[max_size ];
int top = 0;
void push(char c) {......}
char pop( ) {......}
}
I membri di un namespace sono dichiarazioni o definizioni (con eventuali
inizializzazioni) di identificatori di qualunque genere (variabili, funzioni,
typedef, strutture, enumeratori, altri tipi astratti qualsiasi ecc...). Anche il
nome di un namespace (Stack, nell'esempio) un identificatore. Pertanto
definire un namespace significa dichiarare/definire un gruppo di nomi a sua
volta identificato da un nome.
A differenza dalle strutture, Stack non un tipo (non pu essere istanziato da
oggetti) ma identifica semplicemente un ambito di visibilit (scope). I
membri di Stack sono perci identificatori locali, visibili soltanto nello scope
definito da Stack. Il programmatore perci libero di definire gli stessi nomi al
di fuori, senza pericolo di conflitti o ambiguit.
Non ammesso definire un namespace all'interno di un altro scope (per
esempio nel block scope di una funzione o una struttura); e quindi il suo
nome ha global scope cio riconoscibile dappertutto. E' per possibile
"annidare" un namespace all'interno di un altro namespace: in questo caso il
suo scope coincide con quello degli altri membri del namespace superiore.
In definitiva, il termine namespace si identifica con quello di "ambito
dichiarativo con un nome". In questo senso, anche i blocchi delle funzioni e
delle strutture sono dei namespace (con molte funzionalit in pi) e tutto ci
che al di fuori (le variabili globali) detto appartenere al "namespace
globale".

Risoluzione della visibilit

Sorge a questo punto spontanea una domanda: come comunicare fra i


namespace? In altre parole, se i membri di un namespace non sono accessibili
dall'esterno, come si possono usare nel programma ?
Per accedere a un nome definito in un namespace, bisogna "qualificarlo",
associandogli il nome del namespace (che invece visibile, avendo global
scope), tramite l'operatore binario di risoluzione di visibilit :: (doppi due
punti).
Seguitando nell'esempio precedente:
Stack::top (accede al membro top del namespace Stack)
Notare l'analogia di questo operatore con quello unario di riferimento globale
(gi visto a proposito dell'accesso alle variabili globali). Infatti, se il leftoperand manca, vuol dire che il nome dato dal right-operand deve essere
cercato nel namespace globale.

Membri di un namespace definiti esternamente

Abbiamo visto che i membri di un namespace possono essere sia dichiarati


che definiti. Sappiamo per che alcune dichiarazioni non sono definizioni e
che in generale un identificatore utilizzabile dal programma se definito (da
qualche parte) ed dichiarato prima del punto in cui lo si vuole utilizzare.
Possiamo perci separare, dove possibile, le dichiarazioni dalle definizioni e
includere solo le prime fra i membri di un namespace, ponendo le seconde al
di fuori. Nelle definizioni esterne per, i nomi devono essere qualificati,
altrimenti non sarebbero riconoscibili.
La separazione fra dichiarazioni e definizioni applicata soprattutto alle
funzioni. Seguitando con lo stesso esempio:
namespace Stack
{
const int max_size = 100;
char v[max_size ];
int top = 0;
void push(char);
char pop( );
}

void Stack::push(char c) {......}


char Stack::pop( ) {......}
Le funzioni push e pop sono soltanto dichiarate nella definizione del
namespace Stack, e definite altrove con i nomi qualificati. Non necessario,
invece, qualificare i membri di Stack utilizzati all'interno delle funzioni, in
quanto il compilatore, se incontra una variabile locale non definita nell'ambito
della funzione, la va a cercare nel namespace a cui la funzione appartiene.
Quando viene chiamata una funzione membro di un namespace, con
argomenti di cui almeno uno di tipo astratto membro dello stesso
namespace, la qualificazione del nome della funzione non necessaria.
Esempio:
#include <iostream.h>
namespace A { struct AS {int k;}; char ff(AS); }
char A::ff(AS m) { return (char)m.k; }
int main()
{
A::AS m;
m.k = 65;
cout << ff(m) << '\n'; // non importa A:: davanti a ff
}
Infatti il nome di una funzione cercato, non solo nell'ambito della chiamata
(o in ambiti superiori), ma anche in quelli dei namespace in cui sono definiti i
tipi di ogni argomento. Non sono prefissati criteri di precedenza: in caso di
ambiguit il compilatore d un messaggio di errore.
NOTA : si tratta di una funzionalit recente del C++. Infatti il compilatore gcc la
accetta, mentre il Visual C++ pretende la qualificazione.

Namespace annidati

Abbiamo detto che i namespace possono essere definiti solo nell'ambito


globale (cio non si possono definire all'interno di altri blocchi, per esempio di
funzioni o strutture). E' per possibile definire un namespace all'interno di un
altro namespace (namespace "annidati").
Es.:

void f( );
namespace A

void g( );
namespace B

void h( );
}
}
la funzione f dichiarata nel namespace globale; la funzione g dichiarata
nel namespace A; e infine la funzione h dichiarata nel namespace B
definito nel namespace A.
Per accedere (dall'esterno) a un membro del namespace B bisogna ripetere due
volte l'operazione di risoluzione di visibilit.
Es.:

void A::B::h( ) {......}

(definizione esterna della funzione h)

Per i namespace "annidati" valgono le normali regole di visibilit e di


qualificazione: all'interno della funzione h non occorre qualificare i membri
di B (come sempre), ma neppure quelli di A, in quanto i nomi definiti in ambiti
superiori sono ancora visibili negli ambiti sottostanti; viceversa, all'interno della
funzione g bisogna qualificare i membri di B (perch i nomi definiti in
ambiti inferiori non solo visibili in quelli superiori), ma non quelli di A, per cui
sufficiente applicare la risoluzione di visibilit a un solo livello.
Es.
void A::g( ) {.... B::h ( ) ....} ( funzione h chiamata dalla
funzione g )
Infine, dall'interno della funzione globale f bisogna qualificare sia i membri di
A (a un livello: A::) che quelli di B (a due livelli: A::B::) in quanto nessun nome
definito nei due namespace visibile nel namespace globale.

Namespace sinonimi

La scelta del nome di un namespace importante: se troppo breve, rischia il


conflitto con i nomi di altri namespace (per esempio includendo librerie create
da altri programmatori); se molto lungo, pu evitare il conflitto con altri nomi,
ma diventa scomodo se lo si usa ripetutamente per qualificare esternamente i
suoi membri.
Es.:
...}

namespace creato_appositamente_da_me_medesimo

{... int x

con un nome cos lungo (e cos "stupido") non c' pericolo di conflitto, ma
scomodissimo utilizzare in altri ambiti il suo membro x:
creato_appositamente_da_me_medesimo::x = 20;

Entrambi gli inconvenienti possono essere superati, definendo, in un ambito


ristretto (e quindi con scarso pericolo di conflitto), un sinonimo breve di un
nome "vero" lungo (i sinonimi possono anche essere definiti localmente, a
differrenza dei namespace). Per definire un sinonimo si usa la seguente
sintassi (seguitando con l'esempio):
namespace STUP = creato_appositamente_da_me_medesimo;
da questo punto in poi (nello stesso ambito in cui definito il sinonimo STUP)
si pu ogni volta qualificare un membro del suo namespace utilizzando come
left-operand il sinonimo:
STUP::x = 20;
I namespace sinonimi sono utili non solo per abbreviare nomi lunghi, ma
anche per localizzare in un unico punto una modifica che altrimenti si dovrebbe
ripetere in molti punti del programma (come nelle definizioni con const,
#define e typedef). Per esempio, se il nome di un namespace si riferisce alla
versione di una libreria usata dal programma, e questa potrebbe essere
successivamente aggiornata, molto conveniente creare un sinonimo da
utilizzare nel programma al posto del nome della libreria: in questo modo, in caso
di cambiamento di versione della libreria, si pu modificare solo l'istruzione di
definizione del sinonimo, assegnando allo stesso sinonimo il nuovo nome
(altrimenti si dovrebbero modificare tutte le istruzioni che utilizzano quel nome
nel programma).

Namespace anonimi

Nella definizione di un namespace, il nome non obbligatorio. Se lo si omette,


si crea un namespace anonimo.
Es.: namespace

{ int a = 10; int b; void c(double); }

I membri a, b e c del namespace anonimo sono visibili in tutto il file (file


scope), non devono essere qualificati, ma non possono essere utilizzati in files
differenti da quello in cui sono stati definiti (cio, diversamente dagli oggetti
globali, non possono essere collegati dall'esterno tramite lo specificatore
extern).
In altre parole i membri di un namespace anonimo hanno le stesse identiche
propriet degli oggetti globali definiti con lo specificatore static. Per questo
motivo, e allo scopo di ridurre le ambiguit nel significato delle parole-chiave del
linguaggio, il comitato per la definizione dello standard (pur mantenendo, per
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
mantenere l'uso di static esclusivamente per l'allocazione permanente (cio con
lifetime illimitato) di oggetti con visibilit locale (block scope).

Estendibilit della definizione di un namespace

Al contrario delle strutture, i namespace sono costrutti "aperti", nel senso che
possono essere definiti pi volte con lo stesso nome. Non si tratta per di
diverse definizioni, bens di estensioni della definizione iniziale. E quindi, pur
essendovi blocchi diversi di un namespace con lo stesso nome, l'ambito
definito dal namespace con quel nome resta unico.
Ne consegue che, per la ODR (one definition rule), i membri
complessivamente definiti in un namespace (anche se frammentato in pi
blocchi) devono essere tutti diversi (cio nelle estensioni consentito
aggiungere nuovi membri ma non ridefinire membri definiti precedentemente).
Es.:

namespace A

int x ;
}
namespace B

{
OK: A::x e B::x sono definiti in due ambiti
diversi

int x ;
}
void f( ) {... A::y= ...}
namespace A

errore: y non ancora dichiarato in A


OK: estensione del namespace A

int x ;

errore: x gi definito nell'ambito di A

int y ;

OK: y un nuovo membro di A

}
void f( ) {... A::y= ...}

adesso OK

La possibilit di suddividere un namespace in blocchi separati consente, da un


lato, di racchiudere grandi frammenti di programma in un unico namespace e,
dall'altro, di presentare diverse interfacce a diverse categorie di utenti,
mostrandone parti differenti.

Parola-chiave using

Quando un membro di un namespace viene usato ripetutamente fuori dal suo


ambito, esiste la possibilit, aggiungendo una sola istruzione, di evitare il fastidio
di qualificarlo ogni volta.
La parola-chiave using serve a questo scopo e pu essere usata in due modi
diversi:

con un'istruzione di "using-declaration" si rende accessibile un membro


di un namespace nello stesso ambito in cui inserita l'istruzione.
Tornando all'esempio del namespace Stack, l'istruzione:
using Stack::top;
permette di accedere al membro top di Stack, senza bisogno di
qualificarlo, in tutte le istruzioni che seguono all'interno dello stesso
ambito. In sostanza, con la using-declaration, si introduce il sinonimo
locale top di Stack::top .
In una using-declaration va specificato solo il nome del membro
interessato, per cui, in particolare, se il membro una funzione,
l'elenco degli argomenti non va indicato (e neppure le parentesi tonde);
nel caso di pi funzioni in overload con lo stesso nome, la usingdeclaration le rende accessibili tutte.
con un'istruzione di "using-directive" si rendono accessibili tutti i membri
di un namespace nello stesso ambito in cui inserita l'istruzione.
Tornando all'esempio, l'istruzione:
using namespace Stack;
permette di accedere a tutti i membri di Stack, senza bisogno di
qualificarli, in tutte le istruzioni che seguono all'interno dello stesso
ambito.

Entrambe le istruzioni using possono essere inserite in qualunque ambito e in


esso mettono a disposizione sinonimi che a loro volta seguono le normali regole
di visibilit. In particolare:

se le istruzioni using sono inserite in un blocco (di una funzione,


struttura o altro), i sinonimi hanno block scope;
se sono inserite nel namespace globale o in un namespace anonimo, i
sinonimi hanno file scope;
infine, se sono inserite in un altro namespace, i sinonimi hanno lo stesso
scope del namespace che li ospita.

Spesso la using-directive a livello globale usata come "strumento di


transizione", cio per trasportare in C++ vecchio codice scritto in C. Esistono
infatti centinaia di librerie scritte in C, con centinaia di migliaia di righe di codice,
che fanno un uso massiccio ed estensivo di nomi globali. Molte di queste librerie
sono ancora utili e costituiscono un "patrimonio" che non va disperso. D'altra
parte, "affollare" cos pesantemente il namespace globale non fa parte della

"logica" del C++. Il problema stato risolto racchiudendo le librerie in tanti


namespace e facendo ricorso alle using-directive per renderle accessibili
(quando serve). In questo modo si mantiene la compatibilit con i vecchi
programmi, ma i nomi utilizzati dalle librerie non occupano il namespace
globale e quindi non rischiano di creare conflitti in altri contesti.

Precedenze e conflitti fra i nomi

Abbiamo visto che le istruzioni using forniscono la possibilit di evitare la


qualificazione ripetuta dei nomi definiti in un namespace. D'altra parte,
rendendo accessibili delle parti di programma che altrimenti sarebbero nascoste,
indeboliscono il "data hiding" e aumentano la probabilit di conflitti fra nomi e
di errori non sempre riconoscibili. Si tratta pertanto di operare di volta in volta la
scelta pi opportuna, bilanciando "comodit" e "sicurezza".
A questo scopo il C++ definisce delle regole precise che, in taluni casi, vietano i
conflitti di nomi (nel senso che all'occorrenza il compilatore segnala errore) e, in
altri, stabiliscono delle precedenze fra nomi uguali (cio il nome con precedenza
superiore "nasconde" quello con precedenza inferiore). Tali regole sono diverse se
si usa una using-declaration o una using-directive :

una using-declaration aggiunge, nello scope in cui inserita, un nuovo


nome, che si comporta esattamente come tutti gli altri nomi (il fatto che
sia sinonimo di un membro di un namespace del tutto ininfluente), e
pertanto:
o entra il conflitto con un nome uguale definito nello stesso ambito;
o nasconde i nomi uguali (non qualificati) definiti in ambiti superiori.
una using-directive mette a disposizione, nello scope in cui inserita,
tutti i nomi definiti in un namespace; si sottolinea il fatto che li "mette a
disposizione" ma non li aggiunge immediatamente, e pertanto questa
istruzione non genera conflitti di per s. Al momento dell'utilizzo, ogni
nome del namespace interessato si comporta nel modo seguente:
o nascosto da un nome uguale definito nello stesso ambito o in
ambiti superiori, esclusi il namespace globale e il namespace
anonimo;
o entra il conflitto con un nome uguale (non qualificato) definito nel
namespace globale o nel namespace anonimo.
Queste regole (un po' "atipiche") sui nomi resi accessibili dalla usingdirective, sono state concepite al duplice scopo di permettere l'inclusione
di grandi librerie con molti nomi globali senza segnalare i potenziali
conflitti dei nomi che non vengono di fatto utilizzati, e, all'opposto, di non
incoraggiare l'utente "pigro" che continua a definire nomi globali piuttosto
che fare ricorso ai namespace.

nel caso di concorrenza fra using-declaration e using-directive, le


prime prevalgono sulle seconde e risolvono anche i potenziali conflitti
Es.:

namespace A
}

{ ... int x ; ...

namespace B

{ ... int x ; ... } A e B hanno un membro con lo stesso


nome

using namespace A;
using namespace B;
using A::x;

Achtung ! potenziale conflitto ..........


.......... risolto a favore di A !

Collegamento fra namespace definiti in files diversi

Finora abbiamo trattato i namespace intendendo che fossero sempre definiti


nello stesso file. Ci chiediamo ora in che modo possibile il collegamento fra
namespace di file diversi. Prima, per, opportuno ricordare la differenza che
intercorre fra file sorgente e translation unit:

un file sorgente contiene le istruzioni del programma create dal


programmatore;
una translation unit lo stesso file visto dal compilatore, dopo che il
preprocessore ha incluso gli header-files ed eseguito le altre eventuali
direttive (#define, direttive condizionali ecc...).

Due namespace con lo stesso nome appartenenti a due diverse translation


units non sono in conflitto, ma sono da considerarsi come facenti parte dello
stesso unico namespace (per la propriet di estendibilit dei namespace). Il
conflitto, semmai, pu sorgere fra i nomi dei membri del namespace, se viene
violata la ODR. D'altra parte ogni translation unit viene compilata
separatamente e quindi ogni nome utilizzato in una translation unit deve
essere, nella stessa, anche dichiarato. Ne consegue che i membri di uno stesso
namespace che vengono utilizzati in entrambe le translation units, devono
essere, in una delle due, definiti, e nell'altra dichiarati senza essere definiti
(questo discorso vale per gli oggetti e le funzioni non inline, mentre le
funzioni inline, i tipi astratti e altre entit del linguaggio che vederemo, come i
template, possono anche essere ridefiniti, purch gli elementi lessicali di ogni
definizione siano identici).
Diverso l'approccio, se si considerano i file sorgente: ogni file (cio ogni
modulo del programma) dovrebbe essere progettato in modo da non contenere
duplicazioni e da localizzare questo problema soltanto nelle eventuali interfacce
incluse da pi moduli. Queste interfacce dovrebbero contenere solo
dichiarazioni o definizioni "ripetibili".

Quindi il "trucco" consiste sostanzialmente nel progettare al meglio le interfacce


comuni: una "buona" interfaccia dovrebbe essere tale da minimizzare le
dipendenze fra le varie parti del programma, in quanto interfacce con
dipendenze minime conducono a sistemi pi facili da comprendere, con dettagli
implementativi invisibili (data-hiding), pi facili da modificare e pi veloci da
compilare.
Riprendiamo a questo proposito il nostro esempio iniziale del namespace Stack
e mettiamoci "nei panni" sia del progettista che dell'utente.

Il progettista deve individuare con quale strumenti rappresentare la pila


(per esempio con un array, ma ci potrebbero essere anche altre soluzioni),
quali siano le informazioni da memorizzare e mantenere (l'indice
corrispondente all'ultimo dato inserito e la massima dimensione di
accrescimento della pila), quali algoritmi applicare per le operazioni di
inserimento ed estrazione dei dati, e infine come dare all'utente la
possibilit di operare.
L'utente ha bisogno di conoscere solo due cose: il nome della funzione
per inserire un dato e il nome della funzione per estrarlo. E' quindi
opportuno che acceda esclusivamente a tali informazioni (le dichiarazioni
delle due funzioni), che costituiranno l'interfaccia comune fra il file
sorgente del progettista e quello dell'utente.

Si deduce pertanto che il progettista dovr spezzare la definizione del


namespace Stack in due (per fortuna ci possibile!): nella prima parte
metter solo le dichiarazioni delle funzioni push e pop; nella seconda tutto il
resto. Creer poi due files separati: nel primo (l'interfaccia comune) metter
soltanto la prima definizione del namespace Stack , nel secondo metter
l'estensione di Stack e, esternamente al namespace, le definizioni delle due
funzioni. A sua volta l'utente non dovr fare altro che inserire nel suo file
sorgente la direttiva di inclusione dell'interfaccia comune. Cos, qualsiasi
modifica o miglioramento venga fatto al codice di implementazione dello Stack, i
programmi degli utenti non ne verranno minimamente influenzati (al massimo
dovrano essere ri-linkati).

Eccezioni
Segnalazione e gestione degli errori

Il termine eccezione (dall'inglese exception) deriva dall'ottimistica assunzione


che nell'esecuzione di un programma gli errori costituiscano una "circostanza
eccezionale". Anche condividendo tale ottimismo, il problema di come individuare
gli errori e di come gestirli una volta individuati deve essere sempre affrontato con
grande cura nella progettazione di un programma.
Anche in un programma "perfetto" gli errori in fase di esecuzione possono
sempre capitare, perch sono commessi in larga parte da operatori "umani" (quelli
che usano il programma), e quindi lo stesso programma che deve essere in
grado di prevederli e di eseguire le azioni di ripristino, quando possibile.
Quando un programma, specie se di grosse dimensioni, composto da moduli
separati, e soprattutto se i moduli provengono da librerie sviluppate da altri
programmatori, anche la gestione degli errori deve essere tale da minimizzare le
dipendenze fra un modulo e l'altro. In generale, quando un modulo verifica una
condizione di errore, deve limitarsi a segnalare tale condizione, in quanto l'azione
di ripristino dipende pi spesso dal modulo che ha invocato l'operazione piuttosto
che da quello che ha riscontrato l'errore mentre cercava di eseguirla. Separando i
due momenti (la rilevazione dell'errore e la sua gestione) si mantiene il
massimo di indipendenza fra i moduli: l'interfaccia comune conterr gli
strumenti necessari, attivati dal modulo "rilevatore" e utilizzati dal modulo
"gestore"
Il C++ mette a disposizione un meccanismo semplice ma molto efficace di
rilevazione e gestione degli errori: l'idea base che, quando una funzione rileva
un errore che non in grado di affrontare direttamente, l'esecuzione della
funzione termina, ma il controllo non ritorna al punto in cui la funzione stata
chiamata, bens in un altro punto del programma, dove viene eseguita la
procedura di gestione dell'errore. In termini tecnici, la funzione che rileva
l'errore "solleva" o "lancia" (throw) un'eccezione ("marcandola" in qualche
modo, come vedremo) e termina: l'area stack ripercorsa all'indietro e cancellata
(stack unwinding) a vari livelli finch il flusso del programma non raggiunge il
punto (se esiste) in cui l'eccezione pu essere riconosciuta e "catturata" (catch);
in questo punto viene eseguita la procedura di gestione dell'errore; se il punto
non esiste l'intero programma "abortisce".

Il costrutto try

La parola-chiave try introduce un blocco di istruzioni.


Es. :

try

{
m = c / b;
double f = 10.7;
res = fun(f ,m+n);
}
Le istruzioni contenute in un blocco try sono "sotto controllo": in esecuzione,
qualcuna di esse potrebbe generare un errore. Nell'esempio, la funzione fun
potrebbe chiamare un'altra funzione e questa un'altra ancora ecc... , generando
una serie di pacchetti che si accumula sullo stack. L'area dello stack che va da
un un blocco try in su detta: exception stack frame e costituisce l'insieme di
tutte le istruzioni controllate.

L'istruzione throw

Dal punto di visto sintattico, l'istruzione throw identica all'istruzione return di


una funzione (e si comporta all'incirca nello stesso modo):
throw espressione;
Un'istruzione throw pu essere collocata soltanto in un exception stack frame
e segnala il punto in cui si ricontrato un errore (o, come si dice, "sollevata"
un'eccezione). Il valore calcolato dell'espressione, detto: "valore
dell'eccezione" (il cui tipo detto: "tipo dell'eccezione"), ripercorre
"all'indietro" l'exception stack frame (cancellandolo): se a un certo punto del
suo "cammino" l'eccezione viene "catturata" (vedremo come), l'errore pu
essere gestito, altrimenti il programma abortisce (ed quello che succede in
particolare se l'istruzione throw non inserita all'interno di un exception stack
frame).
In pratica throw si comporta come un return "multilivello". Il valore
dell'eccezione viene di solito utilizzato per la descrizione dell'errore commesso
(non per obbligatorio utilizzarlo). Il suo tipo invece di importanza
fondamentale in quanto (come vedremo) costituisce la "marca" di riconoscimento
dell'eccezione e ne permette la "cattura".

Il gestore delle eccezioni: costrutto catch


La parola-chiave catch introduce un blocco di istruzioni che ha lo stesso
formato sintattico della definizione di una funzione, con un solo argomento e
senza valore di ritorno.

catch (tipo argomento )

{ .......... blocco di istruzioni .............. }

Fisicamente un blocco catch deve seguire immediatamente un blocco try. Dal


punto di vista della successione logica delle operazioni, invece, un blocco catch
costituisce il punto terminale di ritorno di un exception stack frame: questo
viene costruito (verso l'alto), a partire da un blocco try, fino a un'istruzione
throw, da cui l'eccezione "sollevata" ridiscende (stack unwinding) fino al
blocco catch corrispondente al blocco try di partenza (oppure passa
direttamente dal blocco try al blocco catch se l'istruzione throw si trova gi
nel blocco try di partenza; in questo caso l'istruzione throw non si comporta
come un return, ma piuttosto come un goto). A questo punto l'eccezione pu
essere "catturata" o meno: se catturata, vengono eseguite le istruzioni del
blocco catch (detto "gestore dell'eccezione") e poi il flusso del programma
prosegue normalmente; se invece l'eccezione non catturata, il programma
abortisce. Se infine non vengono sollevate eccezioni, cio l'exception stack
frame non incontra istruzioni throw, il flusso del programma ridiscende per vie
normali tornando al blocco try da cui era partito, eseguito il quale prosegue
"saltando" il successivo blocco catch.
Un'eccezione viene "catturata" se il suo tipo coincide esattamente con il tipo
dell'argomento di catch. Non sono ammesse conversioni di tipo, neppure
implicite (neanche se i due tipi sono uguali in pratica, come int e long in una
machina a 32 bit). Verificata la coincidenza dei tipi, il valore dell'eccezione
viene trasferito nell'argomento di catch (come se l'istruzione throw
"chiamasse" la "funzione" catch); il trasferimento avviene normalmente per copia
(by value), a meno che l'argomento di catch non sia un riferimento, nel qual
caso il passaggio by reference, che per ha senso solo se l'espressione di
throw un l-value e se "sopravvive" alla distruzione dello stack (cio un
oggetto globale, o definito in un namespace, oppure locale ma
dichiarato static). E' possibile anche che l'argomento di catch sia dichiarato
const, nel qual caso valgono le stesse regole e limitazioni che ci sono per il
passaggio degli argomenti delle funzioni (vedere il capitolo: Puntatori e
costanti - Passaggio degli argomenti trasmessi by value e by reference).
Nel costrutto catch la specifica dell'argomento non obbligatoria (lo solo se
l'argomento viene usato nel blocco di istruzioni). Il tipo, invece, deve essere
sempre specificato, perch serve per la "cattura" dell'eccezione. A questo
proposito utile aggiungere che la scelta del tipo dell'eccezione libera, ma,
per una migliore leggibilit del programma e per evitare confusioni con le altre
eccezioni (in special modo con quelle generate dalle librerie del sistema, fuori dal
nostro controllo), vivamente consigliata la creazione di tipi "ad hoc",
preferibilmente uno per ogni possibile eccezione e con attinenza mnemonica fra
il nome del tipo e il significato dell'errore a cui associato: quindi, evitare l'uso
di tipi nativi (anche se non sarebbe vietato), ma usare solo tipi astratti (per
esempio strutture con nomi "ad hoc").
E' bene che il trattamento delle eccezioni venga usato quando la rilevazione e
la gestione di un errore devono avvenire in parti diverse del programma.
Quando invece un errore pu essere trattato localmente sufficiente servirsi dei
normali controlli del linguaggio (come i costrutti if o switch).
NOTA: per completezza precisiamo che un'eccezione pu essere "catturata"
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


e dell'eredit.

Riconoscimento di un'eccezione fra diverse alternative

Finora abbiamo detto che a un blocco try deve sempre seguire blocco catch. In
realt i blocchi catch possono anche essere pi di uno, disposti
consecutivamente e con tipi di argomento diversi.
Quando un'eccezione, discendendo lungo l'exception stack frame, incontra
una serie di blocchi catch, il suo tipo viene confrontato a uno a uno con quelli
dei blocchi catch e, se si verifica una coincidenza, l'eccezione viene "catturata"
e vengono eseguite le istruzioni del blocco catch in cui la coincidenza stata
trovata. Dopodich il flusso del programma "salta" gli eventuali blocchi catch
successivi e riprende normalmente dalla prima istruzione dopo l'ultimo blocco
catch del gruppo. Il programma abortisce se nessun blocco catch cattura
l'eccezione. Se invece non vengono sollevate eccezioni, il flusso del
programma, eseguite le istruzioni del blocco try, "salta" tutti i blocchi catch del
gruppo.
Se un costrutto catch, al posto del tipo e dell'argomento, presenta "tre
puntini" (ellipsis), significa che in grado di catturare qualsiasi eccezione,
indipendentemente dal suo tipo.
L'ordine in cui appaiono i diversi blocchi catch associati a un blocco try
importante: infatti il confronto con il tipo dell'eccezione da catturare viene
sempre fatto a partire dal primo blocco catch che segue il blocco try e procede
nello stesso ordine: da ci consegue che l'eventuale catch con ellipsis deve
essere sempre l'ultimo blocco del gruppo. L'esempio che segue schematizza la
situazione di un blocco try seguito da tre blocchi catch, di cui l'ultimo con
ellipsis.
try { blocco try }

se non solleva eccezioni, esegue blocco try e salta a


istruzione

catch (tipo1) {
blocco1}

altrimenti, se il tipo dell'eccezione coincide con tipo1,


cattura l'eccezione, esegue blocco1 e salta a istruzione

catch (tipo2) {
blocco2}

altrimenti, se il tipo dell'eccezione coincide con tipo2,


cattura l'eccezione, esegue blocco2 e salta a istruzione

catch (...)

{blocco3} altrimenti, cattura comunque l'eccezione ed esegue


blocco3

istruzione .........

riprende il flusso normale del programma

Blocchi innestati

Una sequenza di blocchi try....catch pu essere a sua volta "innestata" in un


blocco try o in un blocco catch (o in una funzione chiamata, direttamente o
indirettamente, da un blocco try o da un blocco catch).
Se la nuova sequenza interna a un blocco try (cio nella fase "ascendente"
dell'exception stack frame) e successivamente viene sollevata un'eccezione, il
controllo per la cattura dell'eccezione viene fatto anzitutto sui blocchi catch
interni (che sono incontrati prima nella fase di stack unwinding): se
l'eccezione catturata, il problema risolto e anche tutti i blocchi catch
associati al blocco try esterno vengono "saltati"; se invece nessun blocco
interno cattura l'eccezione, il programma non abortisce, ma il controllo passa ai
blocchi catch associati al blocco try esterno.
Se la nuova sequenza interna a un blocco catch (cio se l'eccezione gi
stata catturata), si crea un nuovo exception stack frame a partire da quel
punto: pertanto, se sollevata una nuova eccezione e questa viene catturata, il
programma esegue il blocco catch interno che ha catturato la nuova eccezione
e poi completa l'esecuzione del blocco catch esterno che ha catturato
l'eccezione precedente; se invece la nuova eccezione non catturata, il
programma abortisce.
Anche l'istruzione throw pu comparire in un blocco catch o in una funzione
chiamata, direttamente o indirettamente, da un blocco catch (la sua
collocazione "normale" sarebbe invece in un blocco try o in una funzione
chiamata, direttamente o indirettamente, da un blocco try). In questo caso si
dice che l'eccezione "ri-sollevata", ma non pu essere gestita allo stesso livello
del blocco catch da cui parte, in quanto un blocco catch non pu essere
"chiamato" ricursivamente. Pertanto un'eccezione sollevata dall'interno di un
blocco catch non fa abortire il programma solo se lo stesso blocco catch fa
parte di una sequenza innestata in un blocco try esterno (e saranno i
corrispondenti blocchi catch a occuparsi della sua cattura).
Un caso particolare di eccezione "ri-sollevata" si ha quando l'istruzione throw
appare da sola, senza essere seguita da un'espressione; in questo caso il valore
e il tipo dell'eccezione sono gli stessi del blocco catch in cui l'istruzione throw
inserita (cio il programma "ri-solleva" la stessa eccezione che sta gestendo).

Eccezioni che non sono errori

Come abbiamo detto all'inizio, il concetto di eccezione di norma legato a


quello di errore. Tuttavia il meccanismo di gestione delle eccezioni altro non
che un particolare algoritmo di "controllo", meno strutturato e meno efficiente
rispetto alle strutture di controllo locali (quali if, switch, for ecc...), che per
permette operazioni, come i return "multilivello", che con le strutture tradizionali
sarebbero pi difficili da ottenere o porterebbero a un codice non in grado di
mantenere un adeguato livello di indipendenza fra i diversi moduli del
programma.
Quindi la convenienza o meno dell'utilizzo delle eccezioni non si basa tanto sulla
distinzione fra errori o altre situazioni, quanto piuttosto sul fatto che le due
operazioni di "controllo" e "azione conseguente" siano localizzate insieme (nel qual
caso conviene usare le strutture tradizionali), oppure siano separate in aree
diverse dello stack (e allora preferibile usare le eccezioni).
Per esempio, l'utilizzo delle eccezioni come strutture di controllo potrebbe essere
una tecnica elegante per terminare funzioni di ricerca, soprattutto se la ricerca
avviene attraverso chiamate ricorsive, che "impilano" un numero imprecisato di
pacchetti sullo stack.
Altre "correnti di pensiero", invece, suggersicono di mantenere strettamente
correlato il concetto di eccezione con quello di errore, per evitare la generazione
di codice ambiguo e poco comprensibile (e quindi meno portabile e, in sostanza,
"pi costoso").

Classi e data hiding


Analogia fra classi e strutture

In C++ le classi sono identiche alle strutture, con l'unica differenza formale di
essere introdotte dalla parola-chiave class anzich struct.
In realt la principale differenza fra classi e strutture di natura "storica": le
strutture sono nate in C, con alcune propriet (descritte nel capitolo: "Tipi
definiti dall'utente"); le classi sono nate in C++, con le stesse propriet delle
strutture e molte altre propriet in pi. Successivamente si pensato di
attribuire alle strutture le stesse propriet delle classi. Pertanto le strutture
C++ sono molto diverse dalle strutture C, essendo invece identiche alle classi
(a parte una sola differenza sostanziale, di cui parleremo fra poco). Per questo
motivo, d'ora in poi tratteremo solo di classi, sottintendendo che, in C++,
quanto detto vale anche per le strutture.
Esempio di definizione di una classe:
class point
{ double x; double y; double z; } ;
ogni istanza della classe point rappresenta un punto nello spazio e i suoi
membri sono le coordinate cartesiane del punto.

Specificatori di accesso

In C++, nel blocco di definizione di una classe, possibile utilizzare dei nuovi
specificatori, detti specificatori di accesso, che sono i seguenti:
private:

protected:

public:

gli specificatori private: e protected: hanno significato analogo: la loro


differenza riguarda esclusivamente le classi ereditate, di cui parleremo pi
avanti; per il momento, useremo soltanto lo specificatore private: .
Questi specificatori possono essere inseriti pi volte all'interno della definizione
di una classe: private: fa s che tutti i membri dichiarati da quel punto in poi
(fino al termine della definizione della classe o fino a un nuovo specificatore)
acquisiscano la connotazione di membri privati (in che senso? ... vedremo pi

avanti); public: fa s che tutti i membri successivamente dichiarati siano


pubblici.
L'unica differenza sostanziale fra classe e struttura consiste nel fatto che i
membri di una struttura sono, di default, pubblici, mentre quelli di una
classe sono, di default, privati.

Data hiding

Il "data hiding" (occultamento dei dati) consiste nel rendere certe aree del
programma invisibili ad altre aree del programma. I suoi vantaggi sono evidenti:
favorisce la programmazione modulare, rende pi agevoli le operazioni di
manutenzione del software e, in ultima analisi, permette un modo di programmare
pi efficiente.
Introducendo i namespace, abbiamo detto che il data hiding si realizza
sostanzialmente racchiudendo i nomi all'interno di ambiti di visibilit e
definendo dei canali di comunicazione, ben circoscritti e controllati, come uniche
vie di accesso ai nomi di ambiti diversi. Se tutto quello che serve la protezione
dei nomi degli oggetti, i namespace sono sufficienti a questo scopo.
D'altra parte, questo livello di protezione, limitato ai soli oggetti, pu rivelarsi
inadeguato, se gli oggetti sono istanze di strutture o classi, cio possiedono
membri. E' sorto quindi il problema di proteggere, non solo un oggetto, ma
anche i suoi membri, facendo in modo che, anche quando l'oggetto visibile,
l'accesso ai suoi membri sia rigorosamente controllato.
Il C++ ha realizzato questo obiettivo, estendendo il data hiding anche ai
membri degli oggetti. L'istanza di una classe regolarmente visibile all'interno
del proprio ambito, ma i suoi membri privati non lo sono: non possibile, da
programma, accedere direttamente ai membri privati di una classe.
Es.:

class Persona {
int soldi ;
public:
char telefono[20] ;
char indirizzo[30] ;
};
Persona Giuseppe ;

(istanza della classe Persona)

il programma pu accedere a Giuseppe.telefono e Giuseppe.indirizzo, ma


non a Giuseppe.soldi!

Funzioni membro

A questo punto, la domanda d'obbligo : se i membri privati di una classe sono


inaccessibili, a che cosa servono ?
In realt i membri privati sono inaccessibili direttamente, ma possono essere
raggiunti indirettamente, tramite le cosiddette funzioni-membro.
Infatti il C++ ammette che i membri di una classe possano essere costituiti non
solo da dati, ma anche da funzioni. Queste funzioni possono essere, come ogni
altro membro, pubbliche o private, ma, in ogni caso, possono accedere a
qualunque altro membro della classe, anche ai membri privati. D'altra parte,
mentre una funzione-membro privata pu essere chiamata solo da un'altra
funzione-membro, una funzione-membro pubblica pu anche essere
chiamata dall'esterno, e pertanto costituisce l'unico tramite fra il programma e i
membri della classe.
Questo tipo di architettura del C++ costituisce la base fondamentale della
programmazione a oggetti: ogni istanza di una classe caratterizzata dalle
sue propriet (dati-membro) e dai suoi comportamenti (funzionimembro), detti anche metodi della classe. Con propriet e metodi, un
oggetto diviene un'entit attiva e autosufficiente, che comunica con il
programma in modo rigorosamente controllato. L'azione di chiamare dall'esterno
una funzione-membro pubblica di una classe viene riferita con il termine:
"inviare un messaggio a un oggetto", per evidenziare il fatto che il programma
si limita a dire all'oggetto cosa vuole, ma in realt l'oggetto stesso ad eseguire
l'operazione, tramite i suoi metodi e agendo sulle sue propriet (si dice anche
che le funzioni-membro sono incapsulate negli oggetti).
Nella definizione di una funzione-membro, gli altri membri della sua stessa
classe vanno indicati esclusivamente con il loro nome (senza operatori . o ->).
Il C++, ogni volta che incontra una variabile non dichiarata nella funzione,
cerca, prima di segnalare l'errore, di identificare il suo nome con quello di un
membro della classe (esattamente come accade per i membri di un
namespace, utilizzati in una funzione membro dello stesso namespace).
I metodi possono essere inseriti nella definizione di una classe in due diversi
modi: o come funzioni inline, cio con il loro codice (ma la parola-chiave
inline pu essere omessa in quanto all'interno della definizione di una classe
di default), oppure con la sola dichiarazione separata dal codice, che viene
scritto in altra parte del programma. Riprendendo l'esempio della classe point
(che, per semplicit, riduciamo a due dimensioni):
Esempio del primo modo

Esempio del secondo modo

class point {

class point {

double x;

double x;

double y;

double y;

public:

public:

void set(double x0, double y0)

void set(double, double ) ;

{ x=x0 ; y=y0 ; }

};

};
Se la definizione della funzione-membro set non inserita nell'ambito della
definizione della classe point (secondo modo), il suo nome dovr essere
qualificato con il nome della classe (come vedremo fra poco).
Seguendo l'esempio, definiamo ora l'oggetto p come istanza della classe
point:
point p;
il programma, che non pu accedere alle propriet private p.x e p.y, pu per
accedere a un metodo pubblico dello stesso oggetto, con l'istruzione:
p.set(x0,y0) ;
e quindi agire sull'oggetto nel solo modo che gli sia consentito.
Nel caso che una variabile venga definita come puntatore a una classe,
valgono le stesse regole, con la differenza che bisogna usare (per le funzioni
come per i dati) l'operatore ->
Tornando all'esempio:
point

ptr = new point;

ptr->set(1.5, 0.9) ;

Risoluzione della visibilit

Se il codice di un metodo si trova all'esterno della definizione della classe a cui


appartiene, bisogna "qualificare" il nome della funzione associandogli il nome
classe, tramite l'operatore :: di risoluzione di visibilit. Seguitando
nell'esempio precedente, la definizione esterna della funzione-membro set :
void point::set(double x0, double y0)
{
x = x0 ;
y = y0 ;

}
notiamo che questa regola la stessa che abbiamo visto per i namespace; in
realt si tratta di una regola generale che si applica ogni volta che si deve
accedere dall'esterno a un nome dichiarato in un certo ambito di visibilit, e
lo stesso ambito di visibilit identificato da un nome (come sono appunto sia
i namespace che le classi).
La scelta se un metodo debba essere scritto in forma inline o meno arbitraria:
se inline, l'esecuzione pi veloce, se non lo , la definizione della classe
appare in una forma pi "leggibile". Per esempio, si potrebbero lasciare inline
solo i metodi privati. E' anche possibile scrivere il codice esternamente alla
definizione della classe, ma specificare esplicitamente che la funzione deve
essere trattata come inline, con la seguente istruzione (riprendendo il solito
esempio):
inline void point::set(double x0, double y0)
in ogni caso il compilatore separa automaticamente il codice se la funzione
troppo lunga.
Quando, nella definizione di una classe, si lasciano solo i prototipi dei
metodi, si suole dire che viene creata un'intestazione di classe. La
consuetudine prevalente dei programmatori in C++ quella di creare librerie di
classi, separando in due gruppi distinti, le intestazioni, distribuite in headerfiles, dal codice delle funzioni, compilate separatamente e distribuite in librerie
in formato binario; infatti ai programmatori che utilizzano le classi non interessa
sapere come sono fatte le funzioni di accesso, ma solo come usarle.

Funzioni-membro di sola lettura

Quando un metodo ha il solo compito di riportare informazioni su un oggetto,


senza modificarne il contenuto, si pu, per evitare errori, imporre tale condizione a
priori, inserendo lo specificatore const dopo la lista degli argomenti della
funzione (sia nella dichiarazione che nella definizione). Riprendendo
l'esempio della classe point, aggiungiamo la funzione-membro get:
void point::get(double& x0, double& y0) const
{
x0 = x ;
y0 = y ;
}
la funzione-membro get non pu modificare i membri della sua classe.

Classi membro

Una classe pu anche essere definita all'interno di un'altra classe (oppure


semplicemente dichiarata, e poi definita esternamente, nel qual caso per il
suo nome deve essere qualificato con il nome della classe di appartenenza).
Esempio di definizione di un metodo f di una classe B, definita all'interno di
un'altra classe A:
void A::B::f( ) {......}
Le classi definite all'interno delle altre classi sono dette: classi-membro o
classi annidate. A parte i problemi inerenti all'ambito di visibilit e alla
conseguente necessit di qualificare i loro nomi, queste classi si comportano
esattamente come se fossero indipendenti. Se per sono collocate nella sezione
privata della classe di appartenenza, possono essere istanziate solo dai
metodi di detta classe. In sostanza, annidare una classe dentro un'altra
classe permette di controllare la creazione dei suoi oggetti. L'accesso ai suoi
membri, invece, non dipende dalla collocazione nella classe di appartenenza, ma
solo da come sono dichiarati gli stessi membri al suo interno (cio se pubblici
o privati).

Polimorfismo

Per una programmazione efficiente, anche la scelta dei nomi delle funzioni ha la
sua importanza. In particolare utile che funzioni che svolgono la stessa azione
abbiano lo stesso nome.
Il C++ consente questa possibilit: non solo i metodi di una classe possono
agire su istanze diverse della stessa classe, ma sono anche ammessi metodi di
classi diverse con lo stesso nome e gli stessi argomenti (non confondere con
l'overload, che implica funzioni con lo stesso nome, ma con diverse liste di
argomenti). Il C++ in grado di riconoscere in esecuzione l'oggetto a cui il
metodo applicato e di selezionare ogni volta la funzione che gli compete.
Questa attitudine del linguaggio di rispondere in modo diverso allo stesso
messaggio si chiama "polimorfismo": risponde all'esigenza del C++ di
modellarsi il pi possibile sui concetti della vita reale e, in questo modo, rendere la
programmazione pi facile ed efficiente che in altri linguaggi. L'importanza del
polimorfismo si comprender a pieno quando parleremo dell'eredit e delle
funzioni virtuali.

Puntatore nascosto this

Ci potremmo chiedere, a questo punto, come fa il C++ ad attuare il


polimorfismo: in programmi in formato eseguibile, i nomi degli oggetti e delle
funzioni sono spariti, e sono rimasti solo indirizzi e istruzioni. In altre parole,
come fa il programma a sapere, in esecuzione, su quale oggetto applicare una
funzione?
In realt il compilatore trasforma il codice sorgente, introducendo un puntatore
costante "nascosto" (identificato dalla parola-chiave this) ogni volta che
incontra la chiamata di una funzione-membro, e inserendo lo stesso
puntatore come primo argomento nella funzione.
Chiariamo quanto detto con il seguente esempio, in cui ogg un'istanza di una
certa classe myclass e init() una funzione-membro che utilizza un datomembro x, entrambi della stessa classe myclass:
la definizione della funzione:

void myclass::init() {..... x = .....}

viene trasformata in:

void init(myclass* const this) {..... this>x = .....}

e quindi .....
l'istruzione di chiamata della
funzione:

ogg.init( ) ;

viene tradotta in:

init(&ogg) ;

Come si pu notare dall'esempio, il puntatore nascosto this punta all'oggetto


utilizzato dalla funzione. Il programmatore non tenuto a conoscerlo, tuttavia,
se vuole, pu utilizzarlo in sola lettura (per esempio, in una funzione che deve
restituire l'oggetto stesso, pu usare l'istruzione return *this; ).
Nel caso che la funzione abbia degli argomenti, il puntatore this viene
inserito per primo, e gli altri argomenti vengono spostati in avanti di una
posizione.
Se la funzione un metodo in sola lettura, il compilatore trasforma la sua
definizione nel seguente modo (per esempio):
int myclass::get( ) const ----------> int get(const myclass* const
this)
cio this diventa un puntatore costante a costante. Questo fa s che si
possano definire due metodi identici, l'uno const e l'altro no, perch in realt i
tipi del primo argomento sono diversi (e quindi l'overload ammissibile).

L'introduzione del puntatore this spiega l'apparente "stranezza" di istruzioni


come ogg.init() (in realt il codice della funzione in memoria uno solo, cio
non ne esiste uno per ogni oggetto come per i dati-membro). Pertanto, le
operazioni di accesso ai membri di un oggetto (con gli operatori . e ->),
producono risultati diversi se il right-operand un dato-membro o una
funzione-membro:

se il right-operand un dato-membro (per esempio in un'operazione


tipo ogg.x) il programma accede effettivamente alla memoria in cui
localizzato il membro x dell'oggetto ogg;
se il right-operand una funzione-membro (per esempio in
ogg.init()), il programma esegue la funzione init (che unica per tutta
la classe), aggiungendo, come primo argomento della funzione,
l'indirizzo dell'oggetto ogg.

Membri a livello di classe e accesso "friend"


Membri di tipo enumerato

Ricordiamo che un oggetto di tipo enumerato se pu assumere solo un


definito e limitato insieme di valori interi, detti enumeratori.
Quando un tipo enumerato definito all'interno di una classe, il tipo stesso
identificato esternamente dal suo nome preceduto dal nome della classe con il
solito operatore :: di risoluzione di visibilit. La stessa regola vale quando si
accede separatamente a un singolo enumeratore.
Chiariamo quanto detto con un esempio: definiamo una classe A, contenente la
definizione del tipo enumerato festivo, con enumeratori Sabato e
Domenica, e un membro giorno, di tipo festivo:
class A { public: enum festivo { Sabato, Domenica} giorno; };
vediamo ora vari modi di utilizzo nel programma:
1. A::festivo oggi = A::Sabato ;
crea l'oggetto oggi, istanza del tipo enumerato festivo della classe A
e lo inizializza con il valore dell'enumeratore Sabato;
2. A a;
a.giorno = A::Sabato; ... oppure ... a.giorno = oggi;
crea l'oggetto a, istanza della classe A e assegna il valore
dell'enumeratore Sabato (oppure dell'oggetto oggi dell'esempio
precedente) al membro giorno dell'oggetto a;
3. int domani = A::Domenica ;
crea l'intero domani e lo inizializza con il valore dell'enumeratore
Domenica (conversione di tipo implicita); questa istruzione ammessa
anche se non sono state create istanze di A o di festivo.
Da questi esempi si pu notare, fra l'altro, che gli enumeratori sono identificati
dalla classe e non dal tipo enumerato a cui appartengono: ne consegue che
non possono esistere due enumeratori con lo stesso nome definiti nella stessa
classe (anche se in due tipi enumerati diversi), mentre possono esistere due
enumeratori con lo stesso nome definiti in due classi diverse.
Notiamo inoltre, esaminando la definizione della classe A, che:

il tipo enumerato festivo stato definito nella sezione pubblica: se


cos non fosse, sarebbe accessibile, come di regola, solo dai metodi di A;
le specificazioni del tipo enumerato (festivo) e del membro di A di tipo
festivo (giorno) sono opzionali: si possono omettere quando nel
programma si usano solo gli enumeratori (come nell'esempio 3):
class A { public: enum { Sabato,
Domenica} ; };
questo in realt l'uso pi frequente che si fa dei tipi enumerati
all'interno di una classe: si definisce e si utilizza una serie di
enumeratori, a livello di classe e non dei singoli oggetti

Dati-membro statici

In C++ la parola-chiave static ha un ulteriore significato: se un datomembro di una classe dichiarato static, la variabile unica per tutta la
classe, indipendentemente dal numero di istanze della classe. In altre parole, il
C++ riserva un'area di memoria per ogni oggetto, salvo per i membri static, a
ciascuno dei quali corrisponde un'unica locazione.
Pertanto i membri static appartengono alla classe e non ai singoli oggetti. Per
individuarli si usa il nome della classe con l'operatore ::
Esempio: se sm un membro static di una classe A, la "variabile" sm
individuata dal costrutto: A::sm
I membri static non vengono creati tramite istanze della classe a cui
appartengono, ma devono essere definiti direttamente, nello stesso ambito in
cui definita la classe. Nei rari casi, per, in cui la classe definita in un
block scope, i membri static non sono ammessi. Pertanto un membro static
pu essere definito solo in un namespace (se la classe definita in quel
namespace) o nel namespace globale. Di default un membro static
inizializzato con zero (in modo appropriato al tipo), come tutte le variabili
statiche e globali.
Esempio (supponiamo che la classe sia definita nel namespace globale):
class A {
..................
static int sm ;
..................

(sm un membro static della classe A, che pu essere


privato o pubblico ; se privato, gestibile solo da un
metodo della classe A, pur essendo una variabile statica)

};
int A::sm = 10 ;

(a questo punto definisce e inizializza, con operazione


nell'ambito globale, la variabile statica: A::sm)

int main ( )
ecc...
I membri static sono molto utili per gestire informazioni comuni a tutti gli
oggetti di una classe (per esempio possono fornire i dati di default per
l'inizializzazione degli oggetti), ma nel contempo, essendo essi stessi membri
della classe, permettono di evitare il ricorso a variabili esterne, salvaguardando
cos il data hiding e l'indipendenza del codice di implementazione della classe
dalle altre parti del programma.
NOTA: la principale differenza di significato dello specificatore static, se
applicato a un membro o a un oggetto di una classe, consiste nel fatto che,
nel primo caso, si crea una variabile nell'ambito di una classe (che deve
appartenere a sua volta a un namespace o al namespace globale), nel

secondo si crea una variabile locale nell'ambito di un blocco; in entrambi i casi


il lifetime della variabile persiste fino alla fine del programma. Se invece static
applicato a un oggetto non locale (da evitare, meglio ricorrere al namespace
anonimo), il suo significato completamente diverso (visibilit limitata al file
scope).
NOTA2: per i motivi anzidetti, l'attributo static di un membro di una classe
deve essere specificato soltanto nella dichiarazione e non nella definizione,
perch in quest'ultima assumerebbe il significato di limitare la sua visibilit al
file scope.

Funzioni-membro statiche

Anche le funzioni-membro di una classe possono essere dichiarate static.


Es.:

class A { .....
static int conta( ) ;
(prototipo)
..... };

int A::conta( ) { ..... }


(definizione)
Nel prog. chiamante:
int n = A::conta( );

come si pu notare dall'esempio, nella chiamata di una funzione-membro


static, bisogna qualificare il suo nome con quello della classe di appartenenza.
Notare inoltre che, nella definizione della funzione, lo specificatore static
non va messo (per lo stesso motivo per cui non va messo davanti alla
definizione di un dato-membro static).
Una funzione-membro static (che, come tutti gli altri membri, pu essere
privata o pubblica), accede ai membri della classe ma non collegata a un
oggetto in particolare e quindi non ha il puntatore nascosto this. Ne consegue
che, se deve operare su oggetti, questi devono essere trasmessi esplicitamente
come argomenti.
Normalmente i metodi static vengono usati per trattare dati-membro static o,
in generale, quando non si pone la necessit di operare su un singolo oggetto
della classe (cio quando la presenza del puntatore nascosto this sarebbe un
sovraccarico inutile). Viceversa, quando un metodo deve operare direttamente su
un oggetto (uno e uno solo alla volta), pi conveniente che sia incapsulato
nell'oggetto stesso e quindi non venga dichiarato static.

Funzioni friend

Una normale dichiarazione di un metodo specifica tre cose logicamente


distinte:
1. la funzione pu accedere ai membri privati della classe;
2. la funzione nell' ambito di visibilit della classe;
3. la funzione incapsulata negli oggetti (possiede il puntatore this).
Abbiamo visto che, dichiarando un metodo con lo specificatore static,
possibile fornire alla funzione le prime due propriet, ma non la terza. Se invece
dichiariamo una funzione con lo specificatore friend, possibile fornirle solo
la prima propriet.
Una funzione si dice "friend" di una classe, se definita in un ambito diverso
da quello della classe, ma pu accedere ai suoi membri privati. Per ottenere
ci, bisogna inserire il prototipo della funzione nella definizione della classe
(non importa se nella sezione privata o pubblica), facendo precedere lo
specificatore friend.
Es.:

DEFINIZIONE CLASSE
class A {
int mp ; ..........

DEFINIZIONE FUNZIONE
void amica(A ogg, .....)
{

friend void amica(A, .....) ;


........ };

........ ogg.mp ........


}

la funzione amica, che non un metodo della classe A (nell'esempio


definita nel namespace globale), tuttavia dichiarata con lo specificatore
friend nella definizione della classe A, e quindi pu accedere al suo membri
privati (nell'esempio, a mp). Notare che la funzione, essendo priva del
puntatore this (come i metodi static), pu operare sugli oggetti della classe
solo se gli oggetti interessati le sono trasmessi come argomenti.
Se una stessa funzione friend di due o pi classi, il suo prototipo preceduto
da friend va inserito nelle definizioni di tutte le classi interessate. Sorge allora
un problema, come si pu vedere dall'esempio seguente:
class A {...friend int fun(A,B, .....);...};

<---- a questo punto C++ non sa


ancora che B una classe

class B {...friend int fun(A,B, .....);...};


Ci sono due possibili soluzioni per far sapere al sistema che B una classe: o si
pone in testa al gruppo di istruzioni la dichiarazione anticipata:
class B;
oppure si inserisce, nel prototipo che potrebbe generare errore, la parolachiave class
friend int fun(A,class B, .....);
Le funzioni friend sono preferibili ai metodi static proprio quando devono
accedere a pi classi e quindi non conveniente farli appartenere a una classe

piuttosto che a un'altra. In ogni caso, per favorire la programmazione


modulare, consigliabile aggregare in uno stesso ambito (per esempio in un
namespace) classi e funzioni friend collegate.

Classi friend

Quando tutte le funzioni-membro di una classe B sono friend di una classe


A, possibile, anzich dichiarare ciascuna funzione individualmente, inserire
una sola dichiarazione in A, indicante che l'intera classe B friend:
class A {..........friend class B;..........};
L'uso di funzioni e classi friend permette al C++ di aggirare il data hiding
ogni volta che classi diverse devono interagire strettamente o condividere gli
stessi dati, pur restando distinte.
C' da dire infine che le relazioni di tipo friend non sono simmetriche (se A
friend di B non detto che B sia friend di A), n transitive (se A friend di B
e B friend di C, non detto che A sia friend di C). In sostanza ogni relazione
deve essere esplicitamente dichiarata.

Costruttori e distruttori degli oggetti


Costruzione e distruzione di un oggetto

Abbiamo detto pi volte che quando un oggetto, istanza di un tipo nativo o


astratto, viene creato, si dice che quell'oggetto costruito. Analogamente,
quando l'oggetto cessa di esistere, si dice che quell'oggetto distrutto.
Vediamo le varie circostanze in cui un oggetto pu essere costruito o
distrutto:
1. Un oggetto automatico (cio locale non statico) viene costruito ogni
volta che la sua definizione viene incontrata durante l'esecuzione del
programma, e distrutto ogni volta che il programma esce dall'ambito in
cui tale definizione si trova.
2. Un oggetto locale statico viene costruito la prima volta che la sua
definizione viene incontrata durante l'esecuzione del programma, e
distrutto una sola volta, quando il programma termina.
3. Un oggetto allocato nella memoria dinamica (area heap ) viene
costruito mediante l'operatore new e distrutto mediante l'operatore
delete.
4. Un oggetto, membro non statico di una classe, viene costruito ogni
volta che (o meglio, immediatamente prima che) viene costruito un
oggetto della classe di cui membro, e distrutto ogni volta che (o
meglio, immediatamente dopo che) lo stesso oggetto viene distrutto.
5. Un oggetto, elemento di un array, viene costruito o distrutto ogni
volta che l'array di cui fa parte viene costruito o distrutto.
6. Un oggetto globale, un oggetto di un namespace o un membro
statico di una classe, viene costruito una sola volta, alla "partenza" del
programma e distrutto quando il programma termina.
7. Infine, un oggetto temporaneo viene costruito per memorizzare
risultati parziali durante la valutazione di un'espressione, e distrutto
alla fine dell'espressione completa in cui compare.
Come si pu notare, la costruzione o distruzione di un oggetto pu avvenire
in momenti diversi, in base alla categoria dell'oggetto che si sta considerando. In
ogni caso, sia durante la costruzione che durante la distruzione, potrebbero
rendersi necessarie delle operazioni specifiche. Per esempio, se un membro di
una classe un puntatore, potrebbe essere necessario creare l'area puntata
(che non viene fatto automaticamente, come nel caso degli array) e allocarla
dinamicamente con l'operatore new; quest'area dovr per essere rilasciata,
prima e poi (con l'operatore delete), e capita non di rado che non lo si possa
fare prima della distruzione dell'oggetto. Poich d'altra parte un oggetto pu
anche essere costruito o distrutto automaticamente, si pone il problema di
come "intercettare" il momento della sua costruzione o distruzione.
Nel caso che gli oggetti siano istanze di una classe, il C++ mette a
disposizione un mezzo molto potente, che consiste nella possibilit di definire dei
particolari metodi della classe, che il programma riconosce come funzioni da
eseguire al momento della costruzione o distruzione di un oggetto. Questi

metodi prendono il nome di costruttori e distruttori degli oggetti. Il loro


scopo principale , per i costruttori, di inizializzare i membri e/o allocare
risorse, per i distruttori, di rilasciare le risorse allocate.

Costruttori

I costruttori degli oggetti devono sottostare alle seguenti regole (ci rifaremo al
solito esempio della classe point):
1. devono avere lo stesso nome della classe

prototipo:

point(......);

definizione esterna:

point::point(......) {......}

2. non bisogna specificare il tipo di ritorno (neanche void)


NOTA: in realt la chiamata di un costruttore pu anche essere inserita
in un'espressione; ci significa che un costruttore ritorna "qualcosa" e
precisamente .... l'oggetto che ha appena creato!
3. ammettono argomenti e defaults; i costruttori senza argomenti (o
con tutti argomenti di default) sono detti: "costruttori di default"
prototipo di costruttore di default della classe point:
point( );
prototipo di costruttore della classe point con un argomento
required e uno di default:
point(double,double=0.0);
4. possono esistere pi costruttori, in overload, in una stessa classe. Il
C++ li distingue in base alla lista degli argomenti. Come tutte le
funzioni in overload, non sono ammessi costruttori che differiscano
solo per gli argomenti di default.
5. devono essere dichiarati come funzioni-membro pubbliche, in quanto
sono sempre chiamati dall'esterno della classe a cui appartengono.
I costruttori non sono obbligatori: se una classe non ne possiede, il C++
fornisce un costruttore di default con "corpo nullo" .
Il costruttore di default (dichiarato nella classe oppure fornito dal C++)
viene eseguito automaticamente nel momento in cui l'oggetto viene creato nel
programma (si vedano i vari casi elencati nella sezione precedente). Esempio :
definizione del costruttore di default di
point:

point::point( ) {x=3.5;
y=2.1;}

definizione dell'oggetto p, istanza di point:

point p ;

nel momento in cui l'eseguita l'istruzione di definizione dell'oggetto p, il


costruttore di default va in esecuzione automaticamente, inizializzando p con
3.5 nel membro x e 2.1 nel membro y.
Se invece in una classe esiste almeno un costruttore con argomenti, il C++
non mette a disposizione alcun costruttore di default e perci questo, se
necessario, va esplicitamente definito come metodo della classe. In sua
assenza, i costruttori con argomenti non vengono invocati automaticamente e
pertanto ogni istruzione del programma che determini, direttamente o
indirettamente, la creazione di un oggetto, deve contenere la chiamata esplicita
di uno dei costruttori disponibili, nel modo che dipende dalla categoria
dell'oggetto interessato. Esamineremo i vari casi separatamente, rifacendoci
all'elenco illustrato nella sezione precedente.
Per il momento consideriamo il caso pi frequente, che quello di un oggetto
singolo creato direttamente mediante la definizione del suo nome (casi 1., 2. e
6.): i modi possibili per invocare un costruttore con argomenti sono due, come
mostrato dal seguente esempio:
definizione del costruttore di point :

point::point(double x0, double


y0)
{x=x0; y=y0;}

definizione dell'oggetto p, istanza di


point :
prima forma :
seconda forma :

point p (3.5, 2.1);


point p = point(3.5, 2.1);

la prima forma pi concisa, ma la seconda pi chiara, in quanto ha proprio


l'aspetto di una inizializzazione tramite chiamata esplicita di una funzione. In
entrambi i casi viene invocato un costruttore con due argomenti di tipo
double, che inizializza p inserendo i valori dei due argomenti rispettivamente
nel membro x e nel membro y. Aggiungiamo che la chiamata esplicita pu
essere utilizzata anche per invocare un costruttore di default ( necessaria,
per esempio, quando l'oggetto creato all'interno di un'espressione), per
esempio:
throw Error( ) ;
(solleva un'eccezione e trasmette un oggetto della classe Error, creato con il
costruttore di default).
Terminiamo questa sezione osservando che anche i tipi nativi hanno i loro
costruttori di default (sebbene di solito non si usino), che per, quando
servono, vanno esplicimente chiamati, come nel seguente esempio:
int i = int();
i costruttori di default dei tipi nativi inizializzano le variabili con zero (in
modo appropriato al tipo). Sono utili quando si ha a che fare con tipi
parametrizzati (come i template, che vedremo pi avanti), in cui non noto a
priori se al parametro verr sostituito un tipo nativo o un tipo astratto.

Costruttori e conversione implicita

Un'attenzione particolare merita il costruttore con un solo argomento. In


questo caso, infatti, il costruttore definisce anche una conversione implicita di
tipo dal tipo dell'argomento a quello della classe (ovviamente, spetta al codice
di implementazione del costruttore assicurare che la conversione venga eseguita
in modo corretto). Esempio:

definizione del costruttore di point :

point::point(double d)
{x=d; y=d;}

definizione dell'oggetto p, istanza di point :


point p = 3;
equivalente a :

point p = point(3.0);

Notare che il numero 3 (che di tipo int) convertito implicitamente, prima a


double, e poi nel tipo point (tramite esecuzione del costruttore, che lo utilizza
per inizializzare l'oggetto p). Notare anche (per "chiudere il cerchio") che
un'espressione del tipo point(3.0) formalmente identica a un'operazione di
casting in function-style ( persino ammessa la forma in C-style !).
Le conversioni implicite sono molto utili nella definizione degli operatori in
overload (come vedremo prossimamente).
La conversione implicita pu essere esclusa premettendo, nella dichiarazione
(non nella definizione esterna) del costruttore lo specificatore explicit :
explicit point(double);
il casting continua invece ad essere ammesso (anche nella forma in C-style), in
quanto coincide puramente con la chiamata del costruttore.

Distruttori

I distruttori degli oggetti devono sottostare alle seguenti regole (ci rifaremo al
solito esempio della classe point):
1. devono avere lo stesso nome della classe preceduto da una tilde (~)

2.
3.
4.
5.

prototipo:

~point( );

definizione esterna:

point::~point( ) {......}

non bisogna specificare il tipo di ritorno (neanche void)


non ammettono argomenti
ciascuna classe pu avere al massimo un distruttore
devono essere dichiarati come funzioni-membro pubbliche, in quanto
sono sempre chiamati dall'esterno della classe a cui appartengono.

Come i costruttori, i distruttori non sono obbligatori; sono richiesti quando


necessario liberare risorse allocate dagli oggetti o ripristinare le condizioni
preestistenti alla loro creazione. Se esiste, un distruttore sempre chiamato
automaticamente ogni volta che l'oggetto di cui fa parte sta per essere
distrutto.
Quando pi oggetti sono costruiti in sequenza, e poi sono distrutti
contemporaneamente (per esempio se sono oggetti automatici che escono dal
loro ambito di visibilit), i loro distruttori sono normalmente eseguiti in
sequenza inversa a quella di costruzione.

Oggetti allocati dinamicamente

Se il programma non definisce direttamente un oggetto, ma un suo puntatore,


il costruttore non entra in azione al momento della definizione del puntatore,
bens quando viene allocata dinamicamente la memoria per l'oggetto (caso 3.
dell'elenco). Solito esempio:
point* ptr;

costruisce la "variabile" puntatore ma non l'area puntata

ptr = new point;

costruisce l'area puntata

la seconda istruzione dell'esempio esegue varie cose in una sola volta:

alloca memoria dinamica per un oggetto della classe point


assegna l'indirizzo dell'oggetto, restituito dall'operatore new, al
puntatore ptr
inizializza l'oggetto eseguendo il costruttore di default della classe
point

Quando si vuole che nella creazione di un oggetto sia eseguito un costruttore


con argomenti, bisogna aggiungere, nell'istruzione di allocazione della
memoria, l'elenco dei valori degli argomenti (fra parentesi tonde):
ptr = new point (3.5, 2.1);

questa istruzione cerca, fra i costruttori della classe point, quello con due
argomenti di tipo double, e lo esegue al posto del costruttore di default .
Se si alloca dinamicamente un array di oggetti, sappiamo che la
dimensione dell'array va specificata fra parentesi quadre dopo il nome della
classe. Poich il costruttore chiamato unico per tutti gli elementi
dell'array, questi vengono tutti inizializzati nello stesso modo. Nessun problema
se si usa il costruttore di default (purch sia disponibile):
ptr = new point [10];
ma, quando si vuole usare un costruttore con argomenti:
ptr = new point [10] (3.5, 2.1);
non sempre l'istruzione viene eseguita correttamente: anzitutto alcuni compilatori
pi antichi (come il Visual C++, vers. 6) non l'accettano; quelli che l'accettano
la eseguono bene se il tipo astratto (come nell'esempio di cui sopra), ma se il
tipo nativo, per es.:
ptr = new int [10] (3);
disponendo solo del costruttore di default, tutti gli elementi dell'array
vengono comunque inizializzati con 0 (cio la parte dell'istruzione fra parentesi
tonde viene ignorata).
Gli oggetti allocati dinamicamente non sono mai distrutti in modo
automatico. Per ottenere che vengano distrutti, bisogna usare l'operatore
delete. Es. (al solito ptr punta a oggetti della classe point):
delete ptr; (per un singolo oggetto)
delete [ ] ptr; (per un
array)
a questo punto viene eseguito, per ogni oggetto, il distruttore della classe
point (se esiste) .

Membri puntatori

Una particolare attenzione va rivolta alla programmazione dei costruttori e del


distruttore di un oggetto che contiene membri puntatori.
Infatti, a differenza dal caso degli array, l'area puntata non definita
automaticamente e quindi (a meno che al puntatore non venga successivamente
assegnato l'indirizzo di un'area gi esistente) capita quasi sempre che l'area
debba essere allocata nella memoria heap. e che questa operazione venga
eseguita proprio da un costruttore dell'oggetto.
Analogamente, quando l'oggetto distrutto (per esempio se un oggetto
automatico che va out of scope), sono del pari distrutti tutti i suoi membri,
compresi i membri puntatori, ma non le aree puntate, che continuano ad
esistere senza essere pi raggiungibili (errore di memory leak).

Pertanto indispensabile che sia lo stesso distruttore dell'oggetto a incaricarsi


di distruggere esplicitamente le aree puntate, cosa che pu essere fatta
solamente usando l'operatore delete. Esempio:
CLASSE
class Persona {
char* nome;

COSTRUTTORE
Persona::Persona (int n)

Persona::~Persona ( )

char* cognome;
public:
Persona (int);
~Persona ( );
.... altri metodi };

DISTRUTTORE

nome = new char [n];

delete [ ] nome;

cognome = new char [n];

delete [ ] cognome;

}
DEFINIZIONE DELL'OGGETTO NEL PROGRAMMA
Persona Tizio(25);

l'oggetto Tizio, istanza della classe Persona, viene costruito


automaticamente nella memoria stack, e cos pure i suoi membri. In aggiunta, il
costruttore dell'oggetto alloca nella memoria heap due aree di 25 byte, e
sistema i rispettivi indirizzi nei membri puntatori Tizio.nome e
Tizio.cognome. Quando l'oggetto Tizio va out of scope, il distruttore entra
in azione automaticamente e, con l'operatore delete, libera la memoria heap
allocata per le due aree. Senza il distruttore, sarebbe stata liberata soltanto la
memoria stack occupata dall'oggetto Tizio e dai suoi membri puntatori , ma
non l'area heap indirizzata da questi.

Costruttori di copia

I costruttori di copia sono particolari costruttori che vengono eseguiti quando


un oggetto creato per copia. Ricordiamo brevemente in quali casi ci si
verifica:

definizione di un oggetto e sua inizializzazione tramite un oggetto


esistente dello stesso tipo;
passaggio by value di un argomento a una funzione;
restituzione by value del valore di ritorno di una funzione;
passaggio di un'eccezione al costrutto catch.

Un costruttore di copia deve avere un solo argomento, dello stesso tipo


dell'oggetto da costruire; l'argomento (che rappresenta l'oggetto esistente)
deve essere dichiarato const (per sicurezza) e passato by reference (altrimenti
si creerebbe una copia della copia!). Riprendendo il solito esempio, il costruttore
di copia della classe point :
point::point(const point& q) {......}
e viene chiamato automaticamente ogni volta che si verifica una delle quattro
circostanze sopraelencate.

Per esempio, se definiamo un oggetto p e lo inizializziamo con un oggetto


preesistente q:
point p = q ;
questa istruzione aziona il costruttore di copia, a cui trasmesso q come
argomento.
I costruttori di copia, come ogni altro costruttore, non sono obbligatori: se
una classe non ne possiede, il C++ fornisce un costruttore di copia di default
che esegue la copia membro a membro. Questo pu essere soddisfacente nella
maggioranza dei casi. Tuttavia, se la classe possiede dei membri puntatori,
l'azione di default copia i puntatori, ma non le aree puntate: alla fine si
ritrovano due oggetti i cui rispettivi membri puntatori puntano alla stessa area.
Ci potrebbe essere pericoloso, perch, se viene chiamato il distruttore di uno
dei due oggetti, il membro puntatore dell'altro, che esiste ancora, punta a
un'area che non esiste pi (errore di dangling references).
Nell'esempio seguente una classe di nome A contiene, fra l'altro, un membro
puntatore a int e un costruttore di copia che esegue le operazioni idonee ad
evitare l'errore di cui sopra:
CLASSE

COSTRUTTORE DI COPIA

class A {

A::A(const A& a)

int* pa;

public:

pa = new int ;

A(const A&);
........ };

*pa

= *a.pa ;

in questo modo, a seguito della creazione di un oggetto a2 per copia da un


esistente oggetto a1:
A a2 = a1;
il costruttore di copia fa si che la variabile puntata *a1.pa venga copiata in
*a2.pa; senza il costruttore sarebbe copiato il puntatore a1.pa in a2.pa.

Liste di inizializzazione

Quando un costruttore deve, fra l'altro, inizializzare i membri della propria


classe, lo pu fare tramite una lista di inizializzazione (introdotta dal segno ":"
e inserita nella definizione del costruttore dopo la lista degli argomenti), la
quale sostituisce le istruzioni di assegnazione (in effetti un costruttore non
dovrebbe assegnare bens solo inizializzare, anche se la distinzione pu
sembrare solo formale).
La sintassi di una lista di inizializzazione si desume dal seguente esempio:

CLASSE
class A {
int m1, m2;

COSTRUTTORE
A::A(int p, double q) : m1(p), m2(0),
r(q)
{

double r;
public:

.... eventuali altre operazioni....


}

A(int,double);
........ };
Notare che alcuni membri possono essere inizializzati con valori costanti, altri
con i valori degli argomenti passati al costruttore. L'ordine nella lista
indifferente; in ogni i caso i membri sono costruiti e inizializzati nell'ordine in
cui appaiono nella definizione della classe.
E' buona norma utilizzare le liste di inizializzazione ogni volta che possibile. Il
loro uso indispensabile quando esistono membri della classe dichiarati const
o come riferimenti, per i quali l'inizializzazione obbligatoria.

Membri oggetto

Riprendiamo ora ad esaminare l'elenco presentato all'inizio di questo capitolo e


consideriano la costruzione e distruzione degli oggetti, quando sono membri
non statici di una classe (caso 4. dell'elenco).
Sappiamo gi che una classe pu avere anche tipi classe fra i suoi membri;
per esempio:
class A {
int aa; ........ };
class B {
int bb; ........ };

class C {
A ma;
B mb;
int mc; ........ };

La classe C del nostro esempio viene detta classe composta, in quanto


contiene, fra i suoi membri, oggetti di altre classi (il membro-oggetto ma
della classe A e il membro-oggetto mb della classe B).
Sappiamo inoltre che, creata un'istanza cc di C, le variabili corrispondenti ai
singoli membri vanno indicate nel programma con espressioni del tipo: cc.ma.aa
oppure cc.mb.bb (diritti di accesso permettendo).

Nel momento in cui un oggetto di una classe composta sta per essere
costruito, e prima ancora che il suo costruttore completi l'operazione, sono
eseguiti automaticamente i costruttori che inizializzano i membri delle classi
componenti. Se esistono e si vogliono utilizzare i costruttori di default, non
esiste problema. Ma se deve essere chiamato un costruttore con argomenti, ci
si chiede in che modo tali argomenti possano essere passati, visto che il
costruttore di un membro-oggetto non chiamato esplicitamente.
In questi casi, spetta al costruttore della classe composta provvedere a che
vengano eseguiti correttamente anche i costruttori delle classi componenti.
Per ottenere ci, deve includere, nella sua lista di inizializzazione, tutti (e soli) i
membri-oggetto che non utilizzano il proprio costruttore di default, ciascuno
con i valori di inzializzazione che corrispondono esattamente (cio con gli stessi
tipi e nello stesso ordine) alla lista degli argomenti del rispettivo costruttore.
Seguitando con il nostro esempio:
costruttore di A :

A::A(int x) : aa(x) { ........ }

costruttore di B :

B::B(int x) : bb(x) { ........ }

costruttore di C :

C::C(int x, int y, int z) : ma(z), mb(x), mc(y) { ........ }

Le classi componenti A e B hanno anche una loro vita autonoma e in


particolare possono essere istanziate con oggetti propri. In questo caso il
costruttore di C pu generare i suoi membri-oggetto copiando oggetti gi
costruiti delle classi componenti. Riprendendo l'esempio, un'altra forma del
costruttore di C potrebbe essere:
}

C::C(int x, const A& a, const B& b) : ma(a), mb(b), mc(x) { ........

dove gli argomenti a e b corrispondono a istanze gi create rispettivamente di


A e di B; in tale caso viene eseguito il costruttore di copia, se esiste, oppure di
default viene fatta la copia membro a membro.
Quando un oggetto di una classe composta viene distrutto, vengono
successivamente e automaticamente distrutti tutti i membri delle classi
componenti, in ordine inverso a quello della loro costruzione.

Array di oggetti

Gli elementi di un array di oggetti (caso 5. dell'elenco iniziale) vengono


inizializzati, tramite il costruttore della classe comune di appartenenza, non
appena l'array definito.

Come al solito, non esiste nessun problema se si utilizza il costruttore di


default:
point pt[5];
(costruisce 5 oggetti della classe point, invocando, per ciascuno di essi, il
costruttore di default).
Se invece si vuole (o si deve, per mancanza del costruttore di default) utilizzare
un costruttore con argomenti, bisogna considerare a parte il caso di
costruttore con un solo argomento (o con pi argomenti di cui uno solo
required). Ricordiamo a questo proposito come si inizializza un array di tipo
nativo:
int valori[] = {32, 53, 28, 85, 21};
nello stesso modo si pu inizializzare un array di tipo astratto:
point pt[] = {2.3, -1.2, 0.0, 1.4, 0.5};
ma in questo caso ogni valore di inizializzazione , relativo a un elemento
dell'array, viene passato come argomento al costruttore. Ci possibile in
quanto, grazie alla presenza del costruttore con un solo argomento, ogni
valore convertito implicitamente in un oggetto della classe point
(chiamiamolo pn) e quindi l'espressione precedente diventa:
point pt[] = {p0, p1, p2, p3, p4};
l'inizializzazione in questa forma di un array di un certo tipo, tramite
elementi dello stesso tipo precedentemente costruiti, sempre consentita,
anche per i tipi astratti.
Non esiste invece alcuna possibilit di utilizzare costruttori con due o pi
argomenti.

Oggetti non locali

Abbiamo gi considerato i casi degli oggetti globali, degli oggetti nei


namespace e dei membri statici delle classi (numero 6. dell'elenco iniziale),
come casi particolari di oggetto singolo creato direttamente mediante la
definizione del suo nome (vedere sezione: Costruttori). Sappiamo che tali
oggetti non locali sono costruiti una sola volta, alla partenza del programma, e
distrutti solo quando il programma termina.
Qui vogliamo solo aggiungere alcune considerazioni riguardo all'ordine di
costruzione e distruzione di pi oggetti:

due oggetti definiti nella stessa translation unit sono costruiti nello
stesso ordine in cui la loro definizione appare nel programma, e distrutti
in ordine inverso;
l'ordine di costruzione (e di distruzione) invece indeterminato se i due
oggetti sono definiti in translation unit diverse.

Ne consegue che molto "imprudente" inserire, nel codice del costruttore di un


oggetto non locale, operazioni che coinvolgano oggetti definiti in altre

translation unit (in particolare evitare istruzioni con cin e cout, in quanto non si
pu essere sicuri che gli oggetti globali delle classi di flusso di I/O siano gi
stati costruiti).

Oggetti temporanei

Abbiamo detto che un oggetto temporaneo (caso 7. dell'elenco iniziale) viene


costruito per memorizzare risultati parziali durante la valutazione di
un'espressione, e distrutto alla fine dell'espressione completa in cui
compare (con il termine "espressione completa" si intende un'espressione
che non sia sotto-espressione di un'altra, cio, in pratica, un'intera istruzione di
programma).
Finora abbiamo considerato soltanto operazioni fra tipi nativi, per i quali il
problema della costruzione di un oggetto temporaneo non si pone. Ma, come
vedremo nel prossimo capitolo, il C++ consente anche operazioni fra tipi
astratti, tramite la possibilit di ridefinire, in overload, le funzioni che
competono all'azione di molti operatori (overload degli operatori). Per
esempio, si potrebbe ridefinire l'operatore di somma (+) in modo che accetti
fra i suoi operandi anche oggetti della classe classe point (si tratterebbe in
questo caso di una somma "vettoriale", ottenuta mediante somma membro a
membro delle coordinate dei punti):
point p = p1 + p2;
dove p1 e p2 sono istanze gi create della stessa classe.
In questo caso costruito l'oggetto temporaneo p1 + p2, che viene
distrutto dopo che l'istruzione stata eseguita. Ci chiediamo per: cosa succede
se la classe point non ha un costruttore di default ? La risposta che spetta
al codice di implementazione della funzione, che definisce l'operatore di
somma in overload, provvedere a che l'operazione sia eseguita correttamente
(per esempio potrebbe definire un'istanza locale di point, con valori di
inizalizzazione qualsiasi, usarla per memorizzare la somma di p1 e p2 membro
a membro, e infine trasmetterla come valore di ritorno by value, da copiare
in p).
In generale, tutte le volte che un'operazione crea un oggetto temporaneo, la
funzione che compete a quell'operazione deve creare nel proprio ambito
locale un corrispondente oggetto, che, in quanto costruito mediante
definizione con un nome (categoria 1. del nostro elenco), non pone problemi,
possegga o meno il costruttore di default.

Utilit dei costruttori e distruttori

Poich in C++ ogni oggetto ha una sua precisa connotazione, caratterizzata da


propriet e metodi, i costruttori e i distruttori hanno in realt un campo di
applicazione molto pi vasto della semplice inizializzazione o liberazione di
risorse: in senso lato possono servire ogni qual volta un oggetto necessita di ben
definite operazioni iniziali e finali, incapsulate nell'oggetto stesso. Per esempio,
se l'oggetto consiste in una procedura di help, il costruttore potrebbe servire
per creare la "finestra di aiuto", mentre il distruttore avrebbe il compito di
ripristinare le condizioni preesistenti dello schermo.

Overload degli operatori


Estendibilit del C++

In tutti i linguaggi, gli operatori sono dei simboli convenzionali che rendono pi
agevole la presentazione e lo sviluppo di concetti di uso frequente. Per esempio,
la notazione:
a+b*c
risulta pi agevole della frase:
"moltiplica b per c aggiungi il risultato ad a"
L'utilizzo di una notazione concisa per le operazioni di uso comune di importanza
fondamentale.
Il C++ supporta, come ogni altro linguaggio, un insieme di operazioni per i suoi
tipi nativi. Tuttavia la maggior parte dei concetti utilizzati comunemente non
sono facilmente rappresentabili per mezzo di tipi nativi, e bisogna spesso fare
ricorso ai tipi astratti. Per esempio, i numeri complessi, le matrici, i segnali, le
stringhe di caratteri, le aggregazioni di dati, le code, le liste ecc... sono tutte entit
che meglio si prestano a essere rappresentate mediante le classi. E' pertanto
necessario che anche le operazioni fra queste entit possano essere descritte
tramite simboli convenzionali, in alternativa alla chiamata di funzioni specifiche
(come avviene negli altri linguaggi), che non permetterebbero quella notazione
concisa che, come si detto, di importanza fondamentale per una
programmazione pi semplice e chiara.
Il C++ consente di soddisfare questa esigenza tramite l'overload degli
operatori: il programmatore ha la possibilit di creare nuove funzioni che
ridefiniscono il significato dei simboli delle operazioni, rendendo queste
applicabili anche ai tipi astratti (estendibilit del C++). La caratteristica
determinante per il reale vantaggio di questa tecnica, che, a differenza dalle
normali funzioni, quelle che ridefiniscono gli operatori possono essere
chiamate mediante il solo simbolo dell'operazione (con gli argomenti della
funzione che diventano operandi): in definitiva la chiamata della funzione
"scompare" dal codice del programma e al suo posto si pu inserire una "semplice
e concisa" operazione. Per esempio, se viene creata una funzione che
ridefinisce la somma (+) fra due oggetti, a e b, istanze di una certa classe, in
luogo della chiamata della funzione si pu semplicemente scrivere: a+b. Se si
pensa che un'espressione pu essere costituita da parecchie operazioni
insieme, il vantaggio di questa tecnica per la concisione e la leggibilit del codice
risulta evidente (in alternativa a ripetute chiamate di funzioni, "innestate" l'una
nell'altra). Per esempio, tornando all'espressione iniziale, costituita da solo due
operazioni:
operatori in overload :

a+b*c

chiamata di funzioni specifiche :

somma(a,moltiplica(b,c))

Ridefinizione degli operatori

Per ottenere l'overload di un operatore bisogna creare una funzione il cui


nome (che eccezionalmente non segue le regole generali di specifica degli
identificatori) deve essere costituito dalla parola-chiave operator seguita,
con o senza blanks in mezzo, dal simbolo dell'operatore (es.: operator+). Gli
argomenti della funzione devono corrispondere agli operandi dell'operatore.
Ne consegue che per gli operatori unari necessario un solo argomento, per
quelli binari ce ne vogliono due (e nello stesso ordine, cio il primo argomento
deve corrispondere al left-operand e il secondo argomento al right-operand).
Non concesso "inventare" nuovi simboli, ma si possono solo utilizzare i simboli
degli operatori esistenti. In pi, le regole di precedenza e associativit
restano legate al simbolo e non al suo significato, come pure resta legata al
simbolo la categoria dell'operatore (unario o binario). Per esempio, un
operatore in overload associato al simbolo della divisione (/) non pu mai
essere definito unario e ha sempre la precedenza sull'operatore associato al
simbolo +, qualunque sia il significato di entrambi.
E' possibile avere overload di quasi tutti gli operatori esistenti, salvo: ?:,
sizeof, typeid e pochi altri, fra cui quelli (come :: e .) che hanno come operandi
nomi non "parametrizzabili" (come i nomi delle classi o dei membri di una
classe).
Come per le funzioni in overload, nel caso dello stesso operatore ridefinito pi
volte con tipi diversi, il C++ risolve l'ambiguit in base al contesto degli
operandi, riconoscendone il tipo e decidendo di conseguenza quale operatore
applicare.
Torniamo ora alla classe point e vediamo un esempio di possibile operatore di
somma (il nostro intento di ottenere la somma "vettoriale" fra due punti);
supponiamo che la classe sia provvista di un costruttore con due argomenti:

operazione :

p = p1+p2 ;

funzione somma
:

point operator+(const point& p1, const point&


p2)
{
point ptemp(0.0,0.0);
ptemp.x = p1.x + p2.x ;
ptemp.y = p1.y + p2.y ;
return ptemp ;
}

Notare:
1. la funzione ha un valore di ritorno di tipo point;
2. gli argomenti-operandi sono passati by reference e dichiarati const,
per maggiore sicurezza (const) e rapidit di esecuzione (passaggio by
reference);
3. nella funzione definito un oggetto automatico (ptemp),
inizializzato compatibilmente con il costruttore disponibile (vedere il
problema della inizializzazione degli oggetti temporanei nel capitolo
precedente);
4. in ptemp i due operandi sono sommati membro a membro (la somma
ammessa in quanto fra due tipi double);
5. in uscita ptemp (essendo un oggetto automatico) "muore", ma una sua
copia passata by value al chiamante, dove successivamente
assegnata a p
Nota ulteriore: ammessa anche la chiamata della funzione nella forma
tradizionale:
p = operator+(p1, p2) ;
ma in questo caso si vanificherebbero i vantaggi offerti dalla notazione simbolica
delle operazioni.

Metodi della classe o funzioni esterne?

Finora abbiamo parlato delle funzioni che ridefiniscono gli operatori in


overload, senza preoccuparci di dove tali funzioni debbano essere definite.
Quando esse accedono a membri privati della classe, possono appartenere
soltanto a una delle seguenti tre categorie:
1. sono metodi pubblici non statici della classe;
2. sono metodi pubblici statici della classe;
3. sono funzioni friend della classe.
Escludiamo subito che siano metodi statici, non perch non sia permesso, ma
perch non sarebbe conveniente, in quanto un metodo statico pu essere
chiamato solo se il suo nome qualificato con il nome della classe di
appartenenza,
es.:
p = point::operator+(p1, p2) ;
e quindi non esiste il modo di utilizzarlo nella rappresentazione simbolica di
un'operazione.
Restano pertanto a disposizione solo i metodi non statici e le funzioni friend
(o esterne, se non accedono a membri privati). La scelta pi appropriata
dipende dal contesto degli operandi e dal tipo di operazione. In generale
conviene che sia un metodo quando l'operatore unario, oppure (e in questo

caso obbligatorio) quando il primo operando oggetto della classe e la


funzione lo restituisce come l-value, come accade per esempio per gli
overload degli operatori di assegnazione (=) e in notazione compatta (+=
ecc...). Viceversa, non ha molto senso che sia un metodo l'overload
dell'addizione (che abbiamo visto come esempio nella sezione precedente), il
quale opera su due oggetti e restituisce un risultato da memorizzare in un terzo.
La miglior progettazione degli operatori di una classe consiste nell'individuare
un insieme ben definito di metodi per le operazioni che si applicano su un unico
oggetto o che modificano il loro primo operando, e usare funzioni esterne (o
friend) per le altre operazioni; il codice di queste funzioni risulta per
facilitato, in quanto pu utilizzare gli stessi operatori gi definiti come metodi
(vedremo pi avanti un'alternativa dell'operatore + come funzione esterna, che
usa l'operatore += implementato come metodo).
NOTA: nei tipi astratti, l'esistenza degli operatori in overload + e = non
implica che sia automaticamente definito anche l'operatore in overload +=

Il ruolo del puntatore nascosto this

E' chiaro a tutti perch un'operazione che si applica su un unico oggetto o che
modifica il primo operando preferibile che sia implementata come metodo
della classe? Perch, in quanto metodo non statico, pu sfruttare la presenza
del puntatore nascosto this, che, come sappiamo, punta allo stesso oggetto
della classe in cui il metodo incapsulato e viene automaticamente inserito
dal C++ come primo argomento della funzione.
Ne consegue che:
1. un operatore in overload pu essere implementato come metodo di una
classe solo se il primo operando un oggetto della stessa classe; in
caso contrario deve essere una funzione esterna (dichiarata friend se
accede a membri privati) ;
2. nella definizione del metodo il numero degli argomenti deve essere
ridotto di un'unit rispetto al numero di operandi; in pratica, se
l'operatore binario, ci deve essere un solo argomento (quello
corrispondente al secondo operando), se l'operatore unario, la
funzione non deve avere argomenti.
3. se il risultato dell'operazione l'oggetto stesso l'istruzione di ritorno deve
essere:
return *this;
Vediamo ora, a titolo di esempio, una possibile implementazione di overload
dell'operatore in notazione compatta += della nostra classe point:

operazione :

p += p1 ;

definizione metodo :

point& point::operator+=(const point& p1)


{
x += p1.x ;
y += p1.y ;
return *this ;
}

Notare:
1. la funzione ha un un solo argomento, che corrisponde al secondo
operando p1, in quanto il primo operando p l'oggetto stesso,
trasmesso per mezzo del puntatore nascosto this;
2. la funzione un metodo della classe, e quindi i membri dell'oggetto
p sono indicati solo con il loro nome (il compilatore aggiunge this->
davanti a ognuno di essi);
3. nel codice della funzione l'operatore += "conosciuto", in quanto agisce
sui membri della classe, che sono di tipo double;
4. la funzione ritorna l'oggetto stesso p (deref. di this), by reference
(cio come l-value), modificato dall'operazione (non esistono problemi
di lifetime in questo caso, essendo l'oggetto p definito nel
chiamante);
5. la chiamata della funzione nella forma tradizionale sarebbe:
p.operator+=(p1) ;
tradotta dal compilatore in:
operator+=(&p,p1) ;
Adesso che abbiamo definito l'operatore += come metodo della classe,
l'implementazione dell'operatore +, che invece preferiamo sia una funzione
esterna, pu essere fatta in modo pi semplice (non occorre che sia dichiarata
friend in quanto non accede a membri privati):

operazione :

p = p1+p2 ;

funzione somma :

point operator+(const point& p1, const point& p2)


{
point ptemp = p1; (uso il costruttore di copia)
return ptemp += p2 ;
}

Overload degli operatori di flusso di I/O

Un caso particolare rappresenta l'overload dell'I/O, cio degli operatori di


flusso "<<" (inserimento) e ">>" (estrazione). Notiamo che questi sono gi
degli operatori in overload, in quanto il significato originario dei simboli << e
>> quello di operatori di scorrimento di bit (se gli operandi sono interi).
Se invece il left-operand non un intero, ma l'oggetto cout, abbiamo visto
che l'operatore << definisce un'operazione di output, che eseguita
"inserendo" in cout il dato da scrivere (costituito dal right-operand), il quale a
sua volta pu essere di qualunque tipo nativo o del corrispondente tipo
puntatore (quest'ultimo scritto come numero intero in forma esadecimale,
salvo il tipo char *, che interpretato come stringa).
Il nostro scopo ora quello di creare un ulteriore overload di <<, in modo che
anche un tipo astratto possa essere ammesso come right-operand; per
esempio potremmo volere che l'operazione:
cout << a;
(dove a un'istanza di una classe A)
generi su video una tabella dei valori assunti dai membri di a.
Per fare questo dobbiamo anzitutto sapere che cout, oggetto globale generato
all'inizio dell'esecuzione del programma, un'istanza della classe ostream, che
viene detta "classe di flusso di output" (e dichiarata in <iostream.h>).
Inoltre il primo argomento passato alla funzione dovr essere lo stesso
oggetto cout (in quanto il left-operand dell'operazione), mentre il secondo
argomento, corrispondente al right-operand, dovr essere l'oggetto a da
trasferire in output.
Infine la funzione dovr restituire by-reference lo stesso primo argomento
(cio sempre cout), per permettere l'associazione di ulteriori operazioni nella
stessa istruzione.
Pertanto la funzione per l'overload di << dovr essere cos definita:

ostream& operator<<(ostream& out, const A& a)


{
........ out << a.ma; (ma un membro di A di tipo nativo)
........ return out ;
}
Notare:
1. il primo argomento della funzione appartiene a ostream e non ad A, e
quindi la funzione non pu essere un metodo di A, ma deve essere
dichiarata come funzione friend nella definizione di A; viceversa, gli
overload dell'operatore << con tipi nativi (e loro puntatori) sono
definiti nella stessa classe ostream, e quindi sono metodi di quella
classe;
2. il valore di ritorno della funzione trasmesso by-reference, in quanto
deve essere un l-value di successive operazioni impilate;
3. poich nel chiamante il primo argomento l'oggetto cout, il ritorno
by-reference dello stesso oggetto non rischia mai di creare problemi di
lifetime;

4. per i motivi suddetti, e per l'associativit dell'operatore <<, che


procede da sinistra a destra, si possono impilare pi operazioni di
output in una stessa istruzione. Esempio:
cout << a1 << a2 << a3;
dove a1, a2 e a3 sono tutte istanze di A

Analogamente, si pu definire un overload dell'operatore di estrazione ">>"


per le operazioni di input (per esempio, cin >> a;), tramite la funzione:
istream& operator>>(istream& inp, A& a)
dove istream la classe di flusso di input (anch'essa dichiarata in
<iostream.h>), a cui appartiene l'oggetto globale cin. Notare che in questo
caso il secondo argomento (cio a), sempre passato by-reference, non
dichiarato const, in quanto l'operazione lo deve modificare.

Operatori binari e conversioni

Analogamente a quanto visto negli esempi finora riportati, si possono definire gli
overload dei seguenti operatori binari :

matematici (+ - * / %);
a livello del bit (<< >> & | ^)
in notazione compatta (+= -= *= / = %= <<= >> = &= | =
^=)
relazionali (== != < <= > >=);
logici (&& ||)
di serializzazione ( , )

e di altri che tratteremo separatamente (per una maggiore leggibilit del


programma, si consiglia, anche se non obbligatorio, che gli overload di questi
operatori mantengano comunque qualche "somiglianza" con il loro significato
originario).
Tutti gli operatori sopra riportati avranno ovviamente almeno un operando che
oggetto della classe, non importa se left o right (a parte gli operatori in
notazione compatta, per i quali l'oggetto della classe deve essere sempre
left). L'altro operando pu essere un altro oggetto della stessa classe (come
nell'esempio della somma che abbiamo visto prima), oppure un oggetto di
qualsiasi altro tipo, nativo o astratto. Pertanto possono esistere parecchi
overload dello stesso operatore, ciascuno con un operando di tipo diverso.
Non solo, ma se si vuole salvaguardare la propriet "commutativa" di certe
operazioni (+ * & | ^ == != && ||), o la "simmetria" di altre (< con
>= e > con <=), occorrono, per ognuna di esse, due funzioni, delle quali per
giunta una pu essere metodo e l'altra no.

Ne consegue che, se gli operatori da applicare in overload a una certa classe


non sono progettati attentamente, si rischia di generare una pletora di funzioni,
con varianti spesso molto piccole da una all'altra.
Il C++ offre una soluzione a questo problema, che molto semplice ed efficace:
il numero di funzioni pu essere minimizzato utilizzando i costruttori con un
argomento, che, come abbiamo visto, definiscono anche una conversione
implicita di tipo: se "attrezziamo" la classe con un insieme opportuno di
costruttori con un argomento, possiamo ottenere che tutti i tipi coinvolti nelle
operazioni siano convertiti implicitamente nel tipo della classe e che ogni
operazione sia perci implementata da una sola funzione, quella che opera su
due oggetti della stessa classe. Notare che la conversione implicita viene
eseguita indipendentemente dalla posizione dell'operando, e ci permette in
particolare che ogni operazione "commutativa" sia definibile con una sola
funzione.
Riprendendo la nostra classe point, vogliamo per esempio definire un operazione
di somma fra un vettore p, oggetto di point, e un valore s di tipo double
(detto: "scalare"), in modo tale che lo scalare venga sommato a ogni componente
del vettore. Se definiamo il costruttore:
point::point(double d) : x(d), y(d) { }
otterremo che entrambe le operazioni:
p+s
e
s+p
comportino la conversione implicita di s da tipo double a tipo point, e si
trasformino nell'unica operazione di somma fra due oggetti di point (della
quale abbiamo gi visto un esempio di implementazione).

Operatori unari e casting a tipo nativo

Si possono definire gli overload dei seguenti operatori unari :

incremento e decremento (suffisso) (++


incremento e decremento (prefisso) (++
segni algebrici (+ -)
complemento a 1 e NOT (~ !);
indirizzo e deref. (& *)
casting ( (tipo) )

- - );
- - );

Gli operatori unari devono avere come unico operando un oggetto della
classe in cui sono definiti e quindi possono convenientemente essere definiti
come metodi della stessa classe, nel qual caso le funzioni che li implementano
devono essere senza argomenti.
Tutti gli operatori sopra menzionati sono prefissi dell'operando, salvo gli
operatori di incremento e decremento che possono essere sia prefissi che
suffissi. Per distinguerli, applicata la seguente convenzione: se la funzione
senza argomenti, si tratta di un prefisso, se la funzione contiene un

argomento fittizio di tipo int (che il sistema non usa in quanto l'operatore
unario) si tratta di un suffisso. Inoltre, per i prefissi, il valore di ritorno deve
essere passato by reference, mentre per i suffissi deve essere passato by
value (questo perch i prefissi possono essere degli l-values mentre i suffissi
no). Infine, gli operatori suffissi devono essere progettati con particolare
attenzione, se si vuole conservare la loro propriet di eseguire un'operazione
"posticipata", nonostanza la precedenza alta. Per esempio, un operatore di
incremento suffisso di una generica classe A, potrebbe essere implementato
cos (supponiamo che il corrispondente operatore prefisso sia gi stato
definito):
A A::operator++(int)
{
A temp = *this;
++*this ;
return temp ;
}
come si pu notare, l'oggetto correttamente incrementato, ma al chiamante
non torna l'oggetto stesso, bens una sua copia precedente (temp); in questo
modo, non l'oggetto, ma la sua copia precedente ad essere utilizzata come
operando nelle eventuali successive operazioni dell'espressione di cui fa
parte; solo dopo che l'intera espressione stata eseguita, un nuovo accesso al
nome dell'oggetto ritrover l'oggetto incrementato.
Un caso a parte quello dell'operatore di casting. Come abbiamo visto, la
conversione di tipo pu essere eseguita usando un costruttore con un
argomento: questo consente conversioni, anche implicite, da tipi nativi a tipi
astratti (o fra tipi astratti), ma non pu essere utilizzato per conversioni da
tipi astratti a tipi nativi, in quanto i tipi nativi non hanno costruttori con
un argomento. A questo scopo occore invece definire esplicitamente un
overload dell'operatore di casting, che deve essere espresso nella seguente
forma (esempio di casting da una classe A a double):
A::operator double( )
notare che il tipo di ritorno non deve essere specificato in quanto il C++ lo
riconosce gi dal nome della funzione; notare anche che esiste uno spazio
(obbligatorio) fra le parole operator e double.
La conversione pu essere eseguita implicitamente o esplicitamente, in Cstyle o in function-style. Se eseguita implicitamente, pu verificarsi
un'ambiguit nel caso sia definita anche la conversione in senso inverso.
Esempio:
A a;
a+d;

double d ;
deve convertire un tipo A in double o un double in A ?

Nell'esempio sopra riportato si supposto che:

1. la classe A abbia un metodo che definisce un overload dell'operatore


di casting da A a double;
2. la classe A abbia un costruttore con un argomento double;
3. esista una funzione esterna che definisce un overload dell'operatore di
somma fra due oggetti di A.
in queste condizioni il compilatore segnala un errore di ambiguit, perch non sa
quale delle due conversioni implicite selezionare. In ogni caso, quando si tratta
di operatori in overload, il C++ non fa preferenza fra i metodi della classe e
le altre funzioni .

Operatori in namespace

Abbiamo visto che, per una migliore organizzazione degli operatori in overload
di una classe, preferibile utilizzare in maggioranza funzioni non metodi (se si
tratta di operatori binari), che si appoggino a un insieme limitato di metodi
della classe. Non ci siamo mai chiesti, per, in quale ambito sia conveniente che
tali funzioni vengano definite e, per semplicit, negli esempi (ed esercizi) finora
riportati abbiamo sempre definito le funzioni nel namespace globale.
Questo non , tuttavia, il modo pi corretto di procedere. Come abbiamo detto pi
volte, un affollamento eccessivo del namespace globale pu essere fonte di
confusione e di errori, specialmente in programmi di grosse dimensioni e con
diversi programmatori che lavorano ad un unico progetto.
E' pertanto preferibile "racchiudere" la classe e le funzioni esterne che
implementano gli operatori della classe in un namespace definito con un
nome. In questo modo non si "inquina" il namespace globale e, nel contempo,
si pu mantenere la notazione simbolica nella chiamata delle operazioni.
Infatti, a differenza dai metodi statici, che devono essere sempre qualificati
con il nome della classe, una funzione appartenente a un namespace non ha
bisogno di essere qualificata con il nome del namespace, se appartiene allo
stesso namespace almeno uno dei suoi argomenti.
In generale, data una generica operazione (usiamo l'operatore @, che in realt
non esiste, proprio per indicare un'operazione qualsiasi):
a@
b

(dove a un'istanza di una classe A e b un' istanza di una


classe B)

il compilatore esegue la ricerca della funzione operator@ nel seguente modo:

cerca operator@ come metodo della classe A;


cerca una definizione di operator@ nell'ambito della chiamata (o in
ambiti superiori, fino al namespace globale);
se la classe A definita in un namespace M, cerca una definizione di
operator@ in M;

se la classe B definita in un namespace N, cerca una definizione di


operator@ in N

Non sono fissati criteri di preferenza: se sono trovate pi definizioni di


operator@, il compilatore, se pu, sceglie la "migliore" (per esempio, quella in
cui i tipi degli operandi corrispondono esattamente, rispetto ad altre in cui la
corrispondenza ottenuta dopo una conversione implicita), altrimenti segnala
l'ambiguit.
Nel caso che operator@ sia trovata nel namespace in cui definita una delle
due classi, la funzione deve essere comunque dichiarata friend in entrambe
le classi (se in entrambe accede a membri privati); ci potrebbe far sorgere un
problema di dipendenza circolare, problema che peraltro si risolve mediante
dichiarazione anticipata di una delle classi (per fortuna un namespace si
pu spezzare in pi parti!)

Oggetti-array e array associativi

Tratteremo ora di alcuni overload di operatori binari, da implementare obbligatoriamente


come metodi, in quanto il loro primo operando oggetto della classe e l-value
modificabile. Fermo restando il fatto che la ridefinizione del significato di un operatore in
overload assolutamente libera, questi operatori vengono comunemente ridefiniti con
significati specifici.
Oggetti-array
Il primo overload che esaminiamo quello dell'operatore indice [], che
potrebbe servire, per esempio, se un membro della classe un array. In tal
caso, rinunciando, per non avere ambiguit, a trattare array di oggetti, ma solo
il membro array di ogni oggetto, l'overload dell'operatore indice potrebbe
essere definito come nel seguente esempio:
data una classe A :
class A { int m[10] ; ........ } ;
e una sua istanza a, vogliamo che l'operazione: a[i] non indichi l'oggetto di
indice i di un array di oggetti a (come sarebbe senza overload di []), ma
l'elemento di indice i del membro-array m dell'oggetto a. Per ottenere
questo, basta definire in A il seguente metodo:
int& A::operator[] (const int& i) { return m[i]; }
da notare che il valore di ritorno un riferimento, e questo fa s che
l'operatore [] funzioni come un l-value, rendendo possibili, non solo
operazioni di estrazione, come:
num = a[i];
ma anche operazioni di inserimento, come:
a[i] = num;

Gli oggetti costituiti da un solo membro-array (o in cui il membro-array


predominante) sono talvolta detti: oggetti-array. Rispetto ai normali array,
presentano il vantaggio di poter disporre delle funzionalit in pi offerte dalla
classe di appartenenza; per esempio possono controllare il valore dell'indice,
sollevando eccezione in caso di overflow, oppure modificare la dimensione
dell'array (se il membro-array dichiarato come puntatore) ecc...
Array associativi
L' operatore indice ha un campo di applicazione molto pi vasto e generalizzato
di un normale array. Infatti non esiste nessuna regola che obblighi il secondo
operando a essere un intero, come l'indice di un array; al contrario, lo si pu
definire di un qualsiasi tipo, anche astratto, e ci permette di stabilire una
corrispondenza (o, come talvolta si dice, un'associazione) fra oggetti di due
classi. Un array associativo, spesso chiamato mappa o anche dizionario,
memorizza coppie di valori: dato un valore, la chiave, si pu accedere all'altro, il
valore mappato. La funzione che implementa l'overload dell' operatore
indice fornisce l'algoritmo di mappatura, che associa un oggetto della
classe (primo operando) a ogni valore della chiave (secondo operando).

Oggetti-funzione

Anche l'operatore di chiamata di una funzione pu essere ridefinito. In


questo caso il primo operando deve essere un oggetto della classe (nascosto
da this) e il secondo operando una lista di espressioni, che viene valutata
e trattata secondo le normali regole di passaggio degli argomenti di una
funzione. Il metodo che implementa l'overload di questo operatore deve
essere definito nel seguente modo (supponiamo che il nome della classe sia
A):
tipo del valore di ritorno A::operator() (lista di argomenti) { ........ }
L'uso pi frequente dell'operatore () si ha quando si vuole fornire la normale
sintassi della chiamata di una funzione a oggetti che in qualche modo si
comportano come funzioni (cio che utilizzano in modo predominante un loro
metodo). Tali oggetti sono spesso chiamati oggetti-funzione. Rispetto a una
normale funzione, un oggetto-funzione ha il vantaggio di potersi "appoggiare"
a una classe, e quindi di utilizzare le informazioni gi memorizzate nei suoi
membri, senza bisogno di dover trasmettere ogni volta queste informazioni come
argomenti aggiuntivi nella chiamata.

Puntatori intelligenti

Abbiamo detto all'inizio che non tutti gli operatori possono essere ridefiniti in
overload e in particolare non ammesso ridefinire quegli operatori i cui
operandi sono nomi non "parametrizzabili"; citiamo, a questo proposito,
l'operatore di risoluzione di visibilit (::), in cui il left-operand il nome di
una classe o di un namespace, e gli operatori di selezione di un membro (.
e ->), in cui il right-operand il nome di un membro di una classe.
A questa regola fa eccezione l'operatore ->, che pu essere ridefinito; ma,
proprio perch il suo right-operand non pu essere trasmesso come
argomento di una funzione, l'operatore -> in overload "declassato" da
operatore binario a operatore unario suffisso e mantiene, come unico
operando, il suo originario left-operand, cio l'indirizzo di un oggetto. La
funzione che implementa questo (strano) overload deve essere un metodo di
una classe, dal che si deduce che gli oggetti di tale classe possono essere usati
come puntatori per accedere ai membri di un'altra classe. Per esempio, data
una classe Ptr_to_A:
class Ptr_to_A { ........ public: A* operator->( ); ........ } ;
le sue istanze possono essere utilizzate per accedere a istanze della classe A,
in una maniera molto simile a quella in cui sono utilizzati i normali puntatori.
Se il metodo viene chiamato come una normale funzione, il suo valore di
ritorno pu essere usato come puntatore ad un oggetto di A; se invece si
adotta la notazione simbolica dell'operazione, le regole di sintassi pretendono
che il nome di un membro di A venga comunque aggiunto. Per chiarire,
continuiamo nell'esempio precedente:

Ptr_to_A p ;
A* pa = p.operator->( );

OK

A* pa = p->;

errore di sintassi

int num = p->ma;

OK (ma un membro di A di tipo int)

p->ma = 7 ;

OK (pu anche essere un l-value)

L'overload di -> utile principalmente per creare puntatori "intelligenti", cio


oggetti che si comportano come puntatori, ma con il vantaggio di poter
disporre delle funzionalit in pi offerte dalla classe di appartenenza
(esattamente come gli oggetti-array e gli oggetti-funzione).
C' da sottolineare infine che, come di regola, la definizione dell' overload di ->
non implica che siano automaticamente definite le operazioni equivalenti. Infatti,
mentre per i normali puntatori valgono le seguenti uguaglianze:
p->ma = = (*p).ma = = p[0].ma
le stesse continuano a valere per gli operatori in overload solo se tutti gli
operatori sono definiti in modo tale da produrre volutamente tale risultato.

Operatore di assegnazione

Abbiamo lasciato per ultimo di questo gruppo l'overload dell'operatore di


assegnazione (=), non perch fosse il meno importante (anzi ...), ma
semplicemente perch, negli esempi (e negli esercizi) finora riportati, non ne
abbiamo avuto bisogno. Infatti, come gi per il costruttore senza argomenti e
per il costruttore di copia, il C++ fornisce un operatore di assegnazione di
default, che copia membro a membro l'oggetto right-operand nell'oggetto
left-operand.
Nota In alcune circostanze si potrebbe non desiderare che un oggetto venga
costruito per copia o assegnato. Ma, se non si definiscono overload, il C++
inserir quelli di default, e se invece li si definiscono, il programma li user
direttamente. Come fare allora? La soluzione semplice: definire degli overload
fittizi e collocarli nella sezione privata della classe; in questo modo gli overload
ridefiniti "nasconderanno" quelli di default, ma a loro volta saranno inaccessibili
in quanto metodi non pubblici.
L'assegnazione mediante copia membro a membro pu essere esattamente
ci che si vuole nella maggioranza dei casi, e quindi non ha senso ridefinire
l'operatore. Ma, se la classe possiede membri puntatori, la semplice copia di
un puntatore pu generare due problemi:

dopo la copia, l'area precedentemente puntata dal membro puntatore


del left-operand resta ancora, cio occupa spazio, ma non pi
accessibile (errore di memory leak);
il fatto che due oggetti puntino alla stessa area pericoloso, perch, se
viene chiamato il distruttore di uno dei due oggetti, il membro
puntatore dell'altro, che esiste ancora, punta a un'area che non esiste pi
(errore di dangling references).

Come si pu notare, il secondo problema identico a quello che si presenterebbe


usando il costruttore di copia di default, mentre il primo specifico
dell'operatore di assegnazione (in quanto la copia viene eseguita su un
oggetto gi esistente).
Anche in questo caso, perci necessario che l'operatore di assegnazione
esegua la copia, non del puntatore, ma dell'area puntata. Per evidenziare
analogie e differenze, riprendiamo l'esempio del costruttore di copia del capitolo
precedente (complicandolo un po', cio supponendo che l'area puntata sia un
array con dimensioni definite in un ulteriore membro della classe), e gli
affianchiamo un esempio di corretto metodo di implementazione dell'operatore
di assegnazione:
COSTRUTTORE DI
COPIA

OPERATORE DI
ASSEGNAZIONE

operazioni :

CLASSE

A a1 ; ........ A a2 = a1 ; A a1 , a2 ; ........ a2 = a1 ;
A::A(const A& a)

A& A::operator=(const A& a)

dim = a.dim ;

if (this == &a) return *this;

int* pa;

pa = new int [dim] ;

if (dim != a.dim)

int dim;

for(int i=0; i < dim;


i++)
*(pa+i) = *(a.pa+i) ;

class A {

public:
A( );

delete [] pa;

A(const A&);

dim = a.dim ;

A& operator=
(const A&);

pa = new int [dim] ;


}

........ };

for(int i=0; i < dim; i++)


*(pa+i) = *(a.pa+i) ;
return *this;
}

Notare:
1. la prima istruzione: if (this == &a) return *this; serve a proteggersi
dalla cosidetta auto-assegnazione (a1 = a1); in questo caso la
funzione deve restituire l'oggetto stesso senza fare altro;
2. il metodo che implementa l'operatore di assegnazione un po' pi
complicato del costruttore di copia, in quanto deve deallocare (con
delete) l'area precedentemente puntata dal membro pa di a2 prima di
allocare (con new) la nuova area; tuttavia, se le aree puntate dai
membri pa di a2 e a1 sono di uguali dimensioni, non necessario
deallocare e riallocare, ma si pu semplicemente riutilizzare l'area gi
esistente di a2 per copiarvi i nuovi dati;
3. entrambi i metodi eseguono la copia (tramite un ciclo for) dell'area
puntata e non del puntatore, come avverrebbe se si lasciasse fare ai
metodi di default;
4. la classe dovr contenere altri metodi (o altri costruttori) che si
occupano dell'allocazione iniziale dell'area e dell'inserimento dei dati; per
semplicit li abbiamo omessi.

Ottimizzazione delle copie

Tanto per ribadire il vecchio detto che "non saggio chi non si contraddice mai",
ci contraddiciamo subito: a volte pu essere preferibile copiare i puntatori e
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


copiare il puntatore al posto dell'oggetto.
Supponiamo, per esempio, che un certo oggetto a1 sia di "grosse dimensioni" e
che, a un certo punto del programma, a1 debba essere assegnato a un altro
oggetto a2, oppure un altro oggetto a2 debba essere costruito e
inizializzato con a1. In entrambi i casi sappiamo che a1 viene copiato in a2.
Ma la copia di un "grosso" oggetto pu essere particolarmente onerosa, specie
se effettuata parecchie volte nel programma. Aggiungasi il fatto che spesso
vengono creati e immediatamente distrutti oggetti temporanei, che
moltiplicano il numero delle copie, come si evince dal seguente esempio:
a2 = f(a1);
in questa istruzione vengono eseguite ben 3 copie!
Ci chiediamo a questo punto: ma se, nel corso del programma, a1 e a2 non
vengono modificati, che senso ha eseguire materialmente la copia? Solo la
modifica di almeno uno dei due creerebbe di fatto due oggetti distinti, ma finch
ci non avviene, la duplicazione "prematura" sarebbe un'operazione inutilmente
costosa. In base a questo ragionamento, se si riuscisse a creare un meccanismo,
che, di fronte a una richiesta di copia, si limiti a "prenotarla", ma ne rimandi
l'esecuzione al momento dell'eventuale modifica di uno dei due oggetti (copy on
write), si otterrebbe lo scopo di ottimizzare il numero di copie, eliminando
tutte quelle che, alla fine, sarebbero risultate inutili.
Puntualmente, il C++ che mette a disposizione questo meccanismo. L'idea base
quella di "svuotare" la classe (che chiamiamo A) di tutti i suoi dati-membro,
lasciandovi solo i metodi (compresi gli eventuali metodi che implementano gli
operatori in overload) e al loro posto inserire un unico membro, puntatore a
un'altra classe (che chiamiamo Arep). Questa seconda classe, che viene
preferibilmente definita come struttura, detta "rappresentazione" della
classe A, e in essa vengono inseriti tutti i dati-membro che avrebbero dovuto
essere di A. In questa situazione, si dice che A implementata come handle
(aggancio) alla sua rappresentazione, ma la stessa rappresentazione (cio
la struttura Arep) che contiene realmente i dati.
Pi oggetti di A possono "condividere" la stessa rappresentazione (cio
puntare allo stesso di oggetto di Arep). Per tenere memoria di ci, Arep deve
contenere un ulteriore membro, di tipo int, in cui contare il numero di oggetti
di A agganciati; questo numero, inizializzato con 1, viene incrementato ogni
volta che "prenotata" una copia, e decrementato ogni volta che uno degli
oggetti di A agganciati subisce una modifica: nel primo caso, la copia viene
eseguita solo fra i membri puntatori dei due oggetti di A (in modo che puntino
allo stesso oggetto di Arep); nel secondo caso, uno speciale metodo di Arep fa
s che l'oggetto di Arep "si cloni", cio crei un nuovo oggetto copia di se stesso,
su questo esegua le modifiche richeste, e infine ne assegni l'indirizzo al membro
puntatore dell'oggetto di A da cui provenuta la richiesta di modifica.
Ovviamente spetta ai metodi di A individuare quali operazioni comportino la
modifica di un suo oggetto e attivare le azioni conseguenti che abbiamo
descritto. Per concludere, il distruttore di un oggetto di A deve decrementare
il contatore di agganci nel corrispondente oggetto di Arep, e poi procedere alla
distruzione di detto oggetto solo se il contatore diventato zero.

Da notare che una rappresentazione sempre creata nella memoria heap e


quindi non ha problemi di lifetime, anche se gli oggetti che l'agganciano sono
automatici: questo particolarmente utile, per esempio, nel passaggio by value
degli argomenti e del valore di ritorno fra chiamante e funzione (e
viceversa): la copia viene eseguita solo apparentemente, in quanto permane la
stessa unica rappresentazione, che sopravvive anche in ambiti di visibilit
diversi da quello in cui stata creata. Per esempio, tornando alla nostra
istruzione:
a2 = f(a1);
almeno 2 delle 3 copie previste non vengono eseguite, in quanto l'oggetto a2 si
aggancia direttamente alla rappresentazione creata dall'oggetto locale di f,
passato come valore di ritorno (prima copia "risparmiata") e successivamente
assegnato ad a2 (seconda copia "risparmiata"); per quello che riguarda la terza
copia (passaggio di a1 dal chiamante alla funzione), questa realmente
eseguita solo se il valore locale di a1 modificato in f, altrimenti entrambi gli
oggetti continuano a puntare alla stessa rappresentazione creata nel
chiamante, fino a quando f termina e quindi l'a1 locale "muore" senza che la
copia sia mai stata eseguita.
E' preferibile che Arep sia una struttura perch cos tutti i suoi membri sono
pubblici di default. D'altra parte una rappresentazione di una classe deve
essere accessibile solo dalla classe stessa. Pertanto Arep deve essere pubblica
per A e privata per il "mondo esterno". Per ottenere questo, bisogna definire
Arep "dentro" A (struttura-membro o struttura annidata), nella sua sezione
privata (in questo modo non pu essere istanziata se non da un metodo di A).
Pi elegantemente si pu inserire in A la semplice dichiarazione di Arep e
collocare esternamente la sua definizione; in questo caso, per, il suo nome
deve essere qualificato:
struct A::Arep { ........ };
Nell'esercizio che riportiamo come esempio tentiamo una "rudimentale"
implementazione di una classe "stringa", al solo scopo di fornire ulteriori
chiarimenti su quanto detto (l'esercizio eccezionalmente molto commentato).
Non va utilizzato nella pratica, in quanto la Libreria Standard fornisce una
classe per la gestione delle stringhe molto pi completa.
Nel prossimo esercizio consideriamo i tempi delle copie di oggetti del tipo
"stringa" implementato come nell'esercizio precedente (cio come handle a una
rappresentazione), e li confrontiamo con i tempi ottenuti copiando le
stringhe direttamente.

Espressioni-operazione

Quando si ha a che fare con espressioni che contengono varie operazioni,


sappiamo che ogni operazione crea un oggetto temporaneo, che usato
come operando per l'operazione successiva, secondo l'ordine fissato dai criteri

di precedenza e associativit fra gli operatori. Quando tutte le operazioni di


un'espressione sono state eseguite (cio, come si dice, l'espressione stata
valutata), tutti gli oggetti temporanei creati durante la valutazione
dell'espressione vengono distrutti. Pertanto ogni oggetto temporaneo vien
costruito, passato come operando, e alla fine, distrutto, senza svolgere altra
funzione.
Normalmente ogni operazione viene eseguita mediante chiamata della
funzione che implementa l'overload del corrispondente operatore: questa
funzione di solito costruisce un oggetto locale, che poi ritorna per copia al
chiamante (salvo i casi in cui l'oggetto del valore di ritorno coincida con uno
degli operandi, il passaggio non pu essere eseguito by reference, perch
l'oggetto locale passato non sopravvive alla funzione). E quindi, in ogni
operazione, viene non solo costruito ma anche copiato un oggetto
temporaneo!
Se gli oggetti coinvolti nelle operazioni sono di "grosse dimensioni" (e
soprattutto se le operazioni sono molte), il costo computazionale per la
costruzione e la copia degli oggetti temporanei potrebbe essere troppo
elevato, e quindi bisogna trovare il modo di ottimizzare le prestazioni del
programma minimizzando tale costo. In pratica bisogna ridurre al minimo:

il numero degli oggetti temporanei creati;


il numero di copie;
il numero di cicli di operazioni native in cui ogni operazione viene
tradotta.

La tecnica, anche in questo caso, consiste nella semplice "impostazione" di ogni


operazione (senza eseguirla), tramite un handle a una struttura, che funge
da "rappresentazione" dell'operazione stessa; solo alla fine, l'intera espressione
viene eseguita tutta in una volta, senza creazione di oggetti temporanei, con il
minimo numero possibile di cicli, e senza copie di passaggio. Questa tecnica
sostanzialmente tratta un'espressione come unica operazione, traducendo n
operatori binari in un solo operatore con n+1 operandi.
Supponiamo, per esempio, di avere la seguente espressione:
a = b *c + d ;
e supponiamo per semplicit (anche se non obbligatorio) che gli oggetti: a, b,
c e d appartengano tutti alla stessa classe A. Siamo in presenza di tre
operazioni binarie (che, nell'ordine di esecuzione sono: moltiplicazione,
somma e assegnazione), ma vogliamo, per l'occasione, trasformarle in un'unica
operazione "quaternaria" che esegua, in un sol colpo, l'intera espressione.
Per ottenere questo, procediamo nel seguente modo:
1. definiamo un overload della moltiplicazione fra due oggetti di A, che,
anzich eseguire l'operazione, si limita a istanziare una struttura di
appoggio (che chiamiamo M), la quale non fa altro che memorizzare i
riferimenti ai due operandi (in altre parole, il suo costruttore
inizializza due suoi membri, dichiarati come riferimenti ad A, come
alias di b e c); a sua volta, M contiene un metodo di casting ad A, che
esegue materialmente la moltiplicazione, ma che viene chiamato solo

se l'operazione rientra in un altro contesto (ricordiamo che, nella scelta


dell'overload pi appropriato, il compilatore cerca prima fra quelli in cui i
tipi degli operandi coincidono esattamente, e poi fra quelli in cui la
coincidenza si ha tramite una conversione di tipo);
2. definiamo un overload della somma fra un oggetto di M e un oggetto
di A, che, anche in questo caso, si limita a istanziare una struttura di
appoggio (che chiamiamo MS), la quale, esattamente come M, memorizza
i riferimenti ai due operandi e contiene un metodo di casting ad A;
3. infine, definiamo un overload del costruttore e dell'operatore di
assegnazione di A, entrambi con un oggetto di MS come argomento,
ed entrambi che chiamano un metodo privato di A, il quale proprio
quello deputato ad eseguire, in modo ottimizzato, l'intera operazione.

Eredita'
L'eredit in C++

L'eredit domina e governa tutti gli aspetti della vita. Non solo nel campo della
genetica, ma anche nello stesso pensiero umano, i concetti si aggregano e si
trasmettono secondo relazioni di tipo "genitore-figlio": ogni concetto complesso
non si crea ex-novo, ma deriva da concetti pi semplici, che vengono "ereditati"
e integrati con ulteriori approfondimenti. Per esempio, alle elementari si impara
l'aritmetica usando "mele e arance", alle medie si applicano le nozioni
dell'aritmetica per studiare l'algebra, al liceo si descrivono le formule chimiche
con espressioni algebriche; ma un professore di chimica non penserebbe mai di
insegnare la sua materia ripartendo dalle mele e dalle arance!
E quindi lo stesso processo conoscitivo che si sviluppa e si evolve attraverso
l'eredit. Eppure, esisteva, fino a pochi anni fa, un campo in cui questo principio
generale non veniva applicato: quello dello sviluppo del software (!), che, pur
utilizzando strumenti tecnologici "nuovi" e "avanzati", era in realt in "ritardo"
rispetto a tutti gli altri aspetti della vita: i programmatori continuavano a scrivere
programmi da zero, cio ripartivano proprio, ogni volta, dalle mele e dalle arance!
In realt le cose non stanno proprio cos: anche i linguaggi di programmazione
precedenti al C++ (compreso il C) applicano una "specie" di eredit nel
momento in cui mettono a disposizione le loro librerie di funzioni: un
programmatore pu utilizzarle se soddisfano esattamente le esigenze del suo
problema specifico; ma, quando ci non avviene (come spesso capita), non esiste
altro modo che ricopiare le funzioni e modificarle per adattarle alle proprie
esigenze; questa operazione comporta il rischio di introdurre errori, che a volte
sono ancora pi difficili da localizzare di quando si riscrive il programma da zero!
Il C++ consente invece di applicare lo stesso concetto di eredit che nella vita
reale: gli oggetti possono assumere, per eredit, le caratteristiche di altri
oggetti e aggiungere caratteristiche proprie, esattamente come avviene
nell'evoluzione del processo conoscitivo. Ed questa capacit di uniformarsi alla
vita reale che rende il C++ pi potente degli altri linguaggi: il C++ vanta
caratteristiche peculiari di estendibilit, riusabilit, modularit, e
manutenibilit, proprio grazie ai suoi meccanismi di uniformizzazione alla vita
reale, quali il data hiding, il polimorfismo, l'overload e, ora, l'eredit.

Classi base e derivata

In C++ con il termine "eredit" si intende quel meccanismo per cui si pu creare
una nuova classe, detta classe figlia o derivata, trasferendo in essa tutti i
membri di una classe esistente, detta classe genitrice o base.

La relazione di eredit si specifica nella definizione della classe derivata


(supponendo che la classe base sia gi stata definita), inserendo, dopo il nome
della classe e prima della parentesi graffa di apertura, il simbolo ":" seguito dal
nome della classe base, come nel seguente esempio:
class B : A { ........ } ;
questa scrittura significa che la nuova classe B possiede, oltre ai membri
elencati nella propria definizione, anche quelli ereditati dalla classe esistente
A.
L'eredit procede con struttura gerarchica, o ad albero (come le
subdirectories nell'organizzazione dei files) e quindi una stessa classe pu
essere derivata da una classe base e contemporaneamente genitrice di una o
pi classi figlie. Quando ogni classe figlia ha una sola genitrice si dice che
l'eredit "singola", come nel seguente grafico:

Se una classe figlia ha pi classi genitrici, si dice che l'eredit "multipla",


come nel seguente grafico, dove la classe AB figlia delle classi A3 e B4, e la
classe B23 figlia delle classi B2 e B3:

Nella definizione di una classe derivata per eredit multipla, le due classi
genitrici vanno indicate entrambe, separate da una virgola:
class AB : A3, B4 { ........ } ;

Accesso ai membri della classe base

Introducendo le classi, abbiamo illustrato il significato degli specificatori di


accesso private: e public:, e abbiamo soltanto accennato all'esistenza di un
terzo specificatore: protected:. Ora, in relazione all'eredit, siamo in grado di
descrivere completamente i tre specificatori:

private: (default) indica che tutti i membri seguenti sono privati, e non
possono essere ereditati;
public: indica che tutti i membri seguenti sono pubblici, e possono
essere ereditati;
protected: indica che tutti i membri seguenti sono protetti, nel senso
che sono privati, ma possono essere ereditati;

Quindi, un membro protetto inaccesibile dall'esterno, come i membri


privati, ma pu essere ereditato, come i membri pubblici.
In realt, esiste un'ulteriore restrizione, che ha lo scopo di rendere il data-hiding
ancora pi profondo: l'accessibilit dei membri ereditati da una classe base
dipende anche dallo "specificatore di accesso alla classe base", che deve
essere indicato come nel seguente esempio:
class B : spec.di accesso A { ........ } ;
dove spec.di accesso pu essere: private (default), protected o public
(notare l'assenza dei due punti). Ogni membro ereditato avr l'accesso pi
"restrittivo" fra il proprio originario e quello indicato dallo specificatore di
accesso alla classe base, come chiarito dalla seguente tabella:
Specificatori di accesso alla classe base
Accesso dei membri
nella classe base

private

protected

public

private:

inaccessibili

inaccessibili

inaccessibili

protected:

privati

protetti

protetti

public:

privati

protetti

pubblici

Accessibilit dei membri ereditati

e quindi un membro ereditato pubblico solo se public: nella classe base


e l'accesso della classe derivata alla classe base public.

Se una classe derivata a sua volta genitrice di una nuova classe, in


quest'ultima l'accesso ai membri ereditati governato dalle stesse regole, che
vengono per applicate esclusivamente ai membri della classe "intermedia",
indipendentemente da come questi erano nella classe base. In altre parole, ogni
classe "vede" la sua diretta genitrice, e non si preoccupa degli altri eventuali
"ascendenti".
Normalmente l'accesso alla classe base public. In alcune circostanze,
tuttavia, si pu volere che i suoi membri pubblici e protetti, ereditati nella
classe derivata, siano accessibili unicamente da funzioni membro e friend
della classe derivata stessa: in questo caso, occorre che lo specificatore di
accesso alla classe base sia private; analogamente, se si vuole che i membri
pubblici e protetti di una classe base siano accessibili unicamente da funzioni
membro e friend della classe derivata e di altre eventuali classi derivate da
questa, occorre che lo specificatore di accesso alla classe base sia
protected.

Conversioni fra classi base e derivata

Si dice che l'eredit una relazione di tipo "is a" (un cane un mammifero, con
caratteristiche in pi che lo specializzano). Quindi, se due classi, A e B, sono
rispettivamente base e derivata, gli oggetti di B sono (anche) oggetti di A, ma
non viceversa.
Ne consegue che le conversioni implicite di tipo da B ad A (cio da classe
derivata a classe base) sono sempre ammesse (con il mantenimento dei soli i
membri comuni), e in particolare ogni puntatore (o riferimento) ad A pu
essere assegnato o inizializzato con l'indirizzo (o il nome) di un oggetto di
B. Questo permette, quando si ha a che fare con una gerarchia di classi, di
definire all'inizio un puntatore generico alla classe base "capostipite", e di
assegnargli in seguito (in base al flusso del programma) l'indirizzo di un
oggetto appartenente a una qualunque classe della gerarchia. Ci
particolarmente efficace quando si utilizzano le "funzioni virtuali", di cui
parleremo nel prossimo capitolo.
La conversione opposta, da A a B, non ammessa (a meno che B non abbia un
costruttore con un argomento, di tipo A); fra puntatori (o fra riferimenti)
la conversione ammessa solo se esplicita, tramite casting. Non comunque
un'operazione che abbia molto senso, tantopi che possono insorgere errori che
sfuggono al controllo del compilatore. Per esempio, supponiamo che mb sia un
membro di B (e non di A):
A a;
B& b = (B&)a; b un alias di a, convertito a tipo B& - il compilatore lo accetta
b.mb = .......

per il compilatore va bene (mb membro di B), ma in realt b un


alias di a e mb non membro di A - access violation ?

Tornando alle conversioni implicite da classe derivata a classe base, c' da


aggiungere che si tratta di conversioni di "grado" molto alto (altrimenti dette
"conversioni banali"), cio accettate da tutti i costrutti (come le conversioni da
variabile a costante). Per esempio, il costrutto catch con tipo di argomento
X "cattura" le eccezioni di tipo Y (con Y diverso da X), cio accetta conversioni
da Y a X, solo se:
1. X const Y (o viceversa, solo se l'argomento passato by value)
2. Y una classe derivata da X
mentre, per esempio, non accetta conversioni da int a long (o viceversa).

Costruzione della classe base

Una classe derivata non eredita i costruttori e il distruttore della sua classe
base. In altre parole ogni classe deve fornire i propri costruttori e il
distruttore (oppure utilizzare quelli di default). Quanto detto vale anche per
l'operatore di assegnazione, nel senso che, in sua assenza, la classe derivata
usa l'operatore di default anzich ereditare quello eventualmente presente nella
classe base.
Ogni volta che una classe derivata istanziata, entrano in azione
automaticamente i costruttori di tutte le classi gerarchicamente superiori,
secondo lo stesso ordine gerarchico (prima la classe base "capostipite", poi
tutte le altre, e per ultima la classe che deve creare l'oggetto). Analogamente,
quando l'oggetto "muore", entrano in azione automaticamente i distruttori delle
stesse classi, ma procedendo in ordine inverso (per primo il distruttore
dell'oggetto e per ultimo il distruttore della classe base "capostipite").
Per quello che riguarda i costruttori, il fatto che entrino in azione
automaticamente comporta il solito problema (vedere il capitolo sui Costruttori e
Distruttori degli oggetti), che insorge ogni volta che un oggetto non
costruito con una chiamata esplicita: se eseguito il costruttore di default,
tutto bene, ma come fare se si vuole (o si deve) eseguire un costruttore con
argomenti?
Abbiamo visto che questo problema ha una soluzione diversa per ogni circostanza:
in pratica ci deve sempre essere "qualcun altro" che si occupi di chiamare il
costruttore e fornigli i valori degli argomenti richiesti. Nel caso delle classi
ereditate il "qualcun altro" rappresentato dai costruttori delle classi
derivate, ciascuno dei quali deve provvedere ad attivare il costruttore della
propria diretta genitrice (non preoccupandosi invece delle eventuali altre classi
gerarchicamente superiori). Come gi abbiamo visto nel caso di una classe
composta, il cui costruttore deve includere le chiamate dei costruttori dei
membri-oggetto nella propria lista di inizializzazione, cos vale anche per le
classi ereditate: ogni costruttore di una classe derivata deve includere nella

lista di inizializzazione la chiamata del costruttore della propria genitrice.


Questa operazione si chiama: costruzione della classe base.
Per chiarire quanto detto, consideriamo per esempio una classe A che disponga
di un costruttore con due argomenti:
class A {
protected:
float m1;
int m2;

DEFINIZIONE DEL COSTRUTTORE DI A


A::A(int p, float q) : m1(q), m2(p)
{ .... eventuali altre operazioni del
costruttore di A }

public: A(int,float);
.... altri membri .... };
Vediamo ora come si deve comportare il costruttore di una classe B, derivata
di A:
class B : public A {
int n;
public: B(int,int,float);
.... altri membri .... };

DEFINIZIONE DEL COSTRUTTORE DI B


B::B(int a, int b, float c) : n(b), A(a,c)
{ .... eventuali altre operazioni del
costruttore di B }

Come si pu notare, il costruttore di B deve inserire la chiamata di quello di A


nella propria lista di inizializzazione (se non lo fa, e il costruttore di A esiste,
cio non chiamato di default, il C++ d errore); ovviamente l'ordine
originario degli argomenti del costruttore di A va rigorosamente mantenuto.
Nel caso che B sia a sua volta genitrice di un'altra classe C, il costruttore di C
deve includere nella propria lista di inizializzazione il termine: B(a,b,c), cio la
chiamata del costruttore di B, ma non il termine A(a,c), chiamata del
costruttore di A.
Il costruttore di una classe derivata non pu inizializzare direttamente i
membri ereditati dalla classe base: rifacendoci all'esempio, il costruttore di
B non pu inizializzare i membri m1 e m2 ereditati da A, ma lo pu fare solo
indirettamente, invocando il costruttore di A.
Notiamo infine che il costruttore di A dichiarato public: ci significa che la
classe A pu essere anche istanziata indipendentemente. Se per fosse
dichiarato protected, il costruttore di B lo "vedrebbe" ancora e quindi
potrebbe invocarlo ugualmente nella propria lista di inizializzazione, ma gli
utenti esterni non potrebbero accedervi. Un modo per occultare una classe
base (rendendola disponibile solo per le sue classi derivate) pertanto quello
di dichiarare tutti i suoi costruttori nella sezione protetta.

Regola della dominanza

Finora, negli esempi abbiamo attribuito sempre (e deliberatamente) nomi diversi


ai membri delle classi. Ci chiediamo adesso: cosa succede nel caso che esista un
membro della classe derivata con lo stesso nome di un membro della sua
classe base? Pu insorgere un conflitto fra i nomi, oppure (nel caso che il
membro sia un metodo) si applicano le regole dell'overload? La risposta ad
entrambe le domande : NO. In realt si applica una regola diversa, detta regola
della "dominanza": viene sempre scelto il membro che appartiene alla stessa
classe a cui appartiene l'oggetto.
Per esempio, se due classi, A e B, sono rispettivamente base e derivata e
possiedono entrambe un membro di nome mem, l'operazione:
ogg.mem
seleziona il membro mem di A se ogg istanza di A, oppure il membro mem
di B se ogg istanza di B.
Volendo invece selezionare forzatamente uno dei due, bisogna qualificare il
nome del membro comune mediante il solito operatore di risoluzione della
visibilit. Per esempio:
ogg.A::mem
seleziona sempre il membro mem di A, anche se ogg istanza di B.
La regola della dominanza pu essere sfruttata per modificare i membri
ereditati (soprattutto per quello che riguarda i metodi): l'unico sistema quello
di ridichiararli con lo stesso nome, garantendosi cos che saranno i nuovi
membri, e non gli originari, ad essere utilizzati in tutti gli oggetti della classe
derivata. Non comunque possibile diminuire il numero dei membri ereditati:
le funzioni "indesiderate" potrebbero essere ridefinite con "corpo nullo", ma non
si pu fare di pi.

Eredit e overload

Se vi sono due metodi con lo stesso nome, uno della classe base e l'altro della
classe derivata, abbiamo visto che vale la regola della dominanza e non quella
dell'overload. Ci vero anche se le due funzioni hanno tipi di argomenti
diversi e, in base all'overload, verrebbe selezionata la funzione che appartiene
alla classe a cui non appartiene l'oggetto.
Per fare un esempio (riprendendo quello precedente), supponiamo che ogg sia
un'istanza della classe derivata B, e che entrambe le classi possiedano un
metodo, di nome fun, con un argomento di tipo double nella classe A e di
tipo int nella classe B:
A::fun(double)
B::fun(int)
in esecuzione, la chiamata: ogg.fun(10.7)

non considera l'overload e seleziona comunque la fun di B con argomento int,


operando una conversione implicita da 10.7 a 10
Questo comportamento deriva in realt da una regola pi generale: l'overload
non si applica mai fra funzioni che appartengono a due diversi ambiti di
visibilit, anche se i due ambiti corrispondono a una classe base e alla sua
classe derivata e quindi la funzione della classe base accessibile nella
classe derivata per eredit.

La dichiarazione using

Abbiamo gi incontrato l'istruzione di "using-declaration", parlando dei


namespace, e sappiamo che serve a rendere accessibile un membro di un
namespace nello stesso ambito in cui inserita l'istruzione stessa.
Analogamente, una using-declaration si pu inserire nella definizione di una
classe derivata per trasferire nel suo ambito un membro della classe base.
Riprendendo il solito esempio, supponiamo ora di inserire nella definizione di B
l'istruzione:
using A::fun;
(notare che il nome fun appare da solo, senza argomenti e senza parentesi).
Adesso s che entrambe le funzioni sono nello stesso ambito di visibilit e
quindi si pu applicare l'overload. Pertanto la chiamata:
ogg.fun(10.7)
selezioner correttamente la funzione con argomento double, cio la fun di A.
Una using-declaration, se non si riferisce a un namespace, pu essere
inserita esclusivamente nella definizione di una classe derivata e pu riferirsi
esclusivamente a un membro della classe base. Non sono ammessi altri usi.
Una using-directive pu essere usata solo con i namespace.
Una using-declaration, inserita nella definizione di una classe derivata, pu
avere un altro effetto, oltre a quello di rendere possibile l'overload: permette di
modificare l'accesso ai membri della classe base. Infatti, se un membro della
classe base protetto (non se privato), oppure se lo specificatore di
accesso alla classe base protected o private, e la using-declaration
inserita nella sezione pubblica della classe derivata, quel membro diventa
pubblico. Questo fatto pu essere utilizzato per specificare interfacce che
mettono a disposizione degli utenti parti selezionate di una classe.

Eredit multipla e classi basi virtuali

Supponiamo che una certa classe C derivi, per eredit multipla, da due classi
genitrici B1 e B2. Nella definizione di C, il nome di ognuna delle due classi
base deve essere preceduto dal rispettivo specificatore di accesso (se non
private, che, ricordiamo, lo specificatore di default). Per esempio:
class C : protected B1, public B2 { ........ } ;
in questo caso, nella classe C, i membri ereditati da B1 sono tutti protetti,
mentre quelli ereditati da B2 rimangono come erano nella classe base
(protetti o pubblici).
Il costruttore di C deve costruire entrambe le classi genitrici, cio deve
includere, nella propria lista di inizializzazione, entrambe le chiamate dei
costruttori di B1 e di B2, o meglio, deve includere quei costruttori di B1 o di
B2 che non sono di default, considerati indipendentemente (e quindi, a secondo
delle circostanze, deve includerli entrambi, o uno solo, o nessuno). Anche nel caso
che la classe C non abbia costruttori, obbligatorio definire esplicitamente il
costruttore di default di C (anche con "corpo nullo"), con il solo compito di
costruire le classi genitrici (questa operazione non richiesta solo se anche le
classi genitrici sono entrambe istanziate mediante i loro rispettivi costruttori
di default).
Supponiamo ora che le classi B1 e B2 derivino a loro volta da un'unica classe
base A. Siccome ogni classe derivata si deve occupare solo della sua diretta
genitrice, il compito di costruire la classe A delegato sia a B1 che a B2, ma
non a C. Per cui, quando viene istanziata C, sono costruite direttamente
soltanto le sue dirette genitrici B1 e B2, ma ciascuna di queste costruisce a
sua volta (e separatamente) A; in altre parole, ogni volta che istanziata C, la
sua classe "nonna" A viene costruita due volte (classi base "replicate"),
come illustrato dalla seguente figura:

La replicazione di una classe base pu causare due generi di problemi:

occupazione doppia di memoria, che pu essere poco "piacevole",


soprattutto se gli oggetti di C sono molti e il sizeof(A) grande;
errore di ambiguit: se gli oggetti di C non accedono mai direttamente ai
membri ereditati da A, tutto bene; ma, se dovesse capitare il contrario, il
compilatore darebbe errore, non sapendo se accedere ai membri
ereditati tramite B1 o tramite B2.

Il secondo problema pu essere risolto (in un modo per poco "brillante")


qualificando ogni volta i membri ereditati da A. Per esempio, se ogg
un'istanza di C e ma un membro ereditato da A:
ogg.B1::ma

indica che ma ereditato tramite B1

ogg.B2::ma

indica che ma ereditato tramite B2

Entrambi i problemi, invece, si possono risolvere definendo A come classe base


"virtuale": questo si ottiene inserendo, nelle definizioni di tutte le classi
derivate, la parola-chiave virtual accanto allo specificatore di accesso alla
classe base. Esempio:
class B1 : virtual protected A { ........ } ;
class B2 : virtual public A { ........ } ;
La parola-chiave virtual non ha alcun effetto sulle istanze dirette di B1 e di
B2: ciascuna di esse costruisce la propria classe base normalmente, come se
virtual non fosse specificata. Ma, se viene istanziata la classe C, derivata da
B1 e da B2 per eredit multipla, viene creata una sola copia dei membri
ereditati da A, della cui inizializzazione deve essere lo stesso costruttore di
C ad occuparsene (contravvenendo alla regola generale che vuole che ogni figlia
si occupi solo delle sue immediate genitrici); in altre parole, nella lista di
inizializzazione del costruttore di C devono essere incluse le chiamate, non
solo dei costruttori di B1 e di B2, ma anche del costruttore di A. In sostanza
la parola-chiave virtual dice a B1 e B2 di non prendersi cura di A quando
viene creato un oggetto di C, perch sar la stessa classe "nipote" C ad
occuparsi della sua "nonna".
Pertanto, se una classe base definita virtuale da tutte le sue classi
derivate, viene evitata la replicazione e si realizza la cosidetta eredit a
diamante, rappresentata dal seguente grafico:

Sulla reale efficacia dell'eredit multipla esistono a tutt'oggi pareri discordanti:


qualcuno sostiene che bisognerebbe usarla il meno possibile, perch raramente
pu essere utile ed meno sicura e pi restrittiva dell'eredit singola (per
esempio non si pu convertire un puntatore da classe base virtuale a classe
derivata); altri ritengono al contrario che l'eredit multipla possa essere
necessaria per la risoluzione di molti problemi progettuali, fornendo la possibilit
di associare due classi altrimenti non correlate come parti dell'implementazione di
una terza classe. Questo fatto evidente in modo particolare quando le due
classi giocano ruoli logicamente distinti, come vedremo in un esempio riportato
nel prossimo capitolo, a proposito delle classi base astratte.

Polimorfismo
Late binding e polimorfismo

Abbiamo gi sentito parlare di late binding trattando dei puntatori a funzione:


l'aggancio fra il programma chiamante e la funzione chiamata ritardato
dal momento dalla compilazione a quello dell'esecuzione, perch solo in quella
fase il C++ pu conoscere la funzione selezionata, in base ai dati che
condizionano il flusso del programma. La scelta, tuttavia, avviene all'interno di un
insieme ben definito di funzioni, diverse l'una dall'altra non solo nel contenuto
ma anche nel nome.
Conosciamo anche il significato di polimorfismo: funzioni-membro con lo
stesso nome e gli stessi argomenti, ma appartenenti a oggetti di classi
diverse. Nella terminologia del C++, polimorfismo significa: mandare agli
oggetti lo stesso messaggio ed ottenere da essi comportamenti diversi, sul
modello della vita reale, in cui termini simili determinano azioni diverse, in base al
contesto in cui vengono utilizzati.
Tuttavia il polimorfismo che abbiamo esaminato finora solo apparente: il
puntatore "nascosto" this, introdotto dal compilatore, differenzia gli
argomenti delle funzioni, e quindi non si tratta realmente di polimorfismo, ma
soltanto di overload, cio di un meccanismo che, come sappiamo, permette al
C++ di riconoscere e selezionare la funzione gi in fase di compilazione
(early binding).
Il "vero" polimorfismo, nella pienezza del suo significato "filosofico", deve essere
associato al late binding: la differenziazione di comportamento degli oggetti in
risposta allo stesso messaggio non deve essere statica e predefinita, ma
dinamica, cio deve essere determinata dal contesto del programma in fase di
esecuzione. Vedremo che ci realizzabile solo nell'ambito di una stessa famiglia
di classi, e quindi il "vero" polimorfismo non pu prescindere dall'eredit e si
applica a funzioni-membro, con lo stesso nome e gli stessi argomenti, che
appartengono sia alla classe base che alle sue derivate.

Ambiguit dei puntatori alla classe base

Prendiamo il caso di due classi, di nome A e B, dove A la classe base e B una


sua derivata. Consideriamo due istanze, a e b, rispettivamente di A e di B.
Supponiamo inoltre che entrambe le classi contengano una funzione-membro,
di nome display(), non ereditata da A a B, ma ridefinita in B (traducendo
letteralmente il termine inglese "overridden", si suole dire, in questi casi, che la

funzione display() di A "scavalcata" nella classe B, ma un termine


"orrendo", che non useremo mai).
Sappiamo che, per la regola della dominanza, ogni volta il compilatore seleziona
la funzione che appartiene alla stessa classe a cui appartiene l'oggetto (cio la
classe indicata nell'istruzione di definizione dell'oggetto), e quindi:
a.display()

seleziona la funzione-membro di A

b.display()

seleziona la funzione-membro di B

Supponiamo ora di definire un puntatore ptr alla classe A e di inizializzarlo


con l'indirizzo dell'oggetto a:
A* ptr = &a;
anche in questo caso la funzione pu essere selezionata senza ambiguit e
quindi l'istruzione:
ptr->display()
accede alla funzione display() della classe A.
Abbiamo visto, tuttavia, che a un puntatore definito per una classe base,
possono essere assegnati indirizzi di oggetti di classi derivate, e quindi il
seguente codice valido:
if(.......) ptr = &a;
else ptr = &b;
in questo caso, dinanzi all'eventuale istruzione:
ptr->display()
come si regola il compilatore, visto che l'oggetto a cui punta ptr determinato in
fase di esecuzione? Di default, vale ancora la regola della dominanza e quindi,
essendo ptr definito come puntatore alla classe A, viene selezionata la
funzione display() della classe A, anche se in esecuzione l'oggetto puntato
dovesse appartenere alla classe B.

Funzioni virtuali

Negli esempi esaminati finora, la funzione-membro display() selezionata in


fase di compilazione (early binding); ci avviene anche nell'ultimo caso,
sebbene l'oggetto associato alla funzione sia determinato solo in fase di
esecuzione.
Se per, nella definizione della classe A, la funzione display() dichiarata
con lo specificatore "virtual", il C++ rinvia la scelta della funzione appropriata
alla fase di esecuzione (late binding). In questo modo si realizza il
polimorfismo: lo stesso messaggio (display), inviato a oggetti di classi
diverse, induce a diversi comportamenti, in funzione dei dati del programma.

Un tipo dotato di funzioni virtuali detto: tipo polimorfo. Per ottenere un


comportamento polimorfo in C++, bisogna esclusivamente operare all'interno di
una gerarchia di classi e alle seguenti condizioni:
1. la dichiarazione delle funzioni-membro della classe base (interessate
al polimorfismo) deve essere specificata con la parola-chiave virtual;
non obbligatorio (ma neppure vietato) ripetere la stessa parola-chiave
nelle dichiarazioni delle funzioni-membro delle classi derivate (di
solito lo si fa per migliorare la leggibilit del programma);
2. una funzione dichiarata virtual deve essere sempre anche definita
(senza virtual) nella classe base (al contrario delle normali funzioni che
possono essere dichiarate senza essere definite, quando non si usano);
invece, una classe derivata non ha l'obbligo di ridichiarare (e
ridefinire) tutte le funzioni virtuali della classe base, ma solo quelle
che le servono (quelle non ridefinite vengono ereditate);
3. gli oggetti devono essere manipolati soltanto attraverso puntatori (o
riferimenti); quando invece si accede a un oggetto direttamente, il suo
tipo gi noto al compilatore e quindi il polimorfismo in esecuzione
non si attua.
Si pu anche aggirare la virtualizzazione, qualificando il nome della
funzione con il solito operatore di risoluzione della visibilit. Esempio:
ptr->A::display();
in questo caso esegue la funzione della classe base A, anche se questa stata
dichiarata virtual e ptr punta a un oggetto di B.

Tabelle delle funzioni virtuali

Riprendiamo l'esempio precedente, aggiungendo una nuova classe derivata da


A, che chiamiamo C; questa classe non ridefinisce la funzione display() ma
la eredita da A (come appare nella seguente tabella, dove il termine fra parentesi
quadre facoltativo):
class A {

class B : public A {

class C : public A {

........ public: ......

......... public: ......

..............

virtual void display();

[virtual] void display();

};

};

};
Se ora assegniamo a ptr l'indirizzo di un oggetto che, in base al flusso dei
dati in esecuzione, pu essere indifferentemente di A, di B o di C, dinanzi a
istruzioni del tipo:

ptr->display()
il C++ seleziona in esecuzione la funzione giusta, cio quella di A se l'oggetto
appartiene ad A o a C, quella di B se l'oggetto appartiene a B.
Infatti il C++ prepara, in fase di compilazione, delle tabelle, dette "Tabelle
virtuali" o vtables, una per la classe base e una per ciascuna classe

derivata, in cui sistema gli indirizzi di tutte le funzioni dichiarate virtuali


nella classe base; aggiunge inoltre un nuovo membro in ogni classe, detto
vptr, che punta alla corrispondente vtable.

Il seguente diagramma chiarisce quanto detto, nel caso del nostro esempio:

In questo modo, in fase di esecuzione il C++ pu risalire, dall'indirizzo


contenuto nel membro vptr dell'oggetto puntato da ptr (vptr un datomembro e quindi realmente replicato in ogni oggetto), all'indirizzo della
corretta funzione da selezionare.

Costruttori e distruttori virtuali

I distruttori possono essere virtualizzati, anzi, in certe condizioni


praticamente indispensabile che lo siano, se si vuole assicurare una corretta
ripulitura della memoria. Infatti, proseguendo con il nostro esempio e supponendo
stavolta che gli oggetti siano allocati nell'area heap, l'istruzione:
delete ptr;
assicura che sia invocato il distruttore dell'oggetto realmente puntato da ptr
solo se il distruttore della classe base A stato dichiarato virtual; altrimenti
chiamerebbe comunque il distruttore di A, anche quando, in esecuzione,
stato assegnato a ptr l'indirizzo di un oggetto di una classe derivata.
Viceversa i costruttori non possono essere virtualizzati, per il semplice motivo
che, quando invocato un costruttore, l'oggetto non esiste ancora e quindi non
pu neppure esistere un puntatore con il suo indirizzo. In altre parole, la
nozione di "puntatore a costruttore" una contraddizione in termini.

Tuttavia possibile aggirare questo ostacolo virtualizzando, non il costruttore,


ma un altro metodo della classe, definito in modo che crei un nuovo oggetto
della stessa classe (si deve comunque partire da un oggetto gi esistente) e si
comporti quindi come un "costruttore polimorfo", in cui il tipo dell' oggetto
costruito determinato in fase di esecuzione.
Vediamo ora un'applicazione pratica di quanto detto. Riprendendo il nostro solito
esempio, supponiamo che la classe base A sia provvista di un metodo
pubblico cos definito:
A* A::clone( ) { return new A(*this); }
come si pu notare, la funzione-membro clone crea un nuovo oggetto
nell'area heap, invocando il costruttore di copia di A (oppure quello di default
se la classe ne sprovvista) con argomento *this, e ne restituisce l'indirizzo.
Ogni oggetto pu pertanto generare una copia di se stesso chiamando la
clone. Analogamente definiamo una funzione-membro clone della classe
derivata B:
A* B::clone( ) { return new B(*this); }
Se ora virtualizziamo la funzione clone, inserendo nella definizione della
classe base A la dichiarazione:
virtual A* clone();
troviamo in B la ridefinizione di una funzione virtuale, in quanto sono
coincidenti il nome (clone), la lista degli argomenti (void) e il tipo del valore
di ritorno (A*), e quindi possiamo ottenere da tale funzione un comportamento
polimorfo. In particolare l'istruzione:
A* pnew = ptr->clone();
crea un nuovo oggetto nell'area heap e inizializza pnew con l'indirizzo di tale
oggetto; il tipo di questo nuovo oggetto per deciso solo in fase di
esecuzione (comportamento polimorfo della funzione clone) e coincide con il
tipo puntato da ptr.

Scelta fra velocit e polimorfismo

Il processo early binding pi veloce del late binding, in quanto impegna il


C++ solo in compilazione e non crea nuove tabelle o nuovi puntatori; per
questo motivo la specifica virtual non di default. Tuttavia spesso utile
rinunciare a un po' di velocit in cambio di altri vantaggi, come il polimorfismo,
grazie al quale il C++ e non il programmatore a doversi preoccupare di
selezionare ogni volta il comportamento appropriato in risposta allo stesso
messaggio.

Classi astratte

Nel capitolo "Tipi definiti dall'utente" abbiamo ammesso di utilizzare una


nomenclatura "vecchia" identificando indiscriminatamente con il termine "tipo
astratto" qualunque tipo non nativo del linguaggio. E' giunto il momento di
precisare meglio cosa si intenda in C++ per "tipo astratto".
Una classe base, se definita con funzioni virtuali, "spiega" cosa sono in grado
di fare gli oggetti delle sue classi derivate. Nel nostro esempio, la classe base
A "spiega" che tutti gli oggetti del programma possono essere visualizzati,
ognuno attraverso la propria funzione display(). In sostanza la classe base
fornisce, oltre alle funzioni, anche uno "schema di comportamento" per le
classi derivate.
Estremizzando questo concetto, si pu creare una classe base con funzioni
virtuali senza codice, dette funzioni virtuali pure. Non avendo codice, queste
funzioni servono solo da "schema di comportamento" per le classi derivate
e vanno dichiarate nel seguente modo:
virtual void display() = 0;
(nota: questo l'unico caso in C++ di una dichiarazione con inizializzazione!)
in questo esempio, si definisce che ogni classe derivata avr una sua funzione
di visualizzazione, chiamata sempre con lo stesso nome, e selezionata ogni volta
correttamente grazie al polimorfismo.
Una classe base con almeno una funzione virtuale pura detta classe base
astratta, perch definisce la struttura di una gerarchia di classi, ma non pu
essere istanziata direttamente.
A differenza dalle normali funzioni virtuali, le funzioni virtuali pure devono
essere ridefinite tutte nelle classi derivate (anche con "corpo nullo", quando
non servono). Se una classe derivata non ridefinisce anche una sola funzione
virtuale pura della classe base, rimane una classe astratta e non pu ancora
essere istanziata (a questo punto, una sua eventuale classe derivata, per
diventare "concreta", sufficiente che ridefinisca l'unica funzione virtuale
pura rimasta).
Le classi astratte sono di importanza fondamentale nella programmazione in
C++ ad alto livello, orientata a oggetti. Esse presentano agli utenti delle
interfacce "pure", senza il vincolo degli aspetti implementativi, che sono invece
forniti dalle loro classi derivate. Una gerarchia di classi, che deriva da una o
pi classi astratte, pu essere costruita in modo "incrementale", nel senso di
permettere il "raffinamento" di un progetto, aggiungendo via via nuove classi
senza la necessit di modificare la parte preesistente. Gli utenti non sono coinvolti,
se non vogliono, in questo processo di "raffinamento incrementale", in quanto

vedono sempre la stessa interfaccia e utilizzano sempre le stesse funzioni (che,


grazie al polimorfismo, saranno sempre selezionate sull'oggetto appropriato).

Un rudimentale sistema di figure geometriche

A puro titolo esemplificativo dei concetti finora esposti, si tentato di progettare


l'implementazione di un sistema (molto "rudimentale") di figure geometriche
piane. Abbiamo scelto 6 figure, a ciascuna delle quali abbiamo fatto corrispondere
una classe:
punto

classe Dot

linea

classe Line

triangolo

classe Triangle

rettangolo

classe Rect

quadrato

classe Square

cerchio

classe Circle

Tutte queste classi fanno parte di una gerarchia, al cui vertice si trova un'unica
classe base astratta, di nome Shape, che contiene esclusivamente un
distruttore virtuale (con "corpo nullo") e alcune funzioni virtuali pure. La
classe Shape presenta, quindi, una pura interfaccia, non possedendo datimembro n funzioni-membro implementate, e non pu essere istanziata (il
compilatore darebbe errore).
Dalla classe Shape derivano due classi, anch'esse astratte, di nome
Polygon e Regular (per la precisione, Polygon non astratta, ma il suo
costruttore inserito nella sezione protetta e quindi non pu essere
istanziata dall'esterno; Regular, invece, astratta, in quanto non ridefinisce
tutte le funzioni virtuali pure di Shape).
Finalmente, le classi "concrete" derivano tutte da Polygon e Regular: Dot,
Line, Triangle e Rect derivano da Polygon; Circle deriva da Regular;
Square deriva da Polygon e Regular, per eredit multipla. Si configura cos
il seguente schema:

A queste classi si aggiungono due strutture di appoggio: Point, che fornisce le


coordinate dei punti sul piano, e Shape_Error, per la gestione delle eccezioni. Il
tutto racchiuso in un unico namespace, di nome mini_graphics, che
contiene anche alcune costanti e alcune funzioni esterne alle classi, fra cui due
operatori in overload per la lettura e scrittura di oggetti di Point. Il fatto che
tutte le componenti del sistema appartengano a un namespace permette di
evitare i potenziali conflitti di nomi, in verit molto comuni, come Line e Rect,
con nomi uguali forniti da altre librerie ed eventualmente messi a disposizione da
queste tramite using-directives. Volendo, l'utente provveder ad inserire, negli
ambiti locali del main e delle sue funzioni, le using-declarations necessarie;
a questo proposito viene fornito un header-file contenente tutte le usingdeclarations dei nomi del namespace che possono essere visti dall'utente.
Il sistema accessibile dall'esterno esclusivamente attraverso le funzioni virtuali
pure di Shape, ridefinite nelle classi "concrete"; per cui, definito un
puntatore a Shape, possibile tramite questo sfruttare il polimorfismo e
chiamare ogni volta la funzione-membro dell'oggetto "reale" selezionato in
fase di esecuzione. Non tutte le funzioni, per, sono compatibili con tutti gli
oggetti (per esempio una funzione che fornisce due punti pu essere usata per
definire una linea o un rettangolo, ma non per definire un triangolo); d'altra
parte, in ogni classe "concreta", tutte le funzioni virtuali pure vanno
ridefinite, e ci ha costituito un problema, che poteva essere risolto in due modi:
1. in ogni classe, ridefinire con "corpo nullo" tutte le funzioni incompatibili
(ma in questo modo l'utente non sarebbe stato informato del suo errore);
2. oppure ridefinire tali funzioni in modo da sollevare un'eccezione (ed
quello che stato fatto): le funzioni di questo tipo sono state collocate
nelle classi "intermedie" Polygon e Regular, e quindi non hanno avuto
bisogno di essere ridefinite nelle classi "concrete" (dove sono ridefinite
solo le funzioni "compatibili").
Le funzioni virtuali di Shape sono in tutto 11, divise in 4 gruppi e
precisamente:

funzioni set (con 5 overloads) per impostare i parametri caratteristici di


ogni figura (per esempio, le coordinate del bottom-left-corner e del topright-corner di un rettangolo); all'inizo, i costruttori (di default) delle
classi "concrete" generano figure precostituite;

4 funzioni get... per estrarre informazioni dalle figure (per esempio, le


coordinate di un vertice di un poligono, oppure la lunghezza del diametro
di un cerchio ecc...);
una funzione display per la visualizzazione (non grafica) dei parametri di
una figura (per esempio le coordinate dei punti estremi di una linea o dei
quattro vertici di un quadrato ecc...);
una funzione copy_from per copiare una figura da un'altra; se si tenta
la copia fra due figure diverse sollevata un'eccezione, salvo in questi
casi:
1. copia da quadrato a rettangolo (ammessa in quanto il quadrato
un caso particolare di rettangolo);
2. copia da quadrato a cerchio (ricava il cerchio iscritto al
quadrato);
3. copia da cerchio a quadrato (ricava il quadrato circoscritto al
cerchio)

In effetti, si tratta di un sistema assolutamente "minimale". Ma il nostro scopo non


era quello di generare un prodotto finito, bens di mostrare "come si pone il primo
mattone di una casa". Infatti (e questa la caratteristica principale della
programmazione a oggetti che sfrutta il polimorfismo) il sistema si presta ad
essere agevolmente incrementato in maniera modulare, in tre direzioni:

si possono aggiungere nuove figure (e cio nuove classi) che


ridefiniscono le stesse funzioni virtuali pure di Shape, e quindi si pu
ampliare la gerarchia senza modificare nulla dell'esistente;
si possono aggiungere nuove funzionalit (per esempio, trasformazioni di
coordinate, traslazioni, rotazioni, variazioni della scala ecc...); in questo
caso bisogna apportare qualche modifica al progetto, ma pur sempre in
maniera "incrementale";
si possono creare infine altre gerarchie di classi, che eseguono operazioni
"specializzate", come per esempio la visualizzazione grafica delle figure su
un dato dispositivo (come vedremo nell'esercizio della prossima sezione); il
fatto importante che l'introduzione delle nuove gerarchie non comporta
alcuna modifica della gerarchia Shape, ma si limita a creare degli
"agganci" ad essa, preservando il requisito fondamentale di minimizzare
le dipendenze fra i moduli, che alla base di una corretta
programmazione. Infatti, per come stata progettata, la gerarchia Shape
"device-independent" pu essere visualizzata su qualunque
dispositivo grafico.

Particolare cura stata dedicata alla gestione delle eccezioni. Sono stati
individuati quattro tipi di errori possibili:

errori di input nell'inserimento dei dati (per esempio, digitazione di caratteri


diversi quando sono richieste cifre numeriche);
tentativi di generare figure geometriche "improprie" (per esempio un
triangolo con i tre vertici allineati);
tentativi di eseguire funzioni incompatibili con la figura selezionata;
tentativi di eseguire copie fra figure diverse (salvo nei casi sopraelencati).

Osserviamo, per concludere, che l'introduzione di una classe derivata per


eredit multipla (Square) ha generato qualche piccolo problema aggiuntivo e
richiesto una particolare attenzione:

anzitutto, per evitare la replicazione della classe base, si dovuto


inserire virtual nelle specifiche di accesso a Shape di Polygon e
Regular (e quindi Shape, oltre a essere astratta anche virtuale ....
pi "irreale" di cos....!);
in secondo luogo si sono dovute rifedinire in Square tutte le funzioni
virtuali pure di Shape (comprese quelle "incompatibili"); altrimenti, il
"doppio percorso" da Square a Shape avrebbe generato messaggi di
errore per ambiguit (infatti, se una funzione non ridefinita
ereditata: ma allora, in questo caso, sarebbe ereditata da Polygon o da
Regular?).

Un rudimentale sistema di visualizzazione delle figure

Proseguendo nell'esempio precedente, costruiamo ora una nuova gerarchia di


classi, con lo scopo di visualizzare su un dispositivo grafico le figure definite dalla
gerarchia Shape. Non avendo niente di meglio a disposizione, abbiamo scelto
una ("rudimentalissima") implementazione grafica costituita da caratteri ASCII,
nella quale ogni punto del piano immagine rappresentato da un carattere ("big
pixel") e quindi "disegnare" un punto significa collocare nella posizione
corrispondente un carattere adeguato (per esempio un asterisco). La bassissima
risoluzione di un simile sistema "grafico" produrr figure sicuramente distorte e
poco definite, ma che quello che ci preme sottolineare non l'efficacia del
prodotto, bens il metodo utilizzato per la sua implementazione. Il lettore potr
immaginarsi, al posto di questo sistema, una libreria grafica dotata delle pi
svariate funzionalit e atta a lavorare su dispositivi ad alta risoluzione; ma il
"metodo" per implementare tale libreria, mettendola in relazione con le figure di
Shape, sarebbe esattamente lo stesso.
La classe base della nostra nuova gerarchia si chiama ASC_Screen: una
classe astratta, in quanto possiede una funzione virtuale pura, di nome
draw, cos dichiarata:
virtual void draw() = 0;
Tuttavia, a differenza da Shape che presenta una pura interfaccia,
ASC_Screen deve fornire gli strumenti per l'implementazione della grafica su un
dispositivo "concreto" e quindi stata dotata di tutte le propriet e i metodi
adeguati allo scopo. Poich d'altra parte lo schermo "unico" indipendentemente
dal numero degli oggetti (cio delle figure) presenti, tutti i dati-membro e le
funzioni-membro di ASC_Screen (fuorch draw) sono stati definiti static.
Persino il costruttore e il distruttore (che ovviamente non possono essere
definiti static) si comportano in realt come metodi statici: il primo alloca la
memoria "grafica" solo in occasione del primo oggetto creato, il secondo libera
tale memoria solo quando tutti gli oggetti sono stati distrutti (per riconoscere tali
condizioni usato un membro statico "contatore" degli oggetti, incrementato
dal costruttore e decrementato dal distruttore).

Alcuni metodi di ASC_Screen sono accessibili dall'utente e quindi sono


pubblici; altri sono protetti, in quanto accessibili solo dalle classi derivate, e
altri sono privati, per solo uso interno. Tutti i dati-membro sono privati.
La classe ASC_Screen ha quindi una duplice funzione: quella di essere una
base astratta per gli oggetti delle sue classi derivate, che ridefiniscono la
funzione virtuale pura draw per eseguire i disegni; e quella di fornire, a livello
della classe e non del singolo oggetto, tutte le funzionalit e i dati necessari per
l'implementazione del sistema.
Ed a questo punto che entra in gioco l'eredit multipla, la quale permette una
soluzione semplice, pulita ed efficace al tempo stesso: ogni classe derivata da
ASC_Screen, che rappresenta una figura da graficare, deriva anche dalla
corrispondente classe di Shape: in questo modo, da una parte si ereditano le
caratteristiche generali di una figura, che sono "device-independent", e
dall'altra le funzionalit necessarie per il disegno della stessa figura su un
particolare device.
Le classi derivate da ASC_Screen hanno gli stessi nomi delle corrispondenti di
Shape, con il prefisso ASC_ (e quindi: ASC_Dot, ASC_Line, ecc...). Ogni
classe possiede un unico membro, che ridefinisce la funzione virtuale pura
draw. Non serve nient'altro, in quanto tutto il resto ereditato dalle rispettive
genitrici.
La situazione complessiva adesso rappresentata dal seguente disegno (la
gerarchia ASC_Screen "a testa in gi", per ragioni di spazio):

Nell'esercizio che segue viene visualizzato il disegno di una casa "in stile infantile",
in cui ogni componente (pareti, tetto, porte, finestre ecc...) costituito da una
figura geometrica elementare. In tutto sono definiti 24 oggetti e due array di
24 puntatori, uno a Shape e l'altro a ASC_Screen. L'indirizzo di ogni
oggetto assegnato al corrispondente puntatore (in entrambi gli array), cos
che possibile, per ogni figura, chiamare in modo polimorfo sia le funzioni di
Shape che la draw di ASC_Screen. Quest'ultima non esegue materialmente la
visualizzazione, ma si limita ad inserire degli asterischi (nelle posizioni che

definiscono il contorno della figura) in una matrice bidimensionale di caratteri


(allocata e inizializzata dal costruttore del primo oggetto); poich ogni riga
della matrice terminata con un null, si vengono cos a costituire tante
stringhe quante sono le righe. Alla fine, per visualizzare il tutto, il programma
pu chiamare il metodo pubblico ASC_Screen::OnScreen(), il quale non fa
altro che scrivere le stringhe sullo schermo, l'una sotto l'altra.
Il sistema pure dotato ("sorprendentemente") di alcune funzionalit pi
"avanzate", quali il clipping (lo schermo funge da "finestra" che visualizza solo
una parte dell'immagine, se questa ha un'estensione maggiore), il moving
(possibilit di spostare il centro della "finestra" su un qualunque punto
dell'immagine) e lo zoomming (possibilit di ingrandire o rimpicciolire l'immagine
intorno al centro della "finestra"). Tutte queste operazioni vengono eseguite
chiamando degli opportuni metodi pubblici di ASC_Screen.

Template
Programmazione generica

Nello sviluppo di questo corso, siamo "passati" attraverso vari tipi di


"programmazione", che in realt perseguono sempre lo stesso obiettivo
(suddivisione di un progetto in porzioni indipendenti, allo scopo di minimizzare il
rapporto costi/benefici nella produzione e manutenzione del software), ma che via
via tendono a realizzare tale obiettivo a livelli sempre pi profondi:

programmazione procedurale: la programmazione caratteristica del


linguaggio C (e di tutti gli altri linguaggi precedenti al C++). L'interesse
principale focalizzato sull'elaborazione e sulla scelta degli algoritmi pi
idonei a massimizzarne l'efficienza. Ogni algoritmo lavora in una
funzione, a cui si passano argomenti e da cui si ottiene un valore di
ritorno. Le funzioni sono implementate con gli strumenti tipici del
linguaggio (tipi, variabili, puntatori, costrutti vari ecc...). Dal punto di
vista dell'utente ogni funzione una "scatola nera" e i suoi argomenti e
valore di ritorno sono gli unici canali di comunicazione.
programmazione modulare: l'attenzione si sposta dal progetto delle
procedure all'organizzazione dei dati. Ogni gruppo formato da dati
logicamente correlati e dalle procedure che li utilizzano costituisce un
modulo, in cui i dati sono "occultati" (data hiding). I moduli sono il pi
possibile indipendenti. Le interfacce costituiscono l'unico canale di
comunicazione fra i moduli e i loro utenti. I namespace sono gli
strumenti che il C++ mette a disposizione per realizzare questo tipo di
programmazione.
programmazione a oggetti: l'attenzione si sposta ulteriormente dai
moduli ai singoli oggetti. Attraverso le classi, esiste la possibilit di
definire nuovi tipi. I membri di ogni classe possono essere sia dati che
funzioni e solo alcuni di essi possono essere accessibili dall'esterno. Il
data hiding si trasferisce dentro gli oggetti, che diventano entit attive e
autosufficienti e comunicano con gli utenti solo attraverso i propri membri
pubblici. Ogni nuovo tipo pu essere corredato di un insieme di
operazioni (overload degli operatori) e ulteriormente espanso e
specializzato in modo incrementale e indipendente dal codice gi scritto,
grazie all'eredit e al polimorfismo.

Un ulteriore "salto di qualit" rappresentato dalla cosidetta "programmazione


generica", la quale consente di applicare lo stesso codice a tipi diversi, cio di
definire template (modelli) di classi e funzioni parametrizzando i tipi
utilizzati: nelle classi, si possono parametrizzare i tipi dei dati-membro; nelle
funzioni (e nelle funzioni-membro delle classi) si possono parametrizzare i
tipi degli argomenti e del valore di ritorno. In questo modo si raggiunge il
massimo di indipendenza degli algoritmi dai dati a cui si applicano: per esempio,
un algoritmo di ordinamento pu essere scritto una sola volta, qualunque sia il
tipo dei dati da ordinare.
I template sono risolti staticamente (cio a livello di compilazione) e
pertanto non comportano alcun costo aggiuntivo in fase di esecuzione; sono

invece di enorme utilit per il programmatore, che pu scrivere del codice


"generico", senza doversi preoccupare di differenziarlo in ragione della variet
dei tipi a cui tale codice va applicato. Ci particolarmente vantaggioso quando
si possono creare classi strutturate identicamente, ma differenti solo per i tipi dei
membri e/o per i tipi degli argomenti delle funzioni-membro.
La stessa Libreria Standard del C++ mette a disposizione strutture
precostituite di classi template, dette classi contenitore (liste concatenate,
mappe, vettori ecc...) che possono essere utilizzate specificando, nella creazione
degli oggetti, i valori reali da sostituire ai tipi parametrizzati.

Definizione di una classe template

Una classe (o struttura) template identificata dalla presenza, davanti alla


definizione della classe, dell'espressione:
template<class T>
dove T (che un nome e segue le normali regola di specifica degli
identificatori) rappresenta il parametro di un tipo generico che verr
utilizzato nella dichiarazione di uno o pi membri della classe. In questo
contesto la parola-chiave class non ha il solito significato: indica che T il
nome di un tipo (anche nativo), non necessariamente di una classe. L'ambito
di visibilit di T coincide con quello della classe. Se per una funzionemembro non definita inline ma esternamente, bisogna, al solito, qualificare
il suo nome: in questo caso la qualificazione completa consiste nel ripetere il
prefisso template<class T> ancora prima del tipo di ritorno (che in particolare
pu anche dipendere da T) e inserire <T> dopo il nome della classe. Esempio:
Definizione della classe template A
template<class T> class A {
T mem ;

dato-membro di tipo

parametrizzato

public:
A(const T& m) : mem(m) { }

costruttore inline con un


argomento di
tipo parametrizzato

T get( );

dichiarazione di funzione-membro
con

valore di ritorno di tipo

parametrizzato
........ };

Definizione esterna della funzione-membro get( )


template<class par> par
A<par>::get( )
{

pu
nella

return mem ;

notare che il nome del parametro


anche essere diverso da quello usato
definizione della classe

}
NOTA Nella definizione della funzione get la ripetizione del parametro par nelle
espressioni template<class par> e A<par> potrebbe sembrare ridondante. In
realt le due espressioni hanno significato diverso:

template<class par> introduce, nel corrente ambito di visibilit (in


questo caso della funzione get), il nome par come parametro di
template;
A<par> indica che la classe A un template con parametro par.

In generale, ogni volta che una classe template riferita al di fuori del proprio
ambito (per esempio come argomento di una funzione), obbligatorio
specificarla seguita dal proprio parametro fra parentesi angolari.

I parametri di un template possono anche essere pi di uno, nel qual caso,


nella definizione della classe e nelle definizioni esterne delle sue funzionimembro, tutti i parametri vanno specificati con il prefisso class e separati da
virgole. Esempio:
template<class par1,class par2,class par3>
I template vanno sempre definiti in un namespace, o nel namespace
globale o anche nell'ambito di un'altra classe (template o no). Non possono
essere definiti nell'ambito di un blocco. Non inoltre ammesso definire nello
stesso ambito due classi con lo stesso nome, anche se hanno diverso numero
di parametri oppure se una classe template e l'altra no (in altre parole
l'overload ammesso fra le funzioni, non fra le classi).

Istanza di un template

Un template un semplice modello (come dice la parola stessa in inglese) e


non pu essere usato direttamente. Bisogna prima sostituirne i parametri con
tipi gi precedentemente definiti (che vengono detti argomenti). Solo dopo che

stata fatta questa operazione si crea una nuova classe (cio un nuovo tipo)
che pu essere a sua volta istanziata per la creazione di oggetti.
Il processo di generazione di una classe "reale" partendo da una classe
template e da un argomento detto: istanziazione di un template (notare
l'analogia: come un oggetto si crea istanziando un tipo, cos un tipo si crea
istanziando un template). Se una stessa classe template viene istanziata
pi volte con argomenti diversi, si dice che vengono create diverse
specializzazioni dello stesso template. La sintassi per l'istanziazione di un
template la seguente (riprendiamo l'esempio della classe template A):
A<tipo>
dove tipo il nome di un tipo (nativo o definito dall'utente), da sostituire al
parametro della classe template A nelle dichiarazioni (e definizioni) di tutti
i membri di A in cui tale parametro compare. Quindi la classe "reale" non A,
ma A<tipo>, cio la specializzazione di A con argomento tipo. Ci rende
possibili istruzioni, come per esempio la seguente:
A<int> ai(5);
che costruisce (mediante chiamata del costruttore con un argomento, di
valore 5) un oggetto ai della classe template A, specializzata con
argomento int.

Parametri di default

Come gli argomenti delle funzioni, anche i parametri dei template possono
essere impostati di default. Riprendendo l'esempio precedente, modifichiamo il
prefisso della definizione della classe A in:
template<class T = double>
ci comporta che, se nelle istanziazioni di A si omette l'argomento, questo
sottinteso double; per esempio:
A<> ad(3.7);
equivale a
A<double> ad(3.7);
(notare che le parentesi angolari vanno specificate comunque).
Se una classe template ha pi parametri, quelli di default possono anche
essere espressi in funzione di altri parametri. Supponiamo per esempio di
definire una classe template B nel seguente modo:
template<class T, class U = A<T> > class B { ........ };

in questa classe i parametri sono due: T e U; ma, mentre l'argomento


corrispondente a T deve essere sempre specificato, quello corrispondente a U pu
essere omesso, nel qual caso viene sostituito con il tipo generato dalla classe A
specializzata con l'argomento corrispondente a T. Cos:
B<double,int> crea la specializzazione di B con argomenti double e int,
mentre:
B<int> crea la specializzazione di B con argomenti int e A<int>

Funzioni template

Analogamente alle funzioni-membro di una classe, anche le funzioni non


appartenenti a una classe possono essere dichiarate (e definite) template.
Esempio di dichiarazione di una funzione template:
template<class T> void sort(int n, T* p);
Come si pu notare, uno degli argomenti della funzione sort di tipo
parametrizzato. La funzione ha lo scopo di ordinare un array p di n elementi
di tipo T, e dovr essere istanziata con argomenti di tipi "reali" da sostituire al
parametro T (vedremo pi avanti come si fa). Se un argomento di tipo
definito dall'utente, la classe che corrisponde a T dovr anche contenere tutti
gli overload degli operatori necessari per eseguire i confronti e gli scambi fra
gli elementi dell'array.
Seguitando nell'esempio, allo scopo di evidenziare tutta la "potenza" dei
template confrontiamo ora la nostra funzione con un'analoga funzione di
ordinamento, tratta dalla Run Time Library (che la libreria standard del
C). Il linguaggio C, che ovviamente non conosce i template n l'overload degli
operatori, pu rendere applicabile lo stesso algoritmo di ordinamento a
diversi tipi facendo ricorso agli "strumenti" che ha, e cio ai puntatori a void
(per generalizzare il tipo dell'array) e ai puntatori a funzione (per dar modo
all'utente di fornire la funzione di confronto fra gli elementi dell'array). Inoltre,
nel codice della funzione, dovr eseguire il casting da puntatori a void (che
non sono direttamente utilizzabili) a puntatori a byte (cio a char) e quindi, non
potendo usare direttamente l'aritmetica dei puntatori, dovr anche conoscere
il size del tipo utilizzato (come ulteriore argomento della funzione, che si
aggiunge al puntatore a funzione da usarsi per i confronti). In definitiva, la
funzione "generica" sort del C dovrebbe essere dichiarata nel seguente
modo:
typedef int (*CMP)(const void*, const void*);
void sort(int n, void* p, int size, CMP cmp);

l'utente dovr provvedere a fornire la funzione di confronto "vera" da sostituire a


cmp, e dovr pure preoccuparsi di eseguire, in detta funzione, tutti i necessari
casting da puntatore a void a puntatore al tipo utilizzato nella chiamata.
Risulta evidente che la soluzione con i template di gran lunga preferibile:
molto pi semplice e concisa (sia dal punto di vista del programmatore che da
quello dell'utente) ed anche pi veloce in esecuzione, in quanto non usa
puntatori a funzione, ma solo chiamate dirette (di overload di
operatori che, oltretutto, si possono spesso realizzare inline).

Differenze fra funzioni e classi template

Le funzioni template differiscono dalle classi template principalmente sotto


tre aspetti:
1. Le funzioni template non ammettono parametri di default .
2. Come le classi, anche le funzioni template sono utilizzabili soltanto dopo
che sono state istanziate; ma, mentre nelle classi le istanze devono
essere sempre esplicite (cio gli argomenti non di default devono essere
sempre specificati), nelle funzioni gli argomenti possono essere spesso
dedotti implicitamente dal contesto della chiamata. Riprendendo
l'esempio della funzione sort, la sequenza:
double a[10] = { .........};
sort(10, a);
3. crea automaticamente un'istanza della funzione template sort, con
argomento double dedotto dalla stessa chiamata della funzione.
Quando invece un argomento non pu essere dedotto dal contesto, deve
essere specificato esplicitamente, nello stesso modo in cui lo si fa con le
classi. Esempio:
template<class T> T* create( ) { .........}
int* p = create<int>( ) ;
4. In generale un argomento pu essere dedotto quando corrisponde al
tipo di un argomento della funzione e non pu esserlo quando
corrisponde al tipo del valore di ritorno.
Se una funzione template ha pi parametri, dei quali corrispondenti
argomenti alcuni possono essere dedotti e altri no, gli argomenti
deducibili possono essere omessi solo se sono gli ultimi nella lista

(esattamente come avviene per gli argomenti di default di una


funzione). Esempio (supponiamo che la variabile d sia stata definita
double):

FUNZIONE

CHIAMATA

NOTE
Il secondo argomento
dedotto di tipo double

template<class
T,class U>
T fun1(U);

int m =
fun1<int>(d);

template<class
T,class U>
U fun2(T);

int m =
Il primo argomento non si
fun2<double,int>(d); pu omettere,
anche se deducibile

5. Analogamente alle funzioni tradizionali, e a differenza dalle classi, anche


le funzioni template ammettono l'overload (compresi overload di tipo
"misto", cio fra una funzione tradizionale e una funzione template).
Nel momento della "scelta" (cio quando una funzione in overload viene
chiamata), il compilatore applica le normali regole di risoluzione degli
overload, alle quali si aggiungono le regole per la scelta della
specializzazione che meglio si adatta agli argomenti di chiamata della
funzione. Va precisato, tuttavia, che tali regole dipendono dal tipo di
compilatore usato, in quanto i template rappresentano un aspetto dello
standard C++ ancora in "evoluzione". Nel seguito, ci riferiremo ai criteri
applicati dal compilatore gcc 3.3 (che il pi "moderno" che
conosciamo):
a) fra due funzioni template con lo stesso nome viene scelta quella "pi
specializzata" (cio quella che corrisponde pi esattamente agli argomenti
della chiamata); per esempio, date due funzioni:
template<class T> void fun(T); e template<class T> void
fun(A<T>);
(dove A la classe del nostro esempio iniziale), la chiamata:
fun(5); selezioner la prima funzione, mentre la chiamata:
fun(A<int>(5)); selezioner la seconda funzione;
b) se un argomento dedotto, non sono ammesse conversioni implicite di
tipo, salvo quelle "banali", cio le conversioni fra variabile e costante e
quelle da classe derivata a classe base; in altre parole, se uno stesso
argomento ripetuto pi volte, tutti i tipi dei corrispondenti argomenti
nella chiamata devono essere identici (a parte i casi di convertibilit sopra
menzionati);
c) come per l'overload fra funzioni tradizionali, le funzioni in cui la
corrispondenza fra i tipi esatta sono preferite a quelle in cui la
corrispondenza si ottiene solo dopo una conversione implicita;
d) a parit di tutte le altre condizioni, le funzioni tradizionali sono preferite alle
funzioni template;
e) il compilatore segnala errore se, malgrado tutti gli "sforzi", non trova
nessuna corrispondenza soddisfacente; come pure segnala errore in caso di
ambiguit, cio se trova due diverse soluzioni allo stesso livello di
preferenza.

6. Per maggior chiarimento, vediamo ora alcuni esempi di chiamate di


funzioni e di scelte conseguenti operate dal compilatore, date queste
due funzioni in overload, una tradizionale e l'altra template:
void fun(double,double); e template<class T> void
fun(T,T);

CHIAMATA
fun(1,2);

RISOLUZIONE

NOTE

argomento

fun<int>(1,2);

dedotto,
corrispondenza
esatta
fun(1.1,2.3);

fun(1.1,2.3);

funzione
tradizionale,
preferita

fun('A',2);

fun(double('A'),double(2));

funzione
tradizionale, unica
possibile

fun<char>(69,71.2); fun<char>(char(69),char(71.2)); argomento


esplicito,
conversioni
ammesse
definite le seguenti variabili: int a = ...;
fun(a,c);

const int c = ...;

fun<int>(a,c);

int* p = ...;

argomento
dedotto,
conversione
"banale"

fun(a,p);

ERRORE

conversione non
ammessa da int*
a double

Template e modularit

In relazione alla ODR (One-Definition-Rule), le funzioni template (e le


funzioni-membro delle classi template) appartengono alla stessa categoria
delle funzioni inline e delle classi (vedere capitolo: Tipi definiti dall'utente,
sezione: Strutture), cio in pratica la definizione di una funzione template
pu essere ripetuta identica in pi translation units del programma.
N potrebbe essere diversamente. Infatti, come si detto, i template sono
istanziati staticamente, cio a livello di compilazione, e quindi il codice che

utilizza un template deve essere nella stessa translation unit del codice che lo
definisce. In particolare, se un stesso template usato in pi translation
units, la sua definizione, non solo pu, ma deve essere inclusa in tutte (in altre
parole, non sono ammesse librerie di template gi direttamente in codice binario,
ma solo header-files che includano anche il codice di implementazione in forma
sorgente).
Queste regole, per, contraddicono il principio fondamentale della
programmazione modulare, che stabilisce la separazione e l'indipendenza del
codice dell'utente da quello delle procedure utilizzate: l'interfaccia comune non
dovrebbe contenere le definizioni, ma solo le dichiarazioni delle funzioni (e
delle funzioni-membro delle classi) coinvolte, per modo che qualunque
modifica venga apportata al codice di implementazione di dette funzioni, quello
dell'utente non ne venga influenzato. Con le funzioni template questo non pi
possibile.
Per ovviare a tale grave carenza, e far s che la programmazione generica
costituisca realmente "un passo avanti" nella direzione dell'indipendenza fra le
varie parti di un programma, mantenendo nel contempo tutte le "posizioni"
acquisite dagli altri livelli di programmazione, stata recentemente introdotta
nello standard una nuova parola-chiave: "export", che, usata come prefisso
nella definizione di una funzione template, indica che la stessa definizione
accessibile anche da altre translation units. Spetter poi al linker, e non al
compilatore, generare le eventuali istanze richieste dall'utente. In questo modo
"tutto si rimette a posto", e in particolare:

le funzioni template possono essere compilate separatamente;


nell'interfaccia comune si possono includere solo le dichiarazioni, come
per le funzioni tradizionali.

Tutto ci sarebbe molto "bello", se non fosse che ... putroppo (secondo quello che
ci risulta) nessun compilatore a tutt'oggi implementa la parola-chiave export!
E quindi, per il momento, bisogna ancora includere le definizioni delle funzioni
template nell'interfaccia comune.

Generalit sulla Libreria Standard del C++


Campi di applicazione

La Libreria Standard del C++ costituita da un vasto numero di classi e


funzioni che trattano principalmente di:

Input-Output;

gestione delle stringhe;


gestione degli oggetti "contenitori" di altri oggetti (detti: elementi),
quali: gli array, le liste, le code, le mappe, gli insiemi ecc...;
utilizzo degli "iteratori", per "navigare" attraverso gli elementi di un
contenitore o i caratteri di una stringa;
utilizzo degli "algoritmi", per eseguire operazioni sui contenitori e sui
loro elementi, quali: ricerca, conteggio, inserimento, sostituzione,
ordinamento, merging ecc...; sono previste anche operazioni specifiche,
eseguite tramite oggetti-funzione forniti dall'utente o dalla stessa
Libreria;
operazioni numeriche e matematiche su numeri reali o complessi;
informazioni riguardanti aspetti del linguaggio che dipendono
dall'implementazione (per esempio: il massimo valore di un float).

La programmazione generica largamente applicata nella Libreria: infatti,


nella grande maggioranza le sue classi e funzioni sono template (o
specializzazioni di template). Questo fa s che le stesse operazioni siano
applicabili a una vasta variet di tipi, sia nativi che definiti dall'utente.
In aggiunta alla Libreria Standard del C++, la maggior parte delle
implementazioni offre librerie di "interfacce grafiche", spesso chiamate anche GUI
(graphical user interface), con sistemi a "finestre" per l'interazione fra utente e
programma. Inoltre, la maggior parte degli ambienti di sviluppo integrati fornisce
librerie dette FL (foundation libraries), che supportano lo sviluppo di
applicazioni in accordo con l'ambiente specifico in cui lavorano (per esempio, la
MFC del Visual C++). Sia le GUI che (ovviamente) le FL non fanno parte dello
standard C++ e quindi non verranno trattate in questo corso. La stessa Libreria
Standard sar considerata perlopi "dal punto di vista dell'utente", cio
l'attenzione sar focalizzata sul suo utilizzo, pi che sulla descrizione del suo
contenuto.

Header files

Le classi e le funzioni della Libreria Standard sono raggruppate in una


cinquantina di header files, i cui nomi seguono una particolare convenzione:
non hanno estensione (cio non hanno .h). Per esempio, il principale header file
per le operazioni di input-output <iostream> (al posto di <iostream.h>
della "vecchia" libreria).
In ogni header file si trova di solito una classe (con le eventuali classi
derivate se presente una gerarchia di classi), e varie funzioni esterne di
appoggio, soprattutto per la definizione di operatori in overload.
A volte un header file include altri header files. Tuttavia, all'inizio di ognuno di
essi, sono inserite alcune direttive al preprocessore che, interrogando
opportune costanti predefinite, controllano l'effettiva inclusione del file (cio
non lo includono se gi stato incluso precedentemente). Questo permette
all'utente di inserire tutte le direttive #include che ritiene necessarie, senza
preoccuparsi di generare eventuali duplicazioni di nomi.
La Libreria Standard del C++ ingloba la Run Time Library del C, i cui
header files possono essere specificati in due modi:

con il loro nome tradizionale, per esempio <stdio.h>;


con i nomi della convenzione C++, senza .h, ma con la lettera c davanti,
per esempio <cstdio>

Il namespace std

Tutta la Libreria Standard del C++ definita in un unico namespace, che si


chiama: std.
Pertanto i nomi delle classi, delle funzioni e degli oggetti definiti nella
Libreria devono essere qualificati con il prefisso std::. Per esempio, le
operazioni di ouput sul dispostitivo standard vanno scritte:
std::cout << .....

invece di :

cout << .....

Va precisato, tuttavia, che alcuni compilatori pi "vecchi" non accettano la


qualificazione, e altri, "intermedi", accettano entrambe le forme.
Un'alternativa, anche se sconsigliabile per motivi gi detti pi volte, quella di
trasferire l'intera Libreria nel namespace globale, mediante la using
directive:
using namespace std;

la quale rende disponibili tutti i nomi della Libreria senza bisogno di


qualificarli.
Per semplicit, visto che i nostri programmi di esempio sono in genere molto
brevi, e quindi il pericolo di conflitto fra i nomi praticamente inesistente,
adotteremo questa soluzione.

La Standard Template Library

Un'importante sottinsieme della Libreria Standard del C++ la cosidetta


Standard Template Library (STL), che mette a disposizione degli utenti classi
e funzioni template per la gestione dei contenitori e degli associati iteratori
e algoritmi.
La principale caratteristica della STL quella di fornire la massima genericit: i
template della STL permettono all'utente di generare la specializzazione che
desidera (fatte salve certe premesse), cio di utilizzare la libreria con dati di
qualunque tipo.
Fuori dalla STL, si ritrovano ancora classi e funzioni template, ma in generale
la scelta delle possibili specializzazioni si esaurisce in ambiti pi ristretti. Per
esempio, i template che gestiscono le stringhe e l'input-output limitano la
loro genericit alla scelta della codifica dei caratteri utilizzati. Noi abbiamo
sempre trattato (e tratteremo) soltanto di caratteri ASCII di un byte (il tipo
char), ma bene sapere che sono possibili anche caratteri con codifiche diverse
(per esempio caratteri giapponesi), che occupano pi di un byte (i cosidetti
wide-characters, o caratteri estesi). Poich noi "conosciamo" solo il tipo char,
quando parleremo di stringhe e di input-output ignoreremo il fatto che siano
template, perch in realt tratteremo con template gi specializzati con
argomento <char>.
Un altro esempio: la classe dei numeri complessi un template solo per il
fatto che i tipi delle parti reale e immaginaria possono essere specializzati
con float, double o long double.
Nelle classi e funzioni della STL, invece, la scelta dei tipi degli argomenti
completamente libera: l'unica condizione, per i tipi definiti dall'utente, che
questi siano forniti di tutti gli operatori in overload necessari per eseguire le
operazioni previste.
Nel seguito riportiamo, per completezza, l'elenco (in ordine alfabetico) degli
header files che fanno capo alla STL. Tratteremo solo di alcuni.
<algorithm>

algoritmi

<deque>

contenitore: coda "bifronte"

<functional>

oggetti-funzione

<iterator>

iteratori

<list>

contenitore: double-linked list

<map>

contenitore: array associativo

<memory>

allocazione di memoria per contenitori

<numeric>

operazioni numeriche

<queue>

contenitore: coda (FIFO)

<set>

contenitore: insieme

<stack>

contenitore: pila (LIFO)

<utility>

coppie di dati e operatori relazionali

<vector>

contenitore: array monodimensionale

La Standard Template Library


Generalit

Una classe che memorizza una collezione di oggetti (chiamati elementi), tutti
di un certo tipo (parametrizzato), e detta: "contenitore".
I contenitori della STL sono stati progettati in modo da ottenere il massimo
dell'efficienza accompagnata al massimo della genericit. L'obiettivo
dell'efficienza ha escluso dal progetto l'utilizzo delle funzioni virtuali, che
comportano un costo aggiuntivo in fase di esecuzione; e quindi non esiste
un'interfaccia standard per i contenitori, nella forma di classe base astratta.
Ogni contenitore non deriva da un altro, n da una base comune, ma ripete
l'implementazione di una serie di operazioni standard, ognuna delle quali ha, nei
diversi contenitori, lo stesso nome e significato. Qualche contenitore aggiunge
operazioni specifiche, altri eliminano operazioni inefficienti per le loro
particolari caratteristiche, ma resta un nutrito sottoinsieme di operazioni comuni
a tutti i contenitori. Quanto detto vale non solo per le funzioni che sono
metodi delle classi, ma anche per quelle (dette "algoritmi") che lavorano sui
contenitori dall'esterno.
Gli iteratori permettono di scorrere su un contenitore, accedendo a ogni
elemento singolarmente. Un iteratore astrae e generalizza il concetto di
puntatore a una sequenza di oggetti e pu essere implementato in tanti modi
diversi (per esempio, nel caso di un array sar effettivamente un puntatore,
mentre nel caso di una lista sar un link ecc...). In realt la particolare
implementazione di un iteratore non interessa all'utente, in quanto le
definizioni che riguardano gli iteratori sono identiche, nel nome e nel
significato, in tutti i contenitori.
Riassumendo, "dal punto di vista dell'utente", sia le operazioni (metodi
e algoritmi) che gli iteratori costituiscono, salvo qualche eccezione, un insieme
standard, indipendente dai contenitori a cui vengono applicati. In questo modo
possibile scrivere funzioni template con il massimo della genericit
(parametrizzando non solo il tipo dei dati, ma anche la stessa scelta del
contenitore), senza nulla togliere all'efficienza in fase di esecuzione.
Tutte le classi template dei contenitori hanno almeno due parametri, ma il
secondo (che normalmente riguarda l'allocazione della memoria) pu essere
omesso in quanto il tipo normalmente utilizzato fornito di default. Non
approfondiremo questo argomento e quindi descriveremo sempre le classi
template della STL come se avessero solo il parametro che si riferisce al tipo
degli elementi. In generale, allo scopo di "semplificare" una trattazione che gi
cos abbastanza complessa, trascureremo il pi delle volte sia i parametri di
default dei template che gli argomenti di default delle funzioni.

Iteratori

Abbiamo detto che un iteratore un'astrazione pura, che generalizza il concetto di


puntatore a un elemento di una sequenza.
Sequenze
Anche il concetto di sequenza un'astrazione, che significa: "qualcosa in cui si
pu andare dall'inizio alla fine tramite l'operazione prossimo-elemento", come
esemplificato dalla seguente rappresentazione grafica:

Un iteratore "punta" a un elemento e fornisce un'operazione per far s che


l'iteratore stesso possa puntare all'elemento successivo della sequenza. La
fine di una sequenza corrisponde a un iteratore che "punta" all'ipotetico
elemento che segue immediatamente l'ultimo elemento della sequenza (non
esiste un iteratore NULL, come nei normali puntatori).
Operazioni basilari sugli iteratori
Le operazioni basilari sugli iteratori sono 3 e precisamente:
1. "accedi all'elemento puntato" (dereferenziazione, rappresentata dagli
operatori * e ->)
NOTA: a questo proposito un iteratore viene detto valido se punta
realmente a un elemento, cio se pu essere dereferenziato; un
iteratore non valido se non stato inizializzato, oppure se puntava a
un contenitore che stato ridimensionato (vedere pi avanti) o
distrutto, oppure se punta alla fine di una sequenza
2. "punta al prossimo elemento" (incremento, prefisso o suffisso,
rappresentata dall'operatore ++)
3. "esegui il test di uguaglianza o disuguaglianza" (rappresentate dagli
operatori == e !=)
(notare la perfetta coincidenza, simbolica e semantica, con le rispettive
operazioni sui normali puntatori)
L'esistenza di queste operazioni basilari ci permette di scrivere codice generico
che si pu applicare a qualsiasi contenitore, come nell'esempio della seguente

funzione template, che copia una qualunque sequenza in un'altra (purch in


entrambe siano definiti i rispettivi iteratori):
template <class In, class Out> void copy(In from, In endseq, Out to)
{
while(from !=
endseq)

// cicla da from a endseq (escluso)

{
*to

= *from;

++from;
++to;

// copia l'elemento puntato da from in quello


puntato da to
// punta all'elemento successivo della sequenza di

input

// punta all'elemento successivo della sequenza di

output

}
}
il parametro In corrisponde a un tipo iteratore definito nella sequenza di
input; il parametro Out corrisponde a un tipo iteratore definito nella
sequenza di output (i parametri sono due anzich uno per permettere la copia
anche fra contenitori diversi).
Notare che la nostra copy funziona benissimo anche per i normali puntatori. Per
esempio, dati due array di char, cos definiti:
char vi[100], vo[100];
la funzione copy ottiene il risultato voluto se chiamata nel modo seguente:
copy(vi, vi+100, vo);
in questo punto la copy viene istanziata con gli argomenti char* e char*,
dedotti implicitamente dal contesto della chiamata, e quindi si crea la
specializzazione:
copy<char*,char*>
cio una funzione che non pi template ma "reale", e ottiene come risultato
la copia dell'array vi nell'array vo.
Gli iteratori sono tipi
Come gi anticipato nell'esempio che abbiamo visto, gli iteratori sono tipi. Ogni
tipo iteratore definito nell'ambito della classe contenitore a cui si
riferisce. Ci sono perci molti tipi intrinsecamente diversi di iteratori, dal
momento che ogni iteratore deve essere in grado di svolgere la propria funzione
per un particolare tipo di contenitore. Tuttavia l'utente quasi mai ha bisogno di
conoscere il tipo di uno specifico iteratore: ogni contenitore "conosce" i suoi
tipi iteratori e li rende disponibili con nomi convenzionali, uguali in tutti i
contenitori.

Il pi comune tipo iteratore :


iterator
che punta a un elemento modificabile del contenitore a cui si riferisce.
Gli altri tipi iteratori definiti nelle classi contenitore sono:
const_iterator

punta a elementi non modificabili (analogo di puntatore


a costante)

reverse_iterator

percorre la sequenza in ordine inverso (gli elementi


puntati sono modificabili)

const_reverse_iterator

percorre la sequenza in ordine inverso (gli elementi


puntati non sono modificabili)

NOTA: gli iteratori diretti e inversi non si possono mescolare (cio non sono
amesse conversioni di tipo fra iterator e reverse_iterator).
Un oggetto iteratore si ottiene (come sempre succede quando si tratta con i
tipi) istanziando un tipo iteratore. Poich ogni tipo iteratore definito
nell'ambito di una classe, il suo nome pu essere rappresentato all'esterno solo
se qualificato con il nome della classe di appartenenza (esattamente come
per i membri statici). Per esempio, consideriamo il contenitore vector,
specializzato con argomento int; l'istruzione:
vector<int>::iterator it;
definisce l'oggetto iteratore it, istanza del tipo iterator della classe
vector<int>.
Inizializzazione degli iteratori e funzioni-membro che restituiscono iteratori
L'oggetto it non ancora un iteratore valido, in quanto stato definito ma
non inizializzato ( esattamente lo stesso discorso che si fa per i puntatori).
Per permettere l'inizializzazione di un iteratore, ogni contenitore mette a
disposizione un certo numero di funzioni-membro, che danno accesso agli
estremi della sequenza (come al solito, i nomi di queste funzioni sono gli stessi
in tutti i contenitori):
iterator begin();

restituisce un oggetto iteratore che punta


all'inizio della sequenza

const_iterator begin() const;

come sopra (elementi costanti)

iterator end();

restituisce un oggetto iteratore che punta alla


fine della sequenza

const_iterator end() const;

come sopra (elementi costanti)

reverse_iterator rbegin();

restituisce un oggetto iteratore che punta


all'inizio della sequenza inversa

const_reverse_iterator rbegin()
const;

come sopra (elementi costanti)

reverse_iterator rend();

restituisce un oggetto iteratore che punta alla

fine della sequenza inversa


const_reverse_iterator rend()
const;

come sopra (elementi costanti)

Per esempio, dato un array di n elementi, il valore di ritorno ....


di...

punta all'elemento di indice ...

begin()

end()

n (che non esiste)

rbegin()

n-1

rend()

-1 (che non esiste)

In aggiunta, esiste una funzione-membro (non dei contenitori, ma di


reverse_iterator) che fornisce l'unico modo per passare dal tipo
reverse_iterator al tipo iterator. Questa funzione si chiama base():
applicata a un oggetto reverse_iterator che punta a un certo elemento,
restituisce un oggetto iterator che punta all'elemento successivo.
Infine, un oggetto iteratore pu essere inizializzato (o assegnato) per copia
da un altro oggetto iteratore dello stesso tipo. Questo permette di scrivere
funzioni con argomenti iteratori passati by value (come la copy del nostro
esempio precedente).
Dichiarazione esplicita di tipo
Nell'esempio di definizione dell'oggetto iteratore it, l'espressione:
vector<int>::iterator rappresenta un tipo; il compilatore lo sa, in quanto
riconosce il contenitore vector. Ma se noi volessimo parametrizzare proprio il
contenitore, per esempio passandolo come argomento a una funzione
template:
template <class Cont> void fun(Cont& c)
e poi definendo e inizializzando all'interno della funzione un oggetto
iteratore, con l'istruzione:
Cont::iterator it = c.begin();
il compilatore non l'accetterebbe, non essendo in grado di riconoscere che
l'espressione Cont::iterator rappresenta un tipo. Perch l'espressione sia valida,
occorre in questo caso premettere la parola-chiave typename:
typename Cont::iterator it = c.begin();
e questo fa s che il compilatore accetti provvisoriamente Cont::iterator come
tipo, rinviando il controllo definitivo al momento dell'istanziazione della
funzione.
In generale la parola-chiave typename davanti a un identificatore dichiara
esplicitamente che quell'identificatore un tipo (pu anche essere usata al
posto di class nella definizione di un template). E' obbligatoria (almeno nelle
versioni pi avanzate dello standard) ogni volta che un tipo dipende da un
parametro di template.
Categorie di iteratori e altre operazioni

Senza entrare nei dettagli sull'argomento, che esula dagli intendimenti di questo
corso, vogliamo accennare al fatto che gli iteratori sono classificati in varie
categorie, a seconda delle operazioni che si possono eseguire su di essi. Infatti,
oltre alle 3 operazioni basilari che abbiamo visto (comuni a tutti gli iteratori),
sono possibili altre operazioni, che per si applicano soltanto ad alcune
categorie di iteratori. A loro volta le categorie dipendono sostanzialmente dai
particolari contenitori in cui gli iteratori sono definiti (per esempio: gli
iteratori definiti in vector e in deque appartengono alla categoria: "ad
accesso casuale", mentre gli iteratori definiti in list e in altri contenitori
appartengono alla categoria: "bidirezionale").
Le categorie sono organizzate gerarchicamente, nel senso che le operazioni
ammesse per gli iteratori di una certa categoria lo sono anche per gli iteratori
di categoria superiore, ma non viceversa. Gli stessi algoritmi, che (come
vedremo) hanno sempre argomenti iteratori, pretendono di operare, ognuno,
su una precisa categoria di iteratori (e su quelle gerarchicamente superiori).
Al vertice della gerarchia si trovano gli iteratori ad accesso casuale, seguiti
dagli iteratori bidirezionali (e da altri che non menzioneremo).
Gli iteratori bidirezionali e ad accesso casuale ammettono l'operazione di
decremento (--), che sposta il puntamento sull'elemento precedente della
sequenza, mentre soltanto agli iteratori ad accesso casuale sono riservate
alcune operazioni aggiuntive, quali:

indicizzazione [ ], per esempio it[3] : punta al terzo elemento


successivo
operazioni di confronto: < , <= , > , >=
tutte le operazioni con interi che forniscono un'aritmetica analoga a
quella dei puntatori: + , += , - , -=
a questo proposito: agli iteratori delle altre categorie, per i quali le
suddette operazioni non sono ammesse, la Libreria fornisce due
funzioni (supponiamo che Iter denoti un tipo iteratore):
void advance(Iter& it, int n) al posto di : it += n
e ....
difference_type distance(Iter first, Iter last) al posto di : last first
dove difference_type un tipo (di solito coincidente con int) definito
(come iterator) nel contenitore.

Contenitori Standard

Classificazione dei contenitori


I contenitori della STL sono suddivisi in 2 categorie:

le sequenze (in senso stretto)

i contenitori associativi

A loro volta le sequenze sono classificate in sequenze principali e adattatori.


Questi ultimi sono delle interfacce ridotte di sequenze principali, specializzate
per eseguire un insieme molto limitato di operazioni, e non dispongono di
iteratori.
Nei contenitori associativi gli elementi sono coppie di valori. Dato un valore,
la chiave, si pu (rapidamente) accedere all'altro, il valore mappato. Si pu
pensare a un contenitore associativo come a un array, in cui l'indice (la
chiave) non deve necessariamente essere un intero. Tutti i contenitori
associativi dispongono di iteratori bidirezionali, che percorrono gli elementi
ordinati per chiave (e quindi anche i contenitori associativi possono essere
considerati delle sequenze, in senso lato).
Tipi definiti nei contenitori
Tutti i contenitori mettono a disposizione nomi convenzionali di tipi, definiti
nel proprio ambito. Abbiamo appena visto i 4 tipi iteratori e il tipo
difference_type. Ve ne sono altri, dei quali elenchiamo i pi importanti:
value_type

tipo degli elementi

size_type

tipo degli indici e delle dimensioni (normalmente coincide con


unsigned int)

reference

equivale a value_type&

const_reference equivale a const value_type&


key_type

tipo della chiave nei contenitori associativi

mapped_type

tipo del valore mappato nei contenitori associativi

Costo delle operazioni


Nonostante tutti i tipi definiti nei contenitori e molte funzioni-membro
abbiano nomi standardizzati, per permettere la creazione di funzioni generiche
in cui i contenitori stessi figurino come parametri, non sempre conveniente
sfruttare questa possibilit. In certi casi, infatti, ci sono operazioni che risultano
pi efficienti usando un contenitore piuttosto che un altro, e quindi tali
operazioni, pur essendo disponibili in tutti i contenitori, non dovrebbero essere
inserite in funzioni generiche. In altri casi certe operazioni in alcuni
contenitori non sono neppure disponibili, talmente sarebbero inefficienti, e
quindi un tentativo di inserirle in funzioni generiche produrrebbe un messaggio
di errore. Ogni operazione ha un "costo computazionale", che spesso dipende
dal contenitore in cui eseguita, e quindi a volte non conviene parametrizzare
il contenitore, ma piuttosto selezionare il contenitore pi appropriato. La scelta
deve indirizzarsi a operare il pi possibile a "costo costante", cio indipendente dal
numero di elementi (per esempio, l'accesso a un elemento, data la sua
posizione, a "costo costante" usando vector, e non lo usando list, mentre per
l'inserimento di un elemento "in mezzo" esattamente il contrario).
Sommario dei contenitori

I contenitori della STL sono 10 (3 sequenze principali, 3 adattatori e 4


contenitori associativi) e precisamente:
vector

il contenitore pi completo; memorizza un array


monodimensionale, ai cui elementi pu accedere in modo
"randomatico", tramite iteratori ad accesso casuale e indici; pu
modificare le sue dimensioni, espandendosi in base alle necessit

list

rispetto a vector manca dell'accesso tramite indice e di varie


operazioni sugli iteratori, che non sono ad accesso casuale ma
bidirezionali; pi efficiente di vector nelle operazioni di
inserimento e cancellazione di elementi

deque

una "coda bifronte" cio una sequenza ottimizzata per rendere


le operazioni alle due estremit efficienti come in list, mentre
mantiene gli iteratori ad accesso casuale e l'accesso tramite indice
come in vector (di cui per non mantiene certe funzioni di gestione
delle dimensioni)

stack

un adattatore di deque per operazioni di accesso (top),


inserimento (push) e cancellazione (pop) dell'elemento in coda
alla sequenza

queue

un adattatore di deque per operazioni di inserimento in coda


(push) e cancellazione in testa (pop); l'accesso consentito sia in
coda (back) che in testa (front)

priority_queue definito nell'header-file <queue>; un adattatore di vector


per operazioni di inserimento (push) "ordinato" (cio fatto in modo
che gli elementi della sequenza siano sempre in ordine
decrescente), e per operazioni di cancellazione in testa (pop) e di
accesso in testa (top); il mantenimento degli elementi in ordine
comporta che le operazioni non siano eseguite "a costo costante" (se
l'implementazione "fatta bene" il costo dovrebbe essere
proporzionale al logaritmo del numero di elementi)
map

il pi importante dei contenitori associativi; memorizza una


sequenza di coppie (chiave e valore mappato, entrambi
parametri di map) e fornisce un'accesso rapido a ogni elemento
tramite la sua chiave (ogni chiave deve essere unica all'interno di un
map); mantiene i propri elementi in ordine crescente di chiave;
riguardo al "costo" delle operazioni, valgono le stesse considerazioni
fatte per priority_queue;
La sua operazione caratteristica l'accesso tramite indice
(chiamiamo m un oggetto di map):
valore mappato = m[chiave] oppure m[chiave] = valore

mappato

che funziona sia in estrazione che in inserimento; in ogni caso


cerca l'elemento con quella chiave: se lo trova, estrae (o inserisce)
il valore mappato; se non lo trova, lo crea e inizializza il valore
mappato con il "valore base" del suo tipo (dato da
mapped_type); il valore base zero (in modo appropriato al tipo),
se il tipo nativo, altrimenti un oggetto creato dal costruttore di
default (che in questo caso obbligatorio)
multimap

definito nell'header-file <map>; un contenitore associativo

analogo a map, con la differenza che la chiave pu essere duplicata;


non dispone dell'accesso tramite indice
set

un contenitore associativo analogo a map, con la differenza che


possiede solo la chiave (e quindi ha un solo parametro); non
dispone dell'accesso tramite indice; in pratica una sequenza
ordinata di valori unici e crescenti

multiset

definito nell'header-file <set>; un contenitore associativo


analogo a set, con la differenza che la chiave pu essere duplicata; in
pratica una sequenza ordinata di valori non decrescenti;

A queste classi si aggiunge la struttura template pair, definita in <utility> e


utilizzata dai contenitori associativi:
template <class T, class U> struct pair {........};
un oggetto pair costituito da una coppia di valori, di cui il primo, di tipo T,
memorizzato nel membro first e il secondo, di tipo U, memorizzato nel
membro second. La struttura possiede un costruttore di default, che
inizializza first e second ai valori base dei loro tipi, e un costruttore con un
2 argomenti, per fornire valori iniziali specifici a first e second. Esiste anche la
funzione di Libreria make_pair, che restituisce un oggetto pair, data una
coppia di valori. Gli elementi di map e multimap sono oggetti di pair
Requisiti degli elementi e relazioni d'ordine
Abbiamo detto che i template della STL possono essere istanziati con qualsiasi
tipo di elementi, a libera scelta dell'utente. Se il tipo prescelto nativo (non
puntatore!) non ci sono problemi. Ma se il tipo definito dall'utente, esistono
alcuni requisiti a cui deve soddisfare, se si vuole che le operazioni fornite dalla
Libreria funzionino correttamente.
Anzitutto le copie: gli elementi sono inseriti nel contenitore tramite copia di
oggetti esistenti, e quindi il nostro tipo deve essere provvisto di un costruttore
di copia e di un operatore di assegnazione adeguati (per esempio non devono
eseguire le copie dei membri puntatori ma delle aree puntate ecc...). Se
necessario, deve essere presente anche un corretto distruttore, poich, quando
un contenitore distrutto, sono automaticamente distrutti anche i suoi
elementi.
In secondo luogo, l'ordinamento: i contenitori associativi e priority_queue
ordinano gli elementi (nel momento stesso in cui li inseriscono), e la stessa cosa
viene fatta da alcuni algoritmi che operano sui contenitori. E' pertanto
indispensabile che il nostro tipo sia provvisto delle funzionalit necessarie per
l'ordinamento dei suoi oggetti. A volte queste funzionalit possono essere fornite
da oggetti-funzione specifici (di cui parleremo pi avanti, anticipiamo solo che
questi sono indispensabili nel caso che gli elementi siano puntatori a tipo
nativo), ma di default esse vengono cercate fra gli operatori in overload
definiti nel tipo stesso. Fortunatamente non necessario attrezzare il nostro tipo
con tutti gli operatori relazionali possibili, ma sufficiente che ce ne sia solo
uno: operator<. Infatti la Libreria usa soltanto questo operatore per ordinare
gli elementi. In compenso, pretende che la funzione che implementa
operator< sia "fatta bene", cio applichi un criterio di ordinamento di tipo "strict
weak ordering"; il che significa, in formule:

X < X falso (ordine stretto)


ammessa la possibilit che X < Y e Y < X siano entrambi falsi (ordine
debole); in questo caso si dice che X e Y hanno ordine equivalente
(cio in pratica sono uguali, ma non necessario definire operator==)
devono vale le propriet transitive:
o se X < Y e Y < Z allora X < Z
o se X e Y hanno ordine equivalente e Y e Z hanno ordine
equivalente, allora anche X e Z hanno ordine equivalente

Passiamo ora alla descrizione delle principali funzioni-membro dei contenitori. A parte gli
adattatori, che possiedono poche funzioni specifiche, gli altri contenitori hanno molte
funzioni in comune, con lo stesso nome e lo stesso significato. Pertanto, nella trattazione che
segue, raggruperemo le funzioni non per contenitore, ma per "tematiche", indicando con
Cont il nome di una generica classe contenitore e precisando eventualmente in quale
contenitore un certa funzione o non definita, o definita ma inefficiente; se non
altrimenti specificato, si intende che la funzione definita nelle sequenze principali e nei
contenitori associativi; indicheremo inoltre con Iter il nome di un generico tipo iteratore.
Dimensioni e capacit
Di default lo spazio di memoria per gli elementi di un contenitore allocato
nell'area heap, ma di questo l'utente non deve normalmente preoccuparsi, in
quanto ogni contenitore possiede un distruttore che libera automaticamente
l'area allocata.
La dimensione di un contenitore (cio il numero dei suoi elementi) non
prefissata e immodificabile (come negli array del C). Un oggetto contenitore
"nasce" con una certa dimensione, ma esistono diversi metodi che possono
modificarla (direttamente o implicitamente). La funzione-membro che modifica
direttamente una dimensione :
void Cont::resize(size_type n, value_type
val=value_type())
dove n la nuova dimensione: se minore della dimensione corrente,
vengono mantenuti solo i primi n elementi (con i loro valori); se maggiore,
vengono inseriti i nuovi elementi con valori tutti uguali a val, inizializzato di
default al valore base del loro tipo (value_type); la specifica dell'argomento
opzionale val obbligatoria nel caso che value_type non abbia un
costruttore di default. Il metodo resize definito soltanto nelle sequenze
principali.
Altri metodi, che aggiungono, inseriscono o rimuovono elementi in un
contenitore, ne modificano la dimensione implicitamente (li vedremo fra poco).
In ogni caso, quando la dimensione cambia, gli iteratori precedentemente
definiti potrebbero non essere pi validi (conviene ridefinirli o, almeno,
riinizializzarli).
I seguenti metodi in sola lettura restituiscono informazioni sulla dimensione di
un contenitore:
size_type Cont::size() const

restituisce la dimensione corrente dell'oggetto


*this; definito anche negli adattatori

bool Cont::empty() const

restituisce true se *this vuoto; definito anche


negli adattatori

size_type Cont::max_size()
const

restituisce la dimensione massima che un oggetto


di Cont pu raggiungere ( un numero
normalmente molto grande, che dipende dalla
stessa dimensione di value_type e
dall'implementazione)

Se definiamo "capacit" di un oggetto contenitore la quantit di memoria


correntemente allocata (in termini di numero di elementi), valida la seguente
diseguaglianza:
capacit >= dimensione
questo significa che, se la dimensione aumenta, ma resta inferiore alla
capacit, non viene allocata nuova memoria; appena la dimensione tende a
superare la capacit, si ha una riallocazione della memoria in modo da
ripristinare la diseguaglianza di cui sopra. In altri termini, la differenza:
capacit - dimensione
rappresenta il numero di elementi che si possono inserire senza causare
riallocazione di memoria.
In realt, in tutti i contenitori, salvo vector, capacit e dimensione sono
coincidenti, cio ogni operazione che comporta l'aumento della dimensione
produce contestualmente anche una nuova allocazione di memoria. Per evitare
che ci avvenga troppo spesso e che il "costo" di tali operazioni diventi troppo
elevato, vector mette a disposizione il seguente metodo, che consente di
aumentare la capacit senza modificare la dimensione, cio in pratica di evitare
continue riallocazioni, riservando uno spazio di memoria "preventivo", ma senza
inserirvi nuovi elementi:
void vector::reserve(size_type n)
dove n la nuova capacit: se minore della capacit corrente, la funzione
non ha effetto; se maggiore, alloca spazio per (n - capacit corrente) "futuri"
nuovi elementi. Si deduce che, con reserve, la capacit di un contenitore
pu soltanto aumentare; e la stessa cosa succede a seguito di resize e delle altre
operazioni che modificano la dimensione: la capacit o aumenta (quando
tende a essere superata dalla dimensione), o resta invariata, anche se la
dimensione diminuisce; pertanto non esiste modo di "restituire" memoria al
sistema prima che lo stesso contenitore venga distrutto (in realt un modo
esiste, ma lo vedremo pi avanti, quando parleremo della funzione-membro
swap).
Per ottenere informazioni sulla capacit, disponibile il seguente metodo:
size_type vector::capacity() const
che restituisce la quantit di memoria correntemente allocata, in termini di
numero di elementi.
Costruttori e operatori di copia
Tutti i contenitori dispongono di un certo numero di costruttori, e di operatori
e funzioni per eseguire le copie.
Anzitutto, il costruttore di default, il costruttore di copia e l'operatore di
assegnazione sono definiti in tutti i contenitori (adattatori compresi):
Cont::Cont()

crea un oggetto di Cont con dimensione


nulla

Cont::Cont(const Cont& c)

crea un oggetto di Cont copiandolo


dall'oggetto esistente c

Cont& Cont::operator=(const
Cont& c)

assegna un oggetto esistente c a *this

NOTE:
1. il costruttore di copia e l'operatore di assegnazione non ammettono
conversioni implicite, n fra i tipi dei contenitori, n fra i tipi degli
elementi (in altre parole, non si pu copiare un list in un vector, e
neppure un vector<int> in un vector<double>)
2. il nuovo oggetto creato dal costruttore di copia assume la dimensione
di c, ma non la sua capacit, che viene invece fatta coincidere con la
dimensione (cio allocata memoria solo per gli elementi copiati)
3. dopo l'assegnazione, *this assume la dimensione di c (gli elementi
preesistenti vengono eliminati), ma non riduce la sua capacit originaria
(pu solo aumentarla nel caso che venga superata dalla nuova
dimensione)
4. come noto, i costruttori di copia entrano in azione anche nel passaggio
by value di argomenti a una funzione. Nel caso che tali argomenti
siano oggetti di un contenitore, l'operazione potrebbe essere
"costosa", se la dimensione del contenitore molto grande. Pertanto si
consiglia, quando non necessario altrimenti per motivi particolari, di
passare sempre gli argomenti-contenitore by reference.
Nelle sole sequenze principali sono inoltre definite le due seguenti funzioni:

un costruttore con un 1 argomento (pi altri di default, di cui a noi


interessa solo il primo):
Cont::Cont(size_type n, const_reference val=value_type())
che crea un oggetto di Cont con dimensione n e inizializza gli
elementi con val (riguardo all'argomento di default vedere le
considerazioni fatte a proposito di resize); nella definizione della classe
Cont questa funzione-membro dichiarata explicit, per evitare
"accidentali" conversioni implicite da size_type a Cont;
il metodo assign, che una specie di "estensione" dell'operatore di
assegnazione (non si pu usare un operatore in overload perch
avrebbe "troppi" argomenti):
void Cont::assign(size_type n, const_reference val)
esegue la stessa operazione del costruttore di cui sopra, ma su un
oggetto di Cont gi esistente (altra differenza: il secondo argomento
non di default); come in tutte le operazioni di assegnazione, i
"vecchi" elementi vengono eliminati, la dimensione diventa n, ma la
capacit resta invariata (o aumenta, se era minore di n)

Finora abbiamo esaminato vari casi di operazioni di copia fra contenitori


vincolati a essere dello stesso tipo. Esiste per un costruttore che permette la
creazione degli oggetti di un contenitore mediante copia da un qualunque
altro contenitore, anche di tipo diverso (anche i tipi degli elementi possono
essere diversi, purch convertibili implicitamente gli uni negli altri):
Cont::Cont(Iter first, Iter last)
(dove Iter un tipo iteratore definito in Cont o in un altro contenitore);

questo metodo crea un oggetto di Cont, i cui elementi vengono generati


mediante copia a partire dall'elemento puntato da first fino all'elemento
puntato da last (escluso).
Per esempio, se lst un oggetto di list<int> (gi definito e inizializzato),
possibile creare un oggetto vec di vector<double> copiandovi tutti gli
elementi di lst (e convertendoli da int a double) con l'operazione:
vector<double> vec(lst.begin(),lst.end());
E' anche possibile eseguire un'assegnazione, con operazione analoga su un
oggetto di Cont gi esistente, mediante un overload del metodo assign
(definito solo nelle sequenze principali):
void Cont::assign(Iter first, Iter last)
Riprendendo l'esempio precedente, l'operazione:
vec.assign(lst.begin(),lst.end());
elimina in vec i suoi "vecchi" elementi e li sostituisce con quelli di lst (che
converte da int a double)
Infine, nel numero delle funzioni che eseguono copie di contenitori, si pu
includere anche il metodo swap:
void Cont::swap(Cont& c)
che scambia gli elementi, la dimensione e la capacit fra *this e c; i tipi, sia
dei contenitori che degli elementi, devono essere gli stessi nei due oggetti.
Per ogni contenitore disponibile, oltre al metodo swap, anche una funzione
esterna, con lo stesso nome:
void swap(Cont& c1,Cont& c2)
che scambia c1 con c2
Notare che la peculiarit di swap di scambiare anche le capacit, fornisce un
"trucco" che permette di ridurre la memoria allocata a un oggetto contenitore.
Infatti, supponiamo per esempio di avere un oggetto vec di un contenitore
vector<double>, con dimensione n e capacit m > n; con l'istruzione:
vector<double>* ptmp = new vector<double> (vec);
costruiamo un oggetto nell'area heap (puntato da ptmp) che, essendo una
copia di vec, ha dimensione n e capacit n; quindi, con l'istruzione:
vec.swap(*ptmp);
otteniamo che l'oggetto vec si "scambia" con *ptmp (ma gli elementi sono gli
stessi!) e quindi, in particolare, la sua capacit si riduce a n (mentre quella di
*ptmp diventa m); infine, con l'istruzione:
delete ptmp;
liberiamo la memoria allocata per *ptmp (e per i suoi m elementi). In totale
rimane l'oggetto originario vec con tutto come prima, salvo il fatto che la
memoria in eccesso stata deallocata.
Accesso agli elementi
Tutte le operazioni di accesso agli elementi possono funzionare sia in lettura
che in scrittura, cio possono restituire sia un r-value (lettura) che un lvalue(scrittura).
La pi generale operazione di accesso la dereferenziazione di un iteratore
(che abbiamo gi visto nella sezione dedicata agli iteratori).
I contenitori: vector, deque, e map possono accedere ai propri elementi
anche tramite operatori di indicizzazione:

reference Cont::operator[](size_type i)
per vector e deque; l'argomento i rappresenta l'indice;
const_reference Cont::operator[](size_type i) const
come il precedente, salvo che accede in sola lettura;
mapped_type Cont::operator[](const key_type& k)
per map (vedere la descrizione nella tabella sommaria dei contenitori);
l'argomento k rappresenta la chiave, che funge da indice.

A parte l'ovvia differenza fra i tipi degli indici, c' un'altra fondamentale
differenza fra l'indicizzazione in map e quella in vector e deque: mentre la
prima va sempre "a buon fine" (nel senso che, se un elemento con chiave k
non esiste, l'elemento viene aggiunto), la seconda pu generare un errore (non
segnalato) di valore indefinito (se in lettura) o di access violation (se in
scrittura), nel caso che l'elemento con indice i non esista. In altri termini, i
deve essere sempre compreso nel range fra 0 e size() (escluso). Il fatto che
l'accesso via indice non sia controllato una "scelta" di progetto, che permette
di evitare operazioni "costose" quando il controllo non necessario. Per
esempio, consideriamo il seguente codice:
vector<int> vec(100000); (crea un oggetto vec con 100000 elementi
vuoti)
for(size_type i=0; i < vec.size(); i++) ( li riempie ....)
{ ................. vec[i] = ................. }
sarebbe oltremodo "costoso" (oltre che sciocco) controllare 100000 volte che i
sia nel range!
A volte invece il controllo proprio necessario, specie nei casi in cui il valore di i
risulta da operazioni precedenti e quindi non possibile conoscerlo a priori.
L'accesso via indice "controllato" fornito dal metodo at (definito in vector e
deque):
reference Cont::at(size_type i)
const_eference Cont::at(size_type i) const (per la sola lettura)
che, in caso di errore, genera un'eccezione di tipo out_of_range.
Ci chiedamo a questo punto quale relazione intercorra fra gli indici e gli
iteratori. E' chiaro che (indicando con c un oggetto di vector o di deque e
con it un oggetto iteratore (diretto) che inizializziamo con begin()),
sempre vera l'uguaglianza:
c[0] == *it
e quindi, per analogia con i puntatori, siamo portati a pensare che sia vera
anche la seguente:
c[i] == *(it+i)
in realt lo , ma solo perch abbiamo supposto che c sia un oggetto di vector
o di deque, i cui iteratori sono ad accesso casuale e quindi ammettono
l'operazione + con valori interi; mentre non valida la relazione:
&c[0] == it
in quanto puntatori e iteratori sono tipi differenti.
Le operazioni di accesso in testa e in coda possono anche essere eseguite da
particolari metodi (definiti nelle sequenze principali e nell'adattatore
queue):
reference Cont::front() (accede al primo elemento)
const_reference Cont::front() const (come sopra, in sola lettura)
reference Cont::back() (accede al l'ultimo elemento)

const_reference Cont::back() const (come sopra, in sola lettura)


Gli adattatori stack e priority_queue possono accedere soltanto al primo
elemento (priority_queue) o all'ultimo (stack); entrambe le operazioni
vengono eseguite dal metodo top(), il quale non fa altro che chiamare front()
(in priority_queue) o back() (in stack).
I metodi front, back e top possono generare un errore (incontrollato) se
tentano di accedere a un contenitore vuoto.
Inserimento e cancellazione di elementi
Le operazioni di inserimento e cancellazione di elementi sono presenti in
tutti i contenitori. Tuttavia, in alcuni di essi sono poco efficienti e quindi
necessario capire in quali contenitori conviene eseguire certe operazioni e in
quali no. A questo scopo, presentiamo nella tabella che segue la relazione che
intercorre, in termini di efficienza, fra ogni contenitore e le sue operazioni di
inserimento e cancellazione, che suddividiamo in tre categorie: operazioni in
testa, in "mezzo" e in coda:
inserimento/
cancellazione
in testa
in "mezzo"
in coda

vector

deque

non definita efficiente


inefficiente
efficiente

list

priority_queue

efficiente
efficiente (solo
vedere nota
canc.)

stack

contenit
associat

non
non defin
definita

non
non definita
definita

non
vedere not
definita

efficiente
efficiente (solo
non definita
ins.)

efficiente non defin

inefficiente efficiente
efficiente

queue

NOTA: ricordiamo che nei contenitori associativi gli inserimenti le


cancellazioni sono sempre, come l'accesso, a "costo logaritmico"; in
priority_queue l'inserimento a "costo logaritmico" (perch deve
"ordinare"), mentre la cancellazione a "costo costante".
Ci premesso, vediamo i metodi disponibili per queste operazioni (ricordiamo
che esse modificano implicitamente la dimensione e quindi rendono invalidi gli
iteratori definiti precedentemente); indicheremo con val l'elemento da inserire
e con it l'iteratore che punta all'elemento da cancellare o all'elemento prima
del quale il nuovo elemento deve essere inserito:
inserimento in testa

void Cont::push_front(const_reference val)


(in priority_queue cambia nome in push)

cancellazione in testa

void Cont::pop_front()
(in queue e in priority_queue cambia nome in pop)

inserimento in "mezzo"
(vedere nota)

iterator Cont::insert(iterator it,const_reference val)


(ritorna un iteratore che punta al nuovo elemento)
void Cont::insert(iterator it,size_type
n,const_reference val)
(inserisce n volte val)
void Cont::insert(iterator it,Iter first, Iter last)

(dove Iter un tipo iteratore definito in Cont o in un altro


contenitore; inserisce elementi generati mediante copia a
partire dall'elemento puntato da first fino all'elemento
puntato da last escluso)
cancellazione in "mezzo" iterator Cont::erase(iterator it)
(ritorna un iteratore che punta all'elemento successivo a
quello cancellato, oppure ritorna end() se l'elemento
cancellato era l'ultimo)
iterator Cont::erase(iterator first, iterator last)
(cancella una serie di elementi contigui, a partire
dall'elemento puntato da first fino all'elemento puntato da
last escluso; ritorna come sopra)
void Cont::clear()
(elimina tutti gli elementi; equivale a erase con argomenti
begin() e end(), ma molto pi veloce)
inserimento in coda

void Cont::push_back(const_reference val)


(in queue e stack cambia nome in push)

cancellazione in coda

void Cont::pop_back()
(in stack cambia nome in pop)

NOTA: gli overloads del metodo insert elencati nella tabella riguardano solo le
sequenze principali; nei contenitori associativi insert definito con
overloads diversi (vedere pi avanti).
Tabella riassuntiva delle funzioni comuni
Abbiamo esaurito la trattazione degli adattatori e delle funzionimembro comuni a pi contenitori. Prima di passare alla descrizione dei metodi
specifici di singoli contenitori, presentiamo, nella seguente tabella l'elenco delle
funzioni esaminate finora. La legenda dei simboli usati :
ogni contenitore indicato dalla sua iniziale (es.: v = vector)
a = contenitore associativo (escluso map)
C = "costo costante", L = "costo logaritmico", N = "non definita"
I = "inefficiente" (costo proporzionale al numero di elementi)
v d l m a q p s
dereferenziazione di un iteratore

C C C C C N N N

begin

C C C C C N N N

end

rbegin

rend

resize
size

C C C N N N N N
empty

C C C C C C C C

max_size
reserve

C C C C C N N N
capacity

C N N N N N N N

costruttore di default
costruttore di copia

C C C C C C C C
operator=

costruttore con dimensione

assign

I I I I I I I I
I I I N N N N N

costruttore tramite iteratori

I I I I I N N N

swap

C C C C C N N N

operator[]

C C N L N N N N

at

C C N N N N N N

front

back

C C C N N C N N

top

N N N N N N C C

push_front

pop_front

N C C N N N N N

push_back pop_back

C C C N N N N N

push

N N N N N C L C

pop

N N N N N C C C

insert
clear

erase

I I C L L N N N
C C C C C N N N

Metodi specifici di list


Come si desume dalla tabella, il contenitore list possiede tutte le funzionalit di
vector, escluse la "riserva" di memoria (reserve e capacity) e l'accesso via
indice (operator[] e at); in pi, pu eseguire, come deque, operazioni di
inserimento e cancellazione in testa (push_front e pop_front) ed pi
efficiente di vector e deque nelle operazioni di inserimento e
cancellazione in "mezzo" (insert e erase).
In aggiunta, sono definiti in list alcuni metodi specifici, che forniscono
operazioni particolarmente adatte alla manipolazione delle liste:

metodo splice, in 3 overloads:


void list::splice(iterator it, list& lst)
void list::splice(iterator it, list& lst, iterator first)
void list::splice(iterator it, list& lst, iterator first, iterator last)
il metodo splice "muove" degli elementi (cio li copia, cancellando gli
originari) dall'oggetto lst in *this, inserendoli prima dell'elemento di
*this puntato da it; nel primo overload vengono mossi tutti gli elementi
di lst (che resta vuoto); nel secondo, viene mosso solo l'elemento di lst
puntato da first; nel terzo, vengono mossi gli elementi contigui di lst,
puntati a partire da first fino a last escluso; ammesso che *this e lst
coincidano solo a condizione che il range degli elementi da muovere
non contenga it (e quindi non mai ammesso nel primo caso)
void list::reverse()
inverte gli elementi (cio scambia il primo con l'ultimo, il secondo con il
penultimo ecc...)
void list::sort()
ordina gli elementi in senso ascendente (esiste anche un overload in cui
si pu imporre la condizione d'ordine tramite un oggetto-funzione, ma ne
parleremo in generale quando tratteremo degli algoritmi; la stessa
considerazione vale anche riguardo ai successivi metodi di questo elenco)
void list::remove(const_reference val)
elimina tutti gli elementi che trova uguali a val

void list::merge(list& lst)


muove in *this tutti gli elementi di lst (che resta vuoto); se in entrambe
le liste gli elementi erano in ordine, si mantengono in ordine anche nella
lista risultante, altrimenti gli elementi vengono mescolati senza un ordine
definito
void list::unique()
elimina tutti gli elementi duplicati contigui (l'operazione ha senso solo se
la lista gia in ordine)

Metodi specifici dei contenitori associativi


Abbiamo visto che le classi template map e multimap hanno (almeno) due
parametri: la chiave (tipo key_type) e il valore mappato (tipo
mapped_type), definiti in quest'ordine. I loro elementi (tipo value_type)
sono invece specializzazioni della struttura template pair, con argomenti:
const key_type e value_type.
Le classi template set e multiset possono considerarsi dei contenitori
associativi "degeneri" con un solo parametro: la chiave (gli elementi sono
costituiti dalla chiave stessa, e quindi i tipi key_type e value_type sono
coincidenti, mentre mapped_type non esiste).
Tutti i contenitori associativi possiedono iteratori bidirezionali, che (di
default) percorrono gli elementi in ordine crescente di chiave.
Dell'operatore di indicizzazione (definito solo in map) abbiamo gi detto;
aggiungiamo solo che non pu lavorare su mappe costanti, in quanto, se non
trova un elemento, lo crea. Per eseguire una ricerca senza modificare la mappa,
bisogna usare il metodo find (vedere pi avanti).
Per quello che riguarda l'operazione di inserimento di nuovi elementi, fermo
restando che in map il modo pi semplice e comune quello di usare
l'operatore di indicizzazione come l-value (con un nuovo valore della
chiave), in tutti i contenitori associativi si pu usare il metodo insert, i cui
overloads sono per diversi da quelli elencati nella tabella generale (al solito,
indicheremo con val l'elemento da inserire):

pair<iterator, bool> Cont::insert(const_reference val)


definito solo in map e set; "tenta" di inserire val, cercando se esiste
gi una chiave uguale a val.first (se in map), oppure uguale a val (se
in set); se la trova, non esegue l'inserimento; restituisce un oggetto di
pair, in cui first un iteratore che punta all'elemento (vecchio o nuovo)
con chiave val.first (o val se in set), e second true nel caso che
val sia stato effettivamente inserito
iterator Cont::insert(const_reference val)
come il precedente, salvo che inserisce val comunque e restituisce un
iteratore che punta al nuovo elemento inserito; definito solo in
multimap e multiset
iterator Cont::insert(iterator it,const_reference val)
identico nella forma all'overload definito nelle sequenze principali;
se ne differisce per il significato dell'argomento it, che non rappresenta
pi il punto dove inserire val (nei contenitori associativi ogni elemento
sempre inserito nella posizione d'ordine che gli compete), ma piuttosto il

punto dal quale iniziare la ricerca: se risulta che val deve essere inserito
immediatamente dopo it, l'operazione non pi a "costo logaritmico"
ma a "costo costante" (questo overload pu servire per inserire
rapidamente una sequenza di elementi gi ordinati, utilizzando in ogni
step il valore di ritorno come argomento it per lo step successivo)
void Cont::insert(Iter first, Iter last)
dove Iter un tipo iteratore definito in Cont o in un altro contenitore;
inserisce elementi generati mediante copia a partire dall'elemento
puntato da first fino all'elemento puntato da last escluso

Anche il metodo erase un p diverso, nel senso che fornisce un overload in


pi rispetto a quelli gi visti:
size_type Cont::erase(const key_type& k)
esegue la ricerca degli elementi con chiave k e, se li trova, li cancella;
restituisce il numero degli elementi cancellati (che pu essere 0 se non ne ha
trovato nessuno, e pu essere maggiore di 1 solo in multimap e multiset)
Infine, esistono alcuni metodi definiti solo nei contenitori associativi (per
ognuno di essi esiste anche, ma tralasciamo di indicarla, la versione per gli
oggetti const):

iterator Cont::find(const key_type& k)


restituisce un iteratore che punta al primo elemento con chiave k; se
non ne trova, restituisce end()
iterator Cont::lower_bound(const key_type& k)
esegue in pratica la stessa operazione di find
iterator Cont::upper_bound(const key_type& k)
restituisce un iteratore che punta al primo elemento con chiave
maggiore di k; se non ne trova, restituisce end()
pair<iterator,iterator> Cont::equal_range(const key_type& k)
restituisce una coppia di iteratori in cui first uguale al valore di
ritorno di lower_bound e second uguale al valore di ritorno di
upper_bound
size_type Cont::count(const key_type& k)
restituisce il numero degli elementi con la stessa chiave k

Il metodo find usato preferibilmente in map e set; gli altri hanno senso solo
se usati in contenitori con chiave duplicata (cio in multimap e multiset)
Funzioni esterne
In tutti gli header-files in cui sono definite le classi dei contenitori, anche
definito un insieme (sempre uguale) di funzioni esterne di "appoggio". Abbiamo
gi visto la funzione swap. Le altre sono costituite dal set completo degli
operatori relazionali, che servono per confrontare fra loro oggetti
contenitori. Le regole applicate sono le seguenti:

due oggetti contenitori sono uguali (operator==) se hanno la stessa


dimensione e tutti gli elementi corrispondenti sono uguali (e quindi
necessario che anche nel tipo degli elementi sia definito operator==);
dati due oggetti contenitori, a e b, si definisce a minore di b
(operator<) se a precede b nell'ordinamento "lessicografico", cio se:

1. tutti gli elementi corrispondenti sono uguali e la dimensione di a


minore della dimensione di b, oppure
2. indipendentemente dalla dimensione di a e di b, il primo
elemento di a non uguale al corrispondente elemento di b
minore del corrispondente elemento di b (e quindi necessario
che anche nel tipo degli elementi sia definito operator<)
notare che l'ordine alfabetico un tipico ordinamento lessicografico (in
cui i contenitori sono le parole e gli elementi sono le lettere di ogni
parola)
Gli altri operatori relazionali sono ricavati da operator== e operator< e
precisamente:
operator!=(a,b)

ritorna ...

!(operator==(a,b))

operator>(a,b)

ritorna ...

operator<(b,a)

operator<=(a,b)

ritorna ...

!(operator<(b,a))

operator>=(a,b)

ritorna ...

!(operator<(a,b))

Pertanto, per le operazioni di confronto fra contenitori, necessario che nel


tipo degli elementi siano definiti entrambi operator< e operator== (gli altri
non servono); ricordiamo che invece per le operazioni di ordinamento degli
elementi sufficiente che sia definito operator<

Algoritmi e oggetti-funzione

Algoritmi e sequenze
La STL mette a disposizione una sessantina di funzioni template, dette
"algoritmi" e definite nell'header-file <algorithm>.
Gli algoritmi operano sui contenitori, o meglio, su sequenze di dati. Fra gli
argomenti di ingresso di un algoritmo sempre presente almeno una coppia di
iteratori (di tipo parametrizzato) che definiscono e delimitano una sequenza:
il primo iteratore punta al primo elemento della sequenza, il secondo
iteratore punta alla posizione che segue immediatamente l'ultimo elemento.
Una tale sequenza detta "semi-aperta", in quanto contiene il primo estremo
ma non il secondo; una sequenza semi-aperta permette di utlizzare gli
algoritmi senza dover specificare il caso particolare di una sequenza vuota.
L'intervallo (range) individuato da una sequenza semi-aperta spesso riferito
nella documentazione con la scritta:
[primo iteratore,secondo iteratore)
dove la diversit grafica delle parentesi indica appunto che il primo estremo
appartiene all'intervallo e il secondo estremo no.

Nella chiamata di un algoritmo (che normalmente coincide con la sua


istanziazione, con deduzione implicita degli argomenti del template) gli
argomenti che esprimono i due iteratori devono essere dello stesso tipo
(diversamente il compilatore produre un messaggio di errore). A parte questa
limitazione (peraltro ovvia), gli algoritmi sono perfettamente generici, nel senso
che possono operare su qualsiasi tipo di contenitore (e su qualsiasi tipo degli
elementi), purch provvisto di iteratori; anzi, proprio perch agiscono
attraverso gli iteratori, alcuni algoritmi possono funzionare altrettanto bene su
classi di dati, come le stringhe e le classi di input-output, che non sono
propriamente contenitori, ma che hanno in comune la propriet di definire
sequenze espresse in termini di iteratori. Inoltre, la maggior parte degli
algoritmi funziona anche su normali array (in questo caso, al posto degli
iteratori, bisogna mettere i puntatori, mantenendo per la regola della
sequenza semi-aperta).
Pertanto, la definizione pi comune di un algoritmo (che indichiamo
genericamente con fun) :
template <class Iter, ......> (tipo di ritorno) fun(Iter first, Iter last,
......)
dove Iter il tipo dell'iteratore associato alla sequenza di ingresso e first e
last rappresentano gli estremi della sequenza. Gli altri parametri del
template e gli altri argomenti dell'algoritmo sono costituiti di solito da altri
iteratori (di ingresso o di uscita), da valori di dati o da oggetti-funzione. Se un
algoritmo coinvolge due sequenze, i cui corrispondenti tipi iteratori sono
individuati da due parametri distinti, i tipi delle due sequenze non devono
essere necessariamente gli stessi, purch coincidano i tipi degli elementi (o uno
dei due sia convertibile implicitamente nell'altro).
Oggetti-funzione
Abbiamo gi introdotto il concetto di oggetto-funzione trattando degli
operatori in overload: gli oggetti-funzione appartengono a classi che hanno
la particolare caratteristica di utilizzare in modo predominante un loro metodo,
definito come operatore di chiamata di una funzione:
operator() (lista di argomenti)
il che permette di fornire la normale sintassi della chiamata di una funzione a
oggetti di una classe.
Consideriamo ora il caso di una funzione (la chiamiamo fun) che preveda di
eseguire un certo numero di operazioni, non definite a priori, ma da selezionare
fra diverse operazioni possibili. Occorre pertanto che tali operazioni siano
trasmesse come argomenti di chiamata di fun. Il C risolve il problema
utilizzando i puntatori a funzione: fun definisce fra i suoi argomenti un
puntatore a funzione; questo viene sostituito, in ogni chiamata di fun, con la
funzione "vera" che esegue le operazioni volute. Ma il C++ "pu fare di
meglio"! Infatti i puntatori a funzione potrebbero, in certi casi, rivelarsi
inadeguati, per i seguenti motivi:

la risoluzione di un puntatore a funzione un'operazione "costosa", in


quanto il programma deve ogni volta accedere a una tabella di puntatori;
se una funzione chiamata pi volte, potrebbero esserci informazioni da
conservare o aggiornare; per cui, o si includono tutte queste informazioni

nella lista degli argomenti, o si definiscono allo scopo delle variabili


globali ("brutto", in entrambi i casi!);
la scelta comunque confinata entro un insieme di funzioni predefinite.

Il C++ consente di evitare questi inconvenienti, se, al posto di un puntatore a


funzione, si inserisce, come argomento di fun, un oggetto-funzione di tipo
parametrizzato. Infatti:

la chiamata della funzione (attraverso il metodo operator(), definito


nella classe dell'oggetto-funzione) eseguita pi velocemente, in
quanto non deve accedere a tabelle (oltretutto operator() pu, in certi
casi, essere definito inline);
le informazioni aggiuntive, da conservare o aggiornare, possono essere
memorizzate nei membri definiti nella stessa classe dell'oggettofunzione;
poich la suddetta classe un parametro di template, non esiste
nessun vincolo predefinito sulla scelta della funzione da eseguire (purch
il numero e il tipo dei suoi argomenti sia quello previsto).

Molti algoritmi utilizzano gli oggetti-funzione come argomenti (e le


corrispondenti classi come parametri). L'utente pu chiamare questi
algoritmi fornendo una propria classe come argomento del template; tale
classe deve contenere il metodo operator() (con al massimo due argomenti),
che ha il compito di eseguire le operazioni desiderate sugli elementi di una data
sequenza.
In aggiunta a quelli definiti dall'utente, la STL mette a disposizione un nutrito
numero di oggetti-funzione, le cui classi sono definite nell'header-file
<functional>. Molte di queste classi trasformano sostanzialmente operazioni
in funzioni, in modo da renderle utilizzabili come argomenti negli algoritmi (
il processo logico inverso a quello che porta alla definizione degli operatori in
overload). Nello stesso header-file sono anche definite alcune classi e
funzioni (dette adattatori) che trasformano oggetti-funzione in altri oggettifunzione, sempre allo scopo di renderli utilizzabili negli algoritmi. Non
approfondiremo oltre questo argomento, la cui trattazione, piuttosto complessa,
esula dagli intendimenti di questo corso; ci limiteremo a citare alcuni casi
particolari, quando se ne presenter l'occasione.
For_each
Un vantaggio chiave nell'uso degli algoritmi e degli oggetti-funzione consiste
nella possibilit offerta al programmatore di "risparmiare codice" (e quindi di
"risparmiare errori"!), evitandogli la necessit di scrivere cicli espliciti, che sono
invece eseguiti automaticamente con una sola istruzione. Per comprendere bene
tale vantaggio, consideriamo l'algoritmo "pi generico che esista", for_each, il
quale non fa altro che eseguire "qualcosa" su ogni elemento di una sequenza
(e il "qualcosa" deciso dall'utente). Il codice di implementazione di questo
algoritmo il seguente:
template <class Iter, class Op> Op for_each(Iter first, Iter last, Op
oggf)
{

while (first != last) oggf (*first++);


return oggf;
}
notare che for_each non si interessa di sapere cosa sia realmente il suo terzo
argomento, ma si limita ad applicargli l'operatore (); spetter poi al
compilatore controllare, in ogni punto di istanziazione di for_each, che:
1. nella classe che sostituisce il parametro Op sia definito il
metodo operator();
2. operator() abbia un solo argomento;
3. il tipo dell'argomento di operator() coincida con il tipo dell'elemento
puntato dal tipo iteratore che sostituisce il parametro Iter.
Inoltre, notare che:

for_each ritorna lo stesso oggetto-funzione, per permettere al


chiamante di accedere alle eventuali altre informazioni memorizzate nei
suoi membri;
il terzo argomento pu anche essere una normale funzione, nel qual
caso il valore di ritorno di for_each non ha significato.

Predicati
Un "predicato" un oggetto-funzione che ritorna un valore di tipo bool. Gli
algoritmi fanno molto uso dei predicati, il cui compito spesso di definire criteri
d'ordine alternativi a operator<, oppure di determinare, in base al valore di
ritorno true o false, l'esecuzione o meno di certe operazioni. Per esempio si
possono selezionare, tramite un predicato, solo gli elementi di una sequenza
maggiori di un certo valore. In sostanza, come abbiamo gi visto per for_each, i
predicati servono a risparmiare codice, sostituendo la sola chiamata di un
algoritmo alla scrittura delle istruzioni di un ciclo, contenente al suo interno
costrutti if o altre istruzioni di controllo.
I predicati sono addirittura indispensabili in tutte quelle operazioni che
coinvolgono ordinamenti e confronti fra tipi nativi gestiti da puntatori: in
questo caso l'applicazione di default degli operatori < e == ai puntatori
darebbe luogo a risultati errati.
Algoritmi che non modificano le sequenze
Alcuni algoritmi eseguono operazioni di ricerca, selezione, confronto e
conteggio e non possono modificare gli elementi delle sequenze su cui operano
(i loro argomenti iteratori sono definiti const).
Per ogni algoritmo, esistono sempre due versioni: quella con predicato e quella
senza predicato; di solito la versione senza predicato una parziale

specializzazione della prima, dove il predicato :


elemento == valore
A volte le due versioni hanno lo stesso nome e a volte no. Hanno lo stesso nome
solo quando il numero degli argomenti diverso e quindi la risoluzione
dell'overload non pu generare ambiguit (non dimentichiamo che i tipi degli
argomenti sono parametri di template e quindi potrebbero esserci delle
specializzazioni con i rispettivi tipi coincidenti, generando ambiguit nel caso
che il numero degli argomenti sia uguale). Quando le due versioni non hanno lo
stesso nome, quella con predicato prende il nome dell'altra seguito dal suffisso
_if
Nell'esposizione che segue useremo le seguenti convenzioni:

siccome tutti gli algoritmi sono funzioni template, ometteremo il


prefisso (sempre presente):
template <class ......>
nella definizione di ogni algoritmo; per capire quali siano i suoi
parametri, indicheremo i loro nomi con il colore viola, e in particolare:
o Iter, Iter1, Iter2 saranno parametri di tipi iteratori;
o T sar il parametro del tipo degli elementi;
o Pred sar il parametro di un tipo predicato
nella descrizione di ogni algoritmo adotteremo la notazione della
sequenza semi-aperta:
[primo estremo, secondo estremo)
e useremo le operazioni aritmetiche + e - sugli iteratori (lo faremo per
comodit di esposizione, anche se sappiamo che tali operazioni sono
applicabili solo alla categoria degli iteratori ad accesso casuale, che non
sono in genere quelli utilizzati dagli algoritmi)

Gli algoritmi della "famiglia" find scorrono una sequenza, o una coppia di
sequenze, cercando un valore che verifichi una determinata condizione:
Iter find(Iter first, Iter last, const T& val)
Iter find_if(Iter first, Iter last, Pred pr)
cerca il primo valore di un iteratore it nel range [first, last) tale che risulti true:
nel primo caso e ...
*it == val
pr(*it)
nel secondo caso;
ritorna it se lo trova, oppure last se non lo trova.
Iter find_first_of(Iter1 first1, Iter1 last1, Iter2 first2, Iter2 last2)
Iter find_first_of(Iter1 first1, Iter1 last1, Iter2 first2, Iter2 last2, Pred pr)
cerca il primo valore di un iteratore it1 nel range [first1, last1) tale che risulti true:
*it1 == *it2 nel primo caso e ...
pr(*it1, *it2) nel secondo caso
dove it2 un qualunque valore di un iteratore nel range [first2, last2);
ritorna it1 se lo trova, oppure last1 se non lo trova.
Iter adjacent_find(Iter first, Iter last)
Iter adjacent_find(Iter first, Iter last, Pred pr)
cerca il primo valore di un iteratore it nel range [first, last-1) tale che risulti true:
*it == *(it+1) nel primo caso e ...
pr(*it, *(it+1)) nel secondo caso;
ritorna it se lo trova, oppure last se non lo trova.

Gli algoritmi count e count_if contano le occorrenze di un valore in una


sequenza:
unsigned int count(Iter first, Iter last, const T& val)
unsigned int count_if(Iter first, Iter last, Pred pr)
incrementa un contatore n (inizialmente zero) per ogni valore di un iteratore it nel
range [first, last) tale che risulti true:
nel primo caso e ...
*it == val
pr(*it)
nel secondo caso;
ritorna n.
Gli algoritmi equal e mismatch confrontano due sequenze:
bool equal(Iter1 first1, Iter1 last1, Iter2 first2)
bool equal(Iter1 first1, Iter1 last1, Iter2 first2, Pred pr)
ritorna true solo se, per ogni valore dell'intero N nel range [0, last1-first1) risulta
true:
*(first1+N) == *(first2+N) nel primo caso e ...
pr(*(first1+N), *(first2+N)) nel secondo caso.
pair<Iter1,Iter2> mismatch(Iter1 first1, Iter1 last1, Iter2 first2)
pair<Iter1,Iter2> mismatch(Iter1 first1, Iter1 last1, Iter2 first2, Pred pr)
cerca il pi piccolo valore dell'intero N nel range [0, last1-first1) tale che risulti
false:
*(first1+N) == *(first2+N) nel primo caso e ...
pr(*(first1+N), *(first2+N)) nel secondo caso;
se non lo trova pone N = last1-first1;
ritorna pair(first1+N,first2+N).
NOTA
la seconda sequenza specificata solo dal primo estremo: ci significa che il numero
dei suoi elementi deve essere almeno uguale al numero degli elementi della prima
sequenza; questa tecnica usata in tutti gli algoritmi in cui si utlizzano due sequenze
con operazioni che coinvolgono le coppie degli elementi corrispondenti.

Una classe C++ per le stringhe


La classe string

La Libreria Standard del C++ mette a disposizione una classe per la gestione
delle stringhe, non come array di caratteri (come le stringhe del C), ma come
normali oggetti (e quindi, per esempio, trasferibili per copia, a differenza delle
stringhe del C, nelle chiamate delle funzioni). Questa classe si chiama
string ed definita nell'header file <string>.
Per la verit, il nome string non altro che un sinonimo (definito con
typedef) di:
basic_string<char>
dove basic_string una classe template con tipo di carattere generico, e
quindi string una specializzazione di basic_string con argomento char.
Ma poich, come abbiamo gi detto nel capitolo di introduzione alla Libreria, a
noi interessano solo i caratteri di tipo char, ignoreremo la classe template da
cui string proviene e tratteremo string come una classe specifica (non
template).
Da un altro punto di vista, pi vicino agli interessi dell'utente, string pu essere
considerata come un "contenitore specializzato", e in particolare "somiglia"
molto a vector<char>. Possiede quasi tutte le funzionalit di vector, con alcune
(poche) caratteristiche in meno e altre (molte) caratteristiche in pi; quest'ultime
servono soprattutto per eseguire le operazioni specifiche di manipolazione delle
stringhe (come per esempio la concatenazione).
In particolare, come gli elementi di vector, anche i caratteri di string possono
essere considerati come facenti parte di una sequenza, e quindi string definisce
gli stessi iteratori di vector e della stessa categoria (ad accesso casuale).
Ci rende possibile l'applicazione di tutti gli algoritmi generici della STL anche a
string, tramite i suoi iteratori. Questo fatto indubbiamente un vantaggio, ma
non cos grande come potrebbe sembrare. Infatti gli algoritmi generici sono
pensati principalmente per strutture i cui elementi sono significativi anche se
presi singolarmente, il che non generalmente vero per le stringhe. Per
esempio, ordinare una stringa non ha senso (e quindi gli algoritmi di
ordinamento o di manipolazione di sequenze ordinate sono poco utili se
applicati alle stringhe). L'attenzione maggiore va invece concentrata sui metodi
di string, alcuni dei quali sono implementati in modo da ottenere
un'ottimizzazione pi spinta di quanto non sia possibile nel caso generale.

Confronto fra string e vector<char>

In questa sezione elencheremo le funzionalit comuni a string e vector, e, separatamente, i


metodi di vector non presenti in string. Nelle sezioni successive tratteremo esclusivamente
delle funzioni-membro e delle funzioni esterne specifiche di string. Per il significato dei
nomi, e per la descrizione dei tipi e delle funzioni, vedere il capitolo: La Standard
Template Library, sezione: Contenitori Standard.
Tipi definiti in string
Nell'ambito della classe string sono definiti gli stessi tipi definiti in vector e
in particolare (citiamo i pi importanti): iterator, const_iterator,
reverse_iterator, const_reverse_iterator, difference_type, value_type,
size_type, reference, const_reference
Funzioni-membro comuni
I seguenti metodi, gi descritti nella trattazione dei contenitori, sono definiti
sia in vector che in string, hanno la stessa sintassi di chiamata e svolgono le
medesime operazioni (se vector specializzato con con argomento char):
dereferenziazione di un iteratore
begin

end

rbegin

rend

resize
size

empty

max_size
reserve

capacity

costruttore di default
costruttore di copia

operator= assign

costruttore e assign tramite iteratori


swap
operator[] (accesso non controllato)
at (accesso controllato)
insert

erase

Note:

come gli oggetti di vector, anche quelli di string possono utilizzare i


metodi operator[] e at per accedere ai propri elementi (i singoli
caratteri) tramite indice;
c' una piccola differenza fra i due metodi assign di vector e quelli di
string: i primi ritornano void, mentre i secondi ritornano string&;
a proposito del metodo size, definito in string anche il metodo
length, che fa esattamente la stessa cosa.

Funzioni esterne comuni

Tutte le funzioni esterne di "appoggio" definite nell'header file <vector>


sono anche definite nell'header file <string>; ricordiamo che queste funzioni
sono: swap, operator==, operator!=, operator<, operator<=,
operator>, operator>=. Ognuna di esse ha due argomenti, che nelle
funzioni definite nell'header file <string> sono ovviamente di tipo string.
Funzioni-membro di vector non presenti in string
Un numero molto ridotto di metodi di vector non ridefinito in string:

Costruttore con un 1 argomento


Non ammesso inizializzare una stringa fornendole solo la
dimensione. Per esempio:
string str(7);
un'istruzione errata;
invece possibile inizializzare una stringa fornendole la dimensione e
il carattere di "riempimento". Per esempio:
string str(7,'a'); ok, genera: "aaaaaaa";
in pratica il secondo argomento, che in vector opzionale, in string
obbligatorio
Operazioni in testa e in coda
i seguenti metodi di vector non esistono in string: front,
back, push_back, pop_back
Metodo clear
in compenso esiste un ulteriore overload del metodo erase che esegue
la stessa operazione

Il membro statico npos

La classe string dichiara il seguente dato-membro "atipico":


static const size_type npos;
che inizializzato con il valore -1. Poich d'altra parte il tipo size_type
sempre unsigned, la costante string::npos contiene in realt il massimo
numero positivo possibile. Viene usato come argomento di default di alcune
funzioni-membro o come valore di ritorno "speciale" (per esempio per
indicare che un certo elemento non stato trovato). In pratica npos
rappresenta un indice che "non pu esistere", in quanto maggiore di tutti gli
indici possibili. In un certo senso svolge le stesse funzioni del terminatore nelle
stringhe del C, che non esiste negli oggetti di string (il carattere '\0' pu
essere un elemento di string come tutti gli altri).
Come vedremo, i metodi di string che utilizzano gli indici come argomenti
spesso fanno uso di npos per indicare la fine della stringa.

Costruttori e operazioni di copia

Oltre ai 4 costruttori gi visti (default, copia da oggetto string, copia


tramite iteratori e inizializzazione con carattere di "riempimento"), string
definisce i seguenti costruttori specifici:
string::string(const string& str, size_type ind, size_type n=npos)
copia da str, a partire dall'elemento con indice ind, per n elementi o fino al termine
di str (quello che "arriva prima")
string::string(const char* s)
copia caratteri, a partire da quello puntato da s e fino a quando incontra il carattere
'\0' (escluso); in pratica copia una stringa del C (che pu anche essere una costante
literal)
string::string(const char* s, size_type n)
come sopra, salvo che copia solo n caratteri (se prima non incontra '\0')
Per quello che riguarda le copie in oggetti di string gi esistenti, oltre
all'operatore di assegnazione standard e alle due versioni del metodo assign
(copia tramite iteratori e copia con carattere di "riempimento") presenti anche
in vector, string definisce ulteriori overloads (in tutte le seguenti operazioni
l'oggetto esistente viene cancellato e sostituito da quello ottenuto per copia):
string& string::operator=(const char* s)
copia una stringa del C
string& string::operator=(char c)
copia un singolo carattere; nota: l'assegnazione di un singolo carattere ammessa,
mentre l'inizializzazione non lo
string& string::assign(const string& str)
esegue le stesse operazioni dell'operatore di assegnazione standard
string& string::assign(const string& str, size_type ind, size_type n)
esegue le stesse operazioni del costruttore con uguali argomenti
string& string::assign(const char* s)
esegue le stesse operazioni dell'operatore di assegnazione con uguale argomento
string& string::assign(const char* s, size_type n)
esegue le stesse operazioni del costruttore con uguali argomenti

Gestione degli errori

Abbiamo detto che, come in vector, operator[] non controlla che l'argomento
indice sia compreso nel range [0,size()), mentre il metodo at effettua il
controllo e genera un'eccezione di tipo out_of_range in caso di errore.
Molti altri metodi di string hanno, fra gli argomenti, due tipi size_type
consecutivi, di cui il primo rappresenta un indice (che ha il significato di
"posizione iniziale"), mentre il secondo rappresenta il numero di caratteri "da
quel punto in poi" (abbiamo gi visto cos fatti un costruttore e un metodo
assign). In tutti i casi il primo argomento sempre controllato (generando la
solita eccezione se l'indice non nel range), mentre il secondo non lo mai e
quindi un numero di caratteri troppo alto viene semplicemente interpretato come
"il resto della stringa" (che in particolare l'unica interpretazione possibile se il
valore del secondo argomento npos). Notare che, se la "posizione iniziale" e/o
il numero di caratteri sono dati come numeri negativi, questi vengono convertiti
in valori positivi molto grandi (essendo size_type un tipo unsigned), e quindi,
per esempio:
string(str,2,3);

genera out_of_range

string(str,3,2);

va bene: costruisce un oggetto string per copia da str, a


partire dal quarto carattere fino al termine

I metodi per la ricerca di sotto-stringhe (che vedremo pi avanti) restituiscono


npos in caso di insuccesso, ma non generano eccezioni; se per il programma
dell'utente non controlla il valore di ritorno e lo usa direttamente come
argomento di "posizione" nella chiamata di un'altra funzione, allora s che, in
caso di insuccesso nella ricerca, si genera un'eccezione out_of_range.
I metodi che usano una coppia di iteratori al posto della coppia "posizionenumero" non effettuano nessun controllo (e lo stesso discorso vale per gli
algoritmi, come sappiamo) e quindi spetta al programma dell'utente assicurare
che i limiti del range non vengano oltrepassati.
La stessa cosa dicasi quando la coppia di argomenti "posizione-numero" si
riferisce a una stringa del C: anche qui non viene eseguito nessun controllo (a
parte il controllo sul terminator che viene riconosciuto come fine della stringa)
e quindi bisogna porre la massima attenzione sull'argomento che rappresenta la
"posizione iniziale" (che in questo caso un puntatore a char): anzitutto deve
essere diverso da NULL (altrimenti il programma abortisce) e in secondo luogo
deve realmente puntare a un carattere interno alla stringa.
Un altro tipo di errore (comune anche a vector), molto raro, che genera
un'eccezione di tipo length_error, avviene quando si tenta di costruire una
stringa pi lunga del massimo consentito (dato da max_size). Lo stesso errore
generato se si tenta di superare max_size chiamando un metodo che modifica
la dimensione direttamente (resize) o implicitamente (insert, append,
replace, operator+=), oppure che modifica la capacit (reserve).

Conversioni fra oggetti string e stringhe del C

La conversione da una stringa del C (che indichiamo con s) a un oggetto string


(che indichiamo con str) si ottiene semplicemente assegnando s a str (con
operator= o con il metodo assign), oppure costruendo str per copia da s.
Ovviamente, se si vuole eseguire la conversione inversa, da oggetto string a
stringa del C, non si pu semplicemente invertire gli operandi
nell'assegnazione, in quanto il tipo nativo char* non consente assegnazioni
da oggetti string. Bisogna invece ricorrere ad alcuni metodi definiti nella
stessa classe string. Questi metodi sono 3 e precisamente:
1. const char* string::data() const
scrive i caratteri di *this in un array di cui restituisce il puntatore.
L'array gestito internamente a string e perci non va preallocato n
cancellato. L'oggetto *this non pu essere modificato, nel senso che una
sua successiva modifica invalida l'array, n possono essere modificati i
caratteri dello stesso array (in pratica il metodo data pu operare solo
su oggetti costanti). Non viene aggiunto il terminator alla fine
dell'array e quindi non possibile utilizzare l'array come argomento
nelle funzioni che operano sulle stringhe (in sostanza proprio un array
di caratteri , non una stringa!)
2. const char* string::c_str() const
identico a data, salvo il fatto che aggiunge il terminator alla fine,
creando cos un array di caratteri null terminated, cio una "vera"
stringa del C
3. size_type copy(char* s, size_type n, size_type pos = 0) const
copia n caratteri di *this, a partire dal carattere con indice pos,
nell'array s, preallocato dal chiamante. Restituisce il numero di caratteri
effettivamente copiati. Non aggiunge il terminator alla fine dell'array.
Per copiare tutti i caratteri di *this si pu usare string::npos come
secondo argomento e omettere il terzo.
Da un esame critico dei tre metodi sopracitati, si pu osservare che:
1. data "quasi" inutilizzabile (pu servire solo quando si trattano array di
caratteri e non stringhe)
2. c_str invece molto utile, perch permette di inserire il suo valore di
ritorno come argomento nelle funzioni di Libreria del C che operano
sulle stringhe. Per esempio:
int m = atoi(str.c_str());
(nota: non esistono funzioni C++ che convertono stringhe di caratteri
decimali in numeri).
Tuttavia pu operare solo su oggetti costanti

3. copy ha il vantaggio di permettere la modifica dell'array copiato. Bisogna


per ricordarsi di aggiungere un carattere '\0' in fondo (e bisogna anche
evitare che lo stesso carattere sia presente all'interno della stringa da
copiare)

Confronti fra stringhe

Per confrontare due oggetti string, o un oggetto string e una stringa del C,
la classe string fornisce il metodo compare, con vari overloads. Il valore di
ritorno sempre di tipo int ed ha il seguente significato:

0, se le due stringhe sono identiche;


un numero negativo se *this precede lessicograficamente la stringaargomento;
un numero positvo se *this segue lessicograficamente la stringaargomento.

Rispetto agli operatori relazionali, il metodo compare ha quindi il vantaggio


di restituire il risultato di <, == o > con una sola chiamata. I suoi overloads
sono:

int compare(const string& str) const


confronta *this con l'oggetto string str
int compare(const char* s) const
confronta *this con la stringa del C s
int compare(size_type ind, size_type n, const string& str) const
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"
ind-n, con l'oggetto string str
int compare(size_type ind, size_type n, const string& str,
size_type ind1, size_type n1) const
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"
ind-n, con la sotto-stringa dell'oggetto string str, data dalla coppia
"posizione-numero" ind1-n1
int compare(size_type ind, size_type n, const char* s, size_type
n1=npos) const
confronta la sotto-stringa di *this, data dalla coppia "posizione-numero"
ind-n, con i primi n1 caratteri della stringa del C s

L'utente non pu fornire un criterio di confronto specifico; se lo vuol fare, non


deve usare compare, ma l'algoritmo lexicographical_compare con un
predicato. Per esempio:
lexicographical_compare(s1.begin(),s1.end(),s2.begin(),s2.end(),nocas
e);

restituisce true se la stringa s1 precede la stringa s2 in base al criterio di


confronto dato dalla funzione nocase (fornita dall'utente).
Nell'header-file <string> si trovano varie funzioni esterne di "appoggio" che
implementano diversi overloads degli operatori relazionali: <, <=, ==, !=,
>, >=; per ognuno di essi esistono tre versioni: quella presente anche in
<vector> e negli header-files degli altri contenitori, in cui entrambi gli
operandi sono della stessa classe contenitore (in questo caso string) e quelle
in cui rispettivamente il primo o il secondo operando di tipo const char* (cio
una stringa del C). Questo permette di confrontare indifferentemente due
oggetti string, o un oggetto string e una stringa del C, o una stringa del C
e un oggetto string. In particolare la stringa del C pu essere una costante
literal. Esempio:
if (str == "Hello") .....

Concatenazioni e inserimenti

Concatenare due stringhe significa scrivere le due stringhe l'una di seguito


all'altra in una terza stringa.
Nell'header-file <string> si trovano varie funzioni esterne di "appoggio" che
implementano diversi overloads dell'operatore +, il quale fornisce la stringa
concatenata, date due stringhe come operandi; di queste, una sempre di
tipo const string&, mentre l'altra pu essere ancora di tipo const string&,
oppure di tipo const char* (cio una stringa del C), oppure di tipo char (cio
un singolo carattere). Mantenendo la convenzione simbolica che abbiamo usato
finora, riteniamo a questo punto che la descrizione delle funzioni possa essere
omessa (quando autoesplicativa gi in base ai tipi e ai nomi convenzionali degli
argomenti):

string
string
string
string
string

operator+(const string& str1, const string& str2)


operator+(const string& str, const char* s)
operator+(const char* s, const string& str)
operator+(const string& str, char c)
operator+(char c, const string& str)

Per l'operazione di somma e assegnazione in notazione compatta, sono


disponibili tre metodi che implementano altrettanti overloads dell'operatore
+=. In questo caso la stringa concatenata la stessa di partenza (*this) a cui
viene aggiunta in coda la stringa-argomento:

string& string::operator+=(const string& str)


string& string::operator+=(const char* s)
string& string::operator+=(char c)

Il metodo append esegue la stessa operazione di operator+=, con il


vantaggio che gli argomenti possono essere pi di uno. Ne sono forniti vari
overloads:

string& string::append(const string& str)


string& string::append(const string& str, size_type ind, size_type
n)
string& string::append(const char* s)
string& string::append(const char* s, size_type n)
string& string::append(size_type n, char c)
appende n volte il carattere c
string& string::append(Iter first, Iter last)

Per quello che riguarda l'inserimento di caratteri "in mezzo" a una stringa
(operazione di bassa efficienza, come in vector), sono disponibili ulteriori
overloads del metodo insert (oltre a quelli comuni con vector); tutti
inseriscono caratteri prima dell'elemento di *this con indice pos e
restituiscono by reference lo stesso *this:

string& string::insert(size_type pos, const string& str)


string& string::insert(size_type pos, const string& str, size_type
ind, size_type n)
string& string::insert(size_type pos, const char* s)
string& string::insert(size_type pos, const char* s, size_type n)
string& string::insert(size_type pos, size_type n, char c)
inserisce n volte il carattere c

Ricerca di sotto-stringhe

Nella classe string sono definiti molti metodi che ricercano la stringaargomento come sotto-stringa di *this. Tutti restituiscono un valore di tipo
size_type, che, se la sotto-stringa trovata, rappresenta l'indice del suo
primo carattere; se invece la ricerca fallisce il valore restituito npos. Tutti i
metodi sono definiti const in quanto eseguono la ricerca senza modificare
l'oggetto.
Nell'elenco che segue, suddiviso in vari gruppi, l'argomento di nome pos
rappresenta l'indice dell'elemento di *this da cui iniziare la ricerca, mentre
l'argomento di nome n rappresenta il numero di caratteri della stringaargomento da utilizzare per la ricerca.
Cerca una sotto-stringa:

size_type string::find(const string& str, size_type pos=0) const


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)


const
size_type string::find(char c, size_type pos=0) const

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

size_type string::rfind(const string& str, size_type pos=npos)


const
size_type string::rfind(const char* s, size_type pos=npos) const
size_type string::rfind(const char* s, size_type pos, size_type n)
const
size_type string::rfind(char c, size_type pos=npos) const

Cerca il primo carattere di *this che si trova nella stringa-argomento:

size_type string::find_first_of(const string& str, size_type pos=0)


const
size_type string::find_first_of(const char* s, size_type pos=0)
const
size_type string::find_first_of(const char* s, size_type pos,
size_type n) const
size_type string::find_first_of(char c, size_type pos=0) const

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

size_type string::find_last_of(const string& str, size_type


pos=npos) const
size_type string::find_last_of(const char* s, size_type pos=npos)
const
size_type string::find_last_of(const char* s, size_type pos,
size_type n) const
size_type string::find_last_of(char c, size_type pos=npos) const

Cerca il primo carattere di *this che non si trova nella stringa-argomento:

size_type string::find_first_not_of(const string& str, size_type


pos=0) const
size_type string::find_first_not_of(const char* s, size_type pos=0)
const
size_type string::find_first_not_of(const char* s, size_type pos,
size_type n) const
size_type string::find_first_not_of(char c, size_type pos=0) const

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

size_type string::find_last_not_of(const string& str, size_type


pos=npos) const
size_type string::find_last_not_of(const char* s, size_type
pos=npos) const
size_type string::find_last_not_of(const char* s, size_type pos,
size_type n) const
size_type string::find_last_not_of(char c, size_type pos=npos)
const

Estrazione e sostituzione di sotto-stringhe

Il metodo substr crea una stringa estraendola da *this e la restituisce per


copia:
string string::substr(size_type pos=0, size_type n=npos) const
la stringa originaria non modificata; la nuova stringa coincide con la sottostringa di *this che parte dall'elemento con indice pos e contiene n caratteri.
Il metodo replace, definito con vari overloads, sotituisce una sotto-stringa
di *this con la stringa-argomento (o una sua sotto-stringa) e restituisce by
reference lo stesso *this. Il numero dei nuovi caratteri non deve
necessariamente coincidere con quello preesistente (la nuova sotto-stringa pu
essere pi lunga o pi corta di quella sostituita) e quindi il metodo replace, oltre
a modificare l'oggetto, pu anche modificarne la dimensione.
Nell'elenco che segue, i nomi degli argomenti hanno il seguente significato:

pos : "posizione iniziale" in *this


m : "numero di caratteri" in *this
ind : "posizione iniziale" nella stringa-argomento
n : "numero di caratteri" nella stringa-argomento
ib,ie : iteratori che delimitano la sotto-stringa in *this
n,c: carattere c ripetuto n volte

Metodi che definiscono la sotto-stringa da sostituire mediante la coppia


"posizione-numero":

string& string::replace(size_type pos, size_type m, const string&


str)
string& string::replace(size_type pos, size_type m, const string&
str, size_type ind, size_type n)
string& string::replace(size_type pos, size_type m, const char* s)
string& string::replace(size_type pos, size_type m, const char* s,
size_type n)
string& string::replace(size_type pos, size_type m, size_type n,
char c)

Metodi che definiscono la sotto-stringa da sostituire mediante una coppia di


iteratori:

string& string::replace(iterator ib, iterator ie, const string& str)


string& string::replace(iterator ib, iterator ie, const char* s)

string& string::replace(iterator ib, iterator ie, const char* s,


size_type n)
string& string::replace(iterator ib, iterator ie, size_type n, char c)
string& string::replace(iterator ib, iterator ie, Iter first, Iter last)

Per cancellare una sotto-stringa disponibile un ulteriore overload del


metodo erase (oltre a quelli comuni con vector):
string& string::erase(size_type pos=0, size_type n=npos)
notare che la chiamata di erase senza argomenti equivale alla chiamata di
clear in vector.

Operazioni di input-output

Nell'header-file <string> si trovano due funzioni esterne di "appoggio" che


implementano due ulteriori overloads degli operatori di flusso "<<"
(inserimento) e ">>" (estrazione), con right-operand di tipo string.
Pertanto, la lettura e la scrittura di un oggetto string si possono eseguire
semplicemente utilizzando gli operatori di flusso come per le stringhe del C.
In particolare la lettura "salta" (cio non inserisce nella stringa) i caratteri
bianchi e i caratteri speciali (che anzi usa come separatori fra una stringa e
l'altra). I caratteri "buoni" vengono invece immessi nella stringa l'uno dopo
l'altro a partire dalla "posizione" 0 e fino all'incontro di un separatore; la stringa
letta sostituisce quella memorizzata precedentemente, assumendo (in pi o in
meno) anche una nuova dimensione.
Per la lettura di una stringa che includa anche i caratteri bianchi e i caratteri
speciali, in <string> definita anche la funzione getline:
istream& getline(istream&, string& str, char eol='\n')
che estrae caratteri dal flusso di input e li memorizza in str; l'estrazione
termina quando incontrato il carattere eol, che viene rimosso dal flusso di
input ma non inserito in str. Omettendo il terzo argomento si ottiene
effettivamente la lettura di una intera "linea" di testo.
Il valore di ritono, di tipo riferimento a istream, permette di utilizzare la
chiamata di getline come left-operand di una o pi operazioni di
estrazione. Esempio:
getline(cin,str1,'\t') >> str2 >> str3 ;
legge tutti i caratteri fino al primo tabulatore (escluso), memorizzandoli in
str1, e poi legge due sequenze di caratteri delimitate da separatori e li
memorizza in str2 e str3 .

Librerie statiche e dinamiche in Linux

Introduzione
Un problema che si presenta comunemente nello sviluppo dei programmi che
questi tendono a diventare sempre pi complessi, il tempo richiesto per la loro
compilazione cresce di conseguenza, e la directory di lavoro sempre pi
affollata. E' proprio in questa fase che incominciamo a chiederci se non esista un
modo pi efficiente per organizzare i nostri progetti. Una possibilit che ci viene
offerta dai compilatori sono le librerie.

Librerie in ambiente Linux


Una libreria semplicemente un file contenente codice compilato che pu essere
successivamente incorporato come una unica entit in un nostro programma in
fase di linking; l'utilizzo delle librerie ci permettere di realizzare programmi pi
facili da compilare e mantenere. Di norma le librerie sono indicizzate, cos risulta
pi facile localizzare simboli (funzioni, variabili, classi, etc...) al loro interno. Per
questa ragione il link ad una libreria pi veloce rispetto al caso in cui i moduli
oggetto siano separati nel disco. Inoltre, quando usiamo una libreria abbiamo
meno files da aprire e controllare, e questo comporta un ulteriore aumento della
velocit del processo di link.
Nell'ambiente Linux (come nella maggior parte dei sistemi moderni) le librerie si
suddividono in due famiglie principali:

librerie statiche (static libraries)


librerie dinamiche o condivise (shared libraries)

Ognuna presenta vantaggi e svantaggi, ma tutte hanno una cosa in comune:


costituiscono un catalogo di funzioni, classi, etc..., che ogni programmatore pu
riutilizzare.

Un programma di prova
Prima di vedere come si costruiscono e si usano questi due tipi di librerie,
presentiamo un piccolo programma di prova che ci servir da esempio.
Il programma comprende una collezione di funzioni matematiche (myfuncs) ed
un gestore di errori (la classe ErrMsg):

main.cpp
myfuncs.h
myfuncs.cpp
errmsg.h
errmsg.cpp

Le funzioni ''div'' e ''log'' in sostanza ridefiniscono le operazioni di divisione e il


logaritmo decimale ma in aggiunta permettono una gestione delle eccezioni
tramite il meccanismo di throw-catch.
Il programma pu essere compilato in maniera ''convenzionale'' tramite
l'istruzione:

g++ -o prova main.cpp myfuncs.cpp


errmsg.cpp
L'eseguibile prova si aspetta sulla linea di comando due numeri e calcola in
sequenza il loro rapporto ed il logaritmo del primo:
./prova 10 3
3.33333
1
Queste operazioni vengono eseguite nel main del programma in un blocco try; se si verifica
una eccezione (nella fattispecie una divisione per zero o il logaritmo di un numero negativo) il
blocco catch invoca la funzione membro ErrMsg.print_message() ed il programma termina
con un messaggio di errore:
./prova -10 3
-3.33333
**Severe Error in "double log(double)":Invalid
argument.
Quitting now.

Librerie statiche
Le librerie statiche vengono installate nell'eseguibile del programma prima che
questo possa essere lanciato. Esse sono semplicemente cataloghi di moduli
oggetto che sono stati collezionati in un unico file contenitore. Le librerie statiche
ci permettono di effettuare dei link di programmi senza dover ricompilare il loro
codice sorgente. Per far girare il nostro programma abbiamo bisogno solo del suo
file eseguibile.

Come costruire una libreria statica


Per costruire una libreria statica bisogna partire dai moduli oggetto dei nostri
sorgenti.
g++ -c myfuncs.cpp errmsg.gcc

Una volta compilati i moduli myfuncs.o e errmsg.o, costruiamo la libreria statica


libmath_util.a con il programma di archiviazione ar:

ar r libmath_util.a myfuncs.o errmsg.o


Il comando ar invocato con la flag ''r'' crea la libreria (se ancora non esiste) e vi
inserisce (eventualmente rimpiazzandoli) i moduli oggetto. Nel scegliere il nome di
una libreria statica stata utilizzata la seguente convenzione: il nome del file della
libreria inizia con il prefisso ''lib'' e termina con il suffisso ".a".
Per verificare il contenuto della libreria possiamo usare
ar tv libmath_util.a
rw-r--r-- 223/100 18256 Dec 10 14:24 2003 errmsg.o
rw-r--r-- 223/100 23476 Dec 10 14:23 2003 myfuncs.o

Link con una libreria statica


Una volta creato il nostro archivio, vogliamo utilizzarlo in un programma. Per poter
effettuare il link ad una libreria statica, il compilatore g++ deve essere utilizzato in
questo modo:
g++ -o prova_s main.cpp -L. -lmath_util
Dove abbiamo chiamato l'eseguibile prova_s per ricordarci che stato ottenuto
tramite il link alla libreria statica. Notate che abbiamo omesso il prefisso ''lib'' e il
suffisso ''.a'' quando abbiamo immesso il nome della libreria nella linea di
comando con la flag "-l". Ci pensa il linker ad attaccare queste parti alla fine e
all'inizio del nome di libreria. Notate inoltre l'uso della flag ''-L.'' che dice al
compilatore di cercare la libreria anche nella directory in uso e non solo nelle
directory standard dove risiedono le librerie di sistema (per es. /usr/lib/).
Il processo di link inizia con il caricamento del modulo main.o in cui viene definita
la funzione main(). A questo punto il linker si accorge della presenza dei nomi di
funzioni div e log e della classe ErrMsg, utilizzate dalla funzione main() ma non
definite. Siccome viene fornito al linker il nome della libreria libmath_util.a,
viene fatta una ricerca nei moduli all'interno di questa libreria per cercare quelli in
cui sono definite queste entit. Una volta localizzati, questi moduli vengono
estratti dalla libreria ed inclusi nell'eseguibile del programma.
L'eseguibile prova_s contiene cos tutto il codice necessario al suo funzionamento
ed pronto per essere lanciato.

I limiti del meccanismo del link statico

Si deve precisare che il linker estrae dalla libreria statica solo i moduli
strettamente necessari alla compilazione del programma. Questo dimostra una
certa capacit di economizzare le risorse delle librerie. Pensiamo per a pi
programmi che utilizzano, magari per altri scopi, la stessa libreria statica. I
programmi utilizzano la libreria statica distintamente, cio ognuno ne possiede
una copia. Se questi devono essere eseguiti contemporaneamente nello stesso
sistema, i requisiti di memoria si moltiplicano di conseguenza solo per ospitare
funzioni assolutamente identiche.
Le librerie condivise forniscono un meccanismo che permette a una singola copia
di un modulo di codice di essere condivisa tra diversi programmi nello stesso
sistema operativo. Ci permette di tenere solo una copia di una data libreria in
memoria ad un certo istante.

Librerie condivise
Le librerie condivise (dette anche dinamiche) vengono collegate ad un programma
in due passaggi. In un primo momento, durante la fase di compilazione (Compile
Time), il linker verifica che tutti i simboli (funzioni, variabili, classi, e simili ...)
richieste dal programma siano effettivamente collegate o al programma o ad una
delle sue librerie condivise. In ogni caso i moduli oggetto della libreria
dinamica non vengono inseriti direttamente nel file eseguibile. In un
secondo momento, quando l'eseguibile viene lanciato (Run Time), un programma
di sistema (dynamic loader) controlla quali librerie dinamiche sono state collegate
al nostro programma, le carica in memoria, e le attacca alla copia del programma
in memoria.
La fase di caricamento dinamico rallenta leggermente il lancio del programma, ma
si ottiene il notevole vantaggio che, se un secondo programma collegato alla
stessa libreria condivisa viene lanciato, questo pu utilizzare la stessa copia della
libreria dinamica gi in memoria, con un prezioso risparmio delle risorse del
sistema. Per esempio, le librerie standard del C e del C++ sono delle librerie
condivise utilizzate da tutti i programmi C/C++.
L'uso di librerie condivise ci permette quindi di utilizzare meno memoria per far
girare i nostri programmi e di avere eseguibili molto pi snelli, risparmiando cos
spazio disco.

Come costruire una libreria condivisa


La creazione di una libreria condivisa molto simile alla creazione di una libreria
statica. Si compila una lista di oggetti e li si colleziona in un unico file. Ci sono
per due differenze importanti:
1. Dobbiamo compilare per "Position Independent Code" (PIC). Visto che al
momento della creazione dei moduli oggetto non sappiamo in quale
posizione della memoria saranno inseriti nei programmi che li useranno,
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


"-fPIC" nella fase di compilazione dei moduli oggetto.
2. Contrariamente alle librerie statiche, quelle dinamiche non sono file di
archivio. Una libreria condivisa ha un formato specifico che dipende
dall'architettura per la quale stata creata. Per generarla di usa o il
compilatore stesso con la flag "-shared" o il suo linker.
Consideriamo ancora una volta il nostro programma di prova. I comandi per la
creazione di una libreria condivisa possono presentarsi come segue:
g++ -fPIC -c myfuncs.cpp
g++ -fPIC -c errmsg.cpp
g++ -shared -o libmath_util.so myfuncs.o errmsg.o
Nel scegliere il nome di una libreria condivisa stata utilizzata la convenzione
secondo cui il nome del file della libreria inizia con il prefisso ''lib'' e termina con il
suffisso ".so''.
I primi due comandi compilano i moduli oggetto con l'opzione (fPIC) in maniera
tale che essi siano utilizzabili per una libreria condivisa (possiamo comunque
utilizzarli in un programma normale anche se sono stati compilati con PIC).
L'ultimo comando chiede al compilatore di generare la libreria dinamica.

Link con una libreria condivisa


Come abbiamo gi preannunciato l'uso di una libreria condivisa si articola in due
momenti: Compile time e Run Time. La parte di compilazione e semplice. Il link ad
una libreria condivisa avviene in maniera del tutto simile al caso di una libreria
statica
g++ -o prova_d main.cpp -L. -lmath_util
Dove abbiamo chiamato l'eseguibile prova_d per ricordarci che stato ottenuto
tramite il link alla libreria dinamica.
Se per proviamo a lanciare l'eseguibile otteniamo una sgradita sorpresa:
./prova_d -10 3
./prova_d: error while loading shared libraries:
libmath_util.so:
cannot open shared object file: No such file or
directory
Il dynamic loader non in grado di localizzare la nostra libreria!

Possiamo infatti usare il comando ldd per verificare le dipendenze delle librerie
condivise e scoprire che la nostra libreria non viene localizzata dal loader
dinamico:
ldd ./prova_d
libmath_util.so => not found
libstdc++.so.5 =>
/usr/lib/libstdc++.so.5 (0x40030000)
libm.so.6 => /lib/tls/libm.so.6
(0x400e3000)
libgcc_s.so.1 => /lib/libgcc_s.so.1
(0x40106000)
libc.so.6 => /lib/tls/libc.so.6
(0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2
(0x40000000)
Ci avviene perch la nostra libreria non risiede in una directory standard.

La variabile ambiente LD_LIBRARY_PATH


Ci sono diversi modi per specificare la posizione delle librerie condivise
nell'ambiente linux. Se avete i privilegi di root, una possibilit quella di
aggiungere il path della nostra libreria al file /etc/ld.so.conf per poi lanciare
/sbin/ldconfig . Ma se non avete l'accesso all'utente root, potete sfruttare la
variabile ambiente LD_LIBRARY_PATH per dire al dynamic loader dove cercare
la nostra libreria:
setenv LD_LIBRARY_PATH
/home/murgia/C++/
ldd ./prova_d
libmath_util.so =>
/home/murgia/C++/libmath_util.so
(0x40017000)
libstdc++.so.5 =>
/usr/lib/libstdc++.so.5 (0x40030000)
libm.so.6 => /lib/tls/libm.so.6
(0x400e3000)
libgcc_s.so.1 => /lib/libgcc_s.so.1
(0x40106000)
libc.so.6 => /lib/tls/libc.so.6

(0x42000000)
/lib/ld-linux.so.2 => /lib/ld-linux.so.2
(0x40000000)
In questo caso il programma ldd ci informa che ora il dynamic loader in grado di
localizzare libmath_util.so, ed il programma sar eseguito con successo.

La flag -rpath
Esiste anche la possibilit di passare al linker la locazione della nostra librerie con
l'opzione -rpath in questa maniera
g++ -o prova_d main.cpp -Wl,rpath,/home/murgia/C++/ -L. lmath_util
in questo caso non sar necessario preoccuparsi di definire la variabile ambiente
LD_LIBRARY_PATH.
Si faccia per attenzione al fatto che il linker da' la precedenza al path specificato
con -rpath, se questo non specificato allora usa il valore di LD_LIBRARY_PATH,
e solo infine verifica il contenuto del file /etc/ld.so.conf.

Che tipo di libreria sto usando?


Se nella stessa directory sono presenti sia libmath_util.so che libmath_util.a il
linker preferir la prima. Per forzare il linker ad utilizzare la libreria statica si pu
usare la flag -static.

Un aspetto positivo dell'utilizzo delle librerie condivise


Diversi programmi che fanno uso di librerie comuni possono essere corretti
contemporaneamente intervenendo sulla libreria che fonte di errore. La sola
ricompilazione e sostituzione della libreria risolve un problema comune.

Librerie statiche vs librerie condivise


Per riassumere:
Librerie statiche:

Ogni processo ha la sua copia della libreria statica che sta usando, caricata
in memoria.
Gli eseguibili collegati con librerie statiche sono pi grandi.

Librerie condivise:

Solo una copia della libreria viene conservata in memoria ad un dato istante
(sfruttiamo meno memoria per far girare i nostri programmi e gli eseguibili
sono pi snelli).
I programmi partono pi lentamente.

Le operazioni di input-ouput in C++


La gerarchia di classi stream

La Libreria Standard del C++ mette a disposizione, per l'esecuzione delle


operazioni di input-output, un insieme di classi, funzioni e oggetti globali
(tutti definiti, come sempre, nel namespace std). Fra questi, conosciamo gi gli
oggetti cin, cout e cerr (a cui bisogna aggiungere, per completezza, clog, che
differisce cerr da in quanto opera con output bufferizzato), collegati ai
dispositivi standard stdin, stdout e stderr; e conosciamo anche l'esistenza di
varie funzioni che implementano gli overloads degli operatori di flusso "<<"
(inserimento) e ">>" (estrazione), rispettivemente per la scrittura dei dati su
cout o cerr, e per la lettura dei dati da cin.
Tutte le funzionalit di I/O del C++ sono definite in una decina di headerfiles. Il principale <iostream>, che va sempre incluso. Alcuni altri sono inclusi
dallo stesso <iostream>, per cui citeremo di volta in volta solo quelli necessari.
Alcune classi della Libreria gestiscono operazioni di I/O "ad alto livello", cio
indipendenti dal dispositivo, che pu essere un'unit esterna (come i dispositivi
standard a noi noti), un file, o anche un'area di memoria (in particolare una
stringa); queste classi sono strutturate in un'organizzazione gerarchica: da
un'unica classe base discendono, per eredit, tutte le altre. Ogni loro istanza
detta genericamente "stream" (flusso). Il concetto di stream un'astrazione,
che rappresenta un "qualcosa" da o verso cui "fluisce" una sequenza di bytes;
in sostanza un oggetto stream pu essere interpretato come un "file
intelligente" (con propriet e metodi, come tutti gli oggetti), che agisce come
"sorgente" da cui estrarre (input), o "destinazione" in cui inserire (output) i
dati.
Un altro concetto importante quello della "posizione corrente" in un oggetto
stream (file position indicator), che coincide con l'indice (paragonando lo
stream a un array) del prossimo byte che deve essere letto o scritto. Ogni
operazione di I/O modifica la posizione corrente, la quale pu essere anche
ricavata o impostata direttamente usando particolari metodi (come vedremo). A
questo proposito precisiamo che la parola "inserimento", usata come sinonimo
di operazione di scrittura, ha diverso significato in base al valore della
posizione corrente: se questa interna allo stream, i dati non vengono
"inseriti", ma sovrascritti; se invece la posizione corrente alla fine dello
stream (cio una posizione oltre l'ultimo byte), i nuovi dati vengono
effettivamente inseriti.
La gerarchia di classi stream illustrata dalla seguente figura:

Tutte le classi della gerarchia, salvo ios_base, sono specializzazioni di


template: il nome di ognuna in realt un sinonimo del nome (con prefisso
basic_) di una classe template specializzata con argomento <char>. Per
esempio:
ifstream un sinonimo di: basic_ifstream<char>
ma, come gi detto a proposito della classe string, noi siamo interessati solo al
tipo char e quindi tratteremo direttamente delle classi specializzate e non dei
template da cui provengono.
Le classi ios_base e ios
La classe base della gerarchia, ios_base, contiene propriet e metodi che
sono comuni sia alle operazioni di input che a quelle di output e non
dipendono da parametri di template. Le stesse caratteristiche sono presenti
nella sua classe derivata, ios, con la differenza che questa una
specializzazione con argomento char di
template <class T> class basic_ios,
le cui funzionalit dipendono dal parametro T. Dal nostro punto di vista, per,
non ci sono parametri di template (assumendo sempre T=char), e quindi le
due classi si possono considerare insieme come se fossero un'unica classe.
Entrambe forniscono strumenti di uso generale per le operazioni di I/O, come
ad esempio le funzioni di controllo degli errori, i flags per l'impostazione dei
formati di lettura e/o scrittura, i modi di apertura dei files ecc... (molti di
questi dati-membro sono enumeratori costituiti da un singolo bit in una
posizione specifica, e si possono combinare insieme con operazioni logiche bit
a bit). Entrambe le classi, inoltre, dichiarano i loro costruttori nella sezione
protetta, e quindi non possibile istanziarle direttamente; si devono invece
utilizzare le classi derivate da ios, a partire da istream (per l'input) e
ostream (per l'output), che contengono, per eredit, anche i membri definiti
in ios e ios_base.
Le classi istream, ostream e iostream
La classe istream, derivata diretta di ios, contiene le funzionalit necessarie
per le operazioni di input; in particolare la classe definisce un overload
dell'operatore di flusso ">>" (estrazione), che determina il trasferimento di
dati da un oggetto istream alla memoria. Sebbene non sia escluso che si

possano costruire delle sue istanze nel programma, anche la classe istream,
come gi la sua genitrice ios, serve quasi esclusivamente per fornire propriet
e metodi alle classi derivate. Alla classe istream appartiene, come sappiamo,
l'oggetto globale cin.
La classe ostream, derivata diretta di ios, contiene le funzionalit necessarie
per le operazioni di output; in particolare la classe definisce un overload
dell'operatore di flusso "<<" (inserimento), che determina il trasferimento di
dati dalla memoria a un oggetto ostream. Come istream, ostream serve pi
che altro a fornire propriet e metodi alle sue classi derivate. Alla classe
ostream appartengono, come sappiamo, gli oggetti globali cout, cerr e clog.
La classe iostream, deriva, per eredit multipla, da istream e ostream, e
ne riunisce le funzionalit, senza aggiungere nulla.
Le classi ifstream, ofstream e fstream
Le classi ifstream, ofstream e fstream servono per eseguire operazioni di
I/O su file e derivano rispettivamente da istream, ostream e iostream, a cui
aggiungono poche funzioni-membro (praticamente la open, la close e qualche
altra di minore importanza). Per utilizzarle bisogna includere l'header-file
<fstream>.
La classe ifstream serve per le operazioni di input. Normalmente i suoi
oggetti sono associati a files di sola lettura, che possono essere sia in modo
testo che in modo binario, ad accesso generalmente sequenziale.
La classe ofstream serve per le operazioni di output. Normalmente i suoi
oggetti sono associati a files di sola scrittura, che possono essere sia in modo
testo che in modo binario, ad accesso generalmente sequenziale.
Infine la classe fstream serve per le operazioni sia di input che di output. E'
particolarmente indicata per operare su files binari ad accesso casuale.
Qualunque classe si usi, le operazioni di I/O si eseguono utilizzando gli
operatori di flusso e ponendo l'oggetto associato al file come left-operand
(al posto di cin o cout). In lettura, se il risultato dell'operazione NULL (e
quindi false, se convertito in tipo bool), vuol dire di solito che si raggiunta la
fine del file (eof); questo permette di inserire la lettura di un file in un ciclo
while, in cui la stessa operazione di lettura funge da condizione per il
proseguimento del ciclo.
Sono anche disponibili funzioni-membro (definite nelle classi genitrici
istream e ostream) per la lettura e/o scrittura dei dati, il posizionamento nel
file, la gestione degli errori, la definizione dei formati ecc..., come vedremo in
dettaglio prossimamente.
Le classi istringstream, ostringstream e stringstream
Le classi istringstream, ostringstream e stringstream servono per eseguire
pseudo operazioni di I/O su stringa (come la funzione sprintf del C) e
derivano rispettivamente da istream, ostream e iostream, a cui aggiungono
poche funzioni-membro. Per utilizzarle bisogna includere l'header-file
<sstream>.

La classe istringstream serve per le operazioni di input. Un oggetto


istringstream sostanzialmente una stringa, dalla quale per si possono
estrarre dati, come se fosse un dispositivo periferico o un file. Analogamente ai
files, se il risultato di un'operazione di estrazione NULL, significa che si
raggiunta la fine della stringa (eos).
La classe ostringstream serve per le operazioni di output. Un oggetto
ostringstream sostanzialmente una stringa, nella quale per si possono
inserire dati, come se fosse un dispositivo periferico o un file. Le operazioni di
inserimento possono anche modificare la dimensione della stringa, e quindi
non necessario effettuare controlli sul range. Questo fatto pu essere di grande
utilit perch permette di espandere una stringa liberamente, in base alle
necessit (per esempio, per preparare un output "formattato").
Infine la classe stringstream serve sia per le operazioni di input che di
output.
Tipi definiti nella Libreria
La Libreria di I/O definisce alcuni tipi specifici (molti dei quali sono in realt
sinonimi, creati con typedef, di altri tipi, che a loro volta dipendono
dall'implementazione). I principali sono (per ognuno di essi indichiamo, fra
parentesi tonde, l'ambito o la classe in cui definito, e, fra parentesi quadre,
"normalmente implementato come ..."):

streamsize (namespace std) [sinonimo di int]


indica un numero di bytes consecutivi in un oggetto stream; questo tipo
(come pure i successivi) utilizzato come argomento in varie funzioni di
I/O
streamoff (namespace std) [sinonimo di long]
indica lo spostamento in byte da una certa posizione in un oggetto
stream a un'altra
fmtflags
(ios_base)
[tipo enumerato]
i suoi enumeratori controllano l'impostazione del formato di lettura o
scrittura (vedere pi avanti)
iostate
(ios_base)
[tipo enumerato]
i suoi enumeratori controllano lo stato dell'oggetto stream dopo
un'operazione (vedere pi avanti)
openmode (ios_base)
[tipo enumerato]
i suoi enumeratori controllano il modo di apertura di un file (vedere
prossimo paragrafo)
seekdir
(ios_base)
[tipo enumerato]
i suoi enumeratori si riferiscono a particolari posizioni nell'oggetto
stream, e sono:
ios_base::beg (posizione iniziale)
ios_base::cur (posizione corrente)
ios_base::end (posizione finale)
pos_type
(ios)
[sinonimo di long]
il tipo della posizione corrente nell'oggetto stream
off_type
(ios)
[sinonimo di long]
sostanzialmente un sinonimo di streamoff, con la sola differenza che
definito nella classe ios anzich nel namespace std

Modi di apertura di un file


Relativamente alle operazioni di I/O su file, bisogna precisare anzitutto che la
costruzione di un oggetto stream e l'apertura del file associato all'oggetto
sono due operazioni logicamente e cronologicamente distinte (anche se esiste
un costruttore che fa entrambe le cose, come vedremo). Di solito si usa prima il
costruttore di default dell'oggetto (che non fa nulla) e poi un suo particolare
metodo (la funzione open) che gli associa un file e lo apre. Questo permette
di chiudere il file (tramite un altro metodo, la funzione close) prima che
l'oggetto sia distrutto e quindi riutilizzare l'oggetto stesso associandogli un
altro file. Non possono coesistere due files aperti sullo stesso oggetto. Un file
ancora aperto al momento della distruzione dell'oggetto viene chiuso
automaticamente.
Un file pu essere aperto in diversi modi, a seconda di come si impostano i
seguenti flags (che sono enumeratori del tipo enumerato openmode):

ios_base::in
il file deve essere aperto in lettura
ios_base::out
il file deve essere aperto in scrittura
ios_base::ate
il file deve essere aperto con posizione (inizialmente) sull'eof (significa
"at the end"); di default un file aperto "at the beginning"
ios_base::app
il file deve essere aperto con posizione (permanentemente) sull'eof
(cio i dati si potranno scrivere solo in fondo al file)
ios_base::trunc
il file deve essere aperto con cancellazione del suo contenuto
preesistente; se il file non esiste, viene creato (in tutti gli altri casi deve gi
esistere)
ios_base::binary
il file deve essere aperto in modo "binario", cio i dati devono essere
scritti o letti esattamente come sono; di default il file aperto in
modo "testo", nel qual caso, in output, ogni carattere newline pu
(dipende dall'implementazione!) essere trasformato nella coppia di
caratteri carriage-return/line-feed (e viceversa in input)

Ogni flag rappresentato in una voce memoria da 16 o 32 bit, con un solo bit
diverso da zero e in una posizione diversa da quella dei bit degli altri flags.
Questo permette di combinare insieme due modi con un'operazione di OR bit a
bit, oppure di verificare la presenza di un singolo modo in una combinazione
esistente, estraendolo con un'operazione di AND bit a bit. Per esempio, la
combinazione:
ios_base::in | ios_base::out
indica che il file pu essere aperto sia in lettura che in scrittura. Va precisato,
tuttavia, che il significato di alcune combinazioni dipende dall'implementazione e
quindi va verificato "sperimentalmente", consultando il manuale del proprio
sistema. Per esempio, nelle ultime versioni dello standard, il flag ios_base::out
non pu mai stare da solo, ma deve essere combinato con altri.
Per concludere, i flags ios_base::in e ios_base::out sono anche usati dai
costruttori delle classi che gestiscono l'I/O su stringa.

Operazioni di output

Nella classe ostream sono definite varie funzioni-membro per l'esecuzione


delle operazioni di output. Queste funzioni sono utilizzate direttamente per la
scrittura sui dispositivi standard stdout e stderr e sono ereditate nelle classi
ofstream, fstream, ostringstream e stringstream per la scrittura su file e
su stringa.
Metodi operator<<
Alcuni metodi di ostream definiscono tutti i possibili overloads di
operator<< con argomento di tipo nativo (compresi i tipi ottenuti mediante i
prefissi short, long, signed, unsigned e const). I dati in memoria vengono
convertiti in stringhe di caratteri (in free-format, o con un formato specifico,
come vedremo) e poi inseriti in *this. Per quello che riguarda i puntatori (a
qualunque tipo), definito l'overload con argomento void*, che scrive il
valore dell'indirizzo in formato esadecimale. Fa eccezione il caso di
puntatore a carattere, per il quale definito un overload specifico con
argomento char*: in questo caso non viene scritto l'indirizzo, ma il carattere
puntato, e tutti i caratteri successivi finch non si incontra il valore '\0'
(interpretato come terminatore di una stringa). Come ben sappiamo, anche
possibile definire ulteriori overloads di operator<<, con argomento di tipo
definito dall'utente; questa volta, per, le funzioni non possono essere
metodi di ostream, ma funzioni esterne: nella risoluzione di una chiamata, il
compilatore si comporter in ogni caso correttamente, in quanto cercher, non
prima fra i metodi e poi fra le funzioni esterne (che hanno lo stesso livello di
preferenza), ma sempre prima fra le funzioni (metodi o no) in cui l'argomento
corrisponde esattamente e poi fra quelle in cui la corrispondenza ottenuta
tramite conversione implicita di tipo (questo succede in particolare anche
quando il nostro tipo convertibile implicitamente in un tipo nativo e quindi
selezionerebbe un metodo se questo avesse la precedenza). Questa regola offre
un grande vantaggio, perch permette di scrivere ulteriori overloads di
operator<< senza bisogno di modificare la classe ostream.
Altre funzioni-membro di ostream
Oltre a operator<<, sono definiti in ostream i seguenti metodi (citiamo i pi
importanti):

ostream& ostream::put(char c)
inserisce il carattere c nella posizione corrente di *this; ritorna *this
ostream& ostream::write(char* p, streamsize n)
inserisce nella posizione corrente di *this una sequenza di n bytes, a
partire dal byte puntato da p; ritorna *this. A differenza di operator<<,
scrive i dati binari cos come sono in memoria, senza prima convertirli in

stringhe di caratteri.
NOTA: questo metodo particolarmente indicato per scrivere dati di
qualsiasi tipo nativo (per esempio dati binari su file), operando una
conversione di tipo puntatore nella chiamata. Per esempio, supponendo
che out sia il nome dell'oggetto stream e val un valore intero o
floating, si pu scrivere val in out con la chiamata:
out.write((char*)&val,sizeof(val));
notare il casting, che reintepreta l'indirizzo di val come indirizzo di una
sequenza di sizeof(val) bytes. Nel caso invece che il tipo sia definito
dall'utente, il discorso un po' pi complicato: la soluzione pi "elegante"
quella della cosidetta "serializzazione", che consiste nel creare (nella
classe dell'oggetto da scrivere) un metodo specifico, che scriva in
successione i diversi membri dell'oggetto.
pos_type ostream::tellp()
ritorna la posizione corrente
ostream& ostream::seekp(pos_type pos)
sposta la posizione corrente in pos; ritorna *this; questo metodo
(come il suo overload che segue) si usa principalmente quando l'output
su file ad accesso casuale
ostream& ostream::seekp(off_type off, ios_base::seekdir seek)
sposta la posizione corrente di off bytes a partire dal valore indicato
dall'enumeratore seek; ritorna *this; off pu anche essere negativo
(deve esserlo quando seek coincide con ios_base::end e deve non
esserlo quando seek coincide con ios_base::beg); in ogni caso se
l'operazione tende a spostare la posizione corrente fuori dal range, la
seekp non viene eseguita e la posizione corrente resta invariata; la
posizione corrispondente alla fine dello stream (cio eof o eos)
considerata ancora nel range.

Funzioni virtuali di output


Le funzioni-membro di ostream non sono virtuali, per motivi di efficienza,
dato che in un programma le operazioni di I/O sono in genere molto frequenti.
Tuttavia si pu essere talvolta nella necessit di mandare in output un oggetto
di tipo polimorfo, lasciando alla fase di esecuzione del programma la scelta del
tipo "concreto" fra quelli derivati da un'unica classe base astratta. Per
ottenere questo risultato, bisogna procedere nel seguente modo (supponiamo di
chiamare My_base la classe base astratta):
1. dichiarare in My_base la funzione virtuale pura (che chiamiamo ins):
virtual ostream& ins(ostream& out) const = 0; // scrive
this
su
out
*
2. ridefinire ins in tutte le classi derivate da My_base, in modo che ogni
funzione svolga l'operazione di scrittura appropriata per la sua classe
3. definire il seguente overload di operator<< (ovviamente come
funzione esterna):
ostream& operator<<(ostream& out, const My_base& ogg)
{ return ogg.ins(out); }
Ci assicura che operator<< utilizzi, tramite la funzione virtuale ins, la giusta
operazione di output in istruzioni del tipo:
cout << r;
quando r definito come riferimento a My_base. Questa tecnica di utilit

generale per fornire operazioni che si comportano come funzioni virtuali, ma


con la selezione dinamica basata sul secondo argomento.
Metodi specifici per l'output su file
Nella classe ofstream, derivata di ostream (e anche nella classe fstream,
derivata di iostream, per le operazioni comuni all'output e all'input), sono
definiti alcuni metodi che, insieme a quelli ereditati dalla classe base,
servono per la scrittura su file. Il pi importante di questi il metodo open:
void ofstream::open(const char* filename, ios_base::openmode mode
= ....)
void fstream::open(const char* filename, ios_base::openmode mode
= ....)
che ha due argomenti: il primo, filename, il nome del file da aprire (nota:
una stringa del C, non un oggetto string!), il secondo, mode, rappresenta il
modo di apertura del file ed di default, con valore che dipende dalla classe
e precisamente:

in ofstream (sola scrittura):


mode = ios_base::out | ios_base::trunc
(notare: se il file esiste, viene "troncato", se non esiste viene creato)
in fstream (lettura e scrittura):
mode = ios_base::out | ios_base::in
(notare: il file deve esistere)

Se si verifica un errore, non appaiono messaggi, ma nessuna delle successive


operazioni sul file viene eseguita. Ci si pu accorgere dell'errore interrogando lo
stato dell'oggetto (come vedremo).
Fra gli altri metodi definiti in ofstream citiamo:

void ofstream::close()
chiude il file senza distruggere l'oggetto *this, a cui si pu cos
associare un altro file (oppure di nuovo lo stesso, per esempio con modi
di apertura diversi)
costruttore di default
crea l'oggetto senza aprire nessun file; deve ovviamente essere seguito
da una open
costruttore con esattamente gli stessi argomenti della open (compresi i
defaults)
riunisce insieme le operazioni del costruttore di default e della open (a
cui ovviamente alternativo); anche se generalmente il file resta aperto
fino alla distruzione dell'oggetto, la "prima" apertura tramite
costruttore al posto della open non preclude la possibilit che il file
venga chiuso "anticipatamente" (con la close) e che poi venga associato
all'oggetto un altro file (con una successiva open)
bool ofstream::isopen()
ritorna true se esiste un file aperto associato all'oggetto

La classe fstream definisce esattamente gli stessi metodi di ofstream (l'unica


differenza nel modo di apertura di default del file, dato dal secondo
argomento del costruttore come nella open corrispondente).
Metodi specifici per l'output su stringa
Nella classe ostringstream, derivata di ostream (e anche nella classe
stringstream, derivata di iostream, per le operazioni comuni all'output e
all'input), sono definiti alcuni metodi che, insieme a quelli ereditati dalla
classe base, servono per la scrittura su stringa. I pi importanti sono:

ostringstream::ostringstream(ios_base::openmode mode =
ios_base::out)
costruttore di default (con un argomento di default )
ostringstream::ostringstream(const string& str, ios_base ..come
sopra.. )
costruttore per copia da un oggetto string (con il secondo
argomento di default )
string ostringstream::str()
crea una copia di *this e la ritorna convertita in un oggetto string.
Questo metodo molto utile, in quanto gli oggetti di ostringstream (e
delle altre classi della gerarchia stream) non possiedono le funzionalit
delle stringhe; per poterli utilizzare come stringhe prima necessario
convertirli in oggetti string.
void ostringstream::str(const string& str)
questo secondo overload di str esegue l'operazione inversa del
precedente: sostituisce in *this una copia di un oggetto string

La classe stringstream definisce esattamente gli stessi metodi di


ostringstream, con la differenza che l'argomento di default dei costruttori
(mode) :
mode = ios_base::out | ios_base::in

Operazioni di input

Nella classe istream sono definite varie funzioni-membro per l'esecuzione


delle operazioni di input. Queste funzioni sono utilizzate direttamente per la
lettura dal dispositivo standard stdin e sono ereditate nelle classi ifstream,
fstream, istringstream e stringstream per la lettura da file e da stringa.
Metodi operator>>
Alcuni metodi di istream definiscono tutti i possibili overloads di
operator>> con argomento di tipo nativo (compresi i tipi ottenuti mediante i
prefissi short, long, signed e unsigned). Da *this vengono estratte

stringhe di caratteri, che sono interpretate secondo un certo formato e poi


convertite nel tipo rappresentato dall'argomento, in cui vengono infine
memorizzate. Ognuna di queste stringhe (che chiamiamo "stringhe di input")
delimitata da uno o pi "spazi bianchi" (cos sono definiti i caratteri: spazio,
tabulazione, fine riga, fine pagina e ritorno carrello); tutti gli spazi
bianchi che precedono e seguono una stringa di input vengono "scartati", cio
eliminati dallo stream e non trasferiti in memoria (anche quando l'argomento
di tipo char, nel qual caso non viene estratta una stringa, ma un singolo
carattere, pur sempre tuttavia dopo avere "scartato" tutti gli eventuali spazi
bianchi che lo precedono). Pertanto ogni singola esecuzione di operator>>
converte e trasferisce in memoria una e una sola stringa di input alla volta,
qualunque sia la dimensione dello stream. I caratteri della stringa di input,
inoltre, devono essere tutti validi, in relazione al tipo dell' argomento. Per
esempio, se il dato da leggere di tipo int e la stringa di input contiene un
"punto", questa viene troncata in modo da lasciare il "punto" come primo
carattere della prossima stringa di input da estrarre (vedere la gestione degli
errori nella prossima sezione).
Per quello che riguarda i puntatori (a qualunque tipo), definito un overload
di operator>> con argomento void*, che converte la stringa di input in un
numero intero e lo memorizza nell'argomento (la cosa ha per scarso interesse,
in quanto non si possono mai assegnare valori agli indirizzi). E' importante
invece il caso di puntatore a carattere, per il quale definito un overload
specifico con argomento char*: in questo caso la stringa di input non viene
convertita, ma trasferita cos com' nell'area di memoria puntata dall'argomento;
alla fine viene aggiunto automaticamente il carattere '\0' come terminatore
della stringa memorizzata.
Per ci che concerne la definizione di ulteriori overloads di operator>> con
argomento di tipo definito dall'utente, e la scelta fra i metodi e le funzioni
esterne, vedere le considerazioni fatte a proposito di operator<<.
Altre funzioni-membro di istream
La principale differenza fra gli overloads di operator>> e gli altri metodi di
istream che eseguono operazioni di lettura consiste nel fatto che i primi
estraggono stringhe di input, senza spazi bianchi e interpretate secondo un
certo formato (formatted input functions), mentre gli altri metodi
estraggono singoli bytes (o sequenze di bytes) senza applicare nessun
formato (unformatted input functions) e senza escludere gli spazi bianchi.
Vediamone i principali:

int istream::get()
estrae un byte e lo ritorna al chiamante. Nota: il valore di ritorno
sempre positivo (in quanto definito int e contiene un solo byte, cio al
massimo il numero 255; pertanto un valore di ritorno negativo indica
convenzionalmente che si verificato un errore, oppure che la posizione
corrente era gi sulla fine dello stream (cio su eof o eos)
istream& istream::get(char& c)
estrae un byte e lo memorizza in c; ritorna *this
istream& istream::get(char* p, streamsize n, char delim='\n')
estrae n-1 bytes e li memorizza nell'area puntata da p (facendo seguire il
carattere '\0' come terminatore della stringa memorizzata); ritorna

*this;

il processo di estrazione pu essere interrotto in anticipo, per uno


dei seguenti motivi:
1. stata raggiunta la fine dello stream;
2. stato incontrato il carattere delim; in questo caso delim non
viene estratto e la posizione corrente si attesta sullo stesso
delim
istream& istream::getline(char* p, streamsize n, char delim='\n')
identica alla get precedente, con due differenze:
1. se incontra il carattere delim non lo estrae (come nella get), ma
la posizione corrente si attesta dopo delim (cio delim viene
"saltato")
2. se completa l'estrazione di n-1 bytes senza incontrare delim,
viene impostata una condizione di errore; in pratica ci vuol dire che
l'argomento n serve per imporre la condizione:
posizione di delim - posizione corrente < n
istream& istream::read(char* p, streamsize n)
differisce dalle funzioni precedenti per il fatto che non ha delimitatori (a
parte la fine dello stream) e estrae n bytes (senza aggiungere il
carattere '\0' in fondo); e quindi non legge stringhe di caratteri, ma
dati binari di qualsiasi tipo (vedere la NOTA a proposito del metodo
write di ostream)
streamsize istream::readsome(char* p, streamsize n)
come la read, salvo il fatto che ritorna il numero di bytes effettivamente
letti
istream& istream::ignore(streamsize n=1, int delim=EOF)
"salta" i prossimi n bytes, oppure i prossimi bytes fino a delim
(compreso); il default di delim (EOF) una costante predefinita che
indica la fine dello stream (normalmente implementata con il valore -1);
ignore serve soprattutto per "saltare" caratteri invalidi nella lettura
formattata da una stringa di input

I metodi di interrogazione e modifica diretta della posizione corrente sono:


tellg e seekg (in 2 overloads): hanno gli stessi argomenti e svolgono le stesse
operazioni dei corrispondenti tellp e seekp definiti in ostream.
Metodi specifici per input da file e da stringa
Nelle classi ifstream e istringstream, derivate di istream, sono definiti
esattamente gli stessi metodi che si trovano rispettivamente in ofstream e
ostringstream. L'unica differenza sta nel default dell'argomento mode della
open e dei costruttori, che in questo caso :
mode = ios_base::in

Stato dell'oggetto stream e gestione degli errori

A ogni oggetto stream associato uno "stato", impostando e controllando il


quale possibile gestire gli errori e le condizioni anomale nelle operazioni di
input-output.
Lo stato dell'oggetto rappresentato da un insieme di flags (che sono
enumeratori del tipo enumerato iostate, definito nella classe ios_base),
ciascuno dei quali (come gli enumeratori del tipo openmode) pu essere
combinato con gli altri con un'operazione di OR bit a bit e separato dagli altri
con un'operazione di AND bit a bit. I flags sono i seguenti:

ios_base::goodbit
finora tutto bene e la posizione corrente non sulla fine dello stream;
nessun bit "settato" (valore 0)
ios_base::failbit
si verificato un errore di I/O, oppure si tentato di eseguire
un'operazione non consentita (per esempio la open di un file che non
esiste)
ios_base::badbit
si verificato un errore di I/O irrecuperabile
ios_base::eofbit
la posizione corrente sulla fine dello stream; un successivo tentativo
di lettura imposta anche failbit

La classe ios, derivata di ios_base, fornisce alcuni metodi per la gestione e il


controllo dello stato:

ios_base::iostate ios::rdstate() const


ritorna lo stato che risulta dall'ultima operazione
void ios::clear(iostate st=goodbit)
imposta lo stato con st (cancellando il valore precedente); chiamando
clear() senza argomenti si imposta goodbit, cio si "resettano" i flags
di errore
void ios::setstate(iostate st)
aggiunge il flag st allo stato corrente, eseguendo l'istruzione:
clear(rdstate() | st );
bool ios::good() const
ritorna rdstate() == goodbit
bool ios::fail() const
ritorna bool(rdstate() & failbit)
bool ios::bad() const
ritorna bool(rdstate() & badbit)
bool ios::eof() const
ritorna bool(rdstate() & eofbit)
ios::operator void*() const
ritorna NULL se fail() | bad() true; altrimenti ritorna this (che
per, essendo convertito in un puntatore a void, non pu essere
dereferenziato)
NOTA: questo (strano) metodo necessita di un chiarimento: noto che il
casting a puntatore a void non mai necessario, in quanto un
puntatore a void pu puntare a qualsiasi tipo di oggetto; quindi anche il
semplice nome dell'oggetto pu essere reinterpretato come suo casting
a puntatore a void (!!!). In pratica il compilatore, quando incontra
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


definito un overload del casting a puntatore a void e, se lo trova, lo
applica. Nel nostro caso il metodo ritorna normalmente this e quindi un
espressione del tipo:
cout << cout;
scrive in cout il suo indirizzo! Se per si verificata una condizione di
errore, il metodo ritorna NULL e cio false, se il nome dell'oggetto
inserito in un'espressione logica; questo spiega perch un'operazione di
lettura pu funzionare anche come istruzione di controllo in un ciclo o
in un costrutto if, come nell'esempio che segue:
while ( cin >> .... )
infatti l'operazione >> ritorna cin, che viene convertito da operator
void*: questo a sua volta ritorna l'indirizzo di cin finch non ci sono
errori (e quindi true, essendo un indirizzo sempre diverso da zero) e il
ciclo prosegue; ma quando il programma tenta di leggere la fine dello
stream, si imposta il flag failbit e quindi operator void* ritorna NULL
interrompendo il ciclo.
bool ios::operator !() const
ritorna bool(fail() | bad())
NOTA: le espressioni: if(cin) e if(!!cin) sono equivalenti (!),
mentre le espressioni:
if(cin) e if(cin.good()) non sono equivalenti,
in quanto la prima non controlla il flag eofbit

Quando impostato un qualunque flag diverso da goodbit, nessuna funzione


non const definita nell'oggetto stream pu essere eseguita (senza messaggi di
errore: semplicemente le successive istruzioni con operazioni di I/O non hanno
alcun effetto); tuttavia lo stato pu essere "resettato" chiamando la clear
(successivamente, per, bisogna rimuovere la causa dell'errore se si vuole che le
operazioni di I/O riprendano a essere regolarmente eseguite).
Se, durante un'operazione di lettura formattata da una stringa di input, si
incontra un carattere non ammissibile in relazione al tipo di dato da leggere,
abbiamo gi detto che la stringa di input viene "spezzata" in due: la prima, su
cui viene normalmente eseguita la lettura, termina lasciando fuori il carattere
invalido; la seconda comincia con il carattere invalido (che, se tale anche in
relazione al tipo del successivo dato da leggere, deve essere "saltato"
chiamando la ignore). Per quanto riguarda lo stato, il comportamento diverso
a seconda che il carattere invalido sia o meno il primo carattere della stringa
di input:

se non il primo, lo stato resta definito dal flag goodbit (per la


successiva operazione si pu chiamare la ignore senza la clear);
se il primo, impostato il flag failbit (bisogna chiamare la clear prima
della ignore se si vuole che questa abbia effetto)

Errori gestiti dalle eccezioni


Per una gestione corretta degli errori, sarebbe opportuno controllare lo stato
dopo ogni operazione di I/O. Se per le operazioni sono molte, la cosa non
risulta molto comoda, anche in considerazione del fatto che gli errori sono in
generale poco frequenti. In particolare le operazioni di output sono controllate
assai raramente (bench ogni tanto anche loro falliscano): di solito si verifica che,
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


sono vari e diversi: formati sbagliati, errori umani nella immissione dei dati ecc...,
senza contare il fatto che bisogna sempre controllare il raggiungimento della fine
dello stream. Pertanto l'esame dello stato dopo un'operazione di lettura
quasi sempre necessario.
Tuttavia, come alternativa alla disseminazione di istruzioni if e switch nel
programma, possibile gestire gli errori di input-output anche mediante le
eccezioni. A questo scopo definito nella classe ios_base un oggetto del
tipo enumerato iostate (exception mask), che contiene un insieme di flags
di stato: quando un'operazione di I/O imposta uno di questi flags, viene
generata un'eccezione di tipo ios_base::failure (failure una classe
"annidata" in ios_base) che pu essere catturata e gestita da un blocco
catch:
catch(ios_base::failure) { ..... }
Di default l'exception mask vuoto (cio di default gli errori di I/O non
generano eccezioni), ma possibile cambiarne il contenuto chiamando il
metodo exceptions di ios:
void ios::exceptions(iostate em) che imposta l'exception mask con
em.
Esiste anche un overload di exceptions senza argomenti che ritorna
l'exception mask corrente:
ios_base::iostate ios::exceptions() const
Con i due overloads di exceptions possibile circoscrivere l'uso delle
eccezioni in aree precise del programma; per esempio:
ios_base::iostate em = cin.exceptions();

salva l'exception mask


corrente (no eccezioni)
in em

cin.exceptions(ios_base::badbit|ios_base::failbit);

imposta l'exception
mask con badbit e
failbit

try { ... cin >> ...}

blocco delle istruzioni di


I/O che possono
generare eccezioni

catch(ios_base::failure) { ..... }

blocco di gestione delle


eccezioni

cin.exceptions(em);

ripristina l'exception
mask precedente (no

eccezioni)

Formattazione e manipolatori di formato

Abbiamo detto che le funzioni operator>> (in istream) e operator<< (in


ostream) si distinguono da tutti gli altri metodi delle loro classi per il fatto che
eseguono operazioni di I/O formattate: in particolare operator>> converte
una stringa di input (delimitata da spazi bianchi) nel dato da memorizzare,
mentre operator<< converte il dato da scrivere in una stringa, secondo un
certo formato.
Se non si modificano i defaults, i formati, sia di lettura che scrittura, sono
predefiniti e obbediscono a determinate regole; per esempio: in output non sono
introdotti spazi bianchi (free-format), i numeri sono in base decimale (salvo
gli indirizzi, che sono in esadecimale), i dati floating sono scritti con al pi sei
cifre significative ecc... In tutti gli esempi e gli esercizi visti finora si sono sempre
usati (salvo raro casi) i formati predefiniti.
A volte per il programma ha bisogno di utilizzare formati particolari, per
esempio per incolonnare i dati, oppure per scrivere i numeri con una base
diversa, o in notazione scientifica ecc... E quindi, come gi in C con gli
specificatori di formato (che abbiamo visto all'inizio di questo corso), cos
anche in C++ con altri strumenti, possibile impostare, nelle operazioni di I/O,
formati diversi da quello predefinito. A questo scopo definito nella classe
ios_base il tipo enumerato fmtflags, i cui enumeratori (detti format flags)
controllano il formato, sia di lettura che di scrittura.
I format flags sono all'incirca una ventina. Ciascuno di loro imposta una
particolare opzione, che pu essere combinata con altre (con le solite operazioni
bit a bit). Come gi per la gestione dello stato, esistono anche vari metodi
(definiti in ios_base e in ios) che permettono di impostare un insieme di format
flags, di "resettarli", di combinarli con altri gi impostati ecc...Non ci
dilungheremo su questo argomento, perch, "dal punto di vista dell'utente",
molto pi comodo e rapido gestire il formato tramite i cosidetti "manipolatori",
i quali possono utilizzare i format flags per modificare il formato nelle stesse
istruzioni in cui i dati sono letti o scritti. In generale ogni manipolatore
aggiunge (o rimuove) un'opzione. L'effetto di un manipolatore su un oggetto
stream permanente (salvo in un caso, che vedremo), fino a un eventuale
manipolatore che lo contraddice o fino alla distruzione dell'oggetto.
Manipolatori senza argomenti
I manipolatori sono funzioni esterne alle classi, definite direttamente in std e
raggruppate in alcuni header-files (tutti inclusi da <iostream>). Per capire
come "lavorano", bisogna anzitutto sapere che le classi istream e ostream
forniscono un ulteriore overload dell'operatore di flusso, con un puntatore a
funzione come operando (prendiamo il caso di ostream, che il pi
interessante, ma teniamo presente che quanto si dir vale anche per istream):
ostream& ostream::operator<<(ostream& (*pf)(ostream&))
{ return pf(*this); }
vediamo ora come il C++ risolve un'istruzione del tipo:
cout << fun;
(dove fun una funzione che abbia (guarda caso) valore di ritorno di tipo
ostream e un argomento di tipo ostream ):

1. trova che l'overload di operator<< con operando puntatore a


funzione pf proprio quello "giusto" e sostituisce fun a pf;
2. in operator<< esegue fun(cout) e ritorna il valore di ritorno di fun
supponiamo ora che fun chiami un metodo della classe del suo argomento e
ritorni by reference l'argomento stesso (cio cout, nel nostro esempio);
supponiamo inoltre che il metodo chiamato da fun imposti o rimuova un
format flag. Ne consegue che l'istruzione di cui sopra ha l'effetto di modificare il
formato (e quindi fun un manipolatore); inoltre, per il fatto che fun ritorna
*this, si pu inserire il suo nome (senza argomenti e senza parentesi)
all'interno di una sequenza di operazioni di flusso; per esempio (anticipiamo che
hex un manipolatore):
cout << hex << 1234;
fa s che il numero 1234 (e tutti i successivi, fino a disposizione contraria) venga
scritto in esadecimale.
Alcuni manipolatori sono in due versioni: quella con un certo nome imposta un
format flag, quella con lo stesso nome e con prefisso no lo rimuove; di solito
la versione con prefisso no di default. I principali manipolatori sono i
seguenti:
dec

interi in base decimale (default)

hex

interi in base esadecimale

oct

interi in base ottale

fixed

per i numeri floating corrisponde allo specificatore %f del C

scientific

per i numeri floating corrisponde allo specificatore %e del C

left

allinea a sinistra in un campo di larghezza prefissata (vedere


pi avanti)

right

allinea a destra in un campo di larghezza prefissata (default)

[no]boolalpha

rappresenta un valore booleano con true e false anzich con


1e0

[no]showbase

aggiunge il prefisso 0 per i numeri ottali e 0x per i numeri

esadecimali

[no]showpoint

mostra comunque il punto decimale nei numeri floating

[no]showpos

scrive il segno + davanti ai numeri positivi

[no]uppercase

scrive lettere maiuscole nelle notazioni esadecimale (X) e


esponenziale (E)

[no]skipws

ignora gli spazi bianchi (il default skipws)

flush

scarica il buffer di output

ends

scrive '\0' e scarica il buffer di output

endl

scrive '\n' e scarica il buffer di output

gli ultimi tre manipolatori non modificano il formato ma eseguono


un'operazione (e quindi il loro effetto non permanente, come negli altri casi)
Manipolatori con argomenti

Abbiamo visto che un manipolatore una funzione che viene eseguita al posto
di un puntatore a funzione e quindi il suo nome va specificato, come
operando in un'operazione di flusso, senza parentesi e senza argomenti.
Esistono tuttavia manipolatori che accettano un argomento, cio che vanno
specificati con un valore fra parentesi. In questi casi (consideriamo al solito solo
l'output) l'overload di operator<< non deve avere come argomento un
puntatore a funzione, ma un oggetto di un tipo specifico, restituito come
valore di ritorno dalla funzione che appare come operando e inizializzato
con il valore del suo argomento. Chiariamo quanto detto con un esempio;
questa volta l'istruzione :
cout << fun(x) << .... ;
dove supponiamo che l'argomento x sia di tipo int. La funzione fun (eseguita
con precedenza) non deve fare altro che restituire un oggetto (chiamiamo _fun
il suo tipo) inizializzato con x, cio:
_fun fun(int x) { return _fun(x); }
a sua volta la classe (o meglio, la struttura) _fun deve essere costituita dai
seguenti membri:
int i;
_fun(int x) : i(x) { }
(il costruttore usa l'argomento x per inizializzare il membro i)
L'informazione fornita dall'argomento x del manipolatore fun perci
memorizzata nel membro i della struttura _fun. Ormai il problema risolto,
basta avere un overload di operator<< (che questa volta supponiamo sia una
funzione esterna) con right-operand di tipo _fun:
ostream& operator<<(ostream& os, _fun& f)
che chiami, per l'impostazione del formato, un opportuno metodo di os,
utilizzando l'informazione trasmessa nel membro i dell'oggetto f.
Nelle precedenti versioni dello standard esisteva una sola struttura, di nome
smanip, e un solo overload di operator<< (con right-operand di tipo
smanip) per tutti i manipolatori con argomenti; la struttura smanip
conteneva, come ulteriore membro, un puntatore a funzione, da sostituire
ogni volta con il manipolatore appropriato. A partire dal compilatore gcc 3.2
smanip "deprecated" e al suo posto ci sono tante strutture (e tanti
overloads di operator<<) quanti sono i manipolatori (in realt questo non
un problema, perch i manipolatori con argomenti sono pochi); in compenso
ogni operazione molto pi veloce, in quanto chiama la sua funzione
direttamente, senza passare attraverso i puntatore a funzione.
I manipolatori con argomenti, forniti dalla Libreria, sono definiti in
<iomanip> (che deve essere incluso insieme a <iostream>) e sono 5: setw,
setfill, setprecision, setiosflag e resetiosflag; tralasciamo gli ultimi due, i
quali hanno come argomento direttamente un format flag (o una combinazione
di format flags), coerentemente con il fatto che abbiamo deciso di non
descrivere singolarmente i format flags e i metodi che li gesticono (le stesse
operazioni si fanno pi comodamente ed "elegantemente" usando gli altri
manipolatori). Procediamo invece con la descrizione dei primi tre:

setw(int w)
specifica che nella prossima operazione di output il dato dovr essere
scritto in un campo con un numero minimo di caratteri w: se il numero
effettivo superiore, tutti i caratteri vengono scritti normalmente, se
inferiore, il dato scritto all'interno del campo e allineato di default a

destra (oppure a sinistra se stato specificato il manipolatore left); nella


posizione che compete ai caratteri rimanenti, viene scritto il cosidetto
"carattere di riempimento", che di default uno spazio (codice 32), ma
che pu anche essere modificato con setfill. Il manipolatore setw
l'unico che non ha effetto permanente, ma modifica il formato solo
relativamente alla prossima operazione (dalla successiva il formato
torner com'era prima di setw)
setfill(char c)
stabilisce che il "carattere di riempimento" d'ora in poi sar c
setprecision(int p)
(p deve essere non negativo, altrimenti il manipolatore non ha effetto)
influenza esclusivamente l'output di numeri floating e il suo effetto
diverso, a seconda di come impostato il formato floating; questo pu
assumere tre diverse configurazioni:
1. fixed: impostato dal manipolatore fixed; utilizza la
rappresentazione:
[parte intera].[parte decimale]
(corrisponde allo specificatore di formato %f del C); p indica il
numero esatto di cifre della parte decimale (compresi eventuali zeri
a destra); l'ultima cifra decimale arrotondata; se p zero,
arrotondata la cifra delle unit e il punto decimale non scritto (a
meno che non sia stato specificato il manipolatore showpoint)
2. scientific: impostato dal manipolatore scientific; utilizza la
rappresentazione:
[cifra intera].[parte decimale]e[esponente]
(corrisponde allo specificatore di formato %e del C); come
fixed, p indica il numero esatto di cifre della parte decimale e
l'ultima cifra decimale arrotondata; scrive E al posto di e se stato
specificato il manipolatore uppercase; l'esponente costituito dal
segno, seguito da 2 o 3 (dipende dall'implementazione) cifre intere
3. general: impostato di default; sceglie, fra le rappresentazioni di
fixed e di scientific, quella pi conveniente (corrisponde allo
specificatore di formato %g del C); p indica il numero massimo
di cifre significative; l'ultima cifra significativa arrotondata; gli zeri
non significativi della parte decimale non sono scritti; se il numero
arrotondato a intero non scritto neppure il punto decimale (a meno
che non sia stato specificato il manipolatore showpoint).
NOTA: questo l'unico caso in cui non esiste un manipolatore per
ripristinare il default. Per tornare al formato general dopo che
stato impostato fixed o scientific, bisogna usare il metodo setf
(definito in ios_base), nel seguente modo (supponiamo per
esempio che l'oggetto stream sia cout):
cout.setf(ios_base::fmtflags(0),ios_base::floatfield);

Manipolatori definiti dall'utente


Applicando gli schemi riportati negli esempi di manipolatori con e senza
argomenti, un programmatore pu definire nuovi manipolatori, per il suo uso
specifico.
Nell'esercizio che segue definito un manipolatore, chiamato format (con 2
argomenti!), che permette la scrittura di un dato, di tipo double, specificando
insieme, in un'unica stringa, il formato floating, il campo e la precisione. Il

manipolatore deve essere usato nel modo seguente (supponiamo al solito che
l'oggetto stream sia cout):
cout << format(dato,xw.p);
dove: dato il nostro dato double da scrivere; xw.p una stringa, e in
particolare: x indica il formato floating, che pu assumere i valori f, e o g (con
il significato dei corrispondenti specificatori di formato del C); w
l'argomento di setw e pu essere preceduto dal segno - per indicare
l'allineamento a sinistra; p l'argomento di setprecision
Le altre operazioni di scrittura, eseguite sullo stesso oggetto stream senza
format, non vengono influenzate dalle modifiche al formato apportate da
format. Per esempio:
cout << format(dato1,f7.3) << dato2 ;
scrive dato1 con il formato f7.3 e dato2 con il formato precedentemente
impostato. L'indipendenza fra i due formati viene realizzata in realt con un
"trucco": i dati gestiti da format non sono scritti direttamente su cout, ma su
un oggetto ostringstream, cio su una stringa, trasferita successivamente su
cout.
In considerazione del fatto che a volte si deve scrivere una serie di dati, tutti con
lo stesso formato (per esempio per produrre una tabella allineata sulle colonne),
si pensato anche a due overloads di format con un solo argomento:
cout << format(xw.p); e cout << format(dato);
il primo imposta il formato senza scrivere nulla; il secondo scrive dato
utilizzando il formato precedentemente impostato. Per rendere possibile questa
opzione, le informazioni sul formato sono memorizzate in membri statici della
struttura di appoggio.
Il manipolatore format pu essere utile in alcune circostanze, in quanto la
disomogeneit di comportamento fra setw (effetto "una tantum") e gli altri
manipolatori (effetto permanente) potrebbe talvolta risultare "fastidiosa".

Cenni sulla bufferizzazione

Abbiamo, in varie circostanze, accennato alla presenza di un buffer nelle


operazioni di I/O. In effetti il trasferimento dei dati fra l'oggetto stream e il
dispositivo esterno non avviene quasi mai direttamente, ma attraverso un'area di
memoria in cui i dati vengono accumulati prima di essere trasferiti. Caso tipico la
gestione dell'input da tastiera (vedere: Introduzione all'I/O sui dispositivi
standard - Memorizzazione dei dati introdotti da tastiera). In generale la
presenza del buffer serve per migliorare l'efficienza delle operazioni, riducendo i
"tempi morti" fra I/O e calcolo effettivo. D'altra parte, questo fa s che il
programma non venga esattamente eseguito "in tempo reale", nel senso che
l'esecuzione dell'operazione non sincrona con il risultato. Abbiamo g visto
cosa succede con il buffer di input da tastiera; vediamo ora un esempio degli
effetti introdotti dalla presenza del buffer di output:
long tm = time(NULL)+5 ;

cout << "Aspetta 5 secondi ...." ;


while ( time(NULL) < tm ) ;
cout << " .... ecco fatto!" << endl ;
in realt, a causa del buffer, prima passano 5 secondi e poi le due scritte
appaiono contemporaneamente. Per ottenere il risultato voluto, bisogna
modificare la seconda istruzione in:
cout << "Aspetta 5 secondi ...." << flush ;
in quanto, come sappiamo, il manipolatore flush scarica immediatamente il
buffer di output. Lo stesso scaricato automaticamente quando si passa da
un'operazione di output a un'operazione di input (in altri termini, si dice che
gli oggetti stream cout e cin sono "collegati").
Cenni sulla gerarchia stream buffer
La Libreria Standard mette a disposizione un'altra gerarchia di classi, detta
"stream buffer", costituita da una classe base, che si chiama streambuf, e
dalle sue derivate, filebuf (per le operazioni di I/O su file) e stringbuf (per
le operazioni di I/O su stringa). A ogni oggetto di una classe della
gerarchia stream attached (associato) un oggetto di una classe della
gerarchia stream buffer (o sua derivata fornita dall'utente): le due classi
lavorano insieme, la prima per le operazioni di I/O ad "alto livello" (per esempio
la formattazione), la seconda per l'accesso al buffer di I/O e in generale per
l'I/O di "basso livello". In entrambe le classi esistono membri che gestiscono il
collegamento fra i due oggetti (per esempio il metodo rdbuf() di ios restituisce
l'indirizzo dell'oggetto di streambuf associato e il metodo in_avail() di
streambuf restituisce il numero di caratteri ancora presenti nel buffer).
Di solito il programmatore non ha bisogno di lavorare direttamente con gli
oggetti di streambuf e pu quasi sempre ignorarne l'esistenza. Tuttavia qualche
volta pu essere necessario accedere al buffer di I/O, per esempio se si deve
operare con particolari dispositivi e interfacce che richiedono software di I/O a
basso livello: spesso in questi casi necessario "progettare" una bufferizzazione
specifica, e conviene farlo derivando una nuova classe dalla gerarchia stream
buffer, piuttosto che dalla gerarchia stream, e associando gli oggetti della
nuova classe a quelli delle classi stream gi presenti nella Libreria.
Non ci dilungheremo oltre su questo argomento. Torniamo invece alle classi della
gerarchia stream e completiamo il discorso fornendo ulteriori ragguagli sulle
funzioni che gestitscono il buffer di I/O. Precisiamo anzitutto che la stessa
posizione corrente "si muove" in realt sul buffer e non direttamente
sull'oggetto (anche se il valore assoluto della posizione riferito all'inizio
dell'oggetto), e quindi alcuni metodi di gestione della posizione corrente, che
abbiamo gi visto (tellp, seekp, tellg e seekg), operano effettivamente sul
buffer. Inoltre, tutte le volte che si parlato di caratteri "rimossi" da un
oggetto stream (per esempio con il metodo ignore), in realt si intendeva dire
che erano "rimossi" dal buffer, non fisicamente dall'oggetto.
Gestione del buffer di output
Abbiamo gi visto visto praticamente "tutto" e cio i metodi (di ostream) tellp
(ricava la posizione corrente) e seekp (imposta la posizione corrente), e i
manipolatori flush, ends e endl.

Gestione del buffer di input


Oltre ai metodi di istream, tellg e seekg, gi visti, consideriamo i seguenti:

istream& istream::putback(char c)
inserisce c nel buffer prima della posizione corrente e arretra la
posizione corrente di 1; l'operazione valida solo se preceduta da
almeno una normale lettura (cio non si pu inserire un carattere prima
dell'inizio dell'oggetto); ritorna *this
istream& istream::unget()
come putback, con la differenza che rimette nel buffer l'ultimo carattere
letto
int istream::peek()
ritorna il prossimo carattere da leggere (senza toglierlo dal buffer e
senza spostare la posizione corrente); questo metodo (come anche i
precedenti) pu essere usato per riconoscere il tipo del prossimo dato
prima di leggerlo effettivamente (vedere esercizio).

Conclusioni

La programmazione modulare, la programmazione a oggetti e la


programmazione generica forniscono strumenti formidabili per scrivere codice
ad alto livello. La possibilit di suddivere un programma in porzioni (quasi)
indipendenti rende l'attivit dei programmatori pi facile, piacevole ed efficace e
rende il programma stesso pi flessibile, riutilizzabile, estendibile e di pi facile
manutenzione.
Fra tutti i linguaggi, il C++ quello che maggiormente permette di realizzare
questi obiettivi, grazie ai suoi potenti strumenti concettuali: data hiding,
namespace, classe, overload di funzioni e di operatori, eredit,
polimorfismo e template. Tuttavia, a differenza da altri linguaggi "puri" di
programmazione orientata a oggetti, il C++ non "rinnega" la "cultura" del C,
da cui eredita intatta la potenza e verso cui mantiene la compatibilit,
preservando un "patrimonio" di conoscenze e realizzazioni che non sarebbe stato
conveniente disperdere.
Pertanto il C++ un linguaggio insieme completo e in continua evoluzione: sul
solido impianto del C ha costruito una nuova "filosofia" che gli permette di
espandersi nel tempo. A tutt'oggi il C++ si utilizza praticamente in qualsiasi
dominio applicativo, inclusi quelli (a noi vicini) dell'insegnamento e della ricerca.
Terminiamo questo corso con una serie di consigli utili per un programmatore
C++ non ancora "esperto":

usa "poco" la direttiva #define; al suo posto usa:


o const e enum, per definire valori costanti;
o inline, per evitare la perdita di efficienza dovuta alle chiamate di
funzioni;
o template, per specificare famiglie di funzioni o di tipi;
o namespace, per evitare conflitti nei nomi.
non dichiarare una variabile locale molto prima di usarla; una
dichiarazione pu apparire ovunque possa apparire un'istruzione
non definire mai variabili globali; le variabili non locali siano sempre
definite in un namespace
evita le copie inutili: passa il pi possibile gli argomenti per riferimento;
se non vuoi che gli argomenti vengano modificati, dichiarali const
dimentica le funzioni del C di gestione della memoria dinamica
(malloc, free e compagnia) e al loro posto usa gli operatori new e
delete; per riallocare memoria, non usare la realloc del C, ma i metodi
resize o reserve di vector
suddividi il tuo programma in moduli indipendenti, usando i namespace;
se sei coinvolto in un grosso progetto, potrai sviluppare il software in modo
pi efficiente
ragguppa il pi possibile variabili e funzioni in classi, e usa gli oggetti,
istanze delle classi

gli oggetti sono componenti attive, con propriet e metodi; realizza il


data hiding, rendendo in generale private tutte le propriet e pubblici
solo i metodi che vengono chiamati dall'esterno
se una funzione agisce su un oggetto di una classe, rendila metodo di
quella classe; se non possibile, dichiarala friend (solo per se accede
a membri privati)
sfrutta l'overload degli operatori per definire operazioni fra gli oggetti
associa costruttori e distruttori alle classi che definisci
non ricominciare sempre "da zero": usa l'eredit quando vuoi espandere
un concetto, e la composizione quando vuoi riunire concetti esistenti
struttura la tua gerarchia di classi applicando il polimorfismo: potrai
aggiungere nuove classi senza modificare il codice esistente; non
dimenticare di dichiarare virtual il distruttore della classe base
usa i template quando devi progettare una funzione o una classe da
applicare a tipi diversi di oggetti
minimizza l'uso degli array e delle stringhe del C; la Libreria Standard
del C++ mette a disposizione le classi vector e string, che sono pi
versatili e pi efficienti; in generale, non tentare di costruire da solo quello
che gi fornito dalla Libreria (difficilmente potresti raggiungere il suo
livello di ottimizzazione)
la Standard Template Library fornisce un insieme di classi
(contenitori) e funzioni (algoritmi) che, in quanto template, si
possono applicare a una gamma molto vasta di problemi applicativi: non
farti mai sfuggire l'occasione di utilizzarla!
evita le funzioni di I/O del C; usa le classi di flusso e i relativi
operatori: sono pi facili, pi eleganti e possono avere overload

Vous aimerez peut-être aussi