Sottoprogrammi

 

Obiettivi operativi

          Scrivere funzioni e procedure

          Utilizzare il valore di ritorno delle funzioni

          Gestire il passaggio dei parametri per valore e per riferimento

          Gestire le variabili locali

Obiettivi cognitivi

          La programmazione modulare

          I sottoprogrammi

          Approfondimenti sui concetti di funzione e procedura

          Parametri formali e attuali

          Visibilità delle variabili

 

8.1      Introduzione

 

La risoluzione di un problema passa per le fasi di analisi, codifica del programma e verifica. Quando il problema non è di piccole dimensioni conviene isolare dei sottoproblemi di minore complessità e per ognuno di questi fare l'ipotesi di avere a disposizione un segmento di programma, detto sottoprogramma, che lo risolve.

Lo stesso ragionamento può essere ripetuto sul singolo sottoproblema che, se necessario, deve essere scomposto a sua volta in moduli. Questa metodologia si definisce top-down, in quanto procede dall'alto (top, soluzione complessiva) verso il basso (down, soluzione dei sottoproblemi). Per esempio il programma per il pagamento automatizzato delle buste paga dei dipendenti di una società potrebbe essere scomposto nei sottoprogrammi di immissione delle ore lavorate, di calcolo dello stipendio e di visualizzazione e stampa dei cedolini di ogni dipendente. II calcolo dello stipendio potrebbe a sua volta essere ulteriormente scomposto nei sottoprogrammi che calcolano rispettivamente lo stipendio base, gli straordinari, i premi incentivanti, i rimborsi spesa e le trattenute fiscali. Per ognuno dei sottoprogrammi individuati è importante prima di tutto chiarire quali sono i dati sui quali opera e successivamente quali risultati produce in relazione alla soluzione complessiva.

II linguaggio Pascal mette a disposizione i migliori strumenti per applicare questa metodologia di programmazione. Come vedremo in dettaglio esiste la possibilità di creare sottoprogrammi, è disponibile un meccanismo chiaro e flessibile di condivisione dei dati, si possono definire entità (tipi, variabili, costanti ecc.) locali a un sottoprogramma e quindi visibili solo al suo interno, si possono infine creare gerarchie di sotto programmi.

I sottoprogrammi si usano anche per evitare di replicare più volte porzioni di codice sorgente. Invocare un sottoprograrnma significa infatti mandare in esecuzione la porzione di codice corrispondente. Quindi, se un sottoprogramma è invocato più volte, la sua porzione di codice è eseguita tante volte quante sono le invocazioni. Il risultato è allora quello di avere tante chiamate ma una sola porzione di codice: giust'appunto il sottoprogramma.

Per esempio, se stiamo risolvendo un problema di carattere scientifico e dobbiamo calcolare numerose volte il cubo di alcuni valori numerici, cosa c'è di meglio che creare un sottoprogramma specifico per il calcolo del cubo? Generalmente il programmatore ha a disposizione delle raccolte di sotto-programmi predefiniti già pronti all'uso, dette librerie, che può utilizzare senza conoscerne i dettagli implementativi. È questo il concetto dì scatola nera, cioè di uso di un segmento di codice senza conoscerne il suo funzionamento interno. Come abbiamo evidenziato nel capitolo 2, read e write sono appunto sottoprogrammi la cui dichiarazione è contenuta nelle librerie del Pascal.

Grazie agli specifici strumenti di programmazione modulare messi a disposizione dal Turbo Pascal, il programmatore stesso può creare i suoi personali sottoprogrammi ed eventualmente formare con questi nuove librerie.

 

 

Funzioni

 

Le funzioni sono sottoprogrammi che a partire da uno o più valori presi in ingresso restituiscono un valore al programma chiamante. Come indicato in figura 8.1, una funzione può essere pensata come una scatola nera che a determinati valori in ingresso fa corrispondere, in modo univoco, un determinato valore in uscita.

Per esempio abs, una funzione predefinita del Pascal che abbiamo già più volte utilizzato, applicata a un numero i ne restituisce il valore assoluto, come ci mostra la figura 8.2.

Come programmatori siamo del tutto disinteressati alla struttura interna di una funzione predefinita: ci interessa solo sapere che cosa passarle in ingresso e che cosa ci restituisce in uscita. Se però vogliamo noi stessi creare una nostra specifica funzione dobbiamo anche occuparci di come questa possa svolgere il compito affidatole. Consideriamo l'esempio del listato 8.1, dove è definita la funzione cubo.

Quando il programma va in esecuzione, il controllo passa alla prima istruzione successiva al begin del corpo del programma, che nell'esempio è la write che richiede l'inserimento di un numero. La readln successiva

 

 

 

Funzione

 
 


valori              =>

in                   =>                                                                                valore in => in uscita

ingresso         =>

 

 

 

Figura 8.2

La funzione abs(i} come scatola nera che produce il valore i i

 

 

memorizza il valore immesso dall'utente nella variabile a. Supponiamo che l'utente inserisca il valore 5.23:

Inserisci   il   numero:    5.23

L'istruzione successiva invoca la funzione cubo

b   :=   cubo(a);

II meccanismo adottato è lo stesso visto per le funzioni predefinite. Per esempio, nel programma del listato 2.7 per il calcolo della distanza di due punti, avevamo scritto:

lunghezza := abs(segmento);

al fine di invocare abs, una funzione presente nelle librerie di sistema del Pascal. Questa volta chiamiamo cubo una funzione realizzata dal

 

 

 

 

 

Casella di testo: program calcola_cubo;
var
a, b: real;
{ Funzione che calcola il cubo }
function cubo(x: real): real; 
begin
  cubo := x*x*x; 
end;
{ Corpo del programma principale }
begin
  write ( 'Inserisci un numero :  ' ) ; 
  readln(a);
  b := cubo(a);
 { Chiamata della funzione cubo }
  writeln(a:7:2, ' elevato al cubo = ', b:7:2);
  readln;
end.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

programmatore stesso, la cui dichiarazione è presente nella parte dichiarativa del programma. L'intestazione della funzione che abbiamo creato

 

function   cubo(x:    real):    real;

 

specifica che cubo è il nome di una funzione che restituisce al programma chiamante un valore di tipo real, quello indicato dopo i due punti. Definisce inoltre, fra parentesi tonde, il numero, il tipo e il nome dei parametri della funzione, cioè le variabili su cui agisce. Nel nostro esempio è presente un solo parametro, il cui tipo è real e il cui nome è x. Dopo l'intestazione inizia il corpo della funzione, anch'esso racchiuso tra le parole chiave begin-end. Nel momento in cui il programma chiamante invoca la funzione

b   :=   cubo(a);

passa a cubo il valore contenuto nella variabile a che la funzione riceve nel suo parametro x.

Successivamente vanno in esecuzione le istruzioni che compongono il

sottoprogramma:

 

begin

cubo   :=   x*x*x;

end;

 

II compito svolto da cubo è molto semplice: il valore passato nel parametro x è moltiplicato per se stesso tre volte e il risultato è restituito al programma chiamante.

La restituzione al chiamante avviene mediante un assegnamento a cubo, il nome della funzione stessa. Il chiamante riprende il controllo, assegna il valore calcolato a b ed esegue l'istruzione successiva, visualizzando il risultato.

Nel caso dell'esempio:

 

5.23   elevato   al   cubo   =   143.06

 

La dichiarazione della funzione che comprende l'intestazione e il corpo vanno ad aggiungersi alla parte dichiarativa del programma e precedono quindi il corpo del programma stesso.

Con questo semplice esempio abbiamo messo in luce diversi aspetti riguardanti l'utilizzazione delle funzioni:

          l'invocazione:

b    :=   cubo(a) ;

          il passaggio dei parametri con il chiamante;

          la dichiarazione:

         il ritorno dì un valore:

 

cubo    :=   x*x*x;

 

Passiamo ora a considerare in dettaglio ciascuno dei punti esemplificati.

 

 Dichiarazione di funzioni

 

La dichiarazione di una funzione segue la sintassi:

 

function nome_funzione

[(lista_parametri_formali)]  : tipo;

< parte dichiarativa >

begin

. . .

end;

 

La struttura di una funzione segue sostanzialmente quella di un programma: prima viene la parte dichiarativa dove possono essere dichiarati, come vedremo negli esempi successivi, type, var, const ecc. Segue poi il corpo della funzione, che contiene le istruzioni che la compongono.

L'intestazione differisce da quella di un programma: inizia con la parola chiave function seguita dal nome della funzione e, opzionalmente, dalle parentesi tonde con la lista dei parametri formali. Nel caso questi manchino, la funzione non accetta parametri; tipo è il tipo del valore di ritorno della funzione.

NOTA INFORMATIVA

Come in molti altri testi di informatica, anche in questo utilizziamo convenzionalmente nella sintassi parentesi quadre e acute. Le prime indicano le parti opzionali di comandi e dichiarazioni. Per esempio, nel caso dell'intestazione delle funzioni le parentesi quadre racchiudono la lista (opzionale) dei parametri formali compresi tra parentesi tonde. Nel terzo capitolo le avevamo già utilizzate per indicare la parte opzionale else dell'istruzione if.

Le parentesi acute indicano invece le parti di codice (comandi, condizioni, blocchi, funzioni o procedure ecc.) che, per brevità o maggior chiarezza, vengono espresse con frasi in linguaggio naturale (per noi in italiano) omettendo di scriverne per esteso la codifica Pascal. È comunque ovvio che per avere un programma eseguibile nel listato occorrerà sostituire al contenuto delle eventuali parentesi acute il corrispondente, effettivo codice Pascal.

 

L'intestazione stabilisce quindi il nome della funzione, i valori in ingresso su cui agisce, detti parametri formali, e il tipo del valore di ritorno della funzione stessa; il corpo della funzione è invece formato da un blocco di istruzioni.

 

 

 

 

 

 

 

La lista dei parametri formali è così costituita:

 

parametro!: tipol; parametro2: tipo2;...parametroN: tipoN

 

Nel nostro esempio era presente il solo parametro x di tipo real. Nel blocco della funzione deve comparire almeno un'istruzione di assegnazione di un valore al nome della funzione. L'invocazione dì una funzione è definita dalla sintassi:

 

nome   funzione     [ (lista   parametri   attuali) ];

 

i parametri attuali sono separati da virgole:

 

parametrol,    parametro2, parametroN

 

Nell'esempio di cubo l'unico parametro attuale era a. Nel prossimo paragrafo ci soffermeremo sui parametri formali e attuali. Rispetto al Pascal standard il Turbo Pascal si differenzia in quanto le dichiarazioni di funzioni, costanti, variabili e tipi (anche quelle che vedremo nei prossimi capitoli) non devono seguire rigidamente alcun ordine e possono ripetersi più volte.

Per ì nomi delle funzioni valgono le consuete regole in uso per gli identificatori.

 

Passaggio dei parametri

 

Abbiamo distinto fra due tipi di parametri: i parametri formali ed i parametri attuali. I parametri formali sono quelli il cui tipo, numero complessivo e relativo ordine sono stati dichiarati nella definizione della funzione. I parametri attuali sono quelli che vengono passati alla funzione all'atto della chiamata.

Quando la funzione viene invocata ogni parametro formale, per il quale viene predisposto lo spazio necessario in memoria, è inizializzato con il valore del corrispondente parametro attuale. Questa modalità è detta passaggio di parametri per valore. Deve esistere dunque una coerenza di tipo e di numero tra parametri formali e parametri attuali. Nell'esempio esaminato, al momento in cui viene richiamata la funzione cubo(a) .viene allocato uno spazio di memoria per il parametro formale x e gli viene assegnato il valore del parametro attuale a, come in figura 8.3. La funzione opera sui parametri formali e quando termina la sua esecuzione tali parametri vengono perduti, liberando la memoria corrispondente. Nella figura 8.3 viene evidenziato lo spazio dati del programma, che comprende la variabile a, la quale esisterà fino al termine dell'esecuzione del programma, e lo spazio dati della funzione cubo che comprende il parametro formale x, che esisterà finché la funzione non termina la sua esecuzione.

Vediamo un ulteriore esempio di programma che per il calcolo della potenza. Abbiamo già osservato che il blocco istruzioni di una funzione ha la stessa struttura del blocco begin-end del programma principale; vi possono dunque comparire ulteriori chiamate ad altre funzioni, come nel listato

 

 

 

Casella di testo: {   Calcolo  della  potenza  di   un   numero   reale L'esponente può  essere   O,   1,   2,   3,   4 o 5     }
program   calcola_potenza;
var
base, esponente : integer; 
ptnz: real;
{ Funzioni per il calcolo delle potenze }
function quad(x: real): real;
begin
   quad := x*x; 
end;
function cubo(x: real): real; 
begin
  cubo := x*x*x; end;
function  quar(x:    real): 
real; 
begin
quar    : =   x*x*x*x ;
end;
function quin(x: real): real;
begin
quin := x*x*x*x*x; 
end;

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

L'esecuzione del programma del listato 8.2 inizia con la richiesta all'utente della base e dell'esponente, poi viene chiamata la funzione potè passandole come parametri attuali base ed esponente.

 

ptnz    :=   potè(base,    esponente);

 

La funzione pote riceve in ingresso i valori corrispondenti nei parametri formali b ed e. Se l'utente ha immesso i valori 5 e 2, rispettivamente per la base e per l'esponente, si ha la situazione di figura 8.4. Sottolineiamo ancora il passaggio di parametri per valore: il valore dei

 

Figura 8.5

Passaggio di

parametri Ira

la funzione

chiamante

potè eia

funzione

quad

Casella di testo: { Funzione per selezionare la funzione corrispondente all'esponente
function pote(b: real; e: integer): real; 
begin
  case e of
   0	pote	= 1;
   1	pote	= b;
   2	pote	= quad{b) ;
   3	pote	= cubo(b) ;
   4	pote	= quar(b) ;
   5	pote	= quin(b)
  else pote := -1
  end		
end;		
{ Corpo del programma principale }
begin
  write(' Inserire base ' ) ; 
  readln(base);
  write('Inserire esponente (0-5)  ' ) ; 
  readln(esponente);
  ptnz := pote(base, esponente);
  if ptnz = -1 then
  writeln{'Potenza non prevista' )
 else
  writeln ('La potenza ' , esponente,  ' di ' , base,
' è ' , ptnz) ; 
  readln; 
end.
 

 

 

 

L'esecuzione del programma del listato 8.2 inizia con la richiesta all'utente della base e dell'esponente, poi viene chiamata la funzione potè passandole come parametri attuali base ed esponente.

ptnz    :=   pote(base,    esponente);

 

La funzione pote riceve in ingresso i valori corrispondenti nei parametri formali b ed e. Se l'utente ha immesso i valori 5 e 2, rispettivamente per la base e per l'esponente, si ha la situazione di figura 8.4. Sottolineiamo ancora il passaggio di parametri per valore: il valore dei

 

 

parametri attuali base ed esponente viene copiato nei parametri formali b ed e.

La funzione valuta il valore dell'esponente e dopodiché effettua una delle seguenti azioni: restituisce il valore 1 per esponente 0, la base stessa per esponente 1, invoca la funzione quad per esponente 2, cubo per esponente 3, quar per esponente 4, quìn per esponente 5 o restituisce -1 per segnalare la non disponibilità della potenza richiesta. Se l'esponente è 2, viene invocata la funzione quad e le viene passato dalla funzione pote il parametro attuale b:

 

2 :   pote   : =   quad(b) ;

 

quad lo riceve in ingresso nel parametro formale x (vedi figura 8.5), La funzione quad calcola il quadrato di x e restituisce il risultato a pote, la quale a sua volta lo restituisce al programma principale che l'aveva invocata. Tutti questi passaggi di valore sono illustrati in figura 8.6. Va poi in esecuzione la successiva istruzione del programma principale che gestisce tramite un if il ritorno del valore negativo -1, usato per segnalare la non disponibilità della potenza richiesta. Dunque una funzione può invocare un'altra funzione; perché ciò sìa possibile è però necessario che la funzione chiamata sìa dichiarata prima di quella chiamante. Il programma del listato 8.2 sarebbe stato scorretto se una delle funzioni per il calcolo della potenza fosse stata dichiarata dopo la funzione pote che può invocarla,

 

 

 

 

 

Casella di testo:                                        5           2
	function potè (b: real; e :integer)
begin		
   case	e of	
   0:	potè	i;
   1:	potè	b;       5
   2:	potè  =	quad (b) ;
   3:	potè=	cubo	30 (b) ;
   4:	potè=	quar(b);
   5:	potè =	quin(b)
   else  potè :=	-1
end;
end. 25	
Casella di testo:                          5
function quad(x: real;):real
 begin
   quad := x*x;
 end;	
                       

                                  

 

 

 

 


Casella di testo: Begin   (base=5, esponente=2)
…..
ptnz:=pote(base,esponente);
…..
end.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 


8.5  Valore di ritorno

A ogni funzione è associato un tipo che caratterizza un valore. Questo valore è detto valore di ritorno della funzione ed è restituito dalla funzione al programma chiamante per mezzo di un assegnamento alla variabile identificata dal nome della funzione stessa.

Per esempio, nel programma calcolo_cubo del listato 8.1, la funzione cubo

 

function   cubo(x:    real):    real; begin

cubo    :=   x*x*x;

end;

 

restituisce il controllo al programma chiamante e gli ritorna il cubo di x per mezzo dell'istruzione:

 

cubo   :=   x*x*x;

 

All'interno del blocco istruzioni di una funzione si possono avere più istruzioni che ritornano valori al chiamante.

Per esempio, nel programma calcolo_potenza del listato 8.2, nella funzione potè

 

 

 

 

 

 

 

function potè (b: real; e: integer): real;

begin

case   e   of

0    potè = 1;

1    potè = b;

2    potè = quad(b) ;

3    potè = cubo (b) ;

4    potè - quar (b) ;

5    potè = quin(b)

e:   .se  poi ;e := -1

end     

end;

                       

a ogni scelta del costrutto case corrisponde una uscita e la restituzione di un valore diverso.

 

Invocazione

 

Una funzione viene invocata facendo riferimento al suo nome e passandole una lista di parametri attuali coerente in tipo, numero e ordine con la lista dei parametri formali presenti nella definizione della funzione stessa.

Analizziamo per esempio il programma del listato 8.3. Il contenuto delle variabili b e h di tipo real e p di tipo char viene passato alla funzione area per mezzo dell'istruzione:

 

a   :=   area(b,    h,    p);

 

Le variabili b, h e p sono detti parametri attuali poiché contengono gli specifici valori di ingresso che permettono alla funzione di calcolare l'area del poligono dì quella particolare chiamata.

Ribadiamo come tipo, numero e ordine dei parametri debbano essere coerenti con quelli stabiliti dalla definizione dei parametri formali della funzione. Al posto di una variabile si può comunque passare una costante dello stesso tipo, per esempio:

 

a   :=   area(b,   h,    'T');

 

dove T viene immessa nel parametro formale poi igono. Non sono invece corrette le seguenti invocazioni, dove x è una variabile integer:

a   :=   area('T',   b,   h);

"   "       f'i\ '      <

a   :=   area(b,   h) ;

a   :=   area(b,   h,    'T',   x) ;

a   :=   area (b,   h,   x);

II passaggio dei parametri è infatti errato: nel primo esempio per l'ordine, nel secondo e nel terzo per il numero, nell'ultimo per la discordanza di tipo (real invece di char).

 

 

Casella di testo: { Calcola 1'area di un rettangolo o di un triangolo a scelta dell'utente	}
program calcola_area;
{ base e altezza }
{ area	}
{ tipo di poligono }
var
b, h: real;
 a: real; 
p: char;
{ Funzione per il calcolo dell'area }
function area(base, altezza: real; poligono: char) :real ;
begin
  case poligono of
      'T':     area := base*altezza / 2.0; 
      'R':     area := base*altezza; end end;
{ Corpo del programma principale }
begin 
    repeat
    write{'Inserire tipo di poligono (Triangolo/Rettangolo)  :  ' ) ; 
    readln(p);
until (p='T'  or (p='R’);
  writeln; write{'Inserire base  :  ' ) ;
   readln(b);
  writeln; write('Inserire altezza  : ' ) ; 
   readln(h);
  a := area(b, h, p) ;  { Invocazione della funzione area }
  writeln('II poligono (b=',b:7:2,' h=',h:7:2,') ha area ' ,a : 7 : 2); 
  readln; 
end.
 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Notiamo come per mezzo dell'invocazione di funzione si evidenzi il concetto di scatola nera: il programma chiamante, per poter utilizzare una funzione, deve conoscerne soltanto il nome e l'interfaccia (tipo, ordine e numero dei parametri formali), trascurando i dettagli implementativi. Le funzioni offrono anche un altro vantaggio: una volta definite possono essere invocate quante volte si desidera senza produrre duplicazione di codice. In pratica a n chiamate, con n > 1, corrisponde sempre una sola definizione. Se nel programma calcola_area vogliamo determinare sia l'arca del rettangolo che quella del triangolo, nel programma principale non dobbiamo richiedere all'utente la scelta del poligono ma, immessi i valori della base b e dell'altezza h, possiamo scrivere direttamente:

tri   :=   area(   b,   h,    'T'); ret   :=  area(   b,   h,    'R'];

dove tri e ret sono due variabili di tipo real.

La fruizione area è chiamata due volte dal programma principale, per calcolare l'area del triangolo e del rettangolo entrambi di base b e altezza h. Ripetiamo, come abbiamo già detto nel capitolo 2 a proposito delle funzioni predefìnite, che una funzione può essere invocata dovunque sia lecito inserire un valore dello stesso tipo della funzione, dunque anche all'interno di un'espressione. Quindi

y   :=   a*cubo(x)    +   b*quad(x)    +   c*x   +   d; assegna a y il valore del polinomio or' + bx2 + ex + d.

 

Procedure

 

 

Le procedure, la cui intestazione inizia con la parola chiaveprocedure, al contrario delle funzioni non restituiscono alcun valore. Un tipico esempio di uso di procedure è la visualizzazione di un messaggio o, più in generale, la produzione di un'uscita su un dispositivo periferico. Un esempio è stampa_bin del listato 8.4.

Una possibile esecuzione del programma è la seguente:

 

Inserisci un intero positivo: 13

La sua rappresentazione binaria è: 1101

Vuoi continuare? (s/n): s

Inserisci un intero positivo: 64

La sua rappresentazione binaria è : 1000000

Vuoi continuare? (s/n): n

 

La procedura stampa_bin divide ripetutamente per 2 il numero in base 10 v e memorizza i resti delle divisioni intere nel vettore a che poi legge a ritroso per visualizzare l'equivalente binano di v. Come il lettore avrà osservato, stampa_bin non restituisce alcun valore al chiamante; la sua invocazione avviene citando il suo nome seguito da parentesi tonde contenenti i parametri attuali e dal punto e virgola.

 

 

Casella di testo: Calcolo della rappresentazione binaria
{ Conversione di valori numerici dalla rappresentazione decimale a quella binaria }
program converti_in_binario;
const
  DIM_INT = 16;
var
resp : char ;
num: integer;
{ Procedura di conversione decimale/binario e stampa } 
procedure stampa_bin (v: integer);
type
cifre_binarie = O . . 1 ;
var
 i, j: integer;
 a: array[1..DIM_INT] of cifre_binarie;
begin
   if v = 0 then
    writeln(v) else  begin i := 1;
    while v <> 0 do 
         begin 
         a [i] := v mod 2; v := v div 2; i :=    i+1;
         end;
    for j := i-1 downto 1 do
      write(a[j]) ; 
    end; 
    end;
{Corpo del programma principale}
begin
  resp : = ' s ' ;
   while resp = 's' do 
     begin
     write ('Inserisci un intero positivo:  '); 
     readln(num);
     writeln('La sua rappresentazione binaria a: '); 
     stampa_bin(num) ;   { Chiamata procedura stampa_bin }
     writeln;
     write{'Vuoi continuare? (s/n): '); 
     readln(resp); 
     end; 
end.
 

Un altro esempio di procedura in cui non vengono impiegati parametri è

quello di mess_err del listato 8.5.

La procedura me ss_err ha il solo compito di visualizzare un messaggio

di errore nel caso di inserimento di un denominatore nullo. mess_err

non accetta parametri e non restituisce alcun valore,

Nella dichiarazione il nome di una funzione o di una procedura senza

parametri deve essere seguito direttamente da un punto e virgola.

 

Listata 8.5 Esempio di procedura che non accetta in ingresso alcun parametro

Casella di testo: (Effettua  una  divisione   intera,
visualizza un messaggio di  errore  se  il divisore è nullo}
program    controllo_errore;
var
a,   b,   c:   integer;
procedure mess_err; var
i: integer;
c: char;
 
begin
ERRORE ! DENOMINATORE NULLO') ;
writeln{'
writeln;
writeln('Premere Invio per continuare'); end;
 
');
 
begin
write('Inserire dividendo :') ;
readln (a) ;
writeln;
write ( 'Inserire divisore:  ' ) ;
readln(b);
if b <> 0 then begin
c := a div b;
;
writeln(a, ' diviso ', b, ' = ' , c) 
end 
else
mess_err;
 readln;
 end.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Visibilità

 

Una dichiarazione associa un identificatore (nome di variabile, costante, funzione o procedura) a un determinato ambito di validità, detto scope. In altre parole ciò significa che un nome può essere usato o, come sì usa dire, è visibile soltanto in una specìfica parte del testo del programma. Un nome dichiarato in una funzione o procedura, detto nome locale, ha una visibilità che si estende per tutto il sottoprogramma in cui esso è contenuto.

Un nome definito al di fuori di una funzione o procedura, detto nome globale, ha una visibilità che si estende dal punto di dichiarazione alla fine del programma.

Nel listato 8.4 i, j e a sono variabili locali alla procedura stampa_bin e possono essere utilizzate soltanto al suo interno, Se un'istruzione del corpo del programma principale o di un qualsiasi altro sottoprogramma

vi facesse riferimento si genererebbe un errore. Le variabili resp e num

sono al contrario variabili globali, a cui si può fare riferimento all'interno

di tutto il programma, ad esempio anche nella procedura stampa bin.

All'interno di un sottoprogramma può essere dichiarato un nome identico

a uno già dichiarato precedentemente al di fuori del sottoprogramma stes

so, come nel listato 8.6.           '

La prima variabile x è globale, cioè la sua visibilità si estende dalla sua dichiarazione fino alla fine del programma, mentre la seconda x è locale a p2 e la terza è locale api. Dunque verificate e giustificate la sequenza visualizzata dal programma:

Casella di testo: Esempio di visibilità dei nomi
program visibile ; var
x: integer;
procedure p2; var
x; integer; begin
x := 1;
writeln(x); end;
procedure pi; var
x: integer; 
begin
x := 2;
p2;
writeln(x); end;
{Corpo del programma principale} begin
 x := 3;
p1;
writeln(x); 
readln; 
end.

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

 

Nel caso in esame la dichiarazione di un nome nasconde o, come si dice, maschera la dichiarazione di quello stesso nome fatta precedentemente. Un nome ridefìnito nasconde quindi il precedente significato di quel nome; significato che verrà ripreso all'uscita dal sottoprogramma di appartenenza. Quando si apre un blocco begin-end è possibile effettuare dichiarazioni locali a quel blocco e non visibili ai blocchi superiori

 

if a > b then begin var

end;

e: integer;

 

 

Anche in questo caso si possono avere mascheramenti di nomi dichiarati precedentemente.

È comune che in un programma di grandi dimensioni sì abbia il mascheramento dei nomi e non è infrequente il caso in cui il programmatore non si accorga di aver mascherato un nome all'interno di un blocco. È allora consigliabile identificare le variabili globali con dei nomi caratteristici e come tali univoci; usare nomi per variabili globali del tipo i, j oppure x significa andare inevitabilmente incontro a mascheramenti spesso indesiderati.

Anche i parametri formali di una funzione hanno un campo di visibilità che sì estende dall'inizio alla fine del blocco istruzioni del sottoprogramma, sono quindi considerate, a tutti gli effetti, variabili locali al sottoprogramma:

function g(y: integer; z: char): integer;

var

k, 1: integer

begin

end;

i

Le variabili y e z sono locali alla funzione g e hanno una visibilità che si estende a tutta la funzione. Quindi la definizione di y e z precede la definizione delle altre variabili locali k e 1. Per questo motivo:

 

function f (x: integer): integer;

var

x: integer;        {GENERA UN ERRORE!)

begin

end;

 

è errata, poiché in essa si tenta di definire due volte la variabile locale x nello stesso sottoprogramma.

Oltre al passaggio esplicito di parametri è possibile anche il passaggio implicito. Infatti basta definire una variabile globale, visibile sia alla funzione chiamante che a quella chiamata, per ottenere la condivisione della variabile stessa. Nell'esempio del listato 8.4 si poteva dichiarare una variabile globale v di tipo integer, nella quale il programma principale avrebbe inserito il valore in base 10 e sul quale la procedura stampa_bin avrebbe lavorato direttamente. In questo caso la variabile num non sarebbe stata necessaria e la procedura non avrebbe accettato esplicitamente alcun parametro in ingresso, per cui la sua invocazione sarebbe stata:

stampa_bin;

La struttura di un sottoprogramma è equivalente a quella di un programma: perciò al suo interno possono essere dichiarati altri sottoprogrammi. Per quanto detto, quei sottoprogrammi sono locali al sottoprogramma in

 cui sono dichiarati e dunque non possono essere invocati (non sono visìbili) fuori da questo. Il ragionamento può essere ripetuto, generando così una gerarchia di sottoprogrammi.

Per esempio, nel listato 8.2 le funzioni per il calcolo delle potenze (quad, cubo, quar, quin) potrebbero essere dichiarate all'interno di potè in quanto non vengono utilizzate al di fuori di questa. Provate a effettuare questa modifica e verifìcate la correttezza del ragionamento fatto.

 

8.9  Gestione di una sequenza

 

Consideriamo il problema di far gestire all'utente una o più sequenze di interi attraverso un menu:

 

GESTIONE SEQUENZA

1.   Immissione

2.   Ricerca completa

3.   Ricerca binaria

4.   Visualizzazione

0.   Fine

Scegliere   una   opzione:

 

Le opzioni possono essere scelte un numero di volte qualsiasi finché non si seleziona lo zero che fa terminare il programma. Ovviamente all'inizio occorre scegliere la prima opzione per immettere la sequenza, successivamente questa possibilità può essere nuovamente usata per lavorare su altre sequenze.

Nel listato 8.7 vi proponiamo il programma completo; dato che gli algoritmi relativi sono stati studiati nei capitoli precedenti, ci soffermiamo adesso soltanto sull'uso dei sottoprogrammi e sul passaggio dei parametri.

Listato 8.7   Gestione di una sequenza (prima versione)

{ Gestione di una sequenza di interi memorizzata in un array. Sono permesse le operazioni di immissione sequenza, ricerca sequenziale, ricerca binaria e visualizzazione. Dopo 1'immissione si ha un ordinamento automatico. }

program sequenza;

uses

 crt;

const

MAX ELE = 1000;

type

s = array[l.,MAX_ELE] of integer;

var

vet: s;  { array contenente gli elementi della sequenza }

{ Sottoprogramma di ordinamento del vettore contenente

gli elementi della sequenza di lunghezza n }

procedure ordinamento(n: integer);

var

i, p: integer;

K : boolean; aux: integer;

begin p := n; repeat

p := p + 1; k := false;

for i := 1 to n-1 do

if vet[i] > vet[i+l] then begin

aux := vet[i]; vet[i] := vet[i+l]; vet[i+l] := aux; k := true;

p := i+1;

end;

n := p;

until (k O true);

end;

{ Sottoprogramma di accettazione degli elementi

della sequenza immessi dall'utente    }

function immissione : integer;

var

i, n: integer;

invio : char;

begin repeat

writeln;

write('Numero elementi :  ' ) ;

readln(n);

until (n >= 1) and (n <= MAX_ELE) ;

writeln;

for i := 1 to n do begin

writeln;

write{'Immettere un intero n.', i, ' : ' ) ;

readln(vet[i]); end;

ordinamento(n);    { chiamata procedura di ordinamento } immissione := n;   { restituzione lunghezza sequenza } end;

{ Sottoprogramma di ricerca sequenziale di eie all'interno di una sequenza di lunghezza n    }

function ricerca(n, ele : integer): integer;

var

i : integer;

begin

i := 1;

while (eie O vet[i]) and (i < n) do i := i+1;

ricerca := i ; end;

{ Sottoprogramma di ricerca binaria di eie ali'interno di una sequenza di lunghezza n }

function ric_bin(n, eie: integer): integer;

var

i, alto, basso, pos: integer;

begin

alto := 1; basso := n; pos := -1;

repeat

i := (alto + basso) div 2;

if vet[i] = eie then pos := i

else ìf vet[i] < eie then alto := i+1

else basso := i-1; until (alto > basso) or (pos o -1);

ric_bin := pos;

end;

{ Sottoprogramma di visualizzazione della sequenza di lunghezza n     }

procedure visualizzazione(n: integer);

var

i: integer;

begin

writeln;

for i : =1 to n do

writeln(vet[i]:15); writeln; writeln;

write('Premere Invio per continuare...');

readln;

end;

{ Sottoprogramma di gestione delle scelte effettuate dall'utente al fine di gestire una sequenza        }

procedure gestione_sequenza;

var

n: integer;

scelta : integer;

eie, posizione : integer;

begin clrscr; n := 0;

scelta := -1;

GESTIONE   SEQUENZA'); Immissione'); Ricerca   completa'); Ricerca binaria'); Visualizzazione'); Fine');

while   scelta O   O   do begin clrscr;

writeln;   writeln('

writeln;   writeln('    1.

writeln;   writeln('    2 .

writeln;   writeln{'    3 .

writeln;   writeln{'    4 .

writeln;   writeln{'    0.

writeln;    wri teln;

write('  Scegliere  una   opzione:

readln(scelta);

clrscr;

case scelta of

1    : n := immissione;

2    : begin

write('Elemento da ricercare: ');

readln(ele);

posizione := ricerca(n, ele);

if ele = vet[posizione] then begin

writeln;

writeln('Elemento ' , ele,

' presente in posizione ' , posizione) end else begin

writeln; writeln{'Elemento non presente!');

end;

writeln; writeln;

write('Premere Invio per continuare...');

readln;

end;

3    : begin

write('Elemento da ricercare : ');

readln(ele);

posizione := ric_bin(n, ele); if posizione O -1 then begin writeln; writeln('Elemento ', ele,

1 presente in posizione ' , posizione); end else begin

writeln;

writeln('Elemento non presente!');

end;

writeln; writeln;

write('Premere Invio per continuare... ' ) ;

readln;

end;

4: visualizzazione(n);

end;

end;

end;

begin

gestione_sequenza;

end.

 

 

In primo luogo, dopo l'intestazione del programma, definiamo il tipo s: type   s:    array[l..MAX_ELE]    of   integer;

poi

tutto il programma:

i dichiariamo l'array vet di tipo s che è quindi una variabile globale a tto il Droeramma:

var   vet:    s;

In questo modo tutte le funzioni del programma vi possono accedere direttamente. Nei paragrafi successivi, grazie al passaggio di variabile per indirizzo, vedremo una soluzione migliore.

Veniamo adesso al programma principale. Decidiamo di far svolgere il compito di visualizzare il menu e gestire le scelte dell'utente al sottoprogramma gestione_sequenza; dunque esso non dovrà restituire alcun valore e non accetterà alcun parametro: sarà quindi una procedura

procedure    gestione_sequenza;

che verrà invocata dal programma sequenza con l'istruzione: gestione_sequenza;

Viene naturale far corrispondere alle opzioni una funzione che svolga il compito stabilito. Nel caso venga selezionata la prima opzione, viene immesso il valore 1 nella variabile intera scelta e per mezzo del costrutto case viene mandata in esecuzione la funzione immissione:

1 :    n    :=   immissione;

che ritorna a gestione_sequenza il numero dei valori immessi in modo che possa essere reso noto agli altri sottoprogrammi; tale valore viene memorizzato nella variabile n. Se verificate la dichiarazione della funzione immissione vedrete che questa ritorna effettivamente un intero; osserverete inoltre che non accetta alcun parametro, mancando completamente nella sua intestazione la parte fra parentesi tonde. La funzione immissione, dopo aver memorizzato in vet gli elementi della sequenza immessi dall'utente, chiama a sua volta la procedura ordinamento, passandole come parametro attuale n, il numero di elementi della sequenza. Anche la procedura

ordinamento(n);

agisce sulla variabile generale vet ordinando i suoi elementi. Quando la procedura ha terminato il suo compito, il controllo dell'esecuzione passa di nuovo alla funzione immissione che a sua volta restituisce il controllo a gestione_sequenza, passandole prima il numero di elementi che formano la sequenza:

immissione   :=   n;

La procedura gestione_sequenza visualizza nuovamente il menu; nel caso venga selezionata la seconda opzione, scelta assume il valore 2 e viene eseguita la funzione ricerca a cui deve essere passato, oltre

ma, definiamo il tipo s:

integer; di una variabile globale a

a vi possono accedere di-lassaggio di variabile per

idiamo di far svolgere il e dell'utente al sottopro-on dovrà restituire alcun ndi una procedura

:on l'istruzione:

ia funzione che svolga il a prima opzione, viene .ta e per mezzo del co-nzione immissione:

alla lunghezza della sequenza, anche il valore dell'elemento da ricercare precedentemente richiesto all'utente:

posizione    :=   ricerca(n, ele);

La funzione restituisce un valore intero, che corrisponde alla posizione dove è stato reperito l'elemento. Considerazioni analoghe valgono per la funzione di ricerca binaria r i c_bi n che viene eseguita selezionando l'opzione 3.1 due sottoprogrammi di ricerca pervengono ovviamente allo stesso risultato, anche se in generale la ricerca binaria è più rapida. Nel caso venga selezionata l'opzione 4, va in esecuzione la procedura visualizza, che mostra gli elementi della sequenza. Notiamo che la procedura ordinamento potrebbe venire dichiarata all'interno di immissione in quanto viene invocata solamente da tale funzione. Per ragioni analoghe tutti i sottoprogrammi potrebbero venire dichiarati all'interno di gestione_sequenza. In questo caso i sottoprogrammi sarebbero visibili soltanto all'interno di gestione sequenza: non sarebbe dunque possibile, per esempio, chiamare direttamente dal programma principale la funzione ricerca o la procedura ordinamento. Come utile esercizio vi lasciamo infine il compito di modificare la funzione che effettua la ricerca in modo sequenziale, al fine di migliorarne l'efficienza, considerando che nel caso in esame gli elementi sono ordinati (vedi anche l'esercizio 6.13).

 

  Scomposizione funzionale

 

Vediamo ora alcuni criteri euristici per la progettazione di sottoprogrammi. Il primo criterio riguarda la scelta del nome. Il nome di una funzione o di una procedura deve esprimere in sintesi il suo compito. Saranno allora considerati opportuni nomi del tipo:

 

 

calcola_media

acquisisci_valore

converti_stringa

 ordina_dati

ricerca max

 

 

 

.oi elementi. Quando la o dell'esecuzione passa 'olla restituisce il con-a il numero di elemen-

nuovamente il menu; sita assume il valore e essere passato, oltre

 

mentre si sconsigliano nomi del tipo

x_139    {   oscuro   }

gestore { troppo generico }

trota { umoristico ma poco significativo }

 

La scelta del nome è anche un test sulla bontà del sottoprogramma. Se non si riesce a trovare un nome adatto a descrìvere sinteticamente il suo compito è probabile che il sottoprogramma faccia troppe cose, e quindi potrebbe essere ulteriormente scomposto, oppure non abbia un preciso compito, nel qual caso potrebbe valere la pena essere eliminato! Una volta scelto il nome di un sottoprogramma gli sì devono fornire degli

ingressi che possono essere passati esplicitamente per valore o implicitamente per mezzo di variabili globali. In generale è sconsigliato, anche per problemi di leggibilità, avere una lista di parametri esageratamente nutrita. È buona regola della programmazione strutturata che i soli parametri passati a un sottoprogramma siano quelli esplicitamente menzionati fra i parametri formali. Il passaggio implicito di parametri attraverso variabili globali è quindi in generale sconsigliato; ciò nonostante, quando la lista dei parametri tende a diventare esageratamente lunga si preferisce a volte questa seconda alternativa. Il lettore è comunque invitato a non abusare delle variabili globali: laddove sia possibile è buona norma evitarle. Si osservi comunque che parametri attuali e variabili locali hanno una vita temporanea: vengono cioè creati all'atto dell'esecuzione del sottoprogramma e rimossi all'atto del ritorno al chiamante. La presenza di molti parametri implica quindi un alto consumo di memoria e di tempo di elaborazione. In particolare il passaggio per valore di parametri dì tipo array può risultare molto pesante. Se infatti il compilatore dovesse passare una variabile del tipo:

vet:    array    [1..1000]    of   integer;

occorrerebbe la quantità di tempo non trascurabile necessaria per effettuare il travaso di valori tra due array di 1000 interi. L'ultima osservazione riguarda le uscite di un sottoprogramma. Abbiamo visto che una funzione restituisce al più un solo valore. Come ci si deve comportare quando la funzione deve restituire più di un valore? Esistono due soluzioni:

          usare variabili globali;

          rendere note al sottoprogramma le locazioni, o indirizzi, in cui andare

a depositare le uscite.

Nel prossimo paragrafo tratteremo questo secondo, importante tipo di soluzione.

 

Passaggio di parametri per indirizzo

 

Questa modalità prevede che venga passato l'indirizzo della variabile (array o altro tipo) al sottoprogramma, cioè che gli venga resa nota la locazione della variabile in memoria. In questo modo le istruzioni all'interno di una funzione o di una procedura possono modificare direttamente il contenuto della variabile di cui è stato passato l'indirizzo. Questo meccanismo è noto con il nome di passaggio dei parametri per indirizzo o riferimento.

Consideriamo, per esempio, nel listato 8.8 la semplice procedura s cambia, che ha l'effetto di scambiare il valore dei suoi parametri. La chiamata di questa procedura non produce alcun effetto sui parametri attuali:

scambia(a   b) ; non ha effetto sulle variabili intere a e b. Infatti i valori di a e b sono

 

 

 

Listato 8.8 Scambia valori tra due variabili: procedura sbagliata

program  ko ; var

a,   b:    integer;

t Versione KO di scambia } procedure scambia(x, y: integer);

var

temp : integer;

begin

temp := x;

x := y;

y := temp; end;

begin a := 8; b := 16;

writeln('Prima dello scambio'); writelnfa = ' , a, '  b = ' , b) ;

scambia(a, b) ;

writeln('Dopo   lo   scambio'}; irritali! ('a  =    ' ,   a,    '      b  =   ' ,   b) ;

readln;

end.

 

copiati nei parametri formali x e y, quindi vengono scambiati i valori dei parametri formali e non i valori originali di a e b!

Affinchè scambia abbia qualche effetto deve essere modificata in modo da ricevere non i valori ma gli indirizzi delle variabili, come mostrato nel listato 8.9 e in figura 8.7. Solo in questo modo lo scambio fra x e y ha effetto su a e b poiché all'interno della procedura x e y sono ora degli alias, cioè pseudonimi per a e b.

La dichiarazione var x, y usata nella definizione dei parametri formali determina x e y come variabili passate per indirizzo o riferimento. L'invocazione della procedura scambia non deve essere modificata:

scambiata,    b};

La medesima strategia di passaggio per indirizzo di una variabile si può sfruttare con gli array, come vedremo nell'esempio del prossimo paragrafo.

 

Scambia valori tra due variabili con passaggio di parametri per riferimento

program   ok; var

a,   b:    integer;

{ Versione OK di scambia }

procedure scambia(var x, y: integer); var

temp : integer; begin

temp := x;

x := y;

y := temp; end;

begin a := 8; b := 16;

writeln('Prima dello scambio'); writelnfa = ' , a, '  b = ' , b) ;

scambia(a, b) ;

writeln('Dopo lo scambio'); writeln('a = ', a, '  b = ' , b) ; readln;

end.

 

 

 Parametro array

 

Abbiamo già esaminato in questo capitolo un programma per la gestione di una sequenza che proponeva un menu con le opzioni di immissione, ordinamento, ricerca completa e ricerca binaria. In quella sede il vettore che conteneva la sequenza era una variabile globale a cui tutte le funzioni accedevano direttamente.

Presentiamo adesso le modifiche necessarie perché il tutto avvenga mediante un vettore locale alla funzione gestione_sequenza e tramite il passaggio del suo indirizzo agli altri sottoprogrammi. Dunque non verrà dichiarata la variabile globale array vet ma solamente il tipo corrispondente:

type

s   =   array[1..MAX_ELE]    of   integer;

La procedura gestione_sequenza includerà la definizione dì vet come variabile locale:

procedure gestione_sequenza; var

vet: s;

Sempre all'interno di gestione_sequenza, al momento della chiamata dei sottoprogrammi, tale array deve essere passato come parametro attuale:

1 : n := immissione(vet) ;

2: posizione := ricerca(vet, n, eie);

3: posizione   :=   ric_ bin(vet,    n,   ele);

5:    visualizzazione(vet,    n);

 

La funzione immissione passerà a sua volta il vettore alla procedura ordinamento:

 

ordinamento{vet,    n) ;

 

Nella definizione delle procedure e delle funzioni deve poi essere esplicitato, nella posizione corrispondente, un nuovo parametro formale di tipo var:

procedure   ordinamento(var   vet:    s;   n :    integer); function   immissione (var   vet :    s) :    integer; function  ricerca   (var  vet:   s;   n,   eie:   integer):   integer; function  ric_bin(var  vet:   s;   n,   eie:   integer):   integer; procedure  visualizzazione(var vet:   s;   n:   integer);

Sorprendentemente, all'interno di ogni funzione, non cambia niente: infatti vet adesso è una variabile passata per riferimento mentre nella versione precedente era una variabile globale. La differenza sta in una maggiore chiarezza: infatti quando vogliamo che un sottoprogramma agisca su quel determinato array dobbiamo passarglielo esplicitamente, mentre nella versione precedente qualsiasi funzione o procedura poteva accedervi.

Inoltre potremo utilizzare gli stessi sottoprogrammi per agire parallelamente su più sequenze; è infatti sufficiente invocarli passandogli un diverso array ed eventualmente una diversa lunghezza. Abbiamo guadagnato enormemente in flessibilità: i nostri sottoprogrammi sono riutilizzabili in più occasioni ed è chiaro su cosa agiscano, non essendovi implicate variabili globali.

 

 Cattura di valori da tastiera

 

Nel programma sequenza (listato 8.7) abbiamo utilizzato, per permettere all'utente di effettuare la sua scelta, l'istruzione:

readln(scelta);

dove scelta è una variabile di tipo char. Tale modo di operare è corretto ed è consentito in tutte le versioni del Pascal: l'utente deve prima premere il tasto corrispondente all'opzione desiderata e successivamente premere INVIO.

Per fare in modo che questa seconda azione non sia necessaria, il Turbo Pascal mette a disposizione del programmatore la funzione predefinita readkey, contenuta nel modulo crt, che legge direttamente il carattere immesso dall'utente:

 

scelta    :=   readkey;

 

Sempre nel Turbo Pascal è anche disponibile l'istruzione: repeat   until   keypressed;

che interrompe l'esecuzione del programma finché l'utente non preme un tasto qualsiasi.