Skip to content

Latest commit

 

History

History
572 lines (471 loc) · 21.2 KB

README.md

File metadata and controls

572 lines (471 loc) · 21.2 KB

Lezione 3: programmazione ad oggetti, le classi

Indice

linea

3.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 comportamenti
  • in C++ questo paradigma è realizzato attraveso il concetto di classe, che è una generalizzazione del tipo, mentre gli oggetti sono la generalizzazione delle variabili

linea

3.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

linea

3.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) ;
      }

linea

3.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

linea

3.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)

linea

3.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! } ;

linea

3.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 (3., 4.) ;
  • in questo esempio complesso è la classe (il nuovo tipo), mentre numero_complesso_1 e numero_complesso_2 sono due oggetti

linea

3.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_

linea

3.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)

linea

3.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 classe
  • se non si indica nulla, tutto il contenuto di una classe è private

linea

3.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 funzione main) tramite la sintassi del .:
    numero_complesso_1.modulo ()
  • solitamente, i membri di una classe sono private, mentre i suoi metodi sono public
  • se si definisce una classe con l'identificativo struct invece di class, se non si indica nulla tutto il contenuto della classe è public

linea

3.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 ::

linea

3.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

linea

3.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

linea

3.3.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 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

linea

3.3.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 ;
      }

linea

3.3.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 ;
      }

linea

3.3.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 definisce un costruttore di default vuoto

linea

3.3.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'oggetto orig
    • 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

linea

3.3.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 automaticamente
  • la memoria allocata dinamicamente va ripulita esplicitamente: per fare questo, esiste una funzione dedicata, chiamata distruttore, dove tutti i delete necessari possono essere chiamati
    complesso::~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

linea

3.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

linea

3.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 variabile numero e le assegna un valore in memoria, successivamente le fa assumere il valore di 5

linea

3.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

linea

3.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 chiamato operator+:
    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) ;

linea

3.4.4 Definizione al di fuori della classe

  • 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 ;
      }
  • 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::

linea

3.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 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

linea

3.5.1 Esempi di utilizzo di const con i tipi predefiniti

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 e C4 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 il const_cast:
    const int myConst = 5;
    int* nonConst = const_cast<int*>(&myConst);
    • nonConst punta alla stessa cella di memoria myConst, ma ne può modificare il contenuto

linea

3.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 il C++ richiede di indicare quali metodi non modifichino i membri di una classe, aggiungendo l'attributo const 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 dichiarati const

linea

3.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

linea

3.7 ESERCIZI

  • Gli esercizi relativi alla lezione si trovano qui