Istituto Angioy Carbonia, Corso di Informatica, prof. Gianfranco

ISTITUTO ANGIOY - CARBONIA
Programmazione Orientata agli Oggetti e C++
Prof. Gianfranco Ciaschetti
1. Introduzione e prime nozioni di base
Finora abbiamo usato il C come linguaggio di programmazione per il computer. Il C è un
linguaggio di programmazione di tipo procedurale, nel senso che un programma scritto in C è
costituito da una sequenza di istruzioni, eventualmente organizzate in sottoprogrammi. Si procede,
dunque, secondo il punto di vista del computer: esso sa fare alcune cose, noi gli diciamo cosa fare
con un linguaggio che esso sa interpretare (anche se, in realtà, è il compilatore che conosce il
linguaggio C e lo traduce in linguaggio macchina, il solo che il computer capisce).
Nella Programmazione Orientata agli Oggetti (Object Oriented Programming, o OOP), e in
particolare nel linguaggio C++, le cose cambiano notevolmente. Il C++ è un linguaggio
dichiarativo, nel senso che un programma, piuttosto che da una sequenza d’istruzioni, è composto
da un insieme di oggetti e relazioni tra di essi. Ogni oggetto possiede degli attributi che ne
descrivono le caratteristiche e/o lo stato, e possiede inoltre dei metodi che permettono agli altri
oggetti di interagire con esso.
Facciamo un esempio: l’oggetto cane potrebbe essere descritto dagli attributi nome, razza, età che
ne descrivono le caratteristiche, e dagli attributi posizione, umore che ne descrivono lo stato (ad
esempio, posizione potrebbe essere una variabile che può assumere i valori “seduto”, “accucciato”,
“normale”, “sollevato”), e dai metodi da_la_zampa( ), abbaia( ). Certo, un cane nella realtà è molto
di più di questo, ma noi scegliamo (è solo un esempio) che ai nostri fini ci interessano solo questi
attributi e questi metodi. Inoltre, potremmo non avere bisogno di includere nel nostro programma
anche un oggetto guinzaglio e un oggetto collare, che riteniamo poco importanti. Infine, ci potrebbe
essere un oggetto padrone, anch’esso con dei propri attributi e metodi, che chiede al proprio cane di
dare la zampa, cioè, invoca il metodo da_la_zampa dell’oggetto cane.
Programmare in C++ significa allora osservare la realtà (detta dominio dell’applicazione), e
rappresentarla (o modellarla) con una serie di oggetti in qualche modo collegati tra loro. L’attività
di modellazione a oggetti è una fase molto delicata (in genere, prende il nome di Object Oriented
Analysis, o OOA), e richiede inevitabilmente un processo di astrazione: quali oggetti
rappresentare? E di ogni oggetto, quali attributi e metodi includere? Osserviamo che non tutti i
dettagli sono importanti ai nostri fini, ma solo alcuni. Facciamo un esempio: supponiamo di dover
creare un programma per la gestione dei dati degli studenti dell’Istituto. Sicuramente avremo
bisogno di definire un oggetto studente, specificandone i dati anagrafici, ma non il colore dei capelli
o i suoi hobby o il numero di compagni che ha. In generale, quindi, occorre concentrarsi su quegli
aspetti della realtà che riteniamo significativi per i nostri scopi. Si noti che realizzare un modello
troppo semplice (pochi oggetti con pochi dati) può risultare poco utile, mentre realizzare un
modello troppo complesso (tanti oggetti con tanti dati) può dar luogo a un programma difficilmente
gestibile. Occorre fare, quindi, un compromesso che sia il migliore possibile.
ESEMPIO:
OGGETTO: bicchiere
ATTRIBUTI: forma, capacità, colore, materiale, pieno_o_vuoto
METODI: riempi( ), svuota( )
ESEMPIO:
OGGETTO: motore
ATTRIBUTI: cilindrata, alimentazione, num_cilindri, num_valvole
METODI: accendi( ), spegni( )
Oltre al C++, che è l’estensione a oggetti del linguaggio C, citiamo altri esempi famosi di linguaggi
di programmazione dichiarativi, cioè orientati agli oggetti:
-
Simula67 (il primo)
-
Smalltalk (il puro: tutti gli altri linguaggi derivano – in qualche modo – da un linguaggio
procedurale)
-
Java (multipiattaforma, grazie alla macchina virtuale su cui si appoggia, adatto per
applicazioni su internet lato client)
-
Delphi (evoluzione a oggetti del Turbo Pascal)
-
PHP (per applicazioni web lato server)
-
C# e VisualBasic.NET (evoluzioni di C++ e VisualBasic, rispettivamente, per applicazioni
web lato server).
Oltre al C, invece, sono linguaggi procedurali “classici” linguaggi come il Basic, il Cobol, il Pascal,
il Fortran. Esistono poi altri tipi di linguaggi che non sono né procedurali né dichiarativi, come i
linguaggi funzionali (Lisp, Logo, Caml), o i linguaggi logici (Prolog, Mercury), ma non fanno parte
di questa trattazione.
Tornando alla nostra OOP, il paradigma di programmazione “a oggetti” nasce dall’esigenza di
“modularizzare” il software (ricordate la scomposizione di un programma in funzioni?) facendo in
modo che ogni modulo sia autonomo e indipendente dagli altri. Ricordando i record (le struct in
linguaggio C), gli oggetti (più propriamente, le classi di oggetti del C++) nascono perché si vuole
non solo descrivere come è fatto un record, ma anche cosa sa fare, e come interagisce con le altre
strutture: cioè, si vogliono inserire anche delle funzioni nella struct.
2. Modellazione a oggetti: UML
Abbiamo detto che i linguaggi object oriented sono dichiarativi: programmare significa “dichiarare”
quali sono gli oggetti e quali sono le relazioni tra essi. Prima di fare questo nel linguaggio C++, è
opportuno disegnare un modello a oggetti, che descrive quali sono gli oggetti e quali le relazioni.
Un tale modello prende il nome di Diagramma E/R (entità/relazioni), usando un linguaggio di
rappresentazione di tipo grafico. Tra i linguaggi per rappresentare uno schema E/R, il più diffuso è
l’UML (Unified Modeling Language).
ATTENZIONE: perché ora parliamo di entità, e non di oggetti? Un’entità in UML corrisponde a
una classe di oggetti in C++, tutti con stesse caratteristiche. Ad esempio, l’entità cane può
rappresentare un insieme di oggetti di tipo cane, come il mio cane, il tuo, quel cagnolino che sta
all’ingresso della scuola, ecc. Un particolare cane, invece, è un oggetto, detto anche istanza della
classe. Per rendere più chiaro il concetto, facciamo un parallelo col linguaggio C: un’entità del
linguaggio UML o una classe di oggetti in C++ è come un tipo di dati in C: descrive solo come è
fatto un oggetto. Un oggetto, invece, ha una sua propria rappresentazione in memoria, e può quindi
essere assimilato a una variabile in linguaggio C.
ESEMPIO:
LINGUAGGIO C
LINUGAGGIO C
LINGUAGGIO C++
int a;
struct cane {…};
class cane {…};
/* int = tipo */
struct cane c;
cane miocane;
/* a = variabile */
/* struct = tipo */
/* classe = tipo */
/* c = variabile */
/* miocane = variabile */
la variabile a permette di
memorizzare un dato di tipo
int.
la variabile c permette di la variabile miocane permette di
memorizzare un record di memorizzare un oggetto di tipo
tipo struct cane.
cane.
Il linguaggio di modellazione UML prevede di disegnare ogni entità in un box, come nell’esempio
che segue:
Nell’esempio, viene descritta l’entità BICCHIERE, che definisce quali sono gli attributi e i metodi
degli oggetti di questa classe. Ogni oggetto di tipo BICCHIERE avrà quindi gli attributi forma,
capacita, colore, materiale, pieno_o_vuoto, e i metodi riempi( ) e svuota( ).
In UML, per ogni entità, vengono descritti il suo nome, la lista degli attributi, e la lista dei
metodi. Si noti che occorre specificare per ogni attributo di che tipo di dato si tratta, e per ogni
metodo la firma della funzione, ossia, il tipo di dato restituito e la lista dei parametri (in questo caso
non ce ne sono). Il corpo delle funzioni verrà indicato solo a un grado di dettaglio maggiore, ossia
quando andremo a trasformare il diagramma in un programma in C++, e non fa parte di uno schema
E/R.
Si noti anche che agli attributi è stato dato un segno -, mentre ai metodi è stato dato un segno +.
Queste sono le regole di visibilità, che spiegheremo meglio più avanti. Per il momento, possiamo
brevemente introdurre questo concetto dicendo che gli attributi di un oggetto sono suoi personali, e
non dovrebbero poter essere accessibili dall’esterno. Ad esempio, se io chiedo il nome all’oggetto di
nome Pinco Pallino (istanza della classe studente, ad esempio), non sto accedendo direttamente alla
sua informazione (che è riservata…c’è la privacy), ma sto invocando il suo metodo DimmiNome( ).
L’oggetto Pinco Pallino potrebbe rispondermi con il suo nome vero, o con un altro nome, sono
affari suoi. Per tale motivo, gli attributi sono solitamente dichiarati di tipo private, ossia accessibili
solo all’oggetto stesso, mentre i metodi (che sono quelli con cui esso interagisce con altri oggetti)
sono dichiarati solitamente di tipo public. Tuttavia, sebbene questa è buona pratica, non è una
regola generale, possono esserci delle eccezioni. Ne riparleremo tra un po’!
Oltre alle entità (che abbiamo visto corrispondere a classi di oggetti), in un diagramma UML
compaiono anche le relazioni tra entità. Esse possono essere di diverso tipo:
a) relazioni gerarchiche ISA (letteralmente, is a in inglese)
servono per specificare che un entità è un’altra entità.
Ad esempio: un bicchiere è (is a) un recipiente
b) relazioni di aggregazione HAS (letteralmente, has in inglese)
servono per specificare che un entità ha un’altra entità.
Ad esempio: un’automobile ha (has) un motore (…e anche altri oggetti…)
c) relazioni di associazione
servono per specificare generiche associazioni tra le entità.
Ad esempio: un impiegato lavora in un reparto
I nomi delle associazioni solitamente rappresentano verbi in forma attiva.
Negli ultimi due tipi di relazione possono comparire anche delle molteplicità, che indicano quanti
oggetti sono implicati nella relazione. Ad esempio, nella relazione tra automobile e motore c’è una
molteplicità 0..1 per l’automobile, e 1 per il motore (ogni automobile ha un motore, ogni motore
può essere associato a una sola automobile, se consideriamo anche motori non montati). Sempre a
titolo di esempio, tra impiegato e reparto c’è una molteplicità 1..n per l’impiegato, 1 per il reparto
(ogni impiegato lavora in un reparto, ogni reparto ha uno o più impiegati). Le molteplicità sono
rappresentate nel diagramma come segue:
Le possibili molteplicità sono:
0..1
relazione facoltativa con un solo oggetto
0..n
relazione facoltativa con uno o più oggetti
1
relazione obbligatoria con un solo oggetto
1..n
relazione obbligatoria con uno o più oggetti
Nelle relazioni ISA solitamente non viene specificata la molteplicità, dato che è sempre la stessa:
1 dalla parte della entità “padre”, 0..1 dalla parte dell’entità “figlia”. Ad esempio, tra le entità
bicchiere e recipiente dell’esempio di sopra, possiamo dire che un bicchiere è sempre un recipiente
(molteplicità 1 dalla parte dell’entità recipiente), mentre un recipiente può essere un bicchiere o no
(molteplicità 0..1 dalla parte dell’entità bicchiere).
Per concludere questo paragrafo, diciamo che quando si disegna un diagramma UML si dovrebbe
stare attenti a disegnare solo linee orizzontali e verticali, cercando di non farle sovrapporre troppo,
per non rendere il diagramma illeggibile.
3. Dal diagramma E/R al programma in C++
Una volta disegnato il diagramma E/R, si ottiene il programma in C++ traducendo innanzitutto
ogni entità in una classe. Ad esempio, se riprendiamo l’entità BICCHIERE descritta due pagine fa,
essa viene tradotta in C++ nel seguente modo:
class Bicchiere {
private:
char forma[20];
float capacita;
char colore[10];
char materiale[20];
int pieno_o_vuoto;
public:
void riempi();
void svuota();
};
Il programma C++ sarà allora costituito, innanzitutto, da un insieme di dichiarazioni di classe (una
per ogni file con estenzione .h, di solito), come quella dell’esempio appena fatto, ottenute dallo
schema in UML. Anche qui, come in UML, non compare il corpo dei metodi.
Una volta tradotte le entità del diagramma E/R nelle classi in C++, bisogna tradurre in C++ anche le
diverse relazioni tra le entità.
-
Le relazioni ISA, come vedremo tra poco parlando di ereditarietà, si modellano definendo
una classe (la classe “figlia”) come derivata da un’altra classe (la classe “padre”).
-
Le relazioni HAS si modellano inserendo un oggetto come attributo di un altro oggetto (nel
prossimo paragrafo, quando parleremo di creazione di oggetti, vedremo come fare).
-
Le relazioni di associazione generica vengono tradotte in C++ facendo interagire tra loro gli
oggetti: un oggetto richiama i metodi di un altro oggetto.
4. Creazione e distruzione di oggetti
Una volta definita una classe in C++, come è possibile creare istanze (oggetti) della classe? Per
quanto riguarda la creazione, esiste la possibilità di creare due tipi di oggetti:
- oggetti statici
- oggetti dinamici
Gli oggetti statici vengono creati semplicemente dichiarando una variabile, sono memorizzati in
un’area di memoria che si chiama stack, ed esistono finché è in esecuzione il sottoprogramma nel
quale sono dichiarati, esattamente come le variabili locali delle funzioni in C.
Gli oggetti dinamici, invece, vengono creati con un’istruzione new, sono allocati in un’aria di
memoria chiamata heap, e restano in memoria finché non sono esplicitamente cancellati con
un’istruzione delete. L’istruzione new crea un nuovo oggetto, e restituisce l’indirizzo di memoria
dell’oggetto creato, che deve essere salvato in una variabile di tipo puntatore.
ESEMPIO
data la seguente definizione di classe…
Class Bicchiere
{
…
};
creiamo, ad esempio nel main, un oggetto statico e un oggetto dinamico della classe Bicchiere (non
è detto, in generale, che la creazione di oggetti debba avvenire per forza nel main)
main()
{
Bicchiere b; /* oggetto statico della classe Bicchiere */
/* l’oggetto b “vive” per tutta la durata del main */
Bicchiere *pb = new Bicchiere; /* oggetto dinamico della classe Bicchiere */
/* l’oggetto creato dalla new, il cui indirizzo è salvato nella
variabile pb di tipo puntatore a bicchiere, resta in
memoria finché non viene esplicitamente cancellato */
…
delete pb; /* cancellazione dell’oggetto puntato da pb */
}
ESEMPIO
data la seguente definizione di classe…
Class Cane
{
…
};
creiamo due oggetti statici e due oggetti dinamici della classe Cane
Cane c1, c2; /* oggetti statici della classe cane */
/* gli oggetti c1 e c2 “vivono” per tutta la durata del sottoprogramma dove sono
dichiarati */
…
Cane *pc1, *pc2; /* dichiarazione di due variabili di tipo puntatore a Cane */
pc1 = new Cane; /* primo oggetto dinamico della classe Cane, il suo indirizzo è memorizzato
nel puntatore pc1 */
pc2 = new Cane; /* secondo oggetto dinamico della classe Cane, il suo indirizzo è
memorizzato nel puntatore pc2 */
…
delete pc1; /* cancellazione dell’oggetto puntato da pc1 */
delete pc2; /* cancellazione dell’oggetto puntato da pc1 */
5. Accesso agli attributi e ai metodi di un oggetto
Per accedere agli attributi e ai metodi di un oggetto statico in C++ si usa l’operatore . (punto),
esattamente come si fa per accedere ai campi di un record in C.
Per accedere agli attributi e ai metodi di un oggetto dinamico in C++ si usa l’operatore -> (freccia).
ESEMPIO
Riprendendo l’esempio precedente, supponiamo di avere due oggetti della classe Bicchiere:
l’oggetto statico b e l’oggetto dinamico il cui indirizzo è memorizzato nel puntatore pb.
-
Se vogliamo accedere all’attributo forma dell’oggetto statico b, scriveremo b.forma
-
Se vogliamo accedere, invece, all’attributo forma dell’oggetto dinamico puntato da pb,
scriveremo pb->forma
La stessa notazione si usa anche per i metodi:
-
Se vogliamo invocare il metodo svuota( ) dell’oggetto statico b scriveremo b.svuota( )
-
Se vogliamo invocare il metodo svuota( ) dell’oggetto dinamico puntato da pb scriveremo
pb->svuota( ).
6. Metodi costruttori e metodi distruttori
Quando si crea un nuovo oggetto, è possibile eseguire contestualmente delle operazioni
“collaterali”, come l’inizializzazione dell’oggetto o l’aggiornamento di un contatore del numero di
oggetti creati, o qualsiasi altra operazione vogliamo fare al momento della creazione dell’oggetto.
Questo è possibile definendo nella classe uno o più metodi costruttori, che vengono invocati
all’atto della creazione dell’oggetto. I metodi costruttori:
-
hanno lo stesso nome della classe
-
non hanno un tipo di ritorno
-
possono essere più di uno purché abbiano differente firma
-
sono invocati automaticamente all’atto della creazione di un oggetto
ESEMPIO (solo a titolo di esempio, inseriamo il corpo dei metodi all’interno della definizione della
classe. Vedremo successivamente che, invece, il corpo delle funzioni va messo da un’altra parte).
Class Punto
{
private:
float coordx;
float coordy;
public:
Punto() {coordx = 0; coordy = 0}; /* metodo costruttore senza parametri: inizializza un
punto nell’origine degli assi */
Punto(float x, float y) {coordx = x; coordy = y}; /* metodo costruttore con due parametri
di tipo float: inizializza un punto nelle
coordinate passate come parametri */
};
Quando creiamo un oggetto statico della classe punto:
Punto p1;
essendo stato definito un metodo costruttore senza parametri, questo viene eseguito
automaticamente, inizializzando il punto nell’origine degli assi. Se non c’è il metodo costruttore
vuoto, nessuna inizializzazione dell’oggetto viene effettuata.
Quando invece creiamo un oggetto dinamico della classe punto, abbiamo tre possibili scelte:
-
Punto *p2 = new Punto; /* viene eseguito il metodo costruttore vuoto, se esiste,
altrimenti viene creato un oggetto privo di
inizializzazione */
-
Punto *p2 = new Punto(); /* si può fare solo se è stato definito un metodo
costruttore vuoto, che parte automaticamente
inizializzando l’oggetto */
-
Punto *p2 = new Punto(4,7); /* si può fare solo se è stato definito un metodo
costruttore
con
parametri,
che
parte
automaticamente inizializzando l’oggetto ai valori
passati */
Allo stesso modo dei metodi costruttori, è possibile definire dei metodi distruttori, che vengono
eseguiti automaticamente al momento della cancellazione di un oggetto dinamico. Servono per, ad
esempio, decrementare un contatore degli oggetti, oppure per visualizzare sul monitor un messaggio
che indica la avvenuta cancellazione. I metodi distruttori:
-
hanno lo stesso nome della classe, preceduto dal simbolo ~
-
non hanno un tipo di ritorno
-
ci può essere un solo distruttore per ogni classe (senza parametri)
-
sono invocati automaticamente con un’istruzione delete
Ad esempio, supponiamo di avere una variabile globale num_punti che memorizza il numero di
oggetti creati della classe Punto, e supponiamo di aver definito un metodo distruttore come segue
(anche qui, solo per chiarezza di esposizione, inseriamo il corpo dei metodi all’interno della
definizione della classe):
Class Punto
{
…
public:
…
~Punto() {num_punti--}; /* metodo distruttore */
}
Quando viene eseguita una delete su un oggetto della classe punto, la variabile num_punti viene
decrementata di un’unità.
7. Caratteristiche della programmazione a oggetti
Le principali caratteristiche della programmazione a oggetti, che ne fanno uno strumento
estremamente potente e versatile, oltre all’aspetto dichiarativo illustrato in precedenza, sono
l’ereditarietà, l’incapsulamento e il polimorfismo.
7.1 EREDITARIETÀ
Per ereditarietà si intende la possibilità di definire una classe di oggetti (detta sottoclasse) come
derivata da un’altra classe (detta superclasse), in una relazione gerarchica di tipo ISA. La
sottoclasse eredita tutti gli attributi e i metodi della superclasse, e in più può averne di propri. La
sottoclasse risulta dunque essere una specializzazione della superclasse, mentre la superclasse
risulta essere una generalizzazione della sottoclasse. La cosa è molto conveniente in quanto
permette di risparmiare molto tempo nella programmazione, ad esempio includendo in una
superclasse tutti gli attributi e i metodi comuni a diverse classi di oggetti.
ESEMPIO
Class Impiegato
{
private:
char nome[20];
char cognome[20];
int eta;
};
Class Docente: Impiegato
{
private:
char materia[20];
};
Class Tecnico: Impiegato
{
private:
char lab [5];
};
Le classi Docente e Tecnico sono dichiarate come sottoclassi della superclasse Impiegato. Come
tali, ereditano tutti gli attributi della superclasse, quindi, ogni docente avrà gli attributi nome,
cognome, eta e materia, mentre ogni tecnico avrà gli attributi nome, cognome, eta e lab. E’ sempre
conveniente definire delle superclassi qualora si voglia non ripetere la definizione di certi attributi o
metodi.
Approfondimento: se nella classe derivata non è definito un metodo costruttore, esso viene cercato
nella gerarchia delle superclassi, fino a incontrarne uno utile. Se invece è definito un metodo
costruttore, per il principio del mascheramento, viene usato quest’ultimo.
7.2 INCAPSULAMENTO
Per incapsulamento si intende la possibilità di proteggere i dati di un oggetto, per evitare errori di
programmazione, e rendere più modulare la scrittura dei programmi, in modo da avere così un
codice maggiormente robusto e riusabile.
L’incapsulamento permette ad altri oggetti di interagire con un dato oggetto solo attraverso i suoi
metodi.
L’incapsulamento si ottiene in due modi:
1. Regole di visibilità e Metodi Get e Set
2. Separando interfaccia e implementazione dei metodi, ossia scrivendo solo la
firma – insieme con la definizione della classe - in un file .h (header, interfaccia)
e il corpo in un file .cpp (implementazione).
Regole di visibilità:
Abbiamo già visto le parole chiave private e public. Ricordiamo che un attributo o un metodo
definito come private è visibile solo all’oggetto stesso, mentre un attributo o un metodo definito
come public è visibile a tutti. Oltre a queste, possiamo usare anche le parole chiave protected e
static.
-
Un attributo definito come protected può essere visto solo dalle sottoclassi, ossia da tutti gli
oggetti che sono in relazione ISA con l’oggetto stesso. Ad esempio, se ricordiamo la
relazione ISA tra Bicchiere e Recipiente, e supponendo di avere l’oggetto b della classe
Bicchiere e l’oggetto r della classe Recipiente, b può accedere agli attributi di r.
-
Un attributo definito come static si dice anche variabile di classe (e non di istanza), in
quanto ne esisterà una sola copia per tutti gli oggetti della classe, a differenza degli altri
attributi dei quali ne abbiamo uno per ogni oggetto della classe (che infatti non si chiamano
variabili di classe, ma variabili di istanza). Un attributo static è visibile solo a tutti gli
oggetti della classe, a meno che non venga definito esplicitamente come pubblico.
Approfondimento: le regole di visibilità definiscono anche le regole per l’ereditarietà di attributi e
metodi. In particolare, se gli attributi sono definiti come private nella superclasse, essi non vengono
ereditati nella sottoclasse! Per permettere l’ereditarietà, occorre definirli invece come protected.
Inoltre, perché avvenga l’ereditarietà, occorre che una sottoclasse sia derivata pubblicamente dalla
superclasse, nel seguente modo:
class A : public B { };
Metodi Get e Set:
Per poter accedere dall’esterno alle variabili di istanza, cioè agli attributi di un oggetto, essendo
queste dichiarate come private, occorre in genere definire dei metodi Get che, una volta invocati,
permettono all’oggetto di rendere noti all’esterno i propri dati. Allo stesso modo, occorre (solo
quando ce n’è bisogno) definire dei metodi Set per poter modificare i dati di un oggetto
dall’esterno.
Riprendendo l’esempio della classe Punto, possiamo definire i seguenti metodi (di nuovo,
includiamo il corpo dei metodi nella definizione della classe solo per facilità di esposizione):
class Punto {
private:
… attributi…
public:
… eventuali costruttori e distruttore …
… eventuali altri metodi …
float GetX(){ return coordx;} /* restituisce la coordinata x del punto */
float GetY(){ return coordy;} /* restituisce la coordinata y del punto */
void SetX (float newx){coordx = newx}; /* modifica la coordinata x del punto */
void SetY (float newy){coordy = newy}; /* modifica la coordinata y del punto */
};
Separazione tra interfaccia e implementazione:
Si tratta, come abbiamo detto più volte fino a questo punto, di includere solo la firma dei metodi
insieme alla definizione della classe, in modo che altri oggetti possano vedere solo cosa un oggetto
sa fare, ma non come sa farlo.
Come già detto, la definizione della classe con la firma dei metodi (il cosa l’oggetto sa fare) viene
posta in un file di intestazione (header) con estensione .h, che verrà reso visibile agli altri oggetti
con la direttiva include.
Il corpo dei metodi (il come viene fatto) della classe, invece, viene posto in un file di
implementazione con estensione .cpp, che non verrà reso visibile all’esterno.
La separazione tra interfaccia e implementazione permette una maggior protezione dei dati: ogni
oggetto è visto come un modulo autonomo: solo lui conosce i propri dati (gli attributi) e come
esegue le proprie funzionalità (il corpo dei metodi).
Facciamo un esempio: se un oggetto u1 della classe UOMO vuole sapere il valore dell’attributo
nome di un altro oggetto u2 della stessa classe,
-
Non può accedere direttamente all’attributo – che è privato – invocando u2.nome oppure
u2->nome
-
Può accedere al metodo pubblico u2.GetNome( ), conoscendone l’esistenza – la firma del
metodo è visibile perché definita nel file .h. A questo punto, u2 esegue il suo metodo
GetNome( ) e risponde a u1, ma non è detto che debba per forza dire il suo nome: cosa
accade all’interno del corpo del metodo GetNome( ) dell’oggetto u1 sono fatti suoi! Egli
potrebbe rispondere con il suo vero nome, o con qualsiasi altra cosa.
La separazione tra interfaccia e implementazione permette inoltre una maggior riusabilità e facilità
di mantenimento del programma: e’ possibile, infatti, cambiare in futuro solo l’implementazione di
un metodo, senza dover ricompilare l’intero programma, ma solo il file .cpp che lo contiene, mentre
tutto il resto continua a funzionare così com’è.
Riprendendo l’esempio della classe Punto, scriveremo la definizione della classe nel file punto.h:
Class Punto
{
private:
float coordx;
float coordy;
public:
Punto();
Punto(float x, float y);
float GetX();
float GetY();
void SetX(float newx);
void SetY(float newy);
};
e l’implementazione dei metodi nel file punto.cpp
Punto::Punto() /* classe Punto, implementazione del metodo Punto() */
{
coordx = 0;
coordy = 0
};
Punto::Punto(float x, float y) /* classe Punto, implementazione del metodo Punto(float x, float y) */
{
coordx = x;
coordy = y;
};
float Punto::GetX() /* classe Punto, implementazione del metodo GetX() */
{
return coordx;
};
float Punto::GetY() /* classe Punto, implementazione del metodo GetY() */
{
return coordy;
};
void Punto::SetX(float newx) /* classe Punto, implementazione del metodo SetX() */
{
coordx = newx;
};
void Punto::SetY(float newy) /* classe Punto, implementazione del metodo SetY() */
{
coordy = newy;
};
Tutti gli oggetti che dovranno interagire con oggetti della classe Punto dovranno, inevitabilmente,
conoscere la definizione della classe Punto, e quindi includere nei loro file il file punto.h con la
seguente istruzione:
#include “punto.h”
Ovviamente, anche il file di implementazione punto.cpp dovrà conoscere la definizione della classe,
e quindi avere la stessa direttiva per includere l’header file.
7.3 POLIMORFISMO
Si intende per polimorfismo la possibilità di avere più versioni dello stesso metodo per oggetti di
classi diverse. Un esempio è quello che accade anche in C con l’operatore di divisione / che
applicato a operandi di tipo intero esegue la divisione intera (quoziente), mentre applicato a
operandi di tipo reale (float o double) esegue la divisione con virgola (5/2 restituisce 2, mentre
5.0/2.0 restituisce 2.5). L’utilità di avere funzioni polimorfe sta nel fatto che non dobbiamo
preoccuparci di dover assegnare nomi diversi a metodi simili tra loro, e risulta molto utile se usata
insieme all’ereditarietà.
ESEMPIO:
Supponiamo di avere due sottoclassi Mastino e Bassotto derivate dalla superclasse Cane, definite
rispettivamente nei file mastino.h e bassotto.h
Class Cane {…} /* definizione della classe Cane nel file cane.h */
Class Mastino: Cane {… } /* definizione della classe Mastino nel file mastino.h */
Class Bassotto: Cane {… }
/* definizione della classe Bassotto nel file bassotto.h */
e supponiamo che nella classe Cane sia definito il metodo Abbaia( ). Tale metodo, come sappiamo,
viene ereditato da entrambe le sottoclassi: possiamo allora scrivere due diverse implementazioni del
metodo Abbaia( ), una per la classe Mastino, una per la classe Bassotto, come segue:
Mastino::Abbaia() { … riproduce l’abbaiare di un mastino…}
/* implementazione del metodo Abbaia() per la classe Mastino, nel file mastino.cpp /*
Bassotto::Abbaia(){ … riproduce l’abbaiare di un bassotto…}
/* implementazione del metodo Abbaia() per la classe Mastino, nel file mastino.cpp /*
Potremo allora, supponendo di avere un oggetto m di classe Mastino, e un oggetto b di classe
Bassotto, invocare i metodi m.abbaia( ) e b.abbaia( ) che si comportano in modo diverso, essendo
implementazioni di classi diverse, pur avendo lo stesso nome.
8. Struttura di un programma C++
Quando si programma in C++ con il DevCpp (ma anche con qualsiasi altro compilatore, come ad
esempio il VisualC++), occorre scegliere nuovo -> progetto dal menu file.
Automaticamente, viene aggiunto un file main.cpp al progetto: è quello che dà l’innesco all’intero
programma (come per la scomposizione Top-Down, ricordate? Il sottoprogramma main è quello da
cui inizia tutto!)
Nel main (ma non necessariamente sempre nel main, può accadere anche altrove), è dove vengono
creati e distrutti nuovi oggetti, e i vari oggetti interagiscono tra loro.
Al progetto bisogna poi aggiungere, per ogni entità del diagramma E/R:
-
un file di intestazione con estensione .h per la definizione della classe
-
un file di implementazione con estensione .cpp con il corpo dei metodi della classe
Il file di progetto ha estensione .dev ed è quello più importante perché contiene tutte le informazioni
di progetto che legano tra loro i diversi file.
5. Esercizi
-
-
-
-
-
rappresentare in un diagramma UML il modello a oggetti per descrivere la propria classe
(studenti, insegnanti, banchi, ecc).
rappresentare in un diagramma UML il modello a oggetti per la gestione di una videoteca
(clienti, prestiti, ecc.), quindi tradurre le classi in C++ e realizzare un main di prova per la
creazione e distruzione di alcuni oggetti.
rappresentare in un diagramma UML il modello a oggetti per la gestione di una pinacoteca
(quadri, artisti, visitatori, ecc), quindi tradurre le classi in C++ realizzare un main di prova
per la creazione e distruzione di alcuni oggetti.
realizzare un progetto C++ con le classi bicchiere e bottiglia, e scrivere un main nel quale si
crei un oggetto di tipo bottiglia, due oggetti di tipo bicchiere, e si riempiano i bicchieri
(facendo attenzione ad aggiornare opportunamente il livello del vino nella bottiglia).
estendere il progetto Punti definendo una sottoclasse Punto3D della classe Punto, che
rappresenti un punto in uno spazio tridimensionale (aggiungere un attributo per la coordinata
z).
completare il progetto Cani eliminando dalla classe Cane l’attributo razza, e definendo due
sottoclassi Mastino e Bassotto. Quindi, ridefinire il metodo abbaia() in modo polimorfo.
completare il progetto Data con l’introduzione di due sottoclassi DataBC e DataAC (Before
Christ e After Christ) derivate dalla classe Data, e della funzione polimorfa
NumGiorniDaZero che calcoli, il numero di giorni trascorsi dal 1/1/0000 nel primo caso, e il
numero di giorni che mancano al 1/1/000 nel secondo caso.