Contact me sending an e-mail (antispam defense activated) |
Title: C per applicazioni numeriche Author: Sandro Tosi Last modified: 30/01/2004 Cerchero' qui di fare un breve vademecum per utilizzare il linguaggio di programmazione C per sviluppare applicazioni rivolte al calcolo numerico. Queste note si basano sull'esperienza maturata durante la tesi di laurea su sistemi GNU/Linux, Gentoo e Debian, con compilatore gcc-3.3; pertanto, al variare del sistema operativo o del compilatore, alcune indicazioni potrebbero risultare inesatte e errate: _state attenti_! ***Utilizzo di funzioni matematiche Il C fornisce una nutrita serie di routine matematiche che sono dichiarate nel file `math.h' ma, mentre per alcune di essere (come `fabs', che calcola il valore assoluto di un numero reale) e' possibile farvi riferimento senza problemi, per altre (come `sqrt', che calcola la radice quadrata di un numero) e' necessario aggiungere il parametro di compilazione `-lm' che indica al compilatore di linkare le librerie matematiche. ***I vettori Per chi proviene da altri linguaggi di programmazione (come per esempio Fortran o Pascal) questa e' una novita', ma in C i vettori dichiarati di n elementi, partono da 0 ed arrivano ad n-1: dichiarando, come esempio, int x[n]; potro' accedere agli elementi da x[0] ad x[n-1]; ***I vettori multidimensionali Per dichiarare un array multidimensionale, si usa la dichiarazione classica, accostando tra loro tutte le dimensioni: per dichiarare una matrice di n x m elementi di interi si usa int a[n][m] Si tenga presente che in questo modo si e' definito un array di `m' elementi a loro volta array di `n' elementi. ***Utilizzo di vettori molto grandi Nel caso si abbia la necessita' di utilizzare vettori molto grandi (diciamo con n=2x10^6 float), non e' possibile dichiararli staticamente come float x[n] poiche', una volta compilato, l'eseguibile risultera' tanto grande che il sistema si rifiutera' di caricarlo completamente in memoria inviando all'applicazione il messaggio SEGFAULT. Dunque, per poterne far uso si devono utilizzare le istruzioni di gestione dinamica della memoria: `malloc' e `calloc'; vediamo come si usano: si deve definire un puntatore al tipo di dati del vettore (continuando l'esempio precedente) float *x; e successivamente si chiama una delle due funzioni in questo modo: x = (float *) malloc( n * sizeof(float) ) oppure x = (float *) calloc( n, sizeof(float) ) dove si esegue il cast esplicito al tipo del puntatore in quanto entrambe restituiscono un puntatore ad un area di memoria `non tipata'. `malloc' richiede come parametro la dimensione dell'intera area da allocare, ottenuta come il numero di elementi moltiplicato per la loro dimensione (si e' utilizzato `sizeof' in quanto su architetture differenti la dimensione dei tipi puo' variare), mentre `calloc' chiede 2 parametri, il numero di elementi e la loro dimensione. Le differenze principali, che fanno preferire per le applicazioni numerice `calloc' rispetto a `malloc' (o in generale per la dichiarazione di array), sono il fatto che la prima azzera l'area di memora che alloca, mentre la seconda non lo fa, e che `calloc' alloca lo spazio di memoria in modo contiguo, importante per l'allocazione di array e matrici in applicazioni numeriche. Una volta eseguite queste istruzioni, e' possibile accedere al vettore come al solito: quindi, per accedere al decimo elemento del vettore `x' si usa x[9] IMPORTANTE: terminato di utilizzare il vettore dinamico, si _DEVE_ liberare la memoria allocata tramite l'istruzione `free' che prende come parametro il puntatore all'area di memoria da liberare: free(x) nel caso non si liberi la memoria occupata da vettori dinamici, essa rimarra' occupata fino al termine del programma principale. ***Vettori & parametri Un vettore e', fondamentalmente, un puntatore ad un'area di memoria (anche se definito implicitamente come int x[n]); inoltre i parametri delle funzioni C sono tutti passati per valore: quindi, sebbene non sia possibile modificare il valore del puntatore (cioe' spostare il puntatore nella memoria), sara' possibile modificare gli elementi dell'array. Il puntatore all'array e' passato per valore, ma l'"array" in toto possiamo dire che e' passato per riferimento. Non facciamoci poi illudere dal modo di definire una funzione: int f(int *a) e int f(int a[]) sono definizioni equivalenti, ed e' ancora consentito di modificare gli elementi di `a'. Attenzione dunque che tutte le modifiche apportate al vettore all'interno delle proprie funzioni avranno valenza globale sul vettore. ***Utilizzo delle macro del preprocessore Nelle applicazioni numeriche si cerca in ogni modo di ottimizzare le operazioni svolte, spesso a scapito della leggibilita' del codice; come sappiamo, pero', scrivere un programma in modo ordinato e leggibile consente una sua successiva modifica in tempi molto rapidi oltre a consentire la sua comprensione anche ad una persona che non ha scritto il codice. La prima cosa che si e' soliti fare e' creare una funzione per una parte di codice utilizzata spesso e poi richiamare questa funzione: questo approccio consente di scrivere codice piu' compatto e maggiormente modificabile e manutenibile (una correzione od una modifica a quella parte di codice resa funzione deve essere eseguita una sola volta, invece che in tutti i posti in cui se ne fa uso, diminuendo il tempo necessario alla modifica e la possibilita' di introdurre errori). Purtroppo, questa metodologia male si adatta ai calcoli numerici, nel caso queste funzioni debbano essere richiamate molte volte: infatti, la necessita' di dover salvare le informazioni per la chiamata a funzione ed il loro ripristino introducono dei ritardi inaccettabili. Per risolvere questo inconveniente, possiamo sfruttare una particolarita' del C: le macro del preprocessore (da ora in poi solo macro). Per spiegare cosa sono le macro, facciamo l'esempio delle costanti: nel caso il nostro programma faccia uso frequente di un valore numerico fissato, risulta piu' comodo definire una costante che contenga questo valore e nel codice far riferimento a detta costante invece che al valore numerico che essa contiene; in questo modo si riducono gli errori nel codice (non si correra' il rischio di sbagliare a scrivere questo numero dove viene usato) e si diminuisce il tempo di modifica del programma (in quanto si dovra' soltanto cambiare valore alla costante, invece che a tutte le occorrenze nel codice). Le macro funzionano in maniera molto simile: si definisce una macro tramite la direttiva del preprocessore #define |