Fondamenti di Informatica 2
Ingegneria Informatica (A-I)
Prof. M.T. PAZIENZA
a.a. 2003-2004 – 3° ciclo
Programmazione orientata agli oggetti
Concetto base : separare i dettagli secondari
dell'implementazione (ad esempio, la struttura dati
impiegata) dalle proprietà fondamentali per un uso
corretto (ad esempio, le funzioni che possono avere
accesso ai dati): ogni utilizzo della struttura dati così
come le routine interne di gestione avvengono
attraverso un'interfaccia specifica sottostante.
Programmazione orientata agli oggetti
Ottimizzare la risoluzione di classi di problemi
richiede la definizione di proprietà essenziali
(criteri di astrazione) che si devono
implementare in una classe di problemi ben
specificata (dominio del problema).
Programmazione orientata agli oggetti
L’astrazione può essere effettuata a due livelli
ASTRAZIONE SUI DATI
consiste nell'astrarre le entità (oggetti) costituenti
il sistema, descritte in termini di una struttura dati
e delle operazioni possibili su di essa.
ASTRAZIONE SUL CONTROLLO
consiste nell'astrarre una data funzionalità dai
dettagli della sua implementazione
Programmazione orientata agli oggetti
Il concetto di tipo di dato in un linguaggio di programmazione
tradizionale (procedurale) è quello di insieme dei valori che può
assumere una informazione (una variabile).
Nella Programmazione ad oggetti, il tipo di dati astratto
(ADT) / oggetto estende questa definizione, includendo anche
l'insieme di tutte e sole le operazioni possibili su dati di quel
tipo.
La struttura dati è incapsulata nelle operazioni su di essa
definite.
Programmazione orientata agli oggetti
La programmazione ad oggetti consente di manipolare
gruppi di variabili correlate e non singole variabili.
Le variabili vengono aggregate in oggetti.
La programmazione ad oggetti si basa sull’incapsulazione
delle variabili all’interno degli oggetti e sulla creazione
di codice che descrive il comportamento interno ed
esterno di tali oggetti.
Programmazione orientata agli oggetti
L’incapsulamento è la proprietà per cui la struttura
interna dell’oggetto è nascosta agli altri oggetti.
La comunicazione tra oggetti avviene tramite l’invio di
messaggi. L’implementazione dei messaggi avviene
tramite le chiamate a funzioni dei metodi degli oggetti.
L’incapsulamento fa sì che le operazioni dell’oggetto non
possano essere in nessun modo influenzate da altri
oggetti.
Programmazione orientata agli oggetti
Un oggetto è rappresentato da variabili e da
comportamenti assunti, ed ha un tipo (classe)
Una classe rappresenta l’astrazione di oggetti
aventi le stesse caratteristiche nel dominio del
problema da risolvere
Programmazione orientata agli oggetti
Le caratteristiche che definiscono una classe sono:
• gli attributi o dati membro, che sono le variabili
associate ad una classe
• i metodi o funzioni membro, che sono le procedure o
servizi che la classe mette a disposizione
Le classi definiscono gli ADT e da esse si istanziano (cioè
si creano) gli oggetti
Programmazione orientata agli oggetti
1. Identificare le classi e gli oggetti che
descrivono il problema e la soluzione
2. Identificare e descrivere i dati membro di
ciascun oggetto
3. Identificare ciascun membro funzione/metodo
che può agire su ciascun tipo di oggetto
4. Per ciascuna funzione membro / metodo,
descrivere i suoi scopi, argomenti, precondizioni e post-condizioni
Programmazione orientata agli oggetti
Un linguaggio di programmazione supporta tecniche
basate sugli oggetti se offre meccanismi espliciti per
la definizione di entità (oggetti) che incapsulano una
struttura dati nelle operazioni possibili su di essa.
C++
Il C++ estende tali meccanismi nella direzione della
tipizzazione dei dati, offrendo la possibilità di
definire istanze (cioè, variabili) di un dato tipo
astratto.
Il linguaggio presenta costrutti per la definizione di
classi e di oggetti.
Una classe è una implementazione di un tipo di dati
astratto.
Un oggetto è una istanza di una classe.
Programmazione orientata agli oggetti
Programmazione orientata agli oggetti (bottom-up):
individuazione delle entità astratte (oggetti) facenti parte del
sistema, delle loro proprietà e delle interrelazioni tra di esse.
Riutilizzabilità degli oggetti in diverse architetture software
Programmazione tradizionale (top-down):
individuazione delle funzionalità del sistema da realizzare
raffinamenti successivi, da iterare finché la scomposizione
del sistema individua sottosistemi di complessità accettabile
Concetti chiave
Classe
Oggetto
Ereditarietà
Polimorfismo
“Binding” dinamico
Tipi di dato
Un tipo di dato costituisce la rappresentazione
concreta di un concetto.
Es.:
il tipo (predefinito) float, insieme alle operazioni
+, -, *, /, rappresenta una versione limitata, ma
concreta, del concetto matematico di numero
reale.
Classe
La classe e' un'astrazione che descrive le
proprietà di tutti gli oggetti caratterizzati da:
– uno stesso insieme di operazioni (protocollo od
interfaccia)
– una stessa struttura interna
– uno stesso comportamento
e che consente la creazione di un numero
qualunque di istanze (oggetti).
Ereditarietà
• L’ereditarietà è il meccanismo mediante il quale una
classe acquisisce tutte le caratteristiche di un’altra
classe definita in precedenza
• La classe che eredita le caratteristiche è chiamata
sottoclasse
• La classe che fornisce le proprie caratteristiche viene
chiamata superclasse
• Una sottoclasse contiene tutto ciò che si trova nella
sua superclasse
• Una classe può ereditare le proprie caratteristiche da
più superclassi (ereditarietà multipla)
Polimorfismo
Il polimorfismo è la capacità che hanno oggetti di classi
diverse (ma correlate per il fatto di derivare da una
classe base comune) di rispondere in maniera diversa
ad uno stesso messaggio.
Es.:
se la classe rettangolo deriva dalla classe quadrilatero,
un oggetto rettangolo è una versione più specifica di un
oggetto quadrilatero.
Il calcolo del perimetro è un’operazione che ha senso
sulla classe quadrilatero, ma che assume una forma
diversa per un rettangolo.
Il polimorfismo si implementa attraverso classi virtuali.
Dichiarazione di una classe
In C++ le classi sono identiche alle strutture con la
differenza formale (oltre ad una sostanziale) di essere
introdotte dalla parola-chiave class anziché struct
Es.:
class CPoint {
float x;
float y;
};
ogni istanza della classe CPoint rappresenta un punto
nel piano e i suoi membri sono le coordinate cartesiane
del punto.
Specificatori di accesso
Specificano chi può accedere ai dati membri:
private
protected
public
Possono essere inseriti più volte all'interno della
definizione di una classe.
Differenza sostanziale fra classe e struttura:
i membri di una struttura sono, per default,
pubblici, mentre quelli di una classe sono, per
default, protetti.
Data Hiding
L'istanza di una classe é regolarmente visibile all'interno
del proprio ambito, ma i suoi membri protetti non lo
sono: non é possibile, da programma, accedere
direttamente ai membri protetti di una classe.
Es.:
class Persona {
int soldi;
public: char telefono[20] ;
char indirizzo[30];
};
istanza della classe Persona: Giuseppe
il programma può accedere a Giuseppe.telefono e
Giuseppe.indirizzo, ma non a Giuseppe.soldi.
Funzioni membro
Le funzioni membro per una classe definiscono il
comportamento che esibiscono gli oggetti di quella
classe. Il comportamento di un oggetto prevede azioni
del tipo:
• Permettere l’accesso ai data member di quell’oggetto
• Permettere l’aggiornamento dei data member di
quell’oggetto
• Permettere che i data member di quell’oggetto siano
testati in modi diversi
• Visualizzare i data member di quell’oggetto
Funzioni membro
I membri protetti non sono accessibili direttamente, ma possono
essere raggiunti indirettamente, tramite le funzioni-membro.
Queste funzioni possono essere, come ogni altro membro,
pubbliche o protette, ma, in ogni caso, possono accedere a
qualunque altro membro della classe, anche ai membri protetti.
Mentre una funzione-membro protetta può essere chiamata
solo da un'altra funzione-membro, una funzione-membro
pubblica può anche essere chiamata dall'esterno, e pertanto
costituisce il tramite fra il programma e i membri della classe
(interfaccia).
Dichiarazione di classe (esempio)
class contatore {
int val; //private per default
public:
void reset();
void incr();
void decr();
int visual();
};
Viene creata una classe di nome contatore costituita da :
un intero di nome val e un insieme di operazioni (protocollo o
interfaccia) che rappresentano che cosa può fare un oggetto di
questa classe (azzerarsi, incrementarsi, decrementarsi,
visualizzare il valore)
L’operatore ::risolutore di ambito
Per indicare come opera un oggetto della classe, si deve scrivere il
codice delle funzioni:
void contatore::reset() { val=0;}
void contatore::incr() { val++;}
void contatore::decr() { val--;}
int contatore::visual() { return val;}
Preponendo al nome della funzione il nome della classe, seguito
dall'operatore di risoluzione dell'ambito di azione (::), si fa in
modo che la funzione venga riconosciuta come metodo di una
classe (distinta dalle normali funzioni) e che la sua classe di
appartenenza venga univocamente identificata (anche nel caso di
più funzioni con lo stesso nome).
Dichiarazione di classe (esempio)
class complesso {
double re, im;
public:
void iniz_compl (double r, double i) { re=r; im=i;};
double reale() { return re;};
double immag() { return im;};
};
Viene creata una classe di nome complesso costituita da una
variabile reale ed una immaginaria e un insieme di operazioni
(protocollo o interfaccia) che rappresentano che cosa può fare un
oggetto di questa classe. E’ possibile mantenere distinta la
definizione delle funzioni dalla loro dichiarazione all’interno della
classe.
Dichiarazione di classe (esempio)
class complesso {
double re, im;
public:
void iniz_compl (double r, double i);
double reale() ;
double immag();
};
void complesso::iniz_compl (double r, double i)
{ re=r; im=i;}
double complesso:: reale() { return re;};
double complesso:: immag() { return im;};
Il nome della funzione deve essere preceduto dal nome della classe e
dall’operatore di risolutore d’ambito o di visibilità ::
Dichiarazione di classe (esempio)
Se, ad esempio, si volesse cambiare la
realizzazione del tipo, basterebbe cambiare
la parte privata della classe e la
realizzazione (ma non la dichiarazione)
delle funzioni membro.
Poiché l’interfaccia non è cambiata, i
programmi che usano la classe complesso
restano immutati
Operazioni predefinite sulle classi
Oltre alla selezione di funzioni membro e di campi dato,
esistono altre funzioni predefinite sugli oggetti classe:
•
•
•
•
inizializzazione
assegnazione ad un altro oggetto
uso come argomento di una funzione
ritorno da una funzione
Le operazioni di confronto non sono definite per gli
oggetti classe.
Dichiarazione di oggetti
Una volta definita la classe, si possono istanziare
un numero qualunque di oggetti e questi avranno
la stessa rappresentazione concreta e potranno
eseguire le sole operazioni descritte nella classe.
Es:
contatore c1,c2;
definisce due oggetti/istanze di tipo contatore
ognuno dei quali ha la propria copia di val e può
eseguire solo le operazioni reset(), incr(), decr(),
visual().
Accesso ai membri di una classe
Per usare un oggetto precedentemente definito,
basta scrivere il suo nome seguito
dall’operatore . e dall'operazione che deve
eseguire:
Es.
c1.reset();
Questa istruzione ha il significato di invio del
messaggio all'oggetto c1 di eseguire
l'operazione reset() che nel nostro caso pone a
zero il campo val dell'oggetto c1.
Costruttori e distruttori
Gli oggetti si comportano come normali variabili
rispetto alla visibilità ed al ciclo di vita.
In presenza della dichiarazione:
contatore c1;
il compilatore costruirà un oggetto c1 il cui contenuto in
questo esempio è indefinito, non essendo stato
inizializzato.
Analogamente all'uscita dalla funzione in cui c1 è stato
dichiarato, vi sarà la sua distruzione e ciò in perfetto
accordo con il ciclo di vita delle variabili locali.
In entrambi i casi, il compilatore invoca un costruttore ed
un distruttore di default.
Costruttori
I costruttori (funzioni membro pubbliche di ciascuna
classe) devono sottostare alle seguenti regole (rif.
all’esempio della classe CPoint):
– devono
avere
lo
stesso
nome
della
classe
Es: CPoint::CPoint();
– non bisogna specificare il tipo di ritorno (neanche void);
– ammettono argomenti e defaults
Es: CPoint::CPoint(const float a, const float b=0.0f);
– possono esistere più costruttori, in overload, in una stessa
classe.
Il C++ li distingue in base alla lista degli argomenti
Costruttori
I costruttori sono eseguiti automaticamente nel
momento in cui l'oggetto è dichiarato nel programma
(per questo motivo i costruttori devono essere
definiti pubblici).
Es. :
CPoint::CPoint() { x=3.5f ; y=2.1f }
…
CPoint p;
Nel momento in cui è eseguita l'istruzione di cui
sopra: p.x=3.5f e p.y=2.1f
Costruttori con argomenti
Nel caso in cui un costruttore ammetta argomenti, i loro
valori devono essere specificati nella stessa dichiarazione
dell'oggetto:
Es.:
CPoint::CPoint(const float a, const
float b) { x=a ; y=b; }
CPoint p = CPoint(3.0f,1.2f);
Il costruttore entra in azione quando viene allocata la
memoria per l'oggetto.
Es:
Cpoint* ptr; (il costruttore non è ancora eseguito)
ptr = new CPoint; (adesso sì)
Costruttori
Dichiarazione e inizializzazione possono
coesistere in un'unica istruzione:
Cpoint* ptr = new CPoint(0.0f,0.0f);
–
–
–
–
alloca memoria per un oggetto della classe CPoint,
ne restituisce l'indirizzo,
che usa per inizializzare il puntatore ptr,
e inizializza l'oggetto eseguendo il costruttore
CPoint con gli argomenti fissati ai valori 0.0f e
0.0f
Utilità di costruttori e distruttori
In C++ ogni oggetto ha una sua precisa
connotazione, caratterizzata da proprietà e
metodi.
I costruttori e i distruttori (entrambi funzioni
membro pubbliche di ciascuna classe) hanno un
campo di applicazione molto più vasto della
semplice inizializzazione o rilascio di aree di
memoria: possono servire ogni qual volta un
oggetto richieda ben definite operazioni iniziali e
finali, incapsulate nell’oggetto stesso.
Programmazione orientata agli oggetti
modularità: le classi sono i moduli del sistema software;
coesione dei moduli: una classe è un componente software ben
coeso in quanto rappresentazione di una unica entità;
disaccoppiamento dei moduli: gli oggetti hanno un alto grado
di disaccoppiamento (i metodi operano sulla struttura dati interna
ad un oggetto); il sistema complessivo viene costruito
componendo operazioni sugli oggetti;
riuso: l'ereditarietà consente di riusare la definizione di una classe
nel definire nuove (sotto)classi;
information hiding: sia le strutture dati che gli algoritmi possono
essere nascosti alla visibilità dall'esterno di un oggetto;
estensibilità: l'ereditarietà, il polimorfismo ed il binding dinamico
agevolano l'aggiunta di nuove funzionalità, minimizzando le
modifiche da applicare al sistema per estenderlo.
Librerie di classi
Quando, nella dichiarazione di una classe, si lasciano solo i
prototipi dei metodi, si suole dire che viene creata
un'intestazione di classe.
E’ buona consuetudine creare librerie di classi, separando in due
gruppi distinti:
le intestazioni, distribuite in header-files,
il codice delle funzioni, compilate separatamente e distribuite in
librerie in formato binario;
Ai programmatori che utilizzano le classi non interessa sapere
come sono fatte le funzioni di accesso, ma solo come usarle:
separazione tra il punto di vista di chi realizza la classe ed il
punto di vista di chi usa la classe.
Esempio Librerie di classi
File contator.h
class contatore {
int val;
public:
contatore();
contatore(int);
~contatore();
void reset();
void incr();
void decr();
int visual();
};
File contator.cpp
#include"contator.h"
contatore::contatore() {val=0;}
contatore::contatore(int a)
{val=a;}
contatore::~contatore()
{cout<<"distruttore"<<endl;
}
void contatore::reset() { val=0; }
void contatore::incr() { val++; }
void contatore::decr() { val--; }
int contatore::visual() { return
val; }
Esempio
File prova.cpp
#include<iostream.h>
#include"contator.h"
main()
{
contatore c1;
contatore c2(12);
c1.inc();
c2.dec();
cout<<"c1="<<c1.visual()<<endl;
cout<<"c2="<<c2.visual()<<endl;
}