Linguaggio Pascal

 

per collaborazioni, commenti, critiche, e altro contattateci alla e-mail: clubinfo@libero.it risponderemo al più presto!

di Silvio Sorace


Quinta lezione

11    Sottoprogrammi

    11.1    Funzioni

    11.2    Procedure

    11.3    Esempi

12    Ricorsione


Pagina principale | Lezione precedente | Prossima lezione

11    Sottoprogrammi

L'uomo, nel suo normale modo di agire, dopo aver analizzato il problema in generale, individua i sottoproblemi e li affronta uno per volta, fino alla loro completa risoluzione.
Per seguire questo tipo di logica, si rende necessario uno strumento che, da un certo punto in poi, ci permetta di "sganciarci" dal contesto generale per sviluppare separatamente, e in modo il più possibile indipendente, i singoli nodi di difficoltà.

A tale scopo suddivideremo l'intero programma in:

  1. programma principale;

  2. sottoprogrammi.

A sua volta, il Pascal, permette di suddividere i sottoprogrammi in:

  1. funzioni (function);

  2. procedure (procedure).

Per quanto riguarda la struttura di questo corso, continueremo per ora a considerare il programma come l'insieme di programma principale + sottoprogrammi. In una fase successiva si procederà nell'analisi dl sottoprogramma specifico, considerandolo come un programma a sé stante.


11.1    Funzioni

In matematica, una funzione è una legge che, in base ai valori assunti da uno o più argomenti, fornisce uno ed un solo risultato (valore della funzione).
Ad esempio nella relazione:

    f(x) = 3x + 2

f è il nome della funzione e x è l'argomento. Notiamo che nella relazione, all'argomento x non è associato alcun valore specifico; x è quindi un parametro formale, usato per definire la regola per il calcolo della funzione. Per effettuare il calcolo di f, occorre pertanto associare a x un preciso valore (valore attuale).
Ad esempio assegnando a x il valore 3 si ottiene:

    f(3) = 11

Lo schema generale di un programma che prevede la definizione di funzioni è il seguente:

    program nome_programma;
    var ...
    ...
        function nome_funzione(param1, param2, ...): tipo_del_risultato;
        var...
        ...
        begin
            ...
            corpo della funzione
            ...
        end;
    begin
        ...
        corpo del programma contenente la chiamata della funzione
        ...
    end.

Analizziamo le varie parti:

La parte dichiarativa (all'interno della funzione), contiene la definizione di tutte le variabili usate all'interno della funzione, fatta eccezione per quelle già definite come parametri formali. Sulla cosiddetta visibilità (in inglese scope) torneremo dopo con alcuni esempi.

All'interno del corpo del programma principale, deve essere presente la chiamata della funzione, attraverso il suo nome seguito dalla lista dei parametri attuali, che devono essere nello stesso ordine (della dichiarazione) e dello stesso tipo.

torna sopra


11.2    Procedure

La procedura, così come la funzione, permette di isolare e di risolvere separatamente alcune parti del problema generale.

Una procedura viene invocata attraverso il suo nome, ma ad essa non viene associato alcun valore. Come nelle funzioni, il dialogo tra programma chiamante e procedura, avviene attraverso una lista di parametri.
Nel caso di procedure, la lista di parametri riveste un ruolo fondamentale, in quanto attraverso questa, è possibile lo scambio di più valori. Entriamo con maggiore dettaglio in questo meccanismo:

Come già detto, all'atto della chiamata della procedura, i parametri attuali prendono il posto dei parametri formali. Questa sostituzione può avvenire in due modi:

  1. per valore

  2. per riferimento

Nel passaggio per valore, il valore del parametro attuale (ad esempio x) viene passato al parametro formale (ad esempio y). La procedura quindi lavorando con y, non può in alcun modo modificare x.
Nel passaggio per riferimento, qualsiasi operazione effettuata su y si ripercuote su x, in quanto viene effettuato un legame tra i due parametri.

Ecco che, se vogliamo avere un output anche dalla procedura, basterà passare i parametri per riferimento.
Ma come si fa ad indicare questo diverso approccio in Pascal?

Per attivare il passaggio per riferimento, sarà sufficiente far precedere la lista dei parametri dalla parola chiave var.

    procedure nome_procedura(param1, param2, ...
                              var param10, param11,...  );

Lo schema generale di un programma che prevede la definizione di procedure è il seguente:

    program nome_programma;
    var ...
    ...
        procedure nome_procedura(param1, param2, ...
                              var param10, param11,...  );
        var...
        ...
        begin
            ...
            corpo della procedura
            ...
        end;
    begin
        ...
        corpo del programma contenente la chiamata della procedura
        ...
    end.

 

torna sopra


11.3    Esempi

Ecco un esempio di scope delle variabili:

program visibilita;
uses crt;
var x : integer;

    procedure nasconde;
    var x: integer;
    begin
        writeln('procedura "nasconde", prima dell"assegnamento x = ',x);
        x:= 10;
        writeln('procedura "nasconde", dopo l"assegnamento x = ',x);
    end;

    procedure non_nasconde;
    begin
        writeln('"non_nasconde", prima x = ',x);
        x:= 100;
        writeln('procedura"non_nasconde", dopo x = ',x);
    end;

begin
    clrscr;
    writeln('programma principale, prima x = ',x);
    x:= 1;
    writeln('programma principale, dopo x = ',x);
    nasconde;
    non_nasconde;
    repeat until keypressed;
end.

L'output di questo codice è del tipo:

programma principale,       prima x = 0
programma principale,       dopo x = 1
procedura "nasconde",       prima dell"assegnamento x = 23415
procedura "nasconde",       dopo l"assegnamento x = 10
procedura "non_nasconde",   prima dell"assegnamento x = 1
procedura "non_nasconde",   dopo l"assegnamento x = 100 

Il commento di questi risultati è piuttosto semplice:
La prima cosa da far notare è che la variabile x all'interno del programma principale e la x della procedura "non_nasconde" sono la stessa, contrariamente alla x della procedura "nasconde", che è proprio un'altra variabile.
Un'altra cosa da far notare è che i valori di x prima dell'assegnamento nel programma principale e nella procedura "nasconde", sono valori casuali che corrispondono al contenuto della locazione di memoria in cui sono state allocate le variabili. Proprio questo è il motivo dell'importanza di resettare le variabili prima del loro uso.

Osservazione: All'interno del nome della procedura non_nasconde è presente il carattere "_" (leggi underscore) perché nel nome (di variabile, di procedura, di funzione, di programma, ...) non possono essere presenti degli spazi.

torna sopra


12    Ricorsione

Una funzione al cui interno è presente una chiamata a se stessa è detta ricorsiva.
Un tipico esempio di funzione ricorsiva è quella usata per calcolare il fattoriale di un intero positivo:

	function fattoriale(n: integer): integer;
	begin
		if n = 1 then
			fattoriale := 1
		else
			fattoriale := n * fattoriale(n-1);
	end;

Il fattoriale di un numero n è il prodotto dei primi n numeri interi, ad esempio il fattoriale di 4 (che si indica con 4!) è: 1 * 2 * 3 * 4 = 24

Nella funzione fattoriale abbiamo due casi: il fattoriale di 1 è 1. Per gli altri numeri si sfrutta la proprietà che !n = n * (n-1)!

Attenzione: la condizione n = 1 si chiama "base di ricorsione" e rappresenta la condizione di uscita. Questa garantisce che il processo di ricorsione abbia una fine.

Come già visto nell'esempio della terza lezione (in cui abbiamo calcolato il fattoriale con il ciclo for), tutti i risultati ottenuti mediante funzioni ricorsive possono essere ottenuti mediante funzioni non ricorsive, che in genere sono molto più complesse. Il vantaggio di usare la ricorsione è che determinati problemi trovano in essa una soluzione elegante, chiara e semplice. Vedremo ad esempio per le liste, gli alberi e le strutture dinamiche in genere come questa affermazione sia veritiera. Tuttavia la ricorsività non è una manna. Una funzione ricorsiva è un'arma a doppio taglio. Se infatti una funzione ricorsiva è costruita male mostrerà subito i suoi limiti. In genere il problema connesso con la ricorsività è la facilità con cui il processo termina la memoria del computer, per quanto grande essa sia.

Per chiarire questo concetto vediamo come viene gestita una chiamata di funzione ricorsiva. Introduciamo brevemente il concetto di stack o pila. Uno stack è una zona di memoria del computer in cui vengono memorizzati dei dati, uno sopra l'altro, proprio come se fosse una pila di libri. Finché c'è spazio si può aggiungere un libro, ma se il libro viene ricoperto, bisogna aspettare di prendere tutti i libri che stanno sopra prima di prendere quello.

Supponiamo che il libro giallo sia il dato che ci interessa. Lo poniamo nello stack. Questo viene ricoperto da altri dati; prima di prelevarlo dobbiamo essere sicuri che i libri aggiunti siano stati tolti.

Ma a che serve? Quando in Pascal viene richiamata una funzione si fa ricorso allo stack. Supponiamo che il processore stia eseguendo le istruzioni di una funzione. Si dice che il sistema si trova in un determinato contesto, con certe variabili che assumono certi valori. Adesso però il processore incontra una chiamata di funzione. Tutti i dati e le variabili attuali temporaneamente non servono più perché come sappiamo ogni funzione ha il suo ambiente locale. Tuttavia non devono essere dimenticate perché quando la funzione terminerà dovranno essere ripristinati. Quindi immaginiamo di impacchettare queste variabili e di metterle nello stack (nel quadretto giallo, figura 2).
Nella funzione invocata ci sono nuove variabili e nuovi valori per queste. Supponiamo che anche qui ci sia una chiamata ad una terza funzione. Allora per una seconda volta tutte le variabili locali vengono impacchettate e messe nello stack (figura 3). Quando la terza funzione termina, dallo stack vengono prelevate le variabili "congelate" che vengono ripristinate come se nulla fosse successo (figura 4). Lo stesso avviene quando anche la seconda funzione termina: si ripristina dallo stack le variabili relative all'ambiente iniziale (figura 5).

Questa introduzione ci è servita per spiegare che ogni chiamata di funzione genera una certa occupazione di memoria nello stack. Quando la funzione termina questa memoria viene liberata.

Analizziamo questa funzione ricorsiva

	function ricorsione;
	begin
		ricorsione;
	end

essa richiama se stessa, e poi se stessa e così via all'infinito. Non esiste alcuna possibilità che essa termini. Dal punto di vista pratico invece essa termina presto, ma con un bel segnale di errore di esaurimento della memoria.
Infatti ad ogni successiva invocazione viene allocato dello spazio nello stack, il quale non viene mai rilasciato. Anche il computer più dotato di memoria terminerà in pochi secondi tutta la memoria a sua disposizione, impedendo il proseguo delle operazioni.

torna sopra


Bibliografia

torna sopra


Questo articolo è stato scaricato dal Club di informatica
Pagina curata da:
Silvio Sorace