- 3.1 La generalizzazione del concetto di tipo
- 3.2 Si può fare! La classe dei numeri complessi
- 3.2.1 La definizione della classe (il file
complesso.h
) - 3.2.2 un primo esempio di utilizzo
- 3.2.3 I membri di una classe
- 3.2.4 I metodi di una classe
- 3.2.5 Il campo
private
- 3.2.6 Il campo
public
- 3.2.7 L'implementazione della classe (il file
complesso.cc
) - 3.2.8 Un membro implicito di ogni classe: l'oggetto stesso
- 3.2.1 La definizione della classe (il file
- 3.3 Funzioni speciali di una classe
- 3.4 La ridefinizione di operatori, overloading
- 3.5 L'attributo
const
- 3.6 Classi e puntatori
- 3.7 ESERCIZI
- 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 comportamenti - in
C++
questo paradigma è realizzato attraveso il concetto di classe, che è una generalizzazione del tipo, mentre gli oggetti sono la generalizzazione delle variabili
- 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
- 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) ; }
- 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
- 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
)
- 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!
} ;
- in un qualunque punto del codice sorgente,
si può quindi creare un numero complesso:
complesso numero_complesso_1 (0., 0.) ; complesso numero_complesso_2 (3., 4.) ;
- in questo esempio
complesso
è la classe (il nuovo tipo), mentrenumero_complesso_1
enumero_complesso_2
sono due oggetti
- 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_
- 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)
- 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 classe - se non si indica nulla, tutto il contenuto di una classe è
private
- 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
- 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
::
- 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
- 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
- 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 proprio costruttore
- oggetti di altre classi vengono creati dal proprio costruttore
- 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
- tutti i membri di una classe vengono creati prima dell'inizio dello scope del costruttore
- la sequenza:
è detta lista di inizializzazione
m_real (r), m_imag (i)
- 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 ; }
- 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 ; }
- 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 definisce un costruttore di default vuoto
- è 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
- 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 automaticamente - la 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 proprio distruttore
- 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
- 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
- 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
- in questo caso, il
- 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
- 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) ;
- la funzione
operator+
può essere definita anche al di fuori della classe- in 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 ; }
- bisogna definire metodi pubblici di interfaccia per accedere al valore dei membri
- 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 si scope
complesso::
- essendo una funzione esterna alla classe,
in questo caso non è presente la denominazione si scope
- la parola chiave
const
indica il fatto che non sia permesso cambiare il valore contenuto in una variabile o in un oggetto - const 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
sintassi | effetto |
---|---|
const int C1 = 10 ; |
C1 è un intero il cui valore è costante |
int const C1 = 10 ; |
C1 è un intero il cui valore è costante |
const int * C2 ; |
C2 è un puntatore ad un const int , cioè un puntatore ad un intero costante |
int const * C2 ; |
C2 è un puntatore ad un const int , cioè un puntatore ad un intero costante |
int * const C3 ; |
C3 è un puntatore costante ad un intero variabile |
int const * const C4 ; |
C4 è un puntatore costante ad un intero costante |
- 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) ;
- E' possibile rimuovere l’attributo
const
mediante ilconst_cast
:const int myConst = 5; int* nonConst = const_cast<int*>(&myConst);
nonConst
punta alla stessa cella di memoriamyConst
, ma ne può modificare il contenuto
- 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
- 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
- Gli esercizi relativi alla lezione si trovano qui