Programmazione Scientifica++: Lezione 7

Incapsulamento, Getter/Setter e Costruttori

Docente

Argomenti di Oggi

  • Anatomia di una Classe: Dati e Comportamenti
  • Il Principio del Minor Privilegio
    • Regole dell’Incapsulamento (public vs private)
    • Getters e Setters
  • I Costruttori
    • Default, Parametrizzati e di Copia
    • Cosa il compilatore vi regala, e quando smette di farlo
    • Costruttori privati: il Singleton

Header, compilazione e linking sono la Lezione 8, insieme a stack e heap.

Ripasso: Le Classi in C++

Una classe è il modello (il progetto) per definire un “oggetto”. Contiene: 1. Dati (noti come attributi o data members) 2. Funzioni (note come metodi, member functions o interfaccia)

#include <iostream>

class Neuron {
public: // Accessibile da chiunque!
    double weight; 
    double bias; 
};

int main() {
    Neuron n1;
1    n1.weight = 1.12;
    return 0;
}
1
Modifica diretta di un attributo. Sembra comodo, ma è una PESSIMA IDEA!

Il Principio del Minor Privilegio

Gli utenti della tua classe devono fare affidamento esclusivamente sull’interfaccia pubblica (i metodi), senza mai manipolare o dipendere dai dettagli interni. Questo concetto è il cuore dell’Incapsulamento.

  • private: I dati (le variabili) dovrebbero quasi sempre essere privati. Nessuno dall’esterno può leggerli o scriverli.
  • public: I metodi che definiscono cosa un oggetto “sa fare” devono essere pubblici.

Regola d’oro

Se permetti agli utenti di accedere direttamente ai dati, quando vorrai aggiornare o modificare il funzionamento interno della classe, “romperai” il codice di tutti i tuoi utenti!

Il Problema dei Dati Pubblici (Codice Rotto)

Ecco cosa succede se permetti accesso pubblico. Tu, autore della classe, decidi di migliorare il tuo codice e cambi il nome di una variabile da weight a peso_sinaptico.

La tua Classe Aggiornata:

class Neuron {
public:
    // Rinominato per chiarezza!
    double peso_sinaptico; 
};

Il Codice del Cliente:

int main() {
    Neuron n1;
1    // ERRORE DI COMPILAZIONE!
    n1.weight = 1.12; 
    return 0;
}
1
Il codice del cliente improvvisamente non compila più. Il cliente ti odierà.

La Soluzione: Getters e Setters

Usa metodi pubblici (Helper/Utility) per accedere ai dati privati.

La tua Classe:

class Neuron {
private: 
1    double weight_;

public: 
2    double getWeight() {
        return weight_; 
    } 
    
3    void setWeight(double w) {
        weight_ = w; 
    } 
};
1
Variabile privata. Convenzione C++: suffisso _ per distinguerla.
2
Getter: Legge e restituisce il valore.
3
Setter: Scrive e aggiorna il valore.

Il Codice del Cliente:

int main() {
    Neuron n1;
    
    n1.setWeight(1.12);
    
    std::cout << n1.getWeight();
    
    return 0;
}

Se domani cambi weight_ in peso_sinaptico_, dovrai solo aggiornare i metodi all’interno della classe. Il codice del cliente setWeight() continuerà a funzionare per magia!

Sicurezza con i Setters

I Setters non servono solo a mascherare i nomi. Permettono di convalidare i dati!

class Persona {
private:
    int eta_;

public:
    void setEta(int nuova_eta) {
1        if (nuova_eta >= 0 && nuova_eta < 150) {
            eta_ = nuova_eta;
        } else {
2            std::cerr << "Errore: età non valida!\n";
        }
    }
};
1
Controllo di validità. Blocca i valori insensati!
2
Gestione dell’errore (lo vedremo meglio con le Eccezioni).

I Costruttori

In C++, nel momento in cui crei un oggetto, il compilatore invoca istantaneamente una funzione speciale chiamata Costruttore. - Deve avere lo stesso identico nome della classe. - Non ha nessun tipo di ritorno (nemmeno void!). - Serve a inizializzare i dati in uno stato valido, evitando “spazzatura” di memoria.

class Datum {
private:
    double value_;
    
public:
1    Datum() {
        value_ = 0.0;
    }
};
1
Ecco il Costruttore! Quando scriviamo Datum d1; nel main(), questa funzione parte da sola e imposta value_ a 0.

Tipi di Costruttori: Default e Parametrizzati

Proprio come le normali funzioni, i costruttori possono accettare parametri (Overloading).

class Datum {
private:
    double value_;

public:
    // Costruttore di Default (nessun argomento)
1    Datum() {
        value_ = 0.0; 
    }

    // Costruttore Parametrizzato
2    Datum(double v) {
        value_ = v;
    }
};
1
Chiamato quando facciamo Datum d1;.
2
Chiamato quando facciamo Datum d2(5.5);.

Costruttore di Copia (Copy Constructor)

Cosa succede se creiamo un oggetto assegnandogliene subito un altro uguale?

int main() {
    Datum d1(5.5);
    
1    Datum d2 = d1;
    
    return 0;
}
1
Stiamo chiamando il Costruttore di Copia. Se non ne scriviamo uno noi, il compilatore del C++ lo genera in automatico per noi, copiando uno per uno tutti gli attributi private da d1 a d2.

(Attenzione: questa “copia gratuita” diventerà pericolosissima quando useremo la memoria dinamica! Ne parleremo nella prossima lezione).

E se non scrivo nessun costruttore?

Se non scrivete nessun costruttore, il compilatore ve ne regala uno di default. Sui tipi base (double, int, i puntatori) non fa nulla: i dati restano non inizializzati, con valori spazzatura.

MA ATTENZIONE:

class Datum {
public:
    Datum(double v) { value_ = v; } // Ho scritto SOLO il parametrizzato!
private:
    double value_;
};

int main() {
1    // ERRORE DI COMPILAZIONE!
    Datum d1; 
    return 0;
}
1
Se scrivi un costruttore personalizzato, il C++ si rifiuta di regalarti quello di default. Se provi a creare l’oggetto senza argomenti, il programma non compila. Devi scrivere esplicitamente Datum() { ... }.

Costruttori Privati?

Possiamo mettere un costruttore in private? Certo, ed è il modo per imporre che un oggetto esista una volta sola — un generatore di numeri casuali, per dire: due generatori con lo stesso seme danno la stessa sequenza.

class Generatore {
private:
1    Generatore() { }

public:
2    static Generatore& get() {
        static Generatore unico;
        return unico;
    }
};

int main() {
    // Generatore g; // ERRORE! Il costruttore è privato.
3    Generatore& g = Generatore::get();
    return 0;
}
1
Nessuno da fuori può costruirlo con Generatore g;.
2
L’unico modo per ottenerlo. Qui static vuol dire due cose diverse — metodo che non appartiene a un oggetto, variabile che nasce una volta sola — e sono materia della Lezione 13: per ora è una ricetta.
3
Design Pattern Singleton: una sola istanza, garantita dal linguaggio.

Note

Il randnumgen.hpp che vi daremo fa la stessa cosa più alla svelta, con un unico rng globale. Il Singleton serve quando volete anche impedire il secondo.

Esercizi lampo: cosa stampa?

Due minuti, senza portatile. Compilate mentalmente con -std=c++17.

1. Quante righe escono, e quali?

#include <iostream>

class D {
public:
    D()          { std::cout << "default\n"; }
    D(double)    { std::cout << "param\n"; }
    D(const D&)  { std::cout << "copia\n"; }
};

int main() {
    D a;
    D b(1.0);
    D c = b;
    D d = D(2.0);
}

2. La classe Persona della slide sui setter non ha costruttori.

Persona p;
p.setEta(200);          // l'età non passa il controllo
std::cout << p.getEta();

Esercizi lampo: le risposte

1. Quattro righe: default, param, copia, param. L’ultima riga è la sorpresa: D d = D(2.0); sembra costruire un temporaneo e poi copiarlo, e fino al C++14 faceva esattamente questo — cinque righe. Dal C++17 l’eliminazione della copia in questo caso non è un’ottimizzazione concessa al compilatore, è obbligatoria: il temporaneo non esiste proprio, d viene costruito sul posto. Provate a rimettere -std=c++11 -fno-elide-constructors e la quinta riga ricompare.

2. Non si sa. setEta(200) rifiuta il valore — il setter fa il suo mestiere — ma eta_ non è mai stato inizializzato da nessuno, quindi getEta() legge spazzatura: è undefined behaviour, può stampare qualunque cosa. Ed è esattamente il motivo per cui la slide dopo parla di costruttori: validare gli ingressi non serve a niente se l’oggetto nasce già in uno stato invalido. Incapsulamento e costruttori sono due metà dello stesso problema.