Funzioni

 


 

Definizione di una funzione

 

Una funzione è così definita:

tipo nome(argomenti)
{
            ... istruzioni ... (dette: codice di implementazione della funzione)
}

(notare che la prima istruzione è senza punto e virgola, in quanto é completata dall'ambito che segue)

Gli argomenti vanno specificati insieme al loro tipo (come nelle dichiarazioni delle variabili) e, se più d'uno, separati con delle virgole.

Es.         char MiaFunz(int dato, float valore)
                  la funzione MiaFunz riceve dal programma chiamante gli argomenti:
dato
(di tipo int), e valore (di tipo float), e ritorna un risultato di tipo char

 


 

Dichiarazione di una funzione

 

Se in un file di codice sorgente una funzione é chiamata prima di essere definita, bisogna dichiararla prima di chiamarla.

La dichiarazione di una funzione (detta anche prototipo) é un'unica istruzione, formalmente identica alla prima riga della sua definizione, salvo il fatto che deve terminare con un punto e virgola. Tornando all'esempio precedente la dichiarazione della funzione MiaFunz é:

char MiaFunz(int dato, float valore);

Nella dichiarazione di una funzione i nomi degli argomenti sono fittizi e non é necessario che coincidano con quelli dalla definizione (non é neppure necessario specificarli); invece i tipi sono obbligatori: devono coincidere ed essere nello stesso ordine di quelli della definizione. Es., un'altra dichiarazione valida della funzione MiaFunz é:

char MiaFunz(int, float);

NOTA IMPORTANTE
La tendenza dei programmatori in C++ é di separare le dichiarazioni dalle altre istruzioni di programma: le prime, che possono riguardare non solo funzioni, ma anche costanti predefinite o definizioni di tipi astratti, sono sistemate in header-files (con estensione del nome .h), le seconde in implementation-files (con estensione .c, .cpp o .cxx); ogni implementation-file che contiene riferimenti a funzioni (o altro) dichiarate in header-files, deve includere quest'ultimi mediante la direttiva #include.

[p19] [p19] [p19]

 


 

Istruzione return

 

Nel codice di implementazione di una funzione l'istruzione di ritorno al programma chiamante é:

return espressione;

il valore calcolato dell'espressione viene restituito al programma chiamante come valore di ritorno della funzione (se il suo tipo non coincide con quello dichiarato della funzione, il compilatore segnala un errore, oppure, quando può, esegue una conversione implicita, con warning se c'é pericolo di loss of data)

Non é necessario che tale istruzione sia fisicamente l'ultima (e non é neppure necessario che ve ne sia una sola: dipende dalla presenza delle istruzioni di controllo, che possono interrompere l'esecuzione della funzione in punti diversi). Se la funzione non ha valore di ritorno (tipo void), bisogna specificare return; (da solo). Questa istruzione può essere omessa quando il punto di ritorno coincide con la fine fisica della funzione.

 


 

Comunicazioni fra programma chiamante e funzione

 

  Da programma chiamante a funzione

La chiamata di una funzione non di tipo void può essere inserita come operando in qualsiasi espressione o come argomento nella chiamata di un'altra funzione (in questo caso il compilatore controlla che il tipo della funzione sia ammissibile): la chiamata viene eseguita con precedenza rispetto alle altre operazioni e al suo posto viene sostituito il valore di ritorno restituito dalla funzione.

Il valore di ritorno può non essere utilizzato dal programma chiamante, come se la funzione fosse di tipo void; in questi casi (cioè se la funzione è di tipo void, oppure il valore di ritorno non interessa), la chiamata non può essere inserita in una espressione, ma deve assumere la forma di un'istruzione a se stante.

Quando esegue la chiamata di una funzione, il programma costruisce una copia di ogni argomento, creando delle variabili locali nell'ambito della funzione (passaggio degli argomenti per valore). Ciò significa che tutte le modifiche, fatte dalla funzione al valore di un argomento, hanno effetto soltanto nell'ambito della funzione stessa.

Es.             funzione:   funz(int a)   { ..... a = a+1; .... }
prog. chiamante:         int b = 0 ...... funz(b); .....

il programma, prima di chiamare funz, copia il valore della propria variabile b nell'argomento a, che diventa una variabile locale nell'ambito di funz; per cui a "muore" appena il controllo ritorna al programma e il valore di b resta invariato, qualunque modifica abbia subito a durante l'esecuzione di funz.

A questa regola fa eccezione (per motivi che vedremo in seguito) il caso in cui gli argomenti sono nomi di array (e quindi in particolare di stringhe). Per trasmettere un intero array a una funzione (nel caso di singoli elementi non ci sarebbe eccezione alla regola generale) bisogna inserire nella chiamata il nome dell'array (senza parentesi quadre) e, corrispondentemente nella funzione la dichiarazione di una variabile seguita dalla coppia di parentesi quadre. Non serve specificare la dimensione in quanto la stessa é già stata dichiarata nel programma chiamante (tuttavia, se l'array é multidimensionale l'unico indice che si può omettere é quello all'estrema sinistra). In questa situazione, tutte le modifiche fatte ai singoli elementi dell'array vengono riprodotte sull'array del programma chiamante.

 

  Da funzione a programma chiamante

Quando il controllo torna da una funzione al programma chiamante, tramite l'istruzione: return espressione;, il programma costruisce una copia del valore calcolato dell'espressione (che "muore" appena termina la funzione), creando un valore locale nell'ambito del programma chiamante.

Es.             nel programma chiamante:     ..... int a = funz(); .......
nella funzione: int funz() { ...... return b; .... }

funz restituisce al programma non la variabile b (che, in quanto locale in funz muore appena funz termina), ma una sua copia, che sopravvive a funz e diventa un valore locale (temporaneo, cioè non identificato da un nome) del programma chiamante, assegnato alla variabile a.

[p20]

 


 

Argomenti di default

 

In C++ é consentito "inizializzare" un argomento: come conseguenza, se nella chiamata l'argomento é omesso, il suo valore é assunto, di default, uguale alla costante (o variabile globale) usata per l'inizializzazione. Questa deve essere fatta un'unica volta (e quindi in generale nel prototipo della funzione, ma non nella sua definizione). Es.
prototipo: void scrive(char [ ] = "Messaggio di saluto");
chiamata: scrive();            equivale a: scrive("Messaggio di saluto");
definizione:     void scrive(char ave[ ] )  { ............ }

Se una funzione ha diversi argomenti, di cui alcuni required (da specificare) e altri di default, quelli required devono precedere tutti quelli di default.

 


 

Funzioni con overload

 

A differenza dal C, il C++ consente l'esistenza di più funzioni con lo stesso nome, che sono chiamate: "funzioni con overload". Il compilatore distingue una funzione dall'altra in base alla lista degli argomenti: due funzioni con overload devono differire per il numero e/o per il tipo dei loro argomenti.

Es.               funz(int); e funz(float);         verranno chiamate con lo stesso nome funz, ma sono in realtà due funzioni diverse, in quanto la prima ha un argomento int, la seconda un argomento float.

Non sono ammesse funzioni con overload che differiscano solo per il tipo del valore di ritorno ; né sono ammesse funzioni che differiscano solo per argomenti di default.
Es.                       void funz(int);   e    int funz(int);

non sono accettate, in quanto generano ambiguità: infatti, in una chiamata tipo funz(n), il programma non saprebbe se trasferirsi alla prima oppure alla seconda funzione (non dimentichiamo che il valore di ritorno può non essere utilizzato).

Es.                         funz(int);   e    funz(int, double=0.0);

non sono accettate, in quanto generano ambiguità: infatti, in una chiamata tipo funz(n), il programma non saprebbe se trasferirsi alla prima funzione (che ha un solo argomento), oppure alla seconda (che ha due argomenti, ma il secondo può essere omesso per default).

La tecnica dell'overload, comune sia alle funzioni che agli operatori, é molto usata in C++, perché permette di programmare in modo semplice ed efficiente: funzioni che eseguono operazioni concettualmente simili possono essere chiamate con lo stesso nome, anche se lavorano su dati diversi.
Es., per calcolare il valore assoluto di un numero, qualunque sia il suo tipo, si potrebbe usare sempre una funzione con lo stesso nome (per esempio abs).

[p21]

 


 

Funzioni inline

 

In C++ esiste la possibilità di chiedere al compilatore di espandere ogni chiamata di una funzione con il codice di implementazione della funzione stessa. Questo si ottiene premettendo alla definizione di una funzione lo specificatore inline.

Es.                       inline double cubo(double x)   { return x*x*x ; }

ogni volta che il compilatore trova nel programma la chiamata: cubo(espressione); la trasforma nell'istruzione : (espressione) * (espressione) * (espressione) ;

L'uso dello specificatore inline é molto comune, in quanto permette di eliminare il sovraccarico di lavoro dovuto alla gestione della comunicazione fra programma e funzione. Se però il numero di chiamate della funzione é molto elevato ed é in punti diversi del programma, il vantaggio potrebbe essere annullato dall'eccessivo accrescimento della lunghezza del programma (il vantaggio invece é evidente quando vi sono poche chiamate ma inserite in cicli while o for: in questo caso lo specificatore inline fa crescere di poco la dimensione del programma, ma il numero delle chiamate in esecuzione può essere molto elevato).

In ogni caso il compilatore si riserva il diritto di accettare o rifiutare lo specificatore inline: in pratica, una funzione che consista di più di 4 o 5 righe di istruzioni viene compilata come funzione separata, indipendentemente dalla presenza o meno dello specificatore inline.

 


 

Trasmissione dei parametri tramite l'area stack

 

  Cenni sulle liste

In qualsiasi linguaggio di programmazione le liste di dati possono essere accessibili in vari modi (per esempio in modo randomatico), ma esistono due particolari categorie di liste caratterizzate da metodi di accesso ben definiti e utilizzate in numerose circostanze:

 

  Uso dell'area stack

Nella trasmissione dei parametri fra programma chiamante e funzione vengono utilizzate liste di tipo stack: quando una funzione A chiama una funzione B, sistema in un'area di memoria, detta appunto stack, un pacchetto di dati, comprendenti:

  1. l'area di memoria per tutte le variabili automatiche di B;

  2. la lista degli argomenti di B in cui copia i valori trasmessi da A;

  3. l'indirizzo di rientro in A (cioè il punto di A in cui il  programma deve tornare una volta completata l'esecuzione di B, trasferendovi l'eventuale valore di ritorno).

La funzione B utilizza tale pacchetto e, se a sua volta chiama un'altra funzione C, sistema nell'area stack un altro pacchetto, "impilato" sopra il precedente, come nel seguente schema (tralasciamo le aree riservate alle variabili automatiche):

Area stack Commenti
Indirizzo di rientro in B
Argomento 1 passato a C
Argomento
2 passato a C
      La funzione B chiama la funzione C con due argomenti
Indirizzo di rientro in A
Argomento 1 passato a B
Argomento
2 passato a B
Argomento 3 passato a B
      La funzione A chiama la funzione B con tre argomenti

Quando il controllo deve tornare da C a B, il programma fa riferimento all'ultimo pacchetto entrato nello stack per conoscere l'indirizzo di rientro in B e, eseguita tale operazione, rimuove lo stesso pacchetto dallo stack (cancellando di conseguenza anche le variabili automatiche di C).
La stessa cosa succede quando il controllo rientra da B in A; dopodiché lo stack rimane vuoto.

 


 

Ricorsività delle funzioni

 

Tornando all'esempio precedente, la trasmissione dei parametri attraverso lo stack garantisce che il meccanismo funzioni comunque, sia che A, B e C siano funzioni diverse, sia che si tratti della stessa funzione (ogni volta va a cercare nello stack l'indirizzo di rientro nel programma chiamante e quindi non cambia nulla se tale indirizzo si trova all'interno della stessa funzione).

Ne consegue che in C++ (come in C) le funzioni possono chiamare se stesse (ricorsività delle funzioni). Ovviamente tali funzioni devono sempre contenere un'istruzione di controllo che, se si verificano certe condizioni, ha il compito di interrompere la successione delle chiamate.

Esempio tipico di una funzione chiamata ricorsivamente è quello del calcolo del fattoriale di un numero intero:

int fact(int n) {    
if ( n <= 1 ) return 1; <----- istr. di controllo
return n * fact(n-1);    }

alternativamente, cioè senza usare la ricorsività, si produce codice meno compatto:

int fact(int n)   {
int i = 2, m = 1;
while ( i <= n ) m *= i++ ;
return m;          }

[p22]

 


 

Funzioni con numero variabile di argomenti

 

In C++ (come in C), tramite accesso allo stack, è possibile gestire funzioni con numero variabile di argomenti. Caso tipico é la nota funzione printf, che ha un solo argomento fisso (la control-string), seguito eventualmente dagli argomenti opzionali (i dati da scrivere), il cui numero è determinato in fase di esecuzione, esaminando il contenuto della stessa control-string.

Le funzioni con numero variabile di argomenti vanno dichiarate e definite con tre puntini (ellipsis) al posto della lista degli argomenti opzionali, che devono sempre seguire quelli fissi (deve sempre esistere almeno un argomento fisso).

Es.:                   int funzvar(int a, float b, ...)
gli argomenti fissi della funzione funzvar sono due: a e b; a questi possono seguire altri argomenti (in numero qualsiasi). Normalmente gli argomenti fissi contengono l'informazione (come nella printf) sull'effettivo numero di argomenti usati in una chiamata.

La funzione può accedere al suo pacchetto di chiamata, contenuto nello stack, per mezzo di alcune funzioni di libreria, i cui prototipi si trovano nell'header-file <stdarg.h> ; per memorizzare i valori degli argomenti opzionali trasmessi dal programma chiamante, la funzione deve procedere nel seguente modo:

  1. anzitutto deve definire una variabile, di tipo (astratto) va_list (creato in <stdarg.h>), che serve per accedere alle singole voci dello stack
    Es. :                         va_list marker ;

  2. poi deve chiamare la funzione di libreria va_start, per posizionarsi nello stack sull'inizio degli argomenti opzionali.
    Es. :                         va_start(marker,b) ;           dove b é l'ultimo degli argomenti fissi;

  3. poi, per ogni argomento opzionale che si aspetta di trovare, deve chiamare la funzione di libreria va_arg
    Es. :                         c = va_arg(marker,int) ;
    (notare che il secondo argomento di va_arg definisce il tipo dell'argomento opzionale, il cui valore sarà trasferito in c).

  4. infine deve chiamare la funzione di libreria va_end per chiudere le operazioni
    Es. :                         va_end(marker) ;

[p23]

 


 

Cenni sulla Run Time Library

 

  La libreria standard del C

La Run Time Library è la libreria standard del C, usata anche dal C++, e contiene diverse centinaia di funzioni.

Il codice di implementazione delle funzioni di libreria è fornito in forma già compilata e risiede in files binari (.obj o .lib), mentre i prototipi sono disponibili in formato sorgente e si trovano distribuiti in vari header-files (.h).

Il linker, lanciato da un ambiente di sviluppo, accede in genere automaticamente ai codici binari della libreria. Il compilatore, invece, richiede che tutte le funzioni usate in ogni file sorgente di un'applicazione siano espressamente dichiarate, tramite inclusione dei corrispondenti header-files.

 

  Principali categorie di funzioni della Run-time library

Elenchiamo le principali categorie in cui possono essere classificate le funzioni della Run Time Library. Per informazioni sulle funzioni individualmente consultare l'help dell'ambiente di sviluppo disponibile.

Categorie Header-files
Operazioni di Input/Output

<io.h> , <stdio.h>

Funzioni matematiche e statistiche

<math.h> , <stdlib.h>

Attributi del carattere

<ctype.h>

Conversioni numeri-stringhe

<stdlib.h>

Gestione e manipolazione stringhe

<string.h>

Gestione dell'ambiente

<direct.h> , <stdlib.h>

Gestione degli errori

<stdio.h> , <stdlib.h>

Ricerca e ordinamento dati

<search.h> , <stdlib.h>

Gestione della data e dell'ora

<time.h>

Gest. numero variabile di argomenti      

<stdarg.h>

 


 

Torna all'Indice