Elementi di programmazione ad oggetti a. a. 2009/2010 Corso di Laurea Magistrale in Ingegneria Elettronica Docente: Mauro Mazzieri, Dipartimento di Ingegneria Informatica, Gestionale e dell’Automazione Lezione 4 Introduzione alla progettazione e programmazione ad oggetti: classi ed oggetti, notazione UML. Natura del software Il software è una soluzione temporanea ad un problema Dinamico Complesso Per affrontare la complessità, si suddivide il software in componenti maneggevoli e interagenti Composizione: processo di costruzione a partire da parti semplici Astrazione: trattare le componenti senza interessarsi dei dettagli di come siano costruite Visione limitata alle poche proprietà essenziali, ignorando i dettagli irrilevanti Dati e funzionalità I dati che un programma tratta si distinguono Le descrizioni dei dati sono fissate I valori individuali dei dati cambiano ogni volta Le funzionalità di un programma determinano cosa si può fare con i dati Progettazione orientata agli oggetti Gli oggetti sono astrazioni usate per creare un modello del mondo reale Le funzionalità sono distribuite sugli oggetti Ogni oggetto ha la responsabilità di eseguire certe specifiche funzionalità L’allocazione delle responsabilità sugli oggetti è una parte critica di una buona progettazione Il sistema software è costituito da un insieme di oggetti interagenti Oggetti La collaborazione tra gli oggetti avviene tramite uno scambio di messaggi Ogni oggetto ha Un nome Delle proprietà Dati, costanti e variabili, conosciuti dall’oggetto, di cui rappresentano lo stato Dei metodi Funzioni e procedure eseguibili dall’oggetto Messaggi Ogni oggetto conosce delle informazioni ed è in grado di svolgere alcuni compiti Gli oggetti si scambiano messaggi, corrispondenti a richieste di servizi In risposta alle richieste, gli oggetti eseguono le operazioni e forniscono i risultati Tipologie di messaggi Esistono due tipologie di messaggi Richieste di esecuzione di metodi Richieste di accesso alle proprietà dell’oggetto Limitatamente a ciò che l’oggetto espone Dati e stato I dati hanno una definizione ed un valore associato Le definizioni sono fisse, i valori associati cambiano nel tempo Variabili di istanza Lo spazio di memoria usato per memorizzare tutte le variabili viene allocato al momento della creazione dell’oggetto Lo stato di un oggetto è contenuto nei suoi dati Insieme istanza dei valori associati alle variabili di Notazione UML L’UML è il linguaggio di modellazione standard per il progetto di sistemi software Comprende diversi tipi di diagrammi usati per modellare diversi aspetti Casi d’uso Attività Classi Sequenze Comunicazione Sincronizzazione Interazione Struttura composita Componenti Pacchetti Stati Installazione Classi in UML Notazione UML Attributi: visibilità nome : tipo Visibilità + public # protected ~ package - private Operazioni: visibilità nome (parametri) : tipo di ritorno Definizione di una classe C++ La definizione di classe è composta da Testa: class <nome_classe> Corpo (racchiuso tra parentesi graffe) Punto e virgola o dichiarazioni, es.: class Class1 { /* … */ }; class Class2 { /* … /* } var1, var2; Il corpo della classe contiene i membri Dati Funzioni membro Definizione di metodi e operatore di scope Dichiarazione e definizione nella classe Dichiarazione nella classe <tipo di ritorno> <nome metodo> (<parametri>) { <corpo> }; <tipo di ritorno> <nome metodo> (<parametri>); Definizione del metodo <tipo di ritorno> <nome classe>::<nome metodo>(<parametri>) { <corpo> } Membri I dati sono dichiarati nello stesso modo in cui vengono dichiarate le variabili I metodi sono funzioni dichiarate dentro il corpo della classe Detti anche funzioni membro Instanziazione L’operazione di creazione di un oggetto di una certa classe è detta instanziazione Un oggetto è instanza di una classe Occupa uno spazio in memoria Due oggetti della stessa classe differiscono per lo stato ma hanno gli stessi metodi Quando si dichiara una variabile del tipo di una classe, viene istanziata Inizializzazione di una classe Per inizializzare lo stato di una classe, si utilizzano speciali funzioni membro di inizializzazione: i costruttori Un costruttore è una funzione membro con lo stesso nome della classe e senza tipo di ritorno È frequente sovraccaricare il costruttore La maggior parte delle classi ha un costruttore di default, senza parametri Un costruttore può anche non essere public Esempio: costruttori class Counter { int count; public: Counter() { count = 0; } Counter(int inizio) { count = inizio; } }; Costruttore di default Un costruttore di default è un costruttore che può essere invocato senza parametri Quando si scrive Account::Account() { /* … */ } Stack::Stack(int size = 0) { /* … */ } Complex::Complex(double r = 0.0, double i = 0.0) { /* … */ } Account a; … viene chiamato il costruttore di default, se esiste ed è pubblico Se esiste un costruttore di default pubblico, OK Se il costruttore di default non è pubblico, errore di compilazione Se non esiste alcun costruttore, OK I membri non sono inizializzati, hanno il loro vaore di dafault Se esistono solo costruttori non di default (necessitano di parametri), errore di compilazione Distruttore di una classe Il distruttore è una speciale funzione membro che viene invocata automaticamente quando l’oggetto non è più visibile o viene invocato delete sull’oggetto Il costruttore ha il nome della classe preceduto da ~ Il distruttore non restituisce alcun valore e non può avere parametri Non c’è sovraccaricamento Anche se ci sono più costruttori, c’è un solo distruttore Metodi e visibilità Una classe definisce uno scope Una funzione membro sovraccarica solo le funzioni membro della classe Il nome del metodo non è visibile all’esterno della classe Un metodo si invoca come oggetto.metodo( /* … */ ); puntatoreOggetto->metodo( /* … */ ); I metodi di una classe hanno accesso completo ai membri di una classe Ma non a quelli delle altre classi Accesso ai membri Il principio dell’information hiding o incapsulamento dei dati consiste nell’impedire l’accesso diretto all’implementazione interna di una classe Per default tutti gli elementi definiti in una classe sono privati, ovverro visibili solo all’interno della classe La parola chiave public delimita le variabili e metodi pubblici class nomeClasse { int d[300]; top = 0; public: void push(int); int pop(); } Controllo dell’accesso Le parti della dichiarazione della classe contenenti i membri con diversa visibilità sono delimitate dalle etichette Public Un membro pubblico è accessibile da qualunque parte del programma Private Un membro privato è accessibile solo all’interno della classe Protected Esempio: contatore class Counter { int count; public: Counter(void) { count = 0; } int getCount() { return count; } void incrementCounter() { count++; } void reset() { count = 0; } }; int main() { Counter *c = new Counter(); std::cout << c->getCount() << std::endl; c->incrementCounter(); std::cout << c->getCount() << std::endl; c->incrementCounter(); std::cout << c->getCount() << std::endl; c->reset(); std::cout << c->getCount() << std::endl; system("pause"); return 0; } Interfaccia e implementazione I membri pubblici di una classe ne costituiscono l’interfaccia Definiscono dati e funzionalità visibili dagli utilizzatori della classe Le definizioni dei membri ed i membri privati ne costituiscono l’implementazione Puntatore this Le funzioni membro di una classe hanno un parametro implicito Puntatore this, punta all’istanza dell’oggetto Il compilatore trasforma le funzioni membro in sottoprogrammi a cui viene passato un parametro in più Membri statici Un membro statico appartiene alla classe, non ad una specifica istanza È accessibile da tutti gli oggetti della classe Ne esiste una sola istanza per tutta la classe È comunque nel namespace della classe Rimane l’information hiding Un membro static può essere anche private Un metodo static non ha come parametro implicito in puntatore this Esempio: contatore static class StaticCounter { static int count; public: static int getCount() { return count; } static void incrementCounter() { count++; } static void reset() { count = 0; } void f() { count++; } }; Funzioni friend La parola chiave friend identifica una funzione che ha accesso ai membri privati di una classe Nella sezione public, si mette il prototipo preceduto dalla parola chiave friend Una funzione friend non è una funzione membro Non ha il parametro implicito this Ha visibilità anche fuori dalla classe Una funzione può essere friend di più di una classe Esempio: friend class Matrix { int m; int n; double *data; public: friend Vector *prod(Matrix*, Vector*); } class Vector { int n; double *data; public: Vector(int n) : n(n) { data = new double[n]; } friend Vector *prod(Matrix*, Vector*); }; Vector *prod(Matrix *m, Vector *v) { Vector *result = new Vector(v->n); for (int i = 0; i < m->m; i++) { result->data[i] = 0; for (int j = 0; j < m->n; j++) result->data[i] += m->data[i, j] * v->data[j]; } return result; } Metodi const Un metodo dichiarato const non può modificare lo stato dell’oggetto int getCount() const { return count; } Costruttori e distruttori non possono essere const! Se un metodo non dovrebbe aver bisogno di modificare lo stato, è buona norma dichiararlo const salvo rimuovere il const se consapevolmente e a ragion veduta vogliamo che possa modificare lo stato Classi annidate Si può inserire la dichiarazione di una classe dentro la dichiarazione di un’altra classe Una classe annidata è membro della classe La sua visibilità può essere public, private, protected Il nome della classe è nel campo d’azione della classe che la racchiude Esempio: classi annidate class Node {/*...*/}; class Tree { public: // Node è incapsulata nello scope di Tree // nel campo di azione della classe, Tree::Node // nasconde ::Node class Node {/*...*/}; Node *tree; }; //Tree::Node non è visibile nello scope globale //Node si risolve nella dichiarazione globale di Node Node *pNode; class List { public: // Node è incapsulato nello scope di List // nel campo di azione della classe, List::Node // maschera ::Node class Node {/*...*/}; Node *List; }; Risoluzione dei nomi in classi annidate Per risolvere un nome usato in una classe annidata, si considerano Prima, i nomi dichiarati nella classe annidata Poi, i nomi definiti nella classe che la racchiude Infine, i nomi nel namespace che racchiude la classe che la racchiude Una classe può essere definita dentro un’altra classe, in un namespace… Classi locali Una classe può essere definita anche nel corpo di una funzione Classe locale Visibile solamente nel campo d’azione globale dove è definita Le funzioni membro vanno definite direttamente dentro la classe Una classe locale non può dichiarare membri statici Oggetti passati come parametro Un oggetto può essere usato come tipo di un parametro Gli oggetti vengono passati per valore Le modifiche all’oggetto non sono visibili fuori dalla funzione Possibili problemi a causa di effetti collaterali Es. un oggetto che alloca memoria dinamica per poi rilasciarla; la sua copia rilascia la medesima memoria, danneggiando l’oggetto principale Esempio: oggetto come parametro #include <iostream> #include <stdlib> using namespace std; class myclass { int *p; public: myclass (int i); ~myclass(); int getval() {return *p;} }; myclass::myclass(int i) { cout << “Allocazione di p\n”; p = new int; if (!p) { cout << “Impossibile allocare”; exit(1); } *p=i; } myclass::~myclass() { cout << “Rilascio di p\n”; delete p; } // questo provocherà un problema void display(myclass ob) { cout << ob.getval() << ‘\n’; } main() { myclass a(10); display(a); return 0; } Oggetti passati come parametro Gli oggetti passati come parametro sono normalmente passati tramite puntatori o riferimenti // questo non provocherà problemi void display(myclass& ob) { cout << ob.getval() << ‘\n’; } Oggetti come tipi di ritorno Quando il tipo di ritorno di una funzione è un oggetto, viene creato automaticamente un oggetto temporaneo Dopo che l’oggetto viene restituito, viene distrutto Altro caso in cui la distruzione può provocare effetti collaterali indesiderati: se rilascia le risorse, l’oggetto restituito non è più utilizzabile Esempio: funzione che restituisce oggetto class CharBuffer { char *s; public: CharBuffer(void) { s = '\0'; } ~CharBuffer(void) { if (s) delete(s); std::cout << "rilascio di s" << std::endl; } void show() { std::cout << s << std::endl; } }; void set(char *str) { s = new char[strlen(str)+1]; if (!s) { std::cout << "Impossibile allocare" << std::endl; exit(1); } strcpy(s, str); } CharBuffer input() { char instr[80]; CharBuffer result; std::cout << "Inserire una stringa: "; std::cin >> instr; result.set(instr); return result; } int main() { CharBuffer ob; ob=input(); // questo causa un errore ob.show(); return 0; } Funzione che restituisce oggetto: soluzioni Restituire un puntatore o un riferimento Usare un costruttore di copia Usare un operatore di assegnamento soggetto a overloading Costruttore di copia È sempre possibile copiare un oggetto CharBuffer c = d; La copia di un oggetto viene realizzata copiando ogni membro Non sempre la copia bit per bit è adeguata Si può definire un tipo particolare di costruttore (soggetto a overloading) CharBuffer::CharBuffer(const CharBuffer& c) Esempio: costruttore di copia class CharBuffer { char *s; bool copy; public: CharBuffer(void) { s = 0; copy = false; } CharBuffer(const CharBuffer& c) { if (c.s) { s = new char[strlen(c.s)+1]; if (!s) { std::cout << "Impossibile allocare" << std::endl; exit(1); } strcpy(s, c.s); } else s = 0; copy = true; } ~CharBuffer(void) { if (!copy) { if (s) delete(s); std::cout << "rilascio di s" << std::endl; } else copy = false; } void show() { std::cout << s << std::endl; } void set(char *str) { s = new char[strlen(str)+1]; if (!s) { std::cout << "Impossibile allocare" << std::endl; exit(1); } strcpy(s, str); } }; Union Una union è un particolare tipo di classe in cui i dati vengono memorizzati sovrapposti L’occupazione di memoria è pari a quella del membro più grande È possibile assegnare un valore a solo un membro alla volta Esempio: union union PackedNumber { int i; double d; public: PackedNumber(int i) : i(i) { } PackedNumber(double d) : d(d) { } int getInt() { return i; } double getDouble() { return d; } };