Introduzione al linguaggio C++
5 lezioni
Lunedì, Giovedì ore 12.00-13.30
Alessandro Lonardo
[email protected]
Davide Rossetti
[email protected]
Pagina WEB
http://apegate.roma1.infn.it/~lonardo
Testi base:
-Bjarne Stroustrup, Il Linguaggio C++,
Addison-Wesley.
-Brian W. Kernighan, Dennis M. Ritchie,
Il Linguaggio C, Jackson libri.
Programma del corso
Lezione 1
a)
b)
c)
d)
Paradigmi di programmazione
Dichiarazioni
Tipi
Costanti
Lezione 2
a)
b)
c)
d)
e)
f)
Operatori
Istruzioni
Funzioni
Header File
Il Preprocessore
Le librerie
Lezione 3
a)
b)
c)
d)
Classi
Interfacce ed implementazioni
Caratteristiche delle classi
Costruttori e distruttori
Lezione 4
a)
b)
c)
d)
e)
Classi derivate
Classi astratte
Ereditarietà multipla
Controllo dell’accesso
Memoria dinamica
Lezione 5
•
•
•
Overload di Operatori
Template
La Standard Template Library
argomenti non trattati a lezione
Paradigmi di Programmazione
Programmazione Procedurale
Si definiscano le procedure desiderate;
Si utilizzino gli algoritmi migliori.
-Programmatore concentrato sull’algoritmo
-Supporto fornito dai linguaggi: funzioni, procedure.
(Fortran, Pascal, C...)
-Il programma viene suddiviso in funzioni, ogni funzione
realizza un algoritmo o una parte di esso.
Es.
double sqrt(double arg)
{
//codice per il calcolo della radice quadrata
}
void some_function()
{
double root2 = sqrt(2.0);
//...
}
Programmazione Modulare
Modulo = Dati + Procedure;
Si decida quali sono i moduli necessari;
Si suddivida il programma in modo che i dati siano nascosti nei
Moduli.
-Dati nascosti: nomi delle variabili, delle costanti e dei tipi sono
resi locali al modulo.
-Il linguaggio C consente l’impiego di questo paradigma attraverso
Il concetto di unità di compilazione.
Es.
File stack.h:
//dichiarazione della interfaccia per
//il modulo stack di caratteri
void push(char);
char pop();
const int stack_size = 100;
File stack.cc (implementazione de modulo):
#include “stack.h” //usa l’interfaccia stack
//static significa: simbolo locale
//a questo modulo (file)
static char v[stack_size];
//lo stack viene inizializzato vuoto
static char* p = v;
void push(char c)
{
//implementazione
}
char pop()
{
//implementazione
}
Uso del modulo stack di caratteri:
File bubu.cc:
#include “stack.h” //usa il modulo stack
void some_function()
{
push(‘y’);
char c = pop();
assert(c == ‘y’);
}
-in questo file non si ha accesso alla struttura interna dello stack,
è possibile utilizzare lo stack solo per mezzo delle funzioni esposte
nell’interfaccia del modulo.
-Il linguaggio C++ estende il supporto del C alla programmazione
modulare attraverso l’uso delle classi.
Astrazione dei dati
Si decida quali tipi si desiderano;
si fornisca un insieme completo di operazioni per ogni tipo.
-estende il concetto di modulo al caso in cui siano necessari più
oggetti di un certo tipo (come avrei fatto a dichiarare 2 stack?)
-I linguaggi che supportano la programmazione modulare
permettono l’astrazione dei dati.
-Linguaggi come Ada, Java, C++... supportano il paradigma
della astrazione dei dati.
File complex.h:
//dichiarazione del tipo numero complesso
class Complex
{
double re, im;
public:
Complex(double r, double i) {re = r; im = i;}
Complex(double r) {re = r; im = 0;}
friend
friend
friend
friend
friend
};
complex
Complex
Complex
Complex
Complex
operator+(Complex, Complex);
operator-(Complex, Complex);
operator-(Complex);//unario
operator*(Complex, Complex);
operator/(Complex, Complex);
File complex.cc:
//implementazione del tipo complex
//...
Complex operator+(Complex a1, Complex a2)
{
return Complex(a1.re+a2.re, a1.im+a2.im);
}
//...
File bubu.cc:
//uso del tipo complex
void some_function()
{
Complex a(2.0, 1.0), b(3.14), i(0.0, 1.0), c;
c = a*i+b;
//...
}
-L’uso del tipo complex definito dall’utente è del tutto analogo
a quello dei tipi predefiniti.
-Il tipo di dato astratto è una scatola nera. Il suo comportamento
non può essere cambiato, se non ridefinendo il tipo.
Questa è una limitazione significativa.
Esempio: un sistema grafico che gestisce cerchi, triangoli e quadrati.
-Esistono i seguenti tipi astratti:
class Point {//...};
class Color {//...};
enum Kind {circle, triangle, square};
//rappresentazione di una forma
class Shape
{
Point center;
Color col;
Kind k;
public:
point where()
{return center;}
void move(point to) {center = to; draw(); }
void draw();
void rotate(int);
};
-k è un “campo tipo” utile alle funzioni per determinare il tipo di forma
su cui si lavora:
void Shape::draw() {
switch(k) {
case circle:
//disegna un cerchio
case triangle:
//disegna un triangolo
case square:
//disegna un quadrato
}
}
-Problemi:
1. draw() (come le altre operazioni) deve conoscere tuttti i tipi
di forme su cui si lavora. Se si introduce una nuova forma il
codice di draw() dovrà essere modificato.
2. Non è possibile aggiungere nel sistema la gestione di una
nuova forma se non si ha accesso al codice sorgente di ogni
operazione.
3. Ogni modifica espone il sistema alla introduzione di bug su
codice già sviluppato.
-La sorgente di tutti questi problemi è la mancata espressione della
distinzione tra le proprietà generali di una forma(ha un colore,
ha una posizione, si può disegnare...) e le proprietà di una
forma particolare (il cerchio ha un raggio, si disegna come un
cerchio (!), ...).
-L’espressione di questa distinzione in modo utile per la scrittura del
codice rappresenta la
Programmazione Orientata agli Oggetti
-Il supporto che il linguaggio C++ offre a questo paradigma è il
meccanismo della ereditarietà.
-Il concetto di forma più generale:
class Shape
{
Point center;
Color col;
//è sparito il fastidioso Kind
public:
point where()
{return center;}
void move(point to) {center = to; draw(); }
virtual void draw();
//ora è virtual
virtual void rotate(int); //ora è virtual
};
-“virtual”: può essere ridefinito in una classe derivata
-Una forma particolare:
class Circle : public Shape
{
int radius;
public:
void draw() {//disegna un cerchio!};
void rotate(int) {}//facile implementazione
};
-La classe Circle è derivata (sottoclasse) dalla classe Shape.
-La classe Shape è di base (superclasse) per la classe
Circle.
Esempio di uso: funzione che prende un vettore di size forme
e le ruota di angle gradi.
void rotate_all(Shape v[], int size, int angle)
{
int i = 0;
while (i<size)
{
v[i].rotate(angle);
i = i+1;
}
}
-l’elemento v[i]è in principio una forma qualsiasi, l’operazione
di rotazione sarà quella che gli compete.
-Nella fase di progettazione del software è necessario individuare
la massima quantità di elementi in comune tra i tipi del sistema
e rappresentare queste similitudini utilizzando classi di base comuni.
Paradigma di programmazione orientata agli oggetti
Si determini quali classi si desiderano;
Si fornisca un insieme completo delle operazioni di ogni classe;
Si espliciti ciò che hanno in comune per mezzo della ereditarietà
Dichiarazioni
-Identificatore C++: sequenza di lettere e cifre, il primo carattere
deve essere una lettera (o “underscore”, ‘_’).
Non si possono usare keyword.
Case sensitive.
Buoni identificatori:
Hello hello _bubu_ ApeMaia
un_identificatore_molto_lungo
Non sono accettati:
1var
$pippo for
var1
var2
lunghezza.massima
-Dichiarazione di un identificatore:
Prima dell’uso di qualsiasi identificatore bisogna specificare il
Suo tipo:
Char c;
int count = 1;
char* name = “ciccio”;
Const double pi=3.1415926535897932385
float minus(float arg) { return -arg; }
-Queste sono anche definizioni di identificatori:
definiscono l’entità alla quale il nome si riferisce.
Per le variabili è la quantità di memoria allocata, per le funzioni
la loro implementazione, per le costanti il loro valore.
-le seguenti sono solo dichiarazioni:
extern float sqrt(float arg);
extern int err_num;
struct user;
-Una dichiarazione ha effetto in generale in un sottoinsieme del
programma (visibilità).
int x; // x globale, visibile in tutto il pr.
void f()
{
int x; //x locale, nasconde x globale
x = 1;
{
int x; //locale, nasconde la prec. Locale
x = 2;
}
x = 3;
}
int* p = &x;
Oggetto : zona di memoria
lvalue: espressione che fa riferimento ad un oggetto
-Un oggetto viene creato all’atto della sua definizione e distrutto
quando non è più visibile (anche i locali definiti static)
int a = 1;
void f()
{
int b = 1; //inizializzato ad ogni chiamata
static int c = a; //ini. una sola volta
cout << “ a = “ << a++
<< “ b = “ << b++
<< “ c = “ << c++ << endl;
}
int main()
{
while ( a < 4 )
f();
}
Output:
a = 1 b = 1 c = 1
a = 2 b = 1 c = 2
a = 3 b = 1 c = 3
Tipi
il tipo specifica le operazioni che si possono compire sul dato
e la loro semantica
Tipi fondamentali:
void
Tipi interi:
bool
char
short int
int
long int
Tipi floating point (reali):
float
double
long double
Tipi interi senza segno, valori logici, vettori di bit:
unsigned char
unsigned short int
unsigned int
unsigned long int
Per esplicitare i tipi interi con segno:
signed char
signed short int
signed int
signed long int
-se il tipo è omesso si assume int
-tipi interi e floating point diversi, diversa occupazione di memoria,
velocità di esecuzione...
-Il linguaggio definisce solo queste restrizioni:
1==sizeof(char)<=sizeof(short)<=sizeof(int)<=sizeof(long)
sizeof(float)<=sizeof(double)<=sizeof(long double)
sizeof(I)==sizeof(signed I) == sizeof(unsigned I)
Ad esempio, architettura IA32:
bool
char
short
int
long int
8
8
16
32
32
bit
bit
bit
bit
bit
float
double
long double
32 bit
64 bit
80 bit
Conversione tra i tipi
-implicita: in generale si possono mescolare liberamente variabili
di tipo diverso in una espressione.(non è un bello stile...)
int i = 2;
float f, g = 2.0;
f = i * g - 4;
-esplicita: float r = (float) 2; //cast
float r = float(2);
-promozioni
Tipi derivati
-definiti a partire da quelli base o user-defined per mezzo degli
operatori di dichiarazione:
*
&
[]
puntatore
reference
array
Esempio:
int*
pi;
//tipo = puntatore ad int
double& d;
//tipo = reference a double
float v[10]; //tipo = vettore di 10 float
-Un altro modo di introdurre un tipo derivato è la definizione di
una struttura:
struct Point
{
int x;
int y;
};
Point a,b,c;
Puntatori
puntatore: variabile che contiene l'indirizzo di un'altra variabile
-In C++ i puntatori hanno un tipo associato (eccezione void *).
c
char c = 'y';
'y'
char* p;
c
'y'
c
p = &c;
'y'
c
char c2 = *p;
'y'
p
null
p
&c
p
&c
&c: indirizzo di c.
*p: dereferenziazione di p, accesso all'oggetto puntato.
-avrei potuto scrivere: char *p = &'y'?
c2
'y'
no! ottengo: non-lvalue in unary '&'.
Naturalmente posso dichiarare un puntatore a puntatore:
char** pp = &p;
c
'y'
p
&c
c2
'y'
pp
&p
**pp = 'z';
c
'z'
p
&c
pp
&p
cout << c << '\t' << c2 << endl;
in output:
z
y
c2
'y'
-se p è un puntatore di tipo T* allora *p può comparire ovunque
ci si aspetti un oggetto di tipo T:
int i = 4;
int* pi = &i;
int* pi2;
*pi = *pi + 1; //i=5
pi2 = pi;
*pi2 = i * 2; //i=10
Quanto vale i?
-void*: è il tipo che corrisponde ai puntatori generici, qualsiasi
puntatore può essere convertito a void* e poi riconvertito nel
suo tipo originale senza perdita di informazione.
Questo tipo è utilissimo come parametro di funzioni.
-In realtà esistono anche puntatori a funzione
int (*funp) (int, int);
è la dichiarazione di un puntatore di nome funp ad una funzione
che accetta due parametri di tipo int e restituendo un tipo int
come risultato.
La dereferenziazione di funp restituisce una funzione.
Esempio:
//restituisce il massimo tra arg1 e arg2
int max(int arg1, int arg2) {//...}
//restituisce il minimo tra arg1 e arg2
int min(int arg1, arg2) {//...}
int (*funp) (int, int);
int i = 1, j = 2, k, l;
funp = &max;
k = (*funp)(i, j);
funp = &min;
l = (*funp)(i, j);
Quanto valgono k ed l?
array
tipo T, T[size] è un vettore di size elementi di tipo T.
-L'indice è compreso tra 0 e size-1.
-size deve essere una costante intera; alcune implementazioni
del compilatore (ad es. GNU) permettono l'uso di variabili o
espressioni intere.
float v[3]; //v[0], v[1], v[2]
int m[2][3];//2 vettori di 3 interi
char* vpc[10];//vettore di 10 punt. a char
-Inizializzazione:
//v[] ha 6 elementi
int v[] = {137, -12, 53, 12943, 21, -20};
float vf[] = {12.2, 0.1, -22.1};
double id[3][3] ={
{1.0, 0.0, 0.0},
{0.0, 1.0, 0.0},
{0.0, 0.0, 1.0}
};
char vc[] = {'c', 'i', 'a', 'o', '\0'};
-solo per i vettori di char si può utilizzare una notazione più comoda:
char vocali[] = "aeiou";
//in questo caso il carattere di fine stringa
//viene aggiunto automaticamente
-boundary checking: no!
Il compilatore non controlla la correttezza degli indici degli elementi di
array. Si può facilmente ottenere un errore.
Puntatori ed array
-Il nome di un array può anche essere usato come puntatore al suo primo
elemento:
int v[10];
int* pi = v;
*pi = 0;//equivale a v[0] = 0
pi++;
//ora pi punta a v[1]
*pi = 1;//v[1] = 1
pi--;
//ora pi punta a v[0]
pi = pi +5 ; //ora pi punta a v[5]
int offset = pi - v; //numero di el. tra i 2 p.
aritmetica dei puntatori
+, -, ++, --.
Da usare con grande cautela, è facile puntare ad aree di memoria sbagliate
uscita dal programma con errore, il famigerato
segmentation fault
Strutture
Meccanismo per introdurre tipi di dato costituiti da un insieme di
elementi di tipi (anche) diversi.
struct Particle {
double p[3];
double v[3];
int charge;
};
-si possono dichiarare variabili di questo nuovo tipo:
Particle p1, p2, p3;
-si può accedere ai campi del tipo usando l'operatore .
p1.p[0] = p1.p[1] = p1.p[2] = 0.0;
p1.v[0] = p1.v[1] = p1.v[2] = 0.0;
p1.charge = -1;
//poi vedremo che risulta più comodo
//utilizzare il costruttore
//...
p2 = p1;
//...
-il nome del tipo risulta utilizzabile anche nella definizione del tipo
stesso:
struct Link
{
Link* prev;
Link* succ;
};
-ma ciò non significa che si possono dichiarare oggetti del nuovo tipo
durante la sua dichiarazione!
struct NewType
{
NewType x;
//ERRORE IN COMPILAZIONE
//...
};
-Come si gestiscono i riferimenti incrociati durante le dichiarazioni?
Meccanismo della forward declaration.
Esempio:
struct List; //dichiarazione non definizione
struct Link
{
Link* prev;
Link* succ;
List* member_of;
};
struct List
{
Link* head;
};
-In generale il nome della struct può essere utilizzato prima della sua
definizione quando non è necessario conoscere la sua dimensione.
struct Astruct;
void f(Astruct); //no problem
Astruct a;
f(a);
//Errore!
//Errore!
typedef
-introduce un nuovo nome per un tipo
-comodo per costruire convenzioni proprie:
typedef double Mass;
typedef double Distance;
Mass m1, m2, m3;
Distance d1, d2, d3;
-esempi abbastanza comuni:
typedef unsigned char uchar;
typedef unsigned short ushort;
typedef unsigned int
uint;
-è utile per abbreviare tipi complicati (come i puntatori a funzione):
typedef void (*calc_func)(float);
calc_func func_table[10];
è certamente più espressivo di:
void (* func_table[10])(float);
reference
nome alternativo di un oggetto
-tipo T, T& significa riferimento a T.
int i = 1;
int& r = i; //r ed i si rif. allo stesso int
int x = r; // x = 1
r = 2;
// i = 2
-Una reference deve essere sempre inizializzato (a cosa riferirebbe?)
-Inizializzazione reference != assegnamento di variabile
-Gli operatori applicati ad una reference non agiscono su di essa, ma
sull'oggetto a cui si riferisce:
int ii = 0;
int& rr = ii;
rr++; //è ii che viene incrementato, non rr
-una reference non può essere modificata dopo l'inizializzazione.
-Come vedremo sono utili come parametri di funzioni e nella definizion
degli operatori definiti dall'utente.
costanti senza nome
-costanti intere:
decimali
ottali
esadecimali
0
0
0x0
137
064
0x3
12
3
1
0237
0x7fff 0xfefe
-suffissi U, L, LL:
void
void
void
void
f(int);
f(unsigned int);
f(long int);
f(long long int);
f(3);
f(3U);
f(3L);
f(3LL);
-costanti floating point:
0.0
1.37
2.
1.3e10 1.6e-15
-costanti carattere (ASCII, EBCDIC, UNICODE...)
'a'
'2'
'\n'
'\t'
si può, ma è meglio evitare (portabilità del codice):
'\137' '\x05f' 95 codice ASCII di '_'
costanti con nome
la keyword const premessa alla dichiarazione di un oggetto lo rende
una costante invece di una variabile (deve essere inizializzato):
const int bu = 20;
bu++; //ERRORE
const char* pippo = "abcde";
-Chi è costante il puntatore o l'oggetto puntato?
pippo[2] = 'z'; //ERRORE
pippo = "ciccio";
ovvero ho dichiarato un puntatore a costante.
-Per rendere costante il puntatore si usa l'operatore *const
char *const bubu = "yogi";
bubu[3] = 'a';
bubu = "napo"; //ERRORE
ovvero ho dichiarato un puntatore costante.
-infine:
const char *const cp = "fred";
-notare che non si può:
const int x = 10;
int* pi = &x; //ERRORE, potrei modificare x
const int* pic = &x; //no problem
-vantaggi per il compilatore usando const (e ovviamente per l'utente).
enum
-un nome simbolico per ogni costante:
enum { PICCOLO, MEDIO, GRANDE };
equivale a:
const int PICCOLO = 0;
const int MEDIO
= 1;
const int GRANDE = 2;
-è possibile assegnare un nome, facendo diventare l'enum un nuovo
tipo:
enum Verdure
{
RAPE,
BROCCOLI,
CIPOLLE
};
//...
Verdure cose_da_comprare;
cose_da_comprare = RAPE;
int j = BROCCOLI;
Verdure da_preparare = 2; //ERRORE!!
Verdure da_preparare = Verdure(2); //OK
-in realtà gli enumeratori si possono inizializzare a piacere:
enum Colors
{
red = 2,
green,
blue = green + 1,
grey = blue * 2
};
//...
cout << grey << ' ' << blue << ' '
<< green << ' ' << red << endl;;
Cosa ottengo in uscita?
enum <---> switch
union
-definisce piu` modi di vedere lo stesso oggetto:
// nell’ipotesi sizeof(int)==4
union MultipleAccess
{
int
word_value;
unsigned short halfword_values[2];
unsigned char
byte_values[4];
};
-come si usa ?
MultipleAccess value;
value.word_value = 0xA3458543;
//cosi` accedo ai bytes:
unsigned char first_byte = value.byte_values[0];
//cosi` alle parole di 16 bit (half word):
unsigned short second_halfword =
value.halfword_values[1];
cout
<< hex << value.word_value << ' '
<< (int) first_byte << ' '
<< second_halfword << endl;
-cosa ottengo in uscita?
word = a3458543 byte[0] = 43 halfword[1] = a345
-E' utilissimo quando si abbia necessita di risparmiare memoria
(lo stesso spazio occupa oggetti diversi in momenti diversi):
enum EntryType { STRING, INT};
union EntryValue
{
char* string_val;
int
int_val;
};
struct Entry
{
char*
name;
EntryType type;
EntryValue value;
};
//...
Entry a[10];
a[0].name = "Pippo";
a[0].type = STRING;
a[0].value.string_val = "Amico di Topolino";
a[1].name = "Targa di Paperino";
a[1].type = INT;
a[1].value.int_val = 313;
campi di bit
-modo per inserire oggetti di dimensioni ridotte in una sola word
(economizzando lo spazio occupato).
struct
{
unsigned
unsigned
unsigned
unsigned
} number;
int
int
int
int
sign : 1;
exponent: 8;
fraction0: 7;
fraction1: 16;
-i campi si comportano come degli interi (di dimensione ridotta)
-Tutti i dettagli (come avviene l'allocazione dei campi in memoria...)
dipendono dalla macchina.
-Tipo di dato con cui è facile scrivere codice non portabile
operatori
-aritmetici (tipi interi e floating point):
+ - * /
% resto della div. int (modulo)
++ -- pre e post incremento/decremento
- + unari
-esempio:
int i, j, inc_i, j_inc;
i = j = 3;
inc_i = ++i;
j_inc = j++;
cout << i << '\t' << j << '\t'
<< inc_i << '\t' << j_inc << endl;
ottengo:
4 4 4 3
-relazionali:
>
==
>=
!=
-logici:
&&
||
!
AND
OR
NOT
<
<=
le espressioni formate con questi operatori vengono valutate da sin.
a destra, bloccandosi non appena si determina il risultato.
Attenzione!
int ciao()
{
cout << "Ciao" << endl;
return 1;
}
//...
int i = 10;
unsigned booleano = (i == 10) || (ciao() == 1);
Verremo salutati?
-bit a bit (tipi interi), utili per lavorare con vettori di bit:
&
|
^
<<
>>
~
AND
OR
XOR
shift a sinistra
shift a destra (logico/aritmetico)
complemento ad uno
-mascherare (azzerare) insiemi di bit: AND
n = n & 0xF0 //11110000
-accendere insiemi di bit: OR
n = n | 0x1; //dispari
-moltiplicare per potenze di 2 (x = y * 2 z)
x = y << z;
-dividere per potenze di 2 (x = y / 2 z)
x = y >> z;
- mascherare il bit meno significativo:
n = n & (~0x1);
-assegnamento (semplice e composto):
=
assegnamento
*=
<<=
/=
>>=
es: a *= 2;
%=
&=
+=
|=
--->
a = a * 2;
-vari:
. selezione elemento
-> selezione elemento
es:
struct Color
{
int r,g,b;
};
//...
Color c;
Color* pc = &c;
//...
c.r = pc->r;
-=
^=
object.member
pointer->member
[]
indicizzazione
pointer[expr]
()
chiamata di funzione expr(expr_list)
()
costruzione valore
type(expr_list)
&
indirizzo di
&lvalue
*
dereferenziazione
*expr
new
crea un oggetto
new type
delete distrugge un oggetto delete pointer
sizeof dimensioni del tipo
sizeof type
sizeof dimensioni oggetto
sizeof expr
::
scope resolution
class_name::member
?:
espressione condiz.
expr?expr:expr
,
virgola
expr, expr
associatività
-unari e assegnamento associativi a destra:
a = b = c
--->
a = ( b = c )
*p++
--->
*(p++) //non (*p)++
-tutti gli altri sono associativi a sinistra
precedenza degli operatori: manuale di riferimento!
-esiste la forma funzionale di quasi tutti gli operatori visti:
double n1 = 1.33;
double n2 = .3E-2;
double result;
result = operator+(n1,n2);
-e` come se il compilatore avesse predefinite e utilizzato le
funzioni speciali:
double operator +(const double &d1,
const double &d2);
idem per:
int operator <(const int &n1, const int &n2);
int operator ~(const int &n1);
int operator >>=(const int &n1,
const int &n2);
-nessuno usa questa forma, di solito, ma servono per l’overloading
nei tipi definiti dall'utente
costrutti
if-else
permette di esprimere una decisione
if(espressione)
istruzione_1
else
istruzione_2
-per istruzione si intende anche un blocco di istruzioni (sequenza di
dichiarazioni ed istruzioni tra parentesi graffe), che a sua volta può
contenere altri blocchi...
es.
if( a > b )
max = a;
else
max = b;
switch-case
permette di operare delle scelte multiple
controllando se una espressione assume un
certo valore in un insieme di costanti intere
switch (espressione)
{
case const_expr1 : istruzioni
case const_expr2 : istruzioni
...
default : istruzioni
}
-Risulta conveniente (e migliora la leggibilità del codice) usare
degli enum come valori possibili per i case.
es.
enum Animale {CANE, GATTO, TOPO};
Animale bu;
//...
switch(bu)
{
case CANE:
cout << “BAU!" << endl;
break;
case GATTO:
cout << “MIAO!” << endl;
break;
case TOPO:
cout << “SQUIT!” << endl;
break;
default:
cout << “Un minollo?” << endl;
break;
}
while
permette di eseguire iterativamente una istruzione (o blocco)
while (espressione)
istruzione
espressione viene valutata, se il suo valore
!= 0 allora viene eseguita istruzione ed
espressione viene valutata di nuovo.
Il ciclo si interrompe quando espressione
diventa falsa (uguale a 0).
-istruzione a seconda del valore di espressione
puo` anche non esser mai eseguita.
es.
while(i == 0 && j < 100)
{
//...
if (ww)
break; //esci dal while
if (kk)
continue;//riparti dalla iterazione succ.
v1[j] = v2[j] + v3[j++];//attenzione
}
do-while
controlla la condizione di uscita al termine
di ogni iterazione
do
istruzione
while (espressione);
-e` eseguito almeno una volta :
int k = 0;
...
do
{
…
k++;
} while(k < 100);
for
struttura iterativa alternativa allo while
for (espr1; espr2; espr3)
istruzione
equivale a:
espr1;
while (espr2)
{
istruzione
espr3;
}
-in molti casi è piu' comodo da usare
for( solo_la_prima_volta;
all_inizio_di_ogni_ciclo;
alla_fine_di_ogni_ciclo)
{
// in qualunque momento posso:
// uscire dal ciclo con break
// oppure andare direttamente
// alla iterazione succ. con continue
}
-esempio, trovare il numero di bit ad 1 della variabile x
unsigned int x, tmp;
unsigned char nbit;
//...
for(nbit = 0, tmp = x; tmp != 0; tmp >>= 1)
if( (tmp & 0x1) != 0)
nbit++;
//...
l'operatore virgola!
-esiste anche il goto!
goto identificatore;
identificatore : istruzione
-è meglio evitarlo! Ma in alcuni casi può servire
(codice generato automaticamente, applicazioni real-time...)
for (i = 0; i < n ; i++)
for (j = 0; j < m; j++)
if (a[i] == b[j])
goto trovato;//salta alla label trovato
//non ha trovato elementi comuni
//...
trovato:
//trovato un elemento in comune
//...
funzioni
parte di un programma che svolge un determinato compito
-dichiarazione: si specificano il nome della funzione, il tipo ed
il numero dei parametri in ingresso, il tipo del valore di ritorno
int lsh(const int& op1, const int& op2);
nella dichiarazione i nomi degli argomenti in ingresso sono utili per
aggiungere informazione sulla semantica della funzione, ma viene
ignorato dal compilatore:
extern char* strcpy(char* to, const char* from);
-definizione:
tipo-ritornato nome-funzione(dich. args)
{
dichiarazioni ed istruzione
}
-rispetto alla dichiarazione ho aggiunto il corpo della funzione
extern int min(int op1, int op2);//dichiar.
int min(int op1, int op2) //definizione
{
int min = op1 < op2? op1 : op2;
return min;
}
inline
-specificando che una funzione è inline si dice al compilatore di
espandere il codice di una funzione ad ogni sua chiamata piuttosto
di effettuare una chiamata vera e propria (efficienza/memoria).
...ora calma e sangue freddo...
inline int fatt(int i)
{ return i < 2 ? 1 : i*fatt(i-1); }
ricorsione: funzione che richiama se stessa
-quale è la sequenza di chiamate?
int res = fatt(5);
fatt(5)
fatt(4)
fatt(3)
fatt(2)
fatt(1)
->
->
->
->
->
5
4
3
2
1
*
*
*
*
fatt(4)
fatt(3)
fatt(2)
fatt(1)
=
=
=
=
120
24
6
2
la mente si confonde...allora poi si cita sempre la famosa frase:
l'iterazione è umana,
la ricorsione è divina!
parametri
-formali , attuali:
double sqrt(double d) { //... }
//...
double x, res;
//...
res = sqrt(x);
x:attuale
d:formale
-type checking, conversioni
-passaggio di parametri per valore e per riferimento
void f(int val, int& ref)
{
val++; //inc una copia locale del par. val
ref++; //inc il par. ref
}
-i maestri sconsigliano l'uso esteso del passaggio by ref (io no)
Comunque il passaggio per riferimento è essenziale per ottenere
un codice efficiente quando si definiscono funzioni che accettano
parametri in ingresso di grandi dimesioni (si evita la copia del par.)
void f(const TipoGrande& arg) { //... }
-parametri array:
un argomento T[] viene convertito in un T* nel passaggio, quindi
l'array non può essere passato per valore.
float dot(float v1[], float v2[], int dim)
{
float res = 0.0;
for(int i = 0; i < dim; i++)
res += v1[i] * v2[i];
//equivalente a:
// for(int i = 0; i < dim; i++)
//
res += *v1++ * *v2++;
return res;
}
-per gli array multidimensionali è necessario specificare tutte le
dimensioni tranne la prima:
//ERRORE
float* mul(float m1[][], float m2[][],
int d1, int d2, int d3, int d4 );
//OK
float* f(float m1[][100], float m2[][200],
int d1, int d2);
overload
stesso nome per operazioni diverse su tipi diversi
void print(int);
void print(char *);
attenzione: non si può ridefinire il tipo ritornato!
-parametri di default:
void print(int val, int base = 10)
//...
print(16);
print(16, 10);
print(16, 2);
16
16
10000
-i parametri opzionali vanno messi per ultimi!
-numero non specificato di parametri:
int printf(const char* ...);
è possibile farlo, ma l'utilità è veramente rara --> Manuale!
Preprocessore
realizza la prima fase, separata dalle altre, della compilazione
trasformando il codice sorgente
-principalmente:
#include "nomefile"
sostituisce la linea con il contenuto di nomefile (cerca nomefile nella
stessa dir, se non è specificato un path completo)
#include <nomefile>
come sopra, però nomefile viene cercato nelle directory standard di
inclusione: /usr/include, /usr/local/include
-tipicamente i file che vengono inclusi sono header file (.h)
questi contengono tipicamente:
•definizione di tipi, struct Color {int r,g,b;}
•template (vedremo)
•dichiarazione di funzioni, variabili, costanti
•dichiarazioe di nomi, struct Token
• #include, #define
•...
#define nome
testo da sostituire
sostituisce ad un identificatore una stringa arbitraria.
#define MAX
//...
int v[MAX];
//...
100
diventa:
//...
int v[100];
//...
Si usa per definire macro:
#define min(a,b)
//...
int c = min(1, x);
//...
a<b?a:b
diventa:
//...
int c = 1<x?1:x;
//...
-evitare l'uso eccessivo delle macro!
Il C++ offre costrutti alternativi: const, inline, template
inclusione condizionale
permette di inserire selettivamente parti di codice
-codice sorgente che compila correttamente su diverse architetture:
#ifdef UNIX
#include "unix.h"
#else
#include "msdos.h"
#endif
-definire in modo selettivo le macro
#ifdef VERBOSE
#define message(m) cerr << m;
#else
#define message(m)
#endif
-proteggere da inclusioni multiple:
#ifndef _HEADER_H_
#define _HEADER_H//contenuto del file header.h
#endif
varie
#warning “this header is version 1.2”
#error “I compile only from v1.2 on”
#pragma optimization(on)
# : string-ification
enum Colors { red, green, blue, yellow };
#define FILL_LIST(NAME) { NAME, #NAME }
struct {
Colors c;
const char *name;
} color_list[] = {
FILL_LIST(red),
FILL_LIST(green),
//...
{ 0, NULL }
};
-nomi predefiniti:
__LINE__
__FILE__
__DATE__
__TIME__
costante int, numero corrente del codice sorgente
stringa, nome del file sotto compilazione
stringa, data della compilazione
stringa, ora della compilazione
catena di compilazione
editor
.cc, .h
header file
di sistema
Preprocesor
cpp
.h
.ii codice espanso
Assembler
as
C++
Compiler
.s
assemby
.o
lib?.a
.o
Linker
ln
altri file
oggetto
exe
esecuzione
.so
librerie dinamiche
hello.cc (C++)
#include <iostream.h>
void main()
{
cout << "Ciao!!!" << endl;
}
hello.ii (Output del Preprocessore)
//qua sopra ci sono mooolte righe
class _IO_ostream_withassign : public ostream {
public:
_IO_ostream_withassign& operator=(ostream&);
_IO_ostream_withassign& operator=(_IO_ostream_withassign& r
{ return operator= (static_cast<ostream&> (rhs)); }
};
extern _IO_istream_withassign cin;
extern _IO_ostream_withassign cout, cerr;
extern _IO_ostream_withassign clog;
extern
extern
extern
extern
istream&
istream&
ostream&
ostream&
lock(istream& ins);
unlock(istream& ins);
lock(ostream& outs);
unlock(ostream& outs);
struct Iostream_init { } ;
inline ios& dec(ios& i)
{ i.setf(ios::dec, ios::dec|ios::hex|ios::oct); return i; }
inline ios& hex(ios& i)
{ i.setf(ios::hex, ios::dec|ios::hex|ios::oct); return i; }
inline ios& oct(ios& i)
{ i.setf(ios::oct, ios::dec|ios::hex|ios::oct); return i; }
}
# 1 "hello.cc" 2
void main()
{
cout << "Ciao!!!" << endl;
}
hello.s (Assembly)
.file
"hello.cc"
gcc2_compiled.:
___gnu_compiled_cplusplus:
.def
___terminate;
.scl
.endef
.def
___sjthrow;
.scl
.endef
.def
___main;
.scl
.endef
.text
LC0:
.ascii "Ciao!!!\0"
.align 4
.globl _main
.def
_main; .scl
2;
.endef
_main:
pushl %ebp
movl %esp,%ebp
subl $8,%esp
call ___main
addl $-8,%esp
pushl $LC0
pushl $_cout
call ___ls__7ostreamPCc
addl $16,%esp
addl $-12,%esp
pushl %eax
call _endl__FR7ostream
movl %ebp,%esp
xorl %eax,%eax
popl %ebp
ret
.def
_endl__FR7ostream;
32;
.endef
.def
___ls__7ostreamPCc;
32;
.endef
2;
.type
32;
2;
.type
32;
2;
.type
32;
.type
32;
.scl
2;
.type
.scl
3;
.type
Il file eseguibile (hello.exe)
^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@^@
<83><EC>^X<83>=^@ A^@^@t^A<CC><D9>}<FE>^O<B7>E<FE>
<B7>E<FE>^M?^C^@^@f<89>E<FE><D9>m<FE><83><C4><F4>h
<C3><90><90><90>Ciao!!!^@U<89><E5><83><EC<E8>M<DA>
<C4><F8>hD^P@^@h$ A^@<E8><B3>^R^@^@<83><C4>^P<89><
1<C0><EB>^A<90><89><EC>]<C3>U<89><E5><8B>;^U^D A^@
<D2>t,<C7>^E<AC> A^@^@0A^@<C7>^E( A^@`0A^@<C7>^Eh
A^@<EB>*<89><F6><C7>^E<AC> A^@$!A^@<C7>^E( A^@<84>
<E8> A^@<E4>!A^@<A1>^D A^@<89>^U^D A^@<89><EC>]<C3
:<95>^@^@<89><C3><83>}^L^@tJ<8B><83><C0<8B><89>^B<
<C7>^@^@^@^@<C7>@^D^@^@^@^@<C7>^@^@^@^@f<C7>@^P ^@
<C7>@^X^@^@^@^@<8B>C^D<8D>S^D<89>E<E0><C7>E<E4>^@^
<84>^Q@^@<89><8D>M<E0><89>
<8B><8B>^B<C7>@^\<C4>%A^@<83>}^L^@u1<8D>}<C0><BE><
<A5><8B>^B<8B><8D>U<C0><89>P^\<89><CA><8B>^A<83><C
<D0>f<89>E<C8><8B><8B>^Q<83>}^P^@u
<C6>B^R^D<
<8B>E^P<89>^B<8B>E^T<89>B^D<C7>^@^@^@^@f<C7>B^P ^@
<C7>B^X^@^@^@^@<8B><C7>B^D^@^@^@^@<8B>C^D<8B>^@<89
<89><E5><83><EC>^TS<8B><89><F6><83><C4><F4>S<E8><F
<FA><FF>t^L<A1><E0>@A^@<F6>D^P^u<E1><8B>]<E8><89><
<EC>^TS<8B><8B>^C<8A>P^R<84><D2>t
<80><CA>^B
<C9>t^V<8B>^P<8B>B^D9u^L<83><C4><F4>Q<E8>
^T^@^@<83><C4>^P<8B>^C<83><C4><F4><8B>^@P<E8><8E>A
<8B>E^L<88>^P<C7>C^D^A^@^@^@<EB>^N<90><8B>^C<80>H^
]<E8><89><EC>]<C3><89><F6>U<89><E5><83><EC>^PV<8B>
<FF><FF><FF><EB>Q<89><F6><8B>H^D<85><C9>t^V<8B>^P<
<96>^S^@^@<83><C4>^P<8B>^F<8B>^X<8B>9C^Dr^N<83><C4
<FF>t
<8B>C^D^O<B6>^P<EB>^C<90><89><C2><83><FA><
<8D>e<E8>[^<89><EC>]<C3>U<89><E5><83><EC>^LWVS<8B>
<8A>P^R<84><D2>t
<80><CA>^B<88>P^R<EB>]<90>
^D9u^L<83><C4><F4>Q<E8>^V^S^@^@<83><C4>^P<8B>^C<8B
<E8><93>^^^@^@<89>C^D<EB>&<89><F6><8B>^C<80>H^R^C<
^@<83><C4>^P<83><F8><FF>t<E4><FF>C^D;E^Pu<E4><8D>e
...che però si può disassemblare:
hello.exe:
file format pei-i386
Disassembly of section .text:
00401000 <_mainCRTStartup>:
401000:
55
401001:
89 e5
401003:
83 ec 18
401006:
83 3d 00 20
40100d:
74 01
40100f:
cc
401010:
d9 7d fe
401013:
0f b7 45 fe
401017:
25 c0 f0 ff
40101c:
66 89 45 fe
401020:
0f b7 45 fe
401024:
0d 3f 03 00
401029:
66 89 45 fe
40102d:
d9 6d fe
401030:
83 c4 f4
401033:
68 4c 10 40
401038:
e8 1b da 00
40103d:
89 ec
40103f:
5d
401040:
c3
401041:
90
401042:
90
401043:
90
00401044 <.text>:
401044:
43
401045:
69
0040104c <_main>:
40104c:
55
40104d:
89
40104f:
83
401052:
e8
401057:
83
40105a:
68
40105f:
83
401062:
68
401067:
68
40106c:
e8
401071:
83
401074:
89
401076:
50
41 00 00
ff
00
00
00
61 6f 21 21 21 00
e5
ec
4d
c4
d0
c4
44
24
b3
c4
c0
08
da
f8
27
f8
10
20
12
10
00 00
40 00
40 00
41 00
00 00
push
mov
sub
cmpl
je
int3
fnstcw
movzwl
and
mov
movzwl
or
mov
fldcw
add
push
call
mov
pop
ret
nop
nop
nop
%ebp
%esp,%ebp
$0x18,%esp
$0x0,0x412000
401010 <_mainCRTStar
inc
imul
%ebx
$0x212121,0x6f(%ecx),%esp
push
mov
sub
call
add
push
add
push
push
call
add
mov
push
%ebp
%esp,%ebp
$0x8,%esp
40eaa4 <___main>
$0xfffffff8,%esp
$0x4027d0
$0xfffffff8,%esp
$0x401044
$0x412024
402324 <___ls__7os
$0x10,%esp
%eax,%eax
%eax
0xfffffffe(%ebp)
0xfffffffe(%ebp),%eax
$0xfffff0c0,%eax
%ax,0xfffffffe(%ebp)
0xfffffffe(%ebp),%eax
$0x33f,%eax
%ax,0xfffffffe(%ebp)
0xfffffffe(%ebp)
$0xfffffff4,%esp
$0x40104c
40ea58 <_cygwin_crt0>
%ebp,%esp
%ebp
librerie
un insieme di file oggetto (.o) ottenuti compilando i corrispondenti
file sorgente (.c, .cc) accompagnati da uno o più header file (.h)
con le dichiarazioni per l'uso dei file .o
-supponiamo di voler scrivere una libreria per la crittografia DES, un
possibile header file:
#ifndef _DESCRYPT_H_
#define _DESCRYPT_H_
//necessario per utilizzare librerie C in C++
//istruisce il linker sul modo di chiamata (ABI)
extern "C" {
void encrypt(char *block, int edflag);
void setkey(char *key);
char* crypt(const char *key, const char *salt);
}
#endif
-supponiamo di aver definito queste funzioni nei rispettivi file C:
encrypt.c
setkey.c
crypt.c
-ad esempio lavorando su un sistema UNIX, la seguente serie di
comandi genera la libreria descrypt.a:
$ cc -c encrypt.c setkey.c crypt.c
$ ar cr descrypt.a encrypt.o setkey.o crypt.o
$ ranlib descrypt.a
posso ispezionare il contenuto di una libreria (implementazioni vuote!):
$ nm -s descrypt.a
Archive index:
_encrypt in encrypt.o
_setkey in setkey.o
_crypt in crypt.o
encrypt.o:
00000000 b
00000000 d
00000000 t
00000000 t
00000000 T
00000000 t
.bss
.data
.text
___gnu_compiled_c
_encrypt
gcc2_compiled.
setkey.o:
00000000 b
00000000 d
00000000 t
00000000 t
00000000 T
00000000 t
.bss
.data
.text
___gnu_compiled_c
_setkey
gcc2_compiled.
crypt.o:
00000000
00000000
00000000
00000000
00000000
00000000
.bss
.data
.text
___gnu_compiled_c
_crypt
gcc2_compiled.
b
d
t
t
T
t
-uso della libreria:
nella mia applicazione (secure_link.cc) includerò il file header
che corrisponde alla libreria e farò chiamate alle funzioni là definite
#include "descrypt.h"
//...
setkey(sessionkey);
mycrypt = crypt(sessionkey, sugar);
//...
-per compilare la mia applicazione:
$ c++ secure_link.cc descrypt.a -o secure_link
in questo modo il linker estrae i file .o dalla libreria e li collega con
il file secure_link.o garantendo che venga fornita la definizione
delle funzioni di libreria richiamate in secure_link.cc
-librerie dinamiche (.so sotto UNIX, .dll sotto WINDOWS):
vengono incluse al momento della esecuzione, dimensioni ridotte
degli eseguibili.
-riutilizzo del codice
classi
una classe (class) è un tipo definito dall'utente
e le struct allora?
anche loro!
//definisco il tipo
struct Date { int day, month, year; };
//definisco le operazioni sul tipo
void set_date(date*, int, int, int);
void get_date(date*, int&, int&, int&);
void tomorrow_date(date*);
void yesterday_date(date*);
void print_date(const date*);
-è un pò scomodo, non c'è un legame tra le funzioni ed il tipo (se non
nei parametri e nel nome scelto opportunamente)
-una cosa che ancora non vi avevo detto a proposito delle struct:
struct Date
{
int day, month, year;
//dichiarazione metodi (o funzioni proprie)
void set(int, int, int);
void get(int&, int&, int&);
void tomorrow();
void yesterday();
void print();
};
-i metodi possono essere richiamati solo per una variabile del
tipo che gli compete:
Date
today, xmas;
//...
today.set(24, 7, 2001);
xmas.set(25, 12, 2001);
today.tomorrow();
xmas.print();
today.print();
l'operatore di
scope resolution!
-definizione di un metodo:
void Date::tomorrow()
{
if(++day > 28)
//trentagiornihanovembrecon...
}
Date:: è necessario, potrei aver dichiarato il metodo
void tomorrow() anche per un altra struct
-rimane l'imbarazzante capacità di modificare lo stato interno del
tipo Date manipolando direttamente i suoi campi (e non per mezzo
delle operazioni implementate dai metodi):
today.day = today.month = today.year = -13;
e allora entrano in gioco le classi!
class Date
{
int day, month, year;
public:
void set(int, int, int);
void get(int&, int&, int&);
void tomorrow();
void yesterday();
void print();
};
parte privata
parte pubblica
-i nomi contenuti nella parte privata possono essere manipolati solo
dai metodi della classe
-la parte pubblica è anche detta interfaccia agli oggetti della classe
-in tutte le parti possono essere presenti sia dati(attributi)
che funzioni(metodi)
Oggetto = istanza di una classe.
Identificato dal nome, definisce uno stato che è rappresentato dal
valore dei suoi attributi a un certo istante di tempo.
int i;
int è la classe (il tipo)
i è il nome dell'oggetto(variabile)
encapsulation: nascondere tutti i dettagli di un oggetto che non
contribuiscono in maniera essenziale alle sue caratteristiche
(esposte tramite l'interfaccia)
-una struct è una classe in cui tutti i membri sono pubblici.
void Date::print()
{
cout << day << '/' << month << '/' << year;
}
va tutto bene, Date::print è un metodo della classe Date
ed ha accesso alla sua parte privata.
void print_date(Date day)
{
cout << day.day << '/' << day.month
<< '/' << day.year;
}
non va bene, la funzione print_date non può leggere gli attributi
privati di un oggetto della classe Date
dichiarazione di una classe:
// in particle.h
class Particle
{
private:
// chi lo può chiamare ?
void SetMass(double m);
protected:
Vector
q; //attributi
Vector
p; //o variabili membro
int
charge;
double
mass;
public:
Particle(const Vector& q, //un costruttore
const Vector& p,
int charge,
double mass);
~Particle();
//un distruttore
double GetMass() const
{ return this->mass; }
};
//un metodo
-protected: meno privata di private, vedremo parlando di classi
derivate.
-costruttore: metodo per l'inizializzazione degli oggetti,
ha lo stesso nome della classe
-distruttore: metodo richiamato quando un oggetto esce dallo scope
in cui è stato dichiarato, la memoria da esso occupata viene liberata.
Il distruttore per la classe T si chiama ~T()
-metodo const: può leggere ma non modificare l'oggetto per cui viene
richiamata
-this: nome sempre disponibile nei metodi, per una classe T è di
tipo T* e rappresenta il puntatore all'oggetto di invocazione
-inlining: defininendo metodi nella dichiarazione della classe questi
saranno automaticamente considerate dal compilatore come funzioni
inline (comodo per metodi "piccoli" richiamati di frequente)
-implementazione della classe:
// in particle.cc
Particle::Particle(const Vector& q,
const Vector& p,
int c,
double mass)
{
m_q
= q;
m_p
= p;
m_charge
= c;
m_mass
= mass;
}
void Particle::SetMass(double mass)
{
this->m_mass = mass; // this-> e` opzionale
}
-this e` una varibile sempre disponibile dentro l’implementazione
di ogni metodo di una classe; e` di tipo NOME_CLASS*
in questo caso Particle* .
Più precisamente: Particle* const this
E` come se ogni metodo avesse un ulteriore parametro nascosto
che serve per accedere ai membri del nostro oggetto.
-utile per le classi contenitore: alberi, liste...
Interfaccia-Implementazione
Classe = scatola nera su cui si agisce per mezzo di un certo insieme
di operazioni (interfaccia)
Fintanto che l'interfaccia è fissata, la effettiva realizzazione della
classe (implementazione) può subire dei cambiamenti senza che
l'utente della classe se ne accorga.
-static: questo è uno dei nomi più sovraccaricati di significati nel
mondo dei linguaggi di programmazione (lo abbiamo già incontrato).
Quando lo si usa nella dichiarazione di un membro di una classe
significa:
membro comune a tutti gli oggetti della classe
class Elettrone
{
static double massa;
static int carica;
protected:
Vector
q;
Vector
p;
};
//...
double Elettrone::mass = 9.1091e-31;
int Elettrone::carica = -1;
//...
-anche i metodi possono essere dichiarati static
Un esempio
file geom.h:
class Mat_3_3; //forward declaration
//realizza un vettore 3D
class Vett_3
{
protected:
double v[3];
public:
Vett_3(double x = 0.0,
double y = 0.0,
double z = 0.0)
{ v[0] = x; v[1] = y; v[2] = z; }
void Stampa();
//modulo del vettore
double Mod();
//moltiplicazione per matrice 3x3
Vett_3 Molt(const Mat_3_3& );
//overload dell'operatore somma
//è parte dell'interfaccia,
//anche se non è un metodo
friend Vett_3 operator*(const double,
const Vett_3&);
friend Vett_3 operator+(const Vett_3& ,
const Vett_3&);
};
//realizza una matrice 3x3
class Mat_3_3
{
double m[3][3];
public:
Mat_3_3(double m00 = 0.0,
double m01 = 0.0,
double m02 = 0.0,
double m10 = 0.0,
double m11 = 0.0,
double m12 = 0.0,
double m20 = 0.0,
double m21 = 0.0,
double m22 = 0.0);
void Stampa();
//è parte dell'interfaccia,
//anche se non è un metodo di Mat_3_3
friend Vett_3 Vett_3::Molt(const Mat_3_3& m);
};
-friend: una funzione (anche come nel nostro esempio degli
operatori e delle funzioni membro) che ha accesso alla parte
privata di una classe.
Dal momento che può manipolare liberamente gli oggetti di una classe
è opportunamente inserita nella dichiarazione della classe (nella
interfaccia).
class A
{
friend class B;
//...
};
-il senso della dichiarazione è che tutti i metodi di B sono funzioni
friend di A.
-gli operatori sono spesso dichiarati friend
L'operatore visto prima:
friend Vett_3 operator*(const double,
const Vett_3&);
nella sua definizione avrà libero accesso alla parte privata degli oggett
della classe Vett_3.
L'operatore accetta const Vett_3& come secondo parametro
perchè non deve modificarlo ma allo stesso tempo il compilatore
non devrà fare una copia del parametro.
-file geom.cc;
#include <math.h>
#include <iostream.h>
#include "geom.h"
//metodi ed operatori di Vett_3
void Vett_3::Stampa()
{ cout << "(" << v[0] << '\t' << v[1] << '\t'
<< v[2] << ")" << endl; }
double Vett_3::Mod()
{
double ris = sqrt( v[0]*v[0] + v[1]*v[1]
+ v[2]*v[2] );
return ris;
}
Vett_3 Vett_3::Molt(const Mat_3_3& mat)
{
Vett_3 ris;
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 3; j++)
ris.v[i] += mat.m[i][j] * v[i];
}
return ris;
}
//quelli che seguono non sono metodi di Vett_3
Vett_3 operator*(const double op1, const
Vett_3& op2)
{
Vett_3 ris;
for(int i = 0; i < 3; i++)
ris.v[i] = op1 * op2.v[i]; //!!!Friend!!!
return ris;
}
Vett_3 operator+(const Vett_3& op1,
const Vett_3& op2)
{
Vett_3 ris;
for(int i = 0; i < 3; i++)
ris.v[i] = op1.v[i] + op2.v[i];//idem
return ris;
}
//metodi di Mat_3_3
Mat_3_3::Mat_3_3(double m00 = 0.0,
double m01 = 0.0,
double m02 = 0.0,
double m10 = 0.0,
double m11 = 0.0,
double m12 = 0.0,
double m20 = 0.0,
double m21 = 0.0,
double m22 = 0.0)
{
m[0][0] = m00; m[0][1] = m01; m[0][2] = m02;
m[1][0] = m10; m[1][1] = m11; m[1][2] = m12;
m[2][0] = m20; m[2][1] = m21; m[2][2] = m22;
}
void Mat_3_3::Stampa()
{
for(int i = 0; i < 3; i++)
{
for(int j = 0; j < 3; j++)
cout << m[i][j] << 't';
cout << endl;
}
}
#include <iostream.h>
#include "geom.h"
#define PI 3.14159265358979323846
void main()
{
Vett_3 x(0.0, 3.0, 4.0);
cout << "X:\t";
x.Stampa();
cout << "Modulo(X): " << x.Mod() << endl;
Vett_3 y(0.0, -5.0, 3.0);
Vett_3 z = 2.0 * (x + y);
cout << "Z:\t";
z.Stampa();
double theta = PI/4.0;
double phi
= PI/4.0;
Mat_3_3 Mat(cos(phi), -sin(phi), 0.0,
sin(phi), cos(phi), 0.0,
0.0 ,
0.0,
1.0);
cout << "Mat:" << endl;
Mat.Stampa();
Vett_3 z_phi = z.Molt(Mat);
cout << "z_phi:\t";
z_phi.Stampa();
-ottengo:
$ ./geotest.exe
X:
(0
Modulo(X): 5
Z:
(0
Mat:
0.707107
0.707107
0
z_phi: (0
3
4)
-4
14)
-0.707107
0.707107
0
-5.65685
0
0
1
14)
-supponiamo di voler introdurre un nuovo tipo di dato che rappresenti
un versore in 3 dimensioni.
Potrei dichiararlo nella seguente maniera:
class Vers_3
{
protected:
double v[3];
public:
Vers_3(double x = 0.0,
double y = 0.0,
double z = 0.0);
void Stampa();
//modulo del versore, si spera = 1.0
double Mod();
//moltiplicazione per matrice 3x3
Vers_3 Molt(const Mat_3_3& );
friend Vers_3 operator*(const double,
const Vers_3&);
friend Vers_3 operator+(const Vers_3& ,
const Vers_3&);
};
Somiglia tantissimo alla dichiarazione del Vett_3: i dati sono gli stessi,
le operazioni pure, anche se alcune sono modificate nel
comportamento: Costruttore, Mod()...
-rendo esplicito quello che le classi Vett._3 e Vers_3 hanno in
comune utilizzando il meccanismo della derivazione di classi
-file geom.h:
class Vers_3 : public Vett_3
{
public:
//crea un versore
Vers_3(double x, double y, double v);
//converte in Vett_3 in un Vers_3
Vers_3(const Vett_3& v3);
//crea un versore da coordinate sferiche
Vers_3(double theta, double phi);
//lo sto ridefinendo rispetto alla classe base
void Stampa();
};
Vers_3 specializza la classe Vett_3 al concetto di versore.
Vers_3 è una classe derivata publicamente da Vett_3.
Eredita tutti i dati e le funzioni di Vett_3 dichiarate public
o protected.
Vett_3 è una classe di base per Vers_3.
-importante: è possibile assegnare ad un puntatore a Vett_3 un
puntatore ad un oggetto Vers_3 senza dover fare il cast
esplicito (conversione).
Utilizzando i puntatori si può trattare un oggetto di una classe derivata
come se fosse un oggetto della classe di base.
-file geom.cc:
Vers_3::Vers_3(double x = 0.0,
double y = 0.0,
double z = 0.0) : Vett_3(x, y, z)
{
double inv_mod = 1.0/Mod();
v[0] = inv_mod * v[0];
v[1] = inv_mod * v[1];
v[2] = inv_mod * v[2];
}
Vers_3::Vers_3(const Vett_3& v3) : Vett_3(v3)
{
double inv_mod = 1.0/Mod();
v[0] = inv_mod * v[0];
v[1] = inv_mod * v[1];
v[2] = inv_mod * v[2];
}
Vers_3::Vers_3(double theta, double phi)
{
v[0] = sin(theta) * cos(phi);
v[1] = sin(theta) * sin(phi);
v[2] = cos(theta);
}
void Vers_3::Stampa()
{ cout << "<" << v[0] << '\t' << v[1] << '\t'
<< v[2] << ">" << endl; }
costrutore: ho richiamato il costuttore della classe di base nel
costruttore della classe derivata.
Sto trattando la classe di base come un oggetto proprio della classe
derivata.
-ordine di costruzione: classe di base, elementi, classe derivata
-ordine di distruzione: classe derivata, elementi, classe di base
-Nei costruttori ho richiamato il metodo Mod(), chi era l'oggetto
d'invocazione?
-uso:
#include <iostream.h>
#include "geom.h"
#define PI 3.14159265358979323846
void main()
{
Vers_3 versore(PI/4.0, PI/4.0);
cout << "versore(PI/4.0, PI/4.0):" << endl;
versore.Stampa();//quale Stampa() chiama?
double theta = PI/4.0;
double phi
= PI/4.0;
Mat_3_3 Mat(cos(phi), -sin(phi), 0.0,
sin(phi), cos(phi), 0.0,
0.0 ,
0.0,
1.0);
//ma come, molt non era un metodo di Vett_3
//che restituiva un Vett_3???
Vers_3 ris = versore.molt(Mat);
cout << "...dopo una rotazione di PI/4.0
lungo l'asse Z:" << endl;
ris.Stampa();
}
-ottengo:
$ ./geotest.exe
versore(PI/4.0, PI/4.0):
<0.5
0.5
0.707107>
...dopo una rotazione di PI/4.0 lungo l'asse Z:
<3.92481e-17
0.707107
0.707107>
è stato richiamato Vers_3::Stampa(), come probabilmente ci
aspettavamo.
Ma se avessi scritto:
//...
Vers_3 ris = versore.Molt(Mat);
Vett_3* pv = &ris;//si può fare!
cout << "...dopo una rotazione di PI/4.0
lungo l'asse Z:" << endl;
pv->Stampa();
//...
-avrei ottenuto:
$ ./geotest.exe
versore(PI/4.0, PI/4.0):
<0.5
0.5
0.707107>
...dopo una rotazione di PI/4.0 lungo l'asse Z:
(3.92481e-17
0.707107
0.707107)
brutto!
il C++ prevede la soluzione di questo problema tramite la keyword
virtual
Basta aggiungere questa parola chiave nella dichiarazione dei metodi
della classe di base che si vogliono ridefinire nelle classi derivate:
class Vett_3
{
protected:
double v[3];
public:
//...
virtual void Stampa();
//...
};
con questa modifica otterremo:
$ ./geotest.exe
versore(PI/4.0, PI/4.0):
<0.5
0.5
0.707107>
...dopo una rotazione di PI/4.0 lungo l'asse Z:
<3.92481e-17
0.707107
0.707107>
polimorfismo!
controllo dell'accesso
-private: il nome può essere usato solo dai metodi e dalle funzioni
amiche della classe.
-protected: il nome può essere usato solo dai metodi e dalle funzioni
amiche della classe e delle classi derivate
-public: il nome è utilizzabile da qualunque funzione
variabile
utente
public
ok
protected
private
Nell'esempio abbiamo visto una derivazione public.
In realtà (anche se si usa raramente) si può fare anche una derivazione
protected o private --> Manuale!
classe
base
variabile
public
nella classe derivata
derivazione derivazione derivazione
public
protected
private
public
protected
private
protected
private
protected protected
private
Memoria dinamica
operatori New e Delete
-finora abbiamo creato oggetti automatici (sullo stack) o a livello file:
char *bubu;
int ciccio;
...
void funzione()
{
int anni = 0;
//...
-ma si puo` fare allocazione dinamica:
// chiama costruttore
Vers_3* pv = new Vers_3(theta,phi);
//...
pv->Stampa();
//...
delete pv; // qui viene chiamato
// il distruttore
-gli oggetti dinamici hanno vita fino alla distruzione esplicita con
delete
-si possono creare dinamicamente tutti i tipi , base, composti o classi:
int *index = new int(1123);
double *num = new double;
*num = 12.34;
int j = *index;
delete index; delete num;
-pure vettori, ma attenti ai costruttori con parametri e al delete:
int* vec = new int[100];
Triangle* t1 = new Triangle[20]; //no params
vec[33] = 23;
//…
delete [] vec;
Scarica

ppt - Infn