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