Linguaggio C++ Imparare a programmare: le funzioni (e le procedure) Fino ad ora, sono stati costruiti programmi formati da un unico blocco di istruzioni. Nel caso del linguaggio C++ il blocco di istruzioni che costituisce il programma (main) è racchiuso tra le parentesi graffe { }. Nella maggior parte dei progetti reali non è pensabile di produrre del software sviluppando un unico programma molto lungo e molto complesso. Pertanto delle metodologie utilizzate nella produzione del software (TOP-DOWN, DIVIDE et IMPERA) prevedono sempre una suddivisione del progetto in sottoprogetti e un’articolazione dei sottoprogetti in sottoproblemi. Ciò implica che il linguaggio di programmazione adottato deve prevedere la possibilità di specificare sottoprogrammi e di richiamare un sottoprogramma dal programma principale o da altri sottoprogrammi. Un sottoprogramma è molto simile ad un programma, come il programma deve rispettare le regole lessicali, sintattiche e strutturali del linguaggio di programmazione e come il programma può avere “dati” di ingresso e può produrre “dati” di uscita. In generale il sottoprogramma riceve i “dati di ingresso” dal programma (o sottoprogramma) che lo “chiama” (o “invoca”) e a sua volta il sottoprogramma “ritorna” (o “restituisce”) al programma (o sottoprogramma) che lo ha “chiamato” i “dati di uscita”; il programma (o sottoprogramma) che “chiama” viene detto CHIAMANTE, il sottoprogramma “invocato” viene detto CHIAMATO. Ci sono tre buoni motivi per cui è importante scomporre un progetto in sottoprogetti e definire i relativi sottoprogrammi: • • •
Risparmio di codice Riuso di codice Astrazione
Risparmio di codice: evitare di ripetere più volte in un programma una certa sequenza di istruzioni quindi raggruppare l’insieme di istruzioni da ripetere in un sottoprogramma e richiamarlo tutte le volte che è necessario eseguire nel programma principale quel gruppo di istruzioni Riuso di codice: realizzare un nuovo progetto “riusando” (richiamando) sottoprogrammi sviluppati precedentemente; ad esempio in C++ riusare i sottoprogrammi sqrt(Y), pow(X,2) ecc. Astrazione: raggruppare in un sottoprogramma l’insieme di istruzioni che svolge un determinato compito serve anche a rendere più leggibile il programma, in quanto consente di isolare i sottoproblemi e di scrivere un programma più breve e chiaro. In generale esistono due tipologie di sottoprogrammi: funzioni e procedure. •
•
Una funzione è un sottoprogramma il cui scopo principale è quello di “ritornare”, a chi la invoca, un solo valore a partire da determinati valori di ingresso (la quantità di valori di ingresso può variare da 0 a N); Il concetto di funzione nell’ambito della programmazione è analogo all'omonimo concetto di funzione della matematica, cioè una funzione è una “relazione” fra due insiemi che ad ogni elemento del primo insieme (Dominio) associa uno e un solo valore del secondo insieme (Codominio) una procedura è un sottoprogramma, di carattere più generale che può non "ritornare" alcun particolare valore o può "ritornarne" uno e più di uno a partire da valori di ingresso (la quantità di valori di ingresso può variare da 0 a N) . Alcuni linguaggi (per esempio il C, C++, C#) adottano come modello "standard" quello della funzione e considerano le procedure come caso particolare di funzione che ritorna un valore appartenente all'insieme vuoto.
Pagina 1 di 8
In C++ la dichiarazione dei sottoprogrammi si effettua nei due modi seguenti: Sintassi per la dichiarazione di una funzione: <tipo_valore_restituito> <nome> (<modo_passaggio> <nome_parametro>: <tipo_parametro>; …); [<elenco variabili>;] // risorse locali { <corpo della funzione> return <valore> // la funzione deve “restituire” un valore di tipo <tipo_valore_restituito> }; Sintassi per la dichiarazione di una procedura: void <nome> (<modo_passaggio> <nome_parametro>: <tipo_parametro>; .....); [<elenco variabili>;] // risorse locali { <corpo della funzione (procedura) > }; la prima riga relativa alla dichiarazione di una funzione e di una procedura viene indicata con il nome di INTERFACCIA o PROTOTIPO del sottoprogramma ed è di fondamentale importanza perché è l’”anello di congiunzione” tra “chi realizza” il sottoprogramma e “chi utilizza” il sottoprogramma. Nel Prototipo del sottprogramma, oltre al tipo di sottoprogramma (function o procedure) è indicato il nome ed eventualmente, tra parentesi, l’elenco dei parametri, cioè le variabili che fanno riferimento ai valori ricevuti dal CHIAMANTE, che sono detti parametri formali. Per ciascun parametro (o variabile) deve essere indicato, oltre al nome, il tipo e il modo con cui avviene il passaggio di valori tra CHIAMANTE e CHIAMATO. Le modalità con cui vengono passati i parametri tra CHIAMANTE e CHIAMATO, da un punto di vista logico, sono 3: • ingresso, quando il CHIAMANTE fornisce i valori al CHIAMATO; • uscita, quando il CHIAMATO restituisce i valori al CHIAMANTE; • ingresso/uscita, quando il CHIAMANTE fornisce i valori al CHIAMATO, il CHIAMATO li modifica e li restituisce al CHIAMANTE. In C++, i 3 modi logici di passaggio dei parametri vengono tradotti in 2 modi concreti detti: Passaggio per valore (che indica il modo di passaggio in ingresso): Il valore dei parametri forniti dal CHIAMANTE (parametri attuali) viene copiato nelle variabili del CHIAMATO che rappresentano i parametri formali. Se il sottoprogramma chiamato ne modifica il valore, il sottoprogramma chiamante non potrà “vedere”queste modifiche. Si tratta quindi di passaggio unidirezionale. Per denotare il passaggio per valore basta non scrivere nulla davanti a <nome_parametro> nel prototipo del sottoprogramma. Passaggio per indirizzo o per riferimento (che indica i modi di passaggio in uscita ed in ingresso/uscita): il sottoprogramma chiamato riceve dal chiamante non il valore dei parametri ma l’indirizzo (o riferimento) dei parametri attuali. Se il sottoprogramma chiamato modifica il valore di un parametro passato per indirizzo, la modifica sarà “visibile” anche al sottoprogramma chiamante. Il passaggio è quindi potenzialmente bidirezionale. Per denotare il passaggio per indirizzo bisogna scrivere & in <modo_passaggio> davanti a <nome_parametro> nel prototipo del sottoprogramma. In generale, in una funzione, i parametri “dovrebbero” essere passati solo in ingresso; inoltre, come si vede dal prototipo, alla funzione è associato un tipo <tipo_valore_restituito> che rappresenta il tipo del valore che la funzione restituisce al CHIAMANTE.
Pagina 2 di 8
Esempio 1 Funzione che calcola la potenza di un numero double potenza(double BASE, int ESPONENTE) { //inizio corpo funzione int N; //variabili LOCALI double P; P=1; for (N=1; N<=abs(ESPONENTE); N++) { P=P*BASE; } if (ESPONENTE<0) P=1/P; return P; } //fine funzione
BASE ed ESPONENTE sono i parametri formali passati in ingresso (per valore) che contengono i valori da utilizzare nel corpo della funzione; nella funzione esiste sempre una variabile con lo stesso nome della funzione. E' a tale variabile che va assegnato il risultato del calcolo della funzione, in modo che possa essere restituito al chiamante. Il vantaggio principale delle funzioni è la possibilità di utilizzarle direttamente nelle espressioni o stamparne direttamente il valore restituito:
#include <iostream> using namespace std; 1. double potenza(double BASE, int ESPONENTE) 2. { //inizio corpo funzione 3. int N; //variabili LOCALI 4. double P; 5. P=1; 6. for (N=1; N<=abs(ESPONENTE); N++) 7. { 8. P=P*BASE; 9. } 10. if (ESPONENTE<0) 11. P=1/P; 12. return P; 13. } //fine funzione 1. int main() //inizio programma principale 2. { 3. double X, Y; //variabili GLOBALI 4. int N; 5. cin >> Y >> N; 6. X= potenza(Y,N)- potenza(Y,N-3); //Y, N, N-3 sono i parametri attuali 7. cout << X<<endl; 8. cout << potenza(2,-3)<<endl; //2, -3 valori attuali 9. system("PAUSE"); 10. } //fine programma principale
nel momento della invocazione della funzione (riga 6 e riga 8 del programma principale) i valori dei parametri attuali (e i valori attuali) vengono “copiati” nei parametri formali, rispettivamente BASE ed ESPONENTE, che da quel momento vengono “create” (prima della chiamata del sottoprogramma non esistevano) e inizializzate, vengono “create” le variabili locali e viene eseguito il corpo della funzione, al termine la funzione ritorna al CHIAMANTE il valore calcolato e “distrugge” l’ambiente (parametri formali e variabili locali) creato al momento dell’invocazione. Pertanto anche se i nomi delle variabili locali e i nomi dei parametri formali sono uguali ai nomi delle variabili Pagina 3 di 8
globali, per l’esecutore non c’è ambiguità o confusione perché l’ambiente locale (parametri formali e variabili locali) viene creato successivamente all’ambiente globale (costanti e variabili globali) e in una area diversa della memoria principale (RAM). Ambiente globale
Ambiente locale
Wikipedia
2
8
2
X
Y
N
BASE
8 ESPONENTE
1
1
N
P
Esempio 3 Funzione (procedura) che calcola QUOZIENTE e RESTO di due numeri interi
void DIVISIONE(int A, int B, int &QUOZIENTE, int &RESTO) { //inizio corpo funzione QUOZIENTE= A / B; RESTO= A % B; } //fine funzione A e B sono i parametri formali passati in ingresso (per valore) che contengono i valori da utilizzare nel corpo della procedura; QUOZIENTE, RESTO sono i parametri formali passati in uscita (per indirizzo) che contengono i valori da
restituire al chiamante. #include <iostream> using namespace std; 1. void DIVISIONE(int A, int B, int &QUOZIENTE, int &RESTO) 2. { //inizio corpo funzione 3. QUOZIENTE= A / B; 4. RESTO= A % B; 5. } //fine funzione 1. int main() //inizio programma principale 2. { 3. int X,Y,Q,R; //variabili GLOBALI 4. 5. cin >> X >> Y; 6. DIVISIONE(X,Y,Q,R); //X, Y, Q, R sono i parametri attuali 7. cout << "quoziente "<< Q <<" resto "<< R<< endl; 8. system("PAUSE"); 9. } //fine programma principale nel momento della invocazione della funzione (riga 6 del programma principale) i valori dei parametri attuali, contenuti nelle variabili X,Y vengono “copiati” nei parametri formali, rispettivamente A ed B, che da quel momento vengono “create” (prima della chiamata del sottoprogramma non esistevano) e inizializzate, vengono “create” anche le variabili locali QUOZIENTE, RESTO che contengono l’indirizzo della memoria RAM delle corrispondenti variabili globali Q e R; viene eseguito il corpo della procedura e quando viene eseguita l’istruzione QUOZIENTE = A / B, il valore dell’operazione viene inserito direttamente nella variabile globale Q, utilizzando l’indirizzo contenuto in QUOZIENTE, lo stesso accade per l’istruzione successiva. Al termine della procedura viene “distrutto” l’ambiente (parametri formali e
variabili locali) creato al momento dell’invocazione ma il valore restituito dai parametri di uscita viene conservato nelle corrispondenti variabili globali (Q e R nell’esempio).
Ambiente locale
Ambiente globale 13
4
3
1
X
Y
Q
R
13
4
Indirizzo Q
A
B
QUOZIENTE
Indirizzo R RESTO
QUOZIENTE = A / B il risultato 3 viene inserito direttamente nella variabile globale Q, il cui indirizzo si trova nella variabile locale QUOZIENTE RESTO = A % B; il risultato 1 viene inserito direttamente nella variabile globale R, il cui indirizzo si trova nella variabile locale RESTO
Pagina 4 di 8
Esempio 3 Programma per la gestione di Frazioni. In questo esempio, più articolato, si fa uso dei due tipi di sottoprogrammi, funzioni e procedure con le varie tipologie di passaggio di parametri. In questo esempio si notano tutti i vantaggi dell’uso dei sottoprogrammi: Risparmio di codice, Riuso di codice, Astrazione. #include <iostream> using namespace std; //funzione che calcola MCD int MCD(int X, int Y) //2 parametri di ingresso 1 valore di uscita { int R; do { R = X % Y; X = Y; Y = R; } while (R!=0); return X; } //funzione che calcola mcm int MCM(int X, int Y) //2 parametri di ingresso 1 valore di uscita { return X*Y/MCD(X,Y); } void SEMPLIFICA(int &NUM, int &DEN) //2 parametri di ingresso/uscita { //inizio corpo funzione int M; M = MCD(NUM,DEN); if (M>1) { NUM = NUM / M; DEN = DEN / M; } } //fine funzione void LEGGIFRAZIONE(int &NUM, int &DEN) //2 parametri di uscita { cout<<"Numeratore: "; cin >> NUM; do { cout<<"Denominatore: "; cin >> DEN; } while (DEN==0); } void SOMMAFRAZIONI(int N1, int D1, int N2, int D2, int &NUM, int &DEN) //4 parametri di ingresso e 2 parametri di uscita { DEN = MCM(D1,D2); NUM = N1*(DEN / D1)+ N2*(DEN / D2); SEMPLIFICA(NUM,DEN); } void STAMPAFRAZIONE(int NUM, int DEN) //2 parametri di ingresso { cout<<NUM<<"/"<<DEN; } int main() //inizio programma principale { int N1,D1,N2,D2,N,D; //variabili GLOBALI LEGGIFRAZIONE(N1, D1); LEGGIFRAZIONE(N2, D2); SOMMAFRAZIONI(N1,D1,N2,D2,N,D); STAMPAFRAZIONE(N1,D1); cout<<" + "; STAMPAFRAZIONE(N2,D2); cout<<" = "; STAMPAFRAZIONE(N,D); cout<<endl; system("PAUSE"); } //fine programma principale
Pagina 5 di 8
Una sottoprogramma (funzione e procedura) può a sua volta richiamare un'altro sottoprogramma come si vede dal sottoprogramma SOMMAFRAZIONI. È anche possibile che un sottoprogramma (funzione e procedura) richiami direttamente o indirettamente se stesso. In questo caso, dove in un certo momento, nella RAM, saranno in esecuzione più istanze di uno stesso sottoprogramma si parla di programmazione ricorsiva e il sottoprogramma è detto ricorsivo. Esempio di funzione ricorsiva //funzione che calcola MCD int MCD(int X, int Y) { int R; R = X % Y; if (R==0) { return Y; } else { return MCD(Y,R); } }
Esempio di procedura ricorsiva (gioco della torre di HANOI) void HANOI(int N, int P1, int P2) { if (N > 0) { HANOI(N-1, P1, 6-P1-P2); cout<<"Muovi l'anello "<< N<<" dal piolo "<< P1<<" al piolo "<<P2<<endl; HANOI(N-1, 6-P1-P2, P2); } }
Come si vede dagli esempi, definire sottoprogrammi con parametri consente di scrivere sottoprogrammi indipendenti dal contesto in cui vengono utilizzati, questo ne facilità il loro riutilizzo in problemi diversi. Le diverse modalità di passaggio dei parametri e le regole di accesso alle risorse definite nei sottoprogrammi e all'interno del programma stesso fanno parte dalle regole di scoping (regole di visibilità) che sono implementate in modo leggermente diverso nei vari linguaggi di programmazione. Per facilitare il riuso dei sottoprogrammi, i vari linguaggi di programmazione mettono a disposizione degli strumenti, che consentono di raggruppare i sottoprogrammi in librerie. Nel caso del linguaggio di programmazione C++, un programma complesso si suddivide in unità di compilazione, che sono i file che tipicamente hanno suffisso ".cpp", e da più file di intestazione applicativi, che tipicamente hanno suffisso ".h". Nei file di intestazione si pongono le dichiarazioni delle variabili, costanti, funzioni, e tipi globali. Nelle unità di compilazione si pongono le definizioni delle variabili e funzioni globali, nonché dichiarazioni e definizioni di variabili, costanti, funzioni e tipi locali al modulo. Per utilizzare un elemento definito in un file di “intestazione” (header file) e in una unità separata, nel programma principale (main) bisogna includere il file di “intestazione” con la direttiva #include.
Pagina 6 di 8
Come esempio di uso di moduli separati si riporta l’esempio 3, dove le funzioni MCD e MCM, inserite nel modulo, vengono riusate nel programma. //file principale “frazioni_moduli.cpp” #include <iostream> #include <utili.h> using namespace std; void SEMPLIFICA(int &NUM, int &DEN) //2 parametri di ingresso/uscita { //inizio corpo funzione int M; M = MCD(NUM,DEN); if (M>1) { NUM = NUM / M; DEN = DEN / M; } } //fine funzione void LEGGIFRAZIONE(int &NUM, int &DEN) //2 parametri di uscita { cout<<"Numeratore: "; cin >> NUM; do { cout<<"Denominatore: "; cin >> DEN; } while (DEN==0); } void SOMMAFRAZIONI(int N1, int D1, int N2, int D2, int &NUM, int &DEN) //4 parametri di ingresso e 2 parametri di uscita { DEN = MCM(D1,D2); NUM = N1*(DEN / D1)+ N2*(DEN / D2); SEMPLIFICA(NUM,DEN); } void STAMPAFRAZIONE(int NUM, int DEN) //2 parametri di ingresso { cout<<NUM<<"/"<<DEN; //file utili.h } //intestazione o prototipo della funzione che calcola MCD int main() //inizio programma principale int MCD(int X, int Y); { // intestazione o prototipo della funzione che calcola mcm int N1,D1,N2,D2,N,D; //variabili GLOBALI int MCM(int X, int Y); LEGGIFRAZIONE(N1, D1); LEGGIFRAZIONE(N2, D2); SOMMAFRAZIONI(N1,D1,N2,D2,N,D); STAMPAFRAZIONE(N1,D1); cout<<" + "; STAMPAFRAZIONE(N2,D2); cout<<" = "; STAMPAFRAZIONE(N,D); cout<<endl; system("PAUSE"); } //fine programma principale
//file utili.cpp #include <utili.h> //funzione che calcola MCD int MCD(int X, int Y) { int R; do { R = X % Y; X = Y; Y = R; } while (R!=0); return X; } //funzione che calcola mcm int MCM(int X, int Y) { return X*Y/MCD(X,Y); }
Pagina 7 di 8
Come si vede dall’esempio si ha una vera e propria “separazione tra interfaccia (o prototipo) e implementazione (o corpo)”. Il file utili.h deve risiedere in una cartella che deve essere indicata al linker nel menu “Opzioni di Compilazione” (come da figura). Opzionalmente il file utili.cpp può risiedere sia nella stessa cartella del file utili.h, sia nella cartella dove è memorizzato il file del programma principale.
Pagina 8 di 8