Originariamente pubblicato in data 15/05/1999
A
C
C
O
M
A
Z
Z
I
Il C incrementato
Il C incrementato
Il linguaggio C++ è il più diffuso tra i programmatori professionisti. Ecco una presentazione, mirata a chi sa già programmare in altri linguaggi
Il linguaggio di programmazione più utilizzato professionalmente in questo ultimo scorcio degli anni '80, non c'è dubbio, è il linguaggio C. Da poco tempo a questa parte, però, l'attenzione degli addetti è attratta da un nuovo linguaggio, diretto successore del C: il C++.Bit è stata la prima rivista italiana a presentare un corso di C; oggi vogliamo essere i primi a introdurre il C++ ai nostri lettori.
Il linguaggio C è nato dall'esperienza dei Bell Laboratories, una divisione del gigante americano AT&T. Oggi la AT&T propone ai programmatori, come successore di quel linguaggio, il C++: l'offerta è appetitosa, poiché, come il suo nome vuole intendere, il nuovo linguaggio contiene perfettamente il vecchio. Un compilatore C++, in altri termini, comprende perfettamente tutti i costrutti e le parole chiave del C standard, e aggiunge a queste numerose espansioni, specialmente mirate nelle aree nelle quali il C tradizionale è più debole. Questo significa che un esperto programmatore C può effettuare un passaggio morbido dal linguaggio conosciuto al C++, semplicemente aggiungendo nuove frecce al suo arco.
Le caratteristiche del nuovo linguaggio, che passeremo in rassegna in questo breve articolo, sono davvero notevoli: tanto da convincere diverse software house attive nella creazione di compilatori ad investire nello sviluppo di un compilatore C++. Oggi sono disponibili alcuni compilatori C++ sotto Unix e sotto MS DOS; una spinta notevole al linguaggio è stata data da Apple, che ha annunciato il prossimo rilascio di un compilatore C++ per Macintosh sotto Mpw. Presumibilmente seguiranno compilatori C++ per le altre macchine più diffuse. Una prima espansione del C++ rispetto al suo predecessore è nella dichiarazione delle funzioni, divenuta più chiara e leggibile, in stile Pascal. Osserviamo come si effettua la dichiarazione di una funzione che effettua la media matematica dei due parametri, prima in C standard e poi in C++:
float media ()
/* In C i parametri possono non venire dichiarati */
float media (float num1, float num2)
// In C++, come in Pascal, è necessario.
Inoltre, il C++ introduce la possibilità di passare i parametri by reference (per indirizzo) senza dover utilizzare esplicitamente i puntatori; un metodo analogo ai parametri var del Pascal:
void StampAzzera (int& x)
//Pascal: Procedure StampAzzera (var x: integer)
{
cout << x;
x = 0;
}
La dichiarazione della variabili, pur obbligatoria, non è esasperata come nei linguaggi più strong typed: in C++ una variabile può venire dichiarata ovunque, e non solo nell'intestazione di una funzione; la variabile ha vita nel blocco che segue la dichiarazione: si hanno così variabili globali, locali e localissime (cioè valgono solo entro il gruppo di istruzioni seguenti). Per esempio, se si deve utilizzare un ciclo for, è possibile creare una variabile di ciclo appositamente per l'occasione:
for (int ciclo = 0; ciclo < 100; ciclo++)
cout << ciclo;
// Stampa i numeri da 0 a 100.
Quest'ultima caratteristica è presa da Ada. Vi sono molte altre idee che il C++ recepisce dai linguaggi della famiglia dell'Algol, Pascal e Ada in testa. Tra queste, segnaliamo la dichiarazione di costanti:
const pi = 3.1415926;
e la capacità di istanziare variabili dinamiche. Nel C standard non c'è un metodo pulito per allocare variabili dinamiche, e cioè per assegnare uno spazio in memoria al momento dell'esecuzione, riferito via puntatore. Il C fornisce la funzione calloc, realizzata a sua volta a partire dalla primitiva malloc (contrazione di memory allocation, e cioè allocazione di memoria); quest'ultima deve essere fornita dal sistema operativo. Con malloc e calloc il C assegna uno spazio di memoria, di dimensione specificata dal programmatore ed espressa in byte. In C++, invece, gli operatori new e delete si comportano come i loro (quasi) omonimi new e dispose del Pascal. Per esempio, vediamo come sia possibile in C++ copiare una stringa, di nome sorgente, in una variabile dinamica puntata da dest, un puntatore a carattere:
char * dest;
dest := new char [strlen (sorgente)];
strcpy (sorgente, dest);
Altre due idee di eccezionale potenza nelle mani di un programmatore esperto che il C++ copia da Ada sono l'uso di parametri con default e la capacità di definire un numero variabile di parametri nelle funzioni.
Cominciamo dalla prima possibilità: il C++ ha la capacità di assegnare un valore di default a uno o più parametro di una funzione. Prendete ad esempio questa funzione, che esegue l'elevamento a potenza di un numero intero:
long potenza (int base, int esponente = 2)
{
long risultato = 1;
for (int i = 1; i < esponente + 1; i++)
risultato *= base;
return risultato;
}
Il nostro esempio costituirebbe una semplicissima funzione C se non fosse per la presenza dell'assegnamento = 2 di fianco alla dichiarazione del parametro esponente. Quello che accade è che abbiamo definito un valore di default per l'elevamento a potenza, e cioè l'elevamento al quadrato. Se noi chiameremo la funzione potenza con:
a = potenza (5, 3);
otterremo il valore di 53, ovvero 125, ma se tralasciassimo il secondo parametro:
a = potenza (5);
il C++ eseguirebbe la funzione dando al secondo parametro il valore di 2, e cioè ci restituirebbe 5 al quadrato, ovvero 25.
Veniamo al numero variabile di esponenti. L'uso di una procedura o funzione che accetta un numero variabile, non prefissato, di parametri è una caratteristica diffusa in moltissimi linguaggi di programmazione, ma in genere non utilizzabile dal programmatore. Se prendiamo ad esempio la chiamata Write, oppure la Read, del Pascal, notiamo subito che è consentito utilizzare un numero variabile di parametri. É possibile scrivere:
WriteLn ('Ciao!');
WriteLn ('La risposta è: ', Risposta);
Write ('Totali: ', Tot1, Tot2, Tot3, Tot4);
Nel C++ ci viene data la possibilità di definire noi stessi delle funzioni che accettano un numero variabile di parametri. All'interno della funzione il codice è in grado di scoprire quante variabili sono state passate, di che tipo sono, e può accedere ai parametri uno alla volta. Il meccanismo è moderatamente complesso, e richiede l'uso di alcune macro fornite nella libreria standard stdarg.h del C++. Vediamo solo la dichiarazione di una simile funzione:
void Parm_Variabili (...)
// Tre punti indicano
// che è possibile usare
// un numero variabile di parametri Una caratteristica estremamente potente del linguaggio C++, presa ancora una volta da Ada e dai linguaggi più dinamici, è la capacità di effettuare overloading di operatori, e cioè di dare nuovi significati agli operatori incorporati del linguaggio.
Ogni programmatore dovrebbe sapere (anche se forse non vi ha mai riflettuto a fondo) che quasi tutti i linguaggi forniscono operatori, procedure e funzioni in grado di lavorare su molti tipi distinti di variabili. Basti pensare alla Write di Pascal e all'equivalente PRINT del Basic: queste procedure sono in grado di stampare sullo schermo non solo stringhe di caratteri ma anche numeri interi e in virgola mobile. Un altro esempio è l'operatore + (il più), che viene usato per sommare tra di loro sia numeri interi che numeri reali che, in Basic, addirittura stringhe alfanumeriche.
In tutti questi casi il programmatore utilizza un solo simbolo (Write o +) per chiarezza: il compilatore, osservando il tipo della variabile sulla quale si opera, crea un pezzo di codice macchina adeguato a quella particolare istanza nel programma. Wirth, il creatore di Pascal, in seguito è arrivato alla conclusione che l'overloading degli operatori è poco pulito, e con cristallinità tutta svizzera ha provveduto ad escluderlo dalla sua creatura successiva, il Modula 2.
Ma torniamo al C++: questo linguaggio ci da la possibilità di aggiungere nuovi significati ad un operatore già esistente.
Ammettiamo, per esempio, di aver creato un nuovo tipo di variabili, il tipo punto, caratterizzato da due numeri reali che, come sappiamo, individuano un punto matematico sul piano cartesiano.
class punto {
double x, y;
public:
void operator + (punto);
};
Cos'è questo mostro, chederà qualcuno? Per il momento prendete il costrutto class, sul quale spenderemo qualche parola nel seguito, come una espansione del più familiare typedef: abbiamo definito un tipo punto, caratterizzato come avevamo stabilito da una struct contenente due numeri reali in formato double; e abbiamo espresso il nostro desiderio di espandere l'operatore + in modo che sia possibile usarlo per effettuare la somma di due punti. Overloading significa proprio questo: sovraccaricare un operatore di nuovi significati.
Concettualmente, la cosa è semplice: dobbiamo sommare le singole componenti x ed y, le coordinate dei punti. Ecco come possiamo farlo in C++:
void punto::operator+ (punto pt)
{
x += pt.x;
y += pt.y;
}
Dopo aver effettuato questa redefinizione potremo usare l'operatore + per sommare due punti.
punto p1, p2;
...
p1 + p2;
La mia ridefinizione lascia il risultato nel primo dei due punti: in alcuni casi sarebbe più appropriato avere un operatore che restituisce la somma in una terza variabile: il meccanismo resterebbe comunque lo stesso.
Nei paragrafi precedenti abbiamo visto come il C++ migliori le caratteristiche del suo predecessore, il linguaggio C, aggiungendo numerose funzionalità e caratteristiche. Abbiamo tenuta per ultima l'innovazione più rivoluzionaria: la capacità del C++ di operare come linguaggio object-oriented.
Nei paradigmi tradizionali di programmazione noi ci troviamo ad operare su degli ogetti (le variabili) con il nostro codice. Le procedure e le funzioni che scriviamo sono, in ultima istanza, delle azioni, dei verbi, che operano sui dati. La programmazione orientata all'oggetto è, da questo punto di vista, una vera rivoluzione copernicana, che accentra l'attenzione del programmatore non più sulle azioni, e quindi sugli algoritmi, ma bensì sugli oggetti.
Questo nuovo modo di programmare è giunto all'attenzione del mondo dei programmatori quando ci si è resi conto che in molti ambienti di lavoro complessi, come l'ambiente iconico basato sul mouse che oggi è diffusissimo sui personal computer, lo sviluppo di software in modo object-oriented permette di ridurre drasticamente i tempi di sviluppo: in alcuni casi sino a un terzo di quello necessario operando in modo tradizionale.
Un Pascal espanso per consentire la programmazione object oriented, chiamato MacApp, è stato introdotto qualche anno fa da Apple Computer per il Macintosh, incontrando approvazione entusiastica da parte dei programmatori. Oggi MacApp è giunto alla versione 2.0 sotto l'ambiente di sviluppo Mpw 3.0, e una sua implementazione sul modello cadetto di casa Apple, l'Apple IIgs, è imminente. Da parte sua Microsoft, alfiere della programmazione in ambiente MsDos, non sta certo a guardare: iin una recente intervista apparsa su queste pagine Billy Gates, tychoon della software house, dichiarava esplicitamente il suo massimo interesse per la programmazione per oggetti.
Una trattazione completa di questo nuovo modo di programmare richiederebbe non un articolo ma un libro: saremo, di conseguenza, necessariamente schematici, ma tenteremo di fornire al lettore un'idea chiara dei principi che stanno alla base della programmazione orientata all'oggetto.
La classe in C++
Ammettiamo di dover manipolare degli oggetti di forma rettangolare sullo schermo grafico del calcolatore. Un problema tutt'altro che insulso, poichè le finestre e i dialoghi degli ambienti iconici sono tipicamente rettangolari.
La soluzione classica prevede di creare un tipo rettangolo, e di scrivere una serie di procedure e funzioni che operano su oggetti di quel tipo.
typedef struct {
int alto, sinistra, basso, destra;
// sono le coordinate di due vertici
} RECT;
int area (RECT r)
// eccetera...
Nella programmazione orientata all'oggetto, le procedure e funzioni che operano sull'oggetto sono incapsulate nella definizione dell'oggetto. É possibile dichiarare queste routine in modo che siano pubbliche (accessibili dal programma che utilizza gli oggetti così definiti) o private (accessibili solo dall'interno della definizione). Questo approccio è flessibile ma solido, poichè permette di mascherare i dettagli implementativi del codice.
Vediamo come è possibile in C++ dichiarare l'oggetto rettangolo in modo che su di esso siano definite le operazioni di assegnamento, calcolo dell'area e del perimetro:
class rect {
// La parte privata contiene
// le variabili che descrivono l'oggetto
int x1, y1, latoV, latoO;
public:
// Parte pubblica: le funzioni che
// operano sull'oggetto:
rect (int alto, sinistra, basso, destra);
int Area ();
int Perimetro ();
}
rect::rect (int alto,int sinistra,
int basso,int destra)
// Costruttore dell'oggetto: crea una
// nuova istanza dell'oggetto rect.
{
y1 = alto;
x1 = sinistra;
latoH = destra - sinistra;
latoV = basso - alto;
}
int rect::Area ()
{
return latoV * latoO;
}
int rect::Perimetro ()
{
return 2 * (latoV + latoO);
}
Notiamo la presenza, tra le funzioni che abbiamo dichiarato, di una funzione che ha lo stesso nome dell'oggetto: questa funzione, che pronde il nome di costruttore dell'oggetto, viene automaticamente chiamata quando creiamo un nuovo oggetto di tipo rettangolo.
La caratteristica più importante della programmazione basata su oggetti è la capacità di definire nuovi oggetti basati su oggetti già definiti. Per esempio, potremmo definire un oggetto chiamato finestra, basato sul tipo rettangolo. Aggiungeremmo altri dati (un titolo per la finestra, magari dei colori) e altre funzioni (per spostare la finestra, per ridimensionarla e così via) ma potremmo sfruttare le funzioni già definite, il calcolo dell'area e del perimetro.
Ci fermiamo qui con la nostra trattazione del C++: un linguaggio al quale speriamo di aver interessato il lettore, un linguaggio che a nostro avviso ha tutte le carte in regola per divenire molto importante nella panoramica dell'informatica degli anni '90.