Unità 3

Costruzione di un programma:

lo sviluppo top-down

Accade spesso, specie nei problemi complessi, che una stessa sequenza di istruzioni compaia nella stessa forma in più parti dello stesso programma o che sia utilizzata in più programmi. Gli algoritmi riguardano elaborazioni astratte di dati che possono essere adattate a problemi di natura apparentemente diversi (dal punto di vista informatico due problemi sono diversi se necessitano di elaborazioni diverse e non se trattano di cose diverse). Per fare un esempio riguardante altre discipline basta pensare per esempio alla Geometria: il calcolo dell’area di una superficie varia in relazione alla forma geometrica diversa e non alla natura dell’oggetto. L’area di una banconota o di una lastra di marmo si calcolerà sempre allo stesso modo trattandosi in ambedue i casi di rettangoli, quindi per tornare al nostro caso, l’elaborazione riguardante il calcolo dell’area di un rettangolo ricorrerà nei problemi di calcolo di blocchi di marmo così come nei problemi di calcolo di fogli su cui stampare banconote.


Per risparmiare un inutile lavoro di riscrittura di parti di codice già esistenti, i linguaggi di programmazione prevedono l’uso dei sottoprogrammi. Sostanzialmente un sottoprogramma è una parte del programma che svolge una funzione elementare.


L’uso di sottoprogrammi non è solo limitato al risparmio di lavoro della riscrittura di parti di codice, ma è anche uno strumento che permette di affrontare problemi complessi riconducendoli a un insieme di problemi di difficoltà via via inferiore. Tutto ciò consente al programmatore un controllo maggiore sul programma stesso nascondendo nella fase di risoluzione del singolo sottoprogramma, le altri parti in modo tale da isolare i singoli aspetti del problema da risolvere.


Si tratta del procedimento di stesura per raffinamenti successivi (o top down). Quando la complessità del problema da risolvere cresce, diventa difficoltoso tenere conto contemporaneamente di tutti gli aspetti coinvolti, fin nei minimi particolari, e prendere contemporaneamente tutte le decisioni realiz­zative: in tal caso sarà necessario procedere per approssimazioni successive, cioè decomporre il problema iniziale in sot­toproblemi più semplici. In tal modo si affronterà la risoluzione del problema iniziale considerando in una prima approssimazione risolti, da altri programmi di livello gerarchi­co inferiore, gli aspetti di massima del problema stesso. Si affronterà quindi ciascuno dei sottoproblemi in modo analogo.

In definitiva si comincia specificando la sequenza delle fasi di lavoro necessarie anche se, in questa prima fase, possono mancare i dettagli realizzativi: si presuppone infatti che tali dettagli esistano già. Se poi si passa all’esame di una singola fase di lavoro, questa potrà ancora prevedere azioni complesse ma riguarderà, per come è stata derivata, una parte del problema iniziale. Iterando il procedimento mano a mano si prenderanno in esame programmi che riguardano parti sempre più limitate del problema iniziale. In tal modo la risoluzione di un problema complesso è stata ricondotta alla risoluzione di più problemi semplici (tanti quante sono le funzioni previste dalla elaborazione originaria).


Le tecniche di sviluppo per raffinamenti successivi suggeriscono cioè di scrivere subito il programma completo, come se il linguaggio di pro­grammazione a disposizione fosse di livello molto elevato ed orientato proprio al problema in esame.

Tale programma conterrà, oltre alle solite strutture di controllo, an­che operazioni complesse che dovranno poi essere ulteriormente specificate. Queste operazioni verran­no poi descritte in termini di operazioni ancora più semplici, e così via fino ad arrivare alle operazioni elementari fornite dal linguaggio di pro­grammazione utilizzato.

Inizio


Un esempio di sviluppo top-down



Applichiamo il procedimento descritto alla risoluzione del seguente problema:


In una località geografica sono state rilevate ogni 2 ore le temperature di una giornata. Si vuole conoscere la temperatura media, l’escursione termica e lo scostamento medio dalla media.


Si tratta di scrivere un programma che richiede alcune elaborazioni statistiche su una serie di valori. Si ricorda che la media aritmetica di una serie n di valori è data dal rapporto fra la somma dei valori della serie e il numero n stesso. L’escursione termica è in pratica il campo di variazione cioè la differenza fra il valore massimo e il valore minimo della serie di valori. Lo scostamento medio è la media dei valori assoluti degli scostamenti dalla media aritmetica, dove lo scostamento è la differenza fra il valore considerato della serie e la media aritmetica.


Scriviamo il programma nel modo seguente:



	Inizio
	  Acquisizione temperature rilevate
	  Calcolo media e ricerca massimo e minimo
	  Calcolo escursione termica
	  Calcolo scostamento medio
	  Comunicazione risultati
	Fine


In questa prima approssimazione si sono evidenziati i risultati intermedi da conseguire affinché il problema possa essere risolto. Non si parla di istruzioni eseguibili ma di stati di avanzamento del processo di elaborazione: per il momento non c’è niente di preciso ma il nostro problema è stato ricondotto a 5 problemi ognuno dei quali si occupa di una determinata elaborazione. Viene messa in evidenza la sequenza delle operazioni da effettuare: l’escursione termica si può, per esempio, calcolare solo dopo la ricerca del massimo e del minimo.

Si noti che ad ogni fase di lavoro è assegnato un compito specifico ed è quindi più facile la ricerca di un eventuale sottoprogramma errato: se lo scostamento medio è errato e la media risulta corretta è chiaro che, con molta probabilità, l’errore è localizzato nel penultimo sottoprogramma.


Il primo sottoprogramma possiamo già tradurlo in istruzioni eseguibili. È opportuno tenere presente che a questo livello il problema da risolvere riguarda solamente l’acquisizione delle temperature rilevate. Il resto del programma, a questo livello, non esiste.



	Acquisizione temperature
	Inizio
	  Per indice da 0 a 11
	    Ricevi temperatura rilevata
	  Fine-per
	Fine


Passando al dettaglio del secondo sottoprogramma possiamo pensarlo composto da una fase di inizializzazione dell’accumulatore della somma dei termini della serie e delle variabili che conterranno il massimo ed il minimo della serie stessa. La seconda fase è il calcolo vero e proprio. Anche qui il problema è limitato solo alla parte specificata.



	Calcolo media e ricerca massimo e minimo
	Inizio
	  Inizializza Somma Valori
	  Considera primo elemento serie come Massimo e Minimo
	  Aggiorna Somma e cerca Massimo e Minimo
	  Calcola Media
	Fine




	Aggiorna Somma e cerca Massimo, Minimo
	Inizio
	  Per indice da 1 a 11
	    Aggiorna Somma con elemento considerato
	    Se elemento<Minimo
	      Sostituisci elemento a Minimo
	    Altrimenti
	      Se elemento>Massimo
	        Sostituisci elemento a Massimo
	      Fine-se
	    Fine-se
	  Fine-per
	Fine


Il terzo sottoprogramma è immediato:



	Calcolo escursione termica
	Inizio
	  Escursione = Massimo – Minimo
	Fine

Il quarto sottoprogramma, come il secondo, prevede una inizializzazione:



	Calcolo scostamento medio
	Inizio
	  Azzera Somma scostamenti
	  Aggiorna Somma
	  Calcola Media scostamenti
	Fine


	Aggiorna Somma scostamenti
	Inizio
	  Per indice da 0 a 11
	    Se elemento>Media aritmetica
	      Scostamento = elemento - Media
	    Altrimenti
	      Scostamento = Media - elemento
	    Fine-se
	    Aggiorna Somma scostamenti con scostamento
	  Fine-per
	Fine

L’ultimo sottoprogramma è immediato:



	Comunicazione risultati
	Inizio
	  Comunica Media
	  Comunica Escursione termica
	  Comunica Scostamento medio
	Fine

Il nostro programma a questo punto è interamente svolto. Per ogni sottoprogramma ci si è occupati di un solo aspetto dell’elaborazione: ciò rende la stesura del programma più semplice ed inoltre la manutenzione del programma stesso diventa più semplice. Ogni sottoprogramma diventa più semplice da controllare rispetto al programma nel suo complesso.


Il programma è stato diviso praticamente in 7 parti. Ognuna può essere trattata, in sede di codifica, come un sottoprogramma a parte come anche si può decidere di lasciare ai sottoprogrammi, vedi seconda e quarta fase, il compito dell’elaborazione e lasciare le inizializzazioni nel programma principale (il primo che si è scritto, quello cioè dove è evidenziata tutta l’elaborazione da svolgere). Si può anche decidere, sempre nella seconda e quarta fase per esempio, di considerare le inizializzazioni parte integrante della elaborazioni stessa e quindi rispettare la prima suddivisione di massima del programma, saltando la seconda suddivisione.


Il processo di scomposizione successiva non è fissato in maniera univoca: dipende fortemente dalla soggettività del programmatore. Non ci sono regole sulla quantità di pezzi in cui scomporre il programma. Ci sono delle indicazioni di massima che suggeriscono di limitare il singolo segmento in maniera sufficiente a che il codice non superi di molto la pagina in modo da coprire, con lo sguardo, l'intero o quasi programma e limitare il singolo segmento a poche azioni di modo che sia più semplice isolare eventuali errori. Inoltre il sottoprogramma deve essere quanto più possibile isolato dal contesto in cui opera, cioè il sottoprogramma deve avere al suo interno tutto ciò di cui ha bisogno e non fare riferimento a dati particolari presenti nel programma principale. Ciò porta ad alcuni indubbi vantaggi:



L’utilizzo di sottoprogrammi già pronti per la costruzione di un nuovo programma porta ad una metodologia di sviluppo dei programmi che viene comunemente chiamata bottom-up poiché rappresenta un modo di procedere opposto a quello descritto fino ad ora. Si parte da sottoprogrammi già esistenti che vengono assemblati assieme a nuovi per costruire la nuova elaborazione. In definitiva “… si può affermare che, nella costruzione di un nuovo algoritmo, è dominante il processo top-down, mentre nell’adattamento (a scopi diversi) di un programma già scritto, assume una maggiore importanza il metodo bottom-up.” (N.Wirth)

Inizio


Comunicazioni fra sottoprogrammi



Seguendo il procedimento per scomposizioni successive si arriva alla fine ad un programma principale e ad una serie di sottoprogrammi. Il programma principale chiama in un certo ordine i sottoprogrammi; ogni sottoprogramma oltre che chiamato può anche essere il chiamante di un ulteriore sottoprogramma. Terminato il sottoprogramma l’esecuzione riprende, nel chiamante, dall’istruzione successiva alla chiamata.


Si può dire che tutti i sottoprogrammi fanno parte di un insieme organico: ognuno contribuisce, per la parte di propria competenza, ad una elaborazione finale che è quella fissata dal programma principale. Si tratta in definitiva di un sistema dove l’elaborazione finale richiesta è frutto della cooperazione delle singole parti; ogni sottoprogramma (unità del sistema) riceve i propri input (intesi come somma delle informazioni necessarie all’espletamento delle proprie funzioni) dai sottoprogrammi precedenti, e fornisce i propri output (intesi come somma delle informazioni prodotte al suo interno) ai sottoprogrammi successivi. Per questi ultimi, le informazioni suddette saranno gli input della propria parte di elaborazione.


Quanto detto porterebbe alla conclusione che tutti i sottoprogrammi, facendo parte di un insieme organico, lavorano sulle stesse variabili. D’altra parte se, per esempio, il reparto carrozzeria e il reparto verniciatura operano nella stessa fabbrica di automobili, è ovvio che il reparto verniciatura si occuperà delle stesse carrozzerie prodotte dall’altro reparto. Così in effetti è stato per esempio per i linguaggi di programmazione non strutturati: i sottoprogrammi condividevano le stesse variabili, il sottoprogramma eseguiva una parte limitata di elaborazione ma era fortemente legato al contesto generale. La portabilità di un sottoprogramma, in queste condizioni, è estremamente problematica richiedendo pesanti modifiche al codice stesso (si pensi al fatto che il programma destinazione non usa le stesse variabili del programma che ospitava originariamente il sottoprogramma o, peggio ancora, usa variabili con nomi uguali ma con significati diversi).


Per garantire quanto più possibile la portabilità e l’indipendenza dei sottoprogrammi, i linguaggi strutturati adottano un approccio diverso distinguendo le variabili in base alla visibilità (in inglese scope). In relazione alla visibilità le variabili si dividono in due famiglie principali:



Per quanto ribadito più volte sarebbe necessario utilizzare quanto meno possibile (al limite eliminare) le variabili globali per ridurre il più possibile la dipendenza dal contesto da parte del sottoprogramma.

Riguardando però l’elaborazione dati comuni, è necessario che il programma chiamante sia in condizioni di poter comunicare con il chiamato. Devono cioè esistere delle convenzioni di chiamata cioè delle convenzioni che permettono al chiamante di comunicare dei parametri che rappresenteranno gli input sui quali opererà il chiamato. D’altra parte il chiamato avrà necessità di tornare al chiamante dei parametri che conterranno i risultati della propria elaborazione e che potranno essere gestiti successivamente. Queste convenzioni sono generalmente conosciute come passaggio di parametri.


Il passaggio di parametri può avvenire secondo due modalità:



Per sintetizzare praticamente su cosa passare per valore e cosa per riferimento, si può affermare che gli input di un sottoprogramma sono passati per valore mentre gli output sono passati per riferimento. Gli input di un sottoprogramma sono utili allo stesso per compiere le proprie elaborazioni mentre gli output sono i prodotti della propria elaborazione che devono essere resi disponibili ai sottoprogrammi successivi.

Inizio


Tipi di sottoprogrammi


Si è parlato di sottoprogrammi in modo generico perché si volevano evidenziare le proprietà comuni. In genere si fa distinzione fra due tipi di sottoprogrammi:



È opportuno osservare che quanto espresso prima non esaurisce le comunicazioni fra sottoprogrammi. Da quanto detto infatti potrebbe sembrare che tutte le comunicazioni fra chiamante e chiamato si esauriscano, nella migliore delle ipotesi (funzioni), in un unico valore. In realtà la comunicazione si gioca principalmente sul passaggio di parametri, quindi una procedura può modificare più variabili: basta che riceva per riferimento tali variabili.


Nel linguaggio C ogni sottoprogramma ha un nome e i sottoprogrammi vengono chiamati specificandone il nome. Vedremo in seguito le convenzioni che regolano il passaggio di parametri.

Inizio



I sottoprogrammi in C++: le funzioni.
Istruzione return


Nel linguaggio C++ la maggior parte di quello che si usa è costituito da funzioni. Per poter simulare le procedure che non ritornano alcun valore è stato introdotto il tipo void. Il tipo void o tipo indefinito è utilizzato dal C++ tutte le volte che il valore di ritorno di una funzione non deve essere preso in considerazione. In pratica nel linguaggio C++ le procedure sono funzioni che restituiscono un void.


Possiamo sintetizzare in tre fasi la costruzione e l’uso di una funzione:



tipo-ritornato nome-funzione(dichiarazione parametri)

{

dichiarazioni ed istruzioni

}


Le definizioni di funzioni possono essere scritte in qualsiasi punto del programma: verranno mandate in esecuzione in seguito alla chiamata e quindi non ha alcuna importanza il posto fisico dove sono allocate. Lo standard ANSI ha però fissato delle convenzioni secondo le quali le definizioni delle funzioni è bene codificarle dopo il main. D’altra parte il main stesso non è altro che una funzione particolare che viene eseguita per prima. Il programma quando viene eseguito effettua una chiamata alla funzione main. Il tipo-ritornato non è specificato perché si sottintende int: quando non è specificato il tipo, il linguaggio C++ assume per default il tipo int sebbene, per motivi di comprensibilità, sia sempre opportuno dichiarare il tipo. In definitiva all’atto dell’esecuzione il programma effettua una chiamata alla funzione main, questa può restituire al chiamante (in questo caso il Sistema Operativo) un valore intero. Tale valore potrebbe per esempio essere utilizzato per stabilire l’azione da svolgere in relazione ad una terminazione anomala del programma.

Fra le istruzioni contenute nella definizione della funzione particolare importanza assume l’istruzione return utilizzata per ritornare al chiamante un valore. La sintassi dell’istruzione prevede di specificare dopo la parola chiave return un valore costante o una variabile compatibile con il tipo-ritornato dalla funzione. Es.


return 5; // Ritorna al chiamante il valore 5

return a; // Ritorna al chiamante il valore contenuto nella variabile a


Non è importante che le definizioni di tutte le funzioni usate in un programma seguano l’ordine con cui sono chiamate sebbene, per motivi di chiarezza e leggibilità, è opportuno che sia così e che si segua l’ordine specificato prima. In ogni caso la funzione con il nome main è eseguita per prima, in qualunque posto sia messa, e le funzioni sono eseguite nell’ordine in cui sono chiamate.

I prototipi sono stati introdotti per permettere al compilatore di effettuare un maggiore controllo sui tipi di parametri: conoscendoli in anticipo, infatti, all’atto della chiamata che avviene dalla funzione main (la prima eseguita), è possibile stabilire se i parametri passati sono congruenti con quelli attesi.

Nella costruzione di programmi complessi capita di utilizzare molte funzioni. In questo caso le funzioni sono raggruppate in librerie e i rispettivi prototipi sono raggruppati nei files di intestazione (header files). Gli header sono file di tipo testo che hanno estensione “.h”. Si è avuto modo di utilizzare librerie di funzioni fin dall’inizio. Per esempio sia cin che cout sono funzioni (in realtà si tratta di oggetti, ma ciò sarà chiarito in seguito) contenute in una libreria di sistema che è inclusa, all’atto della compilazione, nel nostro programma. I prototipi di tali funzioni sono nello header iostream.h che, per tali motivi, viene incluso all’inizio del programma.

Inizio


Parametri in C++: un esempio pratico


Nel linguaggio C++ tutti i parametri sono passati alle funzioni per valore, e ciò allo scopo di isolare quanto più possibile la funzione e renderla riutilizzabile. Per passare alla funzione un parametro per riferimento è necessario utilizzare il simbolo & attaccato alla fine della dichiarazione di tipo della variabile o all’inizio del nome della variabile stessa. L’unica eccezione a quanto detto riguarda gli array che sono sempre passati per indirizzo.


La funzione, nella sua definizione, preparerà variabili locali per ricevere i parametri passati dal chiamante. Nel caso di passaggio per indirizzo, ogni variazione effettuata dalla funzione sul contenuto della variabile locale si ripercuoterà sulla variabile corrispondente del chiamante.


I parametri passati dal chiamante sono comunemente chiamati argomenti o parametri reali, mentre quelli presenti nella definizione della funzione sono chiamati parametri formali.


A questo punto si sono acquisite tutte le informazioni che servono per codificare il programma delle elaborazioni statistiche di temperature. Al solito le righe significative sono accompagnate da commenti numerici utilizzati per le osservazioni successive:




	/*
	  Temperatura media, escursione termica e scostamento medio
	  di una serie di temperature rilevate in una località
	  Le temperature sono state rilevate ogni 2 ore
	*/

	#include <iostream.h>
	#define RILEVAZ 12										/*1*/

	void rilevaTemp(int t[]);								/*2*/
	void aggiorna(int t[],int& st,int& tmn,int& tmx);		/*2*/
	float scostMed(int t[],float mt);						/*2*/

	main(){
	  int temp[RILEVAZ],tempMax,tempMin,escur;
	  float medTemp,medSco;
	  int sommaTemp;

	  // Acquisizione temperature rilevate 

	  rilevaTemp(temp);										/*3*/

	  // Calcolo media e ricerca massimo e minimo 

	  sommaTemp = tempMin = tempMax = temp[0];
	  aggiorna(temp,sommaTemp,tempMin,tempMax);				/*4*/
	  medTemp = (float) sommaTemp/RILEVAZ;					/*5*/

	  // Calcolo escursione termica 

	  escur = tempMax-tempMin;

	  // Scostamento medio 

	  medSco = scostMed(temp,medTemp);						/*6*/

	  // Comunicazione risultati 

	  cout << “\nMedia temperature rilevate -> ” << medTemp;
	  cout << “\nEscursione termica ---------> ” << escur;
	  cout << “\nScostamento medio ----------> ” << medSco;
	}

	/* Lettura temperature */

	void rilevaTemp(int t[]){								/*7*/
	  int i;

	  cout << “\nAcquisizione temperature rilevate\n”;
	  for (i=0;i<RILEVAZ;i++){								/*8*/
	    cout << “Temperatura ore ” << i*2 << “ “;  
 	    cin  >> t[i];
	  }
	}

	/* Aggiornamento Somma temperature, temp massima e minima */

	void aggiorna(int t[],int& st,int& tmn,int& tmx){		/*9*/
	  int i;

	  for (i=1;i<RILEVAZ;i++){		
	    st += t[i];
	    t[i]<tmn ? tmn=t[i] : (t[i]>tmx ? tmx=t[i] : 0);	/*10*/
	  }
	}

	/* Calcolo scostamento medio */

	float scostMed(int t[],float mt){						/*11*/
	  float somScost,scost,media;
	  int i;

	  somScost = 0;
	  for (i=0;i<RILEVAZ;i++){
	    scost = t[i]>mt ? t[i]-mt : mt-t[i];
	    somScost += scost;
	  }
	  media = somScost/RILEVAZ;
	  return media;											/*12*/
	}

Nella riga 1 si dichiara una costante. Tale dichiarazione essendo esterna rispetto a tutte le funzioni ha visibilità globale: tutte le funzioni possono utilizzarla. Le variabili globali, se ci sono, devono essere dichiarate in questa posizione in modo da essere disponibili per tutte le funzioni.

Nelle 2 sono dichiarati i prototipi delle funzioni utilizzate. A questo punto quello che importa è il tipo di parametro e la modalità del passaggio del parametro stesso. I nomi dei parametri, potendo mancare, sono aggiunti qui per chiarezza.

Nella riga 3 viene effettuata una chiamata alla funzione rilevaTemp passando il parametro temp. Il nome di un array rappresenta il riferimento all’array stesso. In questo modo la main fa conoscere a rilevaTemp la posizione in memoria dell’array temp.

Nella 4 viene effettuata una chiamata alla funzione aggiorna. Tale funzione ha il compito di aggiornare i valori contenuti in sommaTemp, tempMin e tempMax (che quindi per la funzione rappresentano degli output). L’array temp per la funzione è un input (non viene effettuata da aggiorna alcuna modifica sull’array), basterebbe quindi un passaggio per valore del parametro solo che un array, per quanto osservato precedentemente, è sempre passato per indirizzo: possono essere passati per valore solo i singoli elementi di un array essendo questi delle variabili come tutte le altre. Gli altri parametri sono passati per riferimento in accordo con quanto dichiarato nel prototipo. Nella chiamata alla funzione si specifica, sia per il passaggio per valore che per quello per riferimento, il nome del parametro: sarà il compilatore a distinguere i due casi in relazione a quanto specificato nel prototipo della funzione.

Nella riga 5 si effettua una divisione fra interi, se si vuole conservare anche la parte decimale si deve effettuare un casting.

Nella riga 6 viene effettuata una chiamata alla funzione scostMed, solo che in questo caso a differenze delle precedenti chiamate alle altre funzioni (righe 3 e 4), volendo utilizzare il valore ritornato dalla funzione, tale valore è assegnato ad una variabile.

Nella riga 7 viene definita la funzione rilevaTemp che restituisce un valore indefinito (void). Alla funzione è passato un array (si capisce dalla presenza delle parentesi quadre che accompagnano il parametro). La funzione conserva tale array in t che è locale, così come la variabile intera i definita nella riga successiva. Quando la funzione termina scompaiono sia t che i solo che t era un riferimento a temp e, quindi, le modifiche effettuate a t nella funzione, praticamente erano modifiche effettuate a temp. La variabile i invece, al termine della funzione, scompare senza lasciare alcuna traccia della sua esistenza.

La condizione di controllo del ciclo della riga 8 è espressa come i<RILEVAZ perché gli elementi dell’array sono in numero di RILEVAZ e quindi le posizioni utilizzabili vanno da 0 a RILEVAZ-1.

Per quanto riguarda la definizione della funzione della riga 9 valgono considerazioni analoghe alla definizione della funzione della riga 7 per quanto riguarda l’array. Per gli altri parametri vengono preparati, dalla funzione, delle variabili locali che, per la presenza dell’indicatore di passaggio per riferimento (&), si agganciano ai parametri passati dal chiamante permettendo alla funzione la modifica dei valori contenuti nelle variabili del chiamante.

Nella riga 10 è codificata utilizzando l’operatore ? una doppia condizione nidificata. Se la prima condizione è vera (t[i]<tmn) è effettuata l’assegnazione specificata. Se risulta falsa è testata una nuova condizione (t[i]>tmx). Anche qui se tale condizione è vera viene effettuata una assegnazione, se è falsa non c’è alcuna elaborazione: viene assegnato il valore 0 che, non essendoci alcuna variabile pronta per riceverlo, viene trascurato. Il valore deve essere espresso poiché l’operatore ? deve restituire sempre un valore.

Nella riga 11 è definita una funzione che ritorna un valore di tipo float. Altrove questi sono gli unici sottoprogrammi che sono chiamati funzioni, laddove i precedenti due (che in C++ sono funzioni che restituiscono un valore di tipo void) sarebbero chiamati procedure. Anche in questo caso nelle due righe successive sono dichiarate delle variabili locali.

Nella riga 12 è ritornato al chiamante il valore atteso. La variabile media è dichiarata di tipo float in concordanza con il tipo di ritorno della funzione. La variabile è locale e quindi cessa la sua esistenza al cessare della funzione, ma in questo modo si rende noto al chiamante il valore contenuto in essa.

Inizio


Due osservazioni. Il qualificatore const


Sono necessarie due osservazioni sulla codifica effettuata:


La trasposizione in codifica delle singole fasi dell’elaborazione è stata effettuata con una combinazione di commenti e funzioni. Le funzioni avrebbero potuto essere di più: per esempio tante quante sono le parti in cui si è scomposto il programma. La scelta effettuata in questo caso ha tenuto conto del fatto di evitare una estrema polverizzazione in funzioni che avrebbe reso più difficoltosa la comprensione dell’algoritmo utilizzato per la risoluzione del problema. Quando si trattava di poche istruzioni, queste sono state inglobate nel programma principale. È bene ripetere che questa o altra scelta che è possibile effettuare, dipende dal modo come il programmatore intende organizzare le diverse fasi di lavoro. Bisogna in ogni caso ricordare che ogni parte del programma deve esprimere chiaramente quello che fa; organizzare il programma in modo che sia chiaro cosa si fa e dove ed inoltre ogni fase di lavoro deve quanto più possibile essere isolata.


L’utilizzo della costante globale RILEVAZ rende dipendenti le funzioni da tale costante rendendo più problematico il riutilizzo delle funzioni stesse e il rintracciamento di eventuali errori.

È possibile dichiarare le costanti anche all’interno di una funzione utilizzando la parola chiave const. Es.


Programma A

..

#define RILEVAZ 12

..

main(){

..

Programma B

..

main(){

const int RILEVAZ = 12;

..


Quella espressa nel Programma A non è una vera e propria istruzione ma una direttiva al preprocessore. Prima della compilazione è effettuata una sostituzione: tutte le ricorrenze di RILEVAZ sono sostituite col valore 12 e quindi è effettuata la compilazione. RILEVAZ, in sostanza, nel programma e in memoria non esiste. Le direttive al preprocessore sono azioni da svolgere prima di passare alla compilazione del programma. Anche #include <iostream.h> è una direttiva al preprocessore: essa ordina di includere nel programma lo header iostream.h prima di procedere alla compilazione.

Nel Programma B è invece dichiarata una variabile di tipo int che contiene il valore 12. Il qualificatore const avvisa che il contenuto della variabile associata non può essere modificato.


Se si utilizza la via suggerita dal Programma B, le funzioni non possono conoscere la quantità degli elementi presenti nell’arrray (la dichiarazione è locale): è necessario prevedere un ulteriore parametro che consenta di poter passare alle funzioni tale informazione. Le funzioni che elaborano l’array conterranno, nella definizione, un nuovo parametro che consenta di ricevere il valore della quantità di elementi presenti nell’array stesso.


L’uso della costante globale se da una parte rende le funzioni dipendenti da tale dichiarazione, dall’altra permette di semplificare il passaggio di parametri tra funzioni (si ha necessità di un parametro in meno).


Un uso interessante del qualificatore const consiste nell’eliminazione di una limitazione già messa in evidenza: il passaggio, necessariamente per riferimento, di un array alle funzioni. Basta specificare const per avvisare che l’array non deve essere modificato. In pratica l’array viene passato per valore.

Es.


void rilevaTemp(int t[]);

void aggiorna(const int t[],int& st,int& tmn,int& tmx);

float scostMed(const int t[],float mt);


Alla prima funzione è passato il riferimento all’array. Il compito di tale funzione è, infatti, quello di riempire l’array con i dati provenienti dall’input.

Le funzioni aggiorna e scostMed non modificano l’array, è quindi giusto che se ne impedisca una modifica anche accidentale: l’array in questi casi è un input.

Nonostante si passi alla funzione un indirizzo di memoria, la const impedisce la modifica del contenuto dell’array.


TORNA INDIETRO