Programmazione a oggetti
I costrutti analizzati fin'ora costituiscono gia` un linguaggio che ci consente di realizzare anche programmi
complessi e di fatto, salvo alcuni dettagli, 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),
programmazione generica, gestione delle eccezioni sono solo alcune delle caratteristiche che rendono il C++ diverso dal C e
migliore di quest'ultimo sotto molti aspetti.
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 (un compito gia` di per se` complesso),
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, cercando
contemporaneamente di esporre i principi alla base di tale metodologia di codifica.
E` bene sottolineare subito che non esiste un unico modello di programmazione orientata agli oggetti, ma esistono
differenti formulazioni spesso differenti in pochi dettagli che hanno pero` una influenza notevole, quanto segue
riferira` unicamente al modello offerto dal C++.
L'idea di base
La programmazione orientata agli oggetti (OOP) impone una nuova visione di concetti quali "Tipo
di dato" e "Operazioni sui dati".
In contrapposizione al paradigma procedurale dove si distingue tra entita` passive (Dati) e entita` attive (operazioni
sui dati), la OOP vede queste due categorie come due aspetti di una unica realta`. In ottica procedurale volendo
realizzare una libreria per la matematica sui complessi, sarremmo portati a scrivere
|
#include
#include
|
Tutto cio` e` corretto, ma perche` separare la rappresentazione di un Complex dalle operazioni definite
su di esso (Print e Abs). In particolare il problema insito nella visione procedurale
e` che si puo` continuare ad accedere direttamente alla rappresentazione dei dati eventualmente per scavalcare le operazioni
definite su di essa:
|
int main() {
Complex C;
// Le seguenti 4 linee di codice non
// sono una buona pratica;
C.Re = 0.5;
C.Im = 2.3;
cout << Val.Re << " + i" << Val.Im;
cout << endl;
cout << Abs(C) << endl;
return 0;
}
|
Si tratta di un comportamento abbastanza comune in chi non ha molta esperienza nella nanutenzione del codice. Tale
comportamento nasconde infatti due pericoli:
- Maggiore possibilita` di errore;
- Difficolta` nella modifica del codice;
Nel primo caso ogni qual volta si replica del codice si rischia di introdurre nuovi errori, utilizzando invece
direttamente le funzioni previste ogni errore non puo` che essere localizzato nella funzione stessa.
Nel secondo caso, la modifica della rappresentazione di un Complex e` resa difficile dal fatto che
bisogna cercare nel programma tutti i punti in cui si opera direttamente sulla rappresentazione (se si fossero
utilizzate solo e direttamente le funzioni previste, tale problema non esisterebbe).
Tutti questi problemi sono risolti dalla OOP fondendo insieme dati e operazioni sui dati secondo delle regole ben
precise. Nelle applicazioni object oriented non ci sono piu` entita` attive (procedure) che operano sui dati,
ma unicamente entita` attive (oggetti) che cooperano tra loro. Se il motto della programmazione procedurale e`
"Strutture dati + algoritmi = programmi", quello della OOP non puo` che essere
"Oggetti + cooperazione = programmi".
Strutture e campi funzione
Come precedentemente detto, l'idea di partenza e` quella di fondere in una unica
entita` la rappresentazione dei dati e le operazioni definite su questi.
La soluzione del C++ (e sostanzialmente di tutti i linguaggi object oriented) e` quella di consentire la presenza di campi
funzione all'interno delle strutture:
|
struct Complex {
double Re;
double Im;
// Ora nelle strutture possiamo avere
// dei campi di tipo funzione;
void Print();
double Abs();
};
|
I campi di tipo funzione sono detti funzioni membro oppure metodi, i restanti campi della struttura
vengono denominati membri dato o attributi.
La seconda cosa che si puo` notare e` la scomparsa del parametro di tipo Complex. Questo parametro
altri non sarebbe che il dato su cui si vuole eseguire l'operazione, e che ora viene specificato in altro modo:
|
Complex A;
Complex* C;
/* ... */
A.Print();
C = new Complex;
C -> Print();
float FloatVar = C -> Abs();
|
Nella OOP non ci sono piu` procedure eseguite su certi dati, ma messaggi inviati ad oggetti. Gli oggetti sono
le istanze di una struttura; i messaggi sono le operazioni che possono essere eseguiti su di essi ("Print,
Abs").
Un messaggio viene inviato ad un certo oggetto utilizzando sempre il meccanismo di chiamata di funzione, il legame
tra messaggio e oggetto destinatario viene realizzato con la notazione del punto ("A.Print()") o,
se si dispone di un puntatore a oggetto, tramite l'operatore -> ("C -> Print()").
Non e` possibile invocare un metodo (inviare un messaggio) senza associarvi un oggetto:
|
Complex A;
/* ... */
Print(); // Errore!
|
Un messaggio deve sempre avere un destinatario, ovvero una richiesta di operazione deve sempre specificare chi deve
eseguire quel compito.
Il compilatore traduce la notazione vista prima con una normale chiamata di funzione, invocando il metodo selezionato
e passandogli un parametro nascosto che altri non e` che l'indirizzo dell'oggetto stesso, ma questo lo riprenderemo in
seguito.
Quello che ora e` importante notare e` che siamo ancora in grado di accedere direttamente agli attributi di un oggetto
esattamente come si fa con le normali strutture:
|
// Con riferimento agli esempi riportati sopra:
A.Re = 10; // Ok!
A.Im = .5; // ancora Ok!
// anzicche` A.Print()...
cout << A.Re << " + i" << A.Im;
cout << endl;
|
A questo punto ci si potra` chiedere quali sono in vantaggi di un tale modo di procedere, se poi i problemi precedentemente
esposti non sono stati risolti; in fondo tutto cio` e` solo una nuova notazione sintattica.
Il problema e` che le strutture violano un concetto cardine della OOP, l'incapsulamento. In sostanza il problema e`
che non c'e` alcun modo di impedire l'accesso agli attributi. Tutto e` visibile all'esterno della definizione della
struttura, compresi i campi Re e Im.
Il concetto di incapsulamento dice in sostanza che gli attributi di un oggetto non devono essere accessibili se non ai
soli metodi dell'oggetto stesso.
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 (e vicevera) 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 effetti e` possibile applicare gli specificatori di accesso (public, private e come vedremo
protected) anche alle strutture, ma per le strutture il default e public (per compatibilita` col C).
Esiste anche una terza classe di visibilita` specificata dalla keyword protected, ma analizzaremo questo punto
solo in seguito parlando 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:
|
/* 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. Il programmatore non deve comunque preoccuparsi di cio`
e` il compilatore che risolve tutti i legami tramite tale puntatore.
Allo stesso modo a cui si accede agli attributi dell'oggetto, un metodo puo` anche invocare un altro metodo dell'oggetto stesso:
|
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
|