Lezione 9 – Classi e Oggetti



By admin on febbraio 8, 2010


In passato, i programmatori scrivevano codice che manipolava i dati, i dati stessi e il codice che li manipolava venivano trattati come due elementi separati. La programmazione orientata agli oggetti (OOP), invece, tratta dati e codice come una singola entità, nota come classe. Il concetto della OOP introduce nuove parole di uso comune durante la programmazione in C++ (tutto ciò che verrà detto da qui in poi non sarà più applicabile al C):

CLASSE
ISTANZA
OGGETTO
INCAPSULAMENTO
MEMBRO DATI
FUNZIONE MEMBRO
COSTRUTTORE e DISTRUTTORE

La OOP è un nuovo modi di pensare la programmazione. Non si deve pensare più al codice lineare e alla manipolazione di alcuni dati esterni con una funzione. Si deve adottare l'impostazione mentale che i dati e il codice sono raggruppati all'interno dello stesso corpo.

 

Classe: Incapsulamento

Una classe è una struttura di dati che contiene tutto quanto è necessario per memorizzare e manipolare i dati. Nella OOP, ogni variabile definita all'interno di una classe è denominata membro dati. Le funzioni che manipolano i dati sono dette invece funzioni membro. Dovrebbe essere possibile manipolare i dati membro solo tramite le funzioni membro. Le funzioni esterne non possono accedere ad un membro dati. Questo modo di agire realizza quello che in gergo si chiama incapsulamento dell'informazione.
Vediamo come si crea una classe in C++:

class nome_classe
{
	//dati e funzioni membro
};

ad esempio:

class shape
{
	//dati e funzioni membro dichiarati alla solita maniera
};
 
int main()
{
    shape forma; //esempio di dichiarazione di una var.
    return 0;
}

Quindi la classe viene dichiarata usando la parola riservata class seguita dal nome da dare alla classe.
L'incapsulamento dell'informazione è utile quando non si vuole far accedere a dati critici per il corretto funzionamento del programma. Come vedremo in seguito, il divieto di accesso diretto ai dati non è comunque controllato, ovvero, è possibile rendere visibili all'esterno dati non critici.
Un ultimo concetto che bisogna avere ben chiaro in mente è la distinzione tra classe ed oggetto. La prima è un qualcosa di astratto che non esiste fisicamente e l'unico modo per poterla vedere è leggere la sua definizione (class x {...};), un oggetto è fisicamente presente nella memoria di un calcolatore, ha un proprio spazio di memoria e cosa più importante può essere utilizzato ovvero creato, distrutto accedere ai dati e alle funzioni membro, copiato e passato come parametro di input ad una funzione oppure restituito come parametro di output da  un'altra funzione.

Membri Dati e Funzioni Membro

A questo punto dovrebbe essere gia ben chiaro la distinzione tra un membro dati ed una funzione membro. Ma che differenza c'è tra una funzione membro ed una normalissima funzione esterna? E tra un membro dati ed una variabile generale? Per entrambe le domande la risposta è: Nessuna! Esiste solo una distinzione concettuale, un dato o funzione membro è dichiarata all'interno di una classe ed esiste solo se viene creato un oggetto, mentre le variabili e funzioni generali esistono all'esterno della classe/oggetto. Infatti estendendo l'esempio precedente possiamo scrivere:

class shape
{
  public: //Vedremo dopo cosa significa
    double Larghezza, Altezza;
    double area;
    double CalcolaArea(double l, double h);
};

Semplice no?
Volendo ora scrivere l'implementazione della funzione CalcolaArea(); come bisogna procedere? La sintassi da utilizzare nella definizione delle funzioni membro è la seguente:

tipo_restituito nome_classe::funzione_membro(parametri)
{
	//istruzioni
}

Ovvero

double shape::CalcolaArea(double l, double h)
{
  Larghezza = l;
  Altezza = h;
  area = l*h;
  return area;
}

Come si utilizza una classe? Estendiamo il main visto prima.

int main()
{
    shape forma;
    forma.Larghezza = 10.9;
    forma.Altezza = 100.0;
    cout << forma.CalcolaArea(10.0, 12.5) << endl;
    return 0;
}

Il programma precedente stamperà 125. Quindi come possiamo osservare si utilizza la stessa notazione utilizzata per le struct. Facciamo lo stesso esempio ma questa volta i puntatori:

int main()
{
    shape *forma;
    
    forma = new shape;
    forma->Larghezza = 10.9;
    forma->Altezza = 100.0;
    cout << forma->CalcolaArea(10.0, 12.5) << endl;
    delete forma;
    return 0;
}

 

Visibilità esterna dei dati e funzioni

Senza addentrarci in discussioni filosofiche universitarie sulla visibilità dei dati e funzioni membro, veniamo subito al dunque! Ci sono 3 modi possibili di "nascondere" l'informazione che si realizzano utilizzando le 3 parole riservare public, private e protected. Più precisamente la sintassi è la seguente:

class nome_classe
{
    public:
    //dati e funzioni membro pubbliche

    private:
    //dati e funzioni membro private

    protected:
    //dati e funzioni membro protette
};

Notare i ':' dopo le parole chiavi. Non è necessario utilizzarle tutte, inoltre l'ordine può essere come più si ritiene giusto ed infine è possibile avere più sezioni private, pubbliche o protette:

class classe1
{
  public:
    //dati e funzioni membro pubbliche

  private:
    //dati e funzioni membro private

  public:
    ...
};

Descriviamo ora con più precisione il significato di questi 3 modi di proteggere l'informazione.

Public

Qualunque membro dati e funzione membro dichiarata dopo la parola chiave public è detta essere una funzione pubblica o dato pubblico. Essere pubblico significa che all'esterno della classe si può accedere direttamente a quella funzione o variabile. Ad esempio, è lecito scrivere:

class classe1
{
  public:
    int d1;
    char d2;
    char LeggiCarattere();

  private:
    //dati e funzioni membro private
};

int main()
{
    classe1 c;
    c.d2 = c.LeggiCarattere();
}

 

Private
Qualunque membro dati e funzione membro dichiarata dopo la parola chiave private è detta essere una funzione privata o dato privato. Essere privato significa che all'esterno della classe non è possibile accedere direttamente a quella funzione o variabile. Ad esempio:

class classe1
{
  public:
    int d1;
    char d2;
    char LeggiCarattere();

  private:
    int priv1;
    int leggiIntero();
};

int main()
{
    int i;
    classe1 c;

    c.d2 = c.LeggiCarattere();
    i = c.leggiIntero();  //ERRORE
}

Il precedente programma è sbagliato in quanto la funzione leggiIntero(); è privata ovvero non visibile dall'esterno. Una qualunque funzione dichiarata come pubblica ha comunque accesso, nella sua implementazione, ai dati e funzioni membro privati.

Protected
Lo status di dato o funzione protetta è simile a quello di privato ma ha un comportamento particolare quando una classe eredita i dati e funzioni membro di un'altra classe. Per ora sospendiamo il discorso e lo riprenderemo più in là quando parleremo della ereditarietà.

 

Costruttore e Distruttore

Nella creazione di una classe è possibile dichiarare 2 funzioni particolari che non vengono chiamate direttamente nel codice C++, queste sono il Costruttore e il Distruttore.
Il costruttore ha il nome uguale a quello della classe, può avere parametri di input e non restituisce niente e viene chiamato automaticamente quando la variabile oggetto viene creata. Lo scopo del costruttore è quello di inizializzare i dati membro ed allocare eventualmente memoria dinamica. Non utilizzate mai una chiamata diretta al costruttore di una classe! Il costruttore è opzionale, ovvero, non è necessario che la classe abbia un costruttore.
Il distruttore ha un nome uguale a quello della classe ma è preceduto dal carattere tilde ‘~', non ha parametrici input e non restituisce niente. Viene chiamato automaticamente quando viene distrutta (o deallocata) la variabile oggetto. Lo scopo principale è quello di deallocare la memoria dinamica utilizzata nella classe. Anche per il distruttore e consigliabile evitare di richiamarlo direttamente ed inoltre non è necessario che sia presente.
Per entrambi infine è obbligatorio che siano definiti nella sezione pubblica della classe, in caso contrario il compilatore darà errore.
Facciamo qualche esempio concreto

class shape
{
  public: //Vedremo dopo cosa significa
    shape();  //costruttore
    ~shape(); //distruttore
    double Larghezza, Altezza;
    double area;
    double CalcolaArea(double l, double h);
};

shape::shape()
{
    cout << "Variabili inizializzate" << endl;
    Larghezza = Altezza = 0;
}

shape::~shape()
{
    cout << "Distruzione dell'oggetto" << endl;
}

double shape::CalcolaArea(double l, double h)
{
    Larghezza = l;
    Altezza = h;
    area = l*h;
    return area;
}

int main()
{
    shape *forma;

    forma = new shape;
    forma->Larghezza = 10.9;
    forma->Altezza = 100.0;
    cout << forma->CalcolaArea(10.0, 12.5) << endl;
    delete forma;
    return 0;
}

Il programma precedente produrrà il seguente output sullo schermo:

Variabili inizializzate
125
Distruzione dell'oggetto

 

Ereditarietà

Con l'introduzione della OOP è stato introdotto un nuovo e potente metodo di sviluppo dei programmi: l'ereditarietà. Come il nome suggerisce, con il C++ (ovvero con qualunque linguaggio ad oggetti), è possibile creare una classe che eredita i dati e le funzioni membro di un'altra classe (la classe padre). Ereditare vuol dire che tutto ciò che era contenuto nella classe padre diventa parte integrante della classe figlio e all'esterno sembra un'unica classe.
Ci sono 3 modi di ereditare i dati e membri da una classe padre: pubblico, privato o protetto. Inoltre l'eredità può essere multipla ovvero una classe eredita da più classi padre.
In gergo più tecnico si parla di classe derivata e classe base. In questo modo, la creazione di più livelli di derivazione da luogo ad un sistema gerarchico di classi. Alcuni di questi sistemi sono noti come framework VCL (di Borland®) ed MFC (di Microsoft®).

Riprendiamo il concetto di dato o membro protetto (protected) di una classe, è giunto il momento di spiegare meglio il suo significato. Qualsiasi cosa protetta si comporta come privata all'interno della propria classe. Questo significa che non è possibile accedere ad essa dal mondo esterno. Gli elementi protetti possono essere comunque ereditati da una classe derivata e nella classe derivata essi si comportano come se fossero privati. Una classe derivata non può ereditare gli elementi privati della classe base.
La tabella che segue mostra come sono ereditati i dati e funzioni membri in base a come la classe base viene ereditata (colonne).

 

Definizione del dati e membri della classe

 

Public

Protected

Private

Public

Pubblico

privato

Non acc. dalla classe base

Protected

Privato

Privato

Non acc. dalla classe base

Private

Privato

Privato

Non acc. dalla classe base

Vediamo come si scrive in C++ il codice che permette di eseguire l'ereditarietà.

class nome_classe_derivata: public nome_classe_base
{
    //dati e funzioni membro
};

class nome_classe_derivata: private nome_classe_base
{
    //dati e funzioni membro
};

class nome_classe_derivata: protected nome_classe_base
{
    //dati e funzioni membro
};

Per una eredità multipla si scrive:

class nome_classe_derivata: public nome_classe_base1, nome_classe_base2
{
    //dati e funzioni membro
};

Concludiamo il discorso sulla ereditarietà descrivendo un ultimo importante concetto. Se un classe base ha un costruttore allora la classe derivata deve esporre un costruttore che abbia gli stessi parametri del costruttore della classe base più eventuali altri parametri. Il passaggio dei parametri dal costruttore della classe derivata a quello della classe base avviene in un modo diverso dal solito:

class base
{
  public:
    base(int p1, int p2);
    ...
};

class derivata : public base
{
  public:
    derivata(double a, int b, int c);
    ...
};

derivata::derivata(double a, int b, int c) : base(b, c)
{
    ...
}

Vediamo un semplice esempio funzionante di ereditarietà:

#include <iostream.h>

class shape
{
  protected:
    double length;
    double height;
    double area;

  public:
    void CalcArea();
    void ShowArea();
    shape(double l = 0, double h = 0);
};

class ThreeD : public shape
{
  protected :
    double depth;
    double volume;

  public:
    void CalcVol();
    void ShowVol();
    ThreeD(double z=0,double x=0,double y=0);
};

ThreeD::ThreeD(double z, double x, double y):shape(x,y)
{
    depth = z;
}

shape::shape(double l, double h)
{
    length = l;
    height = h;
}

void shape::CalcArea()
{
    area = length * height;
}

void shape::ShowArea()
{
    cout << "THE AREA IS : " << area;
}

void ThreeD::CalcVol()
{
    volume = depth * length * height;
}

void ThreeD::ShowVol()
{
    cout << "THE VOLUME IS : " << volume << endl;
}

main()
{
    double x, y, z;

    cout << "ENTER THE LENGTH : ";
    cin >> x;
    cout << "ENTER THE HEIGHT : ";
    cin >> y;
    cout << "ENTER THE DEPTH  : ";
    cin >> z;
    ThreeD box(z,x,y);
    box.CalcVol();
    box.ShowVol();
    return 0;
}

 

Polimorfismo
Prima di definire il concetto di Polimorfismo è fondamentale che il concetto di ereditarietà sia ben chiaro ed inoltre bisogna apprenderne un altro: classe astratta. La classe astratta è uno strumento di progettazione che consente di definire funzionalità di base, lasciando che le funzionalità specifiche del programma vengano definite successivamente. Le classi astratte hanno le seguenti caratteristiche:

  • Hanno almeno una funzione membro virtuale pura (che vedremo tra poco).
  • Le classi astratte vengono usate come classe base per creare classi derivate.
  • Ogni classe che contiene una funzione virtuale pura, non può creare un oggetto.

 

Una funzione è definita virtuale anteponendo alla sua definizione la parola chiave virtual. Questo significa che tutte le classi derivate condividono la stessa funzione per evitare l'ambiguità.

Una classe derivata può sovrascrivere la definizione di una funzione membro virtuale ridefinendo la sua funzionalità. La nuova definizione verrà utilizzata in tutte le istanze degli oggetti della classe derivata.
Quando una funzione viene dichiarata virtuale nella classe base, essa rimane virtuale in tutte le classi derivate. Si è liberi di includere od omettere la parola virtual nella ridefinizione della funzione membro.
Una funzione virtuale pura, è una funzione virtuale impostata a zero e non viene specificata alcuna definizione per essa. Sembra un po' strano. La sintassi è la seguente:

virtual tipo_restituito nome_funzione(parametri) = 0;

ad esempio

virtual void f1()=0;

Essa non esegue nulla eccetto impedire alla classe di creare un'istanza dell'oggetto e occupare memoria per assegnargli una definizione quando eredita una classe.
A questo punto si può dire definire il concetto di polimorfismo: Il polimorfismo è la capacità di una funzione membro di avere differenti funzionalità in vari punti dell'albero gerarchico. La funzionalità utilizzata è quella più appropriata per l'oggetto al quale appartiene.

Diamo un esempio:

#include <iostream.h>
#include <stdlib.h>

#define TAB '\t'

class SEQUENCE
{
  protected:
    int back;
    char data[10];

  public:
    virtual void POKE(char ch);
    virtual void POP(void) = 0;
    virtual void PEEK(void) = 0;
    SEQUENCE();
};

SEQUENCE::SEQUENCE()
{
    back = 0;
}

void SEQUENCE::POKE(char ch)
{
    if(back < 9)
    {
        back++;
        data[back] = ch;
        cout << endl;
    }
    else
        cout << endl << "SORRY - FULL" << endl << endl;
}

class MyDEQUE : public SEQUENCE
{
  public:
    MyDEQUE();
    void POP(void);
    void PEEK(void);
}

void MyDEQUE::POP(void)
{
    int index;
    char item;
    
    if(back > 0)
    {
        cout << "LEAVE DEDUE FROM FRONT OR BACK (f/b) : ";
        cin >> item;
        if((item == 'b') || (item == 'B'))
        {
            back--;
        }
        if((item == 'f') || (item == 'F'))
        {
            for(index = 0; index < back; index++)
            {
                data[index] = data[index + 1];
            }
            back--;
        }
    }
    else
    {
        cout << endl << "DEDUE IS EMPTY";
        cout << endl << endl;
    }
}

void MyDEQUE::PEEK(void)
{
    int x;
    
    if (back == 0)
    {
        cout << endl << "DEDUE IS EMPTY";
        cout << endl << endl;
    }
    else
    {
        for (x = 1; x <= back; x++)
        {
            cout << data[x] << TAB;
        }
        cout << endl << endl;
    }
}

MyDEQUE::MyDEQUE() : SEQUENCE()
{
    cout << "LIST CREATED" << endl << endl;
}

char menu(void);

main()
{
    char ch;
    char poker;
    MyDEQUE D;
    
    while (1)
    {
        ch = menu();
        switch(ch)
        {
            case '1' :
            	cout << "Enter the character : ";
               cin >> poker;
               D.POKE(poker);
            	break;
            case '2' : D.POP();
            	break;
            case '3' : D.PEEK();
            	break;
            case '4' : exit(0);                           
        }
    }
    return(0);
}

char menu(void)
{
    char choice;
    
    cout << "1...Join the DEQUE" << endl;
    cout << "2...Leave the DEQUE" << endl;
    cout << "3...Show the DEQUE" << endl;
    cout << "4...Quit the program" << endl << endl;
    cout << "Enter your choice : ";
    cin >> choice;
    return choice;
}




Lascia un Commento