Max Stirner e la controcultura hacker

Introduzione al C++

di Davide CD-Ron Rondini

Capitolo 1 - Per iniziare

PERCHE' IL C++

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:

UN PROGRAMMA VUOTO

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.

IL CLASSICO PROGRAMMA "CIAO MONDO"

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.

I COMMENTI

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:

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.

I TIPI DI DATO FONDAMENTALI

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:


C++, Le parole dedicate del linguaggio sono:

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++:

Il 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.

USO DEI TIPI DI DATO

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

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;

GLI OPERATORI LOGICI E DI RELAZIONE

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

LE CONVERSIONI FRA TIPI DIVERSI

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

Introduzione al C++

Capitolo 2 - Le strutture di controllo

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.)...

IF...THEN...ELSE

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

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à...

IL CICLO DO...WHILE

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);
}

IL CICLO FOR

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.

LA SCELTA MULTIPLA

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;
}

Introduzione al C++

Capitolo 3 - Le Funzioni

Dividere un programma in parti di codice riutilizzabile è diventato fondamentale per tutti i linguaggi di programmazione che aspirino ad avere più di 5 giorni di vita: è una cosa tanto basilare che non intendo dilungarmi su che cosa siano le funzioni e perchè servono nei programmi.

3.1 COME SI SCRIVE UNA FUNZIONE

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.

3.2 VARIABILI GLOBALI E LOCALI

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;
}

3.3 IL PASSAGGIO DEI PARAMETRI

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:

  1. Può essere lasciata inalterata, ovvero il suo valore rimane lo stesso che aveva prima della chiamata della funzione stessa. È questo il caso che avviene in C++ di default e che viene detto passaggio dei parametri per valore. Per far questo in pratica il programma alloca una variabile locale al momento dell'esecuzione della funzione in cui viene copiato il valore della variabile globale passata alla funzione. Questo modo di procedere è quasi sempre più sicuro per quel che riguarda l'accesso alla memoria e per questo è la scelta predefinita del C++.
  2. Può venire modificata dalla funzione stessa, e quindi il valore che assume alla fine dell'esecuzione della funzione risulta quello che è stato assegnato all'interno della funzione stessa. Questo tipo di passaggio non è direttamente definito in C++ e per utilizzarlo è necessario ricorrere ai puntatori. Questo è il passaggio dei parametri per riferimento.

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.

3.4 LE LIBRERIE: PERCHÈ RIFARE CIÒ CHE È GIÀ FATTO?

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...

Home Page