Ambito delle variabili
Esaminiamo più a fondo le variabili dichiarate in una funzione e le loro
relazioni con quelle dichiarate in altre funzioni.
Per loro natura, le funzioni C sono costruite come moduli indipendenti.
Dato che le variabili create in una funzione sono disponibili solo alla
funzione stessa, si dice che esse sono locali alla funzione.
Questo termine si riferisce alla portata o ambito di una variabile,
definito come la sezione del programma in cui la variabile è valida o
nota. Una variabile può avere un ambito locale o globale.
Una variabile (con ambito) locale è una variabile le cui locazioni di
memoria sono state riservate da un’istruzione di dichiarazione
presente nel corpo della funzione.
Le variabili locali sono significative solo se usate in espressioni o
istruzioni entro la funzione che le dichiara.
Ciò significa che lo stesso nome di variabile può essere dichiarato e
usato in più di una funzione: per ogni funzione che dichiara la
variabile, viene creata una variabile distinta.
Tutte le variabili usate finora erano locali. Ciò è un risultato diretto
dell’avere situato le istruzioni di dichiarazione entro le funzioni e
di averle usate come istruzioni di definizione che fanno riservare
al computer la memoria per la variabile dichiarata.
Ma le istruzioni di dichiarazione possono essere situate fuori dalle
funzioni, e non devono necessariamente agire come definizioni
che fanno riservare nuove aree di memoria per la variabile
dichiarata.
Una variabile (con ambito) globale è una variabile la cui area di
memoria è stata creata da un’istruzione di dichiarazione esterna a
qualsiasi funzione. Perciò essa è detta anche esterna.
Le variabili globali possono essere usate da tutte le funzioni di un
programma che siano fisicamente situate dopo le loro
dichiarazioni.
Ciò è illustrato dal programma seguente, dove abbiamo usato
volutamente lo stesso nome di variabile all’interno di entrambe le
funzioni contenute nel programma.
int primonum;
/* crea la variabile GLOBALE primonum */
#include <stdio.h>
void main(void)
{
int secnum;
/* crea una prima variabile LOCALE secnum */
void valfun(void);
primonum = 10;
secnum = 20;
printf("\nDa main(): 1° numero
printf("\nDa main(): 2° numero
valfun();
printf("\nDa main() ancora: 1°
printf("\nDa main() ancora: 2°
= %d",primonum);
= %d\n",secnum);
numero = %d",primonum);
numero = %d",secnum);
}
void valfun(void)
{
int secnum;
/*crea una seconda variabile LOCALE secnum*/
secnum = 30;
/*influenza solo questa variabile LOCALE*/
printf("\nDa valfun(): 1° numero = %d",primonum);
printf("\nDa valfun(): 2° numero = %d\n",secnum);
primonum = 40; /*cambia primonum per entrambe le funzioni*/
return;
}
La variabile primonum è globale, perché la sua area di memoria è
creata da una istruzione di dichiarazione situata al di fuori di una
funzione.
Dato che entrambe le funzioni main() e valfun() sono definite
dopo la dichiarazione di primonum, entrambe possono usare tale
variabile senza bisogno di un’ulteriore dichiarazione.
Il programma contiene anche due variabili locali separate, entrambe
di nome secnum.
La memoria per la variabile secnum nominata in main() è creata
dall’istruzione di dichiarazione situata in main().
Una differente area di memoria per la variabile secnum in
valfun() è creata dall’istruzione di dichiarazione situata nella
funzione valfun().
Ciascuna variabile di nome secnum è locale alla funzione in cui è
creata la sua area di memoria, e può essere usata solo dall’interno
della relativa funzione.
Così, quando secnum() è usata in
• main(), si accede alll’area di memoria riservata da main() per la
sua variabile secnum;
• valfun(), si accede all’area di memoria riservata da valfun()
per la sua variabile secnum.
Quando si esegue il programma, si ottiene la seguente uscita:
Dato che primonum è una variabile globale, il suo valore può essere
usato e cambiato sia da main() sia da valfun().
Inizialmente entrambe le funzioni visualizzano il valore 10 che
main() ha memorizzato in primonum.
Prima di ritornare, valfun() cambia il valore di primonum in 40,
che è il valore visualizzato quando la variabile primonum è
visualizzata successivamente dall’interno di main().
Dato che ogni funzione “conosce” solo le sue proprie variabili locali,
main() può inviare alla funzione printf() solo il valore della
sua secnum, e valfun() può inviare a printf() solo il valore
della sua secnum.
Così, ogni qualvolta secnum è ottenuta da
• main() viene visualizzato il valore 20
• valfun() viene visualizzato 30.
Il C non confonde le due variabili secnum peché in un dato momento
può essere eseguita una sola funzione, e si accede solo all’area di
memoria per la variabile creata dalla funzione correntemente in
esecuzione.
Se la funzione usa una variabile non locale a essa, il programma cerca
il suo nome nelle aree di memoria globale.
Uso delle variabili globali. Anziché passare variabili a una funzione,
sarebbe possibile rendere tutte le variabili globali.
Ciò non va fatto, perché il rendere indiscriminatamente tutte le
variabili globali distrugge istantaneamente tutte le precauzioni fornite
dal C per rendere le funzioni indipendenti e isolate l’una dall’altra,
compresa la necessità di progettare accuratamente il tipo di
argomenti necessari a una funzione, le variabili usate in essa e il
valore restituito.
L’uso di sole variabili globali può essere controproducente,
specialmente in programmi di grandi dimensioni che contengono
molte funzioni create dall’utente.
Dato che tutte le variabili di una funzione devono essere dichiarate, se
si creano funzioni che usano variabili globali è necessario scrivere le
appropriate dichiarazioni globali all’inizio di ogni programma che usi
la funzione, dato che esse non si trovano più nella funzione stessa.
Ancora più lunga e noiosa è la ricerca di un errore in un lungo
programma che usi variabili globali, dato che esse possono essere
usate e modificate da qualsiasi funzione che segua la dichiarazione
globale.
Tuttavia, le variabili globali possono anche risultare molto utili: se più
funzioni richiedono l’accesso a un gruppo di tabelle, le variabili
globali consentono alle funzioni di compiere cambiamenti a una
stessa tabella senza la necessità di passaggi multipli di tabelle.
Classi di memorizzazione delle variabili
Come abbiamo visto, l’ambito di una variabile si può considerare
come lo spazio entro il programma dove la variabile è valida e può
essere usata.
Dato un programma, si può prendere una matita e tracciare un
riquadro intorno alla sezione del programma che rappresenta
l’ambito di validità della variabile.
In aggiunta alla dimensione spaziale rappresentata dal suo ambito,
una variabile possiede anche una dimensione temporale, che si
riferisce alla quantità di tempo durante il quale le locazioni di
memoria sono riservate per essa.
Ad es., tutte le locazioni di memoria di variabili sono rilasciate al
computer quando termina l’esecuzione di un programma.
Tuttavia, mentre un programma è ancora in esecuzione, aree di
memoria per variabili temporanee sono riservate e successivamente
restituite di nuovo al computer.
Dove e per quanto tempo le locazioni di memoria per le variabili siano
mantenute, prima di essere rilasciate, può essere determinato dalla
classe di memorizzazione di una variabile.
Le quattro classi di memorizzazione disponibili sono:
Se si usa uno di questi nomi di classi, esso deve essere situato in
un’istruzione di dichiarazione prima del tipo dati della variabile.
Esempi di istruzioni di dichiarazione che comprendono una
designazione di classe di memorizzazione sono indicati in tabella.
Per illustrare il significato della classe di memorizzazione di una
variabile, consideriamo dapprima le variabili locali (quelle create
entro una funzione) e poi quelle globali (create fuori da una
funzione).
Classi di memorizzazione di variabili locali
Le variabili locali possono essere membri solo delle classi di
memorizzazione
auto. Se nell’istruzione di dichiarazione non s’inserisce alcuna
descrizione di classe, la variabile è assegnata automaticamente alla
classe auto (da automatico), che è pertanto la classe
preimpostata del C.
Perciò tutte le variabili locali finora usate, essendo stata omessa la
designazione di classe, erano auto.
L’area di memoria per le variabili locali automatiche è riservata o
creata automaticamente ogni volta che viene chiamata una funzione
che dichiari variabili automatiche.
Fin tanto che la funzione non abbia restituito il controllo alla sua
funzione chiamante, tutte le variabili automatiche locali alla funzione
sono “vive”, cioè è disponibile per esse l’area di memoria.
Quando la funzione chiamata restituisce il controllo alla sua funzione
chiamante, le sue variabili automatche locali “muoiono”, cioè la
memorizzazione per esse è restituita al computer.
Il processo si ripete ogni volta che una funzione viene chiamata.
Come esempio, consideriamo il programma seguente, dove la
funzione testauto() è chiamata tre volte dalla main().
#include <stdio.h>
void main(void)
{
int cont;
/* cont è variabile AUTO LOCALE */
void testauto(void);
/* prototipo di funzione */
for(cont = 1; cont <= 3; ++cont)
testauto();
}
void testauto(void)
{
int num = 0; /*crea e pone=0 la variabile AUTO num*/
printf("\nLa variabile automatica num vale %d", num);
++num;
return;
}
Esso produce un’uscita in apparenza inaspettata:
Il valore della variabile automatica num è 0
Il valore della variabile automatica num è 0
Il valore della variabile automatica num è 0
Ciò perché, ogni volta che si chiama testauto(), la variabile
automatica num è creata e inizializzata a 0.
Quando la funzione ritorna il controllo a main(), la variabile num è
distrutta insieme a qualsiasi valore memorizzato in essa.
Così, l’effetto di incrementare num in testauto(), prima
dell’istruzione return della funzione, si perde quando il controllo
è restituito a main().
static. L’uso di variabili automatiche è appropriato per la maggior
parte delle applicazioni.
Tuttavia vi sono casi in cui si vorrebbe che una funzione ricordi i
valori tra le chiamate successive, e questo è lo scopo della classe
di memorizzazione static.
Una variabile locale dichiarata static fa sì che il programma
mantenga la variabile e il suo ultimo valore anche quando la
funzione che l‘ha dichiarata non è più in esecuzione.
Una variabile statica locale non viene creata e distrutta ogni volta che
si chiama la funzione che la dichiara ma, una volta creata, rimane
in essere per tutta la vita del programma.
Perciò l’ultimo valore memorizzato nella variabile quando è terminata
l’esecuzione della funzione è disponibile per la funzione la
prossima volta che essa sarà chiamata.
Dato che le variabili statiche locali mantengono i loro valori, esse
non vengono inizializzate in un’istruzione di dichiarazione nella
stessa maniera delle variabili automatiche.
Infatti, consideriamo l’istruzione di dichiarazione del programma
precedente che crea e imposta a zero la variabile automatica num
int num = 0;
Essa è detta inizializzazione di run time, dato che l’inizializzazione ha
luogo ogni volta che s’incontra l’istruzione.
Questo tipo d’inizializzazione resetta il valore della variabile a zero
ogni volta che la funzione è chiamata, distruggendone il valore.
Invece l’istruzione di dichiarazione
static int num = 0;
crea una variabile statica (sia locale sia globale) e vi inserisce un
valore d’inizializzazione una sola volta, quando il programma è
compilato.
Nelle successive chiamate della funzione, il valore nella variabile è
mantenuto senza venire ulteriormente inizializzato.
Perciò, se nel programma precedente modifichiamo come segue
l’istruzione di dichiarazione della variabile num
#include <stdio.h>
void main(void)
{
int cont;
/* cont è variabile AUTO LOCALE */
void test(void);
/* prototipo di funzione */
for(cont = 1; cont <= 3; ++cont)
test();
}
void test(void)
{
static int num = 0; /*num è variabile STATICA LOCALE*/
printf("\nLa variabile statica num vale ora %d", num);
++num;
return;
}
otteniamo la seguente uscita:
Il valore della variabile statica num è ora 0
Il valore della variabile statica num è ora 1
Il valore della variabile statica num è ora 2
Come si vede, la variabile statica num è ipostata a zero solo una
volta. Quindi la funzione test() la incrementa subito prima di
restituire il controllo a main().
Il valore che num ha quando si lascia la funzione test() è
trattenuto e visualizzato quando la funzione è chiamata di nuovo.
• A differenza delle variabili automatiche, che possono essere
inizializzate o con costanti o con espressioni che usano sia costanti
sia variabili inizializate in precedenza,
le variabili statiche possono essere inizializzate
solo usando costanti o espressioni di costanti
quali 3.2+8.0.
• Inoltre, a differenza delle variabili automatiche
le variabili statiche sono impostate a zero
quando non sia fatta una inizializzazione esplicita.
Perciò, nel programma precedente, la specifica inizializzazione di
num a zero non è necessaria.
register. L’ultima classe di memorizzazione disponibile per le
variabili locali è la classe register, che viene usata meno
diffusamente delle classi auto o static.
Le variabili register hanno la stessa durata temporale delle
variabili auto, coè una variabile register locale è creata
quando si entra nella funzione che la dichiara, e distrutta quando
termina l’esecuzione della funzione.
L’unica differenza tra le variabili auto e register è il posto dove
è situata la memoria per la variabile.
La memoria per tutte le variabili (locali e globali), tranne le register
è riservata nell’area di memoria del computer.
Ma i computer hanno anche zone aggiuntive di memoria ad alta
velocità situate direttamente nell’unità centrale di elaborazione, che
possono essere usate anche per la memorizzazione di variabili.
Queste speciali aree di memoria ad alta velocità sono dette registri.
Dato che essi sono fisicamente situati nell’unità centrale di
elaborazione, si può accedere a essi più velocemente che alle
normali aree di memoria situate nell’unità di memoria del
computer.
Inoltre le istruzioni che si riferiscono ai registri richiedono
tipicamente meno spazio di quelle che si riferiscono a locazioni di
memoria, dato che vi sono meno registri accessibili che locazioni
di memoria.
Oltre a diminuire le dimensioni di un programma C compilato, l’uso
di variabili register può anche aumentare la velocità di
esecuzione di un programma, se il computer in uso supporta
questo tipo dati.
Se non lo supporta, oppure se le variabili register dichiarate
eccedono la capacità in registri del computer, le variabili dichiarate
nella classe di memorizzazione register sono convertite
automaticamente nella classe di memorizazione auto.
L’unica restrizione nell’uso della classe di memorizzazione register
è che
non si può usare l’indirizzo di una variabile
register tramite l’operatore &
in quanto i registri non hanno indirizzi di memoria standard.
Classi di memorizzazione di variabili globali
Ricordiamo che le variabili esterne o globali sono create da istruzioni
di dichiarazione esterne a una funzione, e non vengono create e
distrutte con la chiamata a una funzione.
Una volta creata, una variabile globale esiste finché termina
l’esecuzione del programma in cui è dichiarata.
Perciò le variabili globali non possono essere dichiarate come membri
di classi di memorizzazione auto o register, che sono create e
distrutte mentre il programma è in esecuzione, mentre possono
essere dichiarate come membri delle classi di memorizzazione
static
extern
Le classi static ed extern hanno effetto solo sull’ambito, e non
sulla durata temporale delle variabili globali.
Come le variabili locali static, tutte le variabili globali sono
inizializzate a zero al momento della compilazione se non è
presente una esplicita inizializzazione.
extern. Lo scopo della classe di memorizzazione extern è di
estendere l’ambito di una variabile globale oltre i suoi confini naturali
Per comprendere ciò, osserviamo che tutti i programmi visti finora
erano sempre contenuti in un solo file. Ciò non è richiesto dal C.
I programmi di grandi dimensioni consistono tipicamente in molte
funzioni, memorizzate in più file.
Uno schema di esempio è mostrato nella figura seguente, dove le tre
funzioni main(), funz1(), funz2() sono memorizzate in un file,
e le due funzioni funz3(), funz4()in un altro.
Per i file illustrati in figura, le variabili globali prezzo, ricavo e
coupon dichiarate in file1.c possono essere usate solo dalle
funzioni main(), funz1() e funz2() in questo file.
La singola variabile globale tasso, dichiarata in file2.c può
essere usata solo dalle funzioni funz3() e funz4() in
file2.c.
Tuttavia, sebbene la variabile prezzo sia stata creata in file1.c,
potremmo volerla usare in file2.c.
Ciò è possibile se s’inserisce in file2.c l’istruzione di
dichiarazione
extern int prezzo;
come indicato in figura.
Dato che questa istruzione è all’inizio di file2.c, essa estende a
file2.c l’ambito della variabile prezzo, che così può essere
usata in funz3() e funz4().
Così, se s’inserisce in funz4() l’istruzione
extern float ricavo;
si estende a funz4() l’ambito della variabile globale ricavo,
creata in file1.c.
Analogamente, l’ambito della variabile globale tasso, creata in
file2.c, è esteso a funz1() e funz2() se s’inserisce
prima di funz1() l’istruzione di dichiarazione
extern double tasso;
Si osservi che tasso non è disponibile in main().
Un’istruzione di dichiarazione che contenga la parola extern è
differente da qualsiasi altra istruzione di dichiarazione, in quanto
non causa la creazione di una nuova
variabile riservandole nuova memoria
Essa informa semplicemente il computer che la variabile già esiste e
può ora essere usata.
La memorizzazione effettiva per la variabile deve essere creata in
qualche altra parte del programma usando una, e solo una,
istruzione di dichiarazione globale, in cui non vi sia la parola
extern.
L’inizializzazione di una variabile globale può essere fatta con la sua
dichiarazione originale, mentre non è consentita all’interno di una
dichiarazione extern, e causerebbe un errore di compilazione.
L’esistenza della classe di memorizzazione extern è la ragione
per cui abbiamo distinto con cura la creazione e la dichiarazione di
una variabile. Infatti
le istruzioni di dichiarazione che contengono la parola
extern non creano nuove aree di memoria, ma
estendono solo l’ambito di variabili globali esistenti.
static. L’ultima classe globale, static, è usata per evitare
l’estensione di una variabile globale in un secondo file.
Le variabili static globali sono dichiarate nella stessa maniera
delle static locali, tranne per il fatto che l’istruzione di
dichiarazione è situata fuori una funzione.
L’ambito di una variabile static globale non si può estendere oltre il
file in cui è dichiarata. Ciò fornisce un grado di riservatezza per le
variabili static globali.
Dato che esse sono “conosciute” e possono essere usate solo nel file
in cui sono dichiarate, altri file non possono accedervi o cambianre i
valori.
Le variabili static globali non possono essere successivamente
estese a un secondo file con un’istruzione di dichiarazione extern,
dato che ciò causerebbe un errore di compilazione.
Anche i vettori, come le variabili scalari, possono essere dichiarati
dentro o fuori una funzione.
I vettori dichiarati dentro una funzione sono detti vettori locali, quelli
dichiarati fuori sono detti vettori globali.
Consideriamo, ad es., la seguente sezione di codice:
Come le variabili scalari, anche i vettori globali e i locali static
sono creati una sola volta, in fase di compilazione, e conservano i
loro valori finché termina l’esecuzione di main().
I vettori auto sono creati e distrutti ogni volta che viene chiamata la
funzione in cui sono locali.
Perciò i vettori litri, dist e corse sono creati una sola volta,
mentre km è creato e distrutto 10 volte.
Vettori a due dimensioni. Come i vettori a una dimensione, anche
quelli a due dimensioni possono essere dichiarati dentro o fuori una
funzione.
Quelli dichiarati all’interno di una funzione sono locali, quelli all’esterno
sono globali. Ad es., il seguente segmento di codice:
int bingo[2] [3];
main()
{
static double lotto[104] [6];
double totip[52] [6];
.
.
}
dichiara:
 bingo come un vettore a due dimensioni globale di 2 righe e 3
colonne
 lotto come un vettore a due dimensioni statico locale di 104 righe
e 6 colonne
 totip come un vettore automatico locale di 52 righe e 6 colonne.
Chiamate per valore e per riferimento
Nel normale corso delle operazioni, una funzione chiamata riceve
valori dalla sua funzione chiamante, li memorizza nei suoi propri
argomenti locali, manipola questi in modo opportuno ed
eventualmente restituisce un singolo valore.
Questo metodo di chiamare una funzione e passarle valori è detto
chiamata per valore a una funzione.
La chiamata per valore è una caratteristica vantaggiosa del C, che
consente di scrivere le funzioni come entità indipendenti che
possono usare qualsiasi nome di variabile senza preoccuparsi se
anche altre funzioni stiano usando lo stesso nome.
Nello scrivere una funzione, è opportuno considerare gli argomenti o
come variabili inizializzate, o come variabili alle quali saranno
assegnati valori quando la funzione sarà eseguita.
In nessun momento, tuttavia, la funzione chiamata ha accesso diretto
a qualsiasi variabile locale contenuta nella funzione chiamante.
Vi sono però casi in cui conviene dare a una funzione chiamata
accesso alle variabili locali della funzione chiamante, in modo che la
funzione chiamata possa usare e cambiare i loro valori senza la
conoscenza della funzione chiamante, nella quale le variabili locali
sono dichiarate.
Per fare ciò è necessario che alla funzione chiamata sia passato
l’indirizzo della variabile.
Una volta che la funzione chiamata abbia tale indirizzo, essa “sa
dove la variabile vive”, e può accedere a essa usando l’indirizzo e
l’operatore di indirezione *.
Il passaggio di indirizzi è detto chiamata per riferimento a una
funzione, dato che la funzione chiamata può riferire la, o accedere
alla variabile usando l’indirizzo passato.
Vediamo le tecniche necessarie per passare indirizzi a una funzione
e fare sì che essa li accetti e li usi.
Passaggio, memorizzazione e uso degli indirizzi. Per passare,
memorizzare e usare gli indirizzi si usano:
• l’operatore di indirizzo &
• i puntatori
• l’operatore di indirezione *.
Consideriamo, come esempio, la seguente funzione principale:
#include <stdio.h>
void main(void)
{
double primonum, secnum;
void ordinum(double *, double *); /* prototipo */
printf("Scrivi due numeri: ");
scanf("%lf %lf", &primonum, &secnum);
ordinum(&primonum, &secnum);
/* chiamata */
printf("\nIl minore è %lf", primonum);
printf("\nIl maggiore è %lf", secnum);
}
Essa passa gli indirizzi delle due variabili primonum e secnum
alla funzione ordinum() (oltre che, come al solito, a scanf()),
la quale confronta i valori contenuti negli indirizzi passati ed
eventualmente li scambia, in modo che il valore più piccolo vada nel
primo indirizzo.
Se invece degli indirizzi di primonum e secnum fossero passati a
ordinum() i loro valori (come avveniva in un programma
precedente), la funzione non potrebbe scambiare i valori nelle
variabili, perché non avrebbe accesso a esse.
Nella funzione precedente il prototipo della funzione ordinum()
void ordinum(double *, double *);
dichiara che essa non restituisce un valore e si aspetta come
argomenti due puntatori a (indirizzi di) variabili in doppia precisione.
Perciò la successiva chiamata a ordinum() richiede che le siano
passati due indirizzi di numeri in doppia precisione.
Nella intestazione di ordinum()vanno dichiarati due argomenti che
possano memorizzare gli indirizzi passati, quali, ad es.,
double *num1_indir;
double *num2_indir;
(in tal modo sia num1_indir, sia num2_indir puntano a variabili
in doppia precisione).
Quindi l’intestazione sarà:
ordinum(double *num1_indir, double *num_2indir)
Prima di scrivere il corpo di ordinum() che confronti (e scambi se
necessario) i due valori, controlliamo che i valori cui si accede
usando gli indirizzi in num1_indir e num2_indir siano corretti,
scrivendo il programma seguente:
#include <stdio.h>
void main(void)
{
double primonum = 20.0, secnum = 5.0;
void ordinum(double *, double *);
/* prototipo */
ordinum(&primonum, &secnum);
/* chiamata */
}
void ordinum(double *num1_indir, double *num2_indir)
{
printf("Il numero il cui indirizzo è in num1_indir è
%lf", *num1_indir);
printf("\nIl numero il cui indirizzo è in num2_indir è
%lf", *num2_indir);
}
Esso produce la seguente uscita:
Il numero il cui indirizzo è in num1_indir è 20.000000
Il numero il cui indirizzo è in num2_indir è 5.000000
Osservazione. Nelle due chiamate a printf() in ordinum() si
usa l’operatore di indirezione * per accedere ai valori memorizzati
in primonum e secnum.
ordinum() non ha conoscenza di questi nomi di variabile, ma ha
l’indirizzo di primonum memorizzato in num1_indir, e quello di
secnum in num2_indir.
Avendo verificato che ordinum() può accedere alle variabili locali
primonum e secnum di main(), possiamo ora scrivere l’effettiva
funzione ordinum().
Essa confronta i valori nelle variabili primonum e secnum,
controllando se sono nell’ordine desiderato, tramite l’istruzione:
if (*num1_indir > *num2_indir)
La funzione ordinum() è allora:
void ordinum (double *num1_indir, double *num2_indir)
{
double temp;
if (*num1_indir > *num2_indir)
{
temp = *num1_indir;
*num1_indir = *num2_indir;
*num2_indir = temp;
}
return;
}
Si osservi che l’uso dei puntatori ha permesso di scambiare tra loro i
due valori se essi non sono nell’ordine desiderato.
Confronto tra chiamata per valore e per riferimento. Per
confrontare il diverso funzionamento della chiamata per valore e
della chiamata per riferimento a una funzione, consideriamo i due
programmi seguenti, che calcolano il cubo di un numero:
• il primo passando il valore della base (chiamata per valore),
#include <stdio.h>
int cuboVal(int);
int main()
{
int numero = 5;
printf("Il valore del numero è: %d\n", numero);
numero = cuboVal(numero); /*passa il valore di numero*/
printf("Il nuovo valore del numero è: %d\n", numero);
}
int cuboVal(int n)
{
return n*n*n;
}
•il secondo passando il suo indirizzo (chiamata per riferimento).
#include <stdio.h>
void cuboRif(int *);
int main()
{
int numero = 5;
printf("Il valore del numero è: %d\n", numero);
cuboRif(&numero); /* passa l'indirizzo di numero */
printf("Il nuovo valore del numero è: %d\n", numero);
}
void cuboRif(int *punt_n)
{
*punt_n = *punt_n * *punt_n * *punt_n;
}
Il primo programma passa il valore della variabile numero alla
funzione cuboVal utilizzando una chiamata per valore.
La funzione cuboVal eleva al cubo il suo argomento e restituisce a
main() il nuovo valore utilizzando un’istruzione return.
Il nuovo valore viene assegnato a numero nella funzione main().
Il secondo programma passa l’indirizzo della variabile numero alla
funzione cuboRif utilizzando una chiamata per riferimento.
La funzione cuboRif riceve come parametro un puntatore a un
int chiamato punt_n.
La funzione risolve il riferimento del puntatore ed eleva al cubo il
valore puntato da punt_n (cioè n), quindi assegna il valore a
*punt_n (che corrisponde al numero di main()), modificando
quindi il valore di numero in main().
I due programmi producono, ovviamente, la stessa uscita:
Il valore del numero è: 5
Il nuovo valore del numero è: 125
Bolle per riferimento. Come altro esempio, modifichiamo il
programma dell’ordinamento a bolle, in modo che chiami (per
valore) una funzione bolle_rif, la quale chiami a sua volta (per
riferimento) una funzione scambia.
#include <stdio.h>
#define N 10
int main()
{
int a[N] = {22,5,67,98,45,32,101,99,73,10};
int i, passi;
int bolle_rif(int [], int);
passi = bolle_rif(a, N);
printf("Il vettore ordinato in ordine crescente è:\n");
for (i = 0; i < N; ++i)
printf("%d ", a[i]);
}
int bolle_rif(int *elem, int numel)
{
void scambia(int *, int *);
int i, j, passi;
i = 0;
do
{
passi = 0;
for (j=1; j < numel; j++)
{
if (elem[j] < elem[j-1])
{
scambia(&elem[j], &elem[j-1]);
passi++;
}
}
i++;
}
while (i < numel - 1 && passi != 0);
return(i);
}
void scambia(int *punt_elem1, int *punt_elem2)
{
int temp = *punt_elem1;
*punt_elem1 = *punt_elem2;
*punt_elem2 = temp;
}
bolle_rif esegue l’ordinamento del vettore e chiama scambia
per scambiare di posto gli elementi elem[j] ed elem[j-1].
Poiché il C applica l’incapsulamento delle informazioni tra le funzioni,
scambia non avrà accesso ai singoli elementi del vettore di
bolle_rif.
Dato che bolle_rif ha necessità che scambia abbia accesso
agli elementi del vettore, in modo da poterli scambare di posto,
bolle_rif passa a scambia ognuno di quegli elementi
attraverso una chiamata per riferimento: in altri termini, passa in
modo esplicito l’indirizzo di ogni elemento del vettore.
Come sappiamo, un vettore completo sarebbe passato
automaticamente per riferimento, ma i suoi singoli elementi sono
scalari, e sono passati normalmente per valore.
Di conseguenza, nella chiamata a scambia, bolle_rif utilizza
l’operatore di indirizzo & con ognuno degli elementi del vettore,
con l’istruzione seguente:
scambia(&elem[j], &elem[j-1]);
che esegue una chiamata per riferimento.
La funzione scambia riceve &elem[j] nel puntatore
punt_elem1.
Sebbene alla funzione scambia non sia consentito conoscere il
nome elem[j] a causa dell’incapsulamento delle informazioni,
essa può comunque utilizzare *punt_elem1 come sinonimo per
elem[j].
Di conseguenza, quando scambia fa riferimento a *punt_elem1,
in realtà punta a elem[j] di bolle_rif.
(Analogamente, quando fa riferimento a *punt_elem2, in realtà
punta a elem[j-1] di bolle_rif).
Per quanto alla funzione scambia non sia consentito un gruppo di
istruzioni quali:
temp = elem[j];
elem[j] = elem[j-1];
elem[j-1] = temp;
lo stesso effetto si ottiene inserendo in essa il seguente gruppo di
istruzioni:
int temp = *punt_elem1;
*punt_elem1 = *punt_elem2;
*punt_elem2 = temp;
Osservazioni. Vediamo alcune caratteristiche della funzione
bolle_rif degne di nota.
1. Nella sua intestazione
int bolle_rif(int *elem, int numel)
per indicare che riceverà come argomento un vettore
unidimensionale di interi, abbiamo dichiarato elem come
int *elem
anziché come
int elem[]
dato che queste due notazioni sono completamente intercambiabili.
2. Il prototipo della funzione scambia è stato inserito nel corpo di
bolle_rif poiché questa è l’unica funzione che la chiama.
L’avere inserito il prototipo in bolle_rif limita le chiamate accettabili
di scambia esclusivamente a quelle eseguite all’interno di
bolle_rif. Le altre funzioni che tentassero di chiamare scambia
non avrebbero acceso al giusto prototipo di funzione.
Inserire i prototipi all’interno delle definizioni delle funzioni obbedisce al
cosiddetto principio del minimo privilegio.
3.La dimensione del vettore viene passata alla funzione bolle_rif
in modo esplicito, il che offre due benefici principali: la riusabilità del
software e una sua corretta progettazione.
Dato che la funzione è stata definita in modo da ricevere la
dimensione del vettore per mezzo di un argomento, essa potrà
essere utilizzata da un qualsiasi programma che debba riordinare
vettori unidimensionali di interi di qualsiasi dimensione.
4.Avremmo potuto memorizzare la dimensione del vettore in una
variabile globale, che fosse accessibile all’intero programma.
Questo approccio sarebe stato ancora più efficiente, perché non
sarebbe stato necessario creare una copia della dimensione durante
la chiamata della funzione.
Tuttavia, altri programmi che eventualmente richiedessero
l’ordinamento di vettori di interi potrebbero non contenere la stessa
variabile globale, e quindi non potrebbero usare la funzione.
Scarica

Fonda22