| prof. Nunzio Brugaletta |
C++:
programmazione e oggetti |
Applicare la OOP alla risoluzione di un determinato problema vuol dire innanzi tutto individuare le classi interessate (quali sono i dati) e i metodi che devono avere (che operazioni si richiedono sui dati). Utilizzando una similitudine si può pensare al problema da risolvere come ad una piece teatrale: ci sono dei personaggi, ognuno con le proprie caratteristiche e comportamenti, che interagiscono fra loro. Il problema da risolvere è la piece da recitare, i personaggi (tratteggiati ognuno dalle proprie caratteristiche) sono le classi e sono interpretati da attori (le istanze della classe: gli oggetti) che, per il fatto di interpretare determinati personaggi, sanno cosa fare e come comportarsi perché ciò fa parte dell'interpretazione di quel determinato personaggio.
Viene ora riproposto, come primo esempio, il programma per conoscere il totale della fattura di cui siano date le righe che la compongono, ognuna individuata da una determinata quantità e dal prezzo unitario dell'oggetto venduto. Questa volta il problema viene affrontato utilizzando il paradigma OOP.
Le domande da porsi per affrontare il problema sono due: quali sono i dati coinvolti, che tipo di operazioni sono richieste per quei dati. Nel problema proposto si possono identificare due entità:
La fattura individuata, dal punto di vista dei dati, dal totale. Operazioni richieste: inizializzazione totale, aggiornamento del totale, output del totale.
La definizione della classe fattura sarà quindi:
class fattura{
public:
fattura(): totale(0.0) {}; /*1*/
void aggiorna(float t){totale +=t;}; /*2*/
friend ostream& operator<<(ostream&, const fattura&); /*3*/
protected:
float totale; /*4*/
};
ostream& operator<<(ostream& output, const fattura& f) /*5*/
{
output << "Totale fattura: " << f.totale << endl;
return output;
};La classe ha solo una variabile definita nella parte protetta (4).
Il primo metodo definito in 1 è particolare: si tratta del costruttore. Si tratta di un metodo che ha lo stesso nome della classe e che non restituisce alcun valore nemmeno void. Il metodo non può essere richiamato esplicitamente, ma viene richiamato in automatico quando si istanzia un oggetto della classe. In pratica quando si definisce una variabile di tipo fattura viene inizializzata la variabile privata: definire una nuova fattura vuol dire, in automatico, azzerarne il totale. Il costruttore non deve essere per forza definito. Accanto al costruttore potrebbe essere necessario definire anche un distruttore: metodo che viene richiamato in automatico quando cessa la visibilità dell'oggetto. Il distruttore quando c'è ha nome come il costruttore ma preceduto da ~. Se ci fosse per la classe fattura avrebbe nome ~fattura(). Il distruttore diventa necessario, per esempio, quando c'è una allocazione dinamica della memoria e bisogna liberare e recuperare la memoria occupata. Sia al costruttore che al distruttore possono essere passati parametri.
Nel costruttore è uso comune, invece di utilizzare il solito modo, inizializzare i valori base utilizzando la sintassi della 1. Se ci fossero state più variabili, sarebbero state separate utilizzando la virgola.
Il metodo 2 aggiorna il totale della fattura con il parametro passato.
La 3 ridefinisce l'operatore << per oggetti della classe fattura. L'operatore, così come definito nella libreria iostream, consente l'output di stringhe e di variabili di tipi elementari. La ridefinizione è una caratteristica importante della OOP (overloading) che permette la personalizzazione, in questo caso, dell'operatore di inserimento in modo che possa essere usato per oggetti del tipo fattura. Dal punto di vista sintattico è necessario passare alla funzione un parametro di tipo ostream e uno del tipo l'oggetto per il quale si scrive il codice per la ridefinizione. La parola chiave friend permette alla funzione, anche se non è un metodo della classe, l'accesso alla parte privata o protetta.
Il codice della ridefinizione dell'operatore di inserimento (5) istruisce su come deve essere applicato tale operatore agli oggetti della classe.
La riga individuata dalla quantità venduta e dal prezzo unitario. Operazioni: generazione di una nuova riga, calcolo totale.
class riga{
public:
void nuova(int, float);
float totriga();
friend ostream& operator<<(ostream&, const riga&); /*1*/
protected:
int qv;
float pu;
};
ostream& operator<<(ostream& output, const riga& r) /*2*/
{
output << "Quantita\' venduta: " << r.qv
<< " Prezzo unitario: " << r.pu << endl;
return output;
};
// metodo per inserimento di una nuova riga
void riga::nuova(int q, float p) /*3*/
{
qv = q;
pu = p;
};
// metodo per il calcolo del totale della riga
float riga::totriga() /*3*/
{
float tr;
tr = (float) qv*pu;
return tr;
};
A parte i metodi per la generazione di una nuova riga e il calcolo del totale, nella 1 viene ridefinito, per overloading, l'operatore <<. Nelle 2 ne viene specificato il significato per gli oggetti della classe riga.
La definizione dei metodi (3) utilizza l'operatore di visibilità :: per l'accesso agli elementi della classe.
A questo punto la definizione delle classi, in conseguenza delle richieste del programma da sviluppare, è completa: il codice di ogni classe si inserisce, ognuno, per esempio nel namespace elabfat e si registra in un file. Il file c_fattura contiene la definizione della classe fattura, il file c_riga quello della classe riga. I file saranno inclusi nel sorgente del programma che dovrà utilizzare le classi in esse definite.
Prima di procedere oltre è opportuno rispondere ad una eventuale osservazione sulla mancanza, nella definizione della classe, di implementazione di eventuali altri metodi per elaborazioni diverse che potrebbero essere necessario effettuare. In questi casi, infatti, non è possibile conoscere i dati inseriti che sono conservati in variabili private.
Intanto si può osservare che, per lo sviluppo del programma proposto, non è necessario alcun altro metodo. Inoltre il non prevedere alcun metodo, probabilmente utile per un utilizzo futuro, e in altri contesti, della classe, non toglie generalità; si possono implementare nuovi metodi o, addirittura, modificare quelli esistenti utilizzando l'ereditarietà, così come si tratterà in seguito.
#include <iostream>
#include “c_fattura” /*1*/
#include “c_riga” /*1*/
using namespace std;
int main()
{
int qvend,i;
float pzunit;
elabfat::riga r; /*2*/
elabfat::fattura f; /*2*/
// elaborazione righe fattura
for(i=1;;i++){ /*3*/
cout << "Riga n. " << i << endl; /*4*/
cout << "Quantita\'_venduta prezzo_unitario (0 0 per finire) ";
cin >> qvend >> pzunit;
if((qvend<=0) || (pzunit<=0)) /*5*/
break;
r.nuova(qvend,pzunit); /*6*/
cout << r; /*7*/
f.aggiorna(r.totriga()); /*8*/
};
// stampa fattura
cout << f; /*9*/
return 0;
}
Essendo semplice e leggibile il programma non è stato suddiviso in funzioni.
Con le 1 si includono nel file le definizioni della classi. Così da poter, nelle 2, dichiarare istanze delle classi. Il nome del file da includere è racchiuso fra doppi apici ad intendere che si trova nella stessa directory dove è registrato anche il file che contiene il programma che lo includerà. Le parentesi angolari, che racchiudono per esempio iostream, stanno invece a significare che il file si trova nella directory standard, dei file include, in cui va a cercare il compilatore.
La struttura 3 è un ciclo senza fine (non c'è infatti alcuna condizione di controllo) da cui si esce (5) se si inserisce un valore negativo o nullo in una delle due variabili: non ha infatti senso una riga di fattura con tali valori. Il ciclo for è utilizzato per poter far visualizzare, nella 4, la numerazione delle righe.
Dopo aver acquisito i dati dall'input, nella 6 si richiama il metodo nuova per la conservazione dei dati e nella 8 si richiama totriga per l'aggiornamento del totale della fattura.
Nelle 7 e 8 si stampano, rispettivamente, una riga della fattura e la fattura stessa per quello che questo significa, ed è definito nella ridefinizione dell'operatore di inserimento per i vari tipi di oggetti.
| http://ennebi.solira.org |
ennebi@solira.org |