Corso di Strumenti e Tecniche di Programmazione Le classi: notazioni di base in C++ Corso di Laurea Magistrale in Ingegneria delle Telecomunicazioni Università degli Studi di Napoli Federico II Facoltà di Ingegneria
Riferimenti Da C++ a UML: –Capitolo 15: tutto –Capitolo 16: tutto –Capitolo 18 § 1, 2, 4, 6, 8, 10 –Capitolo 19 § 4.1
Caratteristiche di una classe La classe è un modulo software con le seguenti caratteristiche: –È dotata di un’interfaccia (specifica) e di un corpo (implementazione) –La struttura dati “concreta” di un oggetto della classe, e gli algoritmi che ne realizzano le operazioni, sono tenuti nascosti all’interno del modulo che implementa la classe –Lo stato di un oggetto evolve unicamente in relazione alle operazioni ad esso applicate –Le operazioni sono utilizzabili con modalità che prescindono completamente dagli aspetti implementativi; in tal modo è possibile modificare gli algoritmi utilizzati senza modificare l’interfaccia
4 Esempio La classe Automobile –Interfaccia pubblica: dà accesso a “ciò che l'auto può fare” –volante –blocchetto di accensione –pedale dell'acceleratore –… –Implementazione privata: specifica “come l'auto fa ciò che può fare” –meccanica dello sterzo –elettromeccanica dell'avviamento –sistema di alimentazione e accensione –…
Rappresentazione grafica La notazione grafica per una classe è una metafora nella quale sono rappresentati in tre sezioni contigue: il nome della Classe la struttura dati (variabili membro) i metodi della Classe (funzioni membro o metodi)
Le classi in C++ - 1/2 Il linguaggio C++ supporta esplicitamente la dichiarazione e la definizione di tipi astratti da parte dell'utente mediante il costrutto class; le istanze di una classe vengono dette oggetti. In una definizione class occorre specificare sia la struttura dati che le operazioni consentite su di essa. Una classe possiede, in generale, una sezione pubblica ed una privata.
Le classi in C++ - 2/2 La sezione pubblica contiene tipicamente le operazioni (dette anche metodi) consentite ad un utilizzatore della classe. Esse sono tutte e sole le operazioni che un utente può eseguire, in maniera esplicita od implicita, sugli oggetti. La sezione privata comprende le strutture dati e le operazioni che si vogliono rendere inaccessibili dall'esterno. Esempio di interfaccia di un tipo astratto Contatore in C++: class Contatore { public: void Incrementa(); // operazione incremento void Decrementa(); // operazione decremento private: unsigned int value; // valore corrente const unsigned int max;// valore massimo };
Produzione e uso di una classe Il meccanismo delle classi è orientato specificamente alla riusabilità del software Occorre dunque fare riferimento ad una situazione di produzione del software nella quale operano: –Il produttore della classe, il quale mette a punto la specifica, in un file di intestazione (header file) l’implementazione della classe (in un file separato) –L’utilizzatore della classe, il quale ha a disposizione la specifica della classe, crea oggetti e li utilizza nel proprio modulo // Modulo utilizzatore del modulo // Contatore #include “Contatore.h” // Interfaccia del // modulo Contatore class Contatore { … }; // Implementazione del modulo Contatore #include “Contatore.h” Utente.cpp Contatore.hContatore.cpp
Relazione d’uso tra classi Il modulo utente di una data classe può essere il programma principale oppure un’altra classe. Tra due classi può cioè sussistere una relazione d’uso, in cui una svolge il ruole di utente dei servizi offerti dall’altra Ruolo "Cliente" Svolto dalla classe che utilizza le risorse messe a disposizione dall’altra Ruolo "Servente" Svolto dalla classe i cui servizi sono usati dal cliente Notazione grafica: >
Uso di una classe da parte di un modulo utente Un modulo utente di una classe: include la specifica della classe (contenuta nel file nomeClasse.h) definisce oggetti istanze della classe e invoca metodi sugli oggetti C.h Utente.cpp C.cpp include
MIV5678 MIK1178 BO8956 Automobile a b c valori variabili L’istanziazione degli oggetti Un oggetto è una istanza (“esemplare”) di una classe Due esemplari della stessa classe sono distinguibili soltanto per il loro stato (i valori dei dati membro), mentre il comportamento potenziale (le funzioni membro) è identico
Uso di una classe //PROGRAMMA UTENTE DELLA CLASSE C #include "C.h" main() { //instanziazione degli oggetti C c1; //definisce oggetto c1 della classe C C c2; //definisce oggetto c2 della classe C c1.f1(....); //applica ad oggetto c1 il metodo f1 c1.f2(....); //applica ad oggetto c1 il metodo f2 c2.f1(....); //applica ad oggetto c2 il metodo f } //fine programma utente //PROGRAMMA UTENTE DELLA CLASSE C #include "C.h" main() { //instanziazione degli oggetti C c1; //definisce oggetto c1 della classe C C c2; //definisce oggetto c2 della classe C c1.f1(....); //applica ad oggetto c1 il metodo f1 c1.f2(....); //applica ad oggetto c1 il metodo f2 c2.f1(....); //applica ad oggetto c2 il metodo f } //fine programma utente deve includere l'header file
Esempio Lampadina +on() +off() +brighten() +dim() //Esempio d’uso Light lt; //istanziazione di un oggetto lt.on(); //invocazione di un metodo sull’oggetto Nome della classe Interfaccia Lampadina Operazioni
La specifica di una classe Rappresenta un'interfaccia per la classe stessa in cui sono descritte: –le risorse messe a disposizione ai suoi potenziali utenti –le regole sintattiche per il loro utilizzo E' separata dalla implementazione, permette l’utilizzo senza che l’utente conosca i dettagli dell’implementazione È a cura dello sviluppatore della classe È scritta in un apposito "file di intestazione" Rappresenta un'interfaccia per la classe stessa in cui sono descritte: –le risorse messe a disposizione ai suoi potenziali utenti –le regole sintattiche per il loro utilizzo E' separata dalla implementazione, permette l’utilizzo senza che l’utente conosca i dettagli dell’implementazione È a cura dello sviluppatore della classe È scritta in un apposito "file di intestazione"
Specifica: notazione base SPECIFICA DELLA CLASSE //nome del file C.h class C { public: //prototipi delle funzioni membro T1 f1(....); T2 f2(....); private: //struttura dati interna int i; char c; }; //fine specifica della classe C SPECIFICA DELLA CLASSE //nome del file C.h class C { public: //prototipi delle funzioni membro T1 f1(....); T2 f2(....); private: //struttura dati interna int i; char c; }; //fine specifica della classe C
L’implementazione di una classe È la codifica in C++ delle singole operazioni presentate nell'interfaccia della classe È una particolare soluzione (può cambiare l'implementazione senza che cambi l'interfaccia) È a cura dello sviluppatore della classe È scritta in un apposito "file di implementazione " (con estensione.cpp) È la codifica in C++ delle singole operazioni presentate nell'interfaccia della classe È una particolare soluzione (può cambiare l'implementazione senza che cambi l'interfaccia) È a cura dello sviluppatore della classe È scritta in un apposito "file di implementazione " (con estensione.cpp)
Implementazione //IMPLEMENTAZIONE DELLA CLASSE //nome del file C.cpp #include "C.h" T1 C::f1(....) { //realizzazione della funzione f1 } T2 C::f2(....) { //realizzazione della funzione f } //fine del file C.cpp //IMPLEMENTAZIONE DELLA CLASSE //nome del file C.cpp #include "C.h" T1 C::f1(....) { //realizzazione della funzione f1 } T2 C::f2(....) { //realizzazione della funzione f } //fine del file C.cpp deve includere anche l'header file
Generazione del file eseguibile Comp. C++ Comp. C++ C.o Utente.o C.h Utente.cpp C.cpp include C.h include Utente.exe linker
Struttura degli oggetti Ciascun oggetto (istanza di una classe) è costituito: da una parte base, allocata per effetto della definizione dell’oggetto nel programma utente in area dati statici, nell’area stack o nell’area heap, in base alla classe di memorizzazione; da una eventuale estensione, allocata in area heap a b c d e f g \0 int n char * n
Ciclo di vita di un oggetto (1/2) definizione dell’oggetto allocazione inizializzazione deallocazione
Ciclo di vita di un oggetto (2/2) definizione oggetto: a cura del programma utente allocazione parte base: a cura del compilatore allocazione eventuale estensione: mediante una speciale funzione membro detta COSTRUTTORE (a cura quindi del produttore della classe) inizializzazione oggetto: a cura del costruttore deallocazione eventuale estensione: a cura di una speciale funzione membro detta DISTRUTTORE deallocazione parte base: a cura del compilatore
Un costruttore è una funzione membro: –che ha lo stesso nome della classe –non restituisce alcun risultato (neanche void ) class Account { public: Account(double amt); void Withdraw(double amt); void Deposit(double amt); double GetBalance(); int GetTransactions(); private: double balance; int *transactions; }; Account::Account(double amt): balance(amt),transactions(new int) {}; Costruttori il costruttore Liste di assegnazione
Ogni istanziazione di un oggetto della classe produce l’invocazione di un costruttore –l’oggetto è creato come variabile globale o locale: –l’oggetto è creato come variabile dinamica: Se il compilatore non individua il costruttore da chiamare segnala un errore Se l’oggetto ha dei dati membri di tipi classe, su ciascuno di essi è ricorsivamente chiamato un costruttore –i costruttori dei dati membri sono chiamati secondo l’ordine di dichiarazione dei dati –la chiamata dei costruttori dei dati membro precede la chiamata del costruttore dell’oggetto Invocazione dei costruttori Account act(100); Account *act = new Account(100);
PROGRAMMA UTENTE DELLA CLASSE C #include “account.h“ main() { //Allocazione di un oggetto Account act(100); //viene invocato il costruttore //act è una var. automatica, allocata in area stack //Allocazione dinamica di un oggetto Account *pact = new Account(100); //invoca il costr. //il punt. pact è una var. automatica, //allocata in area stack; //l’oggetto puntato è allocato in area heap } //fine programma utente PROGRAMMA UTENTE DELLA CLASSE C #include “account.h“ main() { //Allocazione di un oggetto Account act(100); //viene invocato il costruttore //act è una var. automatica, allocata in area stack //Allocazione dinamica di un oggetto Account *pact = new Account(100); //invoca il costr. //il punt. pact è una var. automatica, //allocata in area stack; //l’oggetto puntato è allocato in area heap } //fine programma utente Invocazione dei costruttori
Distruttori Un distruttore è una funzione membro che: è necessaria solo se l’oggetto presenta un’estensione dinamica ha lo stesso nome della classe, preceduto da ~ (tilde) e non restituisce risultato (neanche void) né ha alcun parametro ha lo scopo di deallocare l’estensione dinamica di un oggetto NON può essere invocata esplicitamente dal programma utente, ma viene invocata implicitamente dal compilatore quando termina il ciclo di vita dell’oggetto
Distruttori: esempio class Account { public: Account(double amt); ~Account(); void Withdraw(double amt); void Deposit(double amt); double GetBalance(); int GetTransactions(); private: double balance; int *transactions; }; Account::Account(double amt): balance(amt),transactions(new int) {}; Account::~Account() { delete transactions; };
Invocazione dei distruttori Se una classe fornisce un distruttore, questo è automaticamente chiamato ogni volta che un oggetto della classe è deallocato: –l’oggetto è locale e si esce dal blocco in cui è dichiarato –l’oggetto è nello heap e su di esso viene eseguita una delete –l’oggetto è dato membro di un altro oggetto e quest’ultimo è deallocato i distruttori dei dati membri sono chiamati secondo l’ordine di dichiarazione dei dati la chiamata dei distruttori dei dati membro segue la chiamata del distruttore dell’oggetto Se una classe non fornisce un distruttore ne viene creato uno di default che invoca ordinatamente i distruttori di tutti i dati membro
//PROGRAMMA UTENTE DELLA CLASSE C #include “account.h“ void f() { //Allocazione dinamica di un oggetto Account *pact = new Account(100); if(…) { Account act(100); //invoca il costruttore … //fine del blocco, viene invocato il distruttore, // e act viene deallocato dallo stack } delete pact; // viene invocato il distruttore // per l’area heap puntata da pact } //fine procedura f //PROGRAMMA UTENTE DELLA CLASSE C #include “account.h“ void f() { //Allocazione dinamica di un oggetto Account *pact = new Account(100); if(…) { Account act(100); //invoca il costruttore … //fine del blocco, viene invocato il distruttore, // e act viene deallocato dallo stack } delete pact; // viene invocato il distruttore // per l’area heap puntata da pact } //fine procedura f Invocazione dei distruttori
Tipi di costruttori con zero argomenti con uno o più argomenti di copia SPECIFICA DELLA CLASSE Nome del file C.h class C { public: //funzioni costruttore C(); //costruttore con zero argomenti C(valori iniziali); //costruttore con più argomenti C(const C& c1); //costruttore di copia private: //struttura dati } //fine specifica della classe C SPECIFICA DELLA CLASSE Nome del file C.h class C { public: //funzioni costruttore C(); //costruttore con zero argomenti C(valori iniziali); //costruttore con più argomenti C(const C& c1); //costruttore di copia private: //struttura dati } //fine specifica della classe C
Le funzioni membro inline Le funzioni membro possono essere dichiarate inline in uno dei seguenti due modi –usando la parola chiave inline nella definizione della funzione membro (che deve apparire nello stesso file in cui è presente l’interfaccia della classe) –senza la parola chiave inline, ma inserendo il corpo della funzione nella stessa interfaccia della classe class Account { public: void Init(double amt); void Withdraw(double amt); void Deposit(double amt); double GetBalance() { return balance; } int GetTransactions(); private: double balance; int transactions; }; inline double Account::GetBalance() { return balance; } In alternativa Nello stesso file Usare con parsimonia le funzioni inline Quando l’implementazione cambia, tutti i file che usano la funzione devono essere ricompilati
I dati membro static I dati membro, pubblici o privati, di una classe possono essere dichiarati static –sono dati creati ed inizializzati una sola volta, indipendentemente dal numero di oggetti istanziati vanno dichiarati nella definizione della classe e definiti ed implementati separatamente usando l’operatore di risoluzione dello scope, i dati pubblici, come le normali variabili globali, possono essere usati dovunque nel programma (ma è preferibile non usarli) class Account { public: … private: static int accounts; }; int Account::accounts = 0; È un dato che conta il numero di oggetti del tipo Account che sono stati istanziati dal programma Va inizializzato a 0 e incrementato da Init
Le funzioni membro static Le funzioni membro, pubbliche o private, di una classe possono essere dichiarate static –sono funzioni che non ricevono l’argomento this e quindi: possono accedere solo ai dati membro statici della classe possono invocare solo funzioni membro statiche della classe class Account { public: … static int GetAccounts(); private: … static int accounts; }; int Account::accounts = 0; int Account::GetAccounts() { return accounts; } Account act; … cout << act.GetAccounts(); cout << Account::GetAccounts(); Le due forme d’invocazione sono equivalenti La forma in alto è da preferire
I dati membro const class T1 { int const size; public: T1(); }; T1::T1(): size(100) {} class T2 { int const size = 100; // Illegal int array[size]; // Illegal … }; class T2 { enum { size = 100 }; int array[size]; … }; class T2 { static int const size; int array[size]; … }; int const T2::size = 100; Il significato di un dato membro const è che ogni oggetto della classe contiene un dato immutabile dopo l’inizializzazione che può essere diversa da oggetto ad oggetto Se si vuole una costante a tempo di compilazione non si può scrivere così! Il problema può essere risolto con l’uso di un’enumerazione anonima o con quello di una costante statica Membri privati Lista d’inizializzazione (vedi n.39)
Le funzioni membro const Le funzioni membro che non modificano l’oggetto possono e devono essere esplicitamente dichiarate const La parola chiave const segue la lista degli argomenti e deve essere presente sia nel prototipo della funzione membro che nella definizione double Account::GetBalance() const { return rep->balance; } int Account::GetTransactions() const { return rep->transactions; } Le funzioni membro const sono dette accessors in quanto servono per accedere all’oggetto senza modificarlo class Account { public: … double GetBalance() const; int GetTransactions() const; private: … }; Le funzioni membro non const sono dette mutators in quanto servono per modificare l’oggetto
Le funzioni membro const sono le uniche che possono essere chiamate su un oggetto const (a parte i costruttori e il distruttore) Gli oggetti const Account const act(150); int bal = act1.GetBalance(); //OK act1.Deposit(50); //Error
Il costruttore della classe Account può essere scritto come segue La lista d’inizializzazione: –precede il corpo del costruttore –elenca i dati membro nell’ordine di dichiarazione –associa ad ogni dato membro il valore iniziale –in generale, è da preferire all’inizializzazione nel corpo per motivi di efficienza altrimenti il compilatore chiama i costruttori di default dei dati membro, prima di eseguire il costruttore che effettua l’inizializzazione voluta –è obbligatoria nei seguenti casi, in cui l’inizializzazione del dato membro non può essere procrastinata: dato membro costante dato membro riferimento dato membro di un tipo classe senza costruttore di default Le liste d’inizializzazione Account::Account(double amt = 0): balance(amt), transactions(0) { } Lista d’inizializzazione
Un esempio class Circle { public: Circle(Color &c, Point const ¢, double const r); … private: Color& color; // Note: the color is a reference Point center; // Note: Point has no default constructor double const radius;// Note: the radius is a constant }; Circle::Circle(Color& c, const Point& cent, const double r) { color = c; // Error: too late to initialize a reference center = cent; // Error: no default constructor for a Point radius = r; // Error: too late to initialize a constant } Circle::Circle(Color& c, Point const ¢, double const r): color(c), center(cent), radius(r) { } Costruttore senza lista d’inizializzazione (scorretto) Costruttore con lista d’inizializzazione (corretto) Vanno inizializzati nel costruttore
Il costruttore di copia di una classe C ha la forma: C(const C&) Viene utilizzato quando occorre realizzare la copia di un oggetto esistente: –nel creare un clone dell’oggetto –nel passare l’oggetto per valore –nel restituire l’oggetto per valore Il costruttore di copia MyString me("Jerry"); MyString clone = me; void OpenFile(MyString); MyString name("flights"); OpenFile(name); MyString NewPasswd() { MyString psw(""); … return psw; } class MyString { public: MyString(char const *s = ""); ~MyString(); … private: int length; char* str; }; Attenzione! Corrisponde a MyString clone(me); È una copia non una assegnazione
Copia superficiale e copia profonda Quando la classe non fornisce il costruttore di copia, il compilatore ne crea uno di default che effettua la copia superficiale dell’oggetto (“shallow copy”) –consiste nell’applicare ordinatamente il costruttore di copia ad ogni dato membro per i dati primitivi, l’operazione si risolve in una semplice copia dei byte relativi Quando tra i dati membro dell’oggetto vi sono puntatori, la copia superficiale non è in generale adeguata ed occorre impiegare la copia profonda (“deep copy”) –in questo caso, il costruttore di copia deve essere esplicitamente programmato
Shallow copy di MyString Deep copy di MyString Copia superficiale e copia profonda Nel caso di shallow copy: –I caratteri delle due stringhe sono condivisi (“sharing”) modificando un carattere di una stringa si modifica anche l’altra stringa se una delle due stringhe è deallocata, il distruttore dealloca l’array di caratteri, lasciando l’altra stringa in uno stato scorretto, con un puntatore “dangling” se si dealloca anche l’altra stringa si può avere un crash del sistema Nel caso di deep copy: –Ogni stringa ha la sua copia dei caratteri qualunque operazione su di una stringa non modifica l’altra stringa
Il costruttore di copia della classe MyString è: Fare attenzione: Se si vuole impedire la copia degli oggetti di una classe è sufficiente dichiarare private il costruttore di copia (se la classe non lo usa non occorre definirlo) Ancora sui costruttori di copia MyString::MyString(MyString const &s) { length = s.length; str = new char[length + 1]; // +1 for '\0' strcpy(str, s.str); } MyString giulia("Giulia Roberts"); MyString giuliaClone; giuliaClone = giulia; Il costruttore di copia non è chiamato nell’assegnazione! Per evitare che si abbia una copia superficiale, occorre fornire il costruttore di assegnazione MyString giulia("Giulia Roberts"); MyString giuliaClone = giulia; Il costruttore di copia è chiamato
L’operatore di assegnazione L’operatore di assegnazione di una classe C ha la forma: C& operator=(const C&); La sua implementazione differisce da quella del costruttore di copia per tre motivi: –deve verificare che non si tratti di “autoassegnazione” ( x = x; ) –deve distruggere l’oggetto valore attuale della variabile a cui si assegna –deve restituire il riferimento alla variabile stessa per consentire catene di assegnazioni ( x = y = z; )
L’operatore di assegnazione class T { public: … T const& operator=(T const &other); … } T const &T::operator=(T const &other) { if (this != &other) { // do whatever is needed to destroy this // do whatever is needed to copy other } return *this;} Il ricevente può solo usare l'oggetto *this per assegnarlo alla variabile più a sinistra, ma non può alterarlo other denota l'oggetto, &other il puntatore all'oggetto
MyString const &MyString::operator=(MyString const &other) { if (this != &other) { delete str; length = other.length; str = new char[length + 1]; strcpy(str, other.str); } return *this; } La scrittura x = y; è una forma abbreviata di x.operator=(y); L’operatore di assegnazione (esempio MyString ) L’operatore di assegnazione della classe MyString è: Anche il costruttore di assegnazione può essere “mascherato” rendendolo private
Generalmente, una classe che necessita di un distruttore esplicito, necessita anche dei costruttori di copia e di assegnazione Se la classe fornisce le due funzioni membro private il distruttore e i due costruttori hanno la forma standard seguente: In sintesi … class T { public: … ~T(); T(T const &other); T const &operator=(T const &other); … private: void destroy(); //destroy inconditionally void copy(T const &other); //copy inconditionally } T::~T() { destroy(); } T::T(T const &other) { copy(other); } T const &T::operator=(T const &other) { if (this != &other) { destroy(); copy(other); } return *this; }
Una maniera alternativa di realizzare l’operatore di assegnazione Se l’operazione di copia presente nel costruttore di assegnazione non va a buon fine –per es., l’operatore new invocato nell’operazione solleva un’eccezione la rappresentazione dell’oggetto è già stata distrutta –questo lascia dei puntatori “dangling” nell’oggetto Per questo motivo può essere preferibile effettuare prima la copia, usando un oggetto temporaneo e il costruttore di copia, e poi la distruzione dell’oggetto e la copia superficiale dell’oggetto temporaneo T const &T::operator=(T const &other) { if (this != &other) { T temp = other; destroy(); shallow_copy(temp); } return *this; }