7.1 Introduzione.
Nel capitolo precedente abbiamo dovuto riscrivere, durante la stesura di algoritmi, interi blocchi di istruzioni per svolgere operazioni identiche su variabili diverse. Ad esempio, nell'algoritmo relativo al problema 12, la maggior parte delle istruzioni serve per risolvere il problema della lettura dei due vettori, mentre per la risoluzione del problema del confronto sono utilizzate solo 5 righe di codice. Lo stesso dicasi per quasi tutti i problemi visti nel capitolo 6. Riscrivere parecchio codice che differisce solo per le variabili su cui si opera, oltre ad essere noioso e poco produttivo per il programmatore, spesso rende il programma finale meno chiaro. Le istruzioni da ripetere potrebbero essere anche molte centinaia, rendendo l'algoritmo illeggibile. Se avessimo la possibilità di definire a piacimento nuove istruzioni ecco che il problema sarebbe risolto.
Esempio: sommare tre vettori di numeri.
Definiamo la somma di tre vettori, con eguale numero di componenti, come un vettore che ha per componenti la somma delle componenti dei tre vettori. Formalizzando avremo:
RISULTATO: DATI: LIMITI: |
r = vettore risultato a, b, c = tre vettori i tre vettori devono avere lo stesso numero di componenti |
ALGORITMO:
BEGIN
leggi vettore a
leggi vettore b
leggi vettore c
somma a+b+c in r
scrivi r
END;
Supponiamo di avere a disposizione nuove istruzioni, come ad esempio una istruzione di lettura che ci restituisca un vettore con associato il numero di elementi effettivamente introdotto. Allora "leggi a", "leggi b", "leggi c" diventerà:
READ_VET( a, n_a );
READ_VET( b, n_b );
READ_VET( c, n_c );
dove con n_a, n_b, n_c si intende il numero di elementi del vettore a, del vettore b e del vettore c.
Similmente somma_vet potrebbe essere un'istruzione che avuto in ingresso i tre vettori da sommare, restituisce in un quarto vettore la somma.
SOMMA_VET( a, b, c, n_a, n_b, n_c, r, n_r )
dove
- a, b, c sono i tre vettori di cui si richiede la somma
- n_a, n_b, n_c sono le quantità di elementi dei tre vettori
- r è il vettore contenente il risultato della somma
- n_r è la quantità di elementi del vettore somma
Anche "scrivi ris" potrebbe diventare:
WRITE_VET( r, n_r )
Il nostro algoritmo diventerà:
BEGIN
READ_VET( a, n_a );
READ_VET( b, n_b );
READ_VET( c, n_c );
SOMMA_VET( a, b, c, n_a, n_b, n_c, r, n_r );
WRITE_VET( r, n_r );
END.
Come si può osservare dall'esempio, l'introduzione di nuove istruzioni presenta numerosi vantaggi e precisamente:
Il linguaggio Pascal fornisce due costrutti per la realizzazione di nuove istruzioni: le procedure e le function.
7.2 Procedure.
7.2.1 Dichiarazione di procedure.
Una procedura è un'unità di programma indipendente. Ha una propria dichiarazione di tipi, costanti e variabili ed eventualmente di altre procedure o funzioni. Come esempio vediamo la dichiarazione di una procedura per la lettura di un vettore.
PROCEDURE read_vet( VAR x:vettore; VAR n_x:INTEGER );
VAR
i:INTEGER;
BEGIN
REPEAT
WRITE( 'Numero elementi = ' );
READLN( n_x );
UNTIL ( n_x >=1 ) AND ( n_x<=dim );
FOR i:=1 TO n_x DO BEGIN
WRITELN( 'Posizione = ', i );
READLN( x[i] );
END;
END;
Nella dichiarazione di una procedura si possono individuare due parti: il titolo della procedura, composto dal nome e da eventuali variabili, e il corpo, composto dalla sequenza di istruzioni che devono essere svolte racchiuse tra begin/end.
Il nome della procedura compare subito dopo la parola riservata procedure e può essere seguito da una lista di variabili con associato il tipo. La lista, se compare, è racchiusa tra due parentesi rotonde.
Nell'esempio visto prima il nome della nuova istruzione è read_vet e la lista delle variabili è composta da due elementi, x e n_x, seguiti dal tipo corrispondente. Queste variabili vengono chiamate parametri formali.
Subito dopo possono comparire la dichiarazione di nuovi tipi, nuove variabili o anche nuove procedure o funzioni. Nell'esempio compare solo la dichiarazione di una variabile, i, preceduta dalla parola riservata var.
Il corpo della procedura descrive le operazioni che debbono essere compiute sui parametri in ingresso quando la nuova istruzione viene eseguita. Possono essere utilizzate tutte le istruzione del Pascal: il corpo è racchiuso da begin/end;.
7.2.2 Chiamata di procedure.
Come già osservato, lo scopo di una procedura è descrivere le istruzioni che debbono essere eseguite per operare su dei dati in ingresso per ottenere dei risultati. E` importante capire che si tratta di descrizione. Non vengono eseguite azioni sino a quando la nuova istruzione non viene usata nel corso del programma. Per esempio, per richiedere l'esecuzione della procedura read_vet bisogna scrivere così:
read_vet( a, n_a );
Questa chiamata è corretta se
La chiamata di una procedura è corretta se nella chiamata vengono indicate tante variabili quanti i parametri formali della procedura e nello stesso ordine di tipo. In pratica i parametri formali vengono inizializzati con variabili indicate nella chiamata dette parametri attuali. L'operazione avviene in base alla posizione occupata.
Riepilogando possiamo dire che con la definizione di procedura si descrive un'operazione complessa che deve essere svolta su dei dati di un determinato tipo. Solo al momento della chiamata vengono definiti i dati sui quali la procedura dovrà operare. I dati dovranno essere dello stesso tipo dei parametri formali e nello stesso numero.
Esempio.
Sia:
PROCEDURE write_ris( x:vettore; n_x:INTEGER );
VAR
i:INTEGER;
BEGIN
WRITELN( 'IL RISULTATO E` : ' );
FOR i:=1 TO n_x DO
WRITELN( 'Componente ', i, ' Risultato ', ris );
END;
la dichiarazione di un'istruzione che stampa un vettore di numeri.
Per utilizzare tale procedura si scriverà:
write_ris( r, n_r );
La chiamata sarà corretta se e solo se sarà
r : vettore;
n_r : INTEGER;
Dopo la chiamata il parametro formale x sarà inizializzato con il contenuto di r mentre il parametro formale n_x sarà inizializzato con il contenuto di n_r.
Il programma completo per il problema presentato in precedenza sarà:
PROGRAM completo( INPUT, OUTPUT );
CONST
dim=200;
TYPE
vettore=ARRAY[1..dim] OF REAL;
VAR
a,b,c, { vettori di input }
r :vettore; { vettore risultato }
n_a, n_b, n_c,
n_r :INTEGER; { numero componenti dei vettori }
PROCEDURE read_vet( VAR x:vettore; VAR n_x:INTEGER );
VAR
i:INTEGER;
BEGIN
REPEAT
WRITE( 'Numero elementi = ' );
READLN( n_x );
UNTIL ( n_x >=1 ) AND ( n_x<=dim );
FOR i:=1 TO n_x DO BEGIN
WRITELN( 'Posizione = ', i );
READLN( x[i] );
END;
END;
PROCEDURE write_ris( x:vettore; n_x:INTEGER );
VAR
i:INTEGER;
BEGIN
WRITELN( 'IL RISULTATO E` : ' );
FOR i:=1 TO n_x DO
WRITELN( 'Componente ', i, ' Risultato ', ris );
END;
PROCEDURE somma_vet( a, b, c :vettore; VAR r :vettore;
n_a,n_b,n_c:INTEGER; VAR n_r:INTEGER );
VAR
i:INTEGER;
BEGIN
n_r:=0;
IF ( n_a=n_b ) AND ( n_b=n_c ) THEN BEGIN
n_r:=n_a;
FOR i:=1 TO n_r DO
r[i]:=a[i]+b[i]+c[i];
END
END;
BEGIN { main program }
READ_VET( a, n_a );
READ_VET( b, n_b );
READ_VET( c, n_c );
SOMMA_VET( a, b, c, n_a, n_b, n_c, r, n_r );
WRITE_VET( r, n_r );
END.
7.3 Function.
Nei capitoli precedenti abbiamo avuto a che fare spesso con blocchi di istruzioni che operavano su dati per dare in risposta un valore: in matematica quando si verifica una situazione del genere si parla di funzione. Ad esempio nel problema di ricerca del massimo di un vettore si può pensare ad una funzione che, operando su un vettore di numeri, restituisce come risultato un valore numerico, il massimo.
Esempio: ricerca del massimo di un vettore
RISULTATO: DATI: |
massimo = massimo valore v = vettore di numeri |
ALGORITMO:
BEGIN
leggi( v );
scrivi( massimo(v) );
END;
massimo è una funzione che ha come dominio v e che restituisce il valore massimo.
7.3.1 Dichiarazione di function.
Per le function valgono le osservazioni viste in precedenza per le procedure. In questo paragrafo sottolineeremo le differenze.
Esempio: ricerca del massimo di un vettore di REAL con alla posizione 0 la dimensione ( dimensione > 0).
FUNCTION massimo( v:vettore ):REAL;
VAR
i, n : INTEGER;
max : REAL;
BEGIN
max:=v[1];
n:=trunc(v[0]);
FOR i:=2 TO n DO
IF v[i] > max THEN max:=v[i];
massimo:=max;
END;
In questo esempio si possono osservare alcune differenze con quanto visto per le procedure:
massimo:=max;
Per il resto valgono tutte le osservazioni fatte con le procedure.
7.3.2 Chiamata di function.
Così come per le procedure, anche le function descrivono un'operazione che avviene su dati generici avuti in ingresso e che però fornisce in output un risultato scalare. L'operazione vera e propria avviene solo al momento della chiamata. Per esempio, utilizzando la function massimo possiamo chiamarla così:
m:=massimo( v );
Per quanto riguarda l'associazione tra parametri formali e attuali valgono le stese considerazioni viste con le procedure. Il meccanismo di chiamata è però diverso. Mentre con le procedure si può pensare a nuove istruzioni, con le function il paragone corretto è con le funzioni matematiche per cui si può pensare di utilizzare direttamente il valore che si ottiene in risposta. Ad esempio poichè il problema richiede la stampa del massimo avremmo potuto scrivere il programma così:
BEGIN
leggi( v );
WRITELN('Il massimo è ', massimo(v) );
END.
La function viene attivata utilizzandola in un'espressione esattamente come se fosse una variabile o una costante. Poichè inoltre ogni function ha associato un tipo, l'espressione in cui compare deve essere compatibile con il tipo dichiarato.
Il programma completo per il problema della stampa del massimo sarà:
PROGRAM stampa_massimo( INPUT, OUTPUT );
CONST
dim=100;
TYPE
vettore=ARRAY[0..dim] OF REAL;
VAR
v:vettore;
PROCEDURE leggi( VAR x:vettore );
VAR
i, n : INTEGER;
BEGIN
REPEAT
WRITE('Quanti elementi ? `);
READLN( n );
UNTIL ( n > 0 ) AND ( n < dim );
v[0]:=n;
FOR i:=1 TO n DO BEGIN
WRITE('Elemento ',i );
READLN(v[I]);
END;
END;
FUNCTION massimo( v:vettore ):REAL;
VAR
i, n : INTEGER;
max : REAL;
BEGIN
max:=v[1];
n:=trunc(v[0]);
FOR i:=2 TO n DO
IF v[i] > max THEN max:=v[i];
massimo:=max;
END;
BEGIN
leggi( v )
WRITELN('Il massimo è ', massimo( v ) );
END.
7.4 Parametri.
Come abbiamo già osservato parecchie volte nei paragrafi precedenti, le procedure e le function, cioè i sottoprogrammi, possono essere pensati come veri e propri programmi. A loro sono permesse tutte le operazioni che sono definite in Pascal, possono cioè fare input/output, dichiarare variabili, tipi, costanti, possono dichiarare nuovi sottoprogrammi, possono infine utilizzare sottoprogrammi dichiarati precedentemente. Oltre a queste operazioni comuni esiste il meccanismo dei parametri che abbiamo descritto brevemente in precedenza. Vediamo allora in dettaglio come avviene il passaggio dei parametri tra il programma chiamante e il sottoprogramma.
Negli esempi proposti nei paragrafi precedenti abbiamo notato due modi diversi per indicare le variabili nella lista dei parametri. Ad esempio nella dichiarazione della procedure read_vet abbiamo scritto:
PROCEDURE read_vet( VAR x:vettore; VAR n_x:INTEGER );
mentre nella dichiarazione della procedure write_ris abbiamo scritto:
PROCEDURE write_ris( x:vettore; n_x:INTEGER );
Come si può osservare nella prima dichiarazione abbiamo utilizzato la parola riservata VAR davanti ad ogni variabile definita nella lista dei parametri, mentre nella seconda non compare VAR. Sempre in precedenza abbiamo utilizzato anche una procedure così dichiarata:
PROCEDURE somma_vet( a, b, c :vettore; VAR r :vettore; n_a, n_b, n_c :INTEGER; VAR n_r:INTEGER);
dove si può notare che nella lista dei parametri formali compaiono variabili con VAR e variabili senza VAR.
Si parla di passaggio per valore quando non si usa VAR, si parla di passaggio per riferimento quando compare VAR.
La differenza tra i due meccanismi è la seguente:
Esempio 1.
PROCEDURE uno( x:INTEGER );
BEGIN
x:=x+1;
WRITELN( x )
END;
BEGIN
a:=1;
uno( a );
WRITELN( a );
END.
Il risultato sarà
2
1
Il parametro attuale al momento della chiamata conterrà il valore 1. Viene creata una copia che verrà utilizzata dalla procedura che incrementerà il valore. La writeln nella procedure stamperà il valore 2. Poichè la procedure ha operato su una copia, la variabile a nel propramma principale non risulterà modificata. La writeln del programma principale stamperà 1
Esempio 2.
PROCEDURE uno( VAR x:INTEGER );
BEGIN
x:=x+1;
WRITELN( x )
END;
BEGIN
a:=1;
uno( a );
WRITELN( a );
END.
Il risultato sarà
2
2
Il parametro formale, al momento della chiamata, verrà sostituito con il parametro attuale. Tutte le operazioni compiute nella procedure sulla variabile x in realtà saranno effettuate sulla variabile a.
Esempio 3.
PROCEDURE uno( x:INTEGER );
BEGIN
x:=x+1;
WRITELN( x )
END;
BEGIN
a:=1;
uno( a+2 );
WRITELN( a );
END.
Il risultato sarà
4
1
Il parametro formale x sarà inizializzato con il risultato dell'espressione a + 2.
Esempio 4.
PROCEDURE uno( VAR x:INTEGER );
BEGIN
x:=x+1;
WRITELN( x )
END;
BEGIN
a:=1;
uno( a+2 );
WRITELN( a );
END.
La chiamata alla procedure uno è scorretta. a+2 è un'espressione e non una variabile.
Esempio 5: riprendiamo il problema 7 svolto nel capitolo precedente: dato un vettore v e un numero x stampare la prima occorrenza di x in v.
RISULTATI: DATI: |
posizione di x x = numero da ricercare v = vettore di numeri |
STRUTTURA DATI:
CONST
dim=100;
TYPE
numeri=ARRAY[0..dim] OF REAL;
VAR
v :numeri;
x :REAL;
trovato :BOOLEAN;
ALGORITMO:
BEGIN
leggi( v ); { lettura vettore; alla posizione 0 dimensione }
WRITE( 'Inserisci elemento da ricercare ' );
READLN( x );
scrivi( ricerca( v, x), x); { stampa il risultato della ricerca }
END.
Sviluppando nei dettagli:
PROCEDURE leggi( VAR x:numeri );
VAR
i, n :INTEGER;
BEGIN
REPEAT
WRITE( 'Numero elementi = ' );
READLN( n );
UNTIL ( n >=1 ) AND ( n<dim );
v[0]:=n;
FOR i:=1 TO n DO BEGIN
WRITELN( 'Posizione = ', i );
READLN( x[i] );
END;
END;
FUNCTION ricerca( v:numeri; x:REAL ) :INTEGER;
VAR
i, n :INTEGER;
BEGIN
i:=0; n:=trunc( v[0] ); { inizializzazione di i, n }
v[n+1]:=x; { inserimento sentinella }
REPEAT
i:=i+1
UNTIL v[i]=x;
IF i=n+1 THEN ricerca:=0 { se i=n+1 allora non trovato }
ELSE ricerca:=i;
END;
se p <> 0 allora trovato
se p = 0 allora non trovato
PROCEDURE scrivi( p, x :INTEGER );
BEGIN
IF p=0 THEN WRITELN( 'Numero ', x, ' non presente' )
ELSE WRITELN( 'Numero ', x, ' trovato alla posizione ', p );
END;
L'inizializzazione di p è il risultato della function ricerca.
Osservazione: in questo paragrafo abbiamo visto con precisione il passaggio di variabili per riferimento e per valore, indicando quando utilizzare l'uno o l'altro. Esiste un'eccezione a quanto detto ed è il caso di passaggio di una grossa quantità di dati, per esempio di un vettore. In questo caso, anche se occorresse un passaggio per valore si preferisce utilizzare un passaggio per riferimento, sia per ragioni di velocità sia di spazio. Infatti fare una copia di un vettore molto grande provoca un grosso spreco di spazio e di tempo.
Esercizi.
7.5 Locale e globale.
Abbiamo osservato che ogni procedure o function può avere definite al proprio interno costanti, tipi, variabili o addirittura altre procedure o function. Tutti questi oggetti esistono all'interno del sottoprogramma in esame, rappresentano cioè l'ambiente di lavoro. Solo le istruzioni di quel determinato sottoprogramma hanno accesso a tali oggetti. Nell'ambiente esterno non esistono. Questi oggetti sono detti locali. Però l'ambiente di lavoro di un sottoprogramma non è costituito solo dagli oggetti locali ma anche da tutti quegli oggetti, comprese procedure e function definite nel sottoprogramma in cui è inserito il sottoprogramma in esame. Se gli oggetti sono definiti a livello di programma principale allora vengono detti globali e sono a disposizione di qualunque sottoprogramma. Sottolineiamo inoltre che le variabili definite in un sottoprogramma perdono il loro contenuto tra una chiamata e l'altra.
Esempio.
PROGRAM caos( INPUT, OUTPUT );
VAR
a0 :REAL;
b0 :INTEGER;
c0 :CHAR;
PROCEDURE livello1;
VAR
a1 :REAL;
b1 :INTEGER;
c1 :CHAR;
FUNCTION livello2_a :REAL;
VAR
a2_a :REAL;
b2_a :INTEGER;
c2_a :CHAR;
BEGIN { codice per livello2_a }
....
....
END;
PROCEDURE livello2_b;
VAR
a2_b :REAL;
b2_b :INTEGER;
c2_b :CHAR;
BEGIN { codice per livello2_b }
....
....
....
END;
BEGIN { codice per livello 1 }
....
....
END;
BEGIN { programma principale }
....
....
....
END.
Le variabili a0, b0, c0 sono definite nel programma principale, sono globali e fanno parte dell'ambiente di tutti i sottoprogrammi.
Le variabili a1, b1, c1 sono definite in livello1, fanno parte dell'ambiente di livello1 e di quello di livello2_a e livello2_b.
Le variabili definite in livello2_a fanno parte solo dell'ambiente di livello2_a così come quelle definite in livello2_b appartengono all'ambiente di livello2_b.
Si potrebbe verificare un conflitto di nome quando una variabile locale ha lo stesso nome di una variabile definita nell'ambiente superiore. In questo caso il Pascal applica la regola che l'ultima definita è quella accessibile.
Esempio.
PROGRAM caos1( INPUT, OUTPUT );
VAR
a, b : REAL;
PROCEDURE uno;
VAR
a1, b : CHAR;
BEGIN { codice per uno }
....
....
END;
BEGIN { programma principale }
....
....
END.
Tra le variabili locali di uno compare la variabile b che ha lo stesso nome della variabile b definita a livello globale. La variabile globale b non appartiene allora all'ambiente di uno.
Prima di chiudere il paragrafo sottolineiamo due aspetti molto importanti di quanto abbiamo detto:
Esempi.
1. Verificare il funzionamento dei seguenti programmi:
PROGRAM a(INPUT, OUTPUT); VAR x :INTEGER; PROCEDURE uno; BEGIN x:=x+1 END; BEGIN x:=1; uno END. PROGRAM c; VAR x:INTEGER; PROCEDURE tre(VAR y:INTEGER); BEGIN y:=y+1; x:=x+1 END; BEGIN x:=1; tre( x ) END. |
PROGRAM b( INPUT, OUTPUT); VAR x :INTEGER; PROCEDURE due( x:CHAR ); BEGIN WRITE(x) END; BEGIN x:=1; due('a') END. PROGRAM d; VAR x:INTEGER; PROCEDURE quattro(VAR y:INTEGER); VAR x:REAL; BEGIN y:=y+1; x:=x+1 END; BEGIN x:=1; quattro( x ) END. |
In a la procedura uno utilizza la variabile globale x incrementandola. Alla fine x varrà due.
In b due utilizza il parametro formale x che è di tipo diverso dalla variabile globale x. Nella procedura due non è possibile fare riferimento alla variabile globale x. La stampa di x in due produrrà a.
In c dopo la chiamata di tre la variabile x conterrà tre. Un uso simile delle variabili è vivamente sconsigliato.
In d la variabile x conterrà due dopo la chiamata di quattro.
Esercizi.
Rispondere alle seguenti domande:
BEGIN
x:=x+1;
WRITELN( x )
END;
se si richiama la procedura esempio
a:=1;
esempio( a );
WRITELN( a );
l'output sarà:
1 2 |
1 1 |
2 2 |
2 1 |
BEGIN
x:=y
END;
se si richiama la procedura scambio
a:=1; b:=3;
scambio( a, b );
WRITELN( a:2, b:2 );
l'output sarà:
1 3 |
3 1 |
3 3 |
1 1 |
VAR
z :INTEGER;
BEGIN
z:=x; x:=y; y:=z
END;
se si richiama la procedura inverti
a:=1; b:=3;
inverti( a, b );
WRITELN( a );
WRITELN( b );
l'output sara':
1 3 |
3 1 |
1 1 |
3 3 |
BEGIN
z:=a; a:=b; b:=z
END;
se si richiama la procedura inverti
z:=5; a:=1; b:=2;
inverti( b, a );
WRITELN( z, a, b );
l'output sarà:
1 1 2 |
5 1 2 |
2 2 1 |
5 2 1 |
VAR
i:INTEGER;
BEGIN
FOR i:=1 TO 100 DO
vet[i]:=i;
a:=vet[1]+vet[3];
b:=vet[5]
END;
se si richiama la procedura incremento
FOR i:=1 TO 100 DO
vet[i]:=0;
a:=1; b:=3;
incremento( a, b );
WRITELN( a, b, vet[7] );
l'output sarà:
1 3 0 |
1 3 7 |
4 5 0 |
4 3 7 |
VAR
vet :VETTORE;
i :INTEGER;
BEGIN
FOR i:=1 TO 100 DO
vet[i]:=x[i]+1
END;
se si richiama incremento
FOR i:=1 TO 100 DO
vet[i]:=i;
a:=vet[1]; b:=vet[3];
incremento( vet );
a:=vet[1]; b:=vet[3];
WRITELN( a, b );
l'output sarà:
1 3 |
2 4 |
0 0 |
Nessuna delle precedenti |
VAR
z:INTEGER;
BEGIN
z:=x;
WRITELN( z:2 );
x:=y; y:=z
END;
se si richiama la procedura inverto
z:=1; a:=2; b:=3;
inverto( a, b );
WRITELN( a:2, b:2 );
l'output sarà:
1 3 3 |
2 3 2 |
2 3 3 |
1 3 2 |
VAR
z,
i:INTEGER;
BEGIN
FOR i:=1 TO 100 DO BEGIN
z:=x[i]; x[i]:=y[i]; y[i]:=z
END
END;
se si richiama la procedura inverto
FOR i:=1 to 100 DO BEGIN
a[i]:=i; b[i]:=i+3
END;
inverto( a, b )
WRITELN( a[2], b[1] );
l'output sarà:
1 5 |
5 1 |
1 1 |
5 5 |
VAR
z:vettore;
i:INTEGER;
BEGIN
FOR i:=1 TO 100 DO
z[i]:=x[i];
FOR i:=1 TO 100 DO
x[i]:=y[i];
FOR i:=1 TO 100 DO
y[i]:=z[i]
END;
se si richiama la procedura inverto
FOR i:=1 TO 100 DO BEGIN
a[i]:=i;
b[i]:=i+3
END;
inverto( a, b );
WRITELN( a[1], b[1] );
l'output sarà:
1 4 4 1 |
1 1 |
4 4 |
VAR
z:INTEGER;
PROCEDURE scambio( VAR a, b:INTEGER );
BEGIN
z:=a; a:=b; b:=z
END;
BEGIN
z:=3;
scambio( a, b );
WRITELN( z );
END;
se si richiama la procedura esempio
z:=1; a:=5; b:=7;
esempio( a, b );
WRITELN( a:2, b:2 );
l'output sarà:
3 7 5 |
1 5 7 |
3 7 5 |
5 5 7 |
7.6 Gestione dell'errore.
Spesso un sottoprogramma deve effettuare controlli sulla correttezza dei dati in ingresso prima di svolgere il proprio compito. Se i dati sono corretti procede. Il programmatore deve comunque sapere se l'operazione richiesta è andata a buon fine e certamente non basta un messaggio.
Esempio 1: scrivere una funzione che calcoli la radice di un numero.
Abbiamo risolto questo problema nel capitolo 5. Riprendiamo l'algoritmo e costruiamo la function radice che avuto in ingresso un numero calcola la radice quadrata.
FUNCTION radice( v:REAL ) :REAL;
CONST
precisione = 1e-10;
VAR
x, y : REAL;
BEGIN
y:= 1;
REPEAT
x:= y;
y:= ( x * x + v ) / (2 * x)
UNTIL abs (x - y) < precisione;
radice:=y
END;
Il calcolo della radice quadrata può avvenire solo se il numero v è positivo. Scrivere però un semplice messaggio di errore non è sufficiente:
FUNCTION radice( v:REAL ) : REAL;
...
BEGIN
IF v<0 THEN WRITELN('Errore nei dati')
ELSE
...
...
END;
L'operatore riceverà il messaggio di errore ma il programma che ha richiesto questa operazione non potrà in alcun modo saperlo e continuerà come nulla fosse. Nel caso esaminato si può risolvere il problema con questa considerazione: poichè il risultato della funzione radice sarà sempre un numero positivo potremmo usare un numero negativo, ad esempio -1, per segnalare l'eventuale errore.
FUNCTION radice( v:REAL ) :REAL;
CONST
precisione = 1e-10;
VAR
x, y : REAL;
BEGIN
IF v<0 THEN radice:=-1
ELSE BEGIN
y:= 1;
REPEAT
x:= y;
y:= ( x * x + v ) / (2 * x)
UNTIL abs (x - y) < precisione;
radice:=y
END
END;
Il programma chiamante potrà controllare il risultato e se ottiene -1 saprà che è avvenuto un errore.
Non sempre è possibile utilizzare dei valori per segnalare l'errore. Vediamo il seguente esempio.
Esempio 2: dato un vettore v e un intero p scrivere un sottoprogramma che restituisca v[p].
Supponiamo che la struttura dati sia la seguente:
CONST
dim=100;
TYPE
vettore=ARRAY[0..dim] OF INTEGER;
VAR
vet:vettore;
ris:INTEGER;
v[0] conterrà la dimensione del vettore. Se p<1 OR p>v[0] allora errore.
FUNCTION valore( v:vettore; p:INTEGER; VAR r:INTEGER ):BOOLEAN;
VAR
ok :BOOLEAN;
BEGIN
ok:=(p>=1) AND (p<=v[0]);
IF ok THEN r:=v[p];
valore:=ok;
END;
La chiamata sarà:
IF valore( vet, 5, ris ) THEN
{ utilizza ris }
...
Questa soluzione del problema può diventare molto pesante per il programmatore. Supponiamo di avere diverse procedure che vengono richiamate sequenzialmente, ciascuna con la possibilità di non svolgere il compito previsto per qualche errore. Il programma principale sarà:
BEGIN
proc1( ..... );
IF NOT errore THEN BEGIN
proc2( ... );
IF NOT errore THEN BEGIN
proc3( ... );
IF NOT errore THEN BEGIN
proc4( ... );
IF errore THEN
messaggio;
END ELSE
messaggio
END ELSE
messaggio
END ELSE
messaggio
END.
Già con il richiamo di quattro procedure il programma diventa illeggibile e molto pesante per il programmatore con il proliferare di controlli e di messaggi. Si può allora definire una nuova procedura per la gestione dell'errore, richiamata ogni volta si presenterà un errore grave, che svolgerà tutti i compiti associati all'errore e interromperà l'esecuzione del programma. Ad esempio
PROCEDURE errore( s:messaggio );
BEGIN
WRITELN('ERRORE: ',s);
GOTO 999
END;
dove
- messaggio=PACKED ARRAY[1..50] OF CHAR;
- GOTO 999 è il salto alla label 999, assegnata all'ultima istruzione del programma. La label 999 deve essere dichiarata in testa al programma, insieme alla variabili, tipi e costanti utilizzando la parola chiave LABEL seguita dall'elenco.
Esempio 3: risolvere il problema proposto nell'esempio 2.
FUNCTION valore( v:vettore; p:INTEGER ):INTEGER;
BEGIN
IF (p<1) OR (p>v[0]) THEN errore('ERRORE: fuori intervallo');
valore:=v[p];
END;
Il programma principale dovrà avere la seguente label
PROGRAM principale( INPUT, OUTPUT );
LABEL 999;
....
....
BEGIN { principale }
...
...
999: END.
OSSERVAZIONE: in alcune implementazioni del Pascal non è possibile eseguire in un sottoprogramma il GOTO ad una label definita fuori. In questo caso è sicuramente disponibile una istruzione HALT che, quando eseguita, blocca l'esecuzione del programma.
7.7 Ricorsione.
Uno strumento particolarmente potente in matematica è il principio di induzione: lo schema di induzione permette di definire funzioni aventi come dominio i naturali e come immagine un generico insieme:
f: N -> I
Lo schema di induzione è il seguente:
- si definisce la base di induzione, cioè il valore di f(0).
- si definisce la funzione per k+1, supponendo di averla già definita per k.
Esempio. Vogliamo dimostrare che 1+2+3+4+..+n = n * (n+1) /2
Per n = 1 si ha 1=1.
Supponiamo vero per n:
1+2+3+..n+n+1 = n * (n+1)/2 + (n+1) = (n^2 + 3n + 2) / 2 = (n+1)*(n+2)/2
Il principio di induzione è molto importante in programmazione dove però si preferisce parlare di definizioni ricorsive: una procedura, una funzione, un oggetto viene detto ricorsivo se esso comprende parzialmente se stesso. Una procedura viene detta direttamente ricorsiva se contiene un riferimento esplicito a se stessa; se contiene un riferimento ad una altra procedura che a sua volta richiama la prima allora è detta indirettamente ricorsiva.
Esempio 1: calcolare il fattoriale di n.
Il fattoriale di n è:
1 se n=0,
n * fattoriale di n-1 se n > 0.
Applicando la definizione avremo:
FUNCTION fatt( n:INTEGER ) :INTEGER;
BEGIN
IF n = 0 THEN fatt:=1
ELSE fatt:= n * fatt(n - 1)
END;
Esempio 2: calcolare n^m.
Soluzione 1.
Per definizione abbiamo che n^m è:
1 se m=0
n * n^(m-1) se m > 0
Applicando la definizone abbiamo:
FUNCTION exp( n:REAL; m:INTEGER ):REAL;
BEGIN
IF m = 0 THEN exp:=1
ELSE exp:= n * exp(n,m - 1)
END;
Soluzione 2.
Per definizione abbiamo che n^m è:
1 se m=0
(n^(m div 2))^2 se m è pari
n * n^(m-1) se m è dispari
Applicando la definizone abbiamo:
FUNCTION quickesp (x:REAL ; y:INTEGER):REAL;
FUNCTION square(x:REAL):REAL;
BEGIN
square:=x*x
END;
BEGIN
IF y=0 THEN quickesp:=1
ELSE
IF (y mod 2) = 0 THEN quickesp:=square (quickesp (x, y div 2))
ELSE quickesp:= x * quickesp(x, y-1)
END;
Esempio 3: calcolare la somma degli interi compresi tra x e y.
La somma degli interi tra x e y sarà:
0 se x> y
x + somma degli interi compresi tra x+1 e y
Allora avremo:
FUNCTION sommint(x:INTEGER; y:INTEGER):INTEGER;
BEGIN
IF x > y THEN sommint:= 0
ELSE sommint:= x + sommint(x+1,y)
END;
Esempio 4: controllare se un numero è pari o dispari.
Se consideriamo che
0 è pari
un numero m è pari se m-1 è dispari
un numero m è dispari se m-1 è pari
avremo che:
FUNCTION dispari(x:INTEGER):BOOLEAN; forward;
FUNCTION pari (x:INTEGER):BOOLEAN;
BEGIN
IF x = 0 THEN pari:=true
ELSE pari:= dispari(x-1)
END;
FUNCTION dispari (x:INTEGER):BOOLEAN;
BEGIN
IF x = 0 THEN dispari:= false
ELSE dispari:= pari(x-1)
END;
OSSERVAZIONE: il termine forward nella definizione di dispari significa che il corpo della funzione verrà definito successivamente.
Esempio 5: calcolare il m.c.d tra due numeri.
Il massimo comun divisore tra due numeri x e y si può così definire:
se y = 0 sarà x
altrimenti sarà uguale al m.c.d. tra y e il resto tra x diviso y
Allora avremo:
FUNCTION mcd(x:INTEGER; y:INTEGER):INTEGER;
BEGIN
IF y = 0 THEN mcd:=x
ELSE mcd:=mcd(y, x mod y)
END;
Esempio 6: calcolare il massimo tra gli elementi di un vettore.
Il massimo tra gli elementi di un vettore si può così definire:
se il vettore ha un solo elemento il massimo è l'elemento stesso
altrimento è il maggiore tra l'ultimo e il massimo dei precedenti.
In base a questa definizione avremo:
FUNCTION magvet(v:vettore; n:INTEGER):REAL;
FUNCTION mag(x:REAL;y:REAL):REAL;
BEGIN
IF x > y THEN
mag:=x
ELSE
mag:=y
END;
BEGIN
IF n=1 THEN
magvet:=v[1]
ELSE
magvet:= mag(v[n], magvet(v,n-1))
END;
dove
vettore=ARRAY[1..dim] OF REAL;
n è la dimensione di v:vettore
Esempio 7: calcolare la somma degli elementi di un vettore.
Se la dimensione del vettore = 0 la somma è 0
altrimenti sarà uguale a v[n] + somma dei precedenti.
FUNCTION somm(v:vettore; n:INTEGER):REAL;
BEGIN
IF n = 0 THEN somm:=0
ELSE somm:=v[n] + somm(v,n-1)
END;
Esercizi Usando funzioni ricorsive calcolare:
a- il prodotto degli elementi di un vettore.
b- il minimo di un vettore.
c- la media aritmetica degli elementi di un vettore.
d- il prodotto scalare di due vettori.
e- la rappresentazione ottale di un numero intero.
f- la somma degli elementi pari di un vettore di interi.
g- il prodotto degli elementi di posizione pari di un vettore.
h- il numero di elementi negativi di un vettore.
7.8 Scomposizione dell'algoritmo.
Dagli esempi fin qui esposti risulta chiaro che la costruzione di un programma è un processo complesso, che in generale non può essere affrontato globalmente. Si passa attraverso fasi di analisi seguite da momenti realizzativi. La soluzione finale non è mai unica, e spesso si è costretti a scegliere tra diverse possibilità. Alcune scelte fatte in precedenza potrebbero rivelarsi inopportune o addirittura errate. Allora è necessario ricominciare dal punto precedente rianalizzando la situazione. La ricerca dell'algoritmo risolutivo può mettere in evidenza errori compiuti durante la definizione del problema o durante la scelta delle strutture dati. Ed anche lo sviluppo dell'algoritmo non è mai una operazione lineare ma avviene in diversi tempi cercando di scomporre il problema in sottoproblemi più semplici. Ciascuna scomposizione deve verificare le seguenti condizioni:
1- la soluzione dei sottoproblemi deve condurre alla soluzione del problema generale
2- le azioni da eseguire per risolvere il sottoproblema devono essere possibili
3- la suddivisione deve dar luogo a sottoproblemi più semplici in base agli strumenti disponibili
Vediamole nei dettagli.
1- Non basta dire che un problema è stato scomposto per essere sicuri che si è più vicini alla soluzione. E` un'affermazione banale ma spesso si verifica che nella ricerca di un algoritmo risolutivo si scomponga il problema in sottoproblemi allontanandosi dalla soluzione. Ad esempio pensiamo al problema del massimo di un vettore. Una possibile scomposizione potrebbe essere:
ordina il vettore in ordine decrescente
il massimo è il primo elemento
Non ci si è assolutamente avvicinati alla soluzione anche perchè alcuni algoritmi di ordinamento prevedono la ricerca del massimo.
2- Anche questa affermazione è ovvia. Se scompongo un problema in sottoproblemi e per la soluzione di uno di questi debbono essere svolte azioni impossibili non abbiamo sicuramente migliorato le cose. Vediamo questo con un esempio:
In un grande cortile recintato ci sono alcuni bambini che giocano alla palla. L'accesso al cortile è permesso da una porta. Durante il gioco la palla finisce fuori dal recinto, vicino alla cancellata, dalla parte opposta rispetto alla porta. Bisogna reuperarla. Pensando di fare prima uno dei bambini risolve il problema in questo modo.
Corro verso la cancellata dove vedo la palla
Supero la cancellata
Recupero al palla e ritorno
La scomposizione del problema sembra perfetta. Ma se la cancellata non si può superare il bambino non riuscirà mai a raggiungere la palla.
3- Gli strumenti disponibili per la soluzione di un problema sono dati dal linguaggio di programmazione. Ma quello che può risultare semplice in un linguaggio potrebbe essere complicato in un altro. Oppure potrebbero esserci complicazioni facilmente risolvibili con altre scomposizioni. Anche affrontando problemi molto semplici ci si può rendere conto di questo. Ad esempio nel risolvere il problema proposto all'inizio del capitolo, dobbiamo affrontare la gestione della situazione di errore nella procedura di somma, poichè i vettori potrebbero essere di dimensione diversa. Ma con una piccola modifica nell'algoritmo iniziale si può semplificare notevolmente il problema.
BEGIN
leggi il numero n di componenti
leggi le n componenti di a
leggi le n componenti di b
leggi le n componenti di c
calcola la somma
scrivi il risultato
END.
che tradotto sarà
BEGIN
REPEAT
WRITE( 'Numero componenti vettori ' );
READLN( n );
UNTIL (n>0) AND (n<=dim);
new_leggi_vet( a, n );
new_leggi_vet( b, n );
new_leggi_vet( c, n );
new_somma_vet( a, b, c, s, n );
write_vet( r, n_r )
END.
PROCEDURE new_leggi_vet( VAR x:vettore; n:INTEGER );
VAR
i:INTEGER;
BEGIN
FOR i:=1 TO n DO BEGIN
WRITELN('Posizione = ',i);
READLN( x[i] );
END;
END;
PROCEDURE new_calcola_somma( a, b, c:vettore; VAR r:vettore; n:INTEGER );
VAR
i:INTEGER;
BEGIN
FOR i:=1 TO n DO
r[i]:=a[i]+b[i]+c[i]
END;
Il metodo di approccio allo sviluppo di algoritmi descritto è detto top-down. Ma si può seguire anche un metodo opposto che prevede di partire dal dettaglio costruendo programmi semplici, collegandoli tra loro per ottenere programmi più sofisticati sino ad ottenere il programma finale. Tale approccio è detto bottom-up. In genere nella costruzione di un nuovo algoritmo si utilizza un approccio top-down, mentre nell'adattare un algoritmo ad un nuovo problema si preferisce il metodo bottom-up. Questo significa anche che, nella prassi comune, poichè un nuovo problema una volta scomposto in sottoproblemi porta inevitabilmente ad avere analogie con problemi già risolti, il motodo di risoluzione sarà spesso inizialmente top-down ma scendendo nei dettagli diventerà bottom-up poichè si cercherà di adattare algoritmi "vecchi" alle nuove esigenze.