Sei sulla pagina 1di 27

Puntatori a funzione

untatore ormai la parola magica del linguaggio C++. Ci sono


programmatori che non riescono ad immaginare la loro esistenza
senza questa importante struttura prevista fin dai tempi del C.

Ma mentre tutti i neofiti del linguaggio familiarizzano presto con i puntatori ai


vari tipi di dati (basti pensare al passaggio di un vettore ad una funzione),
passa molto tempo prima che essi raggiungano la conoscenza dei puntatori
a funzioni: anzi, non sarebbe troppo azzardato dire che molti giovani
programmatori ignorano del tutto questa potente funzionalit del linguaggio.
In queste pagine cercheremo quindi di mostrare questa tecnica molto utile e
potente, che permette addirittura di passare una funzione come argomento di
unaltra!
In questa guida cercheremo di ripercorrere alcuni aspetti pi interessanti dei
puntatori ai dati, ossia la loro rappresentazione in memoria, per poi arrivare a
parlare dei puntatori a funzione, e vederne tutte le potenzialit.
La discussione del primo paragrafo analizza alcuni casi di puntatore a dato e
mostra come sono puntati i vari tipi di dati e come la tipizzazione dei
puntatori in C++ faciliti alcuni compiti.
Vengono mostrati alcuni esempi scritti in Assembler per il processore Intel
8086, che utilizzano solamente le istruzioni pi semplici e banali. La
conoscenza di un qualunque tipo di linguaggio Assembler pu facilitare la
lettura del paragrafo.
La seconda parte invece legata esclusivamente ai puntatori a pezzi di
codice, in particolare alle funzioni.
Per leggere e comprendere questa guida necessario conoscere bene la
sintassi e lutilizzo basilare dei puntatori in C++, almeno come funzionano,
come si utilizzano e come vengono sostituiti ai vettori.

1. Analisi approfondita dei puntatori a dato


1.1

Puntatore a carattere

Supponiamo di avere un dato di tipo carattere. Esso rappresentato, su tutti


i sistemi, con un solo byte, quindi il suo puntatore punter sempre ed
esattamente al dato.

[C++] Puntatore a char


// Dichiarazione delle variabili
char x;
char* p;
// Assegnazione di un valore
x = C;
// Puntiamo al carattere
p = &x;
// Stampiamo il carattere
cout << *p;
Un effetto simile era ottenibile anche in Assembler (processore Intel 8086):

[ASM] Puntatore a char


.DATA
; Definizione del byte contenente il carattere
X
DB
C
.CODE
; Creazione del puntatore
LEA
SI , X
; Ora il registro SI contiene lindirizzo di X
MOV
AL , [SI]
Per evitare di andare troppo nello specifico, nella versione Assembler ci
siamo limitati a spostare il contenuto del puntatore (SI) in un nuovo registro,
attraverso il dereferenziamento del puntatore.
Comunque sia, sia in C++ che in Assembler il puntatore indirizzato
direttamente allunico indirizzo di memoria del carattere.

X
X+1
X+2

C
?
?

Il puntatore punta esattamente alla


cella di memoria in cui contenuto
il valore del carattere.

1.2

Puntatore ad intero

Fintanto che un puntatore punta ad un tipo semplice come un carattere non


sorge quindi alcun problema. Quando invece vogliamo puntare ad un tipo
leggermente pi complesso le cose cambiano. Facciamo lesempio di un
puntatore ad un intero a 2 byte.
In C++ la gestione non cambia minimamente, cos come non cambia nella
versione Assembler. Tuttavia nella rappresentazione in memoria si nota
qualcosa di diverso.

[C++] Puntatore a intero


// Dichiarazione delle variabili
int x;
int* p;
// Assegnazione di un valore
x = 4676;
// Puntiamo al numero
p = &x;
// Stampiamo il numero
cout << *p;

[ASM] Puntatore a intero


.DATA
; Definizione del byte contenente il carattere
X
DW
4676
.CODE
; Creazione del puntatore
LEA
SI , X
; Ora il registro SI contiene lindirizzo di X
MOV
AX , [SI]
Osserviamo quindi la rappresentazione in memoria di questa situazione.

X
X+1
X+2

44h
12h
?

Il puntatore punta alla parte bassa


del numero, che rappresentato
utilizzando due celle di memoria.

Come gi supponevamo, il numero rappresentato in memoria con due


byte. Il numero 4676 in esadecimale diventa per lappunto 1244h.
In memoria i tipi di dati vengono memorizzati seguendo la regola
parte bassa allindirizzo basso, parte alta allindirizzo alto

Otteniamo quindi che la parte bassa del numero (44h) finisce nella cella
bassa di memoria, mentre la parte alta (12h) finisce nella cella successiva.
Il puntatore, che esattamente un numero in esadecimale, pu puntare ad
una sola locazione in memoria. Punta, come visto, alla prima locazione del
numero, ossia quella della parte bassa.
Non abbiamo avuto difficolt nel trasferimento in Assembler in quanto anche
il processore 8086 pu trasferire 16 bit contemporaneamente.
Se analizziamo un caso ancora pi complesso, ad esempio il trasferimento di
un numero float, ci accorgiamo che il puntatore sempre alla prima delle
quattro locazioni utilizzate per rappresentare il numero, ed in Assembler
sarebbe necessario spostare manualmente le due parti in cui viene spezzato
il numero (da 2 byte ciascuna).

1.3

Puntatore a vettore

Passiamo ora ad analizzare un caso pi complesso: lutilizzo dei vettori.


Ancora una volta, partiamo dal caso pi semplice: ogni elemento del vettore
occupa un byte, parliamo quindi di un vettore di caratteri. Per utilizzare un
caso pi classico, osserviamo un caso particolare di vettore di caratteri: la
stringa.
Vediamo come avviene la sua gestione in C++.

[C++] Puntatore a stringa


// Dichiarazione della stringa
char str[] = Ciao mondo;
// Puntatore di tipo carattere
char* p;
// Puntiamo alla stringa
p = str;
// Stampiamo la stringa
cout << p;
Ancora una volta, i vantaggi offerti dal C++ si fanno sentire molto. Gi solo la
stampa estremamente facilitata, cos come lo il riferimento al primo
carattere della stringa. Da ricordare fin da ora che il nome di un vettore
senza quadre equivale allindirizzo del primo elemento. Pertanto scrivere
str o &str[0] del tutto equivalente.
Se non volessimo utilizzare questi vantaggi, ma volessimo invece
evidenziare i vari passaggi utilizzati per eseguire il codice, dovremmo
ricorrere alla versione sottostante

[C++] Puntatore a stringa (V.2)


// Dichiarazione della stringa
char str[] = Ciao mondo;
// Puntatore di tipo carattere
char* p;
// Puntiamo alla stringa
p = &str[0];
// Equivale a p = str
// Stampiamo la stringa
for ( ; *p ; p++)
cout << *p;
Proviamo a comprendere il codice.
Una stringa non altro che un vettore di char in cui lultimo elemento il
terminatore (\0 in C++, $ in DOS). In memoria un vettore rappresentato
semplicemente come una sequenza di elementi adiacenti. Ad esempio, la
stringa Ciao mondo in stile C++ cos rappresentata in memoria.
Notiamo come, inizialmente, il
STR
STR + 1

STR + 10

C
i
a
o

m
o
n
d
o
\0

puntatore sia indirizzato al primo


carattere della stringa.
Non esiste infatti un puntatore di
tipo stringa: i puntatori sono solo
per i tipi primitivi e per i tipi
definiti dallutente, mentre non
sono

consentiti

per

le

aggregazioni di dati. Cos, per


puntare

ad

un

vettore

di

caratteri, dobbiamo puntare ad


un determinato carattere della serie.
Incrementandolo ogni volta di ununit (p = p + 1 o p++), otteniamo
leffetto di incrementare lindirizzo di memoria puntato di ununit,
spostandoci cos di un carattere.
Dobbiamo smettere di incrementare nel momento in cui arriviamo ad un
carattere nullo (!*p). Il carattere nullo in C++, oltre al carattere vuoto, il
carattere di terminazione stringa (\0 per lappunto).
Questo piccolo sistema ci permette quindi di stampare direttamente tutta una
stringa.
Ad ogni giro il puntatore viene incrementato effettivamente di ununit.
Possiamo verificare la verit di questa affermazione scrivendo una versione
Assembler del programma (utilizziamo questa volta una stringa che termina
con il carattere $, utilizzato per il DOS).

[ASM] Puntatore a stringa


.DATA
; Definizione della stringa
STR
db
Ciao mondo$
.CODE
; Puntiamo alla stringa
LEA SI , STR
; Preleviamo ogni carattere
Ciclo: CMP [SI] , $ ; La stringa terminata?
JZ
FCicl
MOV AL , [SI]
; Preleviamo il carattere
INC SI
; Prossimo carattere
FCicl:
Anche in questa versione Assembler, non abbiamo fatto fare nulla al
programma, se non spostare ogni carattere nel registro AL.
Soffermiamoci ora sullistruzione
INC

SI

Come si nota, il puntatore alla stringa viene incrementato di una sola unit
(questo il compito svolto dallistruzione INC). Ci sarebbe stato equivalente
a scrivere
ADD

SI , 1

Abbiamo quindi verificato come sia in C++ che in Assembler un puntatore a


carattere venga ogni volta incrementato di una sola unit.
Proviamo ora a ripetere lesempio per stampare un vettore di interi,
supponendo sempre di lavorare in un sistema in cui gli interi sono a 2 byte.
La versione C++ mostra chiaramente i passaggi che ci interessano:

[C++] Puntatore a vettore di interi


// Vettore di interi
int v[5];
// Puntatore al vettore
int *p;
// Inizializzazione puntatore
p = &v[0];
// Equivalente a p = v
// Riempimento vettore
for (int i = 0; i < 5; i++)
*(p+i) = i;
Il codice riempie il vettore in modo che ad ogni elemento corrisponda valore
uguale al proprio indice.

Da notare listruzione
*(p+i) = i;
che del tutto equivalente a
p[i] = i;
e, considerando a cosa punta p, anche a
v[i] = i;
In realt notiamo che ad ogni iterazione ci spostiamo di un elemento nel
vettore, quindi la cosa puntata dal puntatore diventa quella successiva.
Ma come?! Se gli interi sono a 2 byte e noi ci spostiamo di ununit, come
possibile che il programma funzioni? Funziona eccome, invece. E lo fa grazie
al fatto che i puntatori in C sono tipizzati.
Quando diciamo al programma che stiamo utilizzando un puntatore ad intero,
esso sa sempre che un intero a 2 byte. Ogni incremento di ununit, viene
interpretato come ununit del tipo. Lavorando con gli interi, ogni incremento
di ununit equivale effettivamente a 2 celle di memoria!
Potremmo quindi esplicare il tutto.
Dato un puntatore p
tipo* p;
sempre vera la seguente uguaglianza

p + i = p + i * sizeof (tipo)
Se lavorassimo con dati float, ogni incremento sarebbe di 4 celle di memoria
e cos via. In memoria si avrebbe la situazione mostrata in figura.
In memoria abbiamo indirizzi
V
V+1
V+2

00
00
01
00
02
00
03
00
04
00

sempre crescenti, ma il nostro

P+1

ogni volta. Cos, P + 1, che

P+2

corrisponde

P+3

trova a 2 celle di distanza in

P+4

memoria, non subito adiacente.

puntatore cresce di due celle


al

secondo

elemento del vettore, in realt si

In

C++

rispetto

questo
al

tipo

incremento
puntato

automatico: la stessa cosa non vale in Assembler, dove non esistono i


puntatori ai tipi di dato
Questo significa che il programmatore deve considerare la grandezza del
tipo di dato con cui sta lavorando ed effettuare nel modo opportuno ogni tipo
di incremento e modifica del puntatore, per evitare di puntare ad aree di
memoria che non significano niente per noi.
Se infatti il puntatore venisse incrementato di una sola unit, prelevando
dopo 2 byte per un intero, si otterrebbe un valore formato per met da un
numero e per laltra met da quello successivo.
Per eseguire lo stesso programma avremmo quindi dovuto scrivere qualcosa
di lievemente pi complesso.

[ASM] Puntatore a vettore di interi


.DATA
; Definiamo il vettore
V
dw
5 DUP (?)
L
EQU $ - V
; Lunghezza del vettore
.CODE
; Puntatore
LEA SI , V
; Contatore
MOV CX , L
; Posizione nel vettore
MOV DX , 0
; Riempiamo il vettore
Ciclo: MOV [SI] , DX
ADD SI , 2
INC DX
LOOP Ciclo
Come detto, il puntatore deve essere incrementato manualmente di due
unit, mentre la posizione allinterno del vettore di una sola.
Ancora una volta abbiamo sfruttato la possibilit di trasferire 16 bit
contemporaneamente. Se ci non fosse stato possibile, il programma
sarebbe diventato molto pi complesso:

[ASM] Puntatore a vettore di interi


.DATA
; Definiamo il vettore
V
dw
5 DUP (?)
L
EQU $ - V
; Lunghezza del vettore
.CODE
8

; Puntatore
LEA SI ,
; Contatore
MOV CX ,
; Posizione nel
MOV DH ,
MOV DL ,

V
L
vettore
0
0

; Riempiamo il vettore
Ciclo: MOV [SI] , DL
; Parte bassa
MOV [SI + 1] , DH
; Parte alta
ADD SI , 2
ADD DL , 1
; Prossima posizione
ADC DH , 0
; Spazio per riporti
LOOP Ciclo
Nonostante abbiamo ancora semplificato il nostro compito (ad esempio,
abbiamo usato listruzione LOOP, che lavora a 16 bit), si pu comunque
evidenziare come le operazioni si siano abbastanza allungate.
Per trasferire pi di due byte sono comunque necessarie pi operazioni.
In generale, a seconda del sistema, il trasferimento di un dato pu richiedere
pi di un passaggio, se il sistema non in grado di lavorare con tutti i bit
necessari contemporaneamente.

2. Puntatori a funzioni
2.1

Introduzione

Cos come i dati sono contenuti in memoria (nellarea dati), anche il codice lo
, e si trova nellarea codice. E ovvio che sia cos, altrimenti il nostro
microprocessore non sarebbe in grado di individuarlo ed eseguirlo.
Pertanto, ogni istruzione ha un indirizzo fisico in memoria: per questo motivo
non vi alcuna ragione per cui unistruzione non debba essere puntata da un
puntatore.
In C++ questa tecnica permessa in parte, nel senso che possibile
puntare ad una funzione.
Se

si

ha

qualche

basilare

conoscenza

del

linguaggio

macchina,

probabilmente si sapr come sono formate le istruzioni allinterno del


processore: da un codice operativo, che definisce loperazione da eseguire e
il registro su cui operare, pi un eventuale numero che pu definire un
operando immediato od una locazione di memoria in cui presente un dato
(secondo operando).
Un blocco di codice quindi cos presentato in memoria (i valori nelle
locazioni di memoria sono inseriti a caso).
Il blocco di codice contiene una
Op.Code 01
Numero 01
Op.Code 02
Op.Code 03
Numero 03
Op.Code 04
Numero 04
Op.Code 05
Op.Code 06
Numero 06

05
A0
4C
AA
03
05
5F
67
4D
30

serie di istruzioni (ad indirizzi


crescenti e consecutivi).
Se

il

blocco

inizia

con

listruzione 01 e termina con la


06, e questo blocco una
funzione, allora un puntatore ad
esso sar indirizzato sempre
verso

il

punto

dentrata

(istruzione 01).

2.2

Sintassi

Supponiamo di definire due funzioni in C++, chiamate somma() e


prodotto().

[C++] Funzione somma()


int somma (int a, int b)
{
return a + b;
}

10

[C++] Funzione prodotto()


int prodotto (int a, int b)
{
return a * b;
}
Analizziamo le somiglianze tra queste due funzioni:
1. Esse restituiscono un valore dello stesso tipo (int)
2. Esse accettano un numero uguale di argomenti (2)
3. Esse accettano lo stesso tipo di argomenti (int, int)
In effetti, come si ricorda, una funzione identificata da:

Tipo di dato restituito

Nome della funzione

Numero, nome e tipo degli argomenti

Bene, se sono costanti il primo ed il terzo punto (il nome degli argomenti
facoltativo), allora le due funzioni possono essere puntate da uno stesso
puntatore a funzione.
Un puntatore a funzione tiene costante il tipo restituito e gli argomenti, quindi
pu accettare un qualunque nome.
Pertanto possiamo avere un puntatore ad una funzione che restituisce un
intero e accetta due caratteri, o che restituisce void e accetta due reali e
cos via
Pertanto un puntatore a funzione definito da:

Nome del puntatore

Tipo restituito dalle funzioni puntate

Numero e tipo degli argomenti delle funzioni puntate

Vediamo quindi la sintassi per la creazione di un puntatore a funzione.


tipo-restituito ( * nome-puntatore ) ( lista-parametri )
Nel nostro caso, per creare un puntatore adeguato ad entrambe le funzioni
somma() e prodotto(), avremmo dovuto scrivere:
int (*p)(int,int);

11

Abbiamo cos definito un puntatore ad una funzione che accetta due


argomenti di tipo intero e restituisce un intero.
Da notare subito che le parentesi (*p) sono necessarie, per una questione
di priorit degli operatori. Se avessimo scritto solamente
int *p (int,int);
Il compilatore avrebbe interpretato listruzione come un prototipo ad una
funzione che accetta due parametri interi e restituisce un int* (ossia un
puntatore ad intero); del puntatore a funzione non ci sarebbe stata traccia.
Volendo vi potreste accontentare del fatto che le parentesi sono necessarie
per una questione di priorit, ma vale la pena aprire una parentesi per dare
una motivazione pi esaustiva, in modo da chiarire meglio il concetto.
Entriamo allora nel merito della questione e partiamo analizzando la tavola
delle priorit degli operatori C++.
La tavola tratta dal manuale di riferimento ufficiale del linguaggio, tratta dal
sito ufficiale, www.cplusplus.com.
Priorit

Operatori

Descrizione

Associativit

::

Ambito

Sinistra

() [ ] -> . sizeof

Sinistra

++ --

Incremento e Decremento

Complemento a 1 (bit a bit)

NOT unario

& *

Puntatori e Reference

(type)

Casting di tipo

+ -

Segno

* / %

Operazioni aritmetiche

Sinistra

+ -

Operazioni aritmetiche

Sinistra

<< >>

Shift (bit a bit)

Sinistra

< <= > >=

Operatori relazionali

Sinistra

== !=

Operatori relazionali

Sinistra

& ^ |

Operatori bit a bit

Sinistra

10

&& ||

Operatori logici

Sinistra

11

?:
= += -= *= /= %=
>>= <<= &= ^= |=
,

Operatore condizionale

Destra

Assegnazione

Destra

Virgola, separatore

Sinistra

12
13

Destra

12

Riguardiamo ora la nostra espressione errata:


int *p (int,int);
Il compilatore, utilizzando le precedenze imposte, stabilisce per prima cosa
che ci sono dei simboli vicino allidentificatore p.
Deve scegliere come interpretare lasterisco (*) e la parte tra parentesi.
Seguendo la tabella prevalgono queste ultime (priorit 2 contro 3).
Il compilatore decreta quindi che p una funzione con due argomento interi.
A questo punto associa lasterisco con int per arrivare al tipo restituito
(puntatore ad intero).
Vediamo ora cosa succede con la versione corretta:
int (*p)(int,int);
Il compilatore trova due coppie di parentesi: seguendo la tabella, si segue
lassociativit a sinistra, quindi viene analizzato prima lidentificatore *p,
quindi la coppia di argomenti.
Si stabilisce quindi che *p accetta due argomenti interi.
Siamo di fronte ad un puntatore a funzione che accetta due argomenti interi.
Chiusa la parentesi, torniamo agli aspetti puramente sintattici.
Abbiamo dichiarato un puntatore a funzione, ora dobbiamo assegnargli
qualcosa. Ovviamente possiamo assegnare solo qualcosa di accettabile,
ossia una funzione che restituisce un intero e accetta due interi come
argomenti.
Possiamo allora assegnargli una delle due funzioni create prima (somma() e
prodotto()).
Essendo la variabile di tipo puntatore, il suo contenuto dovr essere un
indirizzo.
Come succede con un vettore, possibile ottenere lindirizzo di una funzione
(in particolare, del suo punto dentrata, ossia dellinizio) semplicemente
scrivendo il suo nome senza nessuna parentesi di seguito.
E inoltre disponibile la versione che antepone il carattere di indirizzo (&)
davanti al nome della funzione, senza argomenti.
Punto dingresso di una funzione
nome-funzione
&nume-funzione

13

Ecco le due possibili assegnazioni.

[C++] Assegnazione di una funzione


// Dichiaro il puntatore
int (*p)(int,int);
// Gli assegno la funzione somma
p = somma;
// Gli assegno la funzione prodotto
p = prodotto;
A questo punto il puntatore indirizzato verso una funzione. Come possiamo
fare per utilizzarne il contenuto? Semplice, come abbiamo sempre fatto con
un qualunque puntatore, ossia usando loperatore * per dereferenziarlo. Una
volta dereferenziato, tuttavia, necessario passare i parametri attuali alla
funzione, in modo che questa venga utilizzata.
Il programma sottostante (completo) mostra un semplice esempio di utilizzo
di funzioni puntate.

[C++] Utilizzo di funzioni puntate


#include <cstdlib>
#include <iostream>
using namespace std;
int somma (int a, int b) { return a + b; }
int prodotto (int a, int b) { return a * b; }
int main(int argc, char *argv[])
{
int (*p)(int,int);
// Utilizzo della funzione somma
p = somma;
cout << (*p)(4,3) << endl;
// Utilizzo della funzione prodotto
p = prodotto;
cout << (*p)(4,3) << endl;

system("PAUSE");
return EXIT_SUCCESS;

C da aggiungere che la dereferenziare il puntatore non richiesto per


richiamare una funzione. Invece di chiamare la funzione con listruzione
(*p)(4,3)
possibile utilizzare semplicemente la forma
p(4,3)
14

Le due forme sono del tutto equivalenti, ma la prima formalmente pi


corretta dal punto di vista teorico ed ha inoltre lenorme vantaggio di essere
pi chiara e leggibile.
La dichiarazione di un puntatore a funzione pu essere lunga e laboriosa. Se
due o pi volte deve essere riscritto lo stesso puntatore, assai facile cadere
in errore: per questo fortemente consigliato lutilizzo di unistruzione
typedef per ridefinire lintero tipo di puntatore.
La sintassi da utilizzare la seguente:
typedef tipo-rest (*alias) (lista-parametri)
Ad esempio, se viene scritta listruzione
typedef int (*punt1)(int,int)
Ogni dichiarazione del tipo
punt1 a;
viene automaticamente convertita in
int (*a)(int,int);
In determinati casi questa sintassi si pu rivelare particolarmente utile e non
c alcun motivo per cui non valga veramente la pena utilizzarla. Lunico
consiglio di fornire sempre una documentazione appropriata e di dare nomi
significativi ai vostri alias.
Laritmetica dei puntatori non vale per i puntatori a funzione. Tuttavia sono
disponibili gli operatori di confronti (!= e ==) per verificare se un puntatore
punta ad una determinata funzione o se ha valore NULL.

2.3

Utilizzo dei puntatori a funzione

Conclusa la parte sintattica, rimane ora da vedere come possibile utilizzarli


in determinate occasioni, per dimostrarne leffettiva importanza.
Una delle applicazioni pi frequenti quella di passare una funzione come
argomento di unaltra funzione.
E sufficiente infatti passare un puntatore e chiamare la funzione dal corpo
della principale.

15

Prima di scrivere noi una funzione che ne accetta unaltra come argomento,
osserviamo il codice scritto da altri e cerchiamo di capirne lutilizzo.
Abbiamo esempi di puntatori a funzioni anche nella libreria standard del C++.
In particolare, dentro la cstdlib, sono presenti alcune funzioni di
ordinamento e ricerca che possono operare su qualsiasi tipo di dato.
Essendo le funzioni disponibili gi dai tempi del linguaggio C, esse non
utilizzavano i template, ma lavoravano con puntatori void*, che, come
probabilmente si sa, sono puntatori generici che vengono poi tipizzati
allinterno del corpo della funzione.
Tuttavia, siccome le funzioni di ordinamento e ricerca devono eseguire
confronti, necessario stabilire una funzione per i confronti tra i vari tipi.
Essendo queste funzioni spesso diverse per i vari tipi di dati (basti pensare
alle classi definite dallutente), necessario per queste funzioni sapere come
comparare gli elementi: per farlo utilizza una funzione che riceve come
argomento.
Questa tecnica si utilizza essenzialmente per il fatto che molti algoritmi sono
praticamente identici sia che si lavori con un tipo di dato che con un altro: la
ricerca binaria, ad esempio, scansione sempre allo stesso modo un vettore
ed effettua dei confronti tra determinati elementi.
Se viene stabilito lo standard con cui si effettua il confronto, il resto
sempre uguale.
Proviamo allora a studiare la funzione bsearch() della libreria cstdlib.
void*

);

bsearch (
const void * key,
const void * base,
size_t num,
size_t width,
int (*fncompare)(const void *, const void * )

Analizziamo ora ogni suo argomento.


const void* key
Puntatore alloggetto da ricercare allinterno del vettore. Deve ovviamente
essere dello stesso tipo del vettore.
const void* base
Vettore su cui effettuare la ricerca.
16

size_t num
Numero di elementi del vettore.
size_t width
Dimensione di ogni elemento del vettore. E sufficiente settarlo a
sizeof (tipo)
int (*fncompare) (const void *, const void * )
Puntatore ad una funzione di confronto. Questa funzione deve accettare due
puntatori al tipo del vettore e restituire un valore intero che rispecchi la
seguente tabella:
Evento

Valore restituito

Il principio di funzionamento

elem1 < elem2

<0

analogo a quello utilizzato dalla

elem1 = elem2

elem1 > elem2

>0

funzione

strcmp()

confrontare

due

per

stringhe.

Il tipo restituito dalla funzione un puntatore allelemento trovato, se


presente, oppure un puntatore NULL nel caso la ricerca non abbia esito
positivo.
Si ricorda che il vettore di input deve essere ordinato in ordine crescente,
altrimenti lalgoritmo di ricerca binaria non eseguibile.
Vediamo quindi un programma che utilizza questa funzione.

[C++] Ricerca binaria


#include <cstdlib>
#include <iostream>
using namespace std;
int compare (const void* a, const void* b)
{
return (*(int*)a - *(int*)b);
}
int main(int argc, char *argv[])
{
// Vettore
int v[100];
// Lettura vettore
int n;
cout << "Elementi: ";
cin >> n;
for (int i = 0; i < n; i++)
{
cout << "Elemento " << i << ": ";
cin >> v[i];
}
17

// Elemento da cercare
int cerca;
cout << "Cerca il numero: ";
cin >> cerca;
// Ricerca
int* p = (int*) bsearch (&cerca, v, n, sizeof (int), compare);
if (!p)
cout << "Elemento non trovato.\n";
else
cout << *p << " presente nel vettore.\n";
system("PAUSE");
return EXIT_SUCCESS;
}
Dopo aver letto il vettore e il valore da cercare da tastiera, il programma
esegue la ricerca attraverso la funzione bsearch(), passandogli come
argomento la funzione compare().
Questa seconda funzione esegue il confronto semplicemente come
sottrazione dei due numeri.
Se questi sono uguali la differenza sar zero, se il primo minore la
differenza sar un numero negativo, positivo nellultimo caso.
Viene cos rispettata la tabella di verit richiesta dalla funzione.
Questo sistema (confronto con sottrazione) comodo da utilizzare con altri
tipi di dati: tutti i numeri ma anche i caratteri.
Proviamo adesso ad utilizzare la stessa funzione qsort() per ordinare dei
vettori di vettori.
Partiamo innanzitutto con lordinare un vettore di stringhe, analizzando il
prototipo della funzione qsort(), contenuta nella libreria cstdlib.
void

);

qsort (
void * base,
size_t num,
size_t width,
int (*fncompare)(const void *, const void *)

Gli argomenti sono uguali a quelli descritti per la funzione bsearch(),


tranne per il fatto che non necessario un elemento da cercare.
Questa volta per effettuare i vari confronti utilizzeremo una funzione che
richiama a sua volta la funzione strcmp(), definita nella libreria cstring.
Lunico motivo per cui non possiamo utilizzarla direttamente che non
possiede le caratteristiche per essere puntata dal puntatore definito dalla
funzione qsort().

18

Vediamo il codice completo del programma.

[C++] Ordinamento vettore di stringhe


#include <cstdlib>
#include <iostream>
#include <cstring>
using namespace std;
int compare (const void* a, const void* b)
{
return strcmp ((char*)a,(char*)b);
}
int main(int argc, char *argv[])
{
// Vettore di stringhe
char str[100][80];
int n;
cout << "Numero stringhe: ";
cin >> n;
for (int i = 0; i < n; i++)
{
cout << "Stringa: ";
cin >> str[i];
}
cout << "NON ORDINATO\n";
for (int i = 0; i < n; i++)
cout << str[i] << endl;
// Ordinamento
qsort (str,n,sizeof str[0],compare);
cout << "ORDINATO\n";
for (int i = 0; i < n; i++)
cout << str[i] << endl;
system("PAUSE");
return EXIT_SUCCESS;
}
Un ulteriore esempio per dimostrare la versatilit dei puntatori a funzione:
lordinamento inverso.
Supponiamo di voler utilizzare qsort() per ordinare in ordine decrescente
un vettore di interi.
Sar sufficiente invertire la funzione di confronto, e renderla come segue:

[C++] Compare() inverso per interi


int compare (const void* a, const void* b)
{
return (*(int*)b - *(int*)a);
}
19

In questo modo i valori restituiti sono simmetrici rispetto a quelli originali e


lordinamento avviene in modo contrario.
Proviamo a questo punto a scrivere noi una funzione che utilizzi i puntatori a
funzioni, in modo da vederne il funzionamento nel suo complesso.
Per prima cosa proviamo a scrivere una versione di ricerca binaria,
esattamente come fa la bsearch().
Stabiliamo di tenere costanti gli stessi parametri della funzione di libreria.
Unimplementazione potrebbe essere come segue ( stato messo il codice
completo di un programma per testarne il funzionamento).

[C++] bsearch() artigianale


#include <cstdlib>
#include <iostream>
using namespace std;
void* mybsearch (
const void*,
const void*,
size_t,
size_t,
int (*)(const void *, const void *)
);
int compC (const void* a, const void* b)
{
return (*(char*)a - *(char*)b);
}
int compI (const void* a, const void* b)
{
return (*(int*)a - *(int*)b);
}
int main(int argc, char *argv[])
{
char str[] = "abcdefghijklmno";
char src = 'p';
int vet[] = { 10 , 20 , 30 , 50 };
int srcI = 10;

//
//
//
//

Vettore di char
Non lo trover
Vettore di interi
Lo trover

char* f1 = (char*)mybsearch (&src, str, strlen(str), sizeof


compC);
int* f2 = (int*) mybsearch (&srcI, vet, 4, sizeof (int), compI);

(char),

if (f1 != NULL)
cout << "il carattere " << *f1 << " e' stato trovato\n";
else
cout << "il carattere " << src << " non e' stato trovato\n";
if (f2 != NULL)
cout << "il numero " << *f2 << " e' stato trovato\n";
else
cout << "il numero " << srcI << " non e' stato trovato\n";

20

system("PAUSE");
return EXIT_SUCCESS;

void* mybsearch (const void* search, const void* vet,


size_t n, size_t width, int (*p)(const void *, const void *))
{
size_t mezzo;
size_t inf = 0;
size_t sup = n - 1;
while (inf <= sup)
{
mezzo = (inf + sup) / 2;
int res = (*p)(((char*)vet+ mezzo*width),search);
if (res == 0)
return ((char*)vet+ mezzo*width);
else
if (res < 0)
inf = mezzo + 1;
else
sup = mezzo - 1;

}
return NULL;
}

Partiamo con due parole sullalgoritmo di ricerca binaria: questa ricerca testa
lelemento centrale di un vettore, confrontandolo con lelemento cercato: se
non vi uguaglianza, si verifica il maggiore dei due: essendo il vettore
ordinato (funzionalit richiesta dallalgoritmo) lecito pensare che, se il
numero cercato maggiore di quello centrale del vettore, allora ci che
cerchiamo potrebbe trovarsi nella seconda met del vettore; se minore,
allora potrebbe essere nella parte inferiore.
Si sposta quindi opportunamente il limite del vettore, in modo da lavorare
solo su una delle due met. Si ripete questo procedimento fino a quando non
si arriva ad un punto che il vettore da testare formato da un solo elemento.
Se non nemmeno quello, allora siamo sicuri che la ricerca ha dato esito
negativo.
Sulla ricerca binaria (detta anche dicotomica) si potrebbe parlare per molto
tempo, fornendo anche dimostrazioni di verit ed efficienza, ma non questo
il nostro obiettivo.
Quello che ci interessa vedere il funzionamento del puntatore a funzione.
Come si nota facilmente, la funzione viene richiamata sempre con lelemento
centrale del vettore e quello ricercato, utilizzando una normale sintassi di
chiamata a funzione puntata. Un po dattenzione merita il pezzo di codice
(char*)vet + mezzo*width
In sostanza questa istruzione converte il vettore ricevuto in input (sotto forma
di void*) in un vettore di char (in modo da gestire ogni suo singolo byte),
21

quindi gli aggiunge la quantit necessaria per arrivare allelemento di indice


mezzo. Se avete letto la prima parte della guida, avrete capito di cosa
parliamo. Ci spostiamo in pratica di
mezzo*sizeof(tipo-vet)
byte in memoria, raggiungendo quindi la locazione di indice mezzo.
Abbiamo gi introdotto, anche se in modo informale, uno dei primi utilizzi dei
puntatori a funzione: dato un algoritmo standard, possibile applicargli criteri
diversi a seconda dellutilizzo che se ne vuole fare. Questi criteri si possono
applicare grazie ai puntatori a funzione.
Una nota importante: quando si scrive una funzione che ne accetta unaltra
come argomento, bene specificare sempre, oltre ai dati intrinseci ricavabili
dal prototipo del puntatore, la natura dei dati restituiti e presi come
argomenti. Ad esempio, per la funzione compare() fondamentale sapere
che il risultato deve essere uguale a zero in caso di uguaglianza e minore o
maggiore di zero a seconda di quale sia lelemento pi grande. Il primo
consiglio che vogliamo quindi fornire quindi quello di allegare alle vostre
routine una piccola guida, magari scritta subito prima del prototipo o, se la
fornite, allinterno dellimplementazione.
Lultimo aspetto che ci rimane da analizzare quello della restituzione di un
puntatore a funzione.
In realt questa tecnica non molto semplice da utilizzare e ricordare, quindi
bene sempre utilizzare unistruzione typedef per facilitare il compito.
Osservate la sintassi da utilizzare per dichiarare una funzione che restituisce
un puntatore ad una seconda funzione.
tipo-punt ( *nome-funz ( arg-funz ) ) ( arg-puntatore );
Analizziamo i vari valori:
tipo_punt
Il valore restituito dalla funzione puntata restituita.
nome-funz
Nome della funzione che restituir il puntatore.
arg-funz
Lista dei parametri della funzione che restituir il puntatore.
22

arg-puntatore
Lista dei tipi della funzione che viene restituita sotto forma di puntatore dalla
funzione.
Ecco un esempio di programma che, a seconda del carattere ricevuto,
restituisce un puntatore alla funzione somma o alla funzione prodotto.

[C++] Restituzione di un puntatore a funzione


#include <cstdlib>
#include <iostream>
using namespace std;
// Funzioni aritmetiche
int somma (int a, int b) { return a + b; }
int prodotto (int a, int b) { return a * b; }
// Funzione che restituisce un puntatore a funzione
int (*scelta(char))(int,int);
int main(int argc, char *argv[])
{
// Lettura operandi
int x1, x2;
cout << "Operando 01: ";
cin >> x1;
cout << "Operando 02: ";
cin >> x2;
// Lettura della scelta
char cosa;
cout << "+ (Addizione\t * (Prodotto):\t";
cin >> cosa;
cout << "Risultato: " << (*scelta(cosa))(x1,x2) << endl;

system("PAUSE");
return EXIT_SUCCESS;

int (*scelta(char what))(int,int)


{
switch (what)
{
case '+': return &somma;
break;
case '*': return &prodotto; break;
default: return NULL;
}
}
Il codice potrebbe risultare decisamente pi leggibile con lutilizzo di una
typedef per definire il puntatore alla funzione.

[C++] Restituzione di un puntatore a funzione V.2


#include <cstdlib>
#include <iostream>
using namespace std;
23

// Funzioni aritmetiche
int somma (int a, int b) { return a + b; }
int prodotto (int a, int b) { return a * b; }
// Definizione del puntatore
typedef int (*operazione)(int,int);
// Funzione che restituisce un puntatore a funzione
operazione scelta (char);
int main(int argc, char *argv[])
{
// Lettura operandi
int x1, x2;
cout << "Operando 01: ";
cin >> x1;
cout << "Operando 02: ";
cin >> x2;
// Lettura della scelta
char cosa;
cout << "+ (Addizione\t * (Prodotto):\t";
cin >> cosa;
cout << "Risultato: " << (*scelta(cosa))(x1,x2) << endl;

system("PAUSE");
return EXIT_SUCCESS;

operazione scelta (char what)


{
switch (what)
{
case '+': return &somma;
break;
case '*': return &prodotto; break;
default: return NULL;
}
}
Decisamente pi chiaro, non trovate?

2.4

Vettori di puntatori a funzione

Come esistono vettori per tutti i tipi di dati, esistono anche i vettori di
puntatori a funzione.
Questa funzionalit si rivela particolarmente utile quando si creano
programmi con men, visto che risparmiano il classico blocco switch che
richiama le funzioni manualmente.
Partiamo dalla sintassi, che a questo punto dovrebbe risultare abbastanza
immediata. Come sempre c da tener presente lutilizzo delle parentesi per
la precedenza degli operatori.
tipo-rest ( * nome-punt [dim-vettore] )( parametri )
24

Abbiamo allocato il vettore in modo statico, quindi la dimensione deve essere


nota gi in fase di compilazione, con tutte le regole che ne conseguono:

dim-vettore deve essere un numero intero

dim-vettore deve essere un parametro immediato o una costante

dim-vettore non pu essere una variabile

Le regole rimangono sempre quelle viste fino ad ora: durante lassegnazione


si assegna ad un elemento del vettore una funzione, durante la chiamata la
dereferziazione facoltativa ma consigliata.
int (*p[10]) (int,int);
p[0] = funz1;
p[1] = funz2;
for (int i = 0; i < 2; i++)
cout << (*p[i]) (5,10) << endl;
Il programma sottostante un esempio complete di utilizzo di un vettore di
puntatori a funzione.

[C++] Vettore di puntatori a funzione


#include <cstdlib>
#include <iostream>
#define NMAX 20
using namespace std;
int
int
int
int
int
int

end (int, int);


somma (int, int);
differenza (int, int);
prodotto (int, int);
quoziente (int, int);
resto (int, int);

int main(int argc, char *argv[])


{
// Puntatore
int (*op[NMAX])(int,int);
// Funzioni
op[0] = NULL;
op[1] = somma;
op[2] = differenza;
op[3] = prodotto;
op[4] = quoziente;
op[5] = resto;
// Variabile per la scelta
int scelta;
do {
// Men
cout << "0. Fine programma\n";
25

cout
cout
cout
cout
cout

<<
<<
<<
<<
<<

"1.
"2.
"3.
"4.
"5.

Somma\n";
Differenza\n";
Prodotto\n";
Quoziente\n";
Resto\n";

// Scelta
do {
cout << "\nSCELTA: ";
cin >> scelta;
} while (scelta < 0 || scelta > 5);
if (scelta != 0)
{
// Lettura operandi
int a, b;
cout << "OPERANDO 1: ";
cin >> a;
cout << "OPERANDO 2: ";
cin >> b;
// Chiamata della funzione
int res = (*op[scelta])(a,b);

// Risultato
cout << "RISULTATO: " << res << endl;

system("PAUSE");
} while (scelta != 0);
}

return EXIT_SUCCESS;

int somma (int a, int b)


{
return a + b;
}
int differenza (int a, int b)
{
return a - b;
}
int prodotto (int a, int b)
{
return a * b;
}
int quoziente (int a, int b)
{
return a / b;
}
int resto (int a, int b)
{
return a % b;
}
Per prima cosa viene riempito il vettore con le funzioni appena create. Visto
che lopzione 0 usata per terminare il programma (e comunque non ha
senso unopzione zero), il primo puntatore lasciato vuoto con il valore

26

NULL. In seguito, letta la variabile scelta, baster accedere allelemento


scelta del vettore per avere la funzione desiderata.
A questo punto dovreste aver familiarizzato con i puntatore a funzione e aver
capito i loro principali utilizzi.
Un esempio che forse vi capiter di utilizzare quello del multi-threading: in
ambiente

Windows,

la

funzione

createThread(),

che

esegue

unoperazione contemporaneamente a quella attuale, richiede, tra i


parametri, proprio un puntatore alla funzione da eseguire in parallelo.
Nel corso della vostra avventura di programmatori vi imbatterete sicuramente
in problemi che necessitano o sono semplificati con i puntatori a funzione,
quindi non dimenticate mai di avere questo potente strumento.

27