Programmazione Avanzata per il Calcolo Scientifico Lezione N. 2

Transcript

Programmazione Avanzata per il Calcolo Scientifico Lezione N. 2
Programmazione Avanzata per il Calcolo
Scientifico
Lezione N. 2
Luca Formaggia
MOX
Dipartimento di Matematica “F. Brioschi”
Politecnico di Milano
A.A. 2011/2012
1/1
2/1
Vettori e matrici in C++
Il C++ prevede diversi modi per organizzare dati che permettano
l’accesso diretto della memoria tramite un indice (vettore) o più
indici (matrici)
I
Gli array “nativi” che si distinguono in statici (quando la
dimensione è nota a-priori), dinamici (vengono allocati
run-time) e automatici (estensione NON STANDARD del
g++)
I
I vector<T> della standard library. Un esempio di contenitore
che permette l’acesso diretto con complessità O(1).
L’utente può inoltre costruire delle classi specializzate (o usare
quelle disponibili in molte librerie pubbliche come SparseLib++)
3/1
Gli array “nativi”
Gli array nativi statici in C++ sono dichiarati nel modo seguente
f l o a t a [ 4 ] ; // un a r r a y d i 4 e l e m e n t i f l o a t
double b [ 2 ] [ 3 ] ; // un a r r a y 2 x3 d i d o u b l e
i n t c [ 2 ] [ 2 ] = { { 1 , 2 } , { 7 , 8 } } ; // i n i z i a l i z z a z i o n e
i n t a=c [ 0 ] [ 1 ] // a c c e s s o a un e l e m e n t o
f l o a t p [ ] = { 1 . 0 , 2 . , 4 . 5 } ; // d i m e n s i o n a m e n t o i m p l i c i t o
4/1
Organizzazione degli array
Questi sono array di dimensione predefinita. Il C++ ordina gli array
multidimensionali per riga.
int p[2][3]={{1,2,3},{4,5,6}}
[0][0]
1
[0][1] [0][2]
2
3
[1][0] [1][1] [1][2]
4
5
6
p
5/1
Gli array visti fin qui sono array a dimensionamento statico. E‘
possibile avere dimensionamento indefinito o dinamico (run time).
Il primo è possibile solo per gli argomenti di una function:
int myfunc(float a[][2]);
Questa funzione ha come argomento un array bidimensionale di
float la cui seconda dimensione è 2 mentre la prima non è
specificata. In generale, solo la prima dimensione può essere
omessa. Questo è legato al modo in cui un array è organizzato in
memoria.
Il dimensionamento dinamico di array nativi utilizza l’operatore new
[] verrà presentato insieme ai puntatori.
6/1
Array automatici
Il compilatore g++ ha come estensione la possibilità di usare array
automatici:
double myfun(int n, ...){
double a[n] // array automatico
...}
ATTENZIONE: è una estensione NON PORTABILE. Lo standard
C++ NON prevede array automatici
7/1
typedef: parte 1
Il comando typedef crea degli alias ai tipi. È un comando molto
utile
t y p e d e f unsigned i n t U i n t ;
t y p e d e f v e c t o r <f l o a t > Vf ;
t y p e d e f double R e a l ;
t y p e d e f R e a l myvect [ 2 0 ] ;
...
Vf v ; // v e ‘ un v e t t o r e d i f l o a t
myvect z // z è un a r r a y d i 20 d o u b l e
Regola d’uso: se togliete typedef avete la dichiarazione di una
variabile:
L’uso di typedef semplifica la scrittura (e lettura) di un codice:
typedef vector<vector<double∗> > Vvdb
8/1
Puntatori
I puntatori sono delle variabili che possono contenere l’indirizzo (in
memoria) di un oggetto o di una funzione. Il loro uso è sopratutto
legato a array a dimensionamento variabile, alla implementazione
del polimorfismo o alla gestione di funzioni argomento di funzioni.
Il linguaggio C++ si sta evolvendo nella direzione di ridurre l’uso
dei puntatori nativi. Ciò è possibile usando smart pointers per
memorizzare l’indirizzo di una variabile, contenitori STL al posto di
array, funtori al posto di puntatori a funzione (come vedremo in
seguito).
Tuttavia l’uso dei puntatori è ancora molto importante.
9/1
Esempi di uso di puntatori
int* pi(0) ; // puntatore a int inizializzato nullo
char** ppc ; // puntatore a puntatore a carattere
int* ap[15]; // array di 15 puntatori a int
double (*) v[3];// puntatore ad un array di 3 doubles
int (*fp)(char*); // puntatore a una funzione
// che prende un char* come argumento e restituisce un int
int f; pi=&f’;//ora p punta a f
(*pi == f) ;// il risultato è ’true’
In C++ il valore 0 indica il puntatore nullo (null pointer).
L’operatore (unario) di dereferenziamento (dereferencing operator)
* restituisce il contenuto della variabile indirizzata dal puntatore,
mentre l’operatore (unario) d’indirizzamento (&) estrae l’indirizzo
di una variabile.
10/1
Puntatori ed array
Puntatori ed array sono concetti collegati
double * pa;
pa=new double[10];//ora pa punta a un array di 10 elementi
pa[3]=9.0;
...
delete []pa;
L’operatore new (nella sua forma standard) richiede memoria al
sistema operativo, nelle tre forme
T* new T
Puntatore a un elemento di tipo T
T* new T(z) Puntatore a un elemento di tipo T
l’oggetto è inizializzato al valore z
T* new T[m] Puntatore a m elementi di tipo T
11/1
Organizzazione della memoria
int i; double a;
float d[2]={1.,2.};
class Vector;
STACK
new double[10];
new float[m];
FREE STORE
double fun(double a){
......}
TEXT
Nello stack prendono posto tutte le variabili la cui dimensione è
nota in fase di compilazione: variabili statiche e variabili
automatiche.
Nel free store (detta anche heap) prendono posto le variabili
allocate “run time” attraverso l’operatore new.
L’area text contiene il codice macchina che traduce il programma.
La distinzione è importante perchè i meccanismi di gestione sono
differenti. In generale, la dimensione della free store richiesta da un
programma non è conosciuta se non a run-time, a differenza di
quella dello stack.
12/1
E se non ci fosse memoria sufficiente nel free store?
Se sistema operativo non è in grado di fornire la memoria richiesta,
new lancia una eccezione di tipo bad_alloc che normalmente fa
abortire il programma.
L’header <new> mette a disposizione una versione di new indicata
da new(std::nothrow) che restituisce il puntatore nullo nel caso di
memoria insufficiente.
#i n c l u d e <new>
#i n c l u d e <i o s t r e a m >
#i n c l u d e < c s t d l i b > // f o r e x i t ( )
..
double ∗ pa ;
i f ( ! ( pa=new ( s t d : : nothrow ) double [ 1 0 0 ] ) ) {
s t d : : cout << ’ ’ Memoria i n s u f f i c i e n t e ’ ’<<s t d : : e n d l ;
c a l l std : : e x i t (1);
\}
...
Il comando std::exit(1) interrompe il programma restituendo al
sistema operativo uno status pari a 1.
13/1
Un’altra possibilità è dire a new() cosa fare attraverso un
new_handler
#i n c l u d e <new>
...
v o i d o u t _ o f _ s t o r e ( ) // F u n x i o n e d e f i n i t a d a l l ’ u t e n t e
{
c e r r << " Not ␣ enough ␣memory"<<e n d l ;
throw b a d _ a l l o c ( ) ; // l a n c i a e c c e z i o n e
}
. . . // n e l programma
// s i d i c h i a r a l ’ h a n d l e r
set_new_handler ( o u t _ o f _ s t o r e ) ;
...
double ∗ a=new double ( 1 0 0 0 0 ) ;
// Se non c ’ e ‘ memoria s u f f i c . v i e n e c h i a m a t o
// o u t _ o f _ s t o r e ( )
..
14/1
delete
L’operatore delete applicato a un puntatore restituisce al sistema
la memoria dell’elemento “puntato”. Se si tratta di un array occorre
usare delete[]. Altrimenti si restituisce al sistema solo primo
elemento dell’array!
Regola d’oro: Ogni new deve avere un delete e ogni new [] un
delete [].
Un’altra buona regola: A meno di casi particolari (che vanno ben
documentati) il responsabile della creazione di un oggetto è anche
responsabile della sua cancellazione (sarà più chiaro quando si
introdurranno le classi).
15/1
Puntatori doppi e array multidimensionali
double ** pa;
pa= new double *[4];
for(int i=0;i<4;++i)pa[i]=new double[3](3.0);
// ora posso indirizzare pa come un array double[4][3];
for(int i=0;i<4;++i) for(int j=0;j<3;++j)pa[i][j]=i*j;
..
for(int i=0;i<4;++i) delete[] pa[i];
delete[] pa;
16/1
Layout della memoria
pa
double ** pa;
pa[0]
[0][0] [0][1] [0][2]
pa[1]
[1][0] [1][1] [1][2]
pa[2]
[2][0] [2][1] [2][2]
pa[3]
[3][0] [3][1] [3][2]
pa[ i ][ j ] si traduce in ∗(∗(pa+i)+j) che fa un offset di
i ∗sizeof(double ∗) seguito da un offeset di j∗sizeof(double) per
raggiungere l’indirizzo cercato.
17/1
Errori comuni
double *p; double * t;
p=new double[10];
delete p;// delete []!
delete t;// t non è assegnato!
Sono 2 errori gravi. Il secondo non sarebbe un errore se avessi
inizializzato t al puntatore nullo!
Un altro errore comune consiste nel dimenticare il delete,
causando i cosidetti memory leak.
Regole generale Limitare l’uso dei pointers, e preferire gli “smart
pointer” (argomento di una prossima lezione). Inizializzare sempre i
puntatori, eventualmente al puntatore nullo:
double * t(0);
18/1
Array dinamici con una dimensione variabile
Puntatori multipli per identificare array multidimensionali sono
inefficienti in quanto il layout in memoria non è, in generale,
contiguo. Il compilatore non può quindi ottimizzare l’accesso alla
memoria cache.
Meglio usare le matrici fornite da librerie specializzate per algebra
lineare (quali eigen o le ublas). Tuttavia un caso particolare è
rappresentato dagli array multidimensionali dove solo una
dimensione è dinamica.
19/1
Array dinamici con una dimensione variabile
Consideriamo per esempio il caso di un vettore che contenga le
coordinate dei punti di una griglia 3D. Conosceremo solo a
run-time, quando leggiamo il file con la griglia, ma si sa a priori il
numero di coordinate: 3.
Un puntatore ad un array la cui la seconda dimensione è 3 viene
definito nel modo seguente:
double ( ∗ ) v [ 3 ] ;
e può essere usato come segue
20/1
Array dinamici con una dimensione variabile
t y p e d e f double AD [ 3 ] ; // p e r s e m p l i c i t a
...
AD ∗ p = new double [ n ] [ 3 ] ; // a r r a y n X 3
// l o r i e m p i o con d e i v a l o r i
f o r ( i n t i =0; i <n;++ i )
f o r ( i n t j =0; j <3; ++j ) p [ i ] [ j ]= double ( i ∗ j ) ;
double * pa[3];
pa
pa[0] pa[1] pa[2]
*pa[0]
{
{
[0][0] [0][1] [0][2] [1][0] [1][1] [1][2] [2][0]
*pa[1]
21/1
Un puntatore ad array non e‘ un puntatore doppio
t y p e d e f double AD [ 3 ] ;
// f u n z i o n e che a c c e t t a un ( ∗ ) d o u b l e [ 3 ]
double normRow (AD ∗ v , i n t const &j ) ;
...
double ∗ ∗ v ;
AD ∗ p ;
...
double b= normRow ( p , 0 ) // OK!
double a=normRow ( v , 0 ) // ERRORE ! ! !
Un puntatore ad array è un tipo diverso da un puntatore doppio.
Nell’esempio precedente v[ i ][ j ] si traduce in ∗(∗(v+i)+j) che fa un
offset di i ∗sizeof(AD) rispetto a v seguito da un offset di
j∗sizeof(double) (essendo v[ i ] un puntatore a double).
22/1
References
In una dichiarazione il simbolo & dopo il nome di un tipo indica una
referenza (reference) al tipo:
double b; double & a=b;
La variabile a è qui un alias di b: l’istruzione a=10.; modifica
anche il contenuto di b.
Le referenze sono utili negli argomenti di funzione o talvolta per
semplificare l’accesso a dati.
Una referenza deve sempre essere inizializzata. A differenza dei
puntatori, il legame della referenza con l’oggetto a cui fa
riferimento non può essere cambiato.
23/1
double horner(double & x);
double & pippo(int i);//ATTENZIONE
double & c= pippo(3);// OK
double& a=horner(5)//NO!;
double * pz= new double;
double & z=*pz;//OK, per ora
z=5.0;// OK *pz è ora 5
delete pz;// NO z è ora “dangling”
z=7;//FORIERO DI DISASTRI
24/1
vector<T>
La standard library mette a disposizione dei contenitori generici.
Qui presentiamo il più semplice (e tra i più utili), std::vector<T>.
È un esempio di class template (di cui parleremo estensivamente
più avanti). È sostanzialmente un array monodimensionale con
memoria dinamica e possibilità di dimensionamento automatico.
Richiede l’header <vector>.
Accesso random
Caratteristiche Aggiunta/canc. alla fine
Aggunta/canc.
1
O(1)
O(1)1
O(N)
Se la capacità è adeguata
25/1
Organizzazione interna
begin()
end ()
size()
capacity()
L’area contenente i dati viene allocata dinamicamente. La capacità
rappresenta l’ampiezza di tale area. La dimensione (size) si riferisce
alla porzione effettivamente occupata (e quindi alla dimensione
effettiva del vector). In generale, la capacità viene solo aumentata
in modo automatico.
begin() e end() sono gli iteratori al primo e all’ultimo elemento+1.
26/1
Esempi
vector<float> a;//creo un vettore vuoto
Sia size che capacity è 0.
vector<float> a(10);//creo un vettore con 10 elementi
Gli elementi sono qui costruiti con il costruttore di default float()
che istanzia l’oggetto e lo inizializza a zero. size() è pari a 10,
capacity() è ≥ 10 (probabilmente 10).
vector<float> a(10,3.14);//vettore inizializzato a 3.14
Gli elementi sono costruiti usando float(3.14) (costruttore di
copia).
27/1
push_back(T const & value)
Il metodo push_back(value) inserisce value alla fine (back) del
vettore. L’area di memoria è gestita dall’algoritmo seguente, dove
size è la dimensione prima dell’inserimento,
I
Se size+1>capacity
a alloca un area di memoria 2 volte la capacità corrente e
corregge capacity di conseguenza;
b copia gli elementi in tale area, che diverrà la nuova area di
memoria;
c libera la vecchia area di memoria;
aggiunge l’elemento alla fine del vettore e pone size=size+1;
28/1
Indirizzamento di elementi di in vector<T>
Gli elementi di un vector<T> possono essere indirizzati usando
l’operatore [] o il metodo at(). Il secondo lancia (throw)
un’eccezione nel caso di indice fuori dall’intervallo [0, size()[
(range_error).
vector<double> a;
b=a[5]; //Errore
c=a.at(5)//Errore, il programma abortisce
// (a meno che non si catturi l’eccezione)
29/1
Riservazione, prego
vector<float>a;
for (i=0;i<1000,++i)a.push_back(i*i);
vector<float>c;
c.reserve(1000);
for (i=0;i<1000,++i)c.push_back(i*i);
vector<float>d;
d.resize(1000);
for (i=0;i<1000,++i)d[i]=i*i;
Quale dei tre schemi è meno efficiente?
Quello per il vettore a, a causa delle riallocazioni di memoria. Nel
caso di c e d dipende dal costo del costruttore di default+
assegnazione (caso di d) rispetto a quello di push_back().
resize()ridimensiona il vettore alla dimensione assegnata usando
il costruttore di default per gli eventuali elementi addizionali.
30/1
Ridurre la capacità di un vettore
Può essere opportuno ridurre la capacità di un vettore fino alla
dimensione effettivamente usata. Non c’è un metodo apposito. Ma
si può fare cosi
vector<double>a;
...//
{// Creo un vettore temporaneo che contiene
// solo gli elementi di a
vector<double> tmp(a);//
// scambio tmp con a
a.swap(tmp);
// tmp e‘ distrutto quando si esce dallo scope
} vector<double>(a).swap(a)
La parte tra graffe può semplificare in
vector<double>(a).swap(a);
Si noti le { } per definire uno scope per la variabile temporanea tmp
Alla fine a.size() = a.capacity() (o quasi)
31/1
Esempio: annientare il contenuto di un vettore
vector<double>a;
...
//voglio ’cancellare’ il contenuto di a
a.clear();//così però la capacità è invariata
vector<double>().swap(a);//capacity() ora è 0
32/1
Iteratori
Gli iteratori sono un modo di accedere in modo uniforme ed
efficiente a tutti i contenitori della STL.
vector<double>a;
typedef vector<double>::iterator ivd;
...
for (ivd i=a.begin(); i!=a.end(); ++i)*i=10.56;
begin() e end() restituiscono un iteratore, che punta
rispettivamente al primo e all’ultimo (+1) elemento del vettore.
L’iteratore può essere deferenziato con l’operatore *, che restituisce
l’elemento associato all’iteratore.
Il test i!=a.end() assicura che siamo ancora all’interno
dell’intervallo di iteratori associati a un elemento del vettore.
33/1
Si poteva anche operare in modo più classico:
...
for (int i=0; i!=a.size();++i)a[i]=10.56;
Usare gli iteratori però:
I
È più efficiente: a[i] → *(&a[0] + i), mentre l’iteratore
accede direttamente l’elemento (ma i metodi begin() e
end() hanno un costo) ;
I
È usabile anche con altri contenitori: l’operatore [] esiste solo
per i vector.
Sono definiti per gli iteratori a vector gli operatori di somma,
sottrazione e confronto: i+1 è l’iteratore associato all’elemento
successivo a quello associato a i (purchè i+1 < end()).
34/1
Una nota importante
Gli iteratori a un vettore sono invalidati ogni volta che l’area di
memoria usata viene modificata, per esempio per inserire un nuovo
valore:
it=a.begin();
v=a[5];// OK
a[2]=-7.6;// OK
a.push_back(7.8); //l’area di memoria del vettore può essere
cambiata
c=*it; //NOOO!
35/1
Operazioni principali con gli iteratori
I
Confronto ==. Se i == 0 l’iteratore è l’iteratore nullo (non
indirizza nulla).
I
Dereferenziamento *
I
Assegnazione: =
I
Avanzamento ++i o i++ e arretramento. --i o i--: Si passa
all’elemento successivo/precedente del contenitore.
Gli operatori precedenti valgono per gli iteratoria qualsiasi
contenitore dalla standard library. Per gli iteratori a vector sono
implementati gli operatori + e - che prendono come secondo
argomento un intero: i=j+2, e avanzano (indietreggiano) l’iteratore
del numero di elementi corrispondente.
36/1
const_iterator
Un const_iterator (iteratore a costante) è un iteratore il cui
oggetto può essere acceduto solo in lettura e non puo‘ quindi essere
modificato.
vector<float> a;
....
vector<float>::const_iterator b(a.begin());
*b=5.8;// Errore
37/1
Sequenze o range
Una sequenza (in inglese sequence o range) è una porzione
“logicamente contigua” di un contenitore definita da due iteratori
validi (possono anche essere dei puntatori) i1 e i2, tali che
l’istruzione
for (iterator i=i1;i<i2;++i) value a=*i;
è valida. Qui value indica il tipo memorizzato nel contenitore.
Un range definisce quindi un intervallo aperto a destra [i1,i2[,
cioè i2 non fa parte del range (e quindi può essere pari a end())
Vi sono molti algoritmi della standard library che operano su
sequenze. Sono in genere da preferirsi rispetto a operare sulle
singole componenti.
38/1
Un esempio
Dati due vettori v1 e v2 si vuole assegnare al primo la seconda
metà del secondo (assumiamo per semplicità che v2 abbia un
numero pari di elementi).
Modo consigliato: usare il metodo assign()
v1.assign(v2.begin()+v2.size()/2,v2.end());
Modo sconsigliato:
int j=0; for(int i=v2.size()/2;i<v2.size();++i,++j)
v1[j]=v2[i];
39/1
Principali metodi e operatori di vector<T>
I
Costruttori vector<T>(), vector<T>(int) e
vector<T>(int,T const &)
I
Indirizzamento diretto: ([int] e at(int))
I
Aggiunta di un valore alla fine: push_back(T const &)
I
Dimensione e capacità: size() e capacity()
I
Ridimensionamento: resize(int)
I
Iteratori all’inizio e alla fine: begin() e end()
I
Swap della memoria: swap(vector<T> &)
I
Cancella il contenuto (ma non rilascia la memoria): clear()
40/1
Principali tipi definiti da vector<T>
I
vector<T>::iterator Iteratore al vettore
I
vector<T>::value_type Tipo degli elementi contenuti (è
pari a T)
I
vector<T>::size_type Tipo utilizzato per indirizzare
elementi di un vettore (tipicamente è un unsigned int, ma
può dipendere dala implementazione)
I
vector<T>::const_iterator Iteratore a valori constanti
(l’elemento indirizzato non può essere modificato)
41/1
Puntatori e vector<T>
Talvolta può essere utile (per esempio per compatibilità con una
funzione che usa puntatori) ’interpretare’ un vector<T> come T *.
double myf(double const * x, int dim);//funzione esterna
...
vector<double> r;
...
y=myf(&r[0],r.size());
Attenzione: con questa tecnica si devono eseguire solo operazioni
che non modifichino l’allocazione dei dati di vector<T>.
Nell’esempio, il trucco non si può usare se myf alloca memoria per
x.
42/1
Le stringhe del C
In C++ si può usare la stessa convenzione del C per le stringhe.
char * l=”Ciao mondo”;// stringa di 10 caratteri
//+ terminatore
char * ch;// puntatore a carattere
ch=new char[n];//stringa a dim. dinamico
...
delete [] ch;
L’header <cstring> richiama una serie di strumenti per manipolare
stringhe (ereditati dal C).
43/1
Le stringe C++
L’header della STL <string> introduce la classe template string,
molto più flessibile dell’equivalente C (che viene però mantenuto
per compatibilità). Si riportano le caratteristiche principali (si veda
esempio_string per maggiori dettagli).
#include<string>
std::string a(“this is”); //una stringa può essere inizializ
std::string b(“ a string”);
std::string c=a+b;// concatenazione
std::cout«c«endl;//operatore «
c.clear();
getline(cin,c)
44/1
Variabili Costanti
Il C++ permette di definire delle costanti usando la parola chiave
const nella dichiarazione. Una variabile const deve essere
inizializzata nel momento della dichiarazione (il caso di variabili
membri di classi è particolare e lo vedremo più avanti).
double const pi=3.14159265358979;
float const e=2.7182818f;
double const Pi=atan(1.0)*4.0;
const unsigned ndim=3u;
La qualifica const indica che la variabile non può essere modificata.
45/1
Regole di constanza
La parola chiave const si associa al tipo alla sua sinistra, a meno
che non compaia all’inizio. In tal caso, non essenduci nessuna
indicazione di tipo alla sua sinistra, si applica a destra. Quindi
const float a è identico a float const a. Nei casi più
complessi si consiglia di leggere da destra verso sinistra ed in
Inglese:
double const * const p
significa “p is a constant pointer to a constant double”. Cioè nè il
puntatore nè il valore “puntato” può essere modificato.
46/1
Esempi
const double * const p
Identico a prima: p is a constant pointer to a (constant) double.
double const * & p
p is a reference to a pointer to a constant double: *p non può
essere cambiato (ma p si).
double & const e=z
Errore! Una referenza è di per sè const: non può essere riassegnata.
47/1
double const a=30;
double const * pa=&a;
double * pc=&a;//&a e‘ un const double *
double const * & pd=pa;
double const b=89.8;
double const * pb=&b;
pb=pd;
*pb=34; // NO *pb e‘ const
48/1
Usate const!
Non abbiate paura di const: è vostro amico.
Variabili che non vengono modificate vanno sempre dichiarate
const:
I
Il programma è di più facile lettura e manutenzione.
I
Si evita di modificare per errore una variabile che si intende
non modificabile.
I
Il compilatore può fare molte ottimizzazioni che altrimenti non
sarebbe in grado di eseguire.
49/1
const_cast<T>
Purtroppo il mondo reale è imperfetto. Potremmo avere la
necessità di passare una variabile const come argomento a una
funzione dove l’argomento non è stato dichiarato const sebbene la
variabile non venga modificata
Occorre usare il comando const_cast<T>(const T&), che
restituisce una referenza alla stessa variabile ma con il flag const
disattivato.
Usare const_cast solo quando strettamente necessario. Spesso il
suo uso è segno di cattivo design del codice.
50/1
Esempio di uso di const_cast<T>
double minmod(float & a, float & b);
...
double fluxlimit(float const& ul,float const& ur)
...
l=minmod(const_cast<float>(ul),const_cast<float>(ur));
...
51/1
Lvalue e rvalue
È il momento di introdurre una terminologia che appare sovente nei
testi (e nei messaggi del compilatore). Ne diamo una definizione
sufficientemente precisa per la maggior parte dei casi (ma non
rigorosa).
Un lvalue è una espressione che può apparire alla sinistra di un
operatore di assegnazione. A un lvalue è sempre associato un’area
di memoria. Un rvalue può solo apparire a destra di una
assegnazione.
Per esempio, una espressione letterale (literal expression o anche
literal constant in Inglese), per esempio 3.1415, è solo un rvalue,
dato che non posso scrivere 3.1415=pi (mentre un double di
nome pi può essere un lvalue).
52/1
Enumerazioni
enum bctype{Dirichlet,Neumann,Robin};// definizione
...
bctype bc=Neumann;// ..
switch(bc){
case Dirichlet:
...
break;
case Neumann:
...
break;
Default:
...
}
53/1
Enumerazioni (enumeration)
Il tipo enumeration (enum) è di fatto un integral type (tipo
assimilabile a un intero). Infatti possono essere associati dei valori
interi.
enum bctype{Dirichlet=0,Neumann=2,Robin=4};//
Rispetto però all’uso di indicatori di tipo intero permettono un
controllo più fine: una variabile di tipo bccond può assumere solo i
valori dichiarati, cioè Dirichlet, Neumann o Robin.
Un elemento di una enumarazione è detto enumeratore
(enumerator)
54/1
funzioni
Le funzioni sono delle porzioni di codice che possono essere
richiamate da altro codice.
I
Dichiarazione di funzione. Fornisce al compilatore la firma
della funzione, in modo che il compilatore possa fare i test di
consistenza e sintattici. Permette all’utente di conoscere come
la funzione deve essere chiamata.
I
Definizione di una funzione Fornisce il codice (body) con
l’implementazione della funzione.
Anche qui vale la regola che una definizione è una dichiarazione.
55/1
Dichiarazione di funzione
Una dichiarazione di funzione consiste degli elementi seguenti:
return type name (argument type , argument type, ...)
const;
Una funzione è caratterizzata da un nome , che la identifica
rispetto alle regole di visibilità, dagli argomenti, dal tipo di ritorno
(return type) ed limitatamente a funzioni membro di una classe
dall’eventuale parametro const.
nome+argomenti (+const) formano la cosidetta firma (signature)
della funzione, che la identifica univocamente.
Esempio
double cyVolume(const double radius,const double length);
double polygArea(const vector<lati> & sides);
Nella dichiarazione i nomi delle variabili possono essere omessi:
double cylVolume(const double,const double);
double polyArea(const vector<lati> &);
56/1
Definizione di funzione
returnType name (argument, argument) const{
...
corpo (body) della funzione
...
return value
}
Il corpo della funzione ne definisce l’implementazione. L’istruzione
return ritorna un valore di tipo returnType. Essa non è presente se
returnType=void.
57/1
Chiamata di una funzione
Nel programma chiamante:
...
double R, L;
vector<double> sides;
...
double volume=cylinderVolume(R,L);
double area=polygonArea(sides);
Gli argomenti R,L e sides nel programma chiamante sono detti
argomenti attuali (actual arguments) della funzione. I
corrispondenti argomenti nella definizione della funzione sono detti
argomenti formali (o locali) e sono a tutti gli effetti delle variabili
locali della funzione.
58/1
Passaggio per valore e per referenza
int fun1(const int i, float b, double c);
float fun2(int& i, float& b, double const & c);
...
int s=fun1(5, z, r);
float g=fun2(h, z, 3.5);
Nella funzione fun1 si dice che gli argomenti formali i, b e c sono
passati per valore. Eventuali loro modifiche in fun1 non altera il
valore degli argomenti attuali corrispondenti.
Si noti come i sia stata dichiarata const. Questo vuole dire che
NON può essere modificata nel corpo di fun1.
59/1
float fun2(int& i, float& b, double const & c);
Nel caso di fun2 gli argomenti formali sono ’passati per referenza’.
Quindi sono delle references agli argomenti attuali. Quindi una loro
modifica in fun2 si riflette sull’argomento attuale corrispondente.
L’uso delle referenze è un altro modo di passare valori dalla
funzione al programma chiamante.
Si noti l’uso del const: c non può essere cambiata: essa è quindi
una variabile di input.
60/1
Demistifichiamo il passaggio per referenza
L’espressione ’passaggio per valore’ o ’passaggio per referenza’ è
causa di confusione. In realtà il meccanismo con cui viene gestito il
passaggio degli argomenti di una funzione in C++ è sempre lo
stesso!
È il tipo associato all’argomento formale che fa cambiare
l’interazione della funzione con l’argomento attuale corrispondente.
Vediamo come il C++ (ma non solo!) gestisce la chiamata a una
funzione.
61/1
I
Gli argomenti (formali) della funzione vengono inizializzati con
i valori degli argomenti attuali corrispondenti;
I
Viene eseguito il corpo della funzione;
I
Nel caso di funzioni con tipo di ritorno non void il valore
dell’espressione che segue il comando return viene reso
disponibile al programma chiamante.
Variabili definite nel corpo della funzione (compresi gli argomenti)
sono variabili locali che vengono distrutte al momento del ritorno al
programma chiamante.
Fanno eccezione le variabili statiche (static variables)
62/1
Esempi
Negli esempi che seguono cercheremo di spiegare il meccanismo
della chiamata di una funzione scrivendo una sorta di codice
equivalente che rimpiazza la chiamata.
Le variabili locali della funzione saranno indicate con
nomefunzione::, per distinguerle dalla variabili del programma
chiamante.
NOTA IMPORTANTE Questi esempi vogliono solo illustrare il
meccanismo di chiamata a funzione e passaggio di valori.
Corrispondono SOLO da un punto di vista concettuale a quello che
accade realmente.
63/1
double fun (const double a, const double b){
a *=b; return a*a }
...
double A(10), B(20);
double C=fun(A,B);
{ }
{ const double fun::a(A); const double fun::b(B);
fun::a *=fun::b;
double C= (fun::a*fun::a);
// nota: C appartiene allo scopo esterno
}
N.B Qui a e b avrebbero dovuto essere dichiarate const!
64/1
double fun (const double & a, const double & b){
a *=b; return a*a }
...
double A(10), B(20);
double C=fun(A,B);
{ }
{ const double & fun::a=A; const double & fun::b(B);
fun::a *=fun::b; //ERRORE!!
double C= (fun::a*fun::a);
// nota: C appartiene allo scopo esterno
}
N.B Dopo la chiamata alla funzione A sarà pari a 200 in quanto
fun::a è un alias di A. Quindi la funzione comunica con l’esterno
non solo tramite C. Qui b avrebbe dovuto essere dichiarata const
ma non a
65/1
Ovviamente una funzione può prendere in input dei puntatori o
degli array, la regola è la stessa
void copy (double const * a, vector<double> & b){
for (int i=0;i<b.size();++i)b[i]=a[i]; }
...
double * A(0), vector<double> B;
...
A=new double[100];
B.resize(100);
copy(A,B);
{ }
{ double * copy::a=A; const vector<double> & copy::b(B);
for (int i=0;i<copy::b.size();++i)copy::b[i]=copy::a[i];
}
Il valore del puntatore A viene usato per assegnare la variabile locale
copy::b, che quindi conterrà quindi l’indirizzo dell’area assegnata
ad A.
Il puntatore copy::b andrebbe dichiarato pointer to a constant
66/1
Regole generali
I
preferire il passaggio per referenza: è più efficiente, sopratutto
per dati “grandi”. Si evita infatti la copia.
I
Dichiarare SEMPRE const & le variabili passate per referenza
in “input”, cioè che non vengono modificate dalla funzione.
I
Una costante letterale (literal), es. 34.2, “abcd”, può essere
passata a una funzione solo per valore o come referenza
costante (const &).
I
Bisogna fare attenzione al passaggio per referenza nel caso di
funzioni ricorsive.
67/1
Allocare memoria per un argomento formale
E‘ possibile allocare memoria per un puntatore passato come
parametro di ritorno. Consideriamo una funzione che fa la lista dei
nodi con una certa condizione al bordo.
int * pList(mesh const & mesh,int const bc){
unsigned count=0;
for(int i=0;i<mesh.numnodes();++i)
if(mesh.nodeBc(i)==bc)++count;
int * list=new int[count];...
return list; }
E‘ comunque sconsigliato: chi ha la responsabilità di cancellare la
lista dei nodi list?
La gestione dinamica della memoria è meglio farla attraverso le
classi (per esempio vector<T>):
void pList(mesh const & mesh,vector<int> & list, int const
bc);
68/1
TROVATE L’ERRORE?
void pList(mesh const & mesh,int * list, int const bc){
unsigned count=0;
for(int i=0;i<mesh.numnodes();++i)
if(mesh.nodeBc(i)==bc)++count;
list=new int[count];
...
}
...
int mylist;
pList(mymesh,&mylist,5);
vi è un errore grave!.
int * pList(mesh const & mesh, int const bc){
unsigned count=0;
for(int i=0;i<mesh.numnodes();++i)
if(mesh.nodeBc(i)==bc)++count;
int * list=new int[count];
...
69/1
Overloading di funzioni
Due funzioni con signature differente sono di fatto due funzioni
distinte. Questo è il meccanismo alla base dell’overloading di
funzione.
float fun(const float * & a, vector<float> & b);
double fun(const double * & a, vector<double> & b);
void fun(int & a);
...
double * z, vector<double> x, int l;
double g=fun(z,x);
//chiama float fun(const double * &, vector<double> & b)
...
fun(l);//chiama fun(int & a)
Il compilatore sceglie la signature che soddisfa gli argomenti nel
modo migliore, tenendo conto anche delle conversioni implicite.
70/1
Argomenti di default
Nella dichiarazione di una funzione si possono fornire argomenti di
default, che devono però essere sempre quelli più a destra:
vector<double> crossProd(vector<double>const &,
vector<double>const &, const int ndim=2);
...
a=crossProd(c,d);//usa ndim=2
...
71/1
Variabili locali statiche
Le variabili definite nel corpo di una funzione sono variabili
automatiche che vengono distrutte all’uscita della funzione (hanno
uno scope locale).
Il valore di una variabile locale la cui dichiarazione è preceduta della
parola chiave static, mantiene il valore tra diverse invocazioni della
funzione.
Nota Bene: Vengono distrutte le variabili automatiche non una
eventuale area di memoria nel free store richiesta usando new. Per
tale scopo occorre usare delete.
72/1
int funct(){
static bool first=true;
if(first){//fai qualcosa solo la prima volta
first=false;
}else{
//parte di codice fatta dalla seconda volta in poi
...
}return}
73/1
Puntatori a funzione
double integranda(const double & x)
...
typedef double (*pf)(const double &);
double simpson(const double a, const double b, pf const
f, const int n);
...
integral= simpson(0,3.1415, integranda,150);
ALTERNATIVA USANDO PUNTATORE A F.
pf p_int=integranda;
integral= simpson(0,3.1415, p_int,150);
...
Il nome di una funzione passato come argomento viene interpretato
come il puntatore alla funzione. Si può comunque usare anche &.
74/1
Una Nota
E‘ preferibile usare funtori (li vedremo in una prossima lezione) al
posto di puntatori a funzione, ovunque ciò sia possibile.
75/1
Puntatori intelligenti (smart pointers)
La standard library fornisce un classe che permette di implementare
il paragigma il puntatoreè l’unico proprietario della risorsa (exclusive
ownership). Si chiama auto_ptr e per usarlo occorre includere
l’header <memory>.
La libreria boost www.boost.org aggunge altri puntatori
intelligenti. In particolare shared_pointer implementa il
paradigma L’ultimo puntatore cancellato cancella l’oggetto Ne
parleremo più avanti nel corso.
76/1