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