Capitolo 9 - Puntatori a funzioni di Roberto Porro In questa parte del corso tratteremo l'argomento dei puntatori a funzione. Anche le funzioni, come tutti i tipi del C (con la sola esclu- sione del tipo register), vengono associate ad un indirizzo in memoria e, tramite tale indirizzo, possono venir utilizzate. Ricordiamo che le funzioni C sono contraddistinte dalla coppia di parentesi tonde () che segue il nome della funzione stessa. Una volta definita la funzione ogni successiva identificazione del suo nome senza parentesi fa riferimento al suo indirizzo. Vediamo un esempio: main() { int funz(); printf( "La funzione funz e' all'indirizzo %d\n", funz ); funz(); } funz() { /* codice */ } Questo programma stampa l'indirizzo in memoria della funzione funz. Ora che siamo a conoscenza dell'indirizzo di funz siamo anche in grado di chiamare funz tramite il suo indirizzo. Vediamo praticamente come si fa. main() { int funz(); int( *funzpt )(); printf( "La funzione funz e' all'indirizzo %d\n", funz ); funzpt = funz; printf( "funzpt fa riferimento a %d\n", funzpt ); funz(); (*funzpt)(); } funz() { /* codice */ } La novita' in questo programma e' che abbiamo scritto int ( *funzpt )(); ovvero il nome di una variabile racchiuso tra parentesi. Se pensiamo ai puntatori usati fino ad ora possiamo chiarire il concetto: int *puntatore; segnala al compilatore di restituire un valore di tipo int, ovvero contiene l'indirizzo di una variabile intera, ovvero e' un puntatore ad intero. Analogamente int ( *funzpt )(); segnala che la (*funzpt)() ritorna un valore di tipo intero; se poi funzpt contiene l'indirizzo della funzione funz, come e' stato dichiarato quando abbiamo scritto funzpt = funz; allora la *funzpt e' l'inizio della funzione stessa (quello che viene detto entry point della funzione) e percio' restituisce un tipo int dopo aver eseguito la funzione. Il fatto di aver racchiuso il nome in parentesi sta nella necessita' di attribuirgli il significato di puntatore a funzione di tipo intero. Se avessimo utilizzato la notazione int *funzpt(); le parentesi relative agli eventuali parametri della funzione avrebbero avuto la precedenza sull'operatore *, il che avrebbe causato la dichiarazione di una funzione che ritorna un puntatore ad una VARIABILE di tipo int e non ad una FUNZIONE. Una funzione chiamata tramite puntatore non deve obbligatoria- mente restituire un valore intero, ma qualunque tipo. Esempio. main() { char *funz(); char *( *funzpt )(); funzpt = funz; printf( "%s", funz() ); printf( "%s", (*funzpt)()); } char *funz() { return( "Ho eseguito funz()\n" ); } La domanda legittima che ci si pone a questo punto e' Ma perche' complicarsi la vita con questi puntatori? Effettivamente se ne puo' fare a meno, ma il loro utilizzo puo' servire a semplificare la struttura dei programmi. Vediamo un esempio. Supponiamo di avere un programma che deve fare una serie di operazioni in base ad una valore che viene passato di volta in volta diverso e supponiamo ancora che questo valore venga fornito o da riga di comando o dalla selezione di bottoni presenti nell'interfaccia del nostro programma a finestre. Per esempio vogliamo raddoppiare il primo valore passato, dividere per tre il secondo ed elevare al quadrato il terzo. In una successiva elaborazione i parametri potrebbero essere in numero maggiore o minore o la sequenza delle operazioni potrebbe essere diversa. Il modo per passare i dati potrebbe essere il seguente: calcola doppio 2 terzo 12 cubo 6 calcola cubo 3 doppio 4 calcola terzo 9 terzo 24 doppio 78 cubo 16 Il programma calcola puo' essere scritto cosi': ... main( argc, **argv ) { int doppio(), terzo(), cubo(), nonso(); int( *funz )(); int i = 1; /* serve a contare i parametri di main */ while( i < argc ) { if( !strcmp( "doppio", argv[i]) ) funz = doppio; else if( !strcmp( "terzo", argv[i] ) funz = terzo; else if( !strcmp( "cubo", argv[i] ) funz = cubo; else /* nessuno di questi */ funz = nonso; printf( "%dn", (*funz)(atoi(argv[i+1]))); i+=2; } } doppio( int i ) { return( i*2 ); } terzo( int i ) { return( i/3 ); } cubo( int i ) { return( i*i ); } nonso( int i ) { return( 0 ); } In questo modo abbiamo scritto un programma che in maniera flessibile processa i parametri passati ed esegue i calcoli nell'ordine richiesto. Se non viene riconosciuta un'operazione viene stampato uno zero. Il programma non esegue un controllo approfondito sui parame- tri passati e sarebbero necessari ulteriori test, ma serve allo scopo di dimostrare l'uso dei puntatori a funzione. N.B. In questo programma non sono stati riportati i file di include che sono: stdio.h e string.h o stdlib.h Questo tipo di programmazione e' molto utilizzato in quelle che vengono definite macchine a stati, ovvero quei processi informatici in cui si passa da un livello di ordine superiore ad uno piu' interno od inferiore, ad un altro ancora piu' interno ritornando poi al livello precedente quando si incon- tra un evento segnalatore. Un tipico esempio e' cio' che avviene durante il parsing dei programmi C: il programma parser del compilatore quando incon- tra il simbolo / passa ad un secondo stato in cui verifica se il simbolo seguente e' un *. In tal caso si entra in un livel- lo diverso, ovvero si e' all'interno di un commento e se ne fuoriesce solo quando si incontra un * seguito da un /. Le alternative sono, ad esempio che il simbolo / sia la ri- chiesta di una divisione, oppure una parte di una stringa di caratteri, cosi' come un * all'interno di un commento non faccia parte del terminatore */. Identificando i vari stati come funzioni che eseguono svariati compiti e' evidente che una struttura di tipo if then else o una switch case richiederebbe una complicazione eccessiva dal momento che non si conosce a priori l'evento successivo a quello attuale, mentre l'uso di puntatori a funzione risulta piu' flessibile. Con questo si conclude la parte sulle funzioni ed i puntatori, anche se l'argomento e' molto vasto e solo la pratica e la analisi di programmi reali puo' portare ad una migliore comprensione degli argomenti. Nei prossimi capitoli tratteremo dei file, i vari modi di gestire i flussi di dati con il C, nonche' l'uso di strutture, union e tipi enumerali.