Motivazioni • Sintassi del linguaggio • Pointers e References • Classi
Transcript
Motivazioni • Sintassi del linguaggio • Pointers e References • Classi
C++ Fabrizio Bianchi - Universita’ di Torino Outline: • Motivazioni • Sintassi del linguaggio • Pointers e References • Classi • Classi Astratte, Inheritance, Polymorphism Motivazioni • Problema: Evoluzione e manutenzione del software. • Un programma originariamente semplice puo’ divenire orrendamente complicato quando cambiano le specifiche. • Un esempio: copiare dei caratteri da tastiera a stampante. SUBROUTINE COPY EXTERNAL READKB LOGICAL READKB EXTERNAL WRTPRN CHARACTER C DO WHILE (READKB(C)) CALL WRTPRN(C) ENDDO RETURN Modifica dei requisiti A seguito di nuove richieste degli utenti, vogliamo poter scrivere anche su un file: SUBROUTINE COPY(FLAG) EXTERNAL READKB LOGICAL READKB EXTERNAL WRTPRN, WRTFL INTEGER FLAG CHARACTER C DO WHILE (READKB(C)) IF (FLAG .EQ. 1) CALL WRTPRN(C) ELSE CALL WRTFL(C) ENDIF ENDDO Se vogliamo anche poter leggere da file: 10 SUBROUTINE COPY(FLAG1, FLAG2) EXTERNAL READKB, READFL EXTERNAL WRTPRN, WRTFL LOGICAL READKB, READFL INTEGER FLAG1, FLAG2 LOGICAL CONT CHARACTER C IF (FLAG1 .EQ. 1) THEN CONT = READKB(C) ELSE CONT = READFL(C) ENDIF IF (CONT) THEN IF (FLAG2 .EQ. 1) THEN CALL WRTPRN(C) ELSE CALL WRTFL(C) ENDIF GOTO 10 ARRRRGGGGHHHHH ENDIF RETURN !!!!! Cambiamenti nel formato dei Dati Supponiamo di avere: COMMON /MYDATA/ P1(4), P2(4), + P3(4), P4(4) REAL P1(4), P2(4), P3(4), P4(4) COSTHETA12 = COSTHETA(P1, P2) COSTHETA13 = COSTHETA(P1, P3) COSTHETA14 = COSTHETA(P1, P4) FUNCTION COSTHETA(P1, P2) REAL P1(4), P2(4) COSTHETA = (P1(1)*P2(1) + P1(2)*P2(2) + + P1(3)*P2(3))/... END Cosa succede se cambia il formato dei dati nel common block ? COMMON /MYDATA/ P(4), E(4), THETA(4), PHI(4) Occorre cambiare la function: FUNCTION COSTHETA1(THETA1, THETA2, + PHI1, PHI2) COSTHETA1 = SIN(THETA1)*SIN(THETA2) * + COS(PHI1-PHI2) + COS(THETA1)*COS(THETA2) END ed anche il codice !: COMMON /MYDATA/ P(4), E(4), + THETA(4), PHI(4) COSTHETA12 = COSTHETA1(THETA(1),THETA(2), + PHI(1), PHI(2)) COSTHETA13 = COSTHETA1(THETA(1),THETA(3), + PHI(1), PHI(3)) COSTHETA14 = COSTHETA1(THETA(1),THETA(4), + PHI(1), PHI(4)) Nell’esempio precedente il codice di analisi ("alto livello") dipende dai dettagli della struttura dati ("basso livello"). Un cambiamento della struttura dei dati si propaga in modo incontrollato. La programmazione “object oriented” utilizzando ad es. il linguaggio C++: • Riduce la dipendenza del codice di alto livello dalla rappresentazione dei dati • Permette il riutilizzo del codice di alto livello • Nasconde i dettagli di implementazione • Supporta tipi di dati astratti Un Semplice Programma: #include <iostream.h> int main(){ //a simple example cout << “ Hello world“ << endl; return 0; } • iostream.h: direttiva per il preprocessore: header file con dichiarazioni per I/O • linee di commenti iniziano con // • cout << : stampa su standard output (schermo) • endl : end of line • il formato e’ libero, le istruzioni terminano con ; Compilato con: c++ myprogram.cc -o test Un altro esempio: #include <iostream.h> int main(){ //read and print 3 floating point numbers float a, b, c; cin >> a >> b >> c; cout << a << “, “ << b << “, “ << c << endl; return 0; } • a, b, c sono variabili di tipo float (Real*4). La dichiarazione del tipo e’ obbligatoria • cin >> legge da standard input (tastiera • il c++ e’ case sensitive ! Tipi predefiniti in C++ • • • • • • • • char bool int short long float doble long double 16 bit 8 16 16 16 32 32 64 64 32 bit 8 32 32 16 32 32 64 128 64 bit 8 32 32 16 64 32 64 128 Operatori #include <iostream.h> int main(){ int i=1; cout << i << “, “; cout << (++i) << “, “; cout << i << “, “; cout << (i++) << “, “; cout << i << endl; return 0; } L’output e’: 1, 2, 2, 2, 3 ++i : operatore di preincremento i++: operatore di postincremento Analogamente --i e i-- Operatori di Assegnazione Fortran x = y x = x + x = x x = x * x = x / y y y y float x=1.0; float y=2.0, z=3.0; float sum =x; sum +=y; sum +=z; x x x x x C++ = y += y -= y *= y /= y Precedenza degli Operatori Valgono le regole delle espressioni algebriche: z = a*x + b*y +c*z; esegue prima le moltiplicazioni e poi le addizioni. Le parentesi hanno la precedenza sul comportamento default In caso di dubbio, usare le parentesi. Le parentesi rendono il codice piu’ leggibile. Controllo di Flusso Fortran C++ do i = 1, 10 . . . . . . . . . enddo for(i =1; i<=10; i++) { . . . . . . . . } if(i.eq.10.and.j.gt.4.or.x)then . . . . . . . . endif if(i==10 && j>4 || x) { . . . . . . . . . . } do while (i .ne. 5) . . . . . . . . . enddo while ( i !=5) { . . . . . . . . } Operatori di Relazione Fortran x .lt. x .le. x .gt. x .ge. x .eq. x .ne. x .and. x .or. y y y y y y y y x x x x x x x x C++ < y <= y > y >= y == y != y && y || y for LOOP for (init-statement; test-expr; increm-expr){ ............. } Tipicamente: for (int i=0; i< count; i++){ ............. } Nested Loops per iterare su tutte le coppie: for (int i=0; i< count-1; i++){ for (int j=i+1; j< count; j++){ ............. } } if Statement if(current_temp > maximum_safe_temp) { cout << “Emergency: Too hot -- flushing “ << endl; flushWithWater(); } if (x < 0) x = -x; y = -y; // abs(x) // e’ sempre eseguita y = abs(x) if (x < 0) { y = -x; } else { y = x; } if (x < 0) { y = -x; } else if (x > 0) { y = x; } else { y = 0; } int i, j; // codice che calcola i e j if(i=j){ // fa qualcosa } Questo frammento di codice assegna ad i il valore di j e se j non e’ zero fa qualcosa. Forse volevate: if(i==j) && e || sono valutati da sinistra a destra e solo se necessario: questo frammento non divide mai per zero: if (d && (x/d <10.) ) { // fa qualcosa } while Example # include <iostream.h> # include <iomanip.h> # include <math.h> int main(){ float x; while (cin >> x) { cout << x << sqrt(x) << endl; } return 0; } legge fino all’ end-of-file <ctrl>-d e’ end-of-file (da tastiera) Funzioni Matematiche # include <iostream.h> # include <math.h> int main(){ double r, theta, phi; cin >> r >> theta >> phi ; double x = r * sin( theta ) * sin( phi ); double y = r * sin( theta ) * cos( phi ); double z = r * cos( theta ); cout << x << ", " << y << ", "<< z << endl; return 0; } math.h e’ l’header file per le funzioni matematiche Non esiste x**4. Usare pow(x,4) Variabili Locali e Globali include <iostream.h> const float pi = 3.1415396; int main() { int j = 0; if ( j == 0 ) { int k = 5; } j = k; // errore: non compila ! return 0; } pi e’ una variabile globale, j e k sono variabili locali. Le variabili locali esistono solo entro un certo scope. int main () { float temp =1.1; int a, b; cin >> a >> b; if (a<b){ float temp =10.0; cout << “ temp = “ << temp << endl; } else{ float temp = 20.0; cout << “ temp = “ << temp << endl; } cout << “ temp fuori = “ << temp << endl; return 0; } Enumeratori enum Color { red, green, blue }; Color screenColor = blue; Color windowColor = red; int n = blue; // valido Color c = 1; // errore Array int x[10]; for ( int i = 0; i < 10, i++ ){ x[i] = 0;} x[10] e’ un array a dimensione fissa. x[0] e x[9] sono il primo e l’ultimo elemento. int x[] = { 1, 2, 3, 4} inizializza gli elementi dell’array x. la dimensione dell’array viene calcolata dal compilatore. double m[5][5]; for ( int i = 0; i < 5; i++ ) { for ( int j = 0; j < 5; j++ ) { m[i][j] = i * j; m[5][5] e’ un array a due dimensioni int a[2][3] = { {1,2,3}, {4,5,6} } gli elementi appaiono “per riga” In fortran appaiono per colonna L’elemento a[0][1] in C++ corrisponde all’elemento A(2,1) in Fortran. Che dolore interfacciare C++ e Fortran ! Prodotto di 2 matrici: float m[3][3], m1[3][3], m2[3][3]; // codice che inizializza m1 ed m2 double sum; for (int i=0; i<3; i++){ for (int j=0; j<3; j++){ sum =0.0; for (int k=0; k<3; k++){ sum += m1[i][k] * m2[k][j]; } m[i][j] = sum; } } Pointers I pointers si riferiscono ad una locazione di memoria. Esempio: #include <iostream.h> int main(){ int j = 12; int *ptr = &j; cout << *ptr << endl; cout << ptr << endl; j = 24; cout << *ptr << endl; cout << ptr << endl; return 0; } Scrive: 12 0x7b03a928 24 0x7b03a928 indirizzo di memoria ptr e’ un puntatore ad un intero. NON e’ una copia dell’intero *ptr e’ cio’ a cui ptr punta (il contenuto della locazione di memoria ptr). Si dice che *ptr dereferenzia il pointer per accedere all’oggetto int *ptr = &j dichiara ptr essere un puntatore ad una variabile di tipo intero e gli assegna come valore l’indirizzo dell’intero j Dereferenziare un puntatore nullo causa un core dump. #include <iostream.h> int main() { int j = 12; int *ptr = 0; cout << *ptr << endl; // crash ! return 0; } Pointers e Array int main(){ float x[5]; for (int j = 0; j < 5; j++) { x[j] = 0;} float *ptr = x; // ptr punta ad x[0] *ptr = 1.5; // x[0] = 1.5 *(ptr+1) = 2.5; // x[1] = 2.5 *(ptr+3) = 3.5; // x[3] = 3.5 } x[0] x[1] x[2] x[3] x[4] 1.5 2.5 0.0 3.5 0.0 ptr ptr+1 ptr+2 ptr+3 ptr+4 x e’ un puntatore al primo elemento dell’Array *x ed x[0] sono lo stesso oggetto x e &x[0] sono lo stesso puntatore ptr+1 e’ un puntatore che punta alla locazione di memoria successiva a ptr *(x+1) ed x[1] sono lo stesso oggetto Un possibile errore: float x[5] float* y, z; y=x; // ok: y e’ un puntatore z=x; // errato: z e’ un float e non un float* Due modi di sommare gli elementi di una matrice: float x[5]; //codice che riempie x double sum=0.0; for (int i=0; i<5; i++) { sum += x[i]; } float x[5]; //codice che riempie x float *y = x; double sum=0.0; for (int i=0; i<5; i++) { sum += *y++; } Un modo difficile (ma istruttivo) di azzerare un array: float x[4]; float* p = &x[4]; while (p !=x) *(--p) = 0.0; x[0] x[1] x[2] x[3] p-4 p-3 p-2 p-1 p p e’ un puntatore che punta alla locazione di memoria successiva all’ultimo elemento dell’array *(--p) = 0.0 prima diminuisce di 1 il puntatore e poi azzera il contenuto della locazione di memoria corrispondente Invertire gli elementi di una matrice: float x[6]; // code to initialize x float * left = &x[0]; float * right = &x[5]; while (left < right) { float temp = *left; *left++ = *right; *right-- = temp; } x[0] x[1] left left+1 x[2] x[3] . . . x[4] x[5] right-1 right Puntatori: allocazione dinamica #include <iostream.h> int main(){ int *ptr = new int; *ptr = 12; cout << *ptr << endl; delete ptr; return 0; } Attenzione: Non usare delete fa accumulare locazioni di memoria inutilizzate (memory leak) Utilizzare puntatori prima del new o dopo il delete causa il crash del programma Riferimento a più locazioni di memoria: #include <iostream.h> int main() { int *ptr = new int[3]; ptr[0] = 10; ptr[1] = 11; ptr[2] = 12; delete [] ptr; return 0; } References float x = 12.1; float& a = x; float &b = x; a, b sono references: condividono la stessa locazione di memoria. La posizione di & e’ opzionale. Non fate confusione tra reference e pointer: int i =3; int &j = i; int *p = &i; // l’oggetto i // reference ad i // pointer ad i i ha un indirizzo di memoria che contiene 3 j ha lo stesso indirizzo di memoria di i p e’ l’indirizzo di memoria di i Funzioni dichiarazione ed implementazione: double coulombsLaw(double q1, double q2, double r) { double k = 8.9875e9; return k * q1 * q2 / (r * r); } uso: int main() { cout << coulombsLaw(1.6e-19, 1.6e-19, 5.3e-11) << “ Newtons” << endl; return 0; } Una funzione e’ identificata dal suo nome e dalla lista degli argomenti. Possono coesistere funzioni con lo stesso nome ed argomenti diversi (function name overloading) Normalmente un programma e’ suddiviso in piu’ files che vengono compilati separatamente. Se la funzione viene usata in un file diverso da quello in cui viene dichiarata: extern double coulombsLaw(double q1, double q2, double r); int main() { cout << coulombsLaw(1.6e-19, 1.6e-19, 5.3e-11) << “ Newtons” << endl; return 0; } extern dice che la funzione e’ esterna e deve essere inclusa in fase di link. Problema: la dichiarazione della funzione deve essere la stessa in tutti i files che la usano. Soluzione: un’unica dichiarazione. Esempio: in math.h ci sono le dichiarazioni: extern double sqrt(double); extern double sin(double); extern double cos(double); // e molte altre L’implementazione e’ in un file math.c: #include <math.h> double sqrt(double x) { // implementazione dell’algoritmo che calcola sqrt return result; } L’ #include <math.h> in math.c e nel codice cliente assicura la consistenza delle dichiarazioni. math.c e’ gia’ compilato e sta in una libreria che il compilatore invoca automaticamente in fase di linking. Normalmente un programma e’ suddiviso in files con estensione .h (oppure .hh) che contengono le dichiarazioni ( gli header files) ed in files con estensione .c (oppure .cc) che cointengono le implementazioni. Piu’files possono essere compilati ed archiviati in una libreria. Per evitare di includere piu’ volte lo stesso header file: #ifndef COULOMBSLAW_H #define COULOMBSLAW_H extern double coulombsLaw(double q1, double q2, double r); #endif Se COULOMBSLAW_H non e’ definito il preprocessore esegue le istruzioni fino all’ #endif. #define COULOMBSLAW_H definisce COULOMBSLAW_H e fa si che #ifndef COULOMBSLAW_H possa essere soddisfatto una volta sola E’ possibile definire un valore di default per tutti od alcuni argomenti di una funzione: #include “myfunction.h” extern double myfunction( double x, double y=0.); Puo’ essere usata: double z = myfunction(r); // usa il valore di default double w = myfunction(r, s); // usa s come secondo argomento Tutti gli argomenti a destra del primo argomento di default devono avere un default. Funzioni semplici (tempo di esecuzione < tempo di chiamata) possono essere dichiarate inline: inline double sqr(double x) { return x*x; } Argomenti delle funzioni la funzione: void f(int i, float x, float *a){ i = 100; x = 101.0; a[0] = 0.0; } viene usata in questo frammento di codice: int j=1; int k=2; float y[] = {3.0, 4.0, 5.0} f(j, k, y); cout << “ j= “ << j << “ k= “ << k << “ y[0]= “ << y[0] << endl; Scrive j= 1 k= 2 y[0]= 0.0 • Il c++ passa gli argomenti “by value”: • all’interno della funzione si crea una “copia” degli argomenti. • j e k non vengono cambiati. • y[0] viene cambiato perche’ la funzione si crea una copia del puntatore e poi modifica il contenuto della locazione di memoria indirizzata dal puntatore. Uso delle references come argomenti: Dichiariamo la funzione: void swap(int& i1, int& i2){ int temp = i1; i1 = i2; i2 = temp; } Usata in: int c = 3; int d = 4; cout << “ c = “ << c << “ d = “ << d << endl; // c = 3 d = 4 swap (c, d); cout << “ c = “ << c << “ d = “ << d << endl; // c = 4 d = 3 c e d vengono cambiati perche’ gli argomenti di swap sono references e quindi c e d creati all’interno della funzione condividono la stessa locazione di memoria dei c e d del codice cliente. E’ possibile definire una funzione in modo ricorsivo: int Stirling(int n, int k) { if (n < k) return 0; if (k == 0 && n > 0) return 0; if (n == k) return 1; return k * Stirling(n-1, k) + Stirling(n-1, k-1); } Esercizio: implementare una funzione fattoriale // fit ad una retta #include <iostream.h> void linefit() { // create arrays with desired numbers of elements int n; cin >> n; float * x = new float[n]; float * y = new float[n]; // read data points for (int i=0; i<n; i++) { cin >> x[i] >> y[i]; } // accumulate sums double sx = 0.0; double sy = 0.0; for (i=0; i<n; i++) { sx += x[i]; sy += y[i]; } // compute coefficients double sx_over_n = sx/n; double stt = 0.0; double b =0.0; for (i=0; i<n; i++) { double ti = x[i] - sx_over_n; stt += ti * ti; b += ti * y[i]; } b/=stt; double a = (sy -sx *b )/n; delete [] x; delete [] y; cout << a << “, “ << b; } int main() { linefit(); return 0 } Stringhe di Caratteri Sono casi speciali di array: char hello1[] = { ‘H’, ‘i’ }; o piu’ semplicemente: char hello2[] = “Hi”; N. B. : la dimensione di hello2 e’ 3 : ‘H’, ‘i’, ‘\0’ Se non ci credete: char hello2[] = “Hi”; int n=0; for (char *p = hello2; *p !=0; p++) { n++; } cout << ‘ numero caratteri: ‘ << n << endl; Oggetti const Le variabili const non possono essere cambiate: int i = 3; i=4; //ok const float e = 2.71828; e = 3; // non compila ! Un argomento dichiarato const non puo’ essere cambiato dalla funzione: void f(int& i, float& x, const float *a) { i=100; x=10*a[0]; a[0] = 0.; // non compila ! } int j = 1; float k = 2.; float y[] = {1., 2., 3., 4.} f(j,k,y); Caccia all’errore: const float a =1.2; float b =3.4; const float *p = &a; // ok: p e’ un ptr ad un const float float* const d = &a; // no: d e’ un ptr const ad un float non // const float* const q = &b; // ok: q e’ const ptr a float non const const float* const r = &a; // ok: r e’ const ptr a const // float *p = 3.; // no: p e’ ptr a const float p = &b; // ok: p non e’ const e quindi posso cambiarlo *p = 3.; // no: ho cambiato p, ma e’ sempre un ptr ad un // const float che quindi non posso cambiare *q = 3.; // ok: cio’ a cui q punta non e’ const q = &a; // no: q e’ const ptr e quindi non posso cambiarlo *r = 3.0;// no: r e’ const ptr a const float r = &b; // no: r e’ const ptr a const float Classi ed oggetti In un esperimento di fisica non si ha solo a che fare con float, integer, double, ma anche con: • • • • • • vettori superfici tracce elementi di rivelatori particelle vertici di decadimento Le classi permettono di definire dei nuovi tipi e le operazioni su di essi. Esempio: una classe Punto una classe per descrivere un punto in due dimensioni Nell’ header file (Point.hh) c’e la dichiarazione della classe: //header file for class Point class Point { public: Point (); // default constructor Point (float x, float y); // constructor da 2 float float distance (Point b); // distanza da due punti float getX(); // accessor for _x data member float getY(); // accessor for _y data member private: float _x; // data member: x float _y; // data member: y }; Notare il ; L’implementazione e’ nel file Point.cc // implementation of class Point #include <math.h> #include “Point.hh” Point:: Point(float x, float y){ _x = x; _y = y; } Point::Point(){} float Point::getX() {return _x;} float Point::getY() {return _y;} float Point::distance (Point b){ float difx = _x - b.getX(); float dify = _y - b.getY(); return sqrt(difx*difx + dify*dify); } Uso della classe Point: #include <iostream.h> #include “Point.hh” int main() { // legge le coordinate di un punto e ne calcola la distanza // dall’origine // invoca il costruttore da due float Point origin(0.,0.); float x; cin >> x; float y; cin >> y; // invoca il costruttore da due float Point p(x,y); // invoca la function distance del point origin per // calcolare distanza al punto p float dist = origin.distance(p) cout << dist << endl; return 0; } Gli attributi privati sono accessibili solo all’interno della classe. Solo i metodi pubblici sono visibili all’esterno. //header file for class Point class Point { public: Point (); // default constructor Point (float x, float y); // constructor da 2 float float distance (Point b); // distanza da due punti float getX(); // accessor for _x data member float getY(); // accessor for _y data member private: float _x; // data member: x float _y; // data member: y }; Interfaccia e implementazione La struttura interna dei dati (_x, _y) che rappresentano l’oggetto della classe Point e’ nascosta (private) ai client della classe. I client non dipendono dalla struttura interna dei dati (come lo erano i client dei common block Fortran). Se la struttura interna cambia (es.: _r, _theta), il codice che usa Point non deve essere modificato. Non e’ possibile accedere direttamente ai dati ! #include <iostream.h> #include "Point.hh" int main() { Non Compila ! Point p(1., 1.); cout << " x = " << p._x; cout << " y = " << p._y << endl; cout << " x = " << p.getX(); cout << " y = " << p.getY() << endl; } Compila Introduciamo una nuova classe: Line retta in uno spazio a due dimensioni, rappresentata come: ax + by + c = 0 //header file for class Line class Point; // forward declaration della classe Point class Line { public: Line (); // default constructor Line (Point p1, Point p2); // constructor da 2 Point float distance (Point p); // distanza ad un punto Point intersection (Line l2); // intersezione fra due // rette static float pallelism_tolerance; private: float _a; // data member: a float _b; // data member: b float _c; // data member: c }; // implementation of class Line #include <math.h> #include <float.h> #include “Point.hh” #include “Line.hh” Line::Line(Point p1, Point p2){ // construct a line through points p1 and p2 if (p1.getX() == p2.getX()){ //vertical line _a=1; _b=0; _c=-p1.getX(); } else { _a = p2.getY() - p1.getY(); _b = p1.getX() - p2.getY(); _c = p1.getY() * p2.getX() - p2.getY() * p1.getX(); } } float Line::distance (Point p){ // returns distance from point to line returns abs( _a*p.getX() + _b*p.getY() + _c) / sqrt(_a*_a +_b*_b); } Point Line::intersect(Line l2) { // intersection with a line. if the angle between the lines // is less then the parallelism tolerance return infinity float det = _a*l2._b - l2._a *_b; float sinsq = (det * det) / ( (_a*_a +_b*_b) * (l2._a*l2._a + l2._b*l2._b) ); if (sinsq < parallelism_tolerance*parallelism_tolerance){ return Point (FLT_MAX, FLT_MAX); // point at infinity, // FLT_MAX from float.h } else { return Point( ( _b * l2._c - l2._b * _c) / det, ( _c * l2._a - l2._c * _a) / det ); } } Float Line::parallelism_tolerance = 0.01745; // 1 degree // esempio uso classi Point e Line #include <iostream.h> #include “Point.hh” #include “Line.hh” int main() { // create a vertical line through origin Line l1(Point (0.,0.), Point(0.,1.)); // create horizontal line Line l2(point (1.,1.), Point(2.,1.)); // compute intersection Point intersection = l1.intersect(l2); cout << “ intersection = ( “ << intersection.getX() << , << intersection.getY() << “ ) “ << endl; return 0; } Una classe vettore (in 3D) // header file: Vector.h #ifndef VECTOR_H #define VECTOR_H class Vector { public: Vector(double x, double y, double z) // costruttore Vector(Vector &); // copy constructor ~Vector(); // destructor double x(); // ritorna componente x double y(); // ritorna componente y double z(); // ritorna componente z double r(); // ritorna modulo double phi(); // ritorna angolo phi double theta(); // ritorna angolo theta private: double x_, y_, z_; }; #endif Il file di implementazione: Vector.cc Vector::Vector(double x, double y, double z) : x_(x), y_(y), z_(z) {} Vector::Hep3Vector(const Vector & p) : x_(p.x()), y_(p.y()), z_(p.z()) {} Vector::~Vector() {} double { return } double { return } Vector::x() x_; Vector::r() sqrt( x_*x_ + y_*y_ + z_*z_); Oggetti Costanti, Selettori e Modificatori const Vector v(1, 0, 0); e’ un vettore il cui stato non puo’ essere cambiato class Vector { public: Vector(double x, double y, double z); Vector(const Vector &); double x() const; double y() const; double z() const; selettori = double r() const; double phi() const; double theta() const; void scale(double s); private: double x_, y_, z_; }; non cambiano gli attributi modificatori = cambiano gli attributi gli attributi di un oggetto const non possono essere modificati dopo la sua creazione int main() { const Vector v(1, 0, 0); double r = v.r() // OK v.scale( 1.1 ); // errore! segnalato dal compilatore } Argomenti delle funzioni Un argomento può essere passato come valore ( la funzione ne crea una copia) o come riferimento ( o puntatore: la funzione accede all’unica copia dell’oggetto esistente in memoria) Passare due oggetti della classe Vector come valore è dispendioso double dotProduct( Vector v1, Vector v2 ) { return v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z(); } Passare solo un riferimento (o un puntatore) agli oggetti è più efficiente. Inoltre, è l’unico modo per poter modificare v1 e v2. double dotProduct( Vector& v1, Vector& v2 ) { return v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z(); } Per evitare di modificare un argomento passato come riferimento, si può dichiararlo const double dotProduct( const Vector& v1, const Vector& v2 ) { return v1.x() * v2.x() + v1.y() * v2.y() + v1.z() * v2.z(); } Operatori E’ possibile ridefinire +, -, *, [], ++, ==, . . . Dichiarazione: class Vector { public: Vector(double x, double y, double z); double x() const; double r() const; Vector & operator = (const Vector &); bool operator == (const Vector &) const; bool operator != (const Vector &) const; Vector & operator + (const Vector&) const; Vector & operator - (const Vector&) const; Vector & operator *= (double); ostream & operator << (ostream &, const Vector &); private: double x_, y_, z_; } Implementazione: Vector & Vector::operator = (const Vector & p) { x_ = p.x(); y_ = p.y(); z_ = p.z(); return *this; } bool Vector::operator == (const Vector& v) const { return (v.x()==x() && v.y()==y() && v.z()==z()); } bool Vector::operator != (const Vector& v) const { return (v.x()!=x() || v.y()!=y() || v.z()!=z()); } Vector Vector::operator + (const Vector& v) const { return Vector(x_ + v.x_, y_ + v.y_, z_ + v.z_); } Vector Vector::operator - (const Vector& v) const { return Vector(x_ - v.x_, y_ - v.y_, z_ - v.z_); } Vector& Vector::operator *= (double a) { x_ *= a; y_ *= a; z_ *= a; return *this; } ostream & operator << (ostream & s, const Vector & q) { return s << "(" << q.x() << "," << q.y() << "," << q.z() << ")"; } Uso: #include <iostream.h> #include "Vector.h" int main() { Vector v1(1, 0, Vector v; v = v1 + v2; cout << " v = " cout << " r = " cout << " theta } v.operator=(v1.operator+(v2)); 0), v2(0, 1, 0); << v << endl; << v.r(); = " << v.theta() << endl; ridefinizione di << Output: v = (1, 1, 0) r = 1.4141 theta = 1.5708 Copy Constructor vs Assignment Operator #include <iostream.h> #include "Vector.h" int main() { Vector v1(1, 2, 3); Vector v2 = v1; Vector v3; v3 = v1; return 0; } // invoca il copy constructor // invoca l’operatore = Overloading di operatori Possono esistere funzioni con lo stesso nome ma con argomenti diversi. class Vector { public: // ... Vector operator * (double s) const; double operator * (const Vector& v) const; private: double x_, y_, z_; }; Vector Vector::operator*(double s) const { return Vector( x_ * s, y_ * s, z_ * s ); } double Vector::operator*(const Vector& v) const { return ( x_ * v.x_ + y_ * v.y_ + z_ * v.z_ ); } Argomenti di default E’ possibile specificare il default per gli argomenti delle funzioni Vector.h class Vector { public: Vector(double x = 0, double y = 0, double z = 0); . . . }; Vector.cc Vector::Vector(double x, double y, double z) : x_(x), y_(y), z_(z) { } main.cc #include <iostream.h> #include "Vector.h" int main() { Vector v(); cout << "v = " << v << endl; } Output: V = (0, 0, 0) Funzioni e dati statici Alcune funzioni e attributi possono essere comuni a tutta la classe e possono venire invocati senza riferimento ad un oggetto della classe Dichiarazione: class ComplexFloat { public: ComplexFloat(float r, float i); // constructor from // real and imaginary part static ComplexFloat fromFile (istream& input); // ..... }; Implementazione: ComplexFloat ComplexFloat::fromFile (istream& input) { float r, i ; input >> r >> i; return ComplexFloat(r, i); }; Uso: ComplexFloat c = ComplexFloat::fromFile(input) una classe particella: Particle.h class Particle { public: Particle(int code, double mass, int charge); int code() const { return code_; } double mass() const { return mass_; } int charge() const { return charge_; } static int numberOfParticles(); private: double mass_; int code_, charge_; static int n_; }; Particle.cc #include "Particle.h" int Particle::n_ = 0; int Particle::numberOfParticles() { return n_; } Particle::Particle(int code, double mass, int charge) : code_(code), mass_(mass), charge_(charge) { n_ ++; } Uso: int nPart = Particle::numberOfParticles() Ereditarieta’ (inheritance) Una classe derivata estende la classe base e ne eredita tutti i metodi e gli attributi Track.h class Track { public: // ....... LorentzVector* momentum() { return p; } protected: // ....... LorentzVector *p; }; DchTrack.h #include "Track.h" class DchTrack : public Track { public: int hits() { return hits_->length(); } DchHit* hit(int n) { return hits_[n]; } protected: list<DchHit> hits_; }; DchTrack è una Track che ha degli attributi in più (hits_) e nuovi metodi (DchHit* hit(int n), int hits()). Gli attributi ed i metodi “protected” sono accessibili dalla classe base e dalle classi derivate. Un possibile problema...e la sua soluzione Un programma di controllo manipola oggetti che rappresentano dei Power Supply modello Acme130, connessi ad un certo indirizzo di un Controller. Abbiamo bisogno di una classe Controller e di una classe Acme130. class Controller { public: Controller (); void insert(const char* deviceName, int adress); void send(int adress, const char* command); void send(int adress, float f); float receive(int adress); private: // ...... non mi interessano i dettagli di Controller class Acme130 { public: Acme130(Controller& aController, int adress); void setVoltage(float volts); double minimumVoltage() const; double maximumVoltage() const; private: controller my_controller; int my_adress; }; // implementazione del costruttore Acme130::Acme130(Controller& aController, int adress){ my_controller(aController); my_adress(adress); my_controller.insert(“Acme130”, adress); } // implementazione della funzione setVoltage void Acme130::setVoltage(float volts){ if(volts > maximumVoltage() || volts < minimumVoltage(){ cout << “ Acme130 Voltage out of Range “ << endl; } else { my_controller.send(my_adress,volts); } } // frammento di codice che usa il tutto Controller aController; // construct aController Acme130 powerSupply(aController, 12); // Acme130 at adress12 powerSupply.setVoltage(3.6); // Set 3.6 Volts Per controllare le tensioni, introduciamo una classe Voltmetro: class Voltmetro{ public: Voltmetro(Controller aController, int adress); float readVoltage(); private: Controller my_controller; int my_adress; } // implementazione readVoltage float Voltmetro::readVoltage(){ return my_controller.receive(my_adress); } Implementiamo una funzione checkCalibration nella classe Controller: float Controller::checkCalibration(Acme130 supply, Voltmetro vm, float tst_voltage) { supply.set(tst_voltage); return abs(tst_voltage - vm.read()) / tst_voltage } Per usare il tutto: Controller aController; // construct aController Acme130 supply1(aController, 12); // supply1 at adress 12 Voltmetro vm(aController, 14); // vm at adress 14 cout << “ Acme130 relative error at 1 volt is: “ << aController.checkCalibration(supply1, vm, 1.0) << endl; Il problema: Introduciamo un nuovo modello di Power Supply: class PS50 { public: PS50(Controller& aController, int adress); void setVoltage(float volts); double minimumVoltage() const; double maximumVoltage() const; private: controller my_controller; int my_adress; }; PS50 supply2(aController, 17); // supply2 at adress 17 Voltmetro vm(aController, 14); // vm at adress 14 cout << “ PS50 relative error at 1 volt is: “ << aController.checkCalibration(supply2, vm, 1.0) << endl; Non compila ! La Soluzione: una classe astratta class PowerSupply { public: virtual void setVoltage(float volts) = 0; virtual double minimumVoltage() const = 0; virtual double maximumVoltage() const = 0; virtual ~PowerSupply(); }; pure virtual function class Acme130 : public PowerSupply { public: Acme130(Controller& aController, int adress); virtual void setVoltage(float volts); virtual double minimumVoltage() const; virtual double maximumVoltage() const; private: controller my_controller; int my_adress; }; checkCalibration diventa: float Controller::checkCalibration(PowerSupply supply, Voltmetro vm, float tst_voltage) { supply.set(tst_voltage); return abs(tst_voltage - vm.read()) / tst_voltage } Il codice cliente: Controller aController; // construct aController Acme130 supply1(aController, 12); // supply1 at adress 12 PS50 supply2(aController, 17); // supply2 at adress 17 Voltmetro vm(aController, 14); // vm at adress 14 cout << “ Acme130 relative error at 1 volt is: “ << aController.checkCalibration(supply1, vm, 1.0) << endl; cout << “ PS50 relative error at 1 volt is: “ << aController.checkCalibration(supply2, vm, 1.0) << endl; Ritornando al primo esempio ... READKBD e READFL possono essere due implementazioni dello stesso metodo virtuale read di una classe InputDevice WRITEKBD e WRITEFL possono essere implementazioni dello stesso metodo virtuale write di una classe OutputDevice void copy(InputDevice& in, OutputDevice& out) { char c; while ( in.read(c) ) out.write(c); } Classi per pacchetto grafico (cerchio, rettangolo,...) Classe rettangolo : file Rectangle.h #ifndef RECTANGLE_H #define RECTANGLE_H #include "Shape.h" class Rectangle : public Shape { public: Rectangle( double lx, double ly ); ~Rectangle(); virtual void draw() const; private: double lx_, ly_; }; File Rectangle.cc #include "Rectangle.h" #include <iostream> Rectangle::Rectangle( double lx, double ly ) : lx_( lx ), ly_( ly ) { cout << "*** costruisco un rettangolo" << endl; } Rectangle::~Rectangle() { cout << "*** distruggo un rettangolo" << endl; } void Rectangle::draw() const { cout << "Disegno un rettangolo di lati " << lx_ << " e " << ly_ << endl; } Esercizio: implementare la classe astratta Shape, da cui deriva la classe concreta Rectangle. Implementare le classi concrete Circle e Triangle come derivate da Shape. Scrivere un main che crea rettangoli, cerchi e triangoli e invoca la funzione draw usando il meccanismo delle classi astratte. Function Template Funzione sqr per calcolare il quadrato di un double: inline double sqr(const double x) { return x * x } Problema: per calcolare il quadrato di n oggetti diversi occorre implementare n funzioni diverse con lo stesso nome. Soluzione: function template: template<class T> inline T sqr(const T x) { return x * x } Uso: int i = 3; double d = 4.; Vector V(1.,1.,1.); cout << “sqr(i) = “ << sqr(i) << endl; cout << “sqr(d) = “ << sqr(d) << endl; cout << “sqr(V) = “ << sqr(V) << endl; Il compilatore genera una funzione con il tipo corretto. Per l’oggetto di tipo T devono essere definiti gli operatori necessari (l’operatore * in questo caso). Class Template Uno stack di interi: occorrono una classe Contenuto ed una classe Stack. Un oggetto Contenuto ha come data member il suo valore (l’intero) ed un pointer all’oggetto di tipo Contenuto che si trova sotto di lui nello Stack class Contenuto { public: Contenuto ( int i, Contenuto* ptn ) { val = i; next = ptn; } int getVal () { return val; } Contenuto* getNext() { return next; ) private: Contenuto* next; int val; }; class Stack{ public: Stack() {top = NULL;} // crea uno stack vuoto ~Stack() {;} // distrugge lo stack // mette un oggetto Contenuto in cima allo Stack void push(int i) { Contenuto* tmp = new Contenuto(i, top); top = tmp; } // restituisce l’oggetto Contenuto in cima allo Stack int pop() { int ret = top->GetVal(); Contenuto* tmp = top; top = top->getNext(); delete tmp return ret; } private: Contenuto* top; }; Uso dello Stack: int main() { Stack s; s.push(10); s.push(20); cout << s.pop() << “ - “ << s.pop() << endl; } Stack s; s.push(10); s.push(20); 20 next NULL NULL top 10 next top NULL 10 next top Vogliamo mettere nello Stack oggetti qualsiasi: occorrono le classi template Contenuto e Stack template <class T> class Contenuto { public: Contenuto ( T i, Contenuto* ptn ) { val = i; next = ptn; } T getVal () { return val; } Contenuto* getNext() { return next; ) private: Contenuto* next; T val; }; template <class T> class Stack{ public: Stack() {top = NULL;} // crea uno stack vuoto ~Stack() {;} // distrugge lo stack // mette un oggetto Contenuto in cima allo Stack void push(T i) { Contenuto<T>* tmp = new Contenuto<T>(i, top); top = tmp; } // restituisce l’oggetto Contenuto in cima allo Stack T pop() { T ret = top->GetVal(); Contenuto<T>* tmp = top; top = top->getNext(); delete tmp return ret; } private: Contenuto<T>* top; }; Per usare una classe template occorre specificare il tipo di oggetti tra < > int main() { Stack<int> s; s.push(10); s.push(20); cout << s.pop() << “ - “ << s.pop() << endl; return 0; } Potrei dichiarare: Stack<float>, Stack<double>, Stack<Point>, Stack<Vector> ... Classi Contenitori: la Standard Template Library E’ una libreria di classi di Contenitori, Iteratori ed Algoritmi. Un Contenitore e’ un oggetto capace di immagazzinare altri oggetti (i suoi elementi). Ogni Contenitore ha un Iteratore associato, cioe’ un puntatore ai suoi elementi, che permette di muoversi tra essi Gli Iteratori possono essere monodirezionali, bidirezionali o ad accesso casuale. Gli Algoritmi sono delle funzioni globali capaci di agire su Contenitori diversi: possono fare operazioni di ordinamento, di ricerca, di trasformazione,... Documentazione: http://www.msoe.edu/~sebern/resource/stl/index.htm Esempio di uso: # include <vector> # include <algorithm> # include <iostream> int main() { vector<int> container; int val; for (int i=0; i<10; i++) { val = (int) ((float) rand()/RAND_MAX*10); container.push_back(val); } vector<int>::iterator it1; for(it1=container.begin(); it1!=container.end(); it1++) cout << "vector : " << *it1 << endl; find ( container.begin(), container.end(), 3 ); sort ( container.begin(), container.end() ); for(it1=container.begin(); it1!=container.end(); it1++) cout << "vector ordinato: " << *it1 << endl; min_element ( container.begin(), container.end() ); return 0; } Funziona anche con list ! # include <list> # include <algorithm> # include <iostream> int main() { list<int> container; int val; for (int i=0; i<10; i++) { val = (int) ((float) rand()/RAND_MAX*10); container.push_back(val); } list<int>::iterator it1; for(it1=container.begin(); it1!=container.end(); it1++) cout << "lista : " << *it1 << endl; find ( container.begin(), container.end(), 3 ); sort ( container.begin(), container.end() ); for(it1=container.begin(); it1!=container.end(); it1++) cout << "lista ordinata: " << *it1 << endl; min_element ( container.begin(), container.end() ); return 0; } Esercizio Creare una classe Hit con data members: int adress float energy // indirizzo del canale di elettronica // energia raccolta nel canale Creare un vettore di Hit ( vector<Hit>) ed ordinarlo secondo il numero di canale. Creare un secondo vector<Hit>, aggiungerlo al primo e ordinare il vector<Hit> risultante. Cenni di programmazione object-oriented Ciclo di vita del software Caratteristiche di un cattivo disegno e di un buon disegno Principi Diagrammi UML Design Patterns Ciclo di vita del Software ˚Requirements ˚Analysis ˚Design ˚Production ˚Testing ˚Maintenance Caratteristiche di un cattivo disegno ˚ Rigidità non può essere cambiato con faciltà non può essere stimato l’impatto di una modifica ˚ Fragilità una modifica singola causa una cascata di modifiche successive i bachi sorgono in aree concettialmente separate dalle aree dove sono avvenute le modifiche ˚ Non riusabilità Esistono molte interdipendenze, quindi non è possibile estrarre parti che potrebbero essere comuni Caratteristiche di un buon disegno Object Oriented ˚ Riduce la dipendenza del codice di alto livello dalla rappresentazione dei dati ˚ Permette il riutilizzo del codice di alto livello ˚ Permette di sviluppare moduli indipendenti l’uno dall’altro ˚ I clienti dipendono da interfacce che non dipendono dalla loro implementazione Open/Closed principle Un buon codice deve essere aperto ad estensioni e chiuso a modifiche L’Object Oriented, con il meccanismo delle classi virtuali, permette di applicare questo principio Unified Modeling Language E’ una notazione per rappresentare le relazioni tra le classi ed i messaggi che queste si scambiano. Sviluppata da Grady Booch, Jim Rumbaugh, Ivar Jacobson. E’ uno standard dalla fine del 1997. Comprende: ˚Class Diagrams ˚Sequence and Collaboration Diagrams ˚Use Case Diagrams ˚State Diagrams Cenni sull’uso dei Class Diagrams: classi, attributi e metodi classe privato pubblico circle r_ : double draw( ) Cenni sull’uso dei Class Diagrams: relazioni tra classi autista by reference (condivisa) automobile motore by value possesso La relazione di contenimento puo’ essere by reference (condivisa: un autusta puo’ guidare piu’ automobili) o by value (possesso: un auto possiede il suo motore). Cardinalita’: un poligono possiede n punti _points Polygone Point 1 1...* Dipendenza: non c’e’ associazione, c’e’ solo relazione d’uso CD Player CD Ereditarieta’ Virtuale Shape Circle Rectangle Design Patterns Piccoli insiemi di classi che collaborano implementando dei comportamenti tipici Elementi di software OO riutilizzabile Ne esamineremo solo alcune Factory client factory abstract product concr prod 1 concr prod 2 I client possono richiedere la creazione di un prodotto senza dipendervi. La Factory dipende dai prodotti concreti, mentre i client dipendono solo da AbstractProduct. Esempio: una factory di Shapes #ifndef SHAPE_H #define SHAPE_H class Shape { public: Shape(); virtual ~Shape(); virtual void draw() const = 0; }; #endif Shape.h #include "Shape.h" Shape.cc #include <iostream> Shape::Shape() { cout << "*** costruisco una shape" << endl; } Shape::~Shape() {cout << "*** distruggo una shape" << endl;} #ifndef CIRCLE_H Circle.h #define CIRCLE_H #include "Shape.h" class Circle : public Shape { public: Circle( double r ); ~Circle(); virtual void draw() const; private: double r_; }; #endif #include "Circle.h" Circle.cc #include <iostream> Circle::Circle( double r ) : r_( r ) { cout << "*** costruisco un cerchio" << endl; } Circle::~Circle() { cout << "*** distruggo un cerchio" << endl; } void Circle::draw() const { cout << "Disegno un cerchio di raggio " << r_ << endl; } #ifndef FACTORY_H #define FACTORY_H Factory.h class Shape; class Factory { public: Factory( int n ); ~Factory(); int length() const; const Shape * getShape( int i ) const; private: Shape * * shapes_; int length_; }; #endif #include "Factory.h" Factory.cc #include "Shape.h" #include "Circle.h" #include "Rectangle.h" int Factory::length() const { return length_; } Factory::~Factory(){ for ( int i = 0 ; i < length(); i ++ ) delete shapes_[ i ]; delete [] shapes_;} const Shape * Factory:: getShape( int i ) const { return shapes_[ i ]; } Factory::Factory( int n ) : length_( n ) , shapes_ ( new Shape * [ n ] ){ for ( int i = 0 ; i < n; i ++ ) { if ( i % 2 == 0 ) shapes_[ i ] = new Circle( i + 1 ); else shapes_[ i ] = new Rectangle( i + 1, 2 * i + 1); } } #include "Factory.h" #include "Shape.h" Main.cc void main() { Factory * f = new Factory( 4 ); for ( int i = 0 ; i < f->length(); i ++ ) f->getShape( i )->draw(); delete f } Singleton Viene usato ogni volta che una classe deve essere instanziata una sola volta, e viene usata da diversi oggetti. Per evitare istanziazione accidentale, il constructor deve essere privato. Singleton _instance : Singleton instance () : Singleton specificService () if (_instance==0) _instance = new Singleton(); return _instance; user_code() { Singleton::instance()->specificService(...); } Composite Il client può trattare componenti e compositi usando la stessa interfaccia. La composizione può essere recursiva Nel nostro esempio di grafica con Shapes un gruppo di shapes può essere considerato un composito. client shape draw() 1..* group draw() circle, rectangle, ... draw() Le classi shape, circle e rectangle non cambiano. Vediamo prima l’implementazione di Factory come vector (STL) di shapes e le modifiche che dobbiamo fare al main. #ifndef FACTORY_H #define FACTORY_H #include<vector> Factory.h class Shape; class Factory { public: Factory( int n ); ~Factory(); int size() const; vector<Shape *>::const_iterator begin() const; vector<Shape *>::const_iterator end() const; private: vector<Shape *> shapes_; }; #include #include #include #include "Factory.h" "Shape.h" "Circle.h" "Rectangle.h" Factory.cc int Factory::size() const { return shapes_.size();} Factory::~Factory() { vector<Shape *>::iterator i; for ( i = shapes_.begin() ; i != shapes_.end(); i ++ ) delete *i; } Factory::Factory( int n ) : shapes_() { for ( int i = 0 ; i < n; i ++ ) { if ( i % 2 == 0 ) shapes_.push_back( new Circle( i + 1 ) ); else shapes_.push_back( new Rectangle( i + 1, 2*i + 1) ); } } vector<Shape *>::const_iterator Factory::begin() const { return shapes_.begin(); } vector<Shape *>::const_iterator Factory::end() const { return shapes_.end(); } #include "Factory.h" #include "Shape.h" void main() { Factory f( 4 ); vector<Shape *>::iterator i; for ( i = f.begin(); ( * i )->draw(); } main.cc i != f.end(); i ++ ) Ora implementiamo group come vector di shapes #ifndef GROUP_H #define GROUP_H #include "Shape.h" #include <vector> class Group : public Shape { public: Group(); void addShape( Shape * ); virtual ~Group(); virtual void draw() const; private: vector<Shape*> container_; }; Group.h #include "Group.h" Group.cc #include <iostream> Group::Group() { cout << "*** creo un gruppo" << endl; } Group::~Group() { cout << "*** distruggo un gruppo" << endl; vector<Shape*>::iterator i; for( i = container_.begin(); i != container_.end(); i++) delete *i; } void Group::draw() const { cout << "Disegno un gruppo" << endl; vector<Shape*>::const_iterator i; for( i = container_.begin(); i != container_.end(); i++){ cout << ">> "; (*i)->draw(); } } void Group::addShape( Shape * s ) { container_.push_back( s ); } Factory.h non cambia. In Factory.cc occorre aggiungere: #include "Group.h" e modificare il costruttore: Factory::Factory( int n ) : shapes_() { for ( int i = 0 ; i < n; i ++ ) { if ( i % 2 == 0 ) shapes_.push_back( new Circle( i + 1 ) ); else shapes_.push_back(new Rectangle( i + 1, 2 * i + 1)); } Group * g = new Group; g->addShape( new Circle( 1.5 ) ); g->addShape( new Rectangle( 1.5, 2.5 ) ); shapes_.push_back( g ); } Main.cc non cambia Visitor Client Visitor visit1 (ConcreteElement1) visit2 (ConcreteElement2) Element accept (Visitor) ConcreteVisitor1 visit1 (ConcreteElement1) visit2 (ConcreteElement2) ConcreteVisitor2 visit1 (ConcreteElement1) visit2 (ConcreteElement2) ConcreteElement1 accept (Visitor v) ConcreteElement2 accept (Visitor v) Il Visitor permette di aggiungere nuove operazioni ad Element senza cambiare l’interfaccia. L’aggiunta di un nuovo Concrete Element impone modifiche a tutti i Visitor. Scriviamo un Visitor che calcoli il perimetro e l’area di cerchio, rettangolo e di “gruppo di shapes”. In questo caso la classe Shape e’ Element; Circle, Rectangle e Group sono i Concrete Element. Continuiamo ad usare la Factory per costruire le shapes. Factory.cc e Factory.h non devono essere modificati. Occorre aggiungere la funzione accept(Visitor*) e #include "Visitor.h" a tutte le shapes: #ifndef CIRCLE_H Circle.h #define CIRCLE_H #include "Shape.h" class Circle : public Shape { public: Circle( double r ); ~Circle(); virtual void draw() const; double r() const; virtual double accept( Visitor * ) const; private: double r_; }; #endif #include "Circle.h" #include "Visitor.h" #include <iostream> Circle.cc Circle::Circle( double r ) : r_( r ) { cout << "*** costruisco un cerchio" << endl; } Circle::~Circle() { cout << "*** distruggo un cerchio" << endl; } void Circle::draw() const { cout << "Sono un cerchio di raggio " << r_ << endl; } double Circle::accept( Visitor * v ) const { return v->visit( this ); } double Circle::r() const { return r_; } Analogamente per le classi Rectangle e Group. Shape.h diventa: #ifndef SHAPE_H #define SHAPE_H class Visitor; class Shape { public: Shape(); virtual ~Shape(); virtual void draw() const = 0; virtual double accept( Visitor * ) const = 0; }; #endif Il Visitor astratto: Visitor.h (N. B.: non serve un Visitor.cc) #ifndef VISITOR_H #define VISITOR_H class Circle; class Rectangle; class Group; class Visitor { public: virtual double visit( const Circle* ) = 0; virtual double visit( const Rectangle* ) = 0; virtual double visit( const Group* ) = 0; }; Il Visitor concreto Perimetro #ifndef PERIMETRO_H #define PERIMETRO_H Perimetro.h #include "Visitor.h" class Perimetro : public Visitor { public: virtual double visit( const Circle* ); virtual double visit( const Rectangle* ); virtual double visit( const Group* ); }; #endif #include "Perimetro.h" Perimetro.cc #include <math.h> #include "Circle.h" #include "Rectangle.h" #include "Group.h" double Perimetro::visit( const Circle* c ) { double r = c->r(); return 2 * M_PI * r; } double Perimetro::visit( const Rectangle* r ) { double lx = r->lx(), ly = r->ly(); return 2 * ( lx + ly ); } double Perimetro::visit( const Group* g ) { double p = 0; vector<Shape *>::const_iterator i; for ( i = g->begin(); i != g->end(); i++ ) { p += ( * i )->accept( this ); } return p; } #include "Factory.h" Main.cc #include "Shape.h" #include "Perimetro.h" #include "Area.h" #include <iostream> void main(){ Factory f(4); Visitor * perimetro = new Perimetro; Visitor * area = new Area; vector<Shape *>::const_iterator i; for ( i = f.begin(); i != f.end(); i ++ ) { ( * i )->draw(); double p = ( * i )->accept( perimetro ); double a = ( * i )->accept( area ); cout << " perimetro: " << p << " area: " << a << endl; } delete perimetro; delete area; } Area.h #ifndef AREA_H #define AREA_H #include "Visitor.h" class Area : public Visitor { public: virtual double visit( const Circle* ); virtual double visit( const Rectangle* ); virtual double visit( const Group* ); }; #endif #include "Area.h" #include <math> #include "Circle.h" #include "Rectangle.h" #include "Group.h" double Area::visit( const Circle* c ){ double r = c->r(); return M_PI * r * r; } double Area::visit( const Rectangle* r ){ double lx = r->lx(), ly = r->ly(); return ( lx * ly ); } double Area::visit( const Group* g ){ double p = 0; Group::const_iterator i; for ( i = g->begin(); i != g->end(); i++ ) { p += ( * i )->accept( this ); } return p; } Area.cc Conclusioni La programmazione in C++, utilizzando le tecniche Object Oriented, può aiutare a migliorare la flessibilita’ ed espansibilita’ del codice riducendo le dipendenze al suo interno ... ma va utilizzato in maniera adeguata, disegnando il codice prima di implementarlo. E’ facile scrivere un codice in C++ come se fosse in F77, ma questo da’ ben pochi vantaggi. La fine .... .... del principio