Programmazione a oggetti
I costrutti analizzati fin'ora costituiscono gia` un
linguaggio che ci consente di realizzare anche programmi complessi e di
fatto, salvo alcune cose, quanto visto costituisce il linguaggio C;
tuttavia il C++ e` molto di piu` e offre caratteristiche nuove che
estendono e migliorano il C: programmazione a oggetti, RTTI (Run Time Type Information), template (modelli) e programmazione generica, gestione delle
eccezioni. Si potrebbe apparentemente dire che si tratta solo di qualche
aggiunta, in realta` nessun'altra affermazione potrebbe essere piu` errata:
le eccezioni semplificano la gestione di situazioni anomale a run time,
mentre il supporto alla programmazione ad oggetti e alla programmazione
generica (e cio` che ruota attorno ad esse) rivoluzionano addirittura il
modo di concepire e realizzare codice e caratterizzano il linguaggio fino a
influenzare il codice prodotto in fase di compilazione (notevolmente
diverso da quello prodotto dal compilatore C).
Inizieremo ora a discutere dei meccanismi offerti dal C++ per la
programmazione orientata agli oggetti. Per coloro che non avessero
conoscenze in merito ai principi che stanno alla base di tale
filosofia e` presente una breve appendice a scopo
puramente introduttivo e assolutamente non completa (una trattazione
approfondita richiederebbe ben altro spazio).
Strutture e campi funzione
La programmazione orientata agli oggetti (OOP)
impone una nuova visione di concetti quali "Tipo di dato" e "Istanze di
tipo". Sostanzialmente mentre altri paradigmi di programmazione vedono
le istanze di un tipo di dato come una entita` passiva, nella
programmazione a oggetti invece tali istanze diventano a tutti gli effetti
entita` (oggetti) attive.
L'idea e` che non bisogna piu` manipolare direttamente i valori di una
struttura (intesa come generico contenitore di valori), meglio lasciare che
sia la struttura stessa a manipolarsi e a compiere le operazioni per noi.
Tutto cio` che bisogna fare e` inviare all'oggetto un messaggio che
specifichi l'operazione da compiere e attendere poi che l'oggetto stesso ci
comunichi il risultato. Il meccanismo dei messaggi viene sostanzialmente
implementato tramite quello della chiamata di funzione e l'insieme dei
messaggi cui un oggetto risponde viene definito associando al tipo dell'oggetto un insieme di funzioni.
In C++ cio` puo` essere realizzato tramite le strutture:
|
struct Complex {
float Re;
float Im;
// Ora nelle strutture possiamo avere
// dei campi di tipo funzione;
void Print();
float Abs();
void Set(float PR, float PI);
};
|
Cio` che sostanzialmente cambia, rispetto a quanto visto, e` che una
struttura puo` possedere campi di tipo funzione (detti funzioni
membro oppure metodi) che costituiscono insieme ai campi
ordinari (membri dato o attributi) l'insieme dei messaggi
(interfaccia) a cui quel tipo e` in grado di rispondere. L'esempio
non mostra come implementare le funzioni membro, per adesso ci basta sapere
che esse vengono definite da qualche parte fuori dalla dichiarazione di
struttura in modo pressocche` identico alle ordinarie funzioni.
Una funzione dichiarata come campo di una struttura puo` essere invocata
ovviamente solo se associata ad una istanza della struttura stessa, dato
che quello che si fa e` inviare un messaggio ad un oggetto. Cio` nella
pratica si fa tramite la stessa sintassi utilizzata per selezionare un
qualsiasi altro campo (solo che ora ci sono anche campi funzione):
|
Complex A;
Complex* C;
A.Set(0.2, 10.3);
A.Print();
C = new Complex;
C -> Set(1.5, 3.0);
float FloatVar = C -> Abs();
|
Nell'esempio viene mostrato come inviare un messaggio: la quarta riga invia
il messaggio Print() all'oggetto A, l'ultima
invece invia il messaggio Abs() all'oggetto puntato da
C e assegna il valore ottenuto alla variabile
FloatVar. Anche la terza riga invia un messaggio ad A, in questo caso il messaggio richiede dei parametri
che vengono forniti nello stesso modo in cui vengono forniti alle
funzioni.
Il vantaggio principale di questo modo di procedere e` il non doversi piu`
preoccupare di come e` fatto quel tipo, se si vuole eseguire una operazione
su una sua istanza (ad esempio visualizzarne il valore) basta inviare il
messaggio corretto, sara` l'oggetto in questione ad eseguirla per noi.
Ovviamente perche` tutto funzioni e` necessario evitare di accedere
direttamente agli attributi di un oggetto, altrimenti crolla uno dei
capisaldi della OOP, e sfortunatamente per noi il meccanismo delle
strutture consente l'accesso diretto a tutto cio` che fa parte della
dichiarazione di struttura, annullando di fatto ogni vantaggio:
|
// Con riferimento agli esempi riportati sopra:
A.Set(6.1, 4.3); // Setta il valore di A
A.Re = 10; // Ok!
A.Im = .5; // ancora Ok!
A.Print();
|
Sintassi della classe
Il problema viene risolto introducendo una
nuova sintassi per la dichiarazione di un tipo oggetto.
Un tipo oggetto viene dichiarato tramite una dichiarazione di
classe, che differisce dalla dichiarazione di struttura sostanzialmente
per i meccanismi di protezione offerti; per il resto tutto cio` che si
applica alle classi si applica allo stesso modo alla dichiarazione di
struttura senza alcuna differenza.
Vediamo dunque come sarebbe stato dichiarato il tipo Complex
tramite la sintassi della classe:
|
class Complex {
public:
void Print(); // definizione eseguita altrove!
/* altre funzioni membro */
private:
float Re; // Parte reale
float Im; // Parte immaginaria
};
|
La differenza e` data dalle keyword public e private che
consentono di specificare i diritti di accesso alle dichiarazioni che le
seguono:
- public: le dichiarazioni che seguono questa keyword sono
visibili sia alla classe che a cio` che sta fuori della classe e
l'invocazione (selezione) di uno di questi campi e` sempre possibile;
- private: tutto cio` che segue e` visibile solo alla classe
stessa, l'accesso ad uno di questi campi e` possibile solo dai
metodi della classe stessa;
come mostra il seguente esempio:
|
Complex A;
Complex * C;
A.Re = 10.2; // Errore!
C -> Im = 0.5; // Ancora errore!
A.Print(); // Ok!
C -> Print() // Ok!
|
Ovviamente le due keyword sono mutuamente esclusive, nel senso che alla
dichiarazione di un metodo o di un attributo si applica la prima keyword
che si incontra risalendo in su; se la dichiarazione non e` preceduta da
nessuna di queste keyword, il default e` private:
|
class Complex {
float Re; // private per
float Im; // default
public:
void Print();
/* altre funzioni membro*/
};
|
In realta` esiste una terza categoria di visibilita` definibile tramite la
keyword protected (che pero` analizzeremo quando parleremo di
ereditarieta`); la sintassi per la dichiarazione
di classe e` dunque:
class <NomeClasse> {
public:
<membri pubblici>
protected:
<membri protetti>
private:
<membri privati>
}; // notare il punto e virgola finale!
Non ci sono limitazioni al tipo di dichiarazioni possibili dentro una delle
tre sezioni di visibilita`: definizioni di variabili o costanti
(attributi), funzioni (metodi) oppure dichiarazioni di tipi (enumerazioni,
unioni, strutture e anche classi), l'importante e` prestare attenzione
a evitare di dichiarare private o protected cio` che deve
essere visibile anche all'esterno della classe, in particolare le
definizioni dei tipi di parametri e valori di ritorno dei metodi
public.
Definizione delle funzioni membro
La definizione dei metodi di una classe puo`
essere eseguita o dentro la dichiarazione di classe, facendo seguire alla
lista di argomenti una coppia di parentesi graffe racchiudente la sequenza
di istruzioni:
|
class Complex {
public:
/* ... */
void Print() {
if (Im >= 0)
cout << Re << " + i" << Im;
else
cout << Re << " - i" << fabs(Im);
// fabs restituisce il valore assoluto!
}
private:
/* ... */
};
|
oppure riportando nella dichiarazione di classe solo il prototipo e
definendo il metodo fuori dalla dichiarazione di classe, nel seguente modo
(anch'esso applicabile alle strutture):
|
/* Questo modo di procedere richiede l'uso
dell'operatore di risoluzione di scope e l'uso del
nome della classe per indicare esattamente quale
metodo si sta definendo (classi diverse possono
avere metodi con lo stesso nome). */
void Complex::Print() {
if (Im >= 0)
cout << Re << " + i" << Im;
else
cout << Re << " - i" << fabs(Im);
}
|
I due metodi non sono comunque del tutto identici: nel primo caso
implicitamente si richiede una espansione inline del codice della funzione,
nel secondo caso se si desidera tale accorgimento bisogna utilizzare
esplicitamente la keyword inline nella definizione del metodo:
|
inline void Complex::Print() {
if (Im >= 0)
cout << Re << " + i" << Im;
else
cout << Re << " - i" << fabs(Im);
}
|
Se la definizione del metodo Print() e` stata studiata con
attenzione, il lettore avra` notato che la funzione accede ai membri dato
senza ricorrere alla notazione del punto, ma semplicemente nominandoli:
quando ci si vuole riferire ai campi dell'oggetto cui e` stato inviato il
messaggio non bisogna adottare alcuna particolare notazione, lo si fa e
basta (i nomi di tutti i membri della classe sono nello scope
di tutti i metodi della stessa classe)!
La domanda corretta da porsi e` come si fa a stabilire dall'interno
di un metodo qual'e` l'effettiva istanza cui ci si riferisce.
Il compito di risolvere correttamente ogni riferimento viene svolto
automaticamente dal compilatore: all'atto della chiamata, ciascun metodo
riceve un parametro aggiuntivo, un puntatore all'oggetto a cui e` stato
inviato il messaggio e tramite questo e` possibile risalire all'indirizzo
corretto; cio` inoltre consente la chiamata di un metodo da parte di un
altro metodo:
|
class MyClass {
public:
void BigOp();
void SmallOp();
private:
void PrivateOp();
/* altre dichiarazioni */
};
/* definizione di SmallOp() e PrivateOp() */
void MyClass::BigOp() {
/* ... */
SmallOp(); // questo messaggio arriva all'oggetto
// a cui e` stato inviato BigOp()
/* ... */
PrivateOp(); // anche questo!
/* ... */
}
|
Ovviamente un metodo puo` avere parametri e/o variabili locali che sono
istanze della stessa classe cui appartiene (il nome della classe e` gia`
visibile all'interno della stessa classe), in questo caso per riferirsi
ai campi del parametro o della variabile locale si deve utilizzare la
notazione del punto:
|
class MyClass {
/* ... */
void Func(MyClass A);
};
void MyClass::Func(MyClass A, /* ... */ ) {
/* ... */
BigOp(); // questo messaggio arriva all'oggetto
// cui e` stato inviato Func(MyClass)
A.BigOp(); // questo invece arriva al parametro.
/* ... */
}
|
In alcuni rari casi puo` essere utile avere accesso al puntatore che il
compilatore aggiunge tra i parametri di un metodo, l'operazione e`
fattibile tramite la keyword this (che in pratica e` il nome del
parametro aggiuntivo), tale pratica quando possibile e` comunque da
evitare.
Pagina precedente - Pagina successiva
C++, una panoramica sul linguaggio - seconda edizione © Copyright 1996-1999, Paolo Marotta
|