CATEGORII DOCUMENTE |
Bulgara | Ceha slovaca | Croata | Engleza | Estona | Finlandeza | Franceza |
Germana | Italiana | Letona | Lituaniana | Maghiara | Olandeza | Poloneza |
Sarba | Slovena | Spaniola | Suedeza | Turca | Ucraineana |
DOCUMENTE SIMILARE |
|
Come ogni moderno linguaggio, sia il C che il C++ consentono di dichiarare sottoprogrammi che possono essere invocati nel corso dell'esecuzione di una sequenza di istruzioni a partire da una sequenza principale (il corpo del programma). Nel caso del C e del C++ questi sottoprogrammi sono chiamati funzioni e sono simili alle funzioni del Pascal. Anche il corpo del programma È modellato tramite una funzione il cui nome deve essere sempre main (vedi esempio “Dichiarazioni”).
Una funzione C/C++, analogamente ad una funzione Pascal, È caratterizzata da un nome che la distingue univocamente nel suo scope (le regole di visibilitÀ di una funzione sono analoghe a quelle viste per le variabili), da un insieme (eventualmente vuoto) di argomenti (parametri della funzione) separati da virgole, e eventualmente il tipo del valore ritornato:
// ecco una funzione che riceve due interi
// e restituisce un altro intero
int Sum(int a, int b);
Gli argomenti presi da una funzione sono quelli racchiusi tra le parentesi tonde, si noti che il tipo dell'argomento deve essere specificato singolarmente per ogni argomento anche quando piÙ argomenti hanno lo stesso tipo; la seguente dichiarazione È pertanto errata:
int Sum2(int a, b); // ERRORE!
Il tipo del valore restituito dalla funzione deve essere specificato prima del nome della funzione e se omesso si sottintende int; se una funzione non ritorna alcun valore va dichiarata void, come mostra il seguente esempio:
// ecco una funzione che non
// ritorna alcun valore
void Foo(char a, float b);
Non È necessario che una funzione abbia dei parametri, in questo caso basta non specificarne oppure indicarlo esplicitamente:
// funzione che non riceve parametri
// e restituisce un int (default)
Funny();
// oppure
Funny2(void);
Il primo esempio vale solo per il C++, in C non specificare alcun argomento equivale a dire 'Qualsiasi numero e tipo di argomenti'; il secondo metodo invece È valido in entrambi i linguaggi, in questo caso void assume il significato 'Nessun argomento'.
Anche in C++ È possibile avere funzioni con numero e tipo di argomenti non specificato:
void Esempio1();
void Esempio2(int Args, );
Il primo esempio mostra come dichiarare una funzione che prende un numero imprecisato (eventualmente 0) di parametri; il secondo esempio invece mostra come dichiarare funzioni che prendono almeno qualche parametro, in questo caso bisogna prima specificare tutti i parametri necessari e poi mettere per indicare eventuali altri parametri.
Quelli che abbiamo visto finora comunque non sono definizioni di funzioni, ma solo dichiarazioni, o per utilizzare un termine proprio del C++, prototipi di funzioni.
I prototipi di funzione, assenti nel C, sono stati introdotti nel C++ per informare il compilatore dell'esistenza di una certa funzione e consentire un maggior controllo sulle chiamate di funzione al fine di identificare errori di tipo (e non solo) e sono utilizzati soprattutto all'interno dei file header per la suddivisione di grossi programmi in piÙ file e la realizzazione di librerie di funzioni, infine nei prototipi non È necessario indicare il nome degli argomenti della funzione:
// la funzione Sum vista sopra poteva
// essere dichiarata anche cosÌ:
int Sum(int, int);
Per implementare (definire) una funzione occorre ripetere il prototipo, specificando il nome degli argomenti (necessario per poter riferire ad essi, ma non obbligatorio se l'argomento non viene utilizzato), seguito da una sequenza di istruzioni racchiusa tra parentesi graffe:
int Sum(int x, int y)
La funzione Sum È costituita da una sola istruzione che calcola la somma degli argomenti e restituisce tramite la keyword return il risultato di tale operazione. Inoltre, benché non evidente dall'esempio, la keyword return provoca l'immediata terminazione della funzione; ecco un esempio non del tutto corretto, che perÃ’ mostra il comportamento di return:
// calcola il quoziente di due numeri
int Div(int a, int b)
Se il divisore È 0, la prima istruzione return restituisce (erroneamente) una stringa (anziché un intero) e provoca la terminazione della funzione, le successive istruzioni della funzione quindi non verrebbero eseguite.
Concludiamo questo paragrafo con alcune considerazioni:
La definizione di una funzione non deve essere seguita da ; (punto e virgola), ciÃ’ tra l'altro consente di distinguere facilmente tra prototipo (dichiarazione) e definizione di funzione: un prototipo lo si riconosce perché la lista di argomenti di una funzione È seguita da ; (punto e virgola), mentre in una definizione la lista di argomenti È seguita da // ERRORE! Nessun valore restituito.
int Sum(int a, int b) // ERRORE! Nessun valore restituito.
int Sum(int a, int b) // OK!
void Sleep(int a) ;
} // OK!
void Sleep(int Delay) ;
return;
} // OK!
La chiamata di una funzione puÒ essere eseguita solo nell'ambito dello scope in cui appare la sua dichiarazione (come giÀ detto le regole di scoping per le dichiarazioni di funzioni sono identiche a quelle per le variabili) specificando il valore assunto da ciascun parametro formale:
void Sleep(int Delay); // definita da qualche parte
int Sum(int a, int b); // definita da qualche parte
void main(void)
La prima e l'ultima chiamata di funzione mostrano come le funzioni void (nel nostro caso Sleep) siano identiche alle procedure Pascal, in particolare l'ultima istruzione È un errore poiché Sleep non restituisce alcun valore.
La seconda chiamata di funzione (la prima di Sum) mostra come recuperare il valore restituito dalla funzione (esattamente come in Pascal). La chiamata successiva invece potrebbe sembrare un errore, in realtÀ si tratta di una chiamata lecita, semplicemente il valore tornato da Sum viene scartato; l'unico motivo per scartare il risultato dell'invocazione di una funzione È quello di sfruttare eventuali effetti laterali di tale chiamata.
I parametri di una funzione si comportano all'interno del corpo della funzione come delle variabili locali e possono quindi essere usati anche a sinistra di un assegnamento (per quanto riguarda le variabili locali ad una funzione, si rimanda al capitolo III, paragrafo 3):
void Assign(int a, int b)
tuttavia qualsiasi modifica ai parametri formali (quelli cioÈ che compaiono nella definizione, nel nostro caso a e b) non si riflette (per quanto visto finora) automaticamente sui parametri attuali (quelli effettivamente usati in una chiamata della funzione):
#include <iostream.h>
void Assign(int a, int b)
void main()
L'esempio appena visto È perfettamente funzionante e se eseguito mostrerebbe come la funzione Assign, pur eseguendo una modifica ai suoi parametri formali, non modifichi i parametri attuali.
Questo comportamento È perfettamente corretto in quanto i parametri attuali vengono passati per valore: ad ogni chiamata della funzione viene cioÈ creata una copia di ogni parametro locale alla funzione stessa; tali copie vengono distrutte quando la chiamata della funzione termina ed il loro contenuto non viene copiato nelle eventuali variabili usate come parametri attuali.
In alcuni casi tuttavia puÒ essere necessario fare in modo che la funzione possa modificare i suoi parametri attuali, in questo caso È necessario passare non una copia, ma un riferimento o un puntatore e agire su questo per modificare una variabile non locale alla funzione. Per adesso non considereremo queste due possibilitÀ, ma rimanderemo la cosa al capitolo successivo non appena avremo parlato di puntatori e reference.
A volte siamo interessati a funzioni il cui comportamento È pienamente definito anche quando in una chiamata non tutti i parametri sono specificati, vogliamo cioÈ essere in grado di avere degli argomenti che assumano un valore di default se per essi non viene specificato alcun valore all'atto della chiamata. Ecco come fare:
int Sum (int a = 0, int b = 0)
Quella che abbiamo appena visto È la definizione della funzione Sum ai cui argomenti sono stati associati dei valori di default (in questo caso 0 per entrambi gli argomenti), ora se la funzione Sum viene chiamata senza specificare il valore di a e/o b il compilatore genera una chiamata a Sum sostituendo il valore di default (0) al parametro non specificato.
Una funzione puÒ avere piÙ argomenti di default, ma le regole del C++ impongono che tali argomenti siano specificati alla fine della lista dei parametri formali nella dichiarazione della funzione:
void Foo(int a, char b = 'a') // Ok!
void Foo2(int a, int c = 4, float f) // Errore!
void Foo3(int a, float f, int c = 4) // Ok!
La dichiarazione di Foo2 È errata perché una volta che È stato specificato un argomento con valore di default, tutti gli argomenti seguenti (in questo caso f) devono possedere un valore di default; l'ultima definizione mostra come si sarebbe dovuto definire Foo2 per non ottenere errori.
La risoluzione di una chiamata di una funzione con argomenti di default naturalmente differisce da quella di una funzione senza argomenti di default in quanto sono necessari un numero di controlli maggiori:
sostanzialmente se nella chiamata per ogni parametro formale È specificato un parametro attuale, allora il valore di ogni parametro attuale viene copiato nel corrispondente parametro formale sovrascrivendo eventuali valori di default; se invece qualche parametro non viene specificato, quelli forniti specificano il valore dei parametri formali secondo la loro posizione e per i rimanenti parametri formali viene utilizzato il valore di default specificato (se nessun valore di default È stato specificato, viene generato un errore):
// riferendo alle precedenti definizioni:
Foo(1, 'b'); // chiama Foo con argomenti 1 e 'b'
Foo(0); // chiama Foo con argomenti 0 e 'a'
Foo('c'); // ?????
Foo3(0); // Errore, mancano parametri!
Foo3(1, 0.0); // chiama Foo3(1, 0.0, 4)
Foo3(1, 1.4, 5); // chiama Foo3(1, 1.4, 5)
Degli esempi appena fatti, il quarto, Foo3(0), È un errore poiché non viene specificato il valore per il secondo argomento della funzione (che non possiede un valore di default); È invece interessante il terzo (Foo('c');): apparentemente potrebbe sembrare un errore, in realtÀ quello che il compilatore fa È convertire il parametro attuale 'c' di tipo char in uno di tipo int e chiamare la funzione sostituendo al primo parametro il risultato della conversione di 'c' al tipo int.
La conversione di tipo sarÀ oggetto di una apposita appendice.
Come giÀ precedentemente accennato, anche il corpo di un programma C/C++ È modellato come una funzione. Tale funzione ha un nome predefinito, main, e viene invocata automaticamente dal sistema quando il programma viene eseguito.
Per adesso possiamo dire che la struttura di un programma È sostanzialmente la seguente:
< Dichiarazioni globali e dichiarazioni di funzioni >
int main(int argc, char* argv[ ])
Un programma È dunque costituito da un insieme (eventualmente vuoto) di dichiarazioni globali di costanti, variabili e di dichiarazioni di funzioni (che non possono essere dichiarate localmente ad altre funzioni), infine il corpo del programma È costituito dalla funzione main, il cui prototipo per esteso È mostrato nello schema riportato sopra.
Nello schema main ritorna un valore di tipo int (che generalmente È utilizzato per comunicare al sistema operativo la causa della terminazione), ma puÒ essere dichiarata void o in teoria tornare un tipo qualsiasi.
Inoltre main puÒ accettare opzionalmente due parametri: il primo È di tipo int e indica il numero di parametri presenti sulla riga di comando attraverso cui È stato eseguito il programma; il secondo parametro (si comprenderÀ in seguito) È un array di stringhe terminate da zero (puntatori a caratteri) contenente i parametri, il primo dei quali (argv[0]) È il nome del programma come riportato sulla riga di comando.
#include <iostream.h>
void main(int argc, char * argv[ ])
Il precedente esempio mostra come accedere ai parametri passati sulla riga di comando; si provi a compilare e ad eseguirlo specificando un numero qualsiasi di parametri, l'output dovrebbe essere simile a:
> test a b c d // questa È la riga di comando
Riga di comando: TEST.EXE
Parametro 1 = a
Parametro 2 = b
Parametro 3 = c
Parametro 4 = d
Le funzioni consentono di scomporre in piÙ parti un grosso programma facilitandone la realizzazione (e anche la manutenzione), tuttavia spesso si È indotti a rinunciare a tale beneficio perché l'overhead imposto dalla chiamata di una funzione È tale da sconsigliare la realizzazione di piccole funzioni.
Le possibili soluzioni in C erano due:
1.Rinunciare alle funzioni piccole, tendendo a scrivere solo poche funzioni corpose;
2.Ricorrere alle macro;
La prima in realtÀ È una pseudo-soluzione e porta spesso a programmi difficili da capire e mantenere
perché in pratica rinuncia ai benefici delle funzioni; la seconda soluzione invece potrebbe andare bene in
C, ma non in C++: una macro puÒ essere vista come una funzione il cui corpo È sostituito (espanso) dal
preprocessore in luogo di ogni chiamata.
Il problema principale È che questo sistema rende difficoltoso se non impossibile ogni controllo statico di tipo; in C tutto sommato ciÃ’ non costituisce un grave problema perché il compilatore C non esegue controlli di tipo, tuttavia il C++ È un linguaggio fortemente tipizzato e l'uso di macro costituisce un grave ostacolo a tali controlli.
Per non rinunciare ai vantaggi forniti dalle (piccole) funzioni e a quelli forniti da un controllo statico dei tipi, sono state introdotte nel C++ le funzioni inline.
Quando una funzione viene definita inline il compilatore ne memorizza il corpo e, quando incontra una chiamata a tale funzione, semplicemente sostituisce alla chiamata della funzione il corpo; tutto ciÒ consente di evitare l'overhead della chiamata e, dato che la cosa È gestita dal compilatore, permette di eseguire tutti i controlli statici di tipo.
Se si desidera che una funzione sia espansa inline dal compilatore, occorre definirla esplicitamente inline:
inline int Sum(int a, int b)
La keyword inline informa il compilatore che si desidera che la funzione Sum sia espansa inline ad ogni chiamata; tuttavia ciÒ non vuol dire che la cosa sia sempre possibile: molti compilatori non sono in grado di espandere inline qualsiasi funzione, tipicamente le funzioni ricorsive sono molto difficili da trattare e il mio compilatore non riesce ad esempio a espandere funzioni contenenti cicli. In questi casi viene generata una normale chiamata di funzione e al piÙ si viene avvisati che la funzione non puÒ essere espansa inline.
Si osservi che, per come sono trattate le funzioni inline, non ha senso utilizzare la keyword inline in un prototipo di funzione perché il compilatore necessita del codice contenuto nel corpo della funzione:
inline int Sum(int a, int b);
int Sum(int a, int b)
In questo caso non viene generato alcun errore, ma la parola chiave inline specificata nel prototipo viene del tutto ignorata; perché abbia effetto inline deve essere specificata nella definizione della funzione:
int Sum(int a, int b);
inline int Sum(int a, int b) // Ora È tutto ok!
Un'altra cosa da tenere presente È che il codice che costituisce una funzione inline deve essere disponibile prima di ogni uso della funzione, altrimenti il compilatore non È in grado di espanderla (non sempre almeno!). Una funzione ordinaria puÃ’ essere usata anche prima della sua definizione, poiché È il linker che si occupa di risolvere i riferimenti (il linker del C++ lavora in due passate); nel caso delle funzioni inline, poiché il lavoro È svolto dal compilatore (che lavora in una passata), non È possibile risolvere correttamente il riferimento.
Una importante conseguenza di tale limitazione È che una funzione puÒ essere inline solo nell'ambito del file in cui È definita, se un file riferisce ad una funzione definita inline in un altro file (come, lo vedremo piÙ avanti), in questo file (il primo) la funzione non potrÀ essere espansa inline; esistono comunque delle soluzioni al problema.
Le funzioni inline consentono quindi di conservare i benefici delle funzioni anche in quei casi in cui le prestazioni sono fondamentali, bisogna perÃ’ valutare attentamente la necessitÀ di rendere inline una funzione, un abuso potrebbe portare a programmi difficili da compilare (perché È necessaria molta memoria) e voluminosi in termini di dimensioni del file eseguibile.
Il termine overloading (da to overload) significa sovraccaricamento e nel contesto del C++ overloading delle funzioni indica la possibilitÀ di attribuire allo stesso nome di funzione piÙ significati. Attribuire piÙ significati significa fare in modo che lo stesso nome di funzione sia in effetti utilizzato per piÙ funzioni contemporaneamente.
Un esempio di overloading ci viene dalla matematica, dove con spesso utilizziamo lo stesso nome di funzione con significati diversi senza starci a pensare troppo, ad esempio + È usato sia per indicare la somma sui naturali che quella sui reali
Ritorniamo per un attimo alla nostra funzione Sum; per come È stata definita, Sum funziona solo sugli interi e non È possibile utilizzarla sui float. Quello che vogliamo È riutilizzare lo stesso nome, attribuendogli un significato diverso e lasciando al compilatore il compito di capire quale versione della funzione va utilizzata di volta in volta. Per fare ciÒ basta definire piÙ volte la stessa funzione:
int Sum(int a, int b); // per sommare due interi
float Sum(float a, float b); // per sommare due float
float Sum(float a, int b); // per la somma di un
float Sum(int a, float b); // float e un intero
Nel nostro esempio ci siamo limitati solo a dichiarare piÙ volte la funzione Sum, ogni volta con un significato diverso (uno per ogni possibile caso di somma in cui possono essere coinvolti, anche contemporaneamente, interi e reali); È chiaro che poi da qualche parte deve esserci una definizione per ciascun prototipo (nel nostro caso tutte le definizioni sono identiche a quella giÀ vista, cambia solo l'intestazione della funzione).
In alcune vecchie versioni del C++ l'intenzione di sovraccaricare una funzione doveva essere esplicitamente comunicata al compilatore tramite la keyword overload:
overload Sum; // ora si puÃ’ sovraccaricare Sum:
int Sum(int a, int b); // per sommare due interi
float Sum(float a, float b); // per sommare due float
float Sum(float a, int b); // per la somma di un
float Sum(int a, float b); // float e un intero
Comunque si tratta di una pratica obsoleta che non va piÙ utilizzata (se possibile!). Le funzioni sovraccaricate si utilizzano esattamente come le normali funzioni:
#include <iostream.h>
int a = 5;
int Y = 10;
float f = 9.5;
float r = 0.5;
cout << 'Sum utilizzata su due interi' << endl;
cout << Sum(a, Y) << endl;
cout << 'Sum utilizzata su due float' << endl;
cout << Sum(f, r) << endl;
cout << 'Sum utilizzata su un intero e un float' << endl;
cout << Sum(a, f) << endl;
cout << 'Sum utilizzata su un float e un intero' << endl;
cout << Sum(r, f) << endl;
È il compilatore che decide quale versione di Sum utilizzare, in base ai parametri forniti; infatti È possibile eseguire l'overloading di una funzione solo a condizione che la nuova versione della funzione differisca dalle precedenti almeno nei tipi dei parametri (o che questi siano forniti in un ordine diverso, come mostrano le ultime due definizioni di Sum):
void Foo(int a, float f);
int Foo(int a, float f); // Errore!
int Foo(float f, int a); // Ok!
char Foo(); // Ok!
char Foo(); // OK!
La seconda dichiarazione È errata perché, per scegliere tra la prima e la seconda versione della funzione, il compilatore si basa unicamente sui tipi dei parametri che nel nostro caso coincidono; la soluzione È mostrata con la terza dichiarazione, ora il compilatore È in grado di distinguere perché il primo parametro anziché essere un int È un float. Infine le ultime due dichiarazioni non sono in conflitto per via delle regole che il compilatore segue per scegliere quale funzione applicare; in linea di massima e secondo la loro prioritÀ:
Match esatto: se esiste una versione della funzione che richiede esattamente quel tipo di parametri (i parametri vengono considerati a uno a uno secondo l'ordine in cui compaiono) o al piÙ conversioni banali (tranne da T* a const T* o a volatile T*, oppure da T& a const T& o a volatile T&);
Mach con promozione: si utilizza (se esiste) una versione della funzione che richieda al piÙ promozioni di tipo (ad esempio da int a long int, oppure da float a double);
Mach con conversioni standard: si utilizza (se esiste) una versione della funzione che richieda al piÙ conversioni di tipo standard (ad esempio da int a unsigned int);
Match con conversioni definite dall'utente: si tenta un matching con una definizione (se esiste), cercando di utilizzare conversioni di tipo definite dal programmatore;
Match con ellissi: si esegue un matching utilizzando (se esiste) una versione della funzione che accetti un qualsiasi numero e tipo di parametri (cioÈ funzioni nel cui prototipo È stato utilizzato il simbolo );
Se nessuna di queste regole puÃ’ essere applicata, si genera un errore (funzione non definita!).
La piena comprensione di queste regole richiede la conoscenza del concetto di conversione di tipo per il quale si rimanda ad una apposita appendice; si accenna inoltre ai tipi puntatore e reference che saranno trattati nel prossimo capitolo, infine si fa riferimento alla keyword volatile. Tale keyword serve ad informare il compilatore che una certa variabile cambia valore in modo aleatorio e che di conseguenza il suo valore va riletto ogni volta che il valore della variabile È richiesto:
volatile int ComPort;
La precedente definizione dice al compilatore che il valore di ComPort È fuori dal controllo del programma (ad esempio perché la variabile È associata ad un qualche registro di un dispositivo di I/O).
Il concetto di overloading di funzioni si estende anche agli operatori del linguaggio, ma questo È un argomento che riprenderemo piÙ avanti.
Politica de confidentialitate | Termeni si conditii de utilizare |
Vizualizari: 724
Importanta:
Termeni si conditii de utilizare | Contact
© SCRIGROUP 2024 . All rights reserved