Repository 32bit  Forum
Repository 64bit  Wiki

ANSI C for dummies

Da Slacky.eu.

Indice

Introduzione

Questo articolo è realizzato sulla base di una serie d'articoli che io (Nuitari) ed un frequentatore del forum di IGZ (Ilweran) abbiamo scritto a quattro mani. Si tratta di un corso di programmazione in ANSI C per principianti assoluti. Spero che possa risultare, per tutti voi, un utile strumento per entrare in questo magnifico mondo. Buona lettura.

Capitolo I: Algoritmi, Elaboratori e Linguaggi

Autore: Ilweran

George Boole, matematico inglese del XIX secolo, risulta una figura centrale e fondamentale nella comprensione degli elaboratori e dei loro linguaggi. Nel 1854 Boole pubblicò "An investigation on the laws of taught" in cui discute la struttura del pensiero logico. Al tempo la teoria più accreditata era che il pensiero logico non potesse prescindere dall'aspetto metafisico ma, con la sua trattazione, Boole smentisce questa teoria e dimostra che il pensiero logico umano può essere concepito come una elementare serie di scelte. Questa teoria è la base dell'algoritmica (scienza che studia gli algoritmi) e del funzionamento stesso dei computer.

Il termine algoritmo significa procedimento di calcolo e indica la sequenza esatta di azioni da compiere per risolvere un problema.

Un problema viene espresso indicando la relazione che intercorre tra i dati in ingresso e i dati in uscita (risoluzione del problema).

	  Problema
	     |
	     |
	     V
	  Algoritmo (azioni da eseguire + ordine in cui devono essere eseguite)
	     |
	     |
	     V
	 Risoluzione

Per esemplificare il concetto, possiamo pensare alle azioni che ognuno di noi compie quando deve fare una doccia:

  1. Entro in bagno
  2. Mi spoglio
  3. Faccio la doccia
  4. Mi rivesto
  5. Esco dal bagno

Se le stesse identiche azioni vengono compiute cambiando l'ordine o omettendone delle parti il risultato sarà ovviamente differente (e imbarazzante nella fattispecie :-)

Nel descrivere un algoritmo si utilizzano istruzioni elementari in quanto il risultato finale deve essere certo, unico e verificabile con una ripetizione. Ogni volta che dovrete fare la doccia potete riutilizzare l'algoritmo sopracitato e avrete la certezza che il risultato sarà sempre lo stesso.

Si parla di procedura quando un algoritmo è espresso seguendo i canoni e i costrutti tipici di un linguaggio di programmazione.

Tornando a Boole: come abbiamo già detto il pensiero logico può essere immaginato come una serie di scelte. Riutilizziamo il nostro algoritmo in modo da includere una scelta e ricalcare così lo schema del pensiero logico:

			 | Ho giocato a calcio? |
			            |
			    falso   |   vero
 non faccio la doccia    <-------------------->   faccio la doccia

Uno pseudocodice è un linguaggio che molti programmatori utilizzano per scrivere e verificare i propri algoritmi. L'algoritmo - doccia - è appunto scritto in pseudocodice. Non è altro che un linguaggio artificiale molto flessibile e simile alla lingua comune che permette a un programmatore di riflettere e verificare la validità e la sequenza delle azioni che andrà poi a tradurre in un linguaggio di programmazione come il linguaggio C. Il fine dello pseudocodice è una corretta progettazione del software e risulta fondamentale quando gli algoritmi diventano più complessi.

Ora abbiamo i mezzi per capire (almeno in superficie) il funzionamento di un computer: un computer è semplicemente una macchina che esegue calcoli e prende scelte logiche. Incredibilmente le uniche azioni che un computer può fare sono leggere dalla memoria un "bit" e verificarne il valore. Un bit (Binary dIgiT) è un dato che può assumere sempre un unico valore da un insieme di due possibilità (binario), ad esempio 0 e 1. Possiamo pensare a un computer come a un esecutore di algoritmi in forma binaria. Non vi spaventino i bit per il momento, le basi dell'aritmetica binaria saranno approfondite in seguito.

Ovviamente noi programmatori non saremo costretti a inserire infinite sequenze di 0 e 1 ma avremo a disposizione linguaggi di alto livello (cioè comprensibili anche all'uomo) che verranno poi tradotti in linguaggio macchina e resi quindi comprensibili anche all'elaboratore.

Capitolo II: Panoramica sui Linguaggi

Autore: Ilweran

Come abbiamo già spiegato un computer comprende solamente sequenze di bit. Quello che ancora non abbiamo spiegato è che questo “linguaggio macchina” è strettamente dipendente dall'hardware (cioè dalle componenti elettroniche) del computer. Ciò significa che un eventuale programma scritto in linguaggio macchina per PowerPC difficilmente funzionerà su tecnologia Intel, proprio perché sono tecnologie differenti che si basano su componenti hardware differenti.

Il linguaggio macchina fu presto considerato non adatto all'utilizzo in quanto sviluppare programmi era lento e faticoso inoltre trovare errori era pressoché impossibile. Si iniziarono quindi ad utilizzare delle abbreviazioni e delle facilitazioni che permettessero di astrarre il concetto del programma ed esprimerlo in modo che fosse comprensibile anche all'essere umano abbandonando le sequenze di numeri del linguaggio macchina: il risultato fu il linguaggio Assembly che, tramite un assemblatore (un “assembler”), permetteva di convertire le istruzioni da linguaggio Assembly in linguaggio macchina.

Nonostante il linguaggio Assembly fosse una grossa facilitazione ed un enorme passo avanti rispetto al linguaggio macchina esso necessitava ancora di troppe istruzioni per eseguire le operazioni più elementari. Furono perciò inventati i linguaggi ad alto livello. Essi permisero un'ulteriore livello di astrazione ed ora i programmatori potevano, con una singola istruzione, eseguire un intero compito essenziale. I linguaggi ad alto livello vengono tradotti in diversi modi che esamineremo più approfonditamente successivamente.

Per spiegare meglio il concetto possiamo fare un esempio: per sommare un elemento a un altro in assembly bisogna caricare il primo elemento nella memoria, aggiungere il secondo elemento allocando il risultato in un altro blocco di memoria ed infine visualizzare il contenuto della locazione in cui è stato allocato il risultato. In un linguaggio ad alto livello basterà invece una singola istruzione scritto in un linguaggio più “umano”: visualizza il risultato del primo elemento + il secondo.

I linguaggi ad alto livello sono normalmente divisi in base al “paradigma”, cioè al metodo logico e formale che il programmatore segue nello scrivere un programma. Generalmente si distinguono 4 paradigmi:

  • Imperativo: I programmi scritti con linguaggi imperativi sono formati da una sequenza d’istruzioni che vanno a modificare i contenuti della memoria ed i seguenti parametri nell'esecuzione del programma. Sono particolarmente importanti le istruzioni di assegnamento. Es.: Pascal, C
  • Funzionale: Nei linguaggi funzionali l'istruzione di assegnamento è addirittura assente, il programma è basato sulle funzioni e sul calcolo del risultato delle funzioni.

Particolarmente importante il concetto di ricorsione (cioè una funzione che richiama se stessa per risolversi) e di lista. Es.: Haskell, Lisp

  • Dichiarativo: I linguaggi dichiarativi sono spesso chiamati linguaggi logici. Si presentano come una sequenza di affermazioni che il programma verificherà come vere o false. Particolarmente importante il concetto di Pattern Matching. Es.: ProLOG, Awk
  • Object Oriented: Il programma è il risultato di una interazione tra oggetti precedentemente definiti che comunicano tramite chiamate. Es. Smalltalk, Java, Ruby

E' possibile dividere ulteriormente i linguaggi di alto livello a seconda di come vengono tradotti in linguaggio macchina.

  • Linguaggi compilati: I linguaggi compilati vengono tradotti in linguaggio macchina con un compilatore che si preoccupa di creare file eseguibili in linguaggio macchina. Nei file eseguibili non resta più traccia del codice sorgente (scritto con linguaggi ad alto livello) ne è possibile ricavare il codice originale dal file binario (scritto in linguaggio macchina). Es.: Pascal, C
  • Linguaggi interpretati: I linguaggi interpretati si basano appunto su di un interprete che traduce i programmi in chiamate di sistema e operazioni. Un programma scritto in un linguaggio interpretato dovrà essere reinterpretato ogni volta che viene eseguito, per questo motivo sono spesso più lenti dei linguaggi compilati ma tendono ad essere più semplici da imparare e più flessibili. Es.: Basic, Lisp
  • Liguaggi ibridi: Linguaggi più recenti dei sopracitati che uniscono alcune caratteristiche dei linguaggi compilati e di quelli interpretati. Il codice sorgente dei linguaggi ibridi viene tradotto in codice binario che non è propriamente quello della macchina. Si tratta di codice che verrà poi interpretato da una Virtual Machine in vero codice binario. In questo modo il codice è molto più portabile e flessibile senza perdere eccessivamente in prestazioni. Es.: Python, Java

Affrontando il linguaggio ANSI C, lo strumento didattico che abbiamo deciso d’usare in questo corso, nel prossimo capitolo andremo a spiegare più in dettaglio l'atto della compilazione e il funzionamento del compilatore stesso al fine di essere in grado di utilizzare pienamente tutti gli strumenti a nostra disposizione e di facilitare la scrittura di programmi complessi, questo senza trascurare l'aspetto storico.

Capitolo III: Introduzione al Linguaggio C

Autore: Ilweran

Passiamo ora a una analisi più specifica della storia del linguaggio C.

Come già accennato precedentemente il linguaggio C deve la sua nascita al sistema operativo UNIX negli anni tra il 1969 e il 1973, anno in cui il linguaggio prese definitivamente le caratteristiche per cui lo conosciamo oggi.

Una delle maggiori migliorie che C porta rispetto ai linguaggi precedenti è la portabilità dei suoi programmi, primo fra tutti proprio UNIX e le sue utility, portabilità dovuta all'utilizzo del compilatore, cioè del programma che trasforma il codice C ad alto livello in codice macchina.

Nonostante UNIX fosse originariamente nato su un PDP-7 l'adozione del C e del suo compilatore da parte di Thompson, Ritchie e Kernighan rese possibile una riscrittura veloce del kernel UNIX innanzitutto per PDP-11, operazione che avrebbe normalmente richiesto molto più tempo, più tardi per Interdata 8/32 e Dec VAX 11/780, quest'ultima all'epoca particolarmente popolare.

Tra il 1973 e il 1980 il linguaggio C si diffuse ulteriormente grazie al porting del compilatore su architetture Honeywell 635 e IBM 360/370 da parte di un ricercatore, Johnson, che diede vita al compilatore PPC, alla nasciata delle prime librerie (da menzionare il lavoro di Lesk, il "portable I/O package" che poi verrà rinominato come "standard I/O library") e il primo tentativo di standardizzazione del linguaggio: la scrittura del libro bianco, il "C programming language" di Kernighan & Ritchie nel 1978.

In questo periodo non bisogna pensare a un linguaggio C come lo conosciamo e intendiamo oggi. Esso infatti ha mantenuto per molto tempo alcune delle caratteristiche dei linguaggi da cui deriva (i già nominati BCPL e B) per evolversi e migliorarsi nel tempo. Per questo motivo il libro bianco fu doppiamente prezioso e importante in quanto tentava di definire un set di regole che i programmatori C dovevano rispettare al fine di rendere i loro programmi portabili.

Durante gli anni '80 il compilatore C venne portato praticamente su tutte le macchine più popolari e divenne di fatto uno dei linguaggi più utilizzati. Inizialmente, nei primi anni 80, i compilatori erano basati sul ppc di Johnson ma, più tardi attorno al 1985, nacquero molti compilatori indipendenti che non erano totalmente conformi al ppc. Apparve dunque chiaro il bisogno di una standardizzazione ufficiale del linguaggio nonostante il libro bianco che era ancora nebuloso e poco chiaro in diversi punti. Questo bisogno fu soddisfatto solamente nel 1989/1990 nello standard ISO/IEC 9899-1990 (che io chiamerò ANSI89 per comodità) che creò finalmente una base di regole comuni a tutti i compilatori.

Risale allo stesso periodo un'altra standardizzazione molto importante che mirava a definire l'interfaccia tra linguaggio e sistema operativo e che, prendendo spunto dall'ANSI89, lo contiene per risultarne un'estensione: la Portable Operating System Interface. POSIX (questo l'acronimo dello standard) è il risultato degli sforzi dell'IEEE (Institute of Electrical and Electronics Engeneers) che mirava a creare un'interfaccia comune con il sistema operativo e, tra le tante cose, definisce chiaramente threads, sockets, estensioni real-time e molti altri aspetti, in modo univoco e indipendentemente dall'implementazione.

Lo standard principale è POSIX.1 che tratta l'interfaccia di base es è in continua espansione. Da citare la bibbia di ogni programmatore C che si rispetti, la seconda e più completa edizione del libro bianco di Kernighan e Ritchie edita poco prima dell'uscita dell'ANSI89.

Questa è la base di conoscenze di ogni programmatore C e l'implementazione comune del linguaggio C in ogni sistema operativo (fatta eccezione di Microsoft Windows che non riconosce lo standard POSIX tranne una implementazione dello stesso in Windows NT).

L'ultima rivisitazione dello standard ANSI è datata 1999 (ANSI99) e dimostra ancora una volta l'assoluta attualità di questo linguaggio che pare non risentire del peso degli anni. Purtroppo pochissimi compilatori supportano (per ora) il nuovo standard: l'unico che lo supporta completamente è il Comeau C e buona parte dello standard è implementata nella famiglia 3.x.x di GCC.

Lo standard è recuperabile dietro pagamento sul sito dell'ANSI, ma per ora ci accontenteremo di un draft facilmente reperibile in rete e chi vi consiglio di scaricare: il nome del file è n869.pdf.

Preprocessore, Compilatore e Linker

Passeremo ora ad esaminare tutte le fasi che portano alla creazione di un eseguibile binario da un programma scritto in linguaggio C.

Tipicamente la prima fase è anche la più ovvia: produrre il file di codice sorgente. Scriveremo quindi il codice in un file di testo grazie a un editor che poi salveremo sull'unita di memoria secondaria (hard-disk, floppy ecc.) con l'estensione .c, fondamentale per far capire al compilatore che si tratta di un sorgente scritto in linguaggio C.

Il sorgente C verrà quindi esaminato dal preprocessore che fondamentalmente non è altro che un manipolatore di testo che tratta il sorgente e lo prepara alla compilazione seguendo le direttive imposte dal programmatore all'interno del sorgente stesso. Il preprocessore riconosce la istruzioni di sua attinenza perchè precedute dal simbolo #. Questa funzionalità dei compilatori C risulta decisamente utile ad esempio per includere file esterni all'interno del programma, per definire macro e altri tipi di sostituzioni e addirittura per definire delle condizioni nella compilazione: conoscere a fondo il preprocessore e le sue direttive è parte fondamentale del know-how di un programmatore C.

A questo punto il sorgente è pronto per essere compilato e viene quindi tradotto in linguaggio macchina creando un file oggetto che immagazzina sull'hard disk o sull'unità di memoria secondaria che preferite (a me è capitato ad esempio di non avere un hard disk :-).

Il file oggetto (estensione .o) non è ancora un eseguibile: tipicamente vengono incluse funzioni da librerie esterne, per questo il file oggetto deve passare per una ulteriore fase di compilazione: il linking. Il file oggetto viene quindi completato dal linker e collegato alle funzioni mancanti producendo una immagine. Questa immagine (normalmente chiamata a.out) è eseguibile, contiene infatti un set di istruzioni in linguaggio macchina che verranno caricate in memoria una per volta ed eseguite dalla CPU producendo in output un risultato.

Tutti questi passaggi non spaventino, il funzionamento di ognuno di questi singoli componenti verrà charito più avanti nel corso. Inoltre, per dovere di cronaca, bisogna dire che nel tempo si è persa la netta distinzione fra ognuno d'essi. Sono molti i casi in cui preprocessore compilatore e linker sono fusi all'interno d'un unico programma chiamato genericamente Compilatore.

Dove procurarsi quindi questi strumenti basilari per iniziare a programmare con il linguaggio c? Questo sarà l'argomento del prossimo capitolo.

Capitolo IV: Procurarsi un Compilatore

Come è ovvio, a seconda del sistema operativo dovremo recuperare un differente set di strumenti.

Procurarsi un compilatore per Windows

Autore: Nuitari

Nell’ambito Windows esistono diversi compilatori free, liberamente scaricabili dalla rete, la maggior parte dei quali trasposizioni ("porting") dello Gnu Compiler (di cui s'è parlato sopra).

Oltre ad un Compilatore, per poter programmare senza impazzire è sicuramente anche necessaria un "IDE". IDE è l'acronimo di "Integrated Development Evironment", ossia "Ambiente di Sviluppo Integrato". In parole povere si tratta di programmi completamente integrati con il Compilatore il cui scopo è mettervi a disposizione un "ambiente di sviluppo" comodo e funzionale, con tutto ciò di cui potete aver bisogno per ralizzare i vostri programmi facilmente e comodamente. Non ultima delle funzioni d'un IDE è l'integrazione con il Debugger, ossia quella parte del Compilatore che vi permette di scovare gli errori nei vostri programmi.

I più famosi Compilatori free per DOS/Windows, con le relative Homepage, sono:

L’ideale almeno agli inizi sarebbe familiarizzare con il linguaggio senza dover al tempo stesso imparare ad utilizzare un IDE. Ciò non toglie che all’alba del 2000 mettersi a programmare usando Notepad o programmi analoghi è quantomeno inconveniente (anche se spesso vi troverete a farlo comunque :-)

Per questo motivo consiglio a tutti coloro non fossero provvisti di qualche Compilatore/IDE commerciale (consigliatissimo il Visual C++ 6.0 di Microsoft, con relativi aggiornamenti) di scaricarsi Bloodshed Dev-C++ (Home Page: www.bloodshed.net).

Dev-C++

Dev-C++ altri non è che un IDE freeware sviluppata sul modello del Visual C++ di Microsoft, perfettamente integrata con il compilatore MinGW.

Potete scaricare un pacchetto autoinstallante comprendente sia il Dev-C++ che il compilatore MinGW dall'homepage del progetto Durante questo corso faremo riferimento a Visual C++ 6 e Dev-C++ come IDE "standard" per quel che riguarda gli ambienti Microsoft.

Coloro che invece utilizzano da tempo Djgpp e non vogliono cambiare compiler, potranno comunque seguire il corso ma ovviamente dovranno ignorare qualsiasi informazione specifica relativa all IDE, oppure potranno scaricarsi la versione di Dev-C++ priva di MinGW configurandolo per funzionare con Djgpp come compiler.

Una volta installato Dev-C++, avviate l'applicazione. Vi apparirà una maschera di configurazione ("Dev-C++ first time configuration") che vi chiederà di selezionare la lingua e lo stile grafico.

Come lingua selezionate ASSOLUTAMENTE l'Inglese. Questo perchè, purtroppo, non tutta l'applicazione è stata tradotta nelle altre lingue e se selezionate una lingua diversa dall'inglese avrete non pochi problemi. Inoltre nel corso faremo riferimento alla versione inglese dell'applicazione. Come stile grafico scegliete pure quello che preferite. Se per caso avete già installato Dev-C++ selezionando una lingua diversa dall'inglese, sarà sufficiente che cancellare la cartella dell'applicazione e rieseguire l'installazione per poter selezionare la lingua corretta.

Se invece possedete il Visual-C++ non è necessario effettuare nessun settaggio aggiuntivo, il programma è pronto per funzionare subito dopo l'installazione.

Una volta avviato, Dev-C++ si presenta con un aspetto del tutto identico a quello di Visual-C++: sulla sinistra è presente una barra verticale bianca, il "Project/Class Browser", tramite la quale potrete navigare all'interno dei file che compongono i vostri progetti. In basso sono presenti invece una serie di "linguette" [tabs] che vi permetteranno di vedere in modo rapido i vari messaggi prodotti dal compilatore, dal debugger, etc.

Dev-C++ funziona con una logica orientata ai "Progetti". Quando iniziate a sviluppare un applicazione come prima cosa dovrete creare un nuovo progetto, il quale oltre a fungere da contenitore di tutti i vari file vi permetterà d'informare il compilatore sul tipo di programma che andrete a realizzare (se un applicazione basata sulle finestre, sulla console, una DLL, etc). Una volta creato un progetto potrete aggiungervi file creandoli ex-novo o includendoli nel caso siano già esistenti (come nel caso di librerie prodotte da terze parti).

Visual-C++ invece ha una logica leggermente diversa, orientata ai "Workspaces". Un Workspace è fondamentalmente un contenitore di Progetti, quindi come prima cosa in Visual-C++ dovrete creare un nuovo Workspace, quindi un nuovo Progetto all'interno del Workspace ed infine potrete passare alla creazione dei singoli files. Sempre per lo stesso motivo la barra bianca alla vostra sinistra non si chiama "Project Browser" come nel Dev-C++ ma "Workspace".

Andremo ad analizzare specificatamente il funzionamento di ognuna di queste IDE in seguito, man mano che se ne presenterà la necessità.

Compilatori ed IDE sotto Linux: GCC ed Emacs

Autore: Ilweran

Questo capitolo è rivolto a chi ha intenzione di seguire questo corso utilizzando GNU/Linux e altri sistemi operativi basati sui tools forniti dal progetto GNU. Per ulteriori informazioni sul progetto rimando ogni riferimento al sito web. Ho deciso d'introdurre le funzioni basilari dell'applicazione Emacs e del compilatore GCC nelle versioni che credo più diffuse: Emacs 21.x e GCC 2.95.x. Non dovrebbero esserci sostanziali differenze tra le versioni precedenti e successive.

Innanzitutto controlliamo che sulla nostra Linux Box siano installati i due programmi in oggetto: andate innanzitutto in modalità testuale o aprite un xterm

nicholas@ilweran:~$ gcc -v
Reading specs from /usr/lib/gcc-lib/i386-linux/2.95.4/specs
gcc version 2.95.4 20011002 (Debian prerelease)

L'output del comando 'gcc -v' dovrebbe essere simile a questo.

Per Emacs basta lanciare dal prompt della shell il comando 'Emacs': se lanciato da un xterm s aprirà la versione X di Emacs, in caso contrario la classica finestra testuale con una schermata di presentazione.

Nel malaugurato e raro caso in cui una di queste due applicazioni non fossero installate e non riusciste a recuperare i pacchetti specifici per la vostra piattaforma mandatemi un PM per avere ulteriore aiuto nella compilazione da sorgenti.

Emacs

Sono ovviamente impossibilitato a spiegare tutte le particolarità di questa splendida piattaforma di sviluppo, sappiate però che il termine editor, cosa che a prima vista potrebbe sembrare, è inadatto almeno quanto chiamare acquilone un Boeing. Fondamentalmente si tratta di un core scritto in linguaggio C che implementa un compilatore di Elisp (Emacs Lisp, una variante del Lisp) con cui vengono scritte diverse applicazioni.

Il mio Emacs ad esempio ha 5 modalità: mailreader, newsreader, IDE per il linguaggio C, IDE per il linguaggio Python, Editor HTML-PhP

Noi ci occuperemo del c-mode, una modalità nativa, utile ai fini di questo corso.

Iniziamo digitando emacs in console: dovrebbe apparire una schermata di benvenuto.

I primi due shortcuts che andremo ad imparare sono l'apertura di un file e la chiusura di Emacs: dalla schermata di presentazione premete ctrl + x seguito da ctrl + f, vedrete la scritta "find file ~/" nel minibuffer alla base di Emacs seguito da un cursore lampeggiante. Potete inserire l'indirizzo del file o premere "?" per visualizzare un file browser; nel caso il file digitato non fosse esistente Emacs provvederà a crearlo.

Abbiamo parlato poco fa di c-mode, ovvero il modulo Emacs che permette di avere nativamente l'indentazione automatica del codice: si può andare in c-mode aprendo un file con le classiche estensioni .c e .h del linguaggio C. E' possibile trovare ulteriori script per syntax highlight e altre comode features nel sito GNU o con una veloce ricerca su Google.

Per il syntax highlight basta inserire la stringa "global-font-lock-mode t" nel file .emacs nella vostra home. Per uscire da Emacs basta premere ctrl + x seguito da ctrl + c, vi verrà chiesto di salvare il file prima di uscire, premete Y o N e il gioco è fatto.

Ora dovreste aver capito il meccanismo: passerò ad elencare qualche shortcut indispensabile. ctrl + h seguito da ctrl + h serve ad avviare l'immenso aiuto in linea.

Con ctrl + x seguito da ctrl + u si annulla l'ultima modifica.

Queste sono le primissime basi per l'editing di un file di testo: vi dirò come compilare all'interno di Emacs dopo aver spiegato le basi del compilatore GCC.

Più diverrete esperti con Emacs (ad esempio con le macro, i buffer ecc.) più la vostra velocità nello scrivere codice aumenterà. Emacs è ovviamente totalmente personalizzabile ed estendibile, a voi e alla vostra curiosità imparare sempre di più. un buon inizio è ctrl + h + t per leggere il tutorial introduttivo.

OT: E' molto divertente cercare gli easter eggs di Emacs (milioni di milioni). Piccolo hint: provate a premere alt + x e digitare dunnet.

GCC

GCC è un compilatore da linea di comando, per ora esamineremo solamente le opzioni più comuni: la prima, la più ovvia:

nicholas@ilweran:~$ gcc ciao.c
nicholas@ilweran:~$ ls
a.out ciao.c

"gcc [nome_file.c]" produce un file "a.out". a.out è un eseguibile, basta digitare ./a.out (il simbolo ./ anticipa il nome dell'eseguibile perchè probabilmente la directory corrente non è nel PATH) per eseguire il programma compilato. Volendo dare un vostro nome al compilato dovrete usare l'opzione -o seguita dal nome del file compilato:

nicholas@ilweran:~$ gcc ciao.c -o ciao
nicholas@ilweran:~$ ls
ciao ciao.c

Come prima: ./ciao per eseguire il file. Altre opzioni molto importanti sono:

  • -ansi che controlla la conformità allo standard del vostro codice (importante per avere applicazioni portabili e per migliorare il vostro stile)
  • -pedantic che rifiuta qualsiasi estensione non conforme allo standard
  • -Wall che abilita tutti i warnings per un corretto uso del C
nicholas@ilweran:~$ gcc -ansi -pedantic -Wall ciao.c -o ciao
ciao.c: In function `main':
ciao.c:6: warning: control reaches end of non-void function

Prima compilava, ora non più. Credo di aver fatto qualche stupidata... :-)

Come usarlo all'interno di Emacs ? Niente di più semplice, premete alt + c e digitate nel minibuffer "compile". Il prompt vi restituirà un comando (presumibilmente make), cambiatelo con la stringa gcc che preferite con le opzioni che desiderate e guardate il risultato. Per ora di carne al fuoco ne ho messa, i tool di sviluppo GNU sono largamente utilizzati e in ttti i processi di sviluppo (gdb, il debugger, yacc per creare parser, make per compilare ecc.) ma, per ora, avete tutto il necessario per cominciare.

Capitolo V: Primo approccio con il C

Autore: Nuitari

Ed eccoci finalmente arrivati alla parte più “concreta” di questo corso: la pratica Dopo tanta teoria, assolutamente indispensabile per capire quel che stiamo per affrontare, è giunto il momento di "sporcarsi le mani" con il codice. Per capire molti degli aspetti della programmazione in C sarà sufficiente scrivere un piccolo programma introduttivo il quale, in se, racchiude già molti concetti che andremo a spiegare con calma. Forse saltare direttamente al codice potrà sembrarvi un po' brusco, ma vi posso assicurare che non esiste metodo d'apprendimento migliore.

Se siete su UNIX/Linux, create un nuovo file sorgente e copiate il codice riportato di seguito:

#include <stdio.h>

/* Hello World! - Primo programma C */
int main(int argc, char *argv[])
{
	printf("Hello World!\n");
	return 0;
}

Quindi compilate il file come spiegato precedentemente per verificare che non abbiate fatto errori. Nel caso, verificate il contenuto del file apportando le dovute correzioni. E' importante che sia ASSOLUTAMENTE IDENTICO, in ogni dettaglio. Attenzione anche alle minuscole e alle maiuscole: il C è un linguaggio "case sensitive", ossia fa distinzione fra maiuscole e minuscole.

Se invece siete su Windows, create un nuovo progetto e copiate il seguente codice:

#include <stdio.h>
#include <stdlib.h>

/* Hello World! - Primo programma C */
int main(int argc, char *argv[])
{
	printf("Hello World!\n");
	system("PAUSE");
	return 0;
}


--- NOTA PER COLORO CHE USANO Dev-C++ ---

Per fare quanto scritto sopra, aprite Dev-C++. Dal menù "File", selezionate "New" e quindi "Source File". Dev-C++ creerà un file di testo nuovo (chiamato "Untitled1.cpp") nel Workspace, permettendovi di scriverci dentro come se fosse Notepad (anche se ha funzionalità completamente differenti, ovviamente). Dopo aver copiato il testo riportato sopra, selezionate "Save As" dal menù "File", quindi scegliete una cartella qualsiasi dove memorizzare il vostro primo programma. Vi consiglio di creare una cartella apposita per i vari esperimenti che farete con il corso, al cui interno creerete cartelle singole per ogni programma. Vi consiglio anche di crearla in una posizione facilmente raggiungibile, come il Desktop. Una volta salvato, selezionate "Compile" dal menù "Execute". Se avete fatto tutto correttamente nella finestra Compile Log in basso sullo schermo deve apparire la scritta "Compilation successful". Nel caso questo non accada, verificate il contenuto del file apportando le dovute correzioni. E' importante che sia ASSOLUTAMENTE IDENTICO, in ogni dettaglio.

--- FINE NOTA Dev-C++ ---


--- NOTA PER COLORO CHE USANO Visual C++ ---

Per fare quanto descritto sopra, aprite Visual C++. Dal menù "File" selezionate "New". Vi apparirà una finestra con le numerose scelte a disposizione. Cliccate sulla linguetta "Files" e fate doppio click su "C++ Source File". Visual C++ creerà un file di testo nuovo (chiamato "Cpp1") nel Workspace, permettendovi di scriverci dentro come se fosse Notepad (anche se ha funzionalità completamente differenti, ovviamente). Dopo aver copiato il testo riportato sopra, selezionate "Save As" dal menù "File", quindi scegliete una cartella qualsiasi dove memorizzare il vostro primo programma. Vi consiglio di creare una cartella apposita per i vari esperimenti che farete con il corso, al cui interno creerete cartelle singole per ogni programma. Vi consiglio anche di crearla in una posizione facilmente raggiungibile, come il Desktop. E' molto importante creare cartelle distinte perchè Visual-C++ produrrà diversi file i quali genererebbero solo casino se non organizzate bene tutto. Una volta salvato, selezionate "Compile" dal menù "Build". Visual-C vi avviserà che per compilare è necessario creare un Workspace ed un Progetto e vi chiederà se volete che ne crei lui uno di default per voi. Rispondete "YES", ovviamente. Se avete fatto tutto correttamente nella finestra Build in basso sullo schermo deve apparire la scritta "0 error(s), 0 warning(s)". Nel caso questo non accada, verificate il contenuto del file apportando le dovute correzioni. E' importante che sia ASSOLUTAMENTE IDENTICO, in ogni dettaglio.

--- FINE NOTA Visual C++ ---

Il motivo per cui sono necessarie 2 versioni diverse dell'applicazione a seconda del sistema operativo è che, sotto Windows, i programmi "console" (ossia quelli solo testuali tipo i vecchi programmi MS-DOS) vengono chiusi nel momento in cui il programma finisce, impedendovi di vedere l’output del programma. Per questo, nella vesione Windows è stato aggiunto un comando che forza il programma ad attendere la pressione d'un tasto, in modo che possiate vedere il risultato. Una volta che avete copiato il programma e che l'avete compilato, è arrivato il momento d'eseguirlo. Per fare questo, in UNIX è sufficiente digitare il nome del file eseguibile prodotto dalla compilazione e dal linkaggio, come spiegato nel capitolo precedente. Se siete in Windows e state usando Dev-C++ dovete selezionare "Run" dal menù "Execute", se state usando Visual-C++ dovete selezionare "Start Debug" dal menù "Build", quindi cliccare su "Go" (oppure premere F5 ).

In ogni caso, come risultato vedrete apparire sullo schermo (in Windows in una finestra apposita) la scritta "Hello World!". In Windows premendo un tasto terminerete l'esecuzione del programma. A questo punto potreste pensare che per ottenere una semplice scritta del genere il lavoro necessario è enorme e molto macchinoso. In realtà quello che fa il computer internamente per visualizzare una “semplice scritta del genere” è ancora più enorme e macchinoso di quello che può sembrare ed il programma C che avete copiato rappresenta già una grandissima semplificazione.

Ma come abbiamo ottenuto questo risultato? Cosa significa quel che avete solamente copiato per il momento? Andiamo ad analizzare il programma Hello World nei minimi dettagli.

Innanzitutto, il programma nella sua interezza risiedendo su un unico "source file" (eviterò di tradurre frasi inglesi il cui significato è ovvio) può essere definito una "translation unit". Per "translation unit" s'intende la singola entità che il compilatore prende "in pasto" per generare il codice oggetto, come descritto nei capitoli precedenti. Programmi più complessi saranno divisi in più translation units, ossia saranno divisi in più source file opportunamente collegati. La prima parte del nostro source file (ossia quella fino alla scritta "/* Hello World...") è detta "dichiarativa", ossia non esegue nessuna "azione" particolare ma ha l'unico scopo d'informare il compilatore su quel che andremo a fare nel programma, in modo che possa compilarlo con successo. In molti linguaggi d'alto livello, come nel Basic, questa parte è del tutto assente. Nel C invece riveste un importanza fondamentale. Esistono diversi tipi di "dichiarazioni": direttive, pragma, definizioni, etc. Con il tempo le esamineremo tutte. Nel nostro caso le dichiarazioni cominciano tutte con il carattere speciale "#", il che le identifica tutte come "direttive al preprocessore". Come abbiamo spiegato all'inizio, la compilazione d'un programma C passa attraverso diverse fasi e strumenti. La prima fase è svolta appunto dal "preprocessore", ossia un programma che prepara i sorgenti per essere trattati dal compilatore. Le righe precedute dal carattere "#" del nostro esempio rappresentano direttive che il programmatore da al preprocessore per influenzarne il funzionamento.

Subito dopo le dichiarazioni troviamo la riga:

/* Hello World! - Primo programma C */

Questa non è nient'altro che un commento, ossia una riga di testo ignorata completamente dal compilatore. I commenti sono utili solo ed unicamente ai programmatori, per permettergli sia d'organizzare i sorgenti in modo più "comprensibile" dal punto di vista visivo (mi si perdoni il gioco di parole ) sia d'inserire informazioni che potrebbero tornare molto utili qualora si dovesse riesaminare il programma a distanza di tempo. Sono commenti tutte le righe precedute dalla sequenza di caratteri "//" nonchè tutte le parti di testo racchiuse dalle sequenze "/*" e "*/" che indicano rispettivamente l'inizio e la fine di un commento.

Subito dopo il commento, nel nostro programma troviamo la riga "int main(int argc, char *argv[])" seguita da una parentesi graffa aperta. Questa riga viene detta "definizione d'una funzione", o "function's header". E' possibile pensare ad un programma C come ad un insieme di piccoli algoritmi, ognuno dei quali svolge un determinato compito. Nei linguaggi di programmazione questi "piccoli algoritmi" vengono detti "funzioni" o "procedure", a seconda che restituiscano o meno un risultato (le funzioni restituiscono un risultato, le procedure no). A dire il vero questa distinzione nel C e nel C++ è puramente accademica, il motivo per cui l'ho adottata in questo corso è per rendervi più semplice il passaggio a linguaggi di più alto livello che invece l'adoperano, come l'objective Pascal od il Visual Basic, considerato che questa vuole essere un introduzione generale alla programmazione. Anche se bisogna dire che una volta imparati correttamente C e C++ non saranno molti i motivi per cui potreste voler cambiare linguaggio La riga "int main(int argc, char *argv[])" definisce quindi una funzione chiamata "main", indicando anche il tipo di dati che restituisce ed il numero e tipo di dati che accetta come "parametri d'ingresso". Non disperatevi se non avete capito Chiariamo bene questo concetto, che potrebbe risultare ostico ai più inesperti. Supponiamo di voler scrivere un'algoritmo che disegni sullo schermo il numero "0". In questo caso l'algoritmo non necessiterà d'avere nessuna "informazione particolare" per svolgere il suo compito, ne alla fine della sua esecuzione dovrà "restituire" qualche informazione particolare al programmatore. Questo è un tipico esempio di procedura. Supponiamo invece di voler scrivere un algoritmo che disegni sullo schermo un numero qualsiasi, in modo da poterlo usare per molte situazioni diverse. In tal caso l'algoritmo necessiterà d'un informazione prima di poter essere eseguito: il numero da stampare. Nel mondo della programmazione questa "informazione" viene detta "parametro". Dopo aver stampato il numero richiesto l'algoritmo non dovrà restituire nessuna informazione particolare al programmatore. Indi per cui sarà anche questo una procedura. Supponiamo infine di voler scrivere un algoritmo che calcoli la somma di due numeri. In tal caso, l'algoritmo necessiterà di due parametri in ingresso (gli addendi) e dovrà restituire un dato in uscita (la somma). Come spiegato sopra, questo particolare tipo di procedura viene detto "funzione", in quanto restituisce un dato che il programmatore potrà trattare come meglio crede (o ignorare). In C una funzione viene definita con la sintassi: tipo_dato_restituito nome_funzione(elenco_parametri). Una procedura si definisce nel medesimo modo, avendo l'accortezza di specificare come tipo di dato restituito "void", ossia nullo, niente. Quindi, tornando al nostro programma, int main(int argc, char *argv[]) definisce una funzione che restituisce un dato INTero (parleremo più avanti dei dati e dei tipi di dati), funzione di nome "main" la quale accetta due parametri, "argc" e "*argv[]", il primo anch'esso di tipo INTero ed il secondo invece di tipo CHARacter. La funzione main è comunque una funzione particolare. Ogni programma che si rispetti deve iniziare da un determinato punto e finire in un altro. La funzione main rappresenta il punto d'inizio di qualsiasi programma C. Per questo motivo, il suo tipo ed i suoi parametri sono predefiniti. Ogni programma ha una funzione main ed ogni funzione main dev'essere definita esattamente così come l'abbiamo scritta nel nostro esempio.

Subito dopo la definizione d'una funzione, bisogna specificare l'elenco delle istruzioni che ne fanno parte. La funzione main non fa eccezione. In C questo è ottenuto racchiudendo l'elenco delle istruzioni fra le parentesi graffe. Generalizzando, si può dire che le parentesi graffe identificano "blocchi di codice" (blocks) da considerare come fossero una "singola entità". Questa generalizzazione è necessaria perchè in realtà è possibile utilizzare blocchi di codice in molte occasioni, non solo nella definizione d'una funzione. In ogni caso questo non è necessario ai fini del nostro esempio. Quel che c'interessa ricordare ora è che nella definizione d'una funzione all'header segue il blocco di codice rappresentante le istruzioni della funzione, racchiuso da parentesi graffe come tutti i blocks.

Analizziamo ora quel che effettua la funzione main.

La prima riga è quella responsabile della visualizzazione della scritta sullo schermo:

	printf("Hello World!\n");


Si può imparare molto sul C già da questa prima piccola riga. printf è una funzione esterna al nostro programma, contenuta nella "libreria standard" del C. In altre parole, con il C sono fornite tutta una serie di funzioni che effettuano i compiti più disparati, dalla visualizzazione di testo alle funzioni grafiche, dalle operazioni matematiche a quelle trigonometriche. Queste funzioni risiedono in veri e propri file sorgenti, che è necessario "includere" nel proprio programma prima di poterle utilizzare. Nella fattispecie, printf fa parte dello "stdio" (Standard I/O) e possiamo usarla solo perchè, all'inizio del nostro programma, abbiamo dato direttiva al preprocessore d'includere i sorgenti dello stdio, con l'istruzione:

	 #include <stdio.h>

Analizzeremo meglio successivamente la direttiva #include. Questa è una caratteristica peculiare del C (ed uno dei suoi punti di forza). Senza questa "libreria standard" il C sarebbe in se e di per se capace d'eseguire ben poche operazioni complesse. Altri linguaggi d'alto livello invece hanno molte funzioni complesse integrate direttamente nel compilatore e non richiedono d'includere sorgenti esterni, come in questo caso. Come potete vedere dall'esempio, la funzione printf ha bisogno d'un parametro (o argomento) il quale viene passato tra parentesi come sempre accade per i parametri delle funzioni. Nel nostro caso il parametro è "Hello World\n", racchiuso da doppi apici. I doppi apici sono necessari per identificare una "sequenza di caratteri", o "stringa" in gergo, in modo che il compilatore non possa confondersi. All'interno della stringa c'è una sequenza di caratteri particolari, "\n", che non viene visualizzata quando eseguiamo il programma. Queste sequenze particolari formate da due caratteri il primo dei quali è il "backslash" (\) in gergo vengono chiamate "sequenze escape" e servono per rappresentare simboli speciali altrimenti non digitabili, come il ritorno a capo. Infatti la sequenza "\n" rappresenta proprio il ritorno a capo ("new line"). Alla fine della nostra riga è presente il simbolo di punto-e-virgola, che in C è necessario per terminare un istruzione ed eseguirne un altra. La maggior parte degli errori, soprattutto all'inizio, dipendono dal fatto che il programmatore si dimentica di mettere il ";".

La riga successiva, se state usando windows, è:

 	system("PAUSE");


Come per printf anche in questo caso stiamo chiamando una funzione esterna al nostro programma. La funzione system è definita nella libreria "stdlib" motivo per cui alla versione windows del programma è stata aggiunta anche la direttiva:

 	#include <stdlib.h>


necessaria ad includere i sorgenti richiesti per poter utilizzare "system". Lo scopo della funzione system è eseguire un comando di "sistema" direttamente dall'interno di un programma. Anche system come printf richiede un parametro (in realtà printf è molto più complessa di quel che appare, la analizzeremo meglioin seguito). Il parametro richiesto è ovviamente il comando di sistema da eseguire, anche questa volta sotto forma di stringa. Noi avevamo, come spiegato sopra, la necessità di far fermare l'esecuzione del programma fino alla pressione di un tasto e quindi abbiamo fatto eseguire a system il comando DOS "pause" che adempie magnificamente a questo scopo. In realtà esistono molti altri modi per causare l'attesa d'un tasto in C, ma questo rappresentava la soluzione più semplice. Anche in questo caso la chiamata alla funzione system è seguita da un punto e virgola.

L'ultima riga del nostro programma d'esempio, sia che siate su Unix/Linux che su Windows, è:

	return 0;


L'istruzione "return" causa l'uscita da una funzione. Nel nostro caso, essendo la funzione da cui usciamo la funzione main, return causa l'uscita immediata dal programma. Come abbiamo visto prima tutte le funzioni restituiscono un dato, e main è una funzione che restituisce un dato di tipo numerico intero. Lo 0 (zero) che segue return specifica che la funzione main alla sua uscita restituirà il valore numerico intero 0. Volendo, è possibile chiamare return come se fosse una funzione qualsiasi, racchiudendo lo 0 tra parentesi in questo modo:

	return(0);


L'effetto sarebbe stato il medesimo. Come abbiamo detto la funzione main è una funzione particolare, e come tale anche il dato che restituisce ha un significato particolare. Se il valore restituito da main equivale a 0, significa che il programma si è concluso senza errori. Un valore restituito da main diverso da 0 indica che durante l'esecuzione del programma s'è verificato un errore.

A questo punto, provate a rileggere questo primo programma. Vi appare tutto più chiaro, non è vero? Ovviamente, abbiamo solo scalfito la profondità che è possibile raggiungere anche solo con questo "banale" esempio. Con il tempo arriverete a padroneggiare ogni più piccolo aspetto del linguaggio e non avrete la benchè minima difficoltà a scrivere programmi decisamente più complessi.

Nel prossimo capitolo analizzeremo ulteriormente questo programma approfondendo le meccaniche del preprocessore, in modo da capire bene cosa avviene al delicato momento della compilazione.

Capitolo VI: I Token e la loro valutazione

Autore: Nuitari

In questo capitolo affronteremo il delicato compito di spiegare gli elementi "basilari" di cui è composto il C, ed il modo in cui questi vengono valutati dal compilatore. E' bene chiarire che salvo diverse indicazioni nel testo, per compilazione s'intende genericamente l'insieme delle operazioni svolte dai tre tools definiti nei capitoli precedenti ( preprocessore compilatore e linker) che portano alla realizzazione d'un file eseguibile partendo da un sorgente. Spesso sentirete parlare di compilatore in riferimento all'intera "suite" di tools, non spaventatevi. Questo dipende dal fatto che oggi come oggi la distinzione fra i tre tool, una volta ben più nitida, è andata smarrendosi, considerato che spesso sono integrati in un unico programma o che comunque il programmatore non sempre li adopera direttamente tutti e tre. E' bene chiarire anche che ciò di cui discuteremo ora non dev'essere "imparato a memoria", ma leggerlo e capirlo vi porterà a comprendere meglio il funzionamento del linguaggio e, soprattutto, del compilatore, comprensione indispensabile per poter correggere gli errori che involontariamente inserirete nei vostri programmi (come sempre accade, soprattutto all'inizio). Tuffiamoci quindi nel meraviglioso mondo dei token.

L'elemento minimo riconosciuto dal compilatore è detto "token". Una token non è nient'altro che una parte di testo del vostro sorgente che il compilatore non può suddividere in elmenti successivi. Le token a loro volta sono classificate in diversi tipi. Il modo in cui il compilatore identifica i token determina spesso la maggior parte degli errori di sintassi, ossia di battitura o di ambiguità (situazioni poco chiare per il compilatore). E' bene capire quindi come il compilatore valuta i vari token.

Il primo modo in cui un compilatore valuta i token è con l'ausilio degli "White Spaces". Per White Spaces s'intendono tutti quei caratteri che hanno lo scopo di fungere da separatori e, secondariamente, di rendere maggiormente leggibile un programma. Negli white spaces rientrano quindi il normale carattere spazio, il carattere di tabulazione, il ritorno a capo e via dicendo. Tutti questi caratteri, invisibili al programmatore, sono trattati dal compilatore con grande attenzione per capire come deve comportarsi. Ad esempio, se riprendiamo la prima riga dell'esempio del capitolo precedente, ossia questa:

#include <stdio.h>


e la scriviamo così:

#inc lude <stdio.h>


e proviamo a compilare, ci verrà segnalato un errore. Questo dipende dal fatto che lo spazio forza il compilatore a considerare "inc" e "lude" come fossero due elementi distinti, come fossero due token. Se invece scriviamo la riga di cui sopra in questo modo:

#include           <stdio.h>


il compilatore non segnala nessun errore. Questo dipende dal fatto che il preprocessore, quando analizza i sorgenti e trova degli white spaces contingui utilizzati come delimitatori di tokens, li elimina trasformandoli in un unico whitespace. Quindi per il compilatore se noi scriviamo

#include <stdio.h>

oppure

#include           <stdio.h>

è esattamente la stessa cosa. Questo è possibile grazie al preprocessore, che prepara i sorgenti prima che il compilatore li tratti. Attenzione: poco sopra abbiamo detto che anche il ritorno a capo è considerato un white-space, il che potrebbe portare a pensare che scrivere:

#include <stdio.h>

oppure

#include

<stdio.h>

sia la stessa cosa. In realtà questo è vero in molti casi, ma non in tutti. Esistono casi particolari in cui i token devono essere tutti sulla stessa riga, e le direttive al preprocessore (ossia tutte le righe precedute dal carattere "#", come spiegato sopra) sono uno di questi casi. Lo stesso non è invece per il normale codice. Se ad esempio prendiamo sempre dall'esempio del precedente capitolo la riga:

printf("Hello World!\n");

e la scriviamo in questo modo:

printf(

"Hello World!\n");

il compilatore non produrrà errori in nessun caso, dato che il ritorno a capo verrà eliminato dal preprocessore come fosse un normale spazio di troppo. Sapere dove il ritorno a capo viene considerato come semplice white space e dove non viene considerato come tale non è fondamentale, dato che in genere è meglio evitare di spezzare le righe di codice perchè aumenta l'illeggibilità.

Un altro caso in cui gli white-spaces non vengono eliminati dal preprocessore sono le costanti stringa (definiremo questo concetto nel capitolo sui dati), ossia le sequenze di caratteri racchiuse fra doppi apici. Quindi scrivere:

printf("Hello World!\n");

e

printf("Hello        World!\n");

produce effettivamente risultati differenti, dato che gli spazi aggiuntivi sono racchiusi fra i doppi apici. Riprendendo il discorso del ritorno a capo, scrivere:

printf("Hello 

World!\n");

spezzando in due la sequenza di caratteri racchiusa fra doppi apici (da ora in poi "stringa") produce un errore di compilazione. Non è infatti possibile includere un ritorno a capo in una stringa in questo modo. Per poter inserire un ritorno a capo in una stringa come abbiamo detto nel capitolo precedente va usata la sequenza escape apposita, ossia la combinazione di caratteri "\n". E' molto, molto, molto importante fare un buon uso dei white spaces per rendere il programma chiaramente leggibile da "semplici" esseri umani, anche se ai fini della compilazione questi vengono poi rimossi in modo trasparente.

Un altro elemento utilizzato dal C per dividere le token sono i commenti. I commenti sono righe descrittive di testo ignorate completamente dal compilatore, le quali come abbiamo spiegato hanno l'unica funzione d'inserire note per il programmatore stesso, sempre per il raggiungimento del grande obbiettivo della leggibilità del codice. I commenti sono racchiusi fra due sequenze di caratteri speciali, "/*" e "*/" i quali identificano rispettivamente l'inizio e la fine del momento. Il preprocessore si preoccuperà di eliminare il commento sostituendolo con un unico white space al momento della compilazione. I commenti possono essere anche multi riga, il che significa che scrivere il nostro programma d'esempio in questo modo:

#include <stdio.h>

/* Hello World! 
- Primo programma C
*/

int main(int argc, char *argv[])
{

	printf("Hello World!\n");
	return 0;
}

non avrebbe causato il benchè minimo errore. Tutto ciò che è racchiuso fra le due sequenze viene ignorato. In C è molto comune anche un altro tipo di commento, da scrivere su una riga singola preceduto dalla sequenza di caratteri "//". Ad esempio:

// Hello World! - Primo programma C

Questa forma di commento non è conforme allo standard ANSI, ma molti compilatori la supportano (a patto che non si usino opzioni che escludono tutte le notazioni non conformi agli standard dalla compilazione). Sarebbe buona norma evitare d'usarle se s'intende scrivere codice che rispetti gli standard, ma onestamente sono pochi i programmatori che lo fanno.

Un altro modo in cui il compilatore separa le token è tramite le altre token. Ad esempio, se noi dovessimo scrivere la riga:

#include <stdio.h>

come

#include<stdio.h>

il compilatore non segnalerebbe nessun errore. Questo perchè in questo caso specifico è in grado di riconoscere "include" come una token senza possibilità d'errore, trattando quindi il resto come token distinte. Ho detto "in questo caso specifico" perchè ci sono situazioni in cui il compilatore non è in grado di dividere da solo le token senza l'ausilio dei white spaces. Ad esempio, se dovessimo scrivere

return0;

invece di

return 0;

genereremmo un errore. Questo dipende dal fatto che in quel determinato punto del programma il compilatore non è in grado d'identificare il tipo di token da solo, assumendo per default che si tratti d'una token di tipo "identificatore" (parleremo + avanti dei vari tipi di token) e gli identificatori possono essere composti anche da numeri oltre che da lettere. Quindi il compilatore tratta "return0" come fosse una singola token e genera un errore, com'era logico aspettarsi.

Tutto questo bel discorso, oltre che per permettervi di capire come individuare e correggere errori nei programmi come detto all'inizio di questo capitolo (il che rappresenterà almeno il 50% della vostra attività come programmatori, mettetevi il cuore in pace), serve per farvi capire una cosa importantissima: usate-gli-white-spaces.

Scrivete SEMPRE codice ben ordinato e leggibile.

Questo aiuterà sia voi che il compilatore, e ridurrà al minimo le possibilità d'errore facendovi risparmiare un sacco di tempo.

Più avanti analizzeremo nello specifico il funzionamento del preprocessore, parlando delle varie fasi di traduzione del codice. In quel capitolo analizzeremo anche i vari tipi di Token.

Capitolo VII: Dati, identificatori e tipi

Autore: Nuitari

Con questo capitolo introdurremo tutta una serie di nozioni fondamentali in quanto alla programmazione in generale ed alla programmazione C in particolare. E’ molto importante che lo leggiate con la massima attenzione.

Sostanzialmente, è possibile definire un programma come un aggregato di “codice” e “dati”: il codice esprime l’algoritmo, i dati sono ciò che l’algoritmo elabora e restituisce,

Per fare un esempio, in una moltiplicazione le regole che usate per moltiplicare due numeri sono l’algoritmo, i due numeri che moltiplicate sono i dati a cui applicate l’algoritmo “moltiplicazione”.

In un programma, i dati vengono memorizzati in aree appositamente riservate della memoria, in modo che il programma possa accedervi ed, eventualmente, modificarli.

Perché il compilatore possa generare codice adatto a trattare queste “zone di memoria” nel modo corretto secondo quelli che sono i vostri desideri, è necessario che conosca la dimensione esatta ed il “tipo” di dati che vi andrete a memorizzare. In alcuni casi il compilatore sarà in grado di determinare tutto automaticamente, in altri sarete voi a dover “dichiarare” esplicitamente gli attributi di queste zone di memoria. Questo concetto all’inizio può sfuggire all’attenzione del principiante, in quanto il cervello umano ha una grande capacità d’astrazione e non fa fatica ad operare con “tipi di dati” differenti. Ad esempio voi siete in grado di fare operazioni matematiche fra numeri naturali e relativi senza essere “influenzati” dal fatto che a tutti gli effetti appartengono ad insiemi differenti. Il computer, ovviamente, manca di questa flessibilità, motivo per cui vi troverete spesso a fare vere e proprie evoluzioni (parlo per esperienza) per lavorare con tipi di dati differenti.

Alcuni di questi dati saranno modificabili durante l’esecuzione del programma (run-time) e prenderanno il nome molto significativo di “variabili”, altri dati non dovranno mai essere modificati ma saranno imputati da voi stessi in fase di stesura del programma (design-time) sotto forma di valori e prenderanno l’altrettanto significativo nome di “costanti”.

Nell’ANSI C le costanti sono dichiarate “implicitamente” all’interno d’un programma, ossia senza nessuna parte di codice riservata allo scopo d’informare il compilatore sulle caratteristiche del dato. Il compilatore è in grado di capire il tipo di dati analizzando il contesto o riconoscendo eventuali simboli prefissi o postfissi. Ad esempio, riprendiamo il nostro programma iniziale e focalizziamo l’attenzione sulla sesta riga:

#include <stdio.h>
#include <stdlib.h>
/* Hello World! - Primo programma C */
int main(int argc, char *argv[])
{
	printf("Hello World!\n");
 - eccetera - 

Come detto nel capitolo 6, la funzione “printf” visualizza sullo schermo quel che è contenuto fra le parentesi (non è proprio così ma assumiamo che sia vero, spiegheremo meglio più avanti l’utilizzo della funzione printf), ossia la scritta “Hello World!\n” (ricordo che gli argomenti di una procedura sono detti “parametri”). Riprendendo l’esempio d’inizio capitolo sulla moltiplicazione, possiamo dire che printf è l’algoritmo, “Hello World!\n” è il dato su cui opera l’algoritmo printf. Di che tipo di dato si tratta? Ovviamente si tratta di una sequenza di caratteri, che come abbiamo spiegato in gergo è definita “stringa”. Inoltre, essendo questo dato un valore inserito direttamente dal programmatore in fase di sviluppo dell’applicazione (in termini informatici anche una stringa è un valore), il quale non subisce variazioni durante l’esecuzione (ne potrebbe, dato che è inserito direttamente nel codice) si tratta di una costante. Ma come fa il compilatore a capire che si tratta di una costante stringa (in inglese: string-literal)? Grazie ai doppi apici aperti all’inizio e chiusi alla fine. Questo è quindi un tipico esempio di definizione di una costante.

Le variabili, invece, nell’ANSI C sono dichiarate esplicitamente, in quanto ad ogni variabile è associato un “identificatore”. Di cosa si tratta?

Come sappiamo, i computer oggigiorno hanno una quantità di memoria enorme. Se pensate alla sola memoria ram del computer come ad una biblioteca, in cui ogni scaffale equivale ad un byte, con 64Mbyte di Ram avreste la bellezza di 67.108.864 possibili scaffali diversi! La zona di memoria in cui viene memorizzato un dato è solitamente decisa automaticamente dal codice generato dal compilatore. Per questo motivo, considerato che “i dati variabili” (da ora in avanti semplicemente “le variabili”) sono tali in quanto è possibile modificarli durante l’esecuzione del programma, come fate a sapere dove sono memorizzati per poterli modificare? Inoltre, supponendo anche di sapere la “locazione di memoria” precisa in cui sono immagazzinati i vari dati, immaginate che difficoltà rappresenterebbe per voi scrivere un programma, se per fare riferimento alle aree di memoria che avete riservato (allocato) doveste usare il loro indirizzo numerico! Immaginate che difficoltà rappresenterebbe infine leggere un programma scritto in questo modo.

Come abbiamo spiegato per un programmatore la leggibilità del codice è un obbiettivo “mission-critical”. Per questo motivo, ad alcune delle zone di memoria che allocherete potrete assegnare un “nome simbolico”, per potervici riferire comodamente, altrimenti chiamato “identificatore”, o “simbolo”.

Anche se nell’ANSI C l’utilizzo degli identificatori è limitato alle variabili, potrebbe essere comodo associare un identificatore anche ad aclune costanti. Ad esempio (esempio molto banale) all’interno d’un ipotetico programma di calcoli geometrici potrebbe capitarvi d’usare spesso il “pigreco”, il cui valore non cambia mai (costante). In questo caso, invece di scrivere ogni volta 3.1415… potrebbe essere molto comodo associare al valore un identificatore ed usare quello invece di ridigitare ogni volta il valore. Per questo, viene in aiuto il preprocessore con le sue direttive. Grazie a #define è infatti possibile associare un nome simbolico ad una qualsiasi costante, qualsiasi sia il tipo. Per esser proprio precisi, #define non dichiara un vero identificatore. Fondamentalmente #define permette di sostituire parti del testo in fase di pre-processing come se definiste una macro (in effetti si chiamano macro), laddove invece gli identificatori invece sono trattati dal compilatore. Sostanzialmente comunque l’effetto è il medesimo che si avrebbe definendo un identificatore, per quanto, è bene tenerlo presente, i nomi simbolici creati con #define NON sono identificatori. Quindi vanno usati come se fossero valori, non identificatori. Analizzeremo la (grande) versatilità e le caratteristiche della direttiva #define in seguito.

Ogni qual volta vorrete associare un identificatore ad una variabile, dovrete tassativamente effettuare una dichiarazione esplicita preventiva (cioè prima dell’effettivo utilizzo dell’identificatore), per i motivi esposti precedentemente. Anche quando definite una funzione (che come spiegato nel capitolo 6 è una procedura che restituisce un valore) dovrete specificare esplicitamente il tipo di dato restituito (vedi main() in Hello World!).

In C, così come in ogni linguaggio di programmazione, esistono diverse sintassi apposita per dichiarare i dati ed i loro tipi e per associarli ad identificatori. La sintassi più comune è:

specificatore-tipo identificatore;

dove identificatore può essere una qualsiasi combinazione di lettere, numeri e del carattere “_” e specificatore-tipo è una parola chiave del C. Prima d’analizzare i vari tipi di dati che è possibile dichiarare, è bene soffermarsi sulle (poche) regole per la scelta d’un identificatore.

Come detto può essere una qualsiasi combinazione di lettere e numeri, MA il primo carattere dev’essere tassativamente una lettera. Quindi, ad esempio, A1 è un identificatore valido, 1A NO. L’ANSI permette un massimo di 31 caratteri per identificatori ad uso interno e di 6 caratteri per identificatori ad uso esterno (analizzeremo successivamente gli identificatori esterni, quindi cercate di rimanere sempre nel limite dei sei caratteri inizialmente). Tutti i caratteri eccedenti il limite, verranno semplicemente ignorati, quindi prestate molta attenzione. Il C interpreta gli identificatori in modo “case sensitive”, ossia facendo distinzione fra minuscole e maiuscole. Quindi pippo e Pippo sono a tutti gli effetti identificatori differenti. Nessun identificatore può essere uguale a parole chiave C. Per “parole chiave” (o keywords) si intende l’insieme di parole riservate del linguaggio C che non dipendono da nessuna libreria. Ad esempio, la parola chiave “return” esiste in C indipendentemente da tutto. Le parole chiave definite dal linguaggio sono poche, ma occorre prestare attenzione. Quindi, ad esempio, non potete creare identificatori che si chiamano “return”, “goto”, etc, ma potete chiamare un identificatore “Return”, “Goto”, etc, anche se ovviamente è sconsigliato per motivi di chiarezza. Nessun identificatore può essere uguale a nomi di funzioni esistenti. Quindi, ad esempio, non potrete chiamare un identificatore “printf”. Ovviamente, come spiegato, le funzioni che avete a disposizione dipendono dai sorgenti esterni che includete nel vostro programma. Quindi, ad esempio, se non includete l’header “stdio.h” (vedi capitolo 6) all’interno del vostro programma a tutti gli effetti non esiste nessuna funzione chiamata “printf”, quindi potreste usare “printf” come nome d’identificatore. Chiaramente è bene evitare comunque di fare una cosa del genere, dato che potrebbe portare a situazioni poco chiare. Infine, come ultima regola, è bene evitare d’usare come nomi d’identificatore un qualsiasi nome che comincia con due caratteri “underscore” (_) o con un “underscore” singolo seguito da una lettera maiuscola, in quanto l’ANSI riserva quel tipo d’identificatori ad utilizzo esclusivo del compilatore.

Chiarite le regole per la scelta dell’identificatore, andiamo quindi ad analizzare i vari “specificatori di tipo” che è possibile utilizzare. Questi sono:

  • char
  • short
  • int
  • long
  • float
  • double
  • signed
  • unsigned
  • void
  • enum
  • struct
  • typedef

char identifica un dato di tipo “carattere”, ossia che contiene un singolo carattere, ed è grande 1 byte.

short identifica un dato di tipo numerico intero (quindi non può contenere valori decimali, con la virgola) con segno (quindi può essere positivo o negativo) ed è grande 2 bytes.

int identifica un dato di tipo numerico intero con segno ed è grande 4 bytes.

long identifica un dato di tipo numerico intero con segno ed è grande 4 bytes.

float identifica un dato di tipo numerico decimale con segno ed è grande 4 bytes.

double identifica un dato di tipo numerico decimale con segno ed è grande 8 bytes.

long double identifica un dato di tipo numerico decimale con segno ed è grande 8 bytes.

Ognuno di questi tipi di dati può essere preceduto dalla keyword “unsigned”, la quale specifica ovviamente che il dato contenuto NON può avere segno (quindi è sempre positivo). Ovviamente la keyword “signed” è pressoché inutilizzata, dato che come avrete capito per default tutti i tipi sono “signed”. E’ possibile specificare unsigned anche per il tipo di dati char, anche se ai fini del tipo di dati memorizzati (carattere) non ha alcun effetto o significato. Allora perché è consentito? In quanto è possibile usare il tipo char anche per memorizzare un piccolo dato numerico, nel qual caso la presenza di un segno ha la sua importanza.

I più attenti fra di voi avranno notato che manca un tipo di dato per le stringhe di caratteri (char non può essere usato per le stringhe, in quanto permette di memorizzare un solo carattere per volta e le stringhe sono “catene” di caratteri). Questo dipende dal fatto che il C nativamente non ha un tipo di dati che permetta di lavorare con le stringhe. Per poterle utilizzare è necessario usare un metodo particolare, che affronteremo successivamente quando parleremo degli “array”. Per il momento, semplicemente non pensateci.

void, enum, struct e typedef sono specificatori di tipo speciali il cui significato per il momento non affrontiamo, vi basti sapere che esistono.

Dato che i dati vengono scritti in memoria e la memoria del computer è un insieme di bit, se ne deduce che maggiore è il numero di bit usato da un tipo, maggiore è il valore che esso può rappresentare. Ad esempio, con 1 byte è possibile rappresentare 256 valori differenti, con 2 bytes 65.535, con 4 bytes 4.294.967.295, con 8 bytes 18.446.744.073.709.551.615 valori differenti :-). Considerato che nel tipo di dati signed normalmente viene riservato un bit per il segno, ne consegue che i dati unsigned possono raggiungere un valore positivo maggiore d’un tipo signed.

A seconda dell’utilizzo che intendete fare di un identificatore, dovrete scegliere attentamente il tipo. Un utilizzo oculato della memoria è uno dei segreti per avere un applicazione snella e veloce, e dev’essere considerato per il programmatore un altro obbiettivo “mission-critical”.

Ovviamente questo non è tutto quel che c’è da sapere sulla dichiarazione degli identificatori e sull’utilizzo degli specificatori di tipo, ma prima di buttare altra carne al fuoco vediamo d’applicare quel che abbiamo imparato finora. Ecco quindi un altro programma d’esempio in cui introduciamo l’uso di variabili ed identificatori.

#include <stdio.h>
#include <stdlib.h>

/* Calcoli geometrici -
   Variabili ed identificatori */

#define PIGRECO 3.1415 /* definisce una macro per il valore del pigreco.
                          solitamente le macro hanno nomi tutti maiuscoli per permettere
                          di distinguerle dai normali identificatori */

int main(int argc, char *argv[])
{
        int raggio, altezza; /* è possibile dichiarare 2 identificatori DELLO STESSO TIPO
                                              sulla stessa riga separandoli con una virgola */


        printf("Sara' calcolato il volume di un cilindro e di un cono la cui base\ne' un cerchio definito dall'utente.\n");

        printf("\nImmettere il raggio del cerchio: ");
        scanf("%i", &raggio);

        printf("Immettere l'altezza: ");
        scanf("%i", &altezza);

        double area_cerchio = raggio * raggio * PIGRECO; /* è possibile valorizzare (inizializzare) 
                                                            un indicatore mentre lo si dichiara */

        double volume_cilindro = area_cerchio * altezza;
        double volume_cono = area_cerchio * (altezza / 3);

        printf("\nIl volume del cilindro e' %f\nIl volume del cono e' %f\n", volume_cilindro, volume_cono);

        system("PAUSE"); /* omettere questa riga su Linux */
        return 0;
}

Questo programma, decisamente più complesso del precedente, introduce tanti nuovi concetti che analizzeremo dettagliatamente in più capitoli. Per il momento non è necessario comprenderne appieno ogni singola sfumatura, bensì è utile concentrarsi sull’argomento di questo capitolo. Analizziamolo passo passo:

#include <stdio.h>
#include <stdlib.h>

/* Calcoli geometrici -
   Variabili ed identificatori */

Questa parte è in tutto identica a quella del programma precedente, quindi la saltiamo.

#define PIGRECO 3.1415 /* definisce una macro per il valore del pigreco.
                          solitamente le macro hanno nomi tutti maiuscoli per permettere
                          di distinguerle dai normali identificatori */

"#define" è una direttiva al preprocessore (come il # che la precede dovrebbe far intuire), similare ad #include quindi. Come spiegato, #define crea una macro di nome PIGRECO. In altre parole, il preprocessore quando “digerirà” il codice sostituirà tutte le occorrenze di “PIGRECO” con 3.1415, come fosse un normale Search&Replace.

int main(int argc, char *argv[])
{

Anche la funzione main() non è variata rispetto a prima, considerato che come abbiamo detto è predefinita e quindi sempre identica.

        int raggio, altezza; /* è possibile dichiarare 2 identificatori DELLO STESSO TIPO
                                              sulla stessa riga separandoli con una virgola */

Eccoci al dunque. Con questa riga abbiamo dichiarato e definito due aree di memoria, due variabili, le quali conterranno due dati di tipo intero con segno, a cui sono associati due identificatori: raggio ed altezza. Come spiegato nel commento, è possibile mettere più dichiarazioni di identificatori dello stesso tipo sulla stessa riga, separandoli con la virgola.

        printf("Sara' calcolato il volume di un cilindro e di un cono la cui base\ne' un cerchio definito dall'utente.\n");

        printf("\nImmettere il raggio del cerchio: ");
        scanf("%i", &raggio);

        printf("Immettere l'altezza: ");
        scanf("%i", &altezza);

Queste righe probabilmente appariranno complesse, ma solo perché non si ha ancora una buona conoscenza delle funzioni printf() e scanf(). printf() come sappiamo visualizza un output sul dispositivo standard di Output, ossia lo schermo. scanf() invece legge uno o più dati dal dispositivo standard di Imput (ossia la tastiera) e li memorizza in delle variabili. La sintassi di scanf() è similare a quella di printf(): il primo parametro, una costante stringa, specifica il tipo di valori che scanf() andrà a leggere, successivamente separate da virgole vengono riportate le variabili in cui i valori devono essere memorizzati. La costante stringa parametro di scanf() è “%i” ed in gergo è definita “format string”, ossia una sequenza particolare di caratteri che determina il formato dei dati letti, %i ha infatti il significato di “intero”. Le varie sequenze di caratteri utilizzabili in scanf() da associare al simbolo %, con i rispettivi formati sono:

  • d Intero decimale
  • i Intero decimale, esadecimale o ottale
  • o ottale
  • u intero unsigned decimale
  • x intero esadecimale
  • e, E, f, g, G Valore a virgola mobile, con segno e parte decimale separata da un punto opzionali, eventualmente seguito da un esponente (decimale con o senza segno) separato dal resto con una lettera “e” o “E” (notazione scientifica)
  • n utilizzo speciale, da vedere in seguito.

Quindi il funzionamento di scanf() dovrebbe apparire chiaro. Un ultimo appunto: i più attenti avranno notato che a sinistra del nome della variabile c’è un carattere “&”. Qual è il suo significato? Il carattere & in c’è è definito “operatore unario”, in quanto richiede un solo parametro alla sua destra (in questo caso la variabile) ed ha il significato di “indirizzo di” (address off). In altre parole la funzione scanf() non vuole i semplici nomi delle variabili in cui memorizzare i dati, ma l’indirizzo di memoria a cui “puntano” gli identificatori. L’operatore & adempie esattamente a questo scopo. Il funzionamento dell’operatore unario & e del suo operatore inverso (“*”, “pointer of”) verrà trattato in seguito. Proseguiamo:

        double area_cerchio = raggio * raggio * PIGRECO; /* è possibile valorizzare (inizializzare) 
                                                            un indicatore mentre lo si dichiara */

        double volume_cilindro = area_cerchio * altezza;
        double volume_cono = area_cerchio * (altezza / 3);

In queste righe dichiariamo e definiamo tre nuove variabili: area_cerchio, volume_cilindro e volume_cono, tutte di tipo “double” (ossia a virgola mobile). Il motivo per cui usiamo dati di tipo double è per avere una maggiore precisione nei risultati, dato che il pigreco è un numero a virgola mobile.

Chi ha studiato un po’ di matematica noterà che non si tratta altro che di semplici espressioni matematiche (il “*” equivale alla moltiplicazione ed il “/” alla divisione): il risultato della formula a destra dell’operatore d’assegnamento “=” viene assegnato per l’appunto alla variabile a sinistra dell’operatore, niente di più semplice. Come potete notare l’espressione matematica usa variabili e costanti numeriche insieme, come se si trattasse di valori. Questo è possibile poiché, ovviamente, le variabili vengono “semplificate” con i valori che contengono, ai fini del calcolo.

Come indicato dal commento, è possibile assegnare il valore ad una variabile mentre la si dichiara. La prima assegnazione di una variabile è detta “inizializzazione”. In genere, quando si definiscono variabili, è bene inizializzarle sempre, dato che potrebbero contenere valori casuali che potrebbero compromettere il funzionamento del programma (vedremo poi come mai).

        printf("\nIl volume del cilindro e' %f\nIl volume del cono e' %f\n", volume_cilindro, volume_cono);

In questa riga abbiamo usato printf() in un modo diverso dal solito. In pratica, riassumento, è possibile far “includere” dentro la stringa stampata da printf() delle variabili, usando gli specificatori di formato già visti per scanf(). Quindi, considerato che ci sono due specificatori %f all’interno della stringa, printf() andrà a pescare i primi due valori che seguono la stringa (sempre separati da virgole, ovviamente, come tutti i parametri) e li posizionerà li in mezzo. In questo caso i valori che intendiamo stampare sono in delle variabili, quindi dopo la stringa ci sono le due variabili che contengono i valori da stampare (dato che facciamo riferimento ai valori contenuti nelle variabili non va usato il prefisso &, che ricordiamo, specifica l’indirizzo di una variabile).

I vari specificatori di formato che è possibile usare con printf() sono i seguenti:

  • c carattere singolo
  • C Wide Character, lo tratteremo in seguito
  • d decimale intero con segno
  • i decimale intero con segno
  • o ottale intero senza segno
  • u decimale intero senza segno
  • x esadecimale intero senza segno (lettere minuscole)
  • X esadecimale intero senza segno (lettere maiuscole)
  • e valore double con eventuali segno ed esponente (e minuscola)
  • E valore double con eventuali segno ed esponente (E Maiuscola)
  • f come sopra, con la possibilità di specificare il formato (lo tratteremo in seguito).
  • g Come "e" od "f", ma più compatto
  • G come sopra ma con l'esponente
  • n lo tratteremo in seguito
  • p lo tratteremo in seguito
  • s Stringa
  • S Wide String, lo tratteremo in seguito

Infine:

        system("PAUSE"); /* omettere questa riga su Linux */
        return 0;
}

Le ultime righe sono identiche a quelle di Hello Word!, quindi non necessitano di commenti.

A questo punto dovrebbe essere chiaro il modo in cui il C tratta i dati. Nel prossimo capitolo approfondiremo l’argomento. Nel frattempo, come esercizio, potreste modificare il programma facendo in modo che accetti come input dall’utente anche numeri con la virgola, e non solo numeri interi.

CAPITOLO VIII: Algoritmi

La risoluzione di ogni problema di elaborazione comporta l'esecuzione, in un ordine ben preciso, di una serie di azioni.Una procedura che risolva un problema in termini di:

  1. azioni che devono essere eseguite e
  2. l'ordine in cui tali azioni devono essere eseguite

è detta algoritmo.

Lo pseudocodice

Uno pseudocodice è un linguaggio artificiale e informale, che che aiuta i programmatori a sviluppare gli algoritmi.In realtà, i programmi scritti in pseudocodice non possono essere eseguiti sui computer. Essi, servono a aiutare il programmatore a 'riflettere' sul programma prima di poterlo scrivere in un vero e proprio linguaggio di programmazione come il C. In molti casi, tutto ciò sarà ottenuto sostituendo le istruzioni in pseudocodice con quelle equivalenti nel linguaggio C.

Il comando di selezione if, if...else

Le strutture di selezione sonousate per scegliere tra percorsi di azione alternativi.Supponete che la votazione per superare un esame sia 60, in pseudocodice sarà:

Se il voto dello studente è maggiore o uguale a 60 visualizza <promosso>

La precedente istruzione in pseudocodice se può essere scritta in C come:

if ( grade >= 60)
  printf( "promosso\n" );

Questa è una delle proprietà dello pseudocodice che lo rende un utile strumento di programmazione,data la sua stretta corrispondenza con il C. Il comando di selezione if esegue l'azione indicata solo quando la condizione risulta vera: in caso contrario,l'azione viene ignorata.Il comando di selezione if...else permette al programmatore di specificare che, nel caso in cui la condizione sia vera, dorà essere eseguita una azione differente da quella che si dovrà eseguire qul'ora la condizione risulti falsa.Per esempio:

Se il voto dello studente è maggiore o uguale a 60 visualizza 'Promosso' altrimenti visualizza 'bocciato'.

La precedente istruzione in pseudocodice se/altrimenti può essere scritta come segue:

if ( grade >= 60 )
  printf( "promosso\n" );
else
  printf( "Bocciato\n" ); 

Il C fornisce anche l'operatore condizionale (?:) che è strettamente correlato con il comando if...else.Quello condizionale è l'unico operatore ternario del C:praticmente accetta tre operandi. Gli operandi, insieme all'operatore condizionale, formano una espressione condizionale. Il primo operando è una condizione,il secondo è il valore che assumerà l'intera espressione qual'ora la condizione risulti essere vera, il terzo operando è il valore che assumerà lintera espressione se la condizione risulti essere falsa,per esempio:

printf( "%s\n", grade >= 60 ? "promosso" : "bocciato" ); 

I valori in una espressione conizionale posono anche essere delle azioni da seguire.Per esempio,l'espressione condizionale:

grade >= 60 ? printf( "promosso\n" ) : printf( "bocciato\n" ); 

si leggerà:se grade è maggiore o uguale a 60 printf( "promosso\n" ), altrimenti printf( "bocciato\n ).

Strumenti personali
Namespace

Varianti