Lezione 2: programmazione ad oggetti, le classi
Contents
Lezione 2: programmazione ad oggetti, le classi¶
2.1 La generalizzazione del concetto di tipo¶
secondo la programmazione object oriented, le funzionalità di un programma vanno associate all’informazione che processano,
così come per ogni tipo predefinito (
int
,float
, et cetera) esistono gli operatori che ne gestiscono i comportamentiin
C++
questo paradigma è realizzato attraveso il concetto di classe, che è una generalizzazione del tipo, mentre gli oggetti sono la generalizzazione delle variabili
2.1.1 Uno sguardo ravvicinato ai tipi predefiniti in C++
¶
un qualunque tipo predefinito è caratterizzato da una serie di proprietà:
funzioni per la gestione della memoria:
allocazione dello spazio nella RAM quando una variabile viene definita
liberazione dello spazio RAM quando una variabile cessa di esistere
operatori per maneggiare le variabili
2.1.2 Un esempio: i numeri complessi¶
costrutti più sofisticati dei tipi predefiniti non godono di queste proprietà
un numero complesso è rappresentato da due numeri reali, che un
C++
si possono scrivere come:double num_parteReale ; double num_parteImmaginaria ;
senza fare uso di classi, le operazioni tipiche dei numeri complessi vanno implementate sotto forma di funzioni:
calcolo del modulo e della fase
somma di numeri complessi
moltiplicazione per un numero reale
ad esempio:
double modulo (double real, double imag) { return sqrt(real * real + imag * imag) ; }
2.1.3 Se i numeri complessi fossero un tipo di C++
¶
le operazioni per gestire i numeri complessi sono praticamente associate soltanto a loro
risulterebbe molto più comodo se fosse possibile definire un numero complesso e associare ad esso le operazioni che lo riguardano:
migliore gestione del programma
proprietà simili a quelle dei tipi predefiniti
mogliore solidità del design del codice sorgente, perché migliora la consistenza del codice e le possibilità di controllo di errori logici
2.2 Si può fare! La classe dei numeri complessi¶
una classe è di fatto la definizione di un nuovo tipo: il caso ideale per la costruzione di una libreria, con un file header (
.h
) ed uno di implementazione (.cc
)
2.2.1 La definizione della classe (il file complesso.h
)¶
ecco come si definisce in
C++
class complesso { public: complesso (double r, double i) ; ~complesso () ; double modulo () ; double fase () ; private: double m_real ; double m_imag ; } ;
attenzione |
---|
dopo la chiusura della parentesi graffa c’è un punto e virgola!
} ;
2.2.2 un primo esempio di utilizzo¶
in un qualunque punto del codice sorgente, si può quindi creare un numero complesso:
complesso numero_complesso_1 (0., 0.) ; complesso numero_complesso_2 (2., 4.) ;
in questo esempio
complesso
è la classe (il nuovo tipo), mentrenumero_complesso_1
enumero_complesso_2
sono due oggetti
2.2.3 I membri di una classe¶
le variabili definite all’interno della definizione della classe sono dette membri della classe:
double m_real ; double m_imag ;
ogni volta che viene creato un oggetto di una classe, viene creata una nuova istanza dei membri della classe associata a quell’oggetto, quindi ogni oggetto ha le proprie variabili membro corrispondenti
i membri possono essere di tipo predefinito, oppure a loro volta oggetti di una classe
è buona regola di programmazione identificare i membri in modo simbolico, ad esempio con il prefisso
m_
2.2.4 I metodi di una classe¶
le funzioni che sono definite all’interno di una classe sono chiamate metodi della classe
hanno automaticamente accesso ai membri dell’oggetto sul quale operano e si invocano su un oggetto utilizzando il nome dell’oggetto seguito da un punto e dal nome del metodo:
numero_complesso_1.modulo ()
i metodi possono avere argomenti, ad esempio uno di essi potrebbe moltiplicare il numero complesso per un numero reale:
numero_complesso_1.dilata (double fattore_di_scala)
2.2.5 Il campo private
¶
i metodi di una classe fungono da interfaccia fra i membri di un oggetto ed il codice sorgente dove l’oggetto è definito
è talvolta auspicabile che i membri possano essere modificati soltanto attraverso i metodi, per evitare che subiscano operazioni che compromettano la funzionalità dell’oggetto nel suo insieme
tutti i metodi ed i membri definiti dopo la parola chiave
private
sono accessibili solo per i metodi della loro classese non si indica nulla, tutto il contenuto di una classe è
private
2.2.6 Il campo public
¶
i metodi ed i membri definiti dopo la parola chiave
public
sono accessibili nel codice sorgente al di fuori della classe (ad esempio nella funzionemain
) tramite la sintassi del.
:numero_complesso_1.modulo ()
solitamente, i membri di una classe sono
private
, mentre i suoi metodi sonopublic
se si definisce una classe con l’identificativo
struct
invece diclass
, se non si indica nulla tutto il contenuto della classe èpublic
2.2.7 L’implementazione della classe (il file complesso.cc
)¶
i metodi di una classe possono essere implementati direttamente nello scope di definizione
solitamente, tuttavia, questo succede in un file separato, dove vanno associati alla classe che li contiene, ad esempio:
#include "complesso.h" double complesso::modulo () { return sqrt (m_real * m_real + m_imag * m_imag) ; }
il nome di ogni metodo è preceduto dal nome della classe, separato dall’operatore di scope resolution
::
2.2.8 Un membro implicito di ogni classe: l’oggetto stesso¶
per ogni classe, è sempre definito il puntatore all’oggetto corrente, rappresentato dal simbolo
this
void complesso::stampami () { std::cout << this->m_real << " + " << this->m_imag << "i" << std::endl ; return ; }
il
.
che si usa per accedere a membri e metodi di un oggetto viene sostituito da->
per i puntatori ad oggetti
2.3 Funzioni speciali di una classe¶
oltre a quelle che servono per maneggiare le variabili, ogni tipo predefinito possiede funzioni dedicate alla creazione ed alla distruzione delle variabili
in una classe, queste funzioni vanno implementate
2.2.1 Il costruttore¶
crea l’oggetto al momento della sua definizione, inizializzando i membri dell’oggetto:
complesso::complesso (double r, double i): m_real (r), m_imag (i) { std::cout << "costruzione di un numero complesso" << std::endl ; }
le variabili di tipi predefiniti vengono create dal costruttore corrispondente
oggetti di altre classi vengono creati dal costruttore corrispondente
il costruttore non ha tipo di ritorno
nello scope del costruttore si possono eseguire istruzioni (in questo esempio c’è una stampa a schermo, che in realtà è scomodo: nessuno vuole un programma troppo petulante)
questo è un buon posto dove allocare dinamicamente la memoria, se necessario
2.2.2 La lista di inizializzazione¶
tutti i membri di una classe vengono creati prima dell’inizio dello scope del costruttore
la sequenza:
m_real (r), m_imag (i)
è detta lista di inizializzazione
ottimizza l’uso della memoria: inizializza ciascun membro al valore fra parentesi al momento della creazione del membro
l’ordine delle variabili deve essere il medesimo della loro definizione all’interno della classe
se non si mettesse la lista di inizializzazione, bisognerebbe inizializzare le variabili nello scope del costruttore, spendendo più tempo di esecuzione:
complesso::complesso (double r, double i): { m_real = r ; m_imag = i ; std::cout << "costruzione di un numero complesso" << std::endl ; }
2.2.3 overloading del costruttore¶
una classe può possedere più di un costruttore, a patto che ciascuno prenda argomenti diversi
ad esempio, si può definire un costruttore che abbia come input soltanto un numero reale:
complesso::complesso (double r): m_real (r), m_imag (0.) { std::cout << "costruzione di un numero complesso" << std::endl ; }
2.2.4 Il costruttore di default¶
un costruttore senza argomenti di input è chiamato costruttore di default:
complesso::complesso (): m_real (0.), m_imag (0.) { std::cout << "costruzione di un numero complesso" << std::endl ; }
se una classe non ha il costruttore, il compilaore spesso definisce un costruttore di default vuoto
2.2.5 Il costruttore di copia, o copy constructor¶
è naturale immaginare di costruire un oggetto nuovo copiando il contenuto di uno esistente:
complesso::complesso (const complesso & orig): m_real (orig.m_real), m_imag (orig.m_imag) {}
una classe ha sempre accesso a tutti i membri di tutti gli oggetti di quella classe se vengono passati come argomenti di una funzione, quindi il copy constructor ha accesso ai membri
private
dell’oggettoorig
esiste una eccezione a questa regola, che vedremo quando parleremo di ereditarietà
l’oggetto
orig
viene passato:per referenza per ragioni di velocità
con l’attributo const per garantire che non venga modificato
anche in questo caso, non c’è tipo di ritorno
2.2.6 Il distruttore¶
al termine della vita di un oggetto, cioè al momento in cui va out of scope, la memoria che occupa va liberata
i suoi membri di tipi predefiniti del
C++
allocati automaticamente vengono distrutti automaticamentela memoria allocata dinamicamente va ripulita esplicitamente: per fare questo, esiste una funzione dedicata, chiamata distruttore, dove tutti i
delete
necessari possono essere chiamaticomplesso::~complesso () { // qui va ripulita la memoria allocata dinamicamente }
eventuali membri che siano oggetti di altre classi saranno distrutti dal distruttore corrispondente
nel distruttore si possono anche implementare comportamenti aggiuntivi, come ad esempio il salvataggio automatico dell’informazione
anche il distruttore non ha tipo di ritorno
se non viene implementato, il compilatore crea automaticamnete un distruttore vuoto
2.4 La ridefinizione di operatori, overloading¶
per i tipi predefiniti di
C++
le operazioni matematiche fondamentali sono effettuate con i simboli algebrici noti:+
,-
,*
,/
,=
….si può definire il comportamento di queste funzioni anche per gli oggetti delle classi (come sempre, si distinguono dagli altri per i diversi tipi in ingresso)
ecco due esempi notevoli
2.4.1 L’operatore di assegnazione per tipi predefiniti¶
una operazione solitamente fattibile con tipi predefiniti è l’assegnazione a partire da una altra variabile esistente:
int numero = 5 ;
in questo caso, il
C++
prima costruisce la variabilenumero
e le assegna un valore in memoria, successivamente le fa assumere il valore di5
2.4.2 L’operatore di assegnazione per una classe¶
il comportamento dell’operatore di assegnazione va definito per una classe
complesso & complesso::operator= (const complesso & orig) { m_real = orig.m_real ; m_imag = orig.m_imag ; return *this ; }
la variabile in ingresso è una referenza costante per garantire velocità e non modificabilità
la variabile in uscita è una referenza all’oggetto, per permettere la seguente sintassi:
complesso numero_complesso_6 = numero_complesso_5 = numero_complesso_2 ;
non viene restituita una copia dell’oggetto corrente per risparmiare tempo
2.4.3 L’operatore di somma¶
vogliamo che l’operazione di somma fra numeri complessi si possa scrivere come:
complesso numero_complesso_4 = numero_complesso_3 + numero_complesso_2 ;
in
C++
si può ottenere defintendo un metodo della classe complesso chiamatooperator+
:complesso complesso::operator+ (const complesso & addendo) { complesso somma (m_real, m_imag) ; somma.m_real = somma.m_real + addendo.m_real ; somma.m_imag = somma.m_imag + addendo.m_imag ; return somma ; }
la variabile in ingresso è una referenza costante per garantire velocità e non modificabilità
la variabile in uscita è un oggetto nuovo
l’
operator+
ha in questo caso un solo argomento, perché uno dei due addendi è l’oggetto sul quale è chiamato. Infatti, le due scritture seguenti sono equivalenti:complesso numero_complesso_4 = numero_complesso_3 + numero_complesso_2 ; complesso numero_complesso_4 = numero_complesso_3.operator+ (numero_complesso_2) ;
2.4.4 Definizione al di fuori della classe¶
la funzione
operator+
può essere definita anche al di fuori della classein questo caso ha due argomenti, che sono entrambi gli addendi
in questo caso, tuttavia, nella funzione i membri privati degli oggetti non sono accessibili
bisogna definire metodi pubblici di interfaccia per accedere al valore dei membri
double complesso::parte_reale () const { return m_real ; }
può essere comodo per definire operazioni fra oggetti eterogenei
complesso operator+ (const complesso & uno, const double & due) { double real = uno.parte_reale () + due ; double imag = uno.parte_immaginaria () ; complesso somma (real, imag) ; return somma ; }
essendo una funzione esterna alla classe, in questo caso non è presente la denominazione di scope
complesso::
L’esempio completo di definizione della classe è qui.
2.5 L’attributo const
¶
la parola chiave
const
indica il fatto che non sia permesso cambiare il valore contenuto in una variabile o in un oggettoconst si applica al primo attributo alla sua sinistra, se non c’è nulla si applica al primo attributo alla sua destra
a seconda della sua posizione, ha effetti differenti
2.5.1 Esempi di utilizzo di const
con i tipi predefiniti¶
sintassi |
effetto |
---|---|
|
|
|
|
|
|
|
|
|
|
|
|
NOTA BENE: siccome
C3
eC4
sono puntatori costanti, vanno immediatamente inizializzati, perché i puntatori non sono inizializzati ad alcun valore di default:int * const C3 (& numero) ; int const * const C4 (& C1) ;
Si veda l’esempio 2.3
2.5.2 Oggetti definiti const
¶
le stesse regole si applicano ad oggetti definiti
tuttavia, si pone il problema aggiuntivo che, in generale, i metodi di una classe possono modificare i membri di un oggetto
per continuare ad utilizzare metodi preservando la caratteristica
const
ilC++
richiede di indicare quali metodi non modifichino i membri di una classe, aggiungendo l’attributoconst
al termine del loro prototpo:double complesso::parte_reale () const { return m_real ; }
su un oggetto di tipo
const
possono essere invocati soltanto i metodi dichiaraticonst
2.6 Classi e puntatori¶
come abbiamo già visto, esistono puntatori e referenze ad oggetti, con i medesimi comportamenti delle variaibli di tipo predefinito
per accedere a metodi e membri di un oggetto attraveso un suo puntatore, si utilizza l’operatore
->
invece di.
le classe possono anche contenere puntatori a variabili di tipo predefinito o ad altri oggetti
nel caso in cui si utilizzi allocazione dinamica della memoria, è prudente invocarla nel costruttore ed è necessario ripulire la memoria nel distruttore