This is an old revision of the document!


Programmazione procedurale

Fino a questo momento ogni problema è stato affrontato individuando prima un algoritmo risolutore e poi traducendolo con un programma. immagine che descrive con una freccia il passaggio dal problema verso la sua soluzione tramite un programma Quando il problema da risolvere è abbastanza grande e complicato, il programma diventa più lungo e difficile da scrivere. In questi casi conviene dividere il problema in problemi più piccoli e semplici da risolvere. Per ogni sottoproblema si ottiene un sottoprogramma (o funzione) che lo risolve. Il programma finale desiderato (che risolve il problema iniziale) si può ottenere rimettendo insieme i diversi sottoprogrammi ottenuti.

immagine che descrive la scomposizione di un problema in problemi parziali, di cui si trovano soluzioni parziali, da riunire insieme

Una volta che delle funzioni sono state scritte per risolvere una parte di un problema, queste possono anche essere riutilizzate per risolvere altri problemi simili. La maggior parte dei programmatori riutilizza infatti delle funzioni scritte da altri programmatori, senza preoccuparsi di scrivere nulla di nuovo, perché quello che serve spesso è già stato scritto da qualcun altro.

I termini procedura e funzione sono quasi sinonimi, anche se vi sono piccole differenze di significato. Nel linguaggio C++ si usa esclusivamente il termine funzione quindi non si parla di procedure. Per capire cosa sia una funzione in un programma basta pensare ad una funzione matematica, dove c'è la variabile indipendente x e quella dipendente y. Quest'ultima variabile di solito è il risultato che si trova elaborando la prima variabile, come y=f(x). Ad esempio, la funzione y=x2 calcola il quadrato dei valori forniti tramite x.

Nella programmazione procedurale, ad ogni funzione viene affidata l'esecuzione di un compito specifico e ogni volta che si deve svolgere quel compito si chiama in esecuzione quella funzione.

Come accade nelle funzioni matematiche, anche le funzioni del programma possono contenere tra parentesi gli argomenti da utilizzare. Alcune funzioni, come main(), possono essere prive di argomenti, ma le parentesi si devono scrivere sempre.

Sarebbe utile eseguire i seguenti programmi in modalità debug e visualizzare il contenuto delle variabili in runtime per capire meglio cosa accade durante l'esecuzione delle funzioni. D'ora in avanti gli identificatori delle funzioni e delle variabili costituiti da più parole si riconosceranno dall'iniziale minuscola ed eventuali altre parole maiuscole tutte attaccate (regola CamelCase).

12.cpp
// Questo programma è stato scritto da Fabio.
// Serve a calcolare il triplo di un numero
// scrivendo tutto il codice nella funzione principale.
#include <iostream>
 
int main ()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  std::cout << "il triplo di " << mioNumero << " vale " 
            << 3*mioNumero << endl;
  return 0;
}

Divisione in sottoproblemi

Il precedente programma era stato scritto usando solo una funzione (main).

immagine che descrive il legame reciproco tra le due funzioni (chiamate visualizzazione e calcolo) Ora verrà riscritto dividendo il programma in due parti che si occupano di due aspetti diversi del problema, usando quindi due funzioni. La prima parte si occuperà della visualizzazione dei dati mentre l'altra del calcolo. Per poter far funzionare questo sistema si deve far comunicare tra loro le due parti. Ovviamente l'esempio è così semplice da risolvere che non sarebbe necessario dividere il programma in due parti, ma inizialmente è necessario farlo su casi semplici per capire come si può dividere il codice di un programma.

La prima parte verrà svolta (come prima) dalla funzione main() mentre per la seconda parte verrà creata una nuova funzione: calcolaTriplo(). Leggendo il nuovo codice sorgente si noterà che è diviso in due parti:

  • in basso vi è la solita definizione di main(), la funzione principale, che come al solito restituisce un valore intero al sistema operativo.
  • in alto vi è la definizione di calcolaTriplo(), che è una nuova funzione. Anche essa restituisce un valore intero, alla funzione main().

Le due parti sono collegate tra di loro, infatti si può notare la presenza, dentro main(), della chiamata di calcolaTriplo(). La prima funzione che viene eseguita è sempre main(). Ogni volta che si incontra una chiamata di una funzione, l'esecuzione delle istruzioni si interrompe per eseguire le istruzioni della funzione che è stata chiamata. Al termine della funzione, si riprende da dove si era interrotto.

13.cpp
// Questo programma è stato scritto da Fabio.
// Come nel programma precedente calcola il triplo di un numero, ma
// ora usando una semplice funzione chiamata calcolaTriplo().
// Questo modo di passare il valore crea una nuova variabile locale (int x)
// questa funzione restituisce un int (return)
 
#include <iostream>
 
// definizione della funzione calcolaTriplo() con parametro formale 'x'
int calcolaTriplo(int x)
{
  return 3*x;
}
 
//definizione della solita funzione main()
int main()
{
  int mioNumero,mioTriplo;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  mioTriplo = calcolaTriplo(mioNumero);  // chiamata della funzione con il 
                                         // parametro effettivo 'mioNumero'
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << mioTriplo << std::endl;
  return 0;
}

immagine che descrive il legame tra due moduli software chiamati main e calcolatriplo Come avviene la “comunicazione” tra le due funzioni?

  1. Dentro main() c'è una chiamata di calcolaTriplo() e il valore da passare viene scritto tra parentesi.
  2. Dentro calcolaTriplo() c'è l'istruzione “return” che restituisce un valore al main().

Dentro la definizione della funzione calcolaTriplo() tra parentesi c'è scritto int x: questo permette di passare alla funzione il valore contenuto in una variabile (non si passa la variabile), e questo meccanismo si chiama appunto passaggio per valore.

immagine che descrive come viene copiato il valore di una variabile locale da main a calcolatriplo

Dentro la funzione calcolaTriplo() la variabile x non sembra essere mai inizializzata, ma grazie al passaggio del valore è come se ci fosse scritto il seguente codice:

 int x = mioNumero;         // definizione di una nuova variabile locale
                            // la sua inizializzazione tramite valore passato tra parentesi

Questo codice serve solo per capire come avviene l'inizializzazione di x dentro la funzione, e non va preso alla lettera, cioè non vanno copiate quelle istruzioni…

Nella definizione della funzione calcolaTriplo() c'è scritto che le si può passare un valore intero. Non è consentito quindi passare più di un valore, oppure un valore non corrispondente al tipo intero.

Valore restituito

Al termine della funzione calcolaTriplo() c'è l'istruzione return, che restituisce il risultato a chi ha chiamato questa funzione. Anche il tipo del risultato restituito deve corrispondere al tipo che è dichiarato davanti al nome della funzione (int). Questo tipo di definizione assomiglia alla definizione di una variabile intera. Infatti, leggendo il codice del main(), si può immaginare di sostituire, al posto della chiamata della funzione calcolaTriplo(mioNumero) una variabile intera dentro cui sarà copiato il valore restituito con return.

NOTA: questa nota deve essere letta solo da chi conosce il linguaggio C. A volte i programmatori C usano il valore restituito da una funzione per stabilire se questa è stata eseguita senza errori (restituendo 0) oppure se ha prodotto un errore (restituendo ad esempio 5). In C++, invece, il valore restituito non dovrebbe essere usato per questo scopo, perché, per stabilire se ci sono stati errori, si dovrebbero usare le exception (vedere gestione delle eccezioni).

Ecco una tabella che mostra la traccia dei valori contenuti nelle aree di memoria utilizzate durante l'esecuzione dell'ultimo esempio:

mioNumero mioTriplo x
9 - -
9 - 9
9 27 -
  • Le variabili dentro la chiamata di una funzione (mioNumero) sono dette parametri effettivi (actual parameters)
  • Le variabili dentro la definizione di una funzione (x) sono dette parametri formali (formal parameters)

Questo tipo di tabella che tiene la traccia dei valori assunti dalle variabili durante l'esecuzione si può paragonare allo strumento di debug. Come si può notare l'esistenza della variabile locale della funzione calcolaTriplo() (il parametro formale x) ha una durata è più breve, rispetto alle variabili della funzione main() (il parametro effettivo mioNumero). Da questo di deduce che ognuna delle funzioni viene eseguita utilizzando aree di memoria separate, che non sono reciprocamente visibili e per questo motivo tali variabili sono dette variabili locali. La comunicazione tra le due funzioni avviene solo grazie al passaggio della copia del valore (dentro x) e grazie al comando return.

Facendo un paragone, la funzione main() è come un datore di lavoro che chiama la funzione calcolaTriplo() ad eseguire un certo lavoro. Quando la chiama le fornisce anche una copia del dato su cui dovrà lavorare. Al termine, la funzione calcolaTriplo() restituisce al suo datore il risultato del suo lavoro.

Debugger

Prima di andare avanti nella lettura è necessario leggere questa premessa

Per l'analisi e l'individuazione dei bug di un programma o per osservare il contenuto della memoria durante la sua esecuzione esistono degli appositi strumenti software utili anche per visualizzare i valori dei parametri attuali e formali durante l'esecuzione di un programma. Tali software di debug permettono quindi di capire cosa accade durante l'esecuzione di un programma.

I software di debug esistono sia con interfaccia grafica sia con interfaccia testuale:

  • Si può usare un IDE (come codelite o ddd o c++dev)
  • Si può usare la linea di comando di gdb, come in questo esempio:
    1. compilare il sorgente.cpp in debug mode
      g++ -g file.cpp -o file.exe
    2. avviare il debugger, specificando anche l'eseguibile da aprire, digitando:
      gdb file.exe
    3. inserire un breackpoint alla riga 5 digitando:
      break file.cpp:5
    4. avviare esecuzione, che si interromperà al break, digitando:
      run
    5. al prompt creare un automatic display digitando:
      display nomevariabile
    6. procedere con passo 1, digitando:
      step
    7. basta premere invio per continuare con passo 1…
    8. Per uscire, digitare:
      quit

Esempi con errori

Questo programma contiene un'istruzione che non rispetta la visibilità delle variabili locali di ogni funzione. Provare a compilare il seguente programma:

13error1.cpp
// Questo programma è stato scritto da Fabio.
// Serve a dimostrare l'ambito di visibilità delle variabili locali
// usando due funzioni, ma contiene un ERRORE....
 
#include <iostream>
 
int calcolaTriplo() 
{
  return 3*mioNumero;  
}
 
int main()
{
  int mioNumero,mioTriplo;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  mioTriplo = calcolaTriplo(); // chiamata della funzione
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << mioTriplo << std::endl;
  return 0;
}

dovrebbe produrre il seguente errore:

 In function ‘int calcolaTriplo()’:
 error: ‘mioNumero’ was not declared in this scope
13error2.cpp
// Questo programma è stato scritto da Fabio.
// e' simile al precedente, ma contiene un ERRORE...
 
#include <iostream>
 
int calcolaTriplo(int x) 
{
  return 3*x;
}
 
int main()
{
  int mioNumero,mioTriplo;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  mioTriplo = calcolaTriplo(mioNumero); // chiamata della funzione
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << x << std::endl;
  return 0;
}

Si dovrebbe ottenere questo errore

   In function ‘int main()’:
        : error: ‘x’ was not declared in this scope 

Questo argomento viene ripreso e approfondito nel prossimo capitolo, nel paragrafo direttive direttive al compilatore.

Che cosa significa la parola “iostream” che si trova quasi sempre all'inizio di ogni programma? Si tratta di un file che si può trovare su ogni sistema operativo dove è installato un compilatore. Se non si conosce la sua collocazione si può fare una ricerca… iostream è un file (senza estensione) che contiene solo semplice testo. Aprendolo si troveranno dei commenti e delle dichiarazioni di classi, operatori, variabili, oggetti e funzioni che sarebbe necessario scrivere prima del main() quando si vogliono usare certe funzioni.

Questo tipo di file sono detti “header file” o file di intestazione.

Usando la direttiva #include si evita di scrivere tutte le cose che contengono.

Esistono numerosi altri file di intestazione, tra cui “string”.

Esempio con string

In C++ esiste un tipo di dato chiamato std::string.

Una variabile di tipo std::string può contenere una frase che usa qualsiasi simbolo della tastiera, ma per poterlo utilizzare è necessario aggiungere

#include <string>

Come già detto, però, l'operatore Extractor >> interrompe la lettura quando incontra un invio, uno spazio bianco o una tabulazione.

14.cpp
// questo programma è stato scritto da Fabio
// mostra che l'operatore Extractor (>>) lascia i caratteri non letti in input 
// e che essi verranno letti al successivo utilizzo di Extractor
 
#include <string>
#include <iostream>
 
int main()
{
    std::string parola;
 
    std::cout << "primo input: scrivi due parole con lo spazio in mezzo" << std::endl;
    std::cin >> parola;
    std::cout << parola << std::endl; // non ha letto la seconda parola
 
 
return 0;
}

Ogni volta che si inserisce uno spazio si deve effettuare una nuova lettura per non perdere i dati in input. Quindi bisognerebbe aggiungere queste istruzioni

    std::cin >> parola;          // ora ha letto la seconda parola
    std::cout << parola << std::endl; 

Questo esempio mostra l'uso di una funzione che legge anche più parole separate da uno spazio.

14bis.cpp
// questo programma è stato scritto da Fabio
// mostra che l'operatore Extractor (>>) lascia in sospeso l'ultimo newline 
// digitato dall'utente
// mostra il funzionamento delle funzioni cin.ignore() e getline();
 
#include <string>
#include <iostream>
 
using namespace std;
 
int main()
{
    string parola;
 
    cout << "scrivi due parole con lo spazio in mezzo" << endl;
    getline(cin,parola);    // questa funzione legge anche gli spazi
    cout << parola << endl; 
 
    cout << "scrivi una sola parola, senza spazi" << endl;
    cin >> parola;
    cout << parola << endl; // rimane uno "newline" non letto.
                            // Se si usasse di nuovo getline()
                            // verrebbe letto solo quest'ultimo "newline"
 
    cin.ignore();             // permette di buttare via l'ultimo carattere non letto
                              // (newline) che l'utente ha premuto
                              // Quindi tra un cin e un getline() e' necessario usare un ignore()
 
    cout << "scrivi due parole con lo spazio in mezzo" << endl;
    getline(cin,parola);      
    cout << parola << endl; 
 
return 0;
}

Un'altro esempio di un programma che contiene una nuova funzione

15.cpp
// Questo programma e` stato scritto da Fabio.
// Dimostra l'utilita` delle funzioni per riutilizzare il codice
// effettuando più chiamate della stessa funzione. 
// 
// Serve a calcolare la media di 4 numeri.
 
#include <iostream>
 
int calcolaMedia(int x, int y)
{
  return (x+y)/2;
}
 
int main()
{
  int numero1,numero2,numero3,numero4;
 
  std::cout << "Per favore scrivi due numeri interi separati da uno spazio: " 
            << std::endl;
  std::cin >> numero1 >> numero2 ;
  int media1 = calcolaMedia(numero1,numero2);
 
  std::cout << "Per favore scrivi due numeri interi separati da uno spazio: " 
            << std::endl;
  std::cin >> numero3 >> numero4;
  int media2 = calcolaMedia(numero3,numero4);
 
  std::cout <<  "La media vale " << calcolaMedia(media1,media2) << endl;
 
  return 0;
 
}

Ora che è chiara l'importanza delle funzioni per il riutilizzo del codice, si può anche capire che il compilatore ha bisogno di controllare la correttezza delle chiamate delle funzioni, che cioè vengano chiamate con il giusto numero e il giusto tipo di parametri. Nei precedenti esempi questo controllo era reso possibile dal fatto che le nuove funzioni erano definite all'inizio del file, prima di essere usate nel main(). L'ordine era il seguente:

  1. definizione di calcolaTriplo()
  2. definizione di main()

Provando ad invertire l'ordine delle definizione delle due precedenti funzioni, si otterrebbe un errore durante la compilazione. Anche se la ragione per cui si vuole spostare la definizione della nuova funzione diverrà chiara solo più avanti, esiste comunque un modo per superare questo problema.

La funzione calcolaTriplo() può essere definita anche dopo main(), ma bisogna comunque dichiarare o “annunciare” la sua forma, cioè, bisogna comunque dire prima al compilatore quali parametri sono richiesti da tale funzione e che tipo essa restituisce. Vedere il seguente esempio, dove si possono individuare tre parti:

  1. dichiarazione di calcolaTriplo()
  2. definizione di main()
  3. definizione di calcolaTriplo()

L'argomento dichiarazione/definizione verrà approfondito nel paragrafo dichiarazione e definizione. Per adesso, potrebbe essere sufficiente notare che la dichiarazione di una funzione corrisponde alla prima riga della definizione, terminata dal punto e virgola (;), mentre la definizione prosegue con le parentesi graffe, senza nessun punto e virgola finale.

16.cpp
// Questo programma è stato scritto da Fabio.
// Come nel programma precedente calcola il triplo di un numero, ma
// serve a dimostrare la differenza tra dichiarazione e definizione di una funzione
 
#include <iostream>
 
// dichiarazione della funzione calcolaTriplo() 
int calcolaTriplo(int x);
 
//definizione della solita funzione main()
int main()
{
  int mioNumero,mioTriplo;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
  mioTriplo = calcolaTriplo(mioNumero);  // chiamata della funzione con il 
                                         // parametro effettivo 'mioNumero'
 
  std::cout << "il triplo di " << mioNumero << " vale "
            << mioTriplo << std::endl;
  return 0;
}
 
// definizione di calcolaTriplo() (che deve rispettare la dichiarazione)
int calcolaTriplo(int x);
{
  return 3*x;
}

Un'altro esempio in cui la dichiarazione di una nuova funzione calcolaMedia() deve precedere main()

17.cpp
// questo programma e` stato scritto da Fabio
// Serve a calcolare la media (valore intero) tra due interi
 
#include <iostream>
 
int media(int x, int y); // dichiarazione 
 
int main()
{
  int numero1,numero2;
 
  std::cout << "Per favore scrivi due numeri interi separati da uno spazio: " 
            << std::endl;
  std::cin >> numero1 >> numero2 ;
 
  std::cout <<  "La media vale" << media(numero1,numero2) << endl;
  return 0;
}
 
int media(int x, int y) // definizione
{
  return (x+y)/2;
}

In base al valore restituito, esistono due diversi tipi di funzioni:

  • quelle che restituiscono un risultato (es: un valore) a chi le chiama;
    • int fun(int x);              // la sua definizione terminerà con un return
  • quelle che non restituiscono nessun risultato a chi le chiama (tipo mancante: void).
    • void gun(float z);           // la sua definizione terminerà SENZA return

Il primo tipo di funzioni può essere chiamato in uno di questi due semplici modi:

  • y = fun(n);
  • std::cout << fun(n);

Il secondo tipo può essere chiamato in questo modo:

  • gun(y);

Il primo tipo consente anche di essere utilizzato con chiamate più articolate, come:

  • y = fun(fun(n));
  • if (y == fun(n)) .......

L'effetto sulla memoria

Per quanto riguarda il primo tipo, si tratta di funzioni che sono progettate per fare una cosa:

  1. restituire un risultato (es: un valore)

Per quanto riguarda il secondo tipo, si può facilmente immaginare che una funzione che non restituisce un risultato dovrà avere sicuramente un qualche altro tipo di effetto nella memoria o nell'output. Ad esempio, potrebbe modificare una variabile o visualizzare un messaggio…

Questo effetto potrebbe produrlo anche una funzione del primo tipo. Questo è solo un caso particolare del primo tipo di funzioni, che, quindi, producono due effetti:

  1. restituiscono un risultato
  2. producono un effetto sulla memoria

Quando interessa esclusivamente il secondo effetto, queste funzioni (del primo tipo) possono essere usate ignorando completamente il valore che restituiscono e chiamate come se fossero del secondo tipo.

Esempio

Si può riscrivere il precedente esempio in modo che la funzione sia di tipo void?

Se il solito esempio venisse scomposto in due problemi di tipo diverso, si potrebbe realizzare una funzione che si occupa di leggere i dati in input e un'altra funzione che si occupa di fare il calcolo e visualizzare l'output. In tal caso la seconda funzione non dovrebbe restituire nulla con return. Si suggerisce di confrontare il vecchio esempio con il seguente e di annotare cosa cambia nella definizione della funzione e cosa cambia nella chiamata della funzione.

immagine che descrive il legame tra due nuove funzioni, una per l'input l'altra per l'output dei dati

18.cpp
    // Questo programma è stato scritto da Fabio.
    // Come nel programma precedente calcola il triplo di un numero, ma
    // la funzione calcolaTriplo() è di tipo void.
    // Questa funzione non restituisce nessun valore (non c'e' return)
 
    #include <iostream>
 
    void calcolaTriplo(int x) // definizione della funzione con parametro formale 'x'
    {
      std::cout << 3*x << std::endl;
    }
 
    int main()
    {
      int mioNumero,mioTriplo;
 
      std::cout << "Per favore scrivi un numero intero: ";
      std::cin >> mioNumero;
 
      std::cout << "il triplo di " << mioNumero << " vale "
                << mioTriplo << endl;
 
      calcolaTriplo(mioNumero); // chiamata della funzione
 
      return 0;
    }

Quest'ultimo esempio serve per introdurre il prossimo paragrafo

18error.cpp
// Questo programma e` stato scritto da Fabio.
// Contiene una funzione di "tipo" void ovvero che non restituisce nulla 
// infatti (manca return)
// ed inoltre mostra un ERRORE dovuto all'effetto del passaggio per valore.
 
#include <iostream>
 
void calcolaTriplo(int x)
{
  x= 3*x; // Ha effetto sul parametro reale?
}
 
int main()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
  std::cout << "il triplo di " << mioNumero << " vale ";
 
  calcolaTriplo(mioNumero);  // chiamata della funzione
  std::cout  << mioNumero << std::endl;
  return 0;
}

Provare sostituire la definizione della funzione con la seguente definizione

void calcolaTriplo(int& x)
// eccetera...

Ispettori e modificatori

Un altro tipo di classificazione delle funzioni le divide in funzioni ispettrici (inspector) e modificatrici (mutator) (o metodi ispettori e modificatori).

Ispettori

Queste non modificano il contenuto delle variabili passate, quindi di solito, per avere un effetto al loro esterno, devono restituire un valore (quindi non possono essere di tipo void); di solito sono usate per prelevare un valore (senza apportare modifiche) perciò in passato venivano chiamati anche metodi getter.

Si possono riconoscere facilmente in quanto è buona abitudine terminare la loro dichiarazione con const. In questo modo infatti il compilatore aiuta a trovare errori di programmazione.

int fun(int x) const;            // funzione costante con passaggio per valore
int fun(const NomeClasse& o) const;  // funzione costante con passaggio per riferimento (costante!!!)
const NomeClasse& fun() const;     // funzione costante che restituisce un riferimento (costante!!!)

Mofidicatori

Queste possono modificare il contenuto delle variabili passate, quindi possono produrre un effetto al loro esterno anche se sono di tipo void; di solito sono usate per impostare un valore perciò in passato venivano chiamati anche metodi setter;

Ovviamente queste si riconoscono dall'assenza di const.

void fun(Classe& o);  // funzione con passaggio per riferimento

Per completare la discussione su ispettori e modificatori si devono conoscere i references (o riferimenti). Si preferisce rimandare questa discussione, intanto sono presentati alcuni esempi da leggere.

Nel programma 13.cpp la variabile mioTriplo NON era veramente necessaria. Si può riscrivere il programma scrivendo la chiamata della funzione calcolaTriplo() come se fosse una variabile intera, che viene visualizzata su std::cout.

19.cpp
// Questo programma e` stato scritto da Fabio.
// Dimostra l'uso di una funzione dentro l'operatore "Insertion" <<
 
#include <iostream>
 
int calcolaTriplo(int x)
{
  return 3*x;
}
 
int main()
{
  int mioNumero;
 
  std::cout << "Per favore scrivi un numero intero: ";
  std::cin >> mioNumero;
 
// la chiamata della funzione (triplicare) dentro Insertion 
  std::cout << "il triplo di "<< mioNumero << " vale "
            << calcolaTriplo(mioNumero) << std::endl;
  return 0;
}
20.cpp
// Questo programma è stato scritto da Fabio.
// Dimostra la differenza tra concetto di dichiarazione e di definizione 
 
#include <iostream>
 
// dichiarazione della funzione
int calcolaMedia(int x, int y); 
 
int main()
{
  int numero1,numero2,numero3,numero4,media;
 
  std::cout << "Per favore scrivi due numeri interi separati da uno spazio: "
            << std::endl;
  std::cin >> numero1 >> numero2;
  std::cout << "Per favore scrivi due numeri interi separati da uno spazio: "
            << std::endl;
  std::cin >> numero3 >> numero4;
 
  media = calcolaMedia(calcolaMedia(numero1,numero2),calcolaMedia(numero3,numero4));
  std::cout <<  "La media vale " << media << endl;
 
  return 0;
 
}
// definizione della funzione
int calcolaMedia(int x, int y)
{
  return (x+y)/2;
}

Dichiarazione e definizione

La dichiarazione è un “annuncio” che viene fatto al compilatore dell'uso di nuovo ente (variabile, oggetto, funzione, ecc.). Questo annuncio contiene alcune informazioni sull'ente, ma non lo definisce in modo completo. Per poterlo usare è necessaria quindi anche la sua definizione. In breve, tutto quello che è viene semplicemente “dichiarato” DEVE essere poi anche “definito” . Una classe può essere usata solo dopo aver effettuato (oltre alla sua definizione) anche la definizione delle funzioni che essa contiene.

Dichiarazione

Annuncia (al compilatore) l'utilizzo di un nuovo oggetto (variabile) o una nuova funzione (o f. membro). La dichiarazione può essere fatta più volte in posti diversi ogni volta che è necessario dare informazioni al compilatore per lavorare su un nuovo oggetto o funzione. Vedere extern

  • La dichiarazione delle funzioni viene fatta nell'header file.
  • La dichiarazione di una funzione, se necessario, può essere fatta anche più di una volta.
  • La dichiarazione delle funzioni membro viene fatta dentro le definizioni delle classi, sempre nell'header file.
  • La dichiarazione di una funzione (f.membro) è detta anche firma. es: int funzione(float a);
  • La dichiarazione di un oggetto (variabile) può essere fatta, in un unico colpo, attraverso la sua definizione (all'interno di una classe o di una funzione (o f.membro).
  • La dichiarazione di una classe contiene solo il nome della classe, e anch'essa termina sempre col punto e virgola (;)
  • nota: per approfondire le dichiarazioni, studiare i namespace….

Definizione

immagine che descrive come il concetto di dichiarazione sia contenuto nel concetto di definizione

La definizione contiene in sé anche una dichiarazione, ma può essere fatta solo una volta, perché la definizione, oltre ad effettuare anche una dichiarazione (l'annuncio), alloca (riserva) anche la necessaria area di memoria per contenere un'ente. L'area può essere immediatamente inizializzata.

  • La definizione di una funzione (f. membro), oltre ad annunciarne l'utilizzo, specifica anche il blocco delle istruzioni che la costituiscono, detto anche il corpo. Viene detta anche implementazione della funzione.
  • Per i precedenti motivi la definizione è unica (non si può ripetere).
  • La definizione di una classe, invece, non alloca memoria né inizializza!
  • La definizione di una classe è l'unica ad avere bisogno delle parentesi graffe, e termina anch'essa col punto e virgola (;)
  • La definizione di una classe (come detto prima) non contiene le definizioni, ma solo le dichiarazioni delle sue funzioni membro.
  • La definizione di una classe deve, per questo motivo, essere seguita dalla definizione (o implementazione) di tutte le sue funzioni membro .

Variabili

Per quanto riguarda le variabili non si distingue tra dichiarazione e definizione perché quando una variabile viene dichiarata viene anche definita.

int x; 

è sia dichiarazione che definizione. Dopo aver visto alcuni esempi, si può stabilire una regola per le dichiarazioni di variabili:

const float tassoMutuo = 0.064;

specificatore + tipo + operatore + nome + operatore + inizializzazione

Funzioni

esempio di dichiarazione (nell'header)

void fun(std::string parola);

esempio di definizione

void fun(std::string parola)
{
  // codice...
}
Le funzioni membro definite dentro la def. della classe sono implicitamenteinline

Classi

esempio di dichiarazione (nell'header)

Class Cosa; // esempio di forward declaration        

esempio di definizione (anch'essa nell'header)

Class Cosa
{
public: 
  Cosa();   // dichiarazione di funzione (da definire)
private:
  int x;
  void fun(std::string parola); // dichiarazione di funzione (da definire)
};

L'implementazione della classe viene fatta nel file sorgente (.cpp) e contiene le funzioni e i dati membro della classe. Il codice che usa oggetti del tipo appartenente ad una determinata classe (detto anche codice client o client code) deve includere l'header di tale classe.

  1. In cosa consiste la fase di compilazione e cosa produce?
  2. Che differenza c'è tra un errore di compilazione e un bug? Fare un esempio.
  3. Che cosa significa l'istruzione return dentro il main()?
  4. Che cosa significa l'istruzione return dentro un'altra funzione?
  5. Quale delle due istruzioni è obbligatoria?
  6. Come si riconosce il nome di una funzione() dal nome di una variabile?
  7. Come si può migliorare la funzione int calcolaMedia(); per non troncare il risultato?
  1. Quanti tipi di funzioni esistono in base al tipo di dato restituito?
  2. Cosa sono i parametri formali e i parametri effettivi?
  3. Cosa significa passaggio per valore dei parametri?

Esercizi: data la dichiarazione di una funzione, scrivere la chiamata

  1. double areaRettangolo(double base, double altezza);
  2. double radiceQuadrata(double numero);
  3. bool segnoPositivo(double numero);
  4. float distanza(float x, float y);
  5. void saluto();
  6. void numeroCasuale();

Esercizi: usare le seguenti funzioni <cmath>

  1. float pow(float a, float b); // restituisce la potenza
  2. float sqrt(float a); // restituisce la radice q.
  3. float fmax(float a, float b); // restituisce il massimo
  4. trovare il massimo di tre numeri usando fmax()…

Esercizi: usare le seguenti funzioni <string> // richiedono dot notation

  1. int find( char c, size_t pos = 0 ) const;
  2. int size() const;

Programmi procedurali da scrivere:

  1. scrivere una funzione che calcola l'area del rettangolo
  2. verificare pre-condizioni sui dati in ingresso prima di usarli (raggio positivo)
  3. verificare post-condizioni sui dati in uscita (area positiva)
  • appunti3s/programmazione_procedurale.1524635754.txt.gz
  • Last modified: 2018/04/25 07:55
  • by 127.0.0.1