LINGUAGGI

PROGRAMMARE IN
C
Patti chiari, amicizia lunga


di Mr. Lambda


   Avviarsi sulla luminosa strada della programmazione con il linguaggio C è possibile a patto che sappiamo adattare le nostre conoscenze di programmazione al nuovo ambiente oppure ci accostiamo fin dall'inizio con idee chiare e precise alle possibilità offerte da questo sorprendente linguaggio. Ed è proprio questo lo scopo di questo nostro incontro. Quindi inforcate i vostri occhiali da sole e seguiteci. 

   Negli articoli che via via vi introdurranno al linguaggio, il C non sarà lasciato a se stesso, ma volta per volta verrà messo in relazione ad altri linguaggi come il BASIC, il Pascal, l'Assembly, ecc. E questo perché riteniamo che alcuni aspetti del C saranno facilmente appresi da coloro che conoscano sufficientemente un altro linguaggio di programmazione. Prendiamo, per esempio, la funzione C chiamata printf() che stampa messaggi sullo schermo. Il suo nome significa ' print formattato '. Se la parola ' funzione ' vi e' nuova, ma non lo dovrebbe essere, potete sostituirla con le parole ' subroutine ' oppure ' procedura '. Una funzione e' una parte di un programma che esegue un determinato lavoro, come stampare messaggi sullo schermo, oppure leggere file dal disco. 
   Ma prima di continuare concediamoci alcuni brevi riferimenti storici. La maggior parte delle Introduzioni al linguaggio C comprendono riferimenti storici per parecchie buone ragioni. Innanzitutto calarsi nella giusta atmosfera e quindi comprenderne i rapporti con l'evoluzione informatica. Brian Kernighan e Dennis Ritchie hanno scritto il libro "The C Programming Language", che voi potete leggere nella traduzione offerta dalla Jackson, nel 1977. Ma il linguaggio C era stato già sviluppato agli inizi degli anni settanta sotto il sistema operativo Unix. Infatti, come già dovreste sapere, a tutt'oggi il sistema operativo Unix è scritto proprio in C e il C a sua volta adotta alcune convenzioni del sistema Unix, come il modo standard dei programmi in C di accettare input e inviare output. Ma c'è dell'altro. E ci sono altre ragioni per questo. C e Amiga sono strettamente imparentati. AmigaDOS, il sistema operativo di Amiga, è basato sul sistema operativo TRIPOS. Martin Richards della Cambridge University, uno dei ricercatori che ha contribuito maggiormente a TRIPOS, ha pure scritto un linguaggio chiamato BCPL, che a sua volta ha ampiamente influenzato la formulazione del linguaggio C e insieme al linguaggio C è stato utilizzato per lo sviluppo del sistema operativo di Amiga. Ma non basta. Commodore-Amiga ha utilizzato il C per scrivere Intuition e Workbench, il software a window e menu di Amiga. 
   Ormai si può dire, senza timore di smentite, che il C è divenuto il linguaggio preferito tra i programmatori per tutte le sue straordinarie qualità e i pochi difetti. Il C è rapido, soddisfacente per la maggior parte delle applicazioni. Il C è veramente versatile e flessibile. Permette al programmatore di fare quasi ogni cosa. Mentre il Pascal può rifiutarsi di permettere l'assegnamento di una variabile in virgola mobile a una variabile carattere, e il BASIC si rifiuta di assegnare una variabile stringa a una variabile intera, il C permette con facilita tali assegnamenti. Per quanto siano possibili, queste mescolanze di tipi di dati non sono consigliabili, ma in ogni caso il C assume che voi sappiate che cosa state facendo e compie esattamente ciò che gli avete chiesto. 
   Questa caratteristica del linguaggio viene chiamata ' typing '. IL Pascal e' un linguaggio fortemente tipizzato, dal momento che l'operatore ' : = ' non ci consente di assegnare un numero in virgola mobile a una variabile carattere. Il BASIC e' meno fortemente tipizzato , dal momento che ' = ' assegnera' numeri in virgola mobile ad interi e viceversa. Tuttavia il C è debolmente tipizzato, anche se offre una varietà e complessità di tipi di dati di tutto rispetto. Il compilatore può produrre un avvertimento o warning nel caso che i tipi di dati non corrispondano, ma continuerà a fare il suo dovere fino in fondo e il programma girerà ugualmente. 
   Ma se qualcuno a questo punto sta già svenendo o giù di lì per la terminologia utilizzata, vi proponiamo i seguenti commenti. I numeri in virgola mobile sono numeri con una componente frazionale. l computer devono rappresentare 7,302 come un numero a virgola mobile, mentre 3751 può essere memorizzato come un numero intero. E importante osservare che ciascun tipo una quantità diversa di spazio di memoria per essere memorizzato. I programmatori più smaliziati considerano il comportamento del C rispetto ai tipi di dati come una qualità del linguaggio e vi si riferiscono in termini di flessibilità. Coloro invece che si avvicinano al C dopo essersi abituati a programmare in Pascal o BASIC considerano questa libertà del linguaggio come una disgrazia. Il C, infatti, ha pochi avvertimenti del tipo abituale in Pascal oppure BASIC. Anche il C è un linguaggio compilato esattamente come il Pascal o il Modula-2 e diversamente dal BASIC, che è un linguaggio interpretato. E se voi conoscete il Pascal oppure il Modula- 2 apprenderete molto più facilmente il C. Quando si parla di linguaggio ' compilato ' significa che un programma viene convertito in un'altra forma prima di essere eseguito. Per i linguaggi compilati, un programmatore crea un testo sorgente o codice sorgente con un editor, e quindi compila il codice sorgente con il compilatore, producendo un codice oggetto o modulo oggetto. I moduli oggetto devono essere poi linkati con un programma linker per produrre un programma eseguibile. Il compilatore traduce il codice sorgente nel linguaggio macchina del computer, e produce una lista di variabili e funzioni utilizzate all'interno di questo programma. Per esempio, un programma puo' utilizzare la funzione printf(). Questa funzione può far parte del linguaggio, ma deve essere definita da qualche parte al di fuori dal codice sorgente, a meno che non la ridefiniate voi per l'occasione. La funzione printf() viene detta appunto esterna o non risolta, dal momento che il compilatore non può generare codice macchina per essa. Un modulo oggetto è composto da questa mescolanza di linguaggio macchina e liste di riferimenti non risolti (unresolved references). 
   Dopo che il programma è stato compilato, tutti questi riferimenti alle funzioni e alle variabili devono essere risolti con il linker. 
   Se voi comprerete un compilatore C, insieme con esso avrete a disposizione diverse collezioni di funzioni pre-scritte e pre-compilate, chiamate librarie. Le funzioni standard come printf() sono incluse in una libreria, mentre sin(), cos() e altre funzioni matematiche sono incluse in un'altra.
   Il modulo oggetto prodotto precedentemente viene linkato con una o più librerie. Il linker utilizza la lista delle funzioni e variabili non risolte del modulo oggetto per individuare ciascuna libreria di cui servirsi. Se una funzione viene trovata in una libreria, vengono cercati il suo codice macchina e la sua lista di riferimenti non risolti, fino a trovare e risolvere tutti i riferimenti. Quindi solamente il linker può produrre un programma eseguibile. Questo file viene composto solamente dalle funzioni che necessarie al programma. Le altre funzioni di libreria vengono lasciate fuori.
   Se il linker non può trovare un riferimento, non può produrre un programma corretto, dal momento che manca qualcosa che costituisce il programma. Il programma linker si fermerà dopo aver stampato una lista di funzioni e/o variabili esterne (unresolved externals) che non riesce a trovare nelle librerie a disposizione. In questo caso, il programmatore dovrebbe esaminare l'output del linker ed effettuare le correzioni del caso. Spesso il testo sorgente contiene errori di ortografia. Se printf() è stato scritto prnitf(), il linker non riuscirà mai a trovare prnitf(), anche se è in grado di individuare printf(). Se, per esempio, sin() e cos() compaiono nella lista di output del linker, il programmatore può anche aver dimenticato di includere la libreria di funzioni matematiche. La vostra abilità di interpretare correttamente l'output del linker si svilupperà con il tempo. 
   Questo processo di compilazione e linkaggio vi costringe a scrivere molto nel CLI. Voi potete però servirvi sia di speciali comandi presenti nei compilatori stessi - LC nel compilatore Lattice C per esempio -, sia di particolari file batch o execute chiamati MAKE. Nell'articolo dedicato al C del numero precedente della rivista vi abbiamo appunto presentato un'utility MAKE che vi permette di utilizzare efficacemente il compilatore Lattice C 3.10. Se voi avete creato un programma C chiamato Prova.c e volete compilarlo, è sufficiente che scriviate sulla linea comandi del CLI. 

   EXECUTE MAKE PROVA 

   E il gioco è fatto. Come vi abbiamo già spiegato il file batch o execute contiene tutti i comandi CLI necessari per la compilazione e il linkaggio di un programma C, evitandovi un mucchio di digitazione ad alto rischio! 

Primi passi 

   Esaminiamo con attenzione un breve e semplice programma in C, BASIC, e Pascal. Il programma moltiplicherà due numeri e stamperà il risultato. Eccolo in C: 

#include "stdio.h"
/* un programma per moltiplicare due numeri */ 

main()
{
  int a,b,c; 
  a = 3;
  b = 4;
  c = a* b;
  printf("Il risultato è %d\n",c);
}

   Eccolo invece in BASIC:

5 REM un programma per moltiplicare
6 REM due numeri
10 a = 3
20 b = 4
30 c = a * b
40 print "Il risultato è ";c 

   E in Pascal: 

program add(input,output);
integer a,b,c;
(* un programma per moltiplicare due numeri *)
begin
   a:= 3;
   b:= 4;
   c:= a*b;
   writeln('l1 risultato:',c);
   end. 

   Per prima cossa potete notare che un programma in C ha molta puntaggiatura. Il C utilizza la puntaggiatura nello stesso modo in cui Pascal utilizza ' begin ' e ' end '. Questo sembra rendere i programmi in C piu' difficili da leggere dei programmi in Pascal, per esempio. Mentre il Pascal utilizza ' begin ' e ' end ' per delimitare l'inizio e la fine delle procedure e delle funzioni, il C utilizza le parentesi graffe ' { ' e ' } '. Inoltre, il Pascal e il C separano i commenti al programma dal codice ancora con l'utilizzo della punteggiatura. Il C utilizza la coppia di ' /* ' e ' */ ', mentre il Pascal utilizza ' (* ' e ' *) '. Il BASIC utilizza invece lo statement REM all'inizio della linea per segnalare all'interprete un commento. Coloro che utilizzano AmigaBASIC sanno pero' che i commenti possono venire segnalati al compilatore anche attraverso l'utilizzo dell'apostrofo ( ' ) e che non sono necessari i numeri di linea. Quasi tutto cio' che viene detto a proposito del Pascal può essere applicato al Modula-2.
   Sia il Pascal che il C sono linguaggi compilati. Osservate come il Pascal e il C dichiarino esplicitamente negli esempi le variabili a, b e c. Il Pascal scrive ' integer' dove il C scrive ' int ' per dichiarare una variabile integera. 
   Questo e' comune ai linguaggi compilati. Un compilatore infatti ha bisogno di conoscere il tipo (type) della variabile prima di utilizzarla nel programma. Questa conoscenza verrà utilizzata per eseguire correttamente le operazioni, come addizioni o moltiplicazioni, quando questa variabile viene utilizzata successivamente, poiché numeri interi e numeri a virgola mobile vengono utilizzati differentemente e occupano un diverso spazio di memoria. L'esempio in BASIC non necessita di dichirazioni di tipo esplicite per ciascuna variabile. Il BASIC è un linguaggio interpretato. (Naturalmente esistono anche ottimi compilatori per il BASIC come quello offerto dalla Microsoft per AmigaBASIC.) 
   Quando voi digitate RUN, il computer analizza il testo di ciascuna linea per eseguire successivamente i diversi passi del programma. Se il BASIC incontra una variabile che non riconosce, asssume che quella variabile sia un numero in virgola mobile. Quindi nel nostro esempio a,b e c sono variabili in virgola mobile. 
   Il C ha pure anche altri tipi di variabili oltre a ' int ': ' float ' per virgola mobile, e ' char ' per variabili carattere e altre che vedremo a suo tempo. Ciascun tipo possiede un intervallo valido di valori. Per esempio. ' int ' e' limitato a più o meno due bilioni in Amiga, mentre ' char ' e' limitato ai valori tra +127 e -128. In realtà per questi intervalli bisogna sempre tenere anche conto delle implementazioni reali dei vari compilatori. 
   I tipi ' integer ', cioe' interi come ' char ' e ' int ', possono venire modificati dalla parola ' unsigned ' - senza segno -, creando in tal modo i tipi ' unsigned char ' e ' unsigned int '. Questi tipi hanno rispettivamente intervalli tra 0 e 255 e tra 0 e oltre i 4,2 bilioni. Se voi conoscete bit e byte, potrete dedurne che questi valori possono essere rappresentati in 8 e 32 bit. Ciascun tipo ha diversi operatori validi, come addizione e sottrazione, moltiplicazione e divisione. Diversamente dal BASIC e dal Pascal, il C e' molto flessibile relativamente alla possibilità di operare con essi. Per esempio, potete addizionare variabili ' char ' e variabili ' int '. Il valore di un ' char ' e' il valore ASCII del carattere memorizzato all'interno di esso. In BASIC potreste ottenere questo valore utilizzando la funzione ASCO, in Pascal potreste invece utilizzare ORDO. 
   Avrete senz'altro osservato che le linee del programma in C e in Pascal terminano con un punto e virgola. Questa è un'altra caratteristica dei linguaggi compilati. Il ' ; ' comunica al compilatore che il programmatore ritiene che la linea sia completa. Infatti, le linee dei programmi in C possono essere distribuite su diverse linee fisiche senza alcuna conseguenza dannosa. 

printf 
(
"Il risultato e' %d"
,
c
)
;

   Il compilatore analizza la linea nello stesso modo. E questo non è possibile con la maggior parte degli interpreti BASIC.
   Incominciamo a prendere in considerazione il codice C.

#include "stdio.h"

   È un comando per una parte speciale del compilatore chiamata preprocessore. Questa linea compilatore: "a questo punto compila anche il file programma chiamato stdio.h". Questo file contiene le dichiarazioni delle variabili e delle funzioni utilizzate da printf() e altre funzioni standard di I/O. Il preprocessore ha anche altre caratteristiche che considereremo in seguito. 
   Osserviamo ora attentamente la linea che contiene la funzione printf(). printf() ha due argomenti, una stringa di caratteri contenente il testo "Il risultato è %d\n" è il nome di una variabile intera, c. Per un programmatore in C questi sono gli argomenti passati alla funzione, gli argomenti della funzione oppure ancora i valori passati alla funzione. Come avrete ormai capito gli argomenti vengono collocati tra le parentesi e separati dalle virgole. 
   Il primo argomento passato alla funzione printf() descrive il formato del testo che noi vogliamo visualizzare sullo schermo. Esso contiene due parti che potrebbero esserci poco familiari, entrambe presenti verso la fine del testo tra virgolette. (Ah, se voi conosceste il FORTRAN, potreste ora contare sulla conoscenza del comando FORMAT!) Il %d significa "qui stampa un valore intero". Il \n significa "qui stampa una carattere di accapo", che dovrebbe muovere il cursore sulla linea successiva, esattamente come succede quando voi premente il tasto Return. Se noi desideriamo visualizzare due valori utilizzando printf(), possiamo scrivere:

printf( "Noi abbiamo %d mele e %d pere.\n",mele, pere); 

   Per ottenere questo risultato:

Noi abbiamo 7 mele e 5 pere. 

   Se naturalmente le variabili ' mele ' e ' pere ' contengono rispettivamente i valori 7 e 5. 
   Noi potremmo chiedere a printf() di stampare un valore in un campo di determinata ampiezza. Se si utilizza %4d come formato per gli interi, il numero sara' visualizzato con gli spazi necessari per riempire quattro posizioni carattere. La funzione printf() ha altri comandi di formattazione - per una visione complessiva e dettagliata consultare sempre il manuale del proprio compilatore -, come %x, per esempio, che stampa valori in esadecimale oppure %C, utilizzato con le stringhe formattate per visualizzare un valore come carattere ASCII, similmente al comando BASIC CHR$(). 
   Le funzioni vengono utilizzate estensivamente in C; e alle funzioni pre-scritte che il linguaggio vi mette a disposizione ben presto si affiancheranno le funzioni che voi stessi scriverete per i vostri programmi. Se voi avete utilizzato il Pascal avrete acquistato una certa familiarità con le procedure e le funzioni. Ebbene le funzioni C ricordano proprio queste ultime dal momento che ritornano tutte un valore. (In realtà esiste la parola riservata VOID che preclude alle funzioni la possibilità di ritornare valori, ma di ciò in seguito, quando ne saprete molto di più.) Le funzioni C, comunque, non possono venire nidificate come succede nel Pascal con le procedure e le funzioni. 
   Se voi non avete mai utilizzato il Pascal, pensate alle funzioni BASIC SIN() e COS(). Queste funzioni ritornano un valore in virgola mobile basato sul loro argomento, e cioè il numero tra parentesi. Tutte le funzioni C ritornano un valore di un determinato tipo. Eccovi un frammento di programma che dichiara una funzione che ritorna un intero, il prodotto di due argomenti interi: 

/* una funzione per moltiplicare due numeri */ 

int mult(a,b) 
int a,b; 
{
   int c; /* una variabile locale */
   return(c);
}

   La prima linea dichiara una nuova funzione chiamata mult(), che ha due argomenti, a e b, entrambi interi. Una dichiarazione di funzione può essere suddivisa in diverse parti. Prima, il tipo della funzione. Qui, noi abbiamo utilizzato ' int '. Se nessun tipo viene dichiarato, il C assume che la nuova funzione sia di tipo ' int '. Seconda, il nome della funzione, ' mult '. Tutte le dichiarazioni delle funzioni hanno un'insieme di parentesi, sia che la funzione abbia argomenti oppure no. Questa funzione ha due argomenti. Il tipo di ciascun argomento presente tra le parentesi deve venir dichiarato prima della parentesi graffa aperta. La parentesi graffa aperta indica al compilatore l'inizio del codice della funzione.
   La linea dopo la parentesi graffa aperta dichiara una variabile locale chiamata c. Se voi conoscete il Pascal, le variabili locali in C lavorano nello stesso modo che in Pascal, tenendo a mente pero' la restrizione che le funzioni non possono essere nidificate in C. Se voi, invece, non conoscete il Pascal, una variabile locale è una variabile a cui non si può accedere da un'altra procedura o funzione in un programma, anche se un'altra funzione dichiara una variabile locale con lo stesso nome. Il valore di una variabile locale è conosciuto solamente durante l'esecuzione di una variabile. Al termine dell'esecuzione della funzione, la variabile locale scompare, e la memoria che essa ha utilizzato vien riutilizzata. 
   Il ' c = a * b ' dovrebbe essere chiaro, qualsiasi linguaggio conosciate. La linea ' return(c); ' rappresenta la fine della funzione mult(). Il valore ' c ' viene ritornato alla funzione che ha chiesto di utilizzare la funzione multo. Vediamo ora un programma completo in C che utilizza la funzione mult() descritta sopra. 

#include "stdio.h"

/* una funzione per moltiplicare due numeri */

int mult(a,b);
int a,b;
{
   int c; /* una variabile locale */
   c=a*b;
   return(c);
}

/* Tutte le variabili dichiarate all'esterno delle
   dichiarazioni della funzione sono variabili
   globali, che possono essere utilizzate da
   tutte le funzioni 
*/

int t; /* una variabile globale */

main()
{
int r,s; /* variabili locali */
r = 3;
s = 4;
t = mult(r,s);
printf("Il risultato e' %d\n" ,t);
}

   In questo programma, la funzione main() chiama mult() con i valori memorizzati in ' r ' e ' s '. Ipotizzando che ' r ' contenga 3 e ' s ' contenga 4, la funzione mult() ritornera' il valore 12. All'interno della funzione main() il valore 12 verrà memorizzato nella variabile globale di main() chiamata ' t '. 
   Un programmatore spiegherebbe cio' che è accaduto dicendo che la funzione mult() è stata ' chiamata ' dalla funzione main() e il valore 12 e' stato ' ritornato ' a main(). Poiché il C e' basato su funzioni e chiamate di funzioni, i programmatori spesso utilizzano queste espressioni. Le espressioni 'chiamata' e 'valore di ritorno'  alludono alla struttura fondamentale del C. Entrambe le espressioni vengono utilizzate nello stesso contesto per programmi in linguaggio macchina. E ora a main().
   La dichiarazione della funzione main() non ha argomenti, e nessun tipo specifico vene dichiarato, in questo modo main() ritornera' un ' int '. main() e' una funzione speciale. Quando il programma C viene avviato dal sistema operativo, main() è la prima funzione che viene eseguita. A parte questo, main() non è in alcun altro modo differente dalle altre funzioni C.
   Ma ritorniamo alle veriabili. L'opposto di locale è globale. Le variabili globali sono tutte quelle variabili che vengono dichiarate all'esterno di tutte le funzioni del programma. D'altra parte le funzioni globali vengono dichiarate nello stesso modo delle variabili locali. La sintassi è la stessa: prima il tipo - int, per esempio -, quindi i nomi delle variabili, separati da virgole, se ce ne sono più di una, e per finire la terminazione ' ; '. Le variabili globali possono venire lette e modificate da qualsiasi funzione del programma. La maggior parte dei programmi presentano una mescolanza di variabili locali e variabili locali. Le variabili vengono utilizzate per calcoli intermedi, dal momento che scompaiono quando la funzione termina. Le variabili globali vengono utilizzate per i dati piu' permanenti, e per quei dati che diverse funzioni possono avar necessità di utilizzare. Se una funzione dichiara una variabile locale con lo stesso nome di una variabile globale, la variabile locale ha la precedenza.
   Nel nostro prossimo incontro approfondiremo l'utilizzo del preprocessore, e come dichiarare altri tipi di variabili. Ci introdurremo nell'argomento fondamentale dell'I/O considerandone le funzioni a disposizione. E forse ci diletteremo con le funzioni speciali di Amiga per le creazione di window.