Fondamenti di Informatica 2 Ingegneria Informatica Docente: Giovanni Macchia a.a
Classi ed Oggetti Il C++ è un linguaggio di programmazione che fornisce gli strumenti per la programmazione ad oggetti. In C++ le unità fondamentali sono le classi. Le classi implementano gli ADT. Gli oggetti sono istanze delle classi. Vengono create considerando le classi come il loro tipo base.
Classi ed Oggetti La definizione di classe avviene tramite la parola chiave class e la forma è simile alla struct, di cui rappresenta l’evoluzione: class nome_classe { public: variabili e funzioni pubbliche; private: variabili e funzioni private; protected: variabili e funzioni protette; };
Classi ed Oggetti Le variabili e funzioni definite dopo la parola chiave public e prima dell’inizio della parola chiave successiva sono accessibili da altre parti del programma. Le variabili e funzioni definite dopo la parola chiave private e prima dell’inizio della parola chiave successiva sono nascoste ad altre parti del programma e possono essere usate solo da funzioni membro della classe. Vedremo più avanti il significato della parola chiave protected. Le parole chiave protected, public e private si chiamano specificatori di accesso ai membri
Classi ed Oggetti Es: class Persona { private: int stipendio; public: char cognome[30]; char indirizzo[30]; }; Persona Ugo; // si istanzia un oggetto avente // come tipo dato la classe Persona; Altre parti del programma possono accedere a Ugo.cognome e Ugo.indirizzo ma non direttamente a Ugo.Stipendio
Classi ed Oggetti E’ possibile per altre parti del programma accedere e/o manipolare i membri protetti tramite le funzioni membro della classe. Le funzioni membro di una classe definiscono il comportamento che esibiscono gli oggetti appartenenti a quella stessa classe. In C++ implementano i metodi dell’ OOP. La classe è una evoluzione della struct di C. La differenza è che i membri senza specificatore di accesso in una classe sono privati, mentre in una struttura sono pubblici.
Classi ed Oggetti Es: class counter { public: void reset( ); void incr (); void decr ( ); int get( ); private: int val; }; funzioni membro
Classi ed Oggetti In alcuni casi, è importante avere la possibilità di definire dei membri di una classe comuni a tutti gli oggetti della stessa classe. Per fare questo, si usa il modificatore static per dichiarare una variabile statica della classe, ovvero condivisa da tutti gli oggetti della classe. Es: class counter { public: void reset( ); …. private: static int val; };
Classi ed Oggetti E’ possibile dichiarare una funzione non membro o una classe come friend di una classe. In questo modo si consente alla funzione o classe friend di accedere ai membri privati delle classe. Es; class counter { public: friend void print(int); ……...};
Classi ed Oggetti: l’operatore :: Una volta definiti i prototipi delle funzioni membro, occorre scrivere il codice delle funzioni e far riconoscere al compilatore che la funzione è un metodo di una determinata classe. Per questo si usa l’operatore di risoluzione di ambito ::. Es: void contatore::reset ( ) { val = 0;}; void contatore::incr ( ) { val++;}; void contatore::decr ( ) { val--;}; int contatore::get ( ) { return val;};
Classi ed Oggetti: l’operatore :: Tramite l’operatore di risoluzione di ambito il compilatore riconosce di quale classe la funzione membro è un metodo e distingue tra metodi aventi lo stesso nome ma appartenenti a classi differenti. Quando una funzione membro chiama una funzione che appartiene alla stessa classe, può farlo direttamente senza ricorrere all’operatore di risoluzione di ambito.
Classi ed Oggetti:dichiarazione di oggetti Una volta definita una classe, è possibile istanziare un numero qualsiasi di oggetti appartenenti alla classe. Questi oggetti saranno distinti tra loro, avranno la stessa rappresentazione e potranno usare gli stessi metodi forniti dalla classe. Es: contatore c1, c2; definisce due oggetti c1 e c2 di tipo contatore ognuno dei quali ha la propria copia di val e può eseguire solo le operazioni definite nella classe contatore.
Classi ed Oggetti: accesso ai membri Per usare i membri di un oggetto si usano l’operatore freccia e l’operatore punto con le stesse modalità usate per le struct. Es: c1.reset( ); Questa istruzione ha il significato di invio di messaggio all’oggetto c1 di eseguire l’operazione reset( ) che azzera la variabile privata val. Un oggetto può inoltre accedere al proprio indirizzo tramite un puntatore di nome this
Classi ed Oggetti: Costruttori e Distruttori Nella maggior parte dei casi occorre inizializzare alcune parti dell’oggetto prima che siano utilizzate. Il C++ consente agli oggetti di inizializzarsi al momento della loro creazione tramite i costruttori. Un costruttore è una funzioni membro pubblica di una classe che ha le seguenti caratteristiche ha lo stesso nome della classe di appartenenza non deve avere un tipo di ritorno (neanche void) può avere argomenti di default
Es: class counter { public: counter (int n=0) {val = n; }; void reset( ); void incr (); void decr ( ); int get( ); private: int val; }; Costruttore Classi ed Oggetti: Costruttori e Distruttori
I costruttori sono invocati automaticamente quando gli oggetti vengono istanziati. Es: contatore c1, c2(6); Quando viene eseguita l’istruzione sopra descritta, c1 viene creato con val = 0 (valore di default) mentre c2 viene istanziato con val = 6. Il costruttore viene invocato quando viene allocata memoria all’oggetto: contatore *p; //il costruttore non è eseguito p = new contatore(2) ; //il costruttore è eseguito
Classi ed Oggetti: Costruttori e Distruttori Se un costruttore ammette argomenti non di default, questi devono essere specificati nella dichiarazione dell’oggetto. Es. class counter { public: counter (int n, int p) {val = n; sem = p; }; … private: int val,sem}; counter c1(2,0), c2(6,3), c3(7,4);
Classi ed Oggetti: Costruttori e Distruttori Il distruttore è complementare al costruttore e viene usato ogni volta che un oggetto viene distrutto, cosa che avviene quando si esce dall’ambito dell’oggetto. Un distruttore è una funzioni membro pubblica di una classe che ha le seguenti caratteristiche ha lo stesso nome della classe di appartenenza preceduto dal carattere ~. non deve avere un tipo di ritorno (neanche void) non può avere argomenti
Es: class counter { public: counter (int n=0) {val = n; }; ~counter ( ) {cout << “contatore distrutto\n”; }; void reset( ); void incr (); void decr ( ); int get( ); private: int val; }; Distruttore Classi ed Oggetti: Costruttori e Distruttori
Un esempio della necessità di distruttore si ha quando un oggetto viene rilasciato ma non tutta la memoria occupata da esso viene rilasciata Es: class stringa { char *str; public: stringa (char *str) {str = new char[80]; }; set (char *str); show( ); }; Quando viene rilasciato un oggetto di tipo stringa, l’area di memoria creata con new non è rilasciata e non è più disponibile sull’heap!!!! Classi ed Oggetti: Costruttori e Distruttori
Modificando la classe introducendo un distruttore si ha class stringa { char *str; public: stringa (char *str) {str = new char[80]; }; ~stringa( ) {if (str != NULL) delete[ ] str; } set (char *str); show( ); }; e si risolve il problema, poiché viene rilasciata l’area di memoria heap tramite la delete[ ]. Classi ed Oggetti: Costruttori e Distruttori
Un oggetto può essere raggruppato in array Es: contatore cont[4]; Si può accedere ad un oggetto tramite puntatore. Per un oggetto vale l’aritmetica dei puntatori, l’operatore &, l’operatore di dereferenza, l’operatore punto, l’operatore freccia I reference a oggetti sono possibili come i reference a variabili si possono passare degli oggetti a funzioni come qualsiasi altra variabile e le funzioni possono restituire un oggetto Classi ed Oggetti: Costruttori e Distruttori
Vi sono comunque dei problemi: 1) quando un oggetto viene passato ad una funzione ne viene fatta una copia bit a bit che viene data al parametro della funzione. Se l’oggetto contiene un puntatore a una locazione di memoria allocata dinamicamente, allora la copia punterà alla medesima regione di memoria, ed ogni modifica si rifletterà sull’originale. L’oggetto originale ne viene pertanto influenzato. Classi ed Oggetti: Costruttori e Distruttori
Oggetto stringa in main() Copia dell’oggetto stringa nel passaggio a funzione Area di memoria per str
2) quando un funzione restituisce un oggetto, il compilatore genera automaticamente un oggetto temporaneo contenente il valore restituito alla funzione. Quando il valore viene restituito alla funzione chiamante, l’oggetto temporaneo va fuori ambito e provoca la chiamata al distruttore, che potrebbe eliminare qualcosa di necessario alla routine chiamante, come della memoria allocata dinamicamente. Classi ed Oggetti: Costruttori e Distruttori
Oggetto stringa restituito dalla funzione Oggetto temporaneo stringa contenente il valore dell’oggetto stringa restituito dalla funzione e sottoposto all’azione del distruttore Area di memoria per str
La risoluzione a questo tipo di problema è il costruttore di copia. Con il costruttore di copia, è possibile specificare con precisione cosa accade quando : un oggetto viene usato per inizializzarne un altro in un’istruzione di dichiarazione un oggetto viene passato come parametro a una funzione viene creato un oggetto temporaneo da usare come valore restituito ad una funzione Classi ed Oggetti: Costruttori e Distruttori
La forma più comune per il prototipo del costruttore di copia è la seguente: nome_classe (const nome_classe &); Es: class stringa { char *str; public: stringa(char *, int =40); //costruttore normale stringa(const stringa &,int =40); //costruttore di copia char *getcar( ){ return str;} }; Classi ed Oggetti: Costruttori e Distruttori
stringa::stringa(char *inp, int SIZE=40) { int i; str = new char[SIZE]; cout << "sono qui \n"; for(i=0; *(inp+i) != '\0';i++) str[i]=inp[i]; str[i]= '\0'; }; stringa::stringa(const stringa &ob,int SIZE=40) { int i; str = new char[SIZE]; for(i=0;ob.str[i]!='\0';i++) str[i]=ob.str[i]; str[i]='\0'; }; Classi ed Oggetti: Costruttori e Distruttori
void display(stringa ob) { for(int i=0;*(ob.getcar()+i) != '\0';i++) cout << *(ob.getcar()+i); cout << "\n"; }; main ( ) { stringa a("PIPPO25"); display(a); //viene invocato il costruttore di copia return 0; } Classi ed Oggetti: Costruttori e Distruttori
E’ possibile effettuare l’overload degli operatori del C++ in relazione ai tipi di classe. E’ possibile, per esempio, effettuare un overload dell’operatore + e fargli eseguire delle operazioni definite dal programmatore. Il concetto di overload è pertanto strettamente legato all’overloading delle funzioni. La forma generale di una funzione operatore utilizza la parola chiave operator nel seguente modo tipo nome_classe::operator op(argomenti); dove op è l’operatore di cui si vuole effettuare l’overloading. Classi ed Oggetti: Overload di operatori
Es: class parall { int x,y,z; public: parall operator=(parall); …..}; parall parall::operator=(parall ob) {parall tmp; tmp.x = ob.x; tmp.y= ob.y; tmp z = ob.z; return tmp; } Classi ed Oggetti: Overload di operatori
Es: parall a, b; ….. b=a; //viene usato l’operatore = di parall L’operatore = effettua l’overloading dell’operatore = standard in C++. Anche altri operatori (p.e. &&, ||) standard in C++ possono essere ridefiniti. Classi ed Oggetti: Overload di operatori
La composizione in C++ avviene specificando gli oggetti componenti (oggetti membro) all’interno della classe di cui fanno parte. Occorre comunque indicare gli argomenti del costruttore della classe composta da passare ai costruttori degli oggetti membro Questo avviene tramite gli inizializzatori di membro, che vengono usati nella definizione del costruttore. Classi ed Oggetti: Composizione
Es: class Data: {Data (int, int, int); …}; Impiegato::Impiegato( char *nome, int giorno, int mese, int anno) : datadinascita(int giorno, int mese, int anno) {…}; Se una classe Impiegato ha un oggetto membro datadinascita di classe Data, i parametri giorno, mese e anno saranno passati al costruttore Data Classi ed Oggetti: Composizione
Nella terminologia del C++, la superclasse si chiama classe base, mentre la sottoclasse si chiama classe derivata. Le modalità di derivazione di una classe da una classe base sono specificate tramite lo specificatore di accesso alla classe base e la forma generale per dichiarare che una classe eredita da un’altra è: class classe_derivata: accesso classe base { corpo della classe_derivata} dove accesso può essere private, public e protected Classi ed Oggetti: Ereditarietà
Es: class impiegato { public: Impiegato (char *) ; ~Impiegato( ); void print( ) const; private: char *nome;}; class interinale: public impiegato { public: ….. void print( ) const; private: double orario; double salario; }; Classi ed Oggetti: Ereditarietà
Quando una classe base viene ereditata come pubblica, tutti suoi membri pubblici diventano pubblici per la classe derivata mentre i suoi membri privati sono nascosti alla classe derivata. Quando una classe base viene ereditata come privata, tutti i suoi membri pubblici diventano membri privati della classe derivata mentre i suoi membri privati sono nascosti alla classe derivata Classi ed Oggetti: Ereditarietà
Ricordiamo che i membri di una classe possono essere private, public e protected. La modalità protected è simile alla private, con una differenza che si riscontra nel caso di classe derivata: quando una classe base viene ereditata come pubblica, tutti suoi membri protected diventano membri protetti per la classe derivata; quando una classe base viene ereditata come privata, tutti suoi membri protected diventano membri privati per la classe derivata In questo modo è possibile dar vita a membri che sono privati per la loro classe ma che sono ereditabili ed accessibili per una classe derivata. Classi ed Oggetti: Ereditarietà
Es: class base { public: void set(int) ;... protected: int i, j; // privato per base, ma accessibile per derivato }; class derivato: public base { public: … // può accedere a i e j di base private: int k; }; Classi ed Oggetti: Ereditarietà
Quando una classe base viene ereditata come protected, tutti suoi membri pubblici e protetti diventano protetti per la classe derivata mentre i suoi membri privati sono nascosti alla classe derivata. Classi ed Oggetti: Ereditarietà
Una classe derivata eredita i membri dalla classe base e pertanto, quando si istanzia un oggetto di classe derivata, il C++ chiama dapprima il costruttore della classe base e successivamente il costruttore della classe derivata. L’esecuzione dei distruttori avviene in ordine inverso: dapprima vengono eseguiti i distruttori della classe derivata e successivamente quelli della classe base. Classi ed Oggetti: Ereditarietà
Nel caso in cui i costruttori della classe base necessitino di parametri, nella dichiarazione dei costruttori delle classi derivate si utilizzano gli inizializzatori di membro : Es: Interinale::Interinale (char *nome): impiegato(nome) { …. } Classi ed Oggetti: Ereditarietà
E’ possibile che una classe erediti le proprietà di più classe basi. In questo caso, si ha la eredità multipla (multiple inheritance). In questo caso la forma generale per dichiarare una classe con multiple inheritance è class classe_derivata: accesso1 classe_base1, accesso2 classe_base2,..accesson classe_basen { corpo della classe_derivata} dove accesso1,..,accesson possono essere private, public e protected Classi ed Oggetti: Ereditarietà
Sono possibili alcune ambiguità nei casi di eredità multipla: class a {public: int i}; class b: public a {public: int j}; class c: public a {public: int k}; class d: public b, public c { int sum}; d ob; ob.i = 5; // istruzione ambigua: quale i ?? A quale i si riferisce? A quella di b o c ?? Si introduce un elemento di ambiguità. Classi ed Oggetti: Ereditarietà
Per risolvere questi tipo di ambiguità, si usano le classi base virtuali. La loro semantica è simile alle classi base non virtuali, con in aggiunta la parola chiave virtual prima delle definizione di accesso. Es: class a {public: int i}; class b: virtual public a {public: int j}; In questo modo, quando due o più oggetti sono derivati da una classe base comune, è possibile evitare che in un oggetto derivato da quegli oggetti siano presenti più copie della classe base. Classi ed Oggetti: Ereditarietà
Il polimorfismo viene implementato tramite le funzioni virtuali. Una funzione virtuale si intende una funzione che viene preceduta dalla parola chiave virtual in una classe base e poi ridefinita in una o più classi derivate. Classi ed Oggetti: Polimorfismo
Es: class a {.. public: virtual void print ( ) {cout << “prima stampa”;} }; class b: public a {.. public: void print ( ) {cout << “seconda stampa”;} }; Classi ed Oggetti: Polimorfismo
Ciascuna classe derivata può quindi avere la propria versione di funzione virtuale, pur mantenendo la medesima interfaccia. Quando una classe derivata non ridefinisce una funzione virtuale, la funzione viene impiegata nel modo in cui è definita nella classe base Classi ed Oggetti: Polimorfismo