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,
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: mentre l'ultima
caratteristica (la gestione delle eccezioni) e` in effetti una estensione (l'aggiunta
di qualcosa che mancava), le prime due non sono semplici aggiunte in quanto non si
limitano a fornire nuove funzionalita`, ma impongono un nuovo 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, si ricorda che e` data per scontata la conoscenza dei
concetti di tale paradigma, per chi non possedesse tali concetti (o desiderasse
riportarli alla mente) e` disponibile un articolo sufficientemente completo
all'indirizzo
http://www.tcstore.it/mondobit, in Mondo Bit N.1.
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 gli 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 a cui quel tipo e` in grado di rispondere (interfaccia). 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.
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.
// 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();
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:
Complex A; Complex * C; A.Re = 10.2; // Errore! C -> Im = .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 <ClassName> { 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); tuttavia esiste una differenza per quanto riguarda le regole di scoping sui tipi annidati:
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); }La differenza e` che 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!
class MyClass { public: void BigOp(); void SmallOp(); private: void PrivateOp(); /* altre dichiarazioni */ }; /* definizione di SmallOp() e PrivateOp() */ void MyClass::BigOp() { /* ... */ SmallOp(); // questo messaggio e` inviato all'oggetto // a cui e` stato inviato BigOp() /* ... */ PrivateOp(); // tutto Ok! /* ... */ }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 al parametro o alla variabile locale deve utilizzare la notazione del punto:
class MyClass { /* ... */ void Func(MyClass A, /* ... */ ); }; void MyClass::Func(MyClass A, /* ... */ ) { /* ... */ BigOp(); // questo messaggio e` inviato all'oggetto // a cui e` stato inviato Func( /* ... */ ) A.BigOp(); // questo invece viene inviato 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.