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, 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.
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, e nella pratica effettuata tramite la stessa sintassi utilizzata per selezionare un qualsiasi altro campo:
    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.
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: come mostra il seguente esempio:
    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: Il motivo e` ovvio, se ad esempio un metodo pubblico dovesse restituire un tipo privato, l'utente della classe non sarebbe in grado di gestire il valore ottenuto perche` non e` in grado di accedere alla definizione di tipo; questo naturalmente non vale per i metodi e gli attributi che se sono privati possono essere direttamente acceduti solo da metodi della classe stessa senza porre alcun problema di visibilita` all'esterno della classe.



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);
    }
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!
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 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.


Pagina precedente - Pagina successiva



C++, una panoramica sul linguaggio - © Copyright 1997, Paolo Marotta