You are on page 1of 66

PROGRAMMAZIONE AD OGGETTI

di Niki Andreozzi, Marco Catallo, Alessandro Iannacone.

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. 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 di32 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 tipidi 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 2mvalori). 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 asigned 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 consigned char.

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 ambitiinnestati), 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 variabileglobale 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 dallafunzione 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 nelladefinizione 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 variabilestatiche sono inizializzate di default a zero (in modo appropriato al tipo). [p07]

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 anchenella dichiarazione in cui la costante inizializzata (ovviamente, come per le variabili, l'inizializzazione deve essere presente una sola volta).

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 ditipo 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 warningin 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 certaoperazione, 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, laconversione 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 floatavvenga prima della divisione, e che questa fornisca il risultato esatto (cio 2.5), dobbiamo convertire esplicitamentealmeno uno dei due operandi e quindi riscrivere cos la terza istruzione: (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 dicasting, suddividendo le conversioni di tipo in altrettante categorie e riservando un operatore per ciascuna di esse (per fornire al r = (float)m/n+a;

compilatore strumenti di controllo pi raffinati). D'altra parte il C-style 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.

Funzioni con overload A differenza dal C, il C++ consente l'esistenza di pi funzioni con lo stesso nome, che sono chiamate: "funzioni conoverload". Il compilatore distingue una funzione dall'altra in base alla lista degli argomenti: due funzioni con overloaddevono 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 funzionidiverse, 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 ammessefunzioni 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). 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 modorandomatico), 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 unafunzione 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 diB, 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 altropacchetto, "impilato" sopra il precedente, come nel seguente schema (tralasciamo le aree riservate alle variabiliautomatiche): Area stack Commenti Indirizzo di rientro in B Argomento 1 passato a C La funzione B chiama la funzione C con due 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 argomenti Argomento 2 passato a 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 funzionicomunque, sia che A, B e C siano funzioni diverse, sia che si tratti della stessa funzione (ogni volta va a cercare nellostack 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 talifunzioni 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); }
Fattoriale in pps

<----- istr. di controllo

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; }

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 controlstring), seguito eventualmente dagliargomenti 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) una chiamata.

sull'effettivo

numero

diargomenti usati in

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'headerfile <stdarg.h> ; per memorizzare i valori degli argomenti opzionalitrasmessi 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 argomentiopzionali. 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) ;

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 unadirettiva, 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. Ognitranslation 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.

Puntatori a costante Nelle definizioni di una variabile puntatore, lo specificatore di tipo const indica che deve essere considerata costantela variabile puntata (non il puntatore!). Es.: definisce il puntatore variabile ptr a costante float. const float* ptr;

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 l-value, anche se la variabile puntata non const. Es.: int datov=50; const int datoc=50; int* ptv; const int* ptc; ptc = &datov; ptv = &datoc;
*ptc

(datov una variabile int) (datoc una costante int) (ptv un puntatore a variabile int) (ptc un puntatore a costante int) (valida, in quanto le conversioni da int* a const int* sono ammesse) (non valida, in quanto le conversioni da const int* a int* non sono ammesse) (deref. l-value non valida, anche se ptc punta a una variabile) (deref. r-value valida, scrive 10)

= 10;

datov=10; cout << *ptc;

Puntatori costanti I puntatori costanti si definiscono specificando l'operatore di dichiarazione * const (al posto di *) Es.: definisce il puntatore costante ptr a variabile float float* const ptr;

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

= 10;

(dato1 e dato2 sono due variabili int) (ptr un puntatore costante, inizializzato con l'indirizzo di dato1) (valida, in quanto ptr punta a una variabile) (non valida, in quanto ptr costante)

ptr = &dato2;

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 unpuntatore costante a costante (di uso piuttosto raro). Es.: const char dato='A'; (dato una costante char, inizializzata con 'A') const char* const ptr = &dato; (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 coninizializzazione; come noto, infatti, la trasmissione by value comporta una creazione per copia, che equivale alladichiarazione 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 acostante 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 aliasdi 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 nelprogramma 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 glioperatori 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

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 nuovotipo, 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; ecc... 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 funzionichiamanti 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. void fsel1(tpfunz pfunz) { ... if(pfunz(4,a,"Ciao",b,pc,m)) .... }

Es:

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; Strutture Come gli array, in C++ (e in C) le strutture sono gruppi di dati; a differenza dagli array, i singoli componenti di unastruttura 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 iltag 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 noncrea 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 ognioggetto in quantit pari alla somma delle memorie che

competono ai singoli membri della struttura (l'operazionesizeof(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 delpreprocessore 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 comeoperandi 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 rightoperand 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

ana3.anni = 27;

del membro anni dell'oggetto ana1 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, l'indirizzo del membro anni dell'oggettoana1; nel caso definisce un puntatore al tipo-struttura anagrafico, l'indirizzodell'oggetto ana1. che inizializza con secondo che inizializza con

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'operazionesia: *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 unpuntatore 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 rightoperand) di un oggetto, istanza di unastruttura, il cui indirizzo dato nel leftoperand (ovviamente anche questo operatore pu restituire sia un r-value che un lvalue). 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 talestruttura al compilatore. Le soluzione pi semplice definire la struttura a cui appartiene il membro prima dellastruttura 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 unastruttura consiste in una istruzione in cui appaiono esclusivamente la parolachiave struct e l'identificatore dellastruttura. Es.: struct data ;

chiaramente si tratta di una dichiarazione-non-definizione (questo il terzo caso che incontriamo, dopo le dichiarazionidi variabili con le specificatore extern e le dichiarazioni di funzioni), nel senso che non rende ancora la strutturautilizzabile, 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 distrutture 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 imembri coinvolti sono puntatori. Per esempio, una sequenza corretta potrebbe essere: struct data ; struct persona { char nome[20]; data*pnascita;} ;

dichiarazione anticipata della struttura data definizione della struttura principale persona co un membropuntatore a data struct data { int giorno; int mese; int anno;person definizione della struttura data con a caio; } ; 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 puntatorialla struttura stessa (per esempio, quando si devono costruire delle liste concatenate). In questo caso, poi, ladichiarazione anticipata non serve in quanto il compilatore conosce gi il nome della struttura che appare all'inizio della sua definizione. No La dipendenza circolare si pu avere anche fra le funzioni (una funzione A che ta: 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 tipodata e assegnare il suo indirizzo a tizio.pnascita)

accesso: tizio.pnascita->anno = 1957; come si pu notare dall'esempio, il numero 1957 stato nel membro anno dell'oggetto il cui indirizzo si nel membro puntatore pnascita dell'istanza tizio della struttura persona.

inserito trova

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 classiereditate, 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 imembri 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 membrisuccessivamente 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 dimanutenzione 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 ainomi 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 distrutture 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. 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! Es.:

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 funzionemembro, una funzione-membro pubblica pu anche essere chiamatadall'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 (funzioni-membro), 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 pubblicadi 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 didefault), 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 class point { double x; double y; public: Esempio del secondo modo class point { double x; double y; 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'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) ; l'esempio, definiamo ora

Risoluzione della visibilit

Se il codice di un metodo si trova all'esterno della definizione della classe a cui appartiene, bisogna "qualificare" ilnome 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 stessoambito 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 imetodi 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 header-files, 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 dellafunzione (sia nella dichiarazione che nella definizione). Riprendendo l'esempio della classe point, aggiungiamo lafunzionemembro get: void point::get(double& x0, doubl e& y0) const

{ x0 = x ; y0 = y ; } la funzione-membro get non pu modificare i membri della sua classe. [p43][p43] [p43]

Classi membro Una classe pu anche essere definita all'interno di un'altra classe (oppure semplicemente dichiarata, e poi definitaesternamente, 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: classimembro 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 essereistanziate 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 classedi 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 chefunzioni 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, inomi 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 dallaparola-chiave this) ogni volta che incontra la chiamata di una funzione-membro, e inserendo lo stesso puntatorecome primo argomento nella funzione. Chiariamo quanto detto con il seguente esempio, in cui ogg un'istanza di una certa classe myclass e init() unafunzione-membro che utilizza un dato-membro x, entrambi della stessa classe myclass: la definizione della funzione: viene trasformata in: void myclass::init() {..... x = .....} 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'unoconst 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 dellafunzione in memoria uno solo, cio non ne esiste uno per ogni oggetto come per i dati-membro). Pertanto,

leoperazioni 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. 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 dellaclasse 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 DEFINIZIONE FUNZIONE

class A { void amica(A ogg, .....) int mp ; .......... { friend void amica(A, .....) ; ........ ogg.mp ........ ........ }; } la funzione amica, che non un metodo della classe A (nell'esempio definita nel namespace globale), tuttaviadichiarata 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 suglioggetti 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 ditutte 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 ladichiarazione anticipata: class B; oppure si inserisce, nel prototipo che potrebbe generare errore, la parola-chiave 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 friendcollegate.

Classi friend

Quando tutte le funzioni-membro di una classe B sono friend di una classe A, possibile, anzich dichiarare ciascunafunzione 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 diA), 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. 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 definizionesi trova. 2. Un oggetto locale statico viene costruito la prima sua definizione viene incontrata durante l'esecuzione e distrutto una sola volta, quando il programma termina. volta che la del programma,

3. Un oggetto allocato nella memoria dinamica (area heap ) viene costruito mediante l'operatore new e distruttomediante l'operatore delete. 4. Un oggetto, membro non statico di una classe, viene costruito ogni volta che (o meglio, immediatamente primache) viene costruito un oggetto della classe di cui membro, e distrutto ogni volta che (o meglio, immediatamentedopo 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 costruitoo 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 allocarladinamicamente 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 edistruttori 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 uncostruttore 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 degliargomenti. Come tutte le funzioni in overload, non sono ammessi costruttori che differiscano solo per gliargomenti di default. 5. devono essere dichiarati come funzioni-membro pubbliche, in quanto sono sempre chiamati dall'esterno dellaclasse 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 : point::point( ) {x=3.5; y= definizione del costruttore di default di point: 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 costruttoredi default e perci questo, se necessario, va esplicitamente definito come metodo della classe. In sua assenza, icostruttori 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 deicostruttori 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 ladefinizione 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 : point p (3.5, 2.1); seconda forma : 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 tramitechiamata 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 alparametro 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 : 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 degli operatori in overload (come vedremo prossimamente). La conversione implicita pu essere nella dichiarazione (non nella definizione esterna) delcostruttore lo specificatore explicit : nella definizione premettendo, point::point(double d) {x=d; y=d;}

esclusa

explicit point(double); il casting continua invece ad essere ammesso (anche nella forma in C-style), in quanto coincide puramente con lachiamata 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 (~) prototipo: ~point( ); definizione esterna: point::~point( ) {......} 2. non bisogna specificare il tipo di ritorno (neanche void) 3. non ammettono argomenti 4. ciascuna classe pu avere al massimo un distruttore 5. devono essere dichiarati come funzioni-membro pubbliche, in quanto sono sempre chiamati dall'esterno dellaclasse a cui appartengono. Come i costruttori, i distruttori non sono obbligatori; sono richiesti quando necessario liberare risorse allocate daglioggetti o ripristinare le condizioni preestistenti alla loro creazione. Se esiste, un distruttore sempre chiamatoautomaticamente 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 oggettiautomatici 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) . [p48]

Membri puntatori Una particolare attenzione va rivolta alla programmazione dei costruttori e del distruttore di un oggetto che contienemembri puntatori. Infatti, a differenza dal caso degli array, l'area puntata non definita automaticamente e quindi (a meno che al puntatorenon venga successivamente assegnato l'indirizzo di un'area gi esistente) capita quasi sempre che l'area debba essereallocata 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; char* cognome; public: Persona (int); ~Persona ( ); .... altri metodi };

COSTRUTTORE Persona::Persona (int n) { nome = new char [n]; cognome = new char [n]; }

DISTRUTTORE Persona::~Persona ( ) { delete [ ] nome; 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 suoimembri. In aggiunta, il costruttore dell'oggetto alloca nella memoria heap due aree di 25 byte, e sistema i rispettiviindirizzi nei membri puntatori Tizio.nome e Tizio.cognome. Quando l'oggetto Tizio va out of scope, il distruttoreentra in azione automaticamente e, con l'operatore delete, libera la memoria heap allocata per le due aree. Senza ildistruttore, sarebbe stata liberata soltanto la memoria stack occupata dall'oggetto Tizio e dai suoi membri puntatori , ma non l'area heap indirizzata da questi. [p49]

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, un oggetto preesistente q:

se definiamo un oggetto p e

lo inizializziamo con

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 puntatoredell'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 copiache 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: A(const A&); pa = new int ; *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 sarebbecopiato il puntatore a1.pa in a2.pa. [p50]

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 soloinizializzare, anche se la distinzione pu sembrare solo formale). La sintassi di una lista di inizializzazione si desume dal seguente esempio: CLASSE class A { COSTRUTTORE A::A(int p, double q) : m1(p), m2(0), r(q)

int m1, m2; { double r; .... eventuali altre operazioni.... public: } A(int,double); ........ }; Notare che alcuni membri possono essere inizializzati con valori costanti, altri con i valori degli argomenti passati alcostruttore. 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. [p51][p51] [p51]

Membri oggetto Riprendiamo ora ad esaminare l'elenco presentato all'inizio di questo capitolo e consideriano la costruzione e distruzionedegli 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 { class C { int aa; ........ }; A ma; class B { B mb; int bb; ........ }; 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 costruttorecompleti l'operazione, sono eseguiti automaticamente i costruttori che inizializzano i membri delle classi compo nenti. Se esistono e si vogliono utilizzare i costruttori di default, non esiste problema. Ma se deve essere chiamato uncostruttore con argomenti, ci si chiede in che modo tali argomenti possano essere passati, visto che il costruttore di un membrooggetto non chiamato esplicitamente.

In questi casi, spetta al costruttore della classe composta provvedere a che vengano eseguiti correttamente anche icostruttori delle classi componenti. Per ottenere ci, deve includere, nella sua lista di inizializzazione, tutti (e soli) imembri-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 rispettivocostruttore. 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 oggettipropri. In questo caso il costruttore di C pu generare i suoi membrioggetto copiando oggetti gi costruiti delleclassi 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 ilcostruttore 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 imembri delle classi componenti, in ordine inverso a quello della loro costruzione. Utilit dei costruttori e distruttori Poich in C++ ogni oggetto ha una sua precisa connotazione, caratterizzata da propriet e metodi, i costruttori e idistruttori 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.

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 aitipi 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'espressioneiniziale, costituita da solo due operazioni: a+b*c operatori in overload : 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 senzablanks in mezzo, dal simbolo dell'operatore (es.: operator+). Gli argomenti della funzione devono corrispondere aglioperandi 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 secondoargomento 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 delladivisione (/) 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 operatoreapplicare. 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 : funzione somma : p = p1+p2 ; 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 costruttoredisponibile (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 talifunzioni 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 membriprivati). 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 lvalue, come accade per esempio per gli overload deglioperatori di assegnazione (=) e in notazione compatta (+= ecc...). Viceversa, non ha molto senso che sia un metodol'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 leoperazioni che si applicano su un unico oggetto o che modificano il loro primo operando, e usare funzioni esterne (ofriend) per le altre operazioni; il codice di queste funzioni risulta per facilitato, in quanto pu utilizzare gli stessioperatori 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 delpuntatore nascosto this, che, come

sappiamo, punta automaticamente Ne consegue che:

allo

stesso oggetto della classe in inserito dal C++ come

cui

il metodo incapsulato e viene primo argomento della funzione.

1. un operatore in overload pu essere implementato come metodo di una classe solo se il primo operando unoggetto della stessa classe; in caso contrario deve essere una funzione esterna (dichiarata friend se accede amembri privati) ; 2. nella definizione del metodo il numero degli argomenti deve essere ridotto di un'unit rispetto al numero dioperandi; in pratica, se l'operatore binario, ci deve essere un solo argomento (quello corrispondente al secondooperando), 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 : definizione metodo : p += p1 ; 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 ditipo double; 4. la funzione ritorna l'oggetto stesso p (deref. di this), by reference (cio come lvalue), modificato dall'operazione (non esistono problemi di lifetime in questo caso, essendo l'oggetto p definito nel chiamante); 5. la chiamata della funzione nella tradotta dal operator+=(&p,p1) ; forma tradizionale sarebbe: p.operator+=(p1) ; compilatore in:

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 : funzione somma : p = p1+p2 ; point operator+(const point& p1, const point& p2) { point ptemp = p1; (uso il costruttore di copia) return ptemp += p2 ; } [p54]

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'operazionedi 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 formaesadecimale, 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-operanddell'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 unmetodo di A, ma deve essere dichiarata come funzione friend nella definizione di A; viceversa, gli overloaddell'operatore << con tipi nativi (e loro puntatori) sono definiti nella stessa classe ostream, e quindi sonometodi di quella classe; 2. il valore di ritorno della funzione trasmesso by-reference, in quanto deve essere un l-value di successiveoperazioni impilate; 3. poich nel chiamante il primo argomento l'oggetto cout, il ritorno byreference 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 impilarepi 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 globalecin. Notare che in questo caso il secondo argomento (cio a), sempre passato by-reference, non dichiarato const, in quanto l'operazione lo deve modificare. 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 operatoredi assegnazione di default, che copia membro a membro l'oggetto rightoperand nell'oggetto left-operand. No In alcune circostanze si potrebbe non desiderare che ta 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 sezioneprivata 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 unpuntatore pu generare due problemi: dopo la copia, l'area precedentemente puntata dal membro puntatore del leftoperand 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 dueoggetti, 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 didefault, 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 diassegnazione: COSTRUTTORE DI COPIA operazioni : A a1 ; ........ A a2 = a1 ; OPERATORE DI ASSEGNAZIONE A a1 , a2 ; ........ a2 = a1 ; A& A::operator=(const A& a) CLASSE class A { int* pa; A::A(const A& a) int dim; public: A( ); A(const A&); A& operator= (const A&); ........ }; {

dim = a.dim ; pa = new int [dim] ; for(int i=0; i < dim; i++) *(pa+i) = *(a.pa+i) ; }

if (this == &a) return *this; if (dim != a.dim) { delete [] pa; dim = a.dim ; 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 necessariodeallocare 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. [p62]

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 unpuntatore 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, a1debba essere assegnato a un altro oggetto a2, oppure un altro oggetto a2 debba essere costruito e inizializzato cona1. 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 vengonocreati 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 eseguirematerialmente 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 diottimizzare 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 glioperatori 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 strutturaArep) 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 Aagganciati; questo numero, inizializzato con 1, viene incrementato ogni volta che "prenotata" una copia, edecrementato 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 Ada 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 unoggetto di A deve decrementare il contatore di agganci nel corrispondente oggetto di Arep, e poi procedere alladistruzione 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 degliargomenti 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 allarappresentazione 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 a1dal 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 unarappresentazione di una classe deve essere accessibile solo dalla classe stessa. Pertanto Arep deve essere pubblica perA e privata per il "mondo esterno". Per ottenere questo, bisogna definire Arep "dentro" A (struttura-membro ostruttura 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.

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, pereredit, 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: ilC++ 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 oderivata, 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 statadefinita), inserendo, dopo il nome della classe e prima della parentesi graffa di apertura, il simbolo ":" seguito dal nomedella 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 quelliereditati 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 classeAB 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, due classi genitrici vanno indicate entrambe, separate da una class AB : A3, B4 { ........ } ;

le virgola:

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 essere ereditati; tutti i membri seguenti sono pubblici, e possono

protected: indica che tutti i membri seguenti sono protetti, nel senso che sono privati, ma possono essereereditati; Quindi, un membro protetto inaccesibile dall'esterno, come i membri privati, ma pu essere ereditato, come imembri pubblici. In realt, esiste un'ulteriore restrizione, che ha lo scopo di rendere il data-hiding ancora pi profondo: l'accessibilit deimembri 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 membroereditato 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: private protected public

Accessibilit dei membri ereditati inaccessibili inaccessibili inaccessibili privati protetti protetti

public:

privati

protetti

pubblici

e quindi un membro ereditato pubblico solo se public: nella classe base e l'accesso della classe derivata allaclasse 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 membripubblici e protetti, ereditati nella classe derivata, siano accessibili unicamente da funzioni membro e friend dellaclasse 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 funzionimembro e friend della classe derivata e di altre eventuali classi derivate da questa, occorre che lo specificatore di accesso alla classe base sia protected. [p66]

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) oggettidi 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 assegnargliin seguito (in base al flusso del programma) l'indirizzo di un oggetto appartenente a una qualunque classe dellagerarchia. 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 tipoA); 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

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 davariabile a costante). Per esempio, il costrutto catch con tipo di argomento X "cattura" le eccezioni di tipo Y (con Ydiverso 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). [p67]

b.mb = .......

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 diassegnazione, 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 classigerarchicamente 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'oggettoe 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 unachiamata esplicita: se eseguito il costruttore di default, tutto bene, ma come fare se si vuole (o si deve) eseguire uncostruttore 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 classiereditate 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 classigerarchicamente 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 classiereditate: ogni costruttore di una classe derivata deve

includere nella lista di inizializzazione la chiamata delcostruttore 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 { DEFINIZIONE DEL COSTRUTTORE DI A protected: A::A(int p, float q) : m1(q), m2(p) float m1; { .... eventuali altre operazioni del int m2; 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 { DEFINIZIONE DEL COSTRUTTORE DI B int n; B::B(int a, int b, float c) : n(b), A(a,c) public: B(int,int,float); { .... eventuali altre operazioni del .... altri membri .... }; 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 delcostruttore 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 istanziataindipendentemente. 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 peroccultare 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 suaclasse 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 nomemem, 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 bisogna qualificare il nome del membro comune solitooperatore di risoluzione della visibilit. uno dei due, mediante il 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 imetodi): 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. [p68]

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 intnella 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 da10.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 classederivata e la funzione della classe base accessibile nella classe derivata per eredit. Eredit multipla e classi basi virtuali

quindi

Supponiamo che una certa classe C derivi, per eredit multipla, da due classi genitrici B1 e B2. Nella definizione diC, 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 definireesplicitamente il costruttore di default di C (anche con "corpo nullo"), con il solo compito di costruire le classigenitrici (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 aC. 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 ilsizeof(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 tramiteB1 o tramite B2. Il secondo problema pu essere risolto (in un modo per poco "brillante") qualificando ogni volta i membri ereditati daA. 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 propriaclasse 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 parolachiavevirtual 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:

[p69][p69] [p69] 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'ereditmultipla possa essere necessaria per la risoluzione di molti problemi progettuali, fornendo la possibilit di associare dueclassi altrimenti non correlate come parti dell'implementazione di una terza classe. Questo fatto evidente in modo particolare quando le due classi giocano ruoli logicamente distintipolimorfismo: 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 dioverload, 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 dueistanze, 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 stessaclasse 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 oggettidi 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 lafunzione 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 ilpolimorfismo: 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 parolachiave nelledichiarazioni 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, unaclasse 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 unoggetto 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 unoggetto di B.

Tabelle delle funzioni virtuali Riprendiamo l'esempio precedente, aggiungendo una nuova classe derivata da A, che chiamiamo C; questa classe nonridefinisce la funzione display() ma la eredita da A (come appare nella seguente tabella, dove il termine fra parentesi quadre facoltativo):

class A { ........ public: ...... virtual void display(); };

class B : public A { ......... public: ...... [virtual] void display(); };

class C : public A { .............. };

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.

In questo modo, in fase di esecuzione il C++ pu risalire, dall'indirizzo contenuto nel membro vptr dell'oggetto puntato da ptr (vptr un dato-membro 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 glioggetti 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, statoassegnato 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 diesecuzione. Vediamo ora un'applicazione pratica di quanto detto. Riprendendo il nostro solito esempio, supponiamo che la classebase 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 copiadi A (oppure quello di default se la classe ne sprovvista) con argomento *this, e ne restituisce l'indirizzo. Ognioggetto 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, nella definizione della classe base A la dichiarazione: inserendo

virtual A* clone(); troviamo in B la ridefinizione di una funzione virtuale, in quanto sono coincidenti il nome (clone), la lista degliargomenti (void) e il tipo del valore di ritorno (A*), e quindi possiamo ottenere da tale funzione un comportamentopolimorfo. 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 stessomessaggio.

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 vannodichiarate 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 ogniclasse 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).

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 itipi degli argomenti delle funzioni-membro. La stessa Libreria Standard del C++ mette a disposizione strutture precostituite di classi template, dette classicontenitore (liste concatenate, mappe, vettori ecc...) che possono essere utilizzate specificando, nella creazione deglioggetti, i valori reali da sostituire ai tipi parametrizzati.

Definizione di una classe template Una classe (o struttura) template alla definizione della classe, dell'espressione: identificata dalla presenza, davanti

template<class T> dove T (che un nome e segue le normali regola di specifica degli identificatori) rappresenta il parametro di un tipogenerico che verr utilizzato nella dichiarazione di uno o pi membri della classe. In questo contesto la parola-chiaveclass 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 funzione-membro 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: costruttore inline con un argomento di A(const T& m) : mem(m) { } 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 notare che il nome del parametro pu >::get( ) { anche essere diverso da quello usato nella

return mem ;

definizione della classe

} NO Nella definizione della funzione get la ripetizione del parametro par nelle TA espressioni template<class par> eA<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), ilnome 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 comeargomento 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 nelledefinizioni esterne delle sue funzioni-membro, 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 stessoambito 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 creaistanziando 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 nelledichiarazioni (e definizioni) di tutti i membri di A in cui tale parametro compare. Quindi la classe "reale" non A, maA<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 templateA, 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 classeA 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 esseredichiarate (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 alparametro 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 glielementi dell'array. Seguitando nell'esempio, allo scopo di evidenziare tutta la "potenza" dei template confrontiamo ora la nostra funzionecon un'analoga funzione di ordinamento, tratta dalla Run Time Library (che la libreria standard del C). Il linguaggioC, che ovviamente non conosce i template n l'overload degli operatori, pu rendere applicabile lo stesso algoritmodi ordinamento a diversi tipi facendo ricorso agli "strumenti" che ha, e cio ai puntatori a void (per generalizzare iltipo dell'array) e ai puntatori a funzione (per dar modo all'utente di fornire la funzione di confronto fra gli elementidell'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 afunzione 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 afunzione, 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 dellachiamata. Riprendendo l'esempio della funzione sort, la sequenza: double a[10] = { .........}; sort(10, a); crea automaticamente un'istanza della funzione template sort, con argomento double dedotto dalla stessachiamata 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>( ) ; 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 template<cla int m = fun1<int Il secondo argomento dedotto di tipo double ss T,class U> >(d); T fun1(U); template<cla int m = Il primo argomento non si pu omettere, ss T,class U> fun2<double,int anche se deducibile >(d); U fun2(T); 3. 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 dicompilatore 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 stessoargomento 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. Per maggior chiarimento, vediamo ora alcuni esempi di chiamate di funzioni e di scelte conseguenti operate dalcompilatore, 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 fun<int>(1,2); NOTE argomento dedotto, corrispondenza esatta funzione tradizionale, preferita unica

fun(1.1,2.3); fun('A',2);

fun(1.1,2.3);

fun(double('A'),double(2) funzione tradizionale, ); possibile

fun<char>(69,71. fun<char>(char(69),char( argomento esplicito, 2); 71.2)); conversioni ammesse definite le seguenti variabili: int a = ...; fun(a,c); fun(a,p); fun<int>(a,c); ERRORE const int c = ...; int* p = ...;

argomento dedotto, conversione "banale" conversione non ammessa da int* a double

Template e modularit

In relazione alla ODR (One-Definition-Rule), le funzioni template (e le funzionimembro 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 unitsdel programma. N potrebbe essere diversamente. Infatti, come si detto, i template sono istanziati staticamente, cio a livello dicompilazione, e quindi il codice che utilizza un template deve essere nella stessa translation unit del codice che lodefinisce. 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 headerfiles che includano anche il codice di implementazione in forma sorgente). Queste regole, per, contraddicono il principio fondamentale della programmazione modulare, che stabilisce laseparazione 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 parolachiave: "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.

You might also like