Il C++ è, come noto, uno dei più diffusi linguaggi di programmazione fra gli innumerevoli oggi disponibili. Esistono ormai anche linguaggi a più alto livello (con linguaggio ad alto livello si intende un linguaggio di programmazione più lontano dal codice macchina e più vicino, per quanto lo possa essere un linguaggio di programmazione per computer, al linguaggio umano) rispetto al C++, e quindi perchè un utente dovrebbe, per necessità o per diletto, rivolgersi proprio al C++? I motivi possono essere molti, ma è chiaro che si tratta soprattutto di una questione di esigenze e di gusti; ad ogni modo il C++ presenta i seguenti indubbi vantaggi:
Cominciamo ora a vedere come si scrive un programma in C++. E cominciamo ovviamente con il programma che non fa assolutamente nulla, che è sempre il primo che ogni corso di programmazione che si rispetti presenta. Il codice è questo:
main()
{
return(0);
}
Questo programma, se compilato ed eseguito non fa
assolutamente nulla, ma vediamone la sintassi: main() indica il
punto in cui inizia la parte principale del programma. Un
programma C++ deve sempre contenere un main, al
quale possono aggiungersi un numero qualsivoglia di funzioni che
svolgono parti specifiche del codice. Le parentesi tonde
contengono i parametri di input per il programma, e in questo
caso sono vuote. Le parentesi graffe invece contengono
il testo del codice, ovvero tutto quello che effettivamente il
programma farà quando viene eseguito.
return è il comando che stabilisce cosa debba
essere l'output della funzione in esame (in C++ il main non
è per nulla diverso da una qualsiasi funzione, se non per
il fatto che viene eseguito per primo e che i suoi input e output
passano al sistema operativo). In questo caso il comando
restituisce al sistema operativo il valore 0, ovvero segnala una
esecuzione corretta del programma. Non e' veramente necessario in
un caso come questo mettere il comando return nel main
perché, per come è stata usata la sintassi, non
è atteso nessun output dal programma.
Lo stesso programma poteva essere scritto in un modo leggermente
differente:
void main(){}
La parola chiave void indica che il programma
deve restituire un valore vuoto, e quindi in questo caso non
c'è bisogno di specificare il comando return. Alcuni
compilatori C++ accettano anche la sintassi senza void né
return. Void può essere inserito anche tra le parentesi
tonde per indicare che main ha come parametro di ingresso una
variabile nulla.
Una cosa importante da segnalare nella sintassi e che il punto e
virgola che appare alla fine dell'unico comando presente: quel
segno di punteggiatura indica al compilatore che è stato
completato il comando; è quindi essenziale e va messo alla
fine di tutte le righe di comando.
Dopo il primo programma vuoto si vuole passare a qualcosa di
più utilizzabile, per esempio un programma che scrive una
riga di testo a video. E qui le cose già si complicano:
tutti i linguaggi di programmazione hanno dei comandi che
implementano l'ingresso da tastiera e l'output a video, solo che
nel C++ (come nel C) questi non sono fra i comandi base!
Questa scelta, apparentemente paradossale, ha la sua ragione
nella fondamentale modularità e flessibilità di
questo linguaggio. E' vero che molti programmi devono prendere
input da utente, fare delle operazioni e mostrare dei risultati,
ma è altrettanto vero che non tutti lo devono fare: ci
sono programmi che devono funzionare completamente in background,
senza che l'utente della macchina neanche se ne accorga (ad
esempio componenti del sistema operativo, driver di periferiche,
virus(!) e molti altri). Per questi programmi sarebbe un assurdo
caricare in fase di compilazione moduli che non andranno mai ad
utilizzare, rendendoli semplicemente più pesanti. Sembra
una sciocchezza, ma è uno dei segreti della potenza del
C++.
Bisogna perciò includere nel programma, prima del main, una stringa che carichi il file header che gestisce i flussi di input/output. Questo file è semplicemente una libreria C++ che contiene le definizioni dei comandi necessari che poi saranno usati nel programma. L'istruzione che serve è:
#include <iostream.h>
Le parentesi circonflesse attorno al nome del file
della libreria indicano al compilatore che essa è
collocata nella cartella di default degli
header, nota al compilatore, mentre, se si fosse incluso
il nome tra virgolette (es: #include "mylib.h") avrebbe indicato
che il file è nella stessa cartella del file sorgente del
programma che si sta scrivendo.
Bisogna però dire che nei compilatori più recenti
si può anche dimenticare di inserire alcune fra le
librerie più comuni, perché se vengono inseriti
comandi relativi a tali librerie il compilatore stesso
provvederà a includerle: ciò non toglie che sia
buona norma includerle manualmente, anche per migliorare la
portabilità del codice.
Vediamo ora il programma completo:
#include <iostream.h>
void main()
{
cout < "Ciao a tutti" << endl;
}
Il comando cout genera gli output a video ed è molto flessibile perché non si limita a visualizzare stringhe come nel caso mostrato (la stringa è ovviamente quella compresa fra le virgolette), ma anche qualsiasi altro tipo di dato, dagli interi ai numeri a virgola mobile, fino anche ai tipi di dato definiti dall'utente (in questo caso si applica una tecnica chiamata overloading degli operatori, che verrà spiegata in futuro dato che è una tecnica di programmazione piuttosto avanzata), e combinazioni degli stessi. Ad esempio il comando
cout << "Il raggio della Terra \'e di " << 6378.14 << " km" << endl;
concatena le due stringhe inserendoci in mezzo il valore
numerico (convertito anch'esso in stringa, ma di questo l'utente
non ha traccia) e stampa a video la scritta: Il raggio
della terra è di 6378.14 km
L'accento, come si vede, è un carattere speciale, e va
preceduto dalla barra \: altri esempi di caratteri
speciali sono la tabulazione (\t) e la riga a capo
(\n). endl è una costante che
serve a sostituire la riga a capo: è perfettamente
equivalente inserire nella stringa un \n.
Una volta mostrato come si genera un output a video, vediamo
anche come si può acquisire un dato dalla tastiera. Per
far questo, sempre usando la libreria
<iostream.h>, esiste il comando duale di
cout, cin. Vediamone subito l'uso
attraverso un esempio:
#include <iostream.h>
void main()
{
int dato;
cout << "Inserire un numero intero: ";
cin >> dato;
cout << "Il valore che hai immesso \'e: " << dato;
}
Come si vede, cin utilizza l'operatore >> anziché <<; questi sono operatori usati per indicare il flusso di dati, come si vedrà più avanti. Una volta inserito il dato, cin provvede automaticamente al ritorno a capo del cursore; non ha perciò senso usare i caratteri speciali come si fa con cout. Il comando int dato crea una variabile di tipo numero intero di nome dato e sarà spiegato più avanti.
In C++ esiste ovviamente la possibilità di inserire righe di commento, che permettono di aggiungere note di spiegazione del codice utili sia per chi debba leggere un sorgente scritto da altri, sia per chi scrive il codice ed ha bisogno di andarlo a riprendere e modificare a distanza di tempo. E' molto utile anche in fase di debugging del programma, perché permette di eliminare temporaneamente aclune righe senza doverle cancellare, se possono tornare utili. In C++ ci sono due modi per scrivere commenti:
//; in questo modo tutto ciò che segue le
barre su quella stessa linea di codice viene ignorato dal
compilatore./* e il simbolo di fine commento
*/.Ricordo soltanto che i commenti non appesantiscono il codice eseguibile perché non vengono proprio compilati e che è buona norma di qualsiasi programmatore scrivere del codice ben commentato.
Una delle cose più importanti da sapere di ogni linguaggio di programmazione è l'uso delle variabili, la loro dichiarazione, l'assegnazione di valori. Il C++, per continuità con il C, è uno di quei linguaggi che richiedono ancora la dichiarazione delle variabili, pratica che alcuni potranno trovare oscura, scomoda e/o obsoleta, soprattutto se abituati con alcuni linguaggi moderni che ne fanno a meno, ma bisogna dire che può non rappresentare soltanto una scomodità: dichiarare le variabili significa un maggiore controllo sul loro uso e perciò una minore probabilità di errori dovuti ad assegnazioni ambigue. Ad ogni modo, se si vuole usare il C++, non c'è alternativa. I nomi delle variabili seguono, come nella quasi totalità dei linguaggi, poche ma ferree regole di composizione:
void,
return, int, ecc| asm | auto | break | case | catch |
| char | class | const | continue | default |
| delete | do | double | else | enum |
| extern | float | for | friend | goto |
| if | inlineint | long | new | operator |
| private | protected | public | register | return |
| short | signed | sizeof | static | struct |
| switch | template | this | throw | try |
| typedef | union | unsigned | virtual | void |
| volatile | wchar_t | while |
La dichiarazione di una variabile si fa inserendo una riga con
il tipo seguito dal nome che le viene assegnato; un esempio si
è già visto precentemente, nel programma di
acquisizione da tastiera. Le variabili generalmente vengono
dichiarate prima all'inizio del programma e possono essere
dichiarate anche fuori dal main (in questo caso sono
variabili globali.
Vediamo ora quali sono i tipi di dato fondamentali implementati in C++:
boolintcharwchar_tfloatdoubleemunvoidIl tipo bool indica una variabile di tipo
booleano, di quelle che rappresentano i valori logici nelle
tabelle di verità e può assumere soltanto i valori
true (vero) oppure false (falso). Alle variabili booleane possono
essere applicati soltanto operatori logici AND
(indicato nel codice con il simbolo &&),
OR (||) oppure NOT
(!).
Il tipo int, come si è visto, genera una
variabile di tipo numero intero. Questo tipo di variabili
occupano in memoria un numero di bit pari alla dimensione dei
registri della macchina su cui si sta operando. Questo rende il
codice compilato dipendente dalla macchina, ma anche molto
efficiente sulla piattaforma su cui lo si usa. Ad ogni modo la
sintassi del codice rimane la stessa su tutte le piattaforme, e
questo garantisce la portabilità
(è sufficiente ricompilare il codice per tutte le
macchine).
Il tipo char serve per rappresentare i caratteri: si tratta in realtà ancora di numeri interi a cui il compilatore assegna una corrispondenza con i caratteri. Per questo sono possibili, per quanto prive di senso, anche operazioni matematiche sui caratteri. I singoli caratteri, al contrario delle stringhe, si indicano fra apici semplici, anzichè doppi. Ad esempio per creare un carattere e assegnargli la lettera a il listato è:
char lettera; lettera = 'a';
E' molto utile far notare che le variabili possono essere inizializzate anche nella stessa linea di comando della dichiarazione. Ad esempio il codice appena scritto è equivalente a:
char lettera = 'a';
Per quanto riguarda i caratteri, bisogna notare che esiste
anche una altro tipo di dato chiamato wchar_t, che
permette di rapprensentare i caratteri Unicode estesi
che non sono rappresentabili da char.
I tipi float e double rappresentano
i numeri in virgola mobile, ovvero i numeri reali, nei limiti
della capacità di memoria dei registri di memoria di un
computer. In genere un float occupa in memoria 4
byte, mentre un double 8, mentre l'effettiva
estensione della gamma numerica varia a seconda della dimensione
della parola di memoria sulla macchina. In ogni caso si
può vedere la dimensione di un tipo di dato in memoria
usando il comando sizeof() e facendolo visualizzare
con un cout.
Il tipo enum crea un tipo di dato i cui elementi
sono associati a delle etichette create dal programmatore;
l'esempio classico sono i giorni della settimana: si possono
associare a dei numeri interi, ma può essere comodo creare
un tipo di dato che li definisce direttamente. In questo caso si
usa enum, con la seguente sintassi, in cui viene
fatto anche un esempio di assegnamento:
emun {lun, mar, mer, gio, ven, sab, dom} giorno;
giorno oggi;
oggi = mar;
Ovviamente le ultime due righe potevano essere accorpate, come visto. Anche i valori di enum vengono associati come i char a dei valori interi, e questi possono essere anche specificati in fase di definizione:
enum {primo = 1, secondo = 2, terzo = 3} podio;
Per ultimo il tipo void, che, come si è
detto, indica un tipo di dato che rappresenta l'insieme vuoto,
che è infatti l'insieme dei valori che può assumere
una variabile di questo tipo.
Per poter utilizzare i tipi di dato astratto bisogna ovviamente definire le operazioni che sono consentite su di essi dal linguaggio; poiché queste sono talmente banali che non hanno certo bisogno di spiegazioni mi limiterò ad elencarle indicandone la sintassi ed eventualmente qualche particolarità nell'uso.
Le operazioni matematiche si applicano normalmente ai tipi int, float e double, ma possono essere applicati, per quanto paia inutile, anche ai char, in quanto, come accennato, essi sono in corrispondenza biunivoca con degli interi.
+ Addizione - Sottrazione * Moltiplicazione / Divisione con quoziente decimale (il risultato è sempre float o double) % Resto della divisione fra interi (questo funziona ovviamente solo per gli int)
Oltre agli operatori classici nel C++ esistono anche degli operatori particolari che permettono di eseguire più rapidamente alcune operazioni come l'incremento di uno di un contatore; L'esempio classico è proprio quello del contatore:
#include <iostream.h>
void main()
{
int i = 5;
cout << i++;
}
Questo programma mostrerà a video il numero 6; infatti l'operatore ++ serve ad aumentare di 1 il valore della variabile int a cui viene applicato. L'istruzione i++ è del tutto equivalente a i + 1. Essa esiste per due motivi:
Analogamente a questo, esistono una serie di altri operatori, indicati in tabella, che eseguono coppie di operazioni.
Operatore Uso Equivalente ++ x++; x = x + 1; -- x--; x = x - 1; += x+=y; x = x + y; -= x-=y; x = x - y; *= x*=y; x = x * y; /= x/=y; x = x / y;
Sugli operatori logici e su quelli che verificano le relazioni fra più variabili c'è talmente poco da dire che penso sia sufficiente la tabella. L'unica nota riguarda l'uguaglianza, che si distingue dall'operazione di assegnamento, come avviene ormai nella maggior parte dei linguaggi. Il risultato di tutti questi operatori è sempre un dato di tipo bool.
< MINORE <= MINORE O UGUALE > MAGGIORE >= MAGGIORE O UGUALE != DIVERSO == UGUALE && congiunzione AND || disgiunzione OR ! negazione NOT
A volte può essere necessario eseguire operazioni fra tipi di dato diverso (l'esempio classico è moltiplicare un intero per un numero a virgola mobile), cosa che il linguaggio permette di fare, ma che bisogna usare con un minimo di accortezza per evitare fraintedimenti con il compilatore. Perlomeno bisogna aver chiaro che tipo di dato si ottiene in uscita. Ad esempio nel caso di un'operazione fra numeri di diverso tipo il risultato sarà del tipo che permette una perdita di informazioni nulla o perlomeno minore. Il prodotto di un int e un float sarà dunque un float, almeno se lo lasciate decidere al compilatore. Esiste infatti il modo di forzare una conversione di tipo in modo da avere in output il tipo di dato che serve: questo si fa mettendo prima del testo dell'espressione da valutare il nome del tipo in cui si vuole convertito. Facciamo un esempio:
int a = 3; float b = 7.35; cout << (a*b) << endl;
In questo caso il risultato mostrato sarà un dato a virgola mobile, ma se si volesse che il risultato fosse di tipo intero allora basterebbe assegnarlo ad una variabile int; in questo caso però la conversione viene fatta in maniera implicita dal compilatore e questo, se il programmatore non se ne avvede può anche creare confusione o risultati inattesi. Per essere certi del tipo di output è bene usare una sintassi del tipo:
int a = 3, c; float b = 7.35; c = (int) a*b;
Questo tipo di sintassi funziona sempre fra i vari formati numerici (ad esempio per forzare un int in un double, ma si può usare in certi casi anche per i caratteri, ad esempio per il passaggio da char a wchar_t, ma anche per ottenere conversioni di numeri in stringhe di caratteri. In quest'ultimo caso però si entra in un campo leggermente minato: non tutti i tipi di conversioni si possono fare e alcune di esse si possono fare solo con un po' di dimestichezza con i puntatori. Più sicuro in questo ambito affidarsi a delle funzioni implementate in alcune librerie, che convertono dati da numeri a stringhe e viceversa, e che per comodità sono riportati in tabella.
| Sintassi | Converte... | ...in... | Libreria |
|---|---|---|---|
| int atoi(const char *s); | stringhe | int | stdlib.h |
| itoa(int value, char *string, int radix); | int | stringhe | stdlib.h |
| double atof(const char *s); | stringhe | double | math.h |
Dopo il primo fondamentale, ma temo pesante, capitolo introduttivo in cui si sono visti i primi programmi che sono in grado si e no di risolvere i problemini delle elementari (quelli, per capirci che un neofita farebbe prima a risolvere a mano che a implementare), passiamo a vedere in rassegna le funzioni che permettono al software di avere una logica che non si limiti all'esecuzione sequenziale. Anche questo tipo di istruzioni, come gli operatori del capitolo precedente, credo necessitino di ben poche spiegazioni, perciò mi limiterò a mostrarne poco più della sintassi: in fin dei conti si tratta dei soliti if...then...else, for, while e poco più (In realtà in C++, contrariamente a molti linguaggi moderni, si possono ancora usare le istruzioni di salto basate su GOTO, ma dato che esiste un teorema (di chi, non ricordo) che dice che qualsiasi algoritmo codificabile con le GOTO si può riscrivere con istruzioni di salto condizionate tipo IF, e visto che si dice possano facilmente indurre a programmi contenenti errori, ho considerato di abbandonare al loro destino i vecchi appassionati del BASIC, sperando che non me ne vogliano.)...
L'istruzione di salto per eccellenza ha una sintassi talmente essenziale in C++ da lasciare quasi perplessi i neofiti: il then non esiste, si scrive if, si mette la condizione booleana fra parentesi tonde e subito dopo l'istruzione da eseguire, chiudendo con il normale punto e virgola. Esempio:
#include<iostream.h>
void main()
{
int a, b = 3;
cin >> a;
if (a > b) cout << "a \'e maggiore di b";
}
L'unica differenza nel caso di istruzioni da eseguire siano più di una è che vanno messe tutte fra una coppia di parentesi graffe. Nel caso invece ci sia anche una condizione alternativa, al posto del punto e virgola si mette un else e si completa con un'altra lista di sitruzioni fra parentesi quadre. Altro esempio, per far vedere la sintassi completa:
#include<iostream.h>
void main()
{
int a, b, c;
cin >> a >> b;
if (a > b)
{
c = a * b;
cout << "c vale" << c << endl;
}
else
{
c = 0;
cout << "c \'e nullo!" << endl;
};
}
Il ciclo while serve per eseguire ripetitivamente delle operazioni finché non è verificata una condizione di uscita, che viene controllata prima di effettuare le operazioni di ogni iterazione. La sintassi è analoga a quella dell'if senza else:
#include<iostream.h>
void main()
{
int i = 0, a;
cout << "Immetti a: ";
cin >> a;
cout << "Sto cercando a..." << endl;
while (i != a)
{
cout << "a non vale " << i << endl;
i++;
};
cout << "Ho trovato! a vale" << i << endl;
}
Questo esempio serviva solo per spiegare la sintassi: spero non vorrete mai spingere il vostro PC ad un tale livello di demenzialità...
Questo tipo di ciclo si differenzia per un solo motivo dal ciclo while: verifica la condizione di uscita dopo aver eseguito il corpo delle istruzioni, e non prima, come è per il ciclo while. La sintassi parte quindi con il do e le istruzioni, e si conclude con il while e la condizione di uscita.
#include<iostream.h>
void main()
{
int i= 10;
cout << "Conto alla rovescia:\n";
do {
cout << i << "...\n";
i--;
} while (i >= 0);
}
In C++ il ciclo for consente alcune operazioni più avanzate del semplice ciclo con aumento o decremento di un contatore, in quanto nella "condizione" che viene specificata all'inizio del ciclo c'è di più della variabile del ciclo e del suo incremento. Partiamo con un esempio della sintassi classica (Anche in questo caso evitate di scrivere mai un programma simile per altro scopo che per esercitazione: la somma in questione si risolve con la formula di Gauss! somma = max*(max + 1)/2
#include<iostream.h>
void main()
{
int i, max, somma = 0;
cout << "Ora sommer\'o i numeri da 1 a: ";
cin >> max;
for(i = 1; i <= max; i++) somma+=i;
cout << "Il risultato \'e: " << somma << endl;
}
In questo modo ciò che sta fra le parentesi tonde del
ciclo for sta a indicare: "partendo da i = 1,
finché i è minore di max, e incrementando i di 1,
esegui...".
Questa è la base; ma tenendo conto che le istruzioni
inserite fra le parentesi sono tra loro indipendenti e che
possono essere più di una oppure nessuna. Se per esempio
nel programma precedente i viene inizializzato a
1 prima del ciclo, la prima parte può essere
tralasciata (in un caso come questo, fra l'altro la variabile i
è perfettamente inutile; basta decrementare max: for(
; max > 0; max--) somma+=max;):
i = 1; for( ; i<=max; i++) somma+=i;
I contatori possono anche essere più di uno in uno stesso ciclo, e possono essere incrementati al di fuori delle condizioni di ciclo. Per esempio si possono usare due contatori, prendere come condizione di stop la loro somma e incrementarne uno solo sotto una condizione if dentro al ciclo. Chiariamo con un esempio:
#include<iostream.h>
void main()
{
int i, j;
for(i = 0, j = 0; i + j < 100; i++)
{
if (i%5 == 0) j+=3;
cout << (i + j) << endl;
};
}
Eseguendo questo programma si vedrà il contatore saltare avanti di un multiplo progressivamente crescente di 3 ogni 5 numeri... Lo so, messo così sembra quasi inutile, ma di certo non si può negare che il C++ sia talmente flessibile da porre pochi limiti alla fantasia del programmatore.
Spesso capita di dover implementare una scelta fra più opzioni; il caso classico è quello di un menù interattivo. Questo tipo di operazione si può eseguire con una serie di if, ma non sempre questo è agevole, soprattutto quando le alternative sono tante. Si rischia di finire in una serie di if annidati, che può portare a confondersi, e quindi a bug ed errori. Per questo motivo quasi tutti i linguaggi definisono una sintassi apposita per la scelta multipla. Il C++ non fa eccezione: il comando si chiama switch e ha una sintassi piuttosto laboriosa:
switch (variabile)
{
case valore:
istruzioni...
break;
altri casi...
default:
istruzioni...
break;
}
La scelta multipla accetta variabili di tutti i tipi ma i casi devono contenere valori singoli e non intervalli, cosa che rappresenta un po' un limite; in compenso si possono assegnare le stesse istruzioni a diversi casi. Il comando default serve per spiegare al compilatore che cosa deve fare in caso nessuna delle opzioni precedenti corrisponda alla variabile. Facciamo ora un esempio per chiarire definitivamente e chiudere il capitolo:
#include<iostream.h>
// Questo semplice programma vi ricorda alcune scadenze...
void main()
{
enum {gennaio, febbraio, marzo, aprile, maggio, giugno,
luglio, agosto, settembre, ottobre, novembre, dicembre}
mese;
mese m;
int menu, g;
cout << "Scegli uno degli appuntamenti da ricordare" << endl;
cout << "1- Dentista\n2- Bollo auto\n3- Compleanno mamma\n";
cin >> menu;
switch(menu)
{
case 1:
g = 12;
m = aprile;
break;
case 2:
g = 28;
m = giugno;
break;
case 3:
g = 14;
m = ottobre;
break;
default:
cout << "Errore! Non hai selezionato un valore valido" << endl;
g = 1;
m = gennaio;
break;
}
cout << "La data che cerchi \'e il " << g << m << endl;
}
In alcuni linguaggi di programmazione si distingue fra funzioni e procedure: le prime sono delle parti di un programma che vengono richiamare per ottenere da esse uno o più valori in uscita (niente di molto diverso dal concetto di funzione che si ha in matematica), le seconde invece sono delle sequenze di istruzioni che vengono eseguite ogni volta che la procedura stessa viene chiamata, ma che non restituisono alcun output. In C (e C++) questa distinzione è fittizia, dato che entrambi ti tipi di funzione hanno la medesima sintassi e logica costruttiva; merito del tipo di dato void! Una funzione infatti viene definita attraverso il proprio nome, le variabili di ingresso e la variabile di uscita: se quest'ultima è una variabile di tipo void la funzione risulta già essere una procedura!
Ad ogni modo, una funzione viene definita con la stessa sintassi di un programma main, e anzi è più corretto dire che è il main ad essere una funzione particolare, in quanto è la prima che viene eseguita quando si lancia il programma. Come spesso accade, un esempio è più chiaro di mille parole: supponiamo di dovere ottenere la distanza tra due punti su un piano di cui si conoscono le coordinate cartesiane (Le funzioni sqrt e pow della libreria math.h implementano ripettivamente la radice quadrata e la potenza di un numero}:
#include <iostream.h>
#include <math.h>
struct Punto {float X, Y};
float distanza(struct Punto Punto1, struct Punto Punto2)
{
return(sqrt(pow((Punto1.X - Punto2.X), 2)
+ pow((Punto1.Y - Punto2.Y), 2)))
}
void main()
{
struct Punto A, B;
cout << "Coordinate del punto A:\nX: ";
cin >> A.X;
cout << "Y: ";
cin >> A.Y;
cout << "Coordinate del punto B:\nX: ";
cin >> B.X;
cout << "Y: ";
cin >> B.Y;
cout << "\nLa distanza tra A e B \'e: " <<
distanza(A, B) << endl;
}
È chiaro come in questo semplice caso l'uso della funzione sia poco utile, ma si pensi a cosa accade in un grosso programma in cui le distanze da calcolare sono centiaia o migliaia, come ad esempio un software di tipo CAD o un videogioco o un qualsiasi software ad interfaccia grafica. In questo caso è evidente quanto sia essenziale avere pronta una funzione che restituisce il valore pronto ogni volta su dei dati diversi con una semplice chiamata. Per non parlare del guadagno in leggibilità che appare chiaro nel codice: anziché l'astrusa formula matematica (resa ancora più di non immediata interpretazione una volta tradotta in codice C), si legge nel testo del main soltanto la chiamata distanza(A,B), chiaramente comprensibile anche senza aver studiato l'implementazione della funzione stessa.
La chiamata della funzione può avvenire in vari modi: nell'esempio fatto compare all'interno di una chiamata di cout, intendendo che il risultato della funzione sarà visualizzato a schermo, ma si può ovviamente assegnare la funzione ad una variabile (purché ovviamente essa sia dello stesso tipo restituito dalla funzione): in pratica la chiamata alla funzione nel codice sta per il suo risultato. Per quanto riguarda parametri passati alla funzione, non è necessario che essi abbiano lo stesso nome di quelli usati nella definizione della funzione, e anzi è bene evitarlo per non fare confusione. È importante invece mantenere l'ordine che è stato definito durante la definizione della funzione; se quest'ultima prevede come parametri un float e un int in questo ordine, bisogna quindi mettere nella chiamata prima un float e poi un int. Per concludere, se invece la funzione non richiede alcun parametro si può lasciare vuote le parentesi tonde oppure inserire un void.
Non è necessario definire il codice di una funzione subito dopo la sua definizione: a volte risulta utile (è una prassi molto usata nella programmazione ad oggetti) definire soltanto il prototipo e lasciare il codice in un'altro punto del programma. Per far questo basta ripetere la riga che definisce il nome, i parametri e l'output della funzione (il suo prototipo, appunto) laddove è effettivamente necessario aver definito la funzione, terminando però la riga stessa con un punto e virgola, anziché inserire le parentesi graffe che contengono l'implementazione.
Ogni parte di un programma ha le sue variabili: il fatto che esse siano oppure no visibili oppure no dalla singola funzione dipende da \emph{dove} sono definite, e in base a questo esse vengono classificate come:
Esempio:
#include<iostream.h>
int a = 5, b = 11; //Variabili globali
int calc(int c, d)
{
int e; //Variabile locale
c++; d++;
e = c*d;
cout << "a vale " << a << ", b vale " << b << endl;
cout << "c vale " << c << ", d vale " << d << endl;
return{e};
}
void main()
{
int f; //Variabile globale
f = calc(a,b);
cout << "f vale " << f << endl;
cout << "a vale " << a << ", b vale " << b << endl;
//cout << "c vale " << c << ", d vale " << d << endl;
}
Nell'esempio a, b, f sono le variabili globali, mentre c, d, e sono locali della funzione calc. Si vede infatti che togliendo il commento all'ultima riga del main si otterrà un errore dal compilatore di un riferimento a variabili inesistenti, perché al momento dell'esecuzione di quel comando cout le variabili con quel nome non sono già più in memoria.
Bisogna tenere presente che nel caso controverso di una variabile locale che abbia lo stesso nome di una globale, all'interno della funzione locale il nome viene considerato assegnato alla variabile locale, mentre quella globale diventa irragiungibile. Questo fenomeno si chiama shadowing. Ad esempio il seguente programma visualizzerà prima il numero 10, poi un 5 (Notare che, restituendo un tipo void, la funzione non ha bisogno di return.):
#include <iostream.h>
int a = 10;
void locale()
{
int a = 5;
cout << a << endl;
}
void main()
{
cout << a << endl;
locale;
}
Uno dei problemi più spinosi quando si parla di funzioni è il passaggio dei parametri, ovvero l'effetto che ha l'esecuzione della funzione sulle variabili una volta terminata l'esecuzione della funzione stessa. Come è noto (e se non lo fosse lo spiego qui ora), l'esecuzione di una funzione può avere due differenti effetti su una variabile che le viene passata come parametro:
Spesso il passaggio per valore è più che sufficiente per la programmazione di base, e se anche si avesse la necessità di far si che la funzione alteri il valore di una variabile, è probabilmente possibile creare una variabile globale che non richiede nessun passaggio di parametri e che viene tranquillamente modificata dal programma. Per questo motivo preferisco rinviare la spiegazione di come fare il passaggio per riferimento al paragrafo dedicato ai puntatori.
Soltanto una breve discussione sull'opportunità di lavorare con delle librerie e sul modo di includerle. Si è già visto come si possono importare le librerie di sistema: quelle di default si includono tra i segni di minore e maggiore, mentre quelle personali tra virgolette. Di librerie ce ne sono a bizzeffe, e basta andare a sbirciare la cartella /usr/include di qualsiasi installazione di Linux per rendersene conto. Senza poi contare che la Rete può essere anch'essa fonte inesauribile di dati. Spesso non conviene andare a sviluppare una funzione che non sia banale o troppo specifica per quello che stiamo facendo, perché è probabile che la si possa trovare gratuitamente fra le risorse del sistema (magari questo per gli utenti Windows è un po' più difficile...) o in rete. Per implementare una libreria è sufficiente inserire il giusto #include e conoscere i prototipi delle funzioni implementate. E spesso è buona norma organizzare il proprio lavoro in librerie perché può sempre tornare utile, anche quando meno ce lo si aspetta.
A volte non vengono date le librerie in codice sorgente C o C++ (i file .C, .CPP, .H, .HPP), ma le librerie possono essere importate anche in formato già compilato: sono le cosiddette librerie dinamiche (sotto Windows sono i file .DLL, nei sistemi Unix hanno diverse estensioni, il più comune è il .so sotto Linux), ovvero dei veri e propri eseguibili che contengono una o più funzioni da usare allo stesso modo di quelle incluse nelle librerie non compilate. In questo caso però il loro utilizzo necessita di conoscere il prototipo della funzione inclusa nella libreria, dato che non lo si può andare a sbirciare nel listato. L'importazione di questi file è più complessa di quella dei semplici sorgenti, in quanto oltre ad avere la libreria è necessario possedere anche le informazioni su quali funzioni vengono messe a disposizione da tale libreria e il modo di compilarle. Queste informazioni sono contenute in altri file di corredo che variano profondamente a seconda del sistema operativo utilizzato, del compilatore e del linguaggio di programmazione (le librerie infatti in questo caso sono già compilate e e perciò le funzioni che implementano sono utilizzabili da programmi scritti in qualsiasi linguaggio).
Visto che la materia è vasta e scottante, mi limiterò a presentare molto in breve come di importa una .DLL in Windows, più per fare un esempio che per pretendere di illustrare il problema in nella sua complessità. A chi dovesse servire la Rete saprà sicuramente fare migliore e più specifico servigio.
Innanzitutto bisogna dire che le librerie tipo .DLL sono a link dinamico, ovvero funzionano secondo un meccanismo essenzialmente diverso da quello delle librerie statiche, che sono invece quelle compilate a partire dai sorgenti. Nel caso delle librerie statiche infatti da ogni file sorgente viene creato un file oggetto (di solito .obj oppure .obj) durante la compilazione. Nella successiva fase di linking, il compilatore assembla tutti questi file, che sono i frammenti del programma in un unico file, che rappresenta l'eseguibile. Tutto il codice di esecuzione del programma è racchiuso in questo file ed in nessuna altro. Nel caso invece delle librerie dinamiche il file eseguibile contiene soltanto il riferimento al caricamento della libreria e alla interfaccia con essa. L'eseguibile che si ottiene dunque ha vita propria ma non riesce ad eseguire le funzioni contenute nella libreria se quest'ultima non è caricata nel sistema. Infatti ogni volta che al programma perviene una chiamata di tali funzioni, quest'ultimo provvede soltanto a caricare in memoria la libreria che provvederà autonomamante ad eseguire la procedura richiesta e a restituire i risultati al programma. Se gradite una metafora, è la stessa differenza tra un'azienda che produce tutto in proprio ed una che esternalizza a terzi alcune parti della produzione e poi assembla in casa. Se a livello di architettura del sistema operativo questa cosa comporta parecchie implicazioni, dal vostro punto di vista l'unica cosa da tenere a mente \e di inserire i file delle librerie nelle distribuzioni dei vostri programmi.
Detto questo esistono due modi di includere le DLL in un programma, definiti link implicito e link esplicito.
Per chi disponesse di una DLL ma non dei prototipi delle funzioni da essa fornite, esiste un trucco per ottenerene almeno i nomi. Si può applicare soltanto se avete a disposizione un vecchio sistema con Windows 95 o 98: esiste infatti una piccola utility che si installa come componente opzionale di Windows chiamata Anteprima o QuickShow che consente, cliccando con il tasto destro del mouse su una DLL di vederne parecchie informazioni, tra cui anche i nomi delle funzioni incluse. Chiaramente, trattandosi di una cosa utile, Microsoft ha ben pensato di eliminarla da tutte le versioni successive di Windows...