C++ - classi, membri e invarianti
In questa pagina 7
Come progettare un tipo nuovo, seguendo l'esempio della classe Date (data del calendario) attraverso versioni successive: ogni iterazione la rende più utile e più sicura, come nello sviluppo vero del software, dove a ogni passo si prova il codice. Le classi sono la base di tutto il resto: costruttori e copia sono in C++ - costruttori, inizializzazione, copia e spostamentouna buona interfaccia è completa e minimale, ha i costruttori giusti, supporta o proibisce la copia, usa tipi adeguati, dichiara const ciò che non modifica e libera le risorse nel distruttore; il costruttore di default (senza argomenti) esiste quando c'è un valore iniziale sensato, si ottiene con valori di default nei membri (in-class initializer) o argomenti di default, e explicit vieta la conversione implicita da un argomento; initializer_liststruct del C (Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C →) o le classi di Python (Classi ed ereditarietà in PythonAttributi di istanza e di classe, metodi di istanza, di classe e statici; confronto e ordinamento con eq e lt; proprietà; ereditarietà, super(), override e polimorfismo; classi astratte come interfacce; eccezioni personalizzate; overloading e shadowing in Python.Classi ed ereditarietà in Python →) ritrova l'idea; l'astrazione sui dati è in Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →.
Tipi definiti dall'utente
C++ ha un forte focus sui tipi. Quelli built-in sono int, char, double, ...; i tipi definiti dall'utente (UDT, user defined types) sono le classi della libreria standard (string, vector, ostream) e quelle scritte dal programmatore. vector, string, list sono costruiti con le stesse tecniche delle classi del programmatore: il C++ è fatto in modo che chi scrive la libreria standard non abbia "superpoteri".
"Types are good for directly representing ideas in code". Un tipo nuovo ha due elementi chiave:
| elemento | significato |
|---|---|
| rappresentazione | come il tipo rappresenta i dati dentro un oggetto: il valore corrente o stato |
| operazioni | che cosa si può fare con un oggetto, cioè con i dati che lo rappresentano |
Anche una macchinetta del caffè o un motore hanno uno stato (livello di acqua, temperatura; giri, carburante o carica). Se si pensa a qualcosa come a un'entità separata, probabilmente si può mappare in una classe. Una classe specifica come sono rappresentati i suoi oggetti, come si creano, come si usano e come si distruggono. (Il termine "oggetto" è un'istanza di una classe.)
Classi, membri, interfaccia
Una classe è composta da tipi built-in, altri UDT e funzioni. Le entità usate per definirla sono i membri: dati membro (data member) e funzioni membro (function member).
class X
{
public:
int m;
int mf(int v) { int old = m; m = v; return old; }
};
X var;
var.m = 7; // accesso a un dato membro: oggetto.membro
int x = var.mf(9); // chiamata di una funzione membro; dentro la classe non serve oggetto.Il principio guida è interfaccia contro implementazione:
| parte | punto di vista | accesso |
|---|---|---|
interfaccia (public) |
di chi usa la classe | accessibile a tutti |
implementazione (private) |
di chi scrive la classe | non accessibile; lettura e scrittura solo tramite l'interfaccia |
Questo permette di controllare lo stato (la coerenza) dei dati. Per default i membri di una class sono privati. Una struttura classica:
class X
{
public:
// interfaccia utente: funzioni, tipi, (più raramente) dati
private:
// dettagli implementativi: funzioni, tipi, dati
};Definizione. Una struct è una variante della
classcon un'unica differenza tecnica: i membri sono pubblici di default. Si usa quando ci sono dati ma non funzioni (retaggio del C).
Gli strumenti che C++ dà per progettare una classe sono: funzioni membro, costruttori, protezione dei dati, overloading degli operatori, gestione della copia, helper function: li incontriamo in questo ordine.
La classe Date, versione per versione
Versione 1: una struct pubblica
struct Date { int y; int m; int d; }; // anno, mese, giorno
Date today;
today.y = 2005; today.m = 12; today.d = 24;Si può fare tutto, anche l'assurdo: x.y = -3; x.m = 13; x.d = 32; compila ed esegue; y.y = 2000; y.m = 2; y.d = 29; è corretto (il 2000 è bisestile) ma il compilatore non lo sa e non lo controlla. L'uso è lento e poco sicuro. Per le operazioni più comuni si scrivono helper function, "programmate e debuggate una sola volta", come init_day(Date&, int y, int m, int d) (controlla e inizializza) e add_day(Date&, int n).
#include <iostream>
using namespace std;
struct Date { // versione 1: tutto pubblico
int y; // anno
int m; // mese
int d; // giorno
};
// helper function: operazioni comuni scritte e provate una volta sola
bool init_day(Date& dd, int y, int m, int d)
{
if (m < 1 || m > 12 || d < 1 || d > 31) return false; // controllo (semplificato)
dd.y = y; dd.m = m; dd.d = d;
return true;
}
int main()
{
Date today; // non inizializzata: valori casuali!
init_day(today, 2008, 3, 30); // la helper function c'e'...
cout << today.y << '-' << today.m << '-' << today.d << '\n';
Date tomorrow;
tomorrow.y = today.y;
tomorrow.m = today.m;
tomorrow.d = today.d + 1; // ...ma si puo' costruire a mano, e bene
cout << tomorrow.y << '-' << tomorrow.m << '-' << tomorrow.d << '\n';
Date x; // niente impedisce date assurde
x.y = -3; x.m = 13; x.d = 32;
cout << x.y << '-' << x.m << '-' << x.d << '\n';
return 0;
}2008-3-30
2008-3-31
-3-13-32Il problema: le helper function ci sono ma si possono bypassare. today può restare non inizializzato e tomorrow si costruisce a mano (tomorrow.d = today.d + 1 può dare 31 marzo, ma il 31 gennaio sarebbe un 32 gennaio): non c'è modo di rendere obbligatorio l'uso delle funzioni. Serve rendere i dati inaccessibili e le funzioni obbligatorie.
Versione 2 e 3: funzioni membro, costruttore, dati privati
Le funzioni membro sono parte integrante della classe (scope di classe): Date(int y, int m, int d); e void add_day(int n);. I dati si rendono private; l'accesso diventa possibile solo attraverso le funzioni pubbliche: l'interfaccia.
#include <iostream>
using namespace std;
class Date { // versione 3: dati privati, costruttore, accessi
public:
Date(int yy, int mm, int dd); // il costruttore e' l'UNICO modo di creare una Date
void add_day(int n);
int year() const { return y; }
int month() const { return m; }
int day() const { return d; }
private:
int y, m, d;
};
Date::Date(int yy, int mm, int dd) // lista di inizializzazione dopo i due punti
: y{yy}, m{mm}, d{dd}
{
}
void Date::add_day(int n)
{
d += n; // (versione semplificata: ignora la fine del mese)
}
int main()
{
Date birthday {1970, 12, 30}; // ok: costruttore
// Date my_birthday; // errore: nessun costruttore senza argomenti
// birthday.m = 14; // errore: m e' privato
birthday.add_day(1);
cout << birthday.year() << '-' << birthday.month() << '-' << birthday.day() << '\n';
return 0;
}1970-12-31Date birthday {1970, 12, 30};invoca il costruttore.Date my_birthday;non compila: non c'è un costruttore senza argomenti, quindi non si può creare unaDatenon inizializzata (con solo un costruttore con argomenti, ometterli è un errore di compilazione).birthday.m = 14;non compila:mè privato. Si legge conmonth(),day(),year().- Il costruttore usa una lista di inizializzazione (member initializer list) dopo i due punti:
: y{yy}, m{mm}, d{dd}.
Il costruttore è una funzione membro con lo stesso nome della classe, usata per inizializzare (costruire) gli oggetti; il suo compito è dare ai dati uno stato di partenza valido: valori di default validi oppure, se riceve argomenti, un controllo di validità.
Lista di inizializzazione contro assegnamenti. L'alternativa è
Date::Date(int yy, int mm, int dd) { y = yy; m = mm; d = dd; }ma richiede due passi (inizializzazione di default e poi assegnamento), mentre la lista inizializza direttamente con il valore giusto, evita il doppio passaggio ed esprime meglio l'intento. Ci sono tipi che devono essere inizializzati nella lista (membri const e reference).
Come si può chiamare un costruttore:
Date last {2000, 12, 31}; // ok, preferibile
Date next = {2014, 2, 14}; // ok, un po' verboso
Date christmas = Date{1976, 12, 24}; // ok, un po' verboso
Date last2(2000, 12, 31); // ok: vecchio stile
Date my_birthday; // errore: non inizializzato
last.add_day(1); // ok
add_day(2); // errore: quale data?Stato e invarianti
Le funzioni membro garantiscono che i dati siano sempre coerenti: suppongono che lo siano in ingresso e garantiscono che lo siano in uscita; cioè l'oggetto è sempre in uno stato valido. L'alternativa (controllare a ogni uso o sperare che l'utente non metta dati sbagliati) non funziona: "Experience shows that 'hoping' can lead to 'pretty good' programs".
Definizione (invariante). Un invariante è una regola che definisce i valori validi dello stato di un oggetto, e che il costruttore stabilisce e ogni funzione membro conserva.
Per Date gli invarianti non sono ovvi: mese tra 1 e 12, giorno tra 1 e il numero di giorni di quel mese, anni bisestili, calendario gregoriano, fusi orari. Per Rational (Esercizio - Numeri razionali con classe e overloading (laboratorio 2)): denominatore diverso da zero e frazione ridotta ai minimi termini. Una classe ben progettata non entra mai in uno stato non valido; il primo passo è saper rilevare l'errore.
Versione finale: rilevare gli errori
is_valid() è una funzione separata dal costruttore (e privata) perché è un'attività diversa dall'inizializzazione e può servire a tutti i costruttori. L'esito si segnala in uno di questi modi: codici di ritorno, scrittura su un membro, eccezione. La classe Invalid è un tipo vuoto, definito dentro Date, che esiste solo per avere un tipo che segnali questo preciso errore.
Eccezione. Meccanismo che separa il rilevamento di un errore (spesso in una funzione chiamata) dalla sua gestione (spesso nel chiamante). Una funzione che non può gestire l'errore non ritorna normalmente: lancia (
throw) un oggetto; un chiamante, diretto o indiretto, lo cattura (catch) con un bloccotry. Se nessuno lo gestisce, il programma termina. È l'unico modo in cui un costruttore può fallire. Dettagli in C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →.
#include <iostream>
using namespace std;
namespace Chrono {
class Date {
public:
class Invalid {}; // tipo vuoto: segnala "data non valida"
Date(int yy, int mm, int dd); // controlla la validita' e inizializza
void add_day(int n); // avanza di n giorni (n >= 0)
int year() const { return y; } // funzioni const: non modificano l'oggetto
int month() const { return m; }
int day() const { return d; }
private:
int y, m, d;
static int days_in_month(int y, int m);
bool is_valid() const; // verifica gli invarianti
};
bool leapyear(int y) { return (y % 4 == 0 && y % 100 != 0) || y % 400 == 0; }
int Date::days_in_month(int y, int m)
{
switch (m) {
case 2: return leapyear(y) ? 29 : 28;
case 4: case 6: case 9: case 11: return 30;
default: return 31;
}
}
bool Date::is_valid() const
{
if (m < 1 || m > 12) return false;
return d >= 1 && d <= days_in_month(y, m);
}
Date::Date(int yy, int mm, int dd)
: y{yy}, m{mm}, d{dd}
{
if (!is_valid()) throw Invalid(); // un costruttore puo' fallire solo cosi'
}
void Date::add_day(int n)
{
for (int i = 0; i < n; ++i) {
if (++d > days_in_month(y, m)) {
d = 1;
if (++m > 12) { m = 1; ++y; }
}
}
}
// helper function: non hanno bisogno dei dati privati, usano l'interfaccia pubblica
bool operator==(const Date& a, const Date& b)
{
return a.year() == b.year() && a.month() == b.month() && a.day() == b.day();
}
bool operator!=(const Date& a, const Date& b) { return !(a == b); }
ostream& operator<<(ostream& os, const Date& dt)
{
return os << dt.year() << '-' << dt.month() << '-' << dt.day();
}
Date next_day(const Date& dt)
{
Date r = dt; // copia
r.add_day(1);
return r;
}
} // namespace Chrono
int main()
{
using namespace Chrono;
try {
Date d {2023, 2, 27};
cout << d << '\n';
d.add_day(2);
cout << "dopo 2 giorni: " << d << '\n';
Date bis {2024, 2, 28};
cout << "28/2/2024 + 1 giorno: " << next_day(bis) << '\n';
cout << (next_day(bis) == Date{2024, 2, 29}) << '\n';
cout << "Provo con 2023-2-30\n";
Date wrong {2023, 2, 30}; // lancia Invalid
cout << "non arrivo qui\n";
}
catch (Date::Invalid) {
cout << "Invalid date!\n";
}
return 0;
}Uscita (l'ultima riga viene dal ramo catch. In WebAssembly le eccezioni non funzionano: il programma è compilato per Linux e l'uscita è ricavata a mano, poi confermata eseguendo una copia in cui throw stampa e termina e try/catch sono tolti):
2023-2-27
dopo 2 giorni: 2023-3-1
28/2/2024 + 1 giorno: 2024-2-29
1
Provo con 2023-2-30
Invalid date!La traccia del add_day: 27 febbraio 2023 + 1 giorno = 28; + 1 giorno = 29 > 28 giorni del mese d = 1, m = 3: 2023-3-1. Nel 2024, bisestile, il 28 febbraio + 1 dà il 29. Date wrong {2023, 2, 30} rende is_valid() falsa (30 > 28), il costruttore lancia Invalid e l'oggetto non esiste.
Dove si scrivono le funzioni membro
Le funzioni membro vanno dichiarate nella classe e possono essere definite dentro o fuori.
| dentro la classe (in-class definition) | fuori dalla classe | |
|---|---|---|
| sintassi | corpo nella definizione della classe | tipo Classe::nome(...) { ... } |
| effetto | rende la funzione automaticamente inline (il codice è replicato a ogni chiamata) | normale funzione |
| pregi | utile per funzioni brevissime e chiamate spesso | interfaccia pulita e corta |
| difetti | classe più lunga e disordinata, dettagli implementativi mischiati all'interfaccia; ogni modifica impone di ricompilare tutto il codice che usa la classe | meno compatto |
Organizzazione consigliata: prima l'interfaccia, poi i dettagli:
class Date {
public:
Date(int y, int m, int d); // controlla validità e inizializza
void add_day(int n);
int month() const;
int day() const;
int year() const;
private:
int y, m, d;
};Buona pratica: non definire funzioni membro dentro la classe. Eccezione: la funzione è molto piccola e si vuole l'inlining. Una funzione con più di una o due espressioni non ne beneficia. inline è solo una richiesta al compilatore, che può ignorarla. Fuori dalla classe, il nome deve avere il prefisso Date::: scordarlo (int month()) definisce una funzione libera e m non è più in scope.
Funzioni membro const
void some_function(Date& d, const Date& start_of_term)
{
int a = d.day(); // ok
int b = start_of_term.day(); // dovrebbe essere ok: legge soltanto
d.add_day(3); // ok
start_of_term.add_day(3); // errore: modifica un oggetto const
}Se day() non è dichiarata const, il compilatore non ha la garanzia che l'oggetto non sarà modificato (la funzione non lo modifica, ma non è esplicito). Errore reale:
error: 'this' argument to member function 'day' has type 'const Date', but function is not marked constLa soluzione è dichiarare const le funzioni membro che non modificano lo stato:
int day() const; // const dopo la lista dei parametri
Month month() const;
void add_day(int n); // modifica: non constModificare un membro dentro una funzione const dà un errore di compilazione (++d; in int Date::day() const). Quindi: identificare le funzioni membro costanti è una regola dell'interfaccia ben fatta.
Funzioni membro o helper function
Per alcune funzionalità si può scegliere fra funzione membro e helper function (funzione libera). Criterio:
"A function that can be simply, elegantly, and efficiently implemented as a freestanding function should be implemented outside the class. That way, a bug in that function cannot directly corrupt the data in a class object." (Stroustrup)
Se i dati membro risultano corrotti, solo le funzioni membro vanno controllate: meno sono, meglio sono protetti i dati. Una libreria di date commerciale può avere funzioni come next_sunday() e next_workday() per un totale di decine di funzioni: non è l'interfaccia minimale che si vuole.
Le helper function sono tipicamente operator==, operator!=, next_sunday, leapyear; usano l'interfaccia pubblica (d.day(), d.month(), d.year()), non i dati privati. Spesso stanno nello stesso namespace della classe (namespace Chrono, come nel programma sopra), così sono vicine alla classe senza farne parte (C++ - scope, namespace e librerielo scope è la regione di testo in cui un nome è valido, dal punto di dichiarazione alla fine dello scope; i tipi di scope sono globale (di file), di classe, locale (blocco o lista argomenti di una funzione), di statement (for) e di namespace; funzioni dentro funzioni sono illegali, le classi locali da evitare; i namespace raggruppano nomi senza creare un tipo e si accedono col nome qualificato namespace::membro oppure con using (la using directive va evitata negli header); un header file è un insieme di dichiarazioni che chi lo include può usare (<> per i file di sistema, "" per quelli del progetto), dev'essere protetto dalle include guard (#ifndef/#define/#endif) per non includere due volte le stesse definizioni; il linker unisce i file oggetto con le librerie, statiche (copiate in ogni eseguibile, self-contained) o dinamiche (una sola copia condivisa, .so su Linux, .dll su Windows, ricompilabili senza toccare gli eseguibili).C++ - scope, namespace e librerie →).
Domande d'esame
1. Quali sono le due parti di una classe e che differenza c'è fra interfaccia e implementazione? Traccia. Interfaccia = parte pubblica, il punto di vista di chi usa la classe; implementazione = parte privata (dati e dettagli), accessibile solo tramite l'interfaccia, il che permette di mantenere lo stato coerente.
2. Perché per Date si usa un costruttore invece di una funzione init_day()?
Traccia. Il costruttore è chiamato obbligatoriamente alla creazione dell'oggetto, quindi non esistono Date non inizializzate o inizializzate male; una funzione init_day() si può non chiamare o bypassare.
3. Che cos'è un invariante? Fare esempi.
Traccia. Regola che definisce gli stati validi; il costruttore la stabilisce, le funzioni membro la mantengono. Date: mese in 1-12 e giorno compatibile col mese e con l'anno bisestile; Rational: denominatore , frazione ridotta; vector: sz uguale alla dimensione del buffer.
4. Quando va definita una funzione dentro la classe e quando fuori? Traccia. Dentro solo se brevissima (1-2 espressioni) e da inlinare; fuori in tutti gli altri casi: interfaccia leggibile, meno ricompilazioni.
5. Che cosa significa dichiarare una funzione membro const?
Traccia. Garantisce che non modifica l'oggetto; è necessaria per poterla chiamare su oggetti const o su const T&; la violazione è un errore di compilazione.
Versione ripasso
- UDT = classi
std(string,vector) e proprie; chi scrive la libreria standard non ha "superpoteri". Un tipo ha rappresentazione (stato) e operazioni. Una classe specifica come gli oggetti si creano, si usano e si distruggono. Rimandi: Tipi di dato astrattiUn ADT è definito dalle operazioni e dal loro comportamento, non dalla rappresentazione; interfaccia e implementazione; realizzazione in Python con le classi e in C con header e tipo opaco; esempio di un ADT Frazione.Tipi di dato astratti →, Strutture (struct) in Cstruct per raggruppare campi di tipo diverso, typedef, accesso con punto e freccia, struct come parametri e valori di ritorno, array di struct, struct allocate dinamicamente e struct autoreferenziali.Strutture (struct) in C →. - Membri: dati membro e funzioni membro;
oggetto.membro. Interfaccia (public) contro implementazione (private); nellaclassi membri sono privati di default, nellastructpubblici. Stato modificabile solo tramite l'interfaccia controllo di coerenza.
class Date {
public:
Date(int y, int m, int d); // controlla validità e inizializza
void add_day(int n);
int year() const; int month() const; int day() const;
private:
int y, m, d;
bool is_valid() const;
};
Date::Date(int yy, int mm, int dd) : y{yy}, m{mm}, d{dd} // lista di inizializzazione
{ if (!is_valid()) throw Invalid(); }- Evoluzione di
Date: (1) struct pubblica + helper function (init_day,add_day): si bypassano; (2) funzioni membro + costruttore; (3) dati privati + accessoriyear(),month(),day(); (4) interfaccia prima, dettagli dopo; (5)is_valid()+ eccezioneInvalid. - Costruttore: stesso nome della classe, dà uno stato iniziale valido. Con soli costruttori con argomenti,
Date d;non compila. Lista di inizializzazione: y{yy}, m{mm}, d{dd}: un solo passo (contro due: default + assegnamento), più esplicita. Forme:Date d {2000,12,31};(consigliata),Date d = {..};,Date d(..);. - Invariante = regola dei valori validi; costruttore la stabilisce, funzioni membro la conservano ("suppongono che valga in ingresso, garantiscono in uscita"). Esempi:
Datemese 1-12 e giorno valido;Rationaldenominatore e ridotta. - Errori: codici di ritorno, dato membro, eccezione (
throw,try,catch): separa rilevamento e gestione; l'unico modo per far fallire un costruttore.class Invalid {};dentroDate= tipo vuoto per segnalare l'errore. Rimando: C++ - eccezioni e gestione delle risorse (RAII)Un errore che non si può gestire dove avviene si segnala con throw e si gestisce con try/catch più in alto; la libreria standard ha la gerarchia std::exception (logic_error e runtime_error con le loro derivate) e le eccezioni proprie si derivano pubblicamente da una di esse; si cattura per riferimento. Le eccezioni mettono in crisi il codice che acquisisce risorse (memoria con new, file, lock, socket): se una chiamata lancia prima della delete, la risorsa si perde. La soluzione è RAII (Resource Acquisition Is Initialization): acquisire la risorsa nel costruttore di un oggetto locale e rilasciarla nel distruttore, perché all'uscita da uno scope, anche per eccezione (stack unwinding), i distruttori degli oggetti locali vengono chiamati. Garanzie di una funzione: basic (nessuna perdita, stato valido), strong (commit or rollback: se fallisce lo stato osservabile è quello di prima) e no-throw (non lancia).C++ - eccezioni e gestione delle risorse (RAII) →. - Funzioni membro: dichiarate nella classe; dentro = automaticamente
inline, utile solo se brevissime, ma allunga la classe e costringe a ricompilare; fuori conDate::nome. Buona pratica: fuori, salvo 1-2 espressioni da inlinare;inlineè solo una richiesta. - Funzioni
const:int day() const;non modificano l'oggetto; necessarie perconst Date&;'this' argument ... has type 'const Date', but function is not marked const. Modificare un membro in unaconsterrore. - Membro o helper function: se si può scrivere con la sola interfaccia pubblica, esterna (
operator==,operator!=,next_sunday,leapyear); così un bug non corrompe i dati; interfaccia piccola. Spesso nello stesso namespace (Chrono). - Traccia
add_day: 2023-2-27 +2 giorni = 2023-3-1; 2024-2-28 +1 = 2024-2-29 (bisestile);Date{2023,2,30}Invalid. - Versione finale in breve (namespace
Chrono, controllo e helper function):
namespace Chrono {
class Date {
public:
class Invalid {};
Date(int yy, int mm, int dd); // lancia Invalid se la data non esiste
void add_day(int n);
int year() const { return y; } int month() const { return m; } int day() const { return d; }
private:
int y, m, d;
static int days_in_month(int y, int m);
bool is_valid() const;
};
bool operator==(const Date& a, const Date& b) // helper: usa solo l'interfaccia
{ return a.year() == b.year() && a.month() == b.month() && a.day() == b.day(); }
}
try { Date wrong {2023, 2, 30}; } catch (Date::Invalid) { cout << "Invalid date!\n"; }- Uscita del programma completo:
2023-2-27,dopo 2 giorni: 2023-3-1,28/2/2024 + 1 giorno: 2024-2-29,1(uguaglianza vera), poiInvalid date!. - Domande tipiche: (1) due parti di una classe = interfaccia pubblica e implementazione privata; (2) costruttore invece di
init_day()perché è obbligatorio alla creazione e non si può bypassare; (3) invariante = regola degli stati validi (esempiDate,Rational,vector:sz= dimensione del buffer); (4) funzioni dentro la classe solo se brevissime; (5)const= non modifica l'oggetto. - Versione 1, perché non basta:
Date x; x.y = -3; x.m = 13; x.d = 32;compila;tomorrow.d = today.d + 1;può dare 32; l'helperinit_daysi può non chiamare. - Forme di costruzione:
Date last {2000,12,31};(consigliata),Date next = {2014,2,14};,Date christmas = Date{1976,12,24};,Date last(2000,12,31);(vecchio stile);last.add_day(1);ok,add_day(2);errore (quale data?). - Errore tipico del compilatore:
start_of_term.add_day(3);conconst Date&'this' argument to member function ... has type 'const Date', but function is not marked const. - Errori tipici: scordare
Date::fuori dalla classe; dimenticareconstsulle funzioni di lettura; dati pubblici che invalidano l'invariante; costruttore che non controlla; mettere troppe funzioni nella classe; assegnare i membri nel corpo invece di usare la lista.
Esercizi su questo argomento
- Esercizio - Classe Image con buffer dinamico (terzo appello 2023-24, programmazione)
- Esercizio - Helper function e la classe Book (terzo appello 2021-22, teoria)
- Esercizio - La classe Complex (primo appello 2022-23, programmazione)
- Esercizio - MyVector con memoria dinamica (laboratorio 3)
- Esercizio - Numeri complessi (quarto appello 2023-24, programmazione)
- Esercizio - Numeri razionali con classe e overloading (laboratorio 2)
- Esercizio - Quiz su conversioni, constexpr, free store e downcasting (terzo appello 2021-22, quiz)
- Esercizio - Quiz su record di attivazione, new[], dangling pointer e struct (terzo appello 2022-23, quiz)
- Esercizio - Rubrica di contatti con list e sort (primo appello 2023-24, programmazione)