GLG Programs fa uso di cookie per migliorare l'esperienza di navigazione degli utenti, ma non per tracciarne un profilo. Proseguendo nella navigazione, si accetta implicitamente l'utilizzo dei cookie.
[OK]Note legaliNon mi importa
Logo GLGPrograms Logo GLGPrograms

Basi di programmazione C++

[Icona della Guida] Introduzione alla programmazione C/C++

Appunti di programmazione C e C++ su GNU/Linux

Indice

Funzioni

Analogia matematica

Come già sappiamo, le funzioni sono una serie di istruzioni già pronte, che possono essere eseguite con una semplice chiamata quando se ne presenta la necessità (vedere il concetto di funzione). Il punto di forza delle funzioni è inoltre dovuto al fatto che è possibile utilizzare quante volte si desidera un codice già scritto, eventualmente fornendo alcune informazioni iniziali, i parametri.

Avendo letto solo il primo capitolo di un qualunque libro di Analisi, o avendo visto un minimo di trigonometria, si sarà già messo a fuoco il concetto di funzione, e inizialmente lo si può considerare per fare alcune analogie e avviare il discorso.

Questa è una funzione matematica che, fornito un numero reale x in ingresso, restituisce il numero reale corrispondente al quadrato di x:

f : R → R
f(x) = x2

Questa, invece, è una funzione analoga in linguaggio C che, fornito un numero "reale" in doppia precisione x in ingresso, restituisce il numero "reale" in doppia precisione corrispondente al quadrato di x:

double f(double x)
{
  double q = x * x;
  return q;
}

Come si può vedere, non è particolarmente difficile. In fondo, una funzione non è altro che una specie di sottoprogramma nel nostro programma, e noi abbiamo già abbastanza familiarità con la funzione speciale main. Il nostro programma può quindi chiamare altri sottoprogrammi, le funzioni, fornendo loro dei dati in ingresso e leggendo le informazioni in uscita.

La funzione chiamante è quella che, appunto, chiama un sottoprogramma, mentre il sottoprogramma è detto funzione chiamata.

Abbiamo già usato alcune funzioni: si pensi a printf e scanf. Noi forniamo i dati in ingresso, ad esempio, ciò che vogliamo stampare, e la funzione si preoccupa, eseguendo delle istruzioni precedentemente preparate da altri programmatori, di mostrare i dati sullo schermo. Altro esempio di funzione è tangente, che a sua volta dipende da due funzioni più elementari, seno e coseno (si ricorda che tan(x) = sin(x) / cos(x) ). Una funzione potrebbe essere anche l'algoritmo per calcolare il codice fiscale: forniti nome, cognome, sesso, data e luogo di nascita, ecco che si può restituire il codice fiscale. Inoltre, in C (e C++), le funzioni sono molto di più: non solo possono avere più parametri in ingresso (Analisi 2) oppure chiamare sé stesse, ma possono modificare attivamente i dati nel chiamante (il loro dominio, volendo continuare con l'analogia matematica), possono non restituire alcun valore, eccetera....

Le funzioni in C

Affinché una funzione possa essere utilizzata in C, è necessario scrivere:

In questa guida sulla programmazione in C, ci limiteremo ad utilizzare un solo file, lo stesso della funzione principale main. I seguenti paragrafi potranno sembrare astratti e poco chiari: si leggano una prima volta, e poi, con calma, si comprendano a fondo per mezzo dell'esempio finale.

Dichiarazione

tipo_di_ritorno nome_funzione(tipo1 [, tipo2]);

dove

Nel seguente esempio, double è il tipo di ritorno della funzione, radice è il suo identificatore, e int è il tipo del primo parametro.

double radice(int);

Definizione

Una volta dichiarata una funzione, è possibile compilare il codice che la utilizza, ma, finché non la si definisce esplicitamente, il linker si rifiuterà di creare un eseguibile, in quanto, senza conoscere le istruzioni che fanno parte della funzione, il programma non può ovviamente funzionare. La sintassi per la definizione è la seguente:

tipo_di_ritorno nome_funzione(tipo1 parametro1 [, tipo2 parametro2, ...])
{
  ...
  return var;
}

dove:

Per comprendere meglio, si osservi il seguente programma commentato:

/* Questo programma definisce una funzione per l'elevamento al quadrato di un numero reale */
#include <iostream>
using namespace std;

double quadrato(double);    /* dichiarazione */

int main()
{
  double n;
  n = quadrato(3.14);       /* chiamata della funzione quadrato.
                               Il valore ritornato viene assegnato alla variabile n */
  cout << n << endl;
  return 0;
}

double quadrato(double x)   /* definizione */
{
  double q = x * x;         /* vengono eseguite le operazioni necessarie a raggiungere il risultato */
  return q;                 /* viene terminata la funzione restituendo il valore di q */
}

Attenzione: le variabili dichiarate all'interno di una funzione, compresa main, non sono visibili da altre funzioni. Pertanto, non è possibile accedere alla variabile n dalla funzione quadrato, e nemmeno alla variabile q dal main. Ne segue che è possibile dichiarare due variabili n (o q) distinte, all'interno di due diverse funzioni, senza che l'una condizioni l'altra. Nell'esempio, inoltre, la variabile q viene definita solo per il tempo di esecuzione di quadrato, dopodiché viene deallocata dopo che la funzione ha ritornato. Si vedranno in maniera più approfondita questi aspetti nel capitolo sulle regole di visibilità.

Dichiarazione + Definizione

Il linguaggio consente di compattare dichiarazione e corpo di una funzione in una singola istruzione: definendo il corpo di una funzione prima che essa venga utilizzata, infatti, è possibile omettere la sua dichiarazione.

Funzioni void

Le funzioni void sono un particolare tipo di funzione che non ritorna nessun valore: per questo, il loro tipo di ritorno speciale è void, cioè nulla. La parola chiave return può essere omessa, ma rimane necessaria se si vuole uscire dalla funzione prima che questa termini.

void stampaSaluto(bool f)
{
  if (! f) return;            /* se f == false, ritorna senza stampare niente */
  cout << "Ciao" << endl;
  return;                     /* return facoltativo */
}

Funzioni inline

Quando viene compilata una funzione, il suo codice macchina viene collocato una volta sola in una regione della memoria principale, ed è accessibile attraverso il suo indirizzo. Quando una funzione è molto piccola e viene chiamata frequentemente (ad esempio, all'interno di un ciclo) dover saltare ogni volta al suo indirizzo per poi tornare indietro può essere sconveniente in termini di tempo.

Le funzioni inline sono funzioni come le altre, con l'unica differenza che il loro codice non risiede in una regione dedicata della memoria, ma, dopo essere stato compilato, viene ricopiato al posto di ogni chiamata alla funzione dal compilatore (anche più volte). Questo permette di aumentare la velocità di esecuzione, a discapito di un maggior uso di memoria per il programma.

Per dichiarare una funzione inline è sufficiente preporre la parola chiave inline alla classica dichiarazione:

inline void funzione();

Parametri di funzioni

Come già detto, una funzione può essere chiamata con più parametri. L'unico modo che il compilatore ha per distinguere i parametri è il loro ordine nella chiamata. Ci si convinca che le due seguenti chiamate alla funzione differenza sono profondamente diverse:

int differenza(int a, int b)
{
  return a - b;
}

int main()
{
  int a = 7, b = 10;
  cout << differenza(10, 7); /*  3 */
  cout << differenza(a, b);  /* -3 */
  cout << differenza(b, a);  /*  3 */
}

Parametri opzionali

È possibile costruire delle funzioni in maniera tale che, se alla loro chiamata vengono omessi dei parametri, essi assumono un valore predefinito: i parametri opzionali.

Per specificare un valore predefinito, è sufficiente indicarlo a fianco del parametro scelto come se fosse una normale espressione di assegnamento:

/* stampa un saluto in Italiano, a meno che non venga specificata un'altra lingua */
void saluto(short lingua = 0)
{
  switch(lingua)
  {
    case 0: cout << "Buongiorno"; break;    /* Italiano */
    case 1: cout << "Good morning"; break;  /* Inglese */
    case 2: cout << "Guten Tag"; break;     /* Tedesco */
    case 3: cout << "Bonjour"; break;       /* Francese */
    case 4: cout << "Buenos dias"; break;   /* Spagnolo */
    default: cout << "Bonan matenon"; break;/* Esperanto */
  }
}

int main()
{
  saluto();   /* saluta in Italiano */
  saluto(1);  /* saluta in Inglese */
  return 0;
}

È possibile dichiarare uno o più parametri opzionali, oppure una combinazione di parametri obbligatori e parametri opzionali. I parametri opzionali devono sempre essere dichiarati dopo i parametri obbligatori. Inoltre, in presenza di più parametri opzionali, non è possibile chiamare la funzione specificando il valore per un parametro opzionale senza averlo specificato anche per quelli alla sua sinistra. Si osservi:

void funzione(int a, int b, int c = 1, int d = 2)
{ ... }

int main()
{
  /* Chiamate non valide */
  funzione();           /* mancano i parametri obbligatori */
  funzione(3);          /* manca un parametro obbligatorio */
  funzione(3, 4, , 6);  /* non si può saltare un parametro opzionale:
                           per specificare d si deve obbligatoriamente specificare anche c */

  /* Chiamate valide */
  funzione(3, 4);       /* a = 3, b = 4, c = 1, d = 2 */
  funzione(3, 4, 5);    /* a = 3, b = 4, c = 5, d = 2 */
  funzione(3, 4, 5, 6); /* a = 3, b = 4, c = 5, d = 6 */
}

Passaggio con puntatore

Negli esempi precedenti abbiamo sempre passato i parametri ad una funzione per copia. Nel passaggio per copia l'oggetto passato viene letteralmente copiato sullo stack, e viene messo a disposizione della funzione: quando la funzione termina, viene deallocato e il suo valore, eventualmente modificato dalla funzione chiamata, viene perso.

Ci sono due casi in cui è obbligatorio passare il puntatore al parametro anziché il parametro:

Passaggio di un array

Un array è caratterizzato dal puntatore al suo primo elemento, ma anche da una lunghezza che, se nel main era accessibile, nella funzione non lo è più: quindi, affinché la funzione possa lavorare sull'array, si devono passare entrambe le informazioni.

#include <iostream>
using namespace std;

/* int* è puntatore a int, cioè l'identificatore del vettore */
void stampaVettore(const int* vettore, const int lunghezza)
{
  for (int i = 0; i < lunghezza; ++i) /* stampa il vettore */
    cout << vettore[i] << "\t";
}

int main()
{
  /* legge le misurazioni */
  cout << "Inserire numero di campioni: ";
  int n;
  cin >> n;

  int* temperatura = new int[n];
  for (int i = 0; i < n; ++i) cin >> temperatura[i];

  /* stampa le misurazioni chiamando la funzione */
  stampaVettore(temperatura, n);
  return 0;
}

Osservazione: quando si passa un puntatore ad una funzione, si comunica ad essa il reale indirizzo in memoria dell'oggetto puntato: questa è quindi potenzialmente in grado di modificarne il contenuto. Se questo è, in alcuni casi, un comportamento desiderato (come vedremo adesso), in altri può portare a degli errori di programmazione. Utilizzando opportunamente la parola chiave const nella definizione della funzione, il pericolo è scongiurato.

Osservazione: nell'esempio abbiamo visto come passare un array, il che è abbastanza immediato sapendo che il suo identificatore è già un puntatore. E se volessimo invece passare una variabile, ad esempio un intero? Niente di più facile: sarà sufficiente passare alla funzione il suo indirizzo per mezzo dell'operatore &.

Modifica dei dati nel chiamante

Avremmo già potuto farlo omettendo la parola chiave const e dereferenziando opportunamente il puntatore. Ecco una tipica funzione di scambio:

/* scambia il contenuto di due interi */
#include <iostream>
using namespace std;

void scambia(int* a, int* b)
{
  int t = *a;
  *a = *b;
  *b = t;
}

int main()
{
  int a = 9;
  int b = 7;
  cout << a "\t" << b << endl;
  scambia(&a, &b);
  cout << a "\t" << b << endl;
  return 0;
}

Riferimenti

I riferimenti, introdotti nel C++, consentono di utilizzare i puntatori in maniera trasparente. Un codice scritto facendo uso di puntatori viene reso molto più snello e di facile lettura se riscritto con i riferimenti: essi infatti non necessitano di dereferenziazione. Semplificando il concetto al puro aspetto estetico del codice, attraverso l'uso di un riferimento è possibile creare una sorta di alias, un nome alternativo, che può essere usato indipendentemente dal nome vero per operare su uno stesso oggetto.

Come i puntatori, anche i riferimenti sono tipizzati, e si definiscono aggiungendo una & come suffisso del tipo, ad esempio tipo&. A differenza di essi, però, si comportano come delle costanti: possono puntare ad un solo oggetto e devono quindi essere inizializzati al momento della definizione.

#include <iostream>
using namespace std;

int main()
{
  /* i e j, pur essendo identificatori diversi, si riferiscono trasparentemente allo stesso oggetto. */
  int i = 10;
  int& j = i;

  cout << "i\tj" << endl;
  cout << i << "\t" << j << endl;
  i = 9;
  cout << i << "\t" << j << endl;
  j = 8;
  cout << i << "\t" << j << endl;

  return 0;
}

Passaggio per riferimento

I riferimenti sono molto comodi per il passaggio di oggetti a funzioni. Il codice che scambia i due interi può essere riscritto così in maniera più leggibile e senza far uso di sporchi simboli:

#include <iostream>
using namespace std;

void scambia(int& a, int& b) /* a e b diventano semplicemente nomi alternativi per x e y */
{
  int t = a;
  a = b;
  b = t;
}

int main()
{
  int x = 1;
  int y = 2;

  scambia(x, y);
  cout << x << "\t" << y << endl;

  return 0;
}

Regole di visibilità

Alcune regole di visibilità sono già state accennate nel corso di questa guida, ma, iniziando a lavorare con le funzioni, è necessario approfondirle. Innanzitutto, è possibile fare una suddivisione macroscopica tra variabili locali e variabili globali:

Nota: in un blocco figlio è possibile dichiarare una nuova variabile con lo stesso nome di una variabile del blocco padre. Così facendo, la variabile del padre viene nascosta e non è più accessibile all'interno del blocco figlio; la variabile originaria non viene comunque sovrascritta e ritorna accessibile non appena termina il blocco figlio.

Nota: attraverso l'utilizzo dell'operatore :: è possibile accedere alle variabili globali, risalendo in un solo colpo tutti i livelli di blocchi. Non è possibile risalire solo di alcuni livelli.

Segue un programma di esempio. Compilarlo e capirlo facendo attenzione al suo output.

#include <iostream>
using namespace std;

void funzione(void);

int a = 0;              /* variabile globale */

int main()
{
  /* riferimenti alla variabile globale 'a' */
  cout << a << endl;
  a = 1;
  cout << a << endl;
  /* una nuova variabile 'a' (locale) viene definita e nasconde la globale */
  int a;
  a = 2;
  cout << a << endl;
  /* risoluzione di visibilità globale */
  cout << ::a << endl;

  /* definizione di una variabile locale */
  int b;
  b = 3;
  cout << b << endl;
  {
    cout << b << endl;
    /* una nuova variabile 'b' (locale al blocco) viene definita e nasconde la 'b' locale al blocco padre */
    int b;
    b = 4;
    cout << b << endl; /* Osservazione: si noti che non è possibile utilizzare l'operatore ::
                          per risalire di un solo livello */
  } /* termina la vita di 'b', e quella del blocco padre è nuovamente visibile */
  cout << b << endl;

  funzione();

  return 0;
}

void funzione()
{
  cout << a << endl; /* variabile globale */
  /* 'b' non è accessibile */
  return;
}

static

Attraverso la parola chiave static è possibile dichiarare variabili con comportamenti e visibilità particolare.

Esempio di variabile statica in una funzione. Al termine della funzione, la variabile c non viene deallocata.

#include <iostream>
using namespace std;

void funzione()
{
  static int c = 0;
  cout << "Funzione chiamata " << ++c << " volte" << endl;
  return;
}

int main()
{
  funzione();
  funzione();
  return 0;
}
Pagina scritta da Giovan BattistaGiovan Battista

Hai una domanda? Scrivici!
Questa pagina ti è piaciuta? Condividila!
Share on Facebook Share on Google+ Share on linkedin