edutecnica
 


Programmazione ad oggetti in C++

Nella programmazione strutturata, viene scritto del codice che serve per manipolare dei dati, in questo contesto, codice e dati vengono trattati come due elementi separati. La programmazione orientata agli oggetti, tratta invece, dati ed oggetti come una singola entità chiamata classe. Tutti i programmi OOP iniziano con una classe. Una classe è una struttura di dati che contiene tutto il necessario per memorizzare e manipolare i dati. Supponiamo di sviluppare un programma per lo studio sui consumi di veicoli: .

#include
using namespace std;

class V {// classe veicolo
public: //dichiarazione degli attributi
     int a;//autonomia (km)
     int l;//volume del serbatoio(lt)
};
main(){
V v1;//istanzio l'oggetto v1 di tipo V
v1.a=300;
v1.l=50;
cout<<"il veicolo ha un consumo:" << v1.a/v1.l << "km/l";
}//__________fine main


Uno scritto di questo tipo sarebbe sufficiente per descrivere un veicolo. Ogni classe è dotata di attributi (membri dati) che sono privati per impostazione predefinita. Noi abbiamo fatto un'eccezione: li abbiamo dichiarati esplicitamente pubblici, altrimenti non avremmo potuto accedervi.

In questo caso i due attributi, a ed l pur facendo parte dell'oggetto v1, sono accessibili dal mondo esterno. Raggiungibili con le invocazioni:

v1.a
v1.l

La filosofia di usare variabili locali e di minimizzare le variabili globali all'interno del codice è sempre stata raccomandata fin da quando la programmazione strutturata, organizzata in funzioni e procedure, ha soppiantato quella sequenziale, caratterizzata da salti e rinvii fra i vari punti del programma (istruzione goto).
Nella programmazione ad oggetti (OOP) questa tendenza è stata inasprita, proprio per evitare l'utilizzo di variabili globali, dato che esse, essendo visibili da tutti i punti del programma, possono influenzare, talvolta in modo incontrollato, l'esecuzione dello stesso, nonché, rendono difficile la gestione del codice, la sua manutenzione e il suo sviluppo .

Incapsulamento

Una classe è una struttura di dati che contiene tutto il necessario per memorizzare e manipolare dati.
Ogni variabile definita all'interno di una classe è denominata attributo, si usa questo termine per distinguerla da una normale variabile.
All'interno della classe vengono implementate anche le funzioni che manipolano gli attributi, esse sono denominate metodi.
Dovrebbe essere possibile manipolare gli attributi di una classe solo attraverso i metodi della classe stessa, non come abbiamo fatto noi, sopra, dove il main può accedere liberamente ai metodi e farne quello che vuole.

#include < iostream >
using namespace std;
class V {//(veicolo)
int a;//autonomia (km)
int l;//volume del sebatoio(lt)
public:
     void seta(int x);//metodi pubblici
     int geta();
     void setl(int y);
     int getl();
};
void V::seta(int x){
a=x;
}
int V::geta(){
return a;
}
void V::setl(int y){
l=y;
}
int V::getl(){
return l;
}
main(){
     V v1;//istanzio l'oggetto v1 di tipo V
     v1.seta(300);
     v1.setl(50);
cout << "il veicolo ha un consumo:" << v1.geta()/v1.getl() << "km/l";
}//__________fine main

All'interno del programma un'eventuale chiamata del tipo:

cout << v1.a;

genera un errore di compilazione dato che l'attributo a è privato e dunque inaccessibile.

Sono, invece, stati implementati 4 metodi pubblici: seta e setl per impostare i valori degli attributi privati a ed l e geta e getl che servono per restituire al programma chiamante i valori di a ed l.
In quest'ultimo caso gli atributi a ed l sono privati ed irraggiungibili dall'esterno.
La loro accessibilità è garantita dai 4 metodi pubblici.


Classi ed oggetti

La riga di codice
V v1;
serve per istanziare l'oggetto v1 di classe V.
Un'istanza è una nuova copia della struttura definita nella classe (schema degli attributi + metodi incapsulati: i metodi sono condivisi tra oggetti diversi della stessa classe).
In questa ottica la classe viene interpretata come il 'calco' che serve a 'coniare' un effettivo oggetto che verrà poi dotato di dati concreti gestito dai corrispondenti metodi.
In altri termini, una classe non è un oggetto, ma una sua descrizione e come tale non occupa memoria.

Costruttori e distruttori

Il costruttore permette all'oggetto appena creato di autoinizializzarsi. Un costruttore è un metodo speciale che appartiene alla classe e ha il medesimo nome della classe.

#include < iostream >
using namespace std;
class V {//(veicolo)
int a;//autonomia (km)
int l;//volume del sebatoio(lt)
public:
     void seta(int x);//metodi
     void setl(int y);
     float consumo();
     V(int x,int y);//costruttore
     ~V();//distruttore

}; //fine classe - implementazione dei metodi
void V::seta(int x){
a=x;
}
void V::setl(int y){
l=y;
}
float V::consumo(){
return (float)a/l;
}
V::V(int x,int y){//implementazione del costruttore
a=x; l=y;
}
V::~V(){//implementazione del distruttore
cout<<"\noggetto distrutto";
}

main(){
     V v1(300,40);//istanzio l'oggetto v1 di tipo V
     cout< <"il veicolo ha un consumo:"<< v1.consumo()<< "km/l";
}//__________fine main

Nel listato precedente sono state eliminati i metodi geta() e getl() perché ci interessa conoscere solo il consumo che ci verrà restituito tramite un apposito metodo pubblico che ci restituisce il rapporto autonomia/litri del veicolo.
Inoltre si nota la dichiarazione e l'implementazione del costruttore. I costruttori non possono restituire valori.
Il costruttore di un oggetto, è invocato al momento della creazione dell'oggetto medesimo.
Ciò significa che è invocato quando viene eseguita la dichiarazione dell'oggetto.l'output del programma è il seguente:
il veicolo ha un consumo:7.5km/l
oggetto distrutto

Il complemento costruttore è il distruttore; in numerose circostanze, un oggetto nel momento in cui viene distrutto deve eseguire una o più azioni (oggetti locali sono creati all'entrata del relativo blocco di codice e distrutti all'uscita).
Lo scopo principale del distruttore, rimane comunque la deallocazione della memoria precedentemente richiesta all'atto dell'istanziazione da parte del costruttore.

Metodi privati

Supponiamo ora, che di un determinato veicolo più che del consumo ci interessi il consumo per persona trasportata; in pratica introdurremo un nuovo parametro, chiamato 'convenienza' che viene definito come:

convenienza = consumo/p

dove con p indichiamo il numero di persone trasportate, in questo modo il metodo consumo() diventa accessorio al calcolo della convenienza; se di esso non interessa stampare i valori, può anche essere dichiarato privato.:

#include < iostream >
using namespace std;
const float prz=1.5;
class V {//(veicolo)
int p;//persone trasportate
int a;//autonomia (km)
int l;//volume del sebatoio(lt)
float consumo();//a/l
public:
V(int x,int y,int z);//costruttore
float convenienza();//consumo/p
};
float V::consumo(){
     return (float)a/l;
}
float V::convenienza(){
     return prz*consumo()/p;
}
V::V(int x,int y,int z){
     a=x;//autonomia
     l=y;//litri di pieno
     p=z;//passeggeri
}
main(){ V v1(300,40,3);//istanzio l'oggetto v1 di tipo V
cout << "il veicolo ha convenienza:" << v1.convenienza() << "E/persona"; }//__________fine main ;


Notiamo come un'eventuale chiamata
cout << v1.consumo();
introdotta nel main, genera un errore di compilazione. dato che la classe consumo() è privata nell'oggetto istanziato e non è accessibile da altre parti del programma.


Ereditarietà

Tutti i veicoli hanno delle stesse caratteristiche di base, come si è visto autonomia, volume del serbatoio in lt, persone trasportate; sappiamo anche che essi subiscono delle classificazioni di massima.
Ad esempio, ci sono moto che possono trasportare solo una persona, mentre altre ne possono trasportare 2.
Ci sono i motocarri e i sidecar; i primi possono trasportare al massimo due persone in cabina (+ il carico) i secondi non hanno carico e possono trasportare fino a 3 persone.
Poi ci sono auto (che hanno 4 ruote) che possono trasportare 5 persone, ma alcune solo 4.
Il discorso potrebbe dilungarsi coi pullman che arrivano ad avere 6 ruote e un bel po' di passeggeri. Tutte queste classi possono essere derivate dalla classe base dei veicoli.


Il sistema gerarchico dell'ereditarietà può essere illustrato come in figura; per convenzione una freccia punta dalla classe derivata fino alla classe base.
L'ereditarietà si esplica permettendo ad una classe di incorporarne un'altra nella propria dichiarazione .

Il seguente programma, si propone di gestire soltanto dei veicoli che siano o motoveicoli (max 3 ruote, 3 passeggeri) o autoveicoli (min=max=4 ruote, 5 passeggeri) .

Tutti questi veicoli avranno come caratteristiche comuni:
a=autonomia (in km)
l=volume del serbatoio (in litri)

Questi attributi rimarranno associati alla classe base denominata veicolo. Pensiamo di realizzare altre 2 classi :la classe M (per i motoveicoli) e la classe A (per gli autoveicoli) con gli ulteriori attributi:
p=persone trasportate
r=numero di ruote

Come si nota, se istanziamo un auto di A l'attributo r=4 è assicurato dalla definizione di costante statica:
static const int ruote=4;
Per ciascuno dei mezzi usati deve essere possibile calcolare la convenienza().
Si deduce, come a questo punto, il metodo consumo(), può restare privato all'interno della classe veicolo, assieme al metodo pubblico convenienza() senza che questi metodi debbano essere riscritti ulteriormente per le due nuove classi.
#include < iostream >
using namespace std;
const float prz=1.5;
class V {//(veicolo)
int a;//autonomia (km)
int l;//volume del sebatoio(lt)
float consumo();//a/l
protected: int p;//passeggeri
//p accessibile solo dalle classi derivate
public: V(int x,int y);//costruttore
float convenienza();//consumo/p
};
V::V(int x,int y){
a=x;//autonomia
l=y;//litri di pieno
}
float V::convenienza(){
return prz*consumo()/p;
}
float V::consumo(){
return (float)a/l;
} //______________________________________fine classe V
class A:public V {
static const int ruote=4;//ruote
public: A(int aut,int ltvol,int pas);//costruttore
};
A::A(int aut,int ltvol,int pas):V(aut,ltvol){//costruttore
p=pas;//passeggeri
r=ruote;
} //______________________________________fine classe A
class M:public V {
int r;//ruote (variabili)
public: M (
int aut, int ltvol,int pas,int ruo);//costruttore
};
M::M(int aut,int ltvol,int pas,int ruo):V(aut,ltvol){//costruttore
p=pas;//passeggeri
r=ruo;//ruote
} //______________________________________fine classe M
main(){
A a1(300,40,5);
cout << "il veicolo ha convenienza:" << a1.convenienza() << "E/persona\n"; M m1(200,20,2,3);
cout << "il veicolo ha convenienza:" << m1.convenienza() << "E/persona\n";
}//__________fine main

Notare come l'attributo p sia stato iscritto con la clausola protected nella classe V; in tal modo esso sarà accessibile solo dai metodi delle due classi derivate M ed A, per altri classi diverse da quelle derivate esso risulterà protetto.
Notare,inoltre, come la costante ruote (numero di ruote) nella classe A (auto) debba essere dichiarata static; la parola static specifica che questo attributo viene allocato in memoria una sola volta per tutte le istanze della classe A; diversamente dall'attributo r della classe M che richiede un'allocazione in memoria ad ogni oggetto riferito alla classe M che viene creato; questo principio non vale solo per le costanti, ma anche per le variabili. Un altro fatto rilevante è come vengono dichiarati i due costruttori:

A::A(int aut,int ltvol,int pas):V(aut,ltvol)
M::M(int aut,int ltvol,int pas,int ruo):V(aut,ltvol)


osservando i due costruttori si nota come essi non riconoscano i primi due parametri che vengono passati alla classe base V che li memorizza nrgli attributi aut (autonomia) ed l (litri del pieno) mentre il parametro pas viene affidato all'attributo p ed il parametro ruo all'attributo r (però solo nel caso di motocicli M perché in caso di auto A r=cost=4).

Polimorfismo

Per poter essere un linguaggio di programmazione orientato agli oggetti, il linguaggio deve supportare il polimorfismo.
Il polimorfismo è un principio della OOP che, data una gerarchia di classi di oggetti, offre la possibilità di usare lo stesso metodo applicato ad oggetti diversi della gerarchia, oppure lo stesso nome per identificare metodi diversi, incapsulati in classi differenti.
Una variante al nostro programma potrebbe essere quella di delegare ad un unico metodo mostra() le caratteristiche del veicolo; il risultato sarebbe il seguente:

#include < iostream >
using namespace std;
const float prz=1.5;
class V {//(veicolo)
     int a;//autonomia (km)
     int l;//volume del sebatoio(lt)
     float consumo();//a/l
     float convenienza();//consumo/p
protected:
      int p;//passeggeri
      int r;//ruote
//p accessibile solo dalle classi derivate
public: V(int x,int y);//costruttore
void mostra();//output delle caratteristiche
};
V::V(int x,int y){
     a=x;//autonomia
     l=y;//litri di pieno
}
void V::mostra() {
cout << "autonomia:" << a << "\tpieno:" << l << "\nconsumo:" << consumo();
cout << "\tconvenienza:" << convenienza() << "\nruote:" << r << endl << endl;
}//fine mostra;
float V::convenienza() {
return prz*consumo()/p;
}
float V::consumo(){
return (float)a/l;
} //______________________________________fine classe V
class A: public V {
static const int ruote=4;//ruote
public:
A(int aut,int ltvol,int pas);//costruttore
};
A::A(int aut,int ltvol,int pas):
V(aut,ltvol){//costruttore
     p=pas;//passeggeri
     r=ruote;
} //______________________________________fine classe A
class M:public V {
public: M(int aut,int ltvol,int pas,int ruo);//costruttore
};
M::M(int aut,int ltvol,int pas,int ruo): V(aut,ltvol){//costruttore
     p=pas;//passeggeri
     r=ruo;//ruote - variabili
} //______________________________________fine classe M
main() {
     A a1(300,40,5);
     a1.mostra();
     M m1(200,20,2,3);
     m1.mostra();
}//__________fine main


La classe convenienza() può diventare privata, in ogni caso sarà accessibile dal metodo pubblico mostra() che così potrò essere invocato dal main attraverso le chiamate:

a1.mostra();
m1.mostra();


supponiamo,ora, che solo nel caso di motocicli M a 2 o a 3 ruote il programma debba specificare se si tratta di una moto, di un sidecar o di un motocarro:

#include < iostream >
using namespace std;
const float prz=1.5;
class V {//(veicolo)
protected://IMPORTANTE!!
     int a;//autonomia (km)
     int l;//volume del sebatoio(lt)
     float consumo();//a/l
     float convenienza();//consumo/p
     int p;//passeggeri
     int r;//ruote //p accessibile solo dalle classi derivate
public: V(int x,int y);//costruttore
void mostra();//output delle caratteristiche
};
V::V(int x,int y){
     a=x;//autonomia
     l=y;//litri di pieno
}
void V::mostra() {
cout << "autonomia:" << a << "\tpieno:" << l << "\nconsumo:"<< consumo();
cout << "\tconvenienza:" << convenienza() << "\nruote:" << r << endl << endl;
}//fine mostra;
float V::convenienza() {
     return prz*consumo()/p;
}
float V::consumo() {
     return (float)a/l;
} //______________________________________fine classe V
class A:public V {
static const int ruote=4;//ruote
public: A(int aut,int ltvol,int pas);//costruttore
};
A::A(int aut,int ltvol,int pas):V(aut,ltvol) {//costruttore
      p=pas;//passeggeri
      r=ruote;
} //______________________________________fine classe A
class M:public V {
char tipo[10];
public:
M(int aut,int ltvol,int pas,int ruo,char s[10]);//costruttore
void mostra();
};
M::M(int aut,int ltvol,int pas,int ruo,char s[10]):V(aut,ltvol){//costruttore
p=pas;//passeggeri
r=ruo;//ruote - variabili
strcpy(tipo,s);
}
void M::mostra() {
cout << "autonomia:" << a << "\tpieno:" << l << "\nconsumo:" << consumo();
cout << "\tconvenienza:" << convenienza() << "\nruote:" << r;
cout << "\t\ttipologia:" << tipo << endl << endl; }//fine mostra;
//______________________________________fine classe M
main(){
A a1(300,40,5);
a1.mostra();
M m1(200,20,2,2,"moto"); m1.mostra();
}//__________fine main


La classe M vede ridefinito il metodo mostra() in tal caso, un oggetto di classe M istanziato, dovrò usare quest'ultimo metodo per l'output a video e non quello riportato nella classe V.
Una importante modifica, riguarda la ridefinizione a 'protected' degli attributi della classe V, questa commutazione è stata resa necessaria per permettere alla classe derivata M di accedere agli attributi (prima) privati della classe V.
Abbiamo usato stesso nome per identificare metodi diversi, incapsulati in classi differenti della gerarchia, dimostrando, così, il principio del polimorfismo.
Nella classe M il metodo mostra() è ulteriormente specializzato per identificare il tipo di veicolo.



edutecnica