Chi può toccare i tuoi dati
Quando hai scritto la tua prima classe, probabilmente hai esposto ogni membro al mondo esterno. Funziona, ma butta via uno dei motivi principali per cui esistono le classi: l'incapsulamento, cioè nascondere lo stato interno di una classe in modo che il resto del programma possa interagire con essa solo attraverso un'interfaccia controllata. Gli specificatori di accesso sono lo strumento con cui tracci quel confine.
Sono esattamente tre: public, private e protected. Ognuno etichetta i membri che lo seguono, e l'etichetta decide quale codice può leggerli o scriverli. Se li usi bene, la tua classe fa rispettare le proprie regole; se li usi male, qualsiasi bug in qualsiasi punto può corrompere lo stato del tuo oggetto.
I tre specificatori
Uno specificatore è una parola chiave seguita dai due punti. Ogni membro dichiarato dopo di esso, fino allo specificatore successivo, ricade in quel livello di accesso.
Togli il commento dall'ultima riga e il compilatore si rifiuta di compilare: balance è private, quindi main non può metterci mano direttamente. È proprio questo lo scopo: l'unico modo per cambiare il saldo è passare da deposit, il che significa che in seguito puoi aggiungere controlli (niente depositi negativi, log, limiti) in un solo punto ed essere certo che vengano sempre applicati.
Ecco il quadro completo:
// accessibile da...
// public ovunque (qualsiasi codice che abbia l'oggetto)
// private solo i membri della classe stessa (+ friend)
// protected i membri della classe stessa E le classi derivate (+ friend)
class e struct: il default
Puoi scrivere quanti blocchi di specificatori vuoi, in qualsiasi ordine. L'accesso dei membri prima del primo specificatore dipende da se hai usato class o struct:
- In una
class, i membri sonoprivateper impostazione predefinita. - In una
struct, i membri sonopublicper impostazione predefinita.
Questo default è l'unica differenza a livello di linguaggio tra le due parole chiave. Una struct può avere metodi, costruttori e sezioni private esattamente come una class.
Per convenzione si usa struct per semplici raggruppamenti di dati pubblici e class quando vuoi comportamento e stato nascosto, ma il compilatore non lo impone: cambia solo il default.
Incapsulamento con getter e setter
Lo schema di tutti i giorni è questo: i dati vanno in una sezione private e un metodo public ne offre un accesso controllato. Un getter di sola lettura restituisce il valore; un setter lo valida prima di assegnarlo. È qui che private ripaga.
Dato che celsius è private, non c'è modo di infilarci un valore non valido: ogni scrittura deve passare da setCelsius, che protegge l'invariante. Nota che i getter sono marcati const: promettono di non modificare l'oggetto, quindi puoi chiamarli anche su oggetti const Temperature.
protected ed ereditarietà
protected conta solo quando entra in gioco l'ereditarietà. Per il codice esterno si comporta come private, ma una classe derivata può raggiungerlo. Usalo per i membri di cui una sottoclasse ha davvero bisogno ma che il resto del programma non dovrebbe comunque vedere.
Un errore comune da principianti è usare protected su ogni membro dati "nel caso una sottoclasse ne abbia bisogno". Così allarghi silenziosamente il contratto della classe: ora ogni sottoclasse può dipendere da quel campo e tu non puoi più cambiarlo liberamente. Preferisci private e passa a protected solo quando una classe derivata ha davvero bisogno di quell'accesso.
La via d'uscita friend
A volte una singola funzione o classe esterna ha legittimamente bisogno di vedere i tuoi dettagli interni: un caso classico è l'overloading dell'operatore <<, che non puoi rendere un membro. La parola chiave friend concede a quella specifica entità l'accesso ai tuoi membri private e protected, e a nessun altro.
friend è un'eccezione voluta e chirurgica: è la classe stessa a indicare esattamente di chi si fida, quindi nessuno può concedersi l'accesso dall'esterno. Usalo con parsimonia; se ti ritrovi ad aggiungere tanti friend, probabilmente quei membri non avrebbero dovuto essere private fin dall'inizio, oppure il tuo design va ripensato.
Errori comuni da evitare
- Rendere
publicogni membro. Sembra comodo, ma perdi tutti i controlli e le invarianti che l'incapsulamento ti garantisce. Parti da datiprivatecon metodipublic. - Dimenticare il default di
class.class Foo { int x; };rendexprivate, quindifoo.x = 5non compila. Se volevi un semplice raggruppamento di dati, usastructo aggiungi un'etichettapublic:. - Abusare di
protected. È un confine più debole diprivatee ha senso solo con l'ereditarietà. Usarlo ovunque lega le sottoclassi a campi che potresti voler cambiare. - Aspettarsi che
privatesia una misura di sicurezza. È una regola in fase di compilazione che impedisce accessi accidentali, non una cifratura. I byte restano in memoria;privateriguarda un design pulito, non la segretezza.
Prossimo: Struct
Hai visto che una struct è in realtà solo una class i cui membri sono public di default. La prossima pagina, sulle struct, approfondisce quando quel default pubblico è esattamente ciò che ti serve, cioè aggregati leggeri per raggruppare valori correlati, e come struct si usa nel C++ idiomatico accanto a classi complete.
Domande frequenti
Qual è la differenza tra public, private e protected in C++?
I membri public sono raggiungibili da qualsiasi punto. I membri private sono raggiungibili solo dall'interno della stessa classe (e dai suoi friend). protected è come private, ma permette di raggiungere il membro anche alle classi derivate. Per convenzione si tengono i dati private e si espone il comportamento tramite metodi public.
In C++ i membri sono private per impostazione predefinita?
In una class sì: tutto è private finché non scrivi uno specificatore di accesso. In una struct il default è public. Questo default è l'unica vera differenza tra class e struct in C++; entrambe possono avere metodi, costruttori e specificatori di accesso.
A cosa serve la parola chiave friend in C++?
friend concede a una funzione o a una classe specifica l'accesso ai tuoi membri private e protected. È un'eccezione voluta e circoscritta all'incapsulamento: la classe indica esattamente di chi si fida, quindi l'accesso non viene mai concesso in modo implicito.