Issuu on Google+

Linguaggio C di Luca Petrosino


Copyright (c) 2010 Luca Petrosino E’ garantito il permesso di copiare, distribuire e/o modificare questo documento seguendo i termini della Licenza per Documentazione Libera GNU, Versione 1.1 o ogni versione successiva pubblicata dalla Free Software Foundation; senza sezioni non modificabili. Una copia della licenza `e acclusa nella sezione intitolata ”Licenza per la documentazione libera GNU”.

2


Indice 1 Introduzione

6

2 Tipi di dati 2.1 Variabili e nomi . . . . . . . 2.2 Tipi di dati . . . . . . . . . 2.3 Dichiarare una variabile . . 2.4 Costanti . . . . . . . . . . . 2.5 Struttura di un programma 2.6 Il primo programma . . . . 2.7 Operatori . . . . . . . . . . 2.7.1 Operatori aritmetici 2.7.2 Operatori relazionali 2.7.3 Operatori logici . . . 2.7.4 Operatori bit a bit .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

7 7 8 9 10 10 12 15 15 16 16 17

3 Strutture di controllo 3.1 Blocco di istruzioni 3.2 if()-else . . . . . . 3.3 else if() . . . . . . 3.4 switch() . . . . . . 3.5 for() . . . . . . . . 3.6 while() . . . . . . . 3.7 do-while() . . . . . 3.8 break e continue . 3.9 goto e label . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

18 18 18 20 20 22 23 24 25 27

4 Il problema dell’input 4.1 Input da tastiera: scanf() 4.2 Libreria string.h . . . . 4.2.1 strlen . . . . . . . 4.2.2 strcmp e strncmp . 4.2.3 strcat e strncat . . 4.2.4 strcpy e strncpy . 4.2.5 strstr . . . . . . . 4.2.6 strchr e strrchar . 4.2.7 strtok . . . . . . . 4.2.8 sprintf . . . . . . . 4.2.9 snprintf . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

. . . . . . . . . . .

28 28 29 29 30 31 31 32 33 33 34 34

. . . . . . . . .

. . . . . . . . .

. . . . . . . . .

3


4.2.10 sscanf . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 4.2.11 gets . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 5 Funzioni e visibilit` a di variabili 5.1 Struttura di una funzione . . . . 5.2 atoi e atof . . . . . . . . . . . . . 5.3 Variabili e funzioni: visibilit`a . . 5.3.1 Variabili static e auto . . 5.3.2 Variabili register e volatile

35 35

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

38 38 41 43 46 47

6 Array e puntatori 6.1 Array . . . . . . . . . . 6.2 Puntatori . . . . . . . . 6.3 Scambio di parametri . 6.4 Gestire la memoria con i 6.5 Puntatori a funzioni . .

. . . . . . . . . . . . . . . . . . puntatori . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

48 48 48 49 52 55

7 Ordinamento di array 7.1 Ordinamento . . . . 7.2 Selection sort . . . . 7.3 Bubble sort . . . . . 7.4 Quick sort . . . . . . 7.5 Merge sort . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

57 57 58 59 61 62

8 Strutture 8.1 Definizione ed esempi . . . . . . 8.2 typedef . . . . . . . . . . . . . . 8.3 Strutture e funzioni . . . . . . . 8.4 Strutture dati, un esempio: lista 8.5 Le union . . . . . . . . . . . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

65 65 67 68 68 73

9 File 9.1 9.2 9.3 9.4 9.5 9.6 9.7

. . . . .

. . . . .

. . . . .

. . . . .

. . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

. . . . . . .

75 75 76 76 77 77 78 79

10 Direttive per il preprocessore 10.1 Includere file di librerie . . . 10.2 Definire costanti e macro . . . 10.3 Macro predefinite . . . . . . . 10.4 Compilazione condizionata . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

80 80 81 81 82

11 Funzioni di libreria 11.1 Libreria ctype.h . 11.2 Libreria math.h . . 11.3 Libreria stdlib.h 11.4 Libreria time.h . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

85 85 86 86 87

Aprire e chiudere file . . . . Scrivere su file . . . . . . . Leggere da file . . . . . . . Esempio: il file esiste? . . . Esempio: il comando cat . . Esempio: lettura e scrittura Altre funzioni per file . . .

. . . .

. . . .

. . . .

. . . .

. . . .

. . . .

4


12 Licenza per la documentazione 12.1 Preamble . . . . . . . . . . . 12.2 Applicability and definictions 12.3 Verbatim copying . . . . . . . 12.4 Copying in quantity . . . . . 12.5 Modifications . . . . . . . . . 12.6 Combining documents . . . . 12.7 Collections of documents . . . 12.8 Aggregation with independent 12.9 Translation . . . . . . . . . . 12.10Termination . . . . . . . . . . 12.11Future revision of this license 12.12Relicensing . . . . . . . . . .

libera GNU . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . works . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .

5

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

. . . . . . . . . . . .

88 88 88 90 90 91 92 93 93 93 94 94 95


Capitolo 1

Introduzione Il linguaggio C, proposto da Dennis Ritchie nel 1972, fu ideato nei laboratori Bell della AT&T come evoluzione del linguaggio B (usato da Ken Thompson per la scrittura dei primi sistemi operativi UNIX). L’obiettivo rimaneva (ed `e tuttora) sempre lo stesso, supportare la scrittura di sistemi operativi e applicazioni software per lo stesso! L’interazione con l’hardware, che avviene con un linguaggio macchina (tipicamente un linguaggio assembly legato all’architettura del processore), viene in questo modo semplificata ulteriormente. Il linguaggio C `e infatti un linguaggio di alto livello (traduce le istruzioni del programma in linguaggio macchina) ma all’occassione permette al programmatore (esperto) di continuare ad usare il linguaggio macchina. Per questo motivo, talvolta viene anche identificato con la locuzione (pi` u ambigua) linguaggio di medio livello, se non addirittura (in modo certamente improprio) come macro-assembly, o assembly portabile. Il C `e conosciuto per la sua efficienza, e si `e imposto come linguaggio di riferimento per la realizzazione di software di sistema su gran parte delle piattaforme hardware moderne. La standardizzazione del linguaggio garantisce la portabilit` a dei programmi scritti in C su qualsiasi piattaforma. Il linguaggio C viene definito formalmente nel 1978 da Brian Kernighan e Dennis Ritchie. Nel 1983 l’American National Standars Institute (ANSI) inizi`o la definizione di uno standard che, nel 1990, diede quindi origine allo Standard ANSI C (ISO C89). L’evoluzione del linguaggio `e poi continuata negli anni, l’ultimo standard rivisto risale al 1999 (ISO C99, che adotta alcune delle caratteristiche presenti in C++). Il linguaggio C, da cui oggi derivano tanti altri linguaggi di programmazione, merita di sicuro di essere appreso ed `e quello che cercheremo di fare in questo piccolo manuale. Invito il lettore (curioso) a consultare la Rete per ampliare, poi, successivamente la conoscenza del linguaggio di programmazione.

6


Capitolo 2

Tipi di dati L’informazione `e alla base dell’informatica. Per poterla manipolare bisogna prima racchiuderla in un contenitore adeguato. In questo capitolo, pertanto, ci occuperemo dei tipi di dati offerti dal linguaggio, i vari contenitori per l’informazione.

2.1

Variabili e nomi

Le variabili di un programma contengono l’informazione, quella cercata o attesa dall’utente. Ogni variabile, prima di essere adoperata, va necessariamente dichiarata. La dichiarazione di una variabile prevede un nome di un tipo (per l’identificazione di un tipo di dati) e un nome per la variabile (ed eventualmente un valore iniziale). Il tipo assegnato ad una variabile determina, inevitabilmente, i possibili valori che essa pu`o assumere. Il nome scelto per la variabile, invece, permetter` a in seguito di poter fare riferimenti alla variabile dichiarata. La dichiarazione di una variabile provoca l’allocazione in memoria dello spazio utile a conservare i possibili valori assunti dalla stessa variabile. Una variabile che assume lo stesso valore per tutta la durata del programma viene detta costante. Ogni variabile occupa spazio in memoria. Se non vogliamo appesantire il programma che stiamo scrivendo dobbiamo, allora, sforzarci di definire le variabili che realmente ci occorrono! Esistono alcune importanti limitazioni che riguardano i possibili nomi di variabili. Ogni nome di variabile pu`o essere composto sia da lettere che da cifre. Il primo carattere, tuttavia, deve essere necessariamente una lettera. Non sono inoltre permessi caratteri di spazio, se proprio ci occorrono meglio allora usare il carattere ” ” (detto underscore). In ogni caso il nome di una variabile pu`o contenere al pi` u 31 caratteri! Il linguaggio C `e sensitive case, distingue cio`e i caratteri maiuscoli da quelli scritti in minuscolo. Pertanto, una variabile con nome x `e del tutto diversa da una variabile avente nome X. Vi capiter`a molto spesso di dare uno sguardo al codice sorgente di un programma (scritto da altri), una convenzione assai comune fra i programmatori prevede l’uso di caratteri in minuscolo per le variabili e di caratteri in maiuscolo per le costanti. Vi chiederete, perch`e? In questo modo, osservando il codice sorgente del programma, si pu`o stabilire immediatamente la natura di una variabile! Per questo motivo invito il lettore a usare

7


questa convenzione fin dai primi programmi. Il nome da assegnare ad una variabile deve essere scelto in base al compito dato alla stessa. Ci` o produrr` a un buon codice sorgente autodocumentato (soprattutto se accompagnato da righe di commento come vedremo a breve). Alcuni nomi di variabili sono tuttavia vietati poich`e possono confondere il compilatore, che li interpreta quindi in altro modo. A tale proposito vanno evitati i nomi usati per identificare i tipi di dati (int, float, etc..) e le parole chiave usate dal linguaggio per descrivere il flusso di esecuzione del programma (if, else, for, while, etc...). Tutte le parole chiave usate dal linguaggio, che vedremo nei capitoli successivi, non possono essere usate come nome per variabili di programma.

2.2

Tipi di dati

A differenza dei moderni linguaggi di programmazione, il linguaggio C offre al programmatore pochi tipi di dati (fondamentali): • int: tipo dati per numeri interi; • float: tipo dati per numeri razionali; • double: tipo dati per numeri razionali; • char: tipo dati per rappresentare un carattere; Ognuno di questi tipi di dati pu`o inoltre essere completato con un ulteriore specificatore: long, short, signed e unsigned. Una variabile di tipo int occupa in memoria, solitamente, 16 bit. Ci`o permette, quindi, la rappresentazione di 65536 simboli numerici, suddivisi nell’intervallo che va da -32768 a +32767. Su alcune macchine una variabile di tipo int pu`o anche occupare 32 bit. A tale proposito, per far aderire meglio il tipo di dati alla variabile, si pu` o riccorrere ad uno degli specificatori elencati in precedenza. Una variabile short int occuper`a allora 16 bit, mentre una variabile long int occuper` a 32 bit (da -2147483648 a +2147483647). Di default ogni variabile di tipo int `e dichiarata come signed int, in questo modo l’intervallo di valori viene equamente suddiviso (a meno dello 0) fra valori negativi e positivi. Se nell’applicazione che stiamo scrivendo una variabile assume esclusivamente valori positivi si pu` o allora dichiarare la stessa variabile come unsigned short. In tal caso tutti i 16 bit verranno impiegati per la rappresentazione di valori positivi o privi di segno (e quindi nell’intervallo da 0 a 65535). Con unsigned long (32 bit), invece, possiamo rappresentare numeri nell’intervallo da 0 a 4294976295. Per i numeri razionali esistono due tipi di dati, float e double. Ognuno di questi prevede una determinata precisione che pu`o variare a seconda dell’architettura del processore. Il tipo float `e anche detto a singola precisione (occupa, di default, 32 bit), a differenza del tipo double che viene invece detto a doppia precisione (pu` o occupare 32 bit). I numeri razionali sono rappresentati attraverso la notazione esponenziale e prevedono una mantissa e un esponente. La precisione `e stabilita dai bit assegnati all’esponente sicch`e per i due tipi di dati non varia l’intervallo di valori rappresentabili ma il numero di cifre decimali. Un float offre una precisione che solitamente si spinge fino alla settima cifra decimale mentre un double, essendo pi` u precisio, arriva fino alla sedicesima cifra decimale! Per alcuni compilatori, infine, una variabile dichiarata come 8


long float equivale a dichiarare una varibile di tipo double. Il tipo di dati char occupa 8 bit e permette, quindi, la rappresentazione di 256 simboli (suddivisi in caratteri dell’alfabeto, numeri e simboli). L’intervallo di valori va da -127 a +128 se la variabile `e dichiarata come signed char. Se invece dichiariamo una variabile come unsigned char l’intero intervallo di valori va da 0 a 255. Di default la dichiarazione di una variabile char sottointende la dichiarazione unsigned char. La possibilit`a di dichiarare una variabile come signed char esiste esclusivamente per garantire un certo grado di compatibilit`a verso alcune architetture di processori. I file limits.h e float.h contengono le informazioni usate dal compilatore per le ampiezze dei tipi di dati discusse in questo paragrafo. Tipo (signed-unsigned) (signed-unsigned) (signed-unsigned) (long) float double (signed-unsigned)

2.3

short int int long int

char

Occupazione in memoria 16 bit 16 oppure 32 bit 32 bit 16 (32) bit 32 bit 8 bit

Dichiarare una variabile

Ogni variabile, prima di essere manipolata e quindi utilizzata, va dichiarata indicando al compilatore un tipo, un nome ed eventualmente un valore (inizializzazione o variabile inizializzata). Ogni istruzione, ad eccezione delle dichiarazioni di costanti (che vedremo a breve) e di altre direttive (come quelle orientate al preprocessore, che vedremo in seguito), deve necessariamente terminare con il carattere ;. int x,y,z; float sqr=1.0e-5; double space; int i=0; char answer; char input[100]; La riga int x,y,z dichiara tre variabili di tipo intero avente nome, rispettivamente, x, y e z. La riga float sqr=1.0e-5 dichiara una variabile gi`a inizializzata, di tipo float, avente nome sqr e con valori fissati sia per la mantissa che per l’esponente. La variabile space, invece, `e di tipo double, a doppia precisione, e non `e inizializzata. La variabile i, di tipo intero, viene dichiarata e inizializzata a 0. Incontreremo spesso nei programmi inizializzazioni di questo tipo. La dichiarazione char answer alloca lo spazio necessario a memorizzare una variabile di tipo char, con nome answer. Una variabile fatta in questo modo potrebbe ad esempio raccogliere l’input digitato dall’utente come risposta ad una domanda a lui sottoposta dal programma in esecuzione. L’ultima dichiarazione, char input[100], alloca lo spazio necessario a memorizzare un array di 100 caratteri. Ci occuperemo degli array in seguito, per adesso `e sufficiente sapere che un array `e una 9


collezione di elementi, tutti dello stesso tipo, raggruppato sotto un unico nome (quello dato alla variabile).

2.4

Costanti

Per definire una costante si ricorre alla direttiva #define seguita dal nome da assegnare alla costante e dal valore da assegnare alla stessa (valido per tutta la durata del programma). Una costante avente valore intero viene di default memorizzata in un int. Qualora il valore assegnato dovesse risultare particolarmente grande il compilatore lo considerer` a come long. Per indicare immediatamente la rappresentazione di una costante long si pu`o fare seguire al valore numerico la lettera l (oppure L). Ad esempio: #define K 100, definisce una costante int avente nome K e valore 100. Ed ancora: #define K 100000000L, dichiara una costante con nome K e valore 100000000L (si tratta quindi di un long int). Anche i qualificatori unsigned e signed possono essere impiegati per descrivere meglio una costante numerica. Ogni costante definita, di default, `e una costante signed, dotata quindi di segno. Se si vuole indicare una costante priva di segno, quindi unsigned, `e sufficiente far seguire il valore numerico dalla lettera u oppure U. Ad esempio, la costante cos`ı definita: #define K 100UL, dichiara una costante numerica avente valore 100 e rappresentata in un unsigned long int! Possiamo anche specificare il valore di una costante numerica in ottale oppure in esadecimale (anzich`e usare la rappresentazione decimale). Se siamo interessati a una costante numerica ottale `e sufficiente anteporre alla stessa il simbolo/carattere 0. Se invece siamo interessati ad una costante numerica esadecimale dobbiamo allora anteporre alla stessa il prefisso 0x. Se la nostra costante `e invece un carattere (ad esempio usato durante un confronto con altri caratteri) essa va allora racchiusa da apici. Ad esempio: #define quit ’q’, dichiara la costante di tipo char avente nome quit e valore q. Possiamo allora usare la suddetta costante all’interno del nostro programma, ad esempio per stabilire se terminare un programma o meno quando l’input dato dall’utente `e uguale alla costante quit. Una sequenza di caratteri costituisce una stringa, una stringa costante va invece racchiusa fra doppi apici: #define user "root". Nell’esempio appena visto abbiamo dichiarato la stringa costante avente nome user e valore root.

2.5

Struttura di un programma

Sebbene sia possibile dichiarare in qualsiasi punto una variabile di programma (prima del suo uso), `e bene dare al programma una struttura ben definita, anche perch`e un programma scritto in linguaggio C prevede una particolare struttura! Questo faciliter` a senza dubbio la lettura e la manutenzione del codice. Il file che raccoglie tutte le istruzioni del programma viene detto file o codice sorgente ed ha solitamente l’estensione .c. Un file sorgente pu`o richiedere l’uso di una particolare funzione di libreria (o variabile in essa definita). Le funzioni di librerie di programmi vengono riunite in particolari file dotati di estensione .h (detti header ), l’implementazione delle stesse funzioni avviene invece in normali

10


file .c. Un programma pu` o, allora, essere composto da uno o pi` u file (e file di libreria), dipende sia dalla complessit`a del software che dalla sua suddivisione logica (che a volta `e anche naturale!). Il linguaggio C `e un linguaggio compilato, per l’esecuzione del programma bisogna necessariamente realizzare un file eseguibile mediante operazione di compilazione. Il compilatore, allora, legge dal file sorgente le istruzioni e le traduce in linguaggio macchina, generando un file con estensione .o (oppure .obj). E’ all’interno di questo file che vengono scritte le istruzioni in linguaggio macchina, il file eseguibile creato al termine del processo di compilazione viene allora collegato al suddetto file (operazione di linking). Un file sorgente deve necessariamente iniziare suggerendo al compilatore (che ne tradurr` a le istruzioni) gli header che il programma intende usare. Gli header usati vengono dichiarati nel file sorgente attraverso la direttiva #include. Se nel file sorgente viene usata una nuova funzione (che non sia cio`e presente in funzioni di librerie), si deve allora dichiarare quest’ultima fornendo al compilatore un prototipo prima, e una sua implementazione dopo. Quindi si pu`o procedere alla dichiarazione delle costanti usate dal programma (se queste esistono). A tal proposito si user` a allora la direttiva #define. Ogni programma si deve sviluppare all’interno di una funzione principale chiamata main (l’esecuzione del programma non `e altro che l’esecuzione delle istruzioni contenute nel main del programma). #include <FILE_DI_LIBRERIA_1> #include <FILE_DI_LIBRERIA_2> ... #include <FILE_DI_LIBRERIA_N> PROTOTIPO_FUNZIONE_1() PROTOTIPO_FUNZIONE_2() ... PROTOTIPO_FUNZIONE_N() #define COSTANTE_1 #define COSTANTE_2 ... #define COSTANTE_N main() { DICHIARAZIONI_VARIABILI ... ISTRUZIONE_1 ISTRUZIONE_2 ... ISTRUZIONE_N } FUNZIONE_1() { ... }

11


FUNZIONE_2() { ... } ... FUNZIONE_N() { ... }

2.6

Il primo programma

In questo paragrafo osserveremo da vicino il primo programma scritto in linguaggio C. Si tratta di un esempio, ormai, diventato famoso: la stampa di un messaggio nella finestra del terminale. /* Primo programma */ #include <stdio.h> int main(void) { printf ("Hello world!\n"); return 0; } Prima di iniziare a commentarne ogni singola linea vi invito ad individuare nel codice sorgente la struttura del programma cos`ı come essa `e stata descritta nel paragrafo precedente (alcuni blocchi dichiarativi non sempre sono presenti!). La prima riga del codice sorgente ci permette di introdurre i tag da usare per i commenti alle istruzioni. Tutto ci`o che si trova fra i caratteri /* e */ non viene considerato dal compilatore. Possiamo dunque inserire l`ı i nostri commenti e scrivere, eventualmente, gli stessi anche su pi` u righe! Ecco un esempio: /* Programma A: introduzione Il programma A va eseguito con ... */ Se il commento che desideriamo inserire deve invece occupare una sola righa possiamo allora far iniziare la stessa riga con i simboli // e aggiungere, quindi, di seguito il commento. Ecco un esempio: // Programma A: il programma A va eseguito con... Quest’ultima forma di commento viene spesse volte usate per commentare un’istruzione del programma che durante una fase di sviluppo del programma non deve essere eseguita. L’istruzione verr` a interpretata dal compilatore come un commento e non verr` a, pertanto, eseguita. L’istruzione #include <stdio.h> suggerisce al compilatore l’intenzione, da parte del programma, di usare funzioni della libreria stdio.h. In questo semplice esempio non ci sono dichiarazioni di prototipi di funzioni e costanti. Il codice sorgente prevede immediatamente la definizione del main. Le istruzioni che lo

12


compongono (cos`ı come quelle di ogni funzione, non dimenticate che main `e una funzione!) vanno sempre racchiuse all’interno dei simboli { e } che stabiliscono l’inizio e la fine del blocco di istruzioni. Ogni funzione pu` o ricevere un input e generare un output, come risposta alla chiamata. La funzione main dell’esempio restituisce al termine dell’esecuzione il valore intero 0, non ricevendo alcun input. L’assenza di input e/o output viene indicato dalla parola chiave void (che quindi non pu`o essere usata come nome di variabile, ricordate?). L’istruzione return 0 indica il valore ritornato. Ogni istruzione termina con il carattere ;. L’istruzione printf(...) invoca la chiamata alla funzione printf, della libreria stdio.h. Per la suddetta funzione, tutto ci`o che si viene a trovare fra i doppi apici viene stampato nella finestra del terminale. Il messaggio stampato `e Hello world!, l’esecuzione del programma conferma l’assenza dei caratteri \n, cosa significano quei caratteri? Per la stampa di messaggi di testo esistono diverse sequenze di escape, ovvero, sequenze di caratteri che suggeriscono alla funzione printf di stampare particolari caratteri di formattazione del testo. Ecco un elenco: • \n: sta per new line ed indica alla funzione di stampare il successivo testo su una nuova riga; • \t: sta per tab ed indica alla funzione di stampare un carattere di tab; • \b: sta per backspace ed indica alla funzione di cancellare l’ultimo carattere stampato; • \a: sta per alarm, la funzione indica al sistema operativo di emettere un suono (solitamente un beep, se l’hardware `e dotato di speaker); • \f: il successivo messaggio di stampa verr`a stampato in una nuova pagina del terminale; • \": visto che i doppi apici delimitano il messaggio da stampare, per usarli all’interno di un messaggio si deve ricorrere a questa sequenza di escape; • \’: sequenza di escape per la stampa di un singolo apice; Ecco una versione leggermente differente dello stesso programma: /* Primo programma */ #include <stdio.h> #define messaggio "Hello world!\n" int main(void) { printf (messaggio); return 0; } La costante messaggio, il cui valore `e inizializzato alla stringa Hello world!\n, viene passato come parametro alla funzione printf. Ci`o `e del tutto lecito, la funzione stampa stringhe di caratteri e la variabile messaggio `e stata dichiarata come una stringa! E se vogliamo stampare il valore di una variabile o costante

13


di diverso tipo? Ad esempio un int oppure un float? La funzione printf `e ancora valida per i nostri scopi, dobbiamo solo segnalare alla funzione le nostre intenzioni formattando il testo da stampare in modo adeguato. Ogni tipo di dati viene identificato da uno dei seguenti formattatori : • %d oppure %i: per variabili di tipo int; • %u: per variabili di tipo int senza segno; • %f:per variabili dipo float o double; • %c: per variabili di tipo char; • %s: per stringhe; La stampa del carattere % all’interno di una stringa avviene attraverso il formattatore %%. La variabile che invece dovr` a prendere il posto del formattatore va indicata subito dopo il messaggio da stampare. Il messaggio e la variabile da stampare vanno separati da una virgola. L’ordine delle variabili da sostituire nel messaggio `e anche l’ordine con cui queste vengono fornite alla funzione printf. Ecco un esempio: /* Era il primo programma */ #include <stdio.h> #define messaggio "Variabile" int main(void) { int a=100; float x=100.001; char answer=’y’; printf ("%s a: %i\n",messaggio,a); printf ("%s x: %.2f\n",messaggio,x); printf ("%s answer: %c\n",messaggio,answer); return 0; } Per la stampa della variabile float viene usata una particolare formattazione. Anzich`e stampare la variabile con il formattatore %f (che adoperer`a nella stampa tutte le cifre decimali disponibili per il tipo float) si fanno precedere allo stesso i caratteri .2. In questo modo si sta suggerendo al compilatore di interpretare la stampa della variabile float adoperando per la stessa due sole cifre decimali. A tale proposito invito il lettore ad eseguire lo stesso programma avendo per`o cura di sostituire il formattatore %.2f con il formattatore %.3f. Ci`o provocher`a anche la stampa della terza cifra decimale (la variabile x ha valore 100,001). Possiamo anche forzare la stampa delle cifre che compone il float, suggerendo al compilatore il numero minimo di cifre che dovr`a comporre la variabile. Se infatti si usa il formattatore %8.3f il numero frazionario verr`a stampato in un campo di almeno otto caratteri e con almeno tre cifre decimali.

14


Sequenza di escape \a \b \f \n \r \t \v

2.7

Descrizione Alarm, fa emettere un suono all’hardware Backspace, colloca il cursore indietro di una posizione Nuova pagina New line, colloca il cursore su una nuova linea Return, ritorno del carrello Tab, tab orizzontale Vertical tab, tab verticale

Operatori

Gli operatori stabiliscono le operazioni da fare con le variabili e in questo paragrafo ne analizzeremo le modalit`a d’uso.

2.7.1

Operatori aritmetici

I simboli per gli operatori aritmetici sono: +, -, * e /. Il simbolo % rappresenta invece l’operatore modulo, restituisce il resto della divisione x/y. L’ordine di precedenza degli operatori `e il seguente: %, /, * hanno la stessa precedenza e precedono gli operatori + e - (che pure hanno la stessa precedenza). Meglio, dunque, fare uso di parentesi tonde per stabilire con esattezza l’ordine con cui verranno eseguite le operazioni aritmetche. L’operatore % non si applica ai float e double. Un esempio: /* Operatori aritmetici */ #include <stdio.h> int main(void) { int x=10; int y=2; float z=3.1; printf ("%d + %d = %d\n",x,y,x+y); printf ("%d - %d = %d\n",x,y,x-y); printf ("%d * %d = %d\n",x,y,x*y); printf ("%d / %d = %d\n",x,y,x/y); printf ("%d %% %d = %d\n",x,y,x%y); printf ("%d + %.1f = %.1f\n",x,z,x+z); printf ("%d + %.1f = %d\n",x,z,x+(int)z); return 0; } Questo l’output generato: 10 10 10 10 10 10 10

+ * / % + +

2 = 2 = 2 = 2 = 2 = 3.1 3.1

12 8 20 5 0 = 13.1 = 13

15


Notate qualcosa di strano? Anzich`e dichiarare una variabile per contenere il risultato dell’operazione si `e passato alla funzione print l’intera espressione (aritmetica), risparmiando una variabile (ed altre istruzioni). Nelle ultime due istruzioni si tenta la somma tra un int ed un float. La prima di queste operazioni (%d + %.1f = %.1f) vuole scrivere il risultato sotto forma di float (con una cifra decimale). I parametri passati alla funzione printf sono, rispettivamente: x, z ed x+z. Il valore stampato `e 13.1! La variabile int viene convertita in float e successivamente sommata all’altra variabile float (il risultato prevede inoltre la stampa di una cifra decimale). Nell’ultima istruzione (%d + %.1f = %d) si tenta sempre la somma fra x e z, il risultato da stampare deve essere tuttavia un int. I parametri passati alla funzione printf sono, nell’ordine: x, z e x+(int)z. Il valore stampato `e 13! La variabile float viene convertita in int e sommata all’altra variabile int. Per indicare, dunque, il tipo di dati per la conversione si fa precedere alla variabile il nuovo tipo, racchiuso fra parentesi tonde (operazione di casting). Quando una conversione di una variabile non prevede la perdita dell’informazione (come nel primo esempio, passando da int a float), l’operazione di casting non `e necessaria e pu` o essere tralasciata. Se la conversione prevede, invece, la perdita dell’informazione (nell’esempio, da float a int, viene eliminata la parte decimale) bisogna segnalare al compilatore la nostra intenzione con un casting (un messaggio di warning, altrimenti, segnaler`a il fatto).

2.7.2

Operatori relazionali

Gli operatori relazionali si applicano a variabili dello stesso tipo. Gli operatori relazionali previsti dal linguaggio sono: >, >=, < e <=. Le operazioni di confronto fra variabili sono largamente utilizzate per controllare il flusso di esecuzione del programma. Osserveremo ancora meglio gli operatori relazionali quando tratteremo, nel prossimo capitolo, le strutture di controllo. Per adesso `e sufficiente sapere quanto gi` a detto: gli operatori relazionali ritornano il risultato del confronto fra due variabili. Tutti gli operatori relazionali elencati sopra hanno la stessa precedenza. Essi, tuttavia, vengono eseguiti solo dopo gli operatori aritmetici. Pertanto, l’espressione x<y-1 equivale all’espressione x<(y-1). Appartengono agli operatori relazionali anche gli operatori di uguaglianza == e != che significano, rispettivamente: uguale a (ad esempio: x==y, x uguale a y) e diverso da (ad esempio: x!=y, x diverso da y). Gli operatori relazionali precedono gli operatori di uguaglianza nell’esecuzione.

2.7.3

Operatori logici

Gli operatori logici riconosciuti dal linguaggio sono: &&, || e !. L’operatore logico && corrisponde all’operazione logica and. L’operatore logico ||, invece, corrisponde all’operazione logica or. L’operatore logico !, invece, corrisponde all’operazione logica not. La valutazione delle espressioni avviene da sinistra verso destra. L’operatore && precede l’operatore || in fase di esecuzione, entrambi hanno, tuttavia, un precedenza inferiore nei confronti degli operatori relazionali e di uguaglianza.

16


2.7.4

Operatori bit a bit

Gli operatori bit a bit si applicano esclusivamente alle variabili di tipo int, sia esse long, short che char (il tipo dati char `e un particolare tipo int). Ecco un elenco degli operatori bit a bit offerti dal linguaggio: • &: per l’operazione logica and; • |: per l’operazione logica or inclusivo; • ^: per l’operazione logica or esclusivo; • <<: per lo shift a sinistra di N bit, i posti lasciati liberi verranno riempiti con tanti 0 per un numero di volte pari ad N; • >>: per lo shift a destra di N bit, i bit liberati vengono riempiti con tanti 0 per un numero di volte pari ad N (se l’hardware compie uno shift logico, altri tipi di hardware possono anche riempire i posti liberati dai bit scartati con tanti 1, shift aritmetico); • ~: per l’operazione logica di complemento ad 1; Ecco un esempio: /* Operatori bit a bit */ #include <stdio.h> #define MASK 3 int main(void) { int x=7; printf("%d AND %d : %d\n",x,MASK,x&MASK); printf("%d OR %d : %d\n",x,MASK,x|MASK); printf("%d XOR %d: %d\n",x,MASK,x^MASK); printf("%d << 2: %d\n",x,x<<2); printf("%d >> 2: %d\n",x,x>>2); printf("%d AND ~MASK: %d\n",x,MASK,x&(~MASK)); return 0; } Questo l’output generato: 7 7 7 7 7 7

AND 3 : 3 OR 3 : 7 XOR 3: 4 << 2: 28 >> 2: 1 AND ~MASK: 3

Ricordo che la variabile x, inizializzata a 7, ha valore 111 in binario mentre la costante MASK, inizializzata a 3, ha valore 011 in binario. Verificate l’esattezza delle operazioni logiche eseguite.

17


Capitolo 3

Strutture di controllo L’ordine con cui vengono eseguite le istruzioni di un programma determina il flusso di esecuzione dello stesso. Se vogliamo controllare il flusso di esecuzione del programma dobbiamo allora imparare ad usare le strutture di controllo trattate in questo capitolo.

3.1

Blocco di istruzioni

I blocchi di istruzioni vanno sempre delimitati dai simboli { e }. All’interno di un blocco di istruzioni possono essere dichiarate e inizializzate tutte le variabili utili all’esecuzione delle successive istruzioni. La visibilit`a delle suddette variabili, tuttavia, `e locale al blocco di istruzioni (tratteremo in seguito le regole per la visibilit` a delle variabili). Al di fuori del blocco di istruzioni possono cio`e esistere variabili avente lo stesso nome delle variabili gi`a usate nel blocco di istruzioni. Si tratta, ovviamente, di altre variabili. Pertanto, `e preferibile assegnare nomi diversi alle variabili, ove possibile. /* Blocco di istruzioni */ { DICHIARAZIONI_VARIABILI ... ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; } Useremo i blocchi di istruzioni per la scrittura di funzioni di programma (nel capitolo 5) e istruzioni legate al particolare flusso di esecuzione del programma (nei successivi paragrafi).

3.2

if()-else

Per stabilire l’esecuzione di un blocco di istruzioni piuttosto che un altro si esegue solitamente un test su una variabile (a volte detta anche variabile di

18


controllo). Molto spesso il test avviene fra un valore assegnato ad una variabile e quello invece ritornato da una funzione. La prima struttura di controllo elementare `e l’istruzione if()-else: /* if()-else */ if (espressione) { /* BLOCCO A */ ISTRUZIONE_1a; ISTRUZIONE_2a; ... ISTRUZIONE_Na; } else { /* BLOCCO B */ ISTRUZIONE_1b; ISTRUZIONE_2b; ... ISTRUZIONE_Nb; } Se l’espressione valutata in if() `e verificata viene, allora, eseguito il blocco A di istruzioni. Se l’espressione valutata in if() non `e invece verificata viene allora eseguito il blocco B di istruzioni, se tale blocco di istruzioni esiste. Infatti, il blocco di istruzioni relativo all’else pu`o essere opzionale. Se uno dei due blocchi di istruzioni si riduce ad una singola istruzione `e possibile omettere i simboli { e }. Pertanto `e anche possibile una struttura fatta in questo modo: /* if()-else */ if (espressione) ISTRUZIONE_1a; else ISTRUZIONE_1b; Siccome il linguaggio permette di nidificare le strutture di controllo una dentro l’altra bisogna prestare particolare attenzione quando decidiamo di omettere i simboli { e }. Ogni istruzione else viene associata all’istruzione if pi` u interna, se questa `e priva di un’istruzione else, altrimenti il compilatore segnaler`a l’errore. /* if()-else */ if (espressione1) ISTRUZIONE_1a; if (espressione2) ISTRUZIONE_1a1; else ISTRUZIONE_1a2; Nell’esempio l’istruzione else `e allora associata all’istruzione if(espresione2). Se invece si vuole associare l’istruzione else al primo if(espressione1) bisogna usare necessariamente i simboli { e }:

19


/* if()-else */ if (espressione1) { ISTRUZIONE_1a; if (espressione2) ISTRUZIONE_1a1; } else ISTRUZIONE_1a2;

3.3

else if()

Talvolta la mancata esecuzione di un blocco di istruzioni, suggerite da un if, non implica l’esecuzione del blocco di istruzioni suggerite in else. In altre parole, si possono incontrare casi in cui anzich`e effettuare una scelta fra due blocchi di istruzioni sia invece necessaria una scelta fra pi` u di due blocchi di istruzioni! L’istruzione else if() estende, allora, la struttura di controllo if()-else vista prima e da luogo ad una nuova struttura di controllo: /* else if() */ if (espressione1) ISTRUZIONE_1; else if (espressione2) ISTRUZIONE_2; else if (espressione3) ISTRUZIONE_3; ... else ISTRUZIONE_N; L’ultima istruzione else, dunque, viene eseguita solo e soltante se tutte le precedenti espressioni nelle rispettive istruzioni if() danno esito negativo. Tale istruzione pu` o anche essere omessa, anche se molto spesso essa viene usata per effettuare ulteriori controlli e verifiche.

3.4

switch()

Se l’espressione che stabilisce l’esecuzione di un blocco di istruzioni `e invece unica e se abbiamo gi` a individuato all’interno del programma (o della funzione) il numero di casi che occorre trattare con particolari istruzioni, possiamo allora utilizzare l’istruzione switch. Si tratta di una particolare struttura di controllo che, verificata l’espressione, devia il flusso di esecuzione in uno dei casi previsti (dal programmatore). Ogni caso (case) `e etichettato con una etichetta (label ), se l’espressione usata come test ritorna quel valore verr`a allora eseguito il blocco di istruzioni che ha inizio a partire dall’etichetta che fa riferimento a quel caso. Le istruzioni, poi, continuano fino all’istruzione break, se presente. Se questa infatti non `e presente le istruzioni vengono eseguite fino alla fine dell’intera struttura di controllo. Pertanto, l’etichetta stabilisce il punto di ingresso nella struttura di controllo mentre l’istruzione break stabilisce invece il punto di uscita!

20


Se nessuna delle etichette assegnate ai casi previsti corrisponde al valore tornato dallâ&#x20AC;&#x2122;espressione si pu` o allora prevedere un ulteriore blocco di istruzioni di default ed identificato dallâ&#x20AC;&#x2122;etichetta default. Il seguente esempio: /* switch A */ #include <stdio.h> int main(void) { int x=2; printf("Ho eseguito le istruzioni:"); switch (x) { case 1: printf(" 1 "); break; case 2: printf(" 2 "); break; case 3: printf(" 3 "); break; default: printf(" default "); break; } printf("\n"); return 0; } provoca la stampa della stringa: Ho eseguito le istruzioni: modifichiamo il programma in questo modo: /* switch B */ #include <stdio.h> int main(void) { int x=2; printf("Ho eseguito le istruzioni:"); switch (x) { case 1: printf(" 1 "); case 2: printf(" 2 "); case 3: printf(" 3 "); default: printf(" default "); break; } printf("\n"); return 0; } 21

2. Se, invece,


la stringa stampata sar` a: Ho eseguito le istruzioni: 2 3 default (essendo x pari a 2). L’ultima istruzione break `e indispensabile se si vuole realmente uscire dalla struttura di controllo.

3.5

for()

L’istruzione for permette la descrizione di cicli di istruzioni. Questa la sintassi da usare: for (espressione1;espressione2;espressione3) { ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; } oppure: for (espressione1;espressione2;espressione3) ISTRUZIONE_1; se l’istruzione da iterare `e unica (si omettono cio`e le parentesi graffe). All’interno dell’istruzione for prendono posto tre espressioni, ognuna separata dalla successiva dal carattere ; (punto e virgola). Solitamente espressione1 ed espressione3 sono assegnamenti di valori a variabili di programma (oppure, in alcuni casi, chiamate a funzioni). Dovendo iterare un blocco di istruzioni, l’istruzione permette attraverso espressione1 di assegnare il valore iniziale per una variabile di conteggio. espressione3, invece, stabilisce come incrementare la variabile di conteggio al termine di una singola iterazione. Infine, espressione2 descrive il test da effettuare prima di ogni iterazione e stabilisce quando terminare l’intero ciclo di istruzioni. E’ anche possibile tralasciare una o tutte delle tre espressioni. Se omettiamo tutte e tre le espressioni (i caratteri ; vanno comunque segnati) il ciclo di istruzioni verr` a allora eseguito all’infinito! La stessa cosa avviene anche in assenza di espressione2, in tal caso il compilatore assumer`a sempre vero l’esito dovuto dal test. In questo esempio: /* for */ #include <stdio.h> #define MESSAGGIO "Hello world!\n" int main(void) { int i; for(i=0;MESSAGGIO[i]!=’\0’;i=i+1) printf("%c",MESSAGGIO[i]); } la costante MESSAGGIO, una stringa di caratteri, viene stampata un carattere per volta. A tale proposito ci serviamo di un ciclo for in cui:

22


• espressione1: prevede l’assegnamento della variabile i a 0; • espressione2: prevede l’esecuzione del ciclo di istruzioni finch`e il carattere letto dalla stringa `e diverso dal carattere \0; • espressione3: prevede, per la successiva iterazione, l’incremento della variabile i di un valore; Per comprendere meglio il significato di queste espressioni vi invito a leggere le seguenti motivazioni. Ogni stringa di carattari viene memorizzata all’interno di un array di caratteri. L’accesso al singolo elemento dell’array (nel nostro caso un char) avviene passando al nome della variabile un indice di posizione, racchiuso fra i simboli [ e ]. Pertanto, l’espression MESSAGGIO[i] accede all’elemento i-esimo dell’array. Poich`e la posizione del primo elemento all’interno di un array ha valore pari a 0, espressione1 assegna ad i il valore iniziale (il primo carattere da leggere). espressione3, invece, dovendo incrementare la variabile per la successiva iterazione incrementa i per il prossimo carattere e quindi i+1. Quando deve terminare questo ciclo? espressione2 indica quando terminare il ciclo, ovvero quando il carattere i-esimo letto `e pari a \0. Perch`e? Per far intendere al compilatore la fine di una stringa di caratteri si utilizza il token \0.

3.6

while()

L’istruzione while prevede la seguente sintassi o struttura: while (espressione) { ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_3; } espressione viene valutata prima di ogni iterazione. Se espressione ritorna un valore diverso da zero, il blocco di istruzioni viene eseguito ed espressione viene valutata nuovamente. L’iterazione continua finch`e espressione non risulta falsa. Per convenzione ogni valore diverso da zero fa convalidare l’espressione, si dir` a allora che l’espressione `e vera, cio`e verificata. Al contrario, invece, ogni valore pari a zero non convalida l’espressione che, quindi, si dir`a falsa. E’ possibile descrivere un ciclo di istruzioni sia attraverso l’istruzione for che while, ad esempio: espressione_1; while (espressione_2) { ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; espressione_3 } equivale a scrivere: 23


for (espressione1;espressione2;espressione3) { ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; } In questo esempio, invece, viene proposto un programma capace di convertire numeri decimali in numeri in binari (a 4 bit). Il programma fa uso di istruzioni/cicli while e for: /* convertitore decimale/binario */ #include <stdio.h> #define N_BIT 4 #define BASE 2 int main(void) { int dec_x=14; int bin_x[N_BIT]; int i=0; printf("%d(%d): ",dec_x,BASE); for (i=0;i<N_BIT;i=i+1) bin_x[i]=0; i=0; while (dec_x!=0) { bin_x[i]=dec_x%BASE; dec_x=dec_x/BASE; i=i+1; } for (i=N_BIT-1;i>=0;i=i-1) printf("%d",bin_x[i]); } La conversione di dec x in bin x (un array di int con soli valori 1 e 0) procede fintanto che dec x `e diverso da zero. Il primo ciclo for azzera tutti gli elementi dell’array (l’operazione `e necessaria poich`e dopo occorre poi stampare l’intero array che altrimenti conterrebbe valori sporchi, ovvero non inizializzati!). Il secondo ciclo for stampa l’array contenenti i resti delle divisioni che vanno, per` o, presi a partire dall’ultimo. Il programma, allora, non effettua l’inversione degli elementi nell’array ma si limita, invece, a stampare lo stesso vettore proprio a partire dall’ultimo int!

3.7

do-while()

Nei cicli for e while l’espressione che stabilisce se continuare o meno l’iterazione viene controllata prima che lo stesso abbia inizio. Con la struttura do-while(), invece, possiamo descrivere un ciclo di istruzioni in cui la condizione di uscita viene verificata alla fine di ogni iterazione. Pertanto, mentre per le istruzioni for e while il blocco di istruzioni pu`o anche non essere eseguito, con l’istruzione do-while() il blocco di istruzioni viene eseguito almeno una volta! La sintassi o la struttura prevista per questa istruzione `e la seguente: 24


do { ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; } while(espressione); Se espressione risulta vera, il blocco di istruzioni viene eseguito nuovamente. /* do-while() */ #include <stdio.h> #include <time.h> #define MAX 6 int main(void) { int dado; int count=0; srand (time(NULL)); do { dado=(rand()%MAX)+1; count=count+1; } while (dado!=MAX); printf("Dopo %d lanci ho realizzato %d!\n",count,dado); } Nell’esempio mostrato sopra viene simulato il lancio di un dado. Per generare numeri casuali (da 0 a 32767) si ricorre alla funzione rand(), definita nella libreria stdlib (e quindi inclusa nel programma attraverso l’header stdlib.h). Per fare in modo che il numero generato e assegnato alla variabile dado sia compreso fra 6 valori (da 0 a 5, per questo si aggiunge 1 alla variabile) si ricorre, allora, all’operatore modulo. Ancora prima di iniziare a generare numeri casuali viene chiamata la funzione srand() che inizializza con un seme i successivi numeri generati da rand(), che altrimenti si ripeterebbero. Come seme (un int) viene usata l’ora locale, ottenuta dalla funzione time() (dichiarata nella libreria time ed importata nel programma attraverso l’header time.h). Il programma prevede il lancio di un dado, si esce dal ciclo non appena il dado lanciato ritorna il valore 6. Attraverso l’uso dell’istruzione do-while() siamo in grado di modellare la situazione descritta sopra!

3.8

break e continue

Abbiamo gi` a incontrato l’istruzione break in occasione dell’istruzione switch. Aggiungiamo, ora, che il significato di questa istruzione `e ancora valido se applicato ad altre istruzioni. In ogni caso, con break possiamo interrompere l’esecuzione di un ciclo di istruzioni in corso. L’esecuzione del codice, allora, riprender` a dall’istruzione immediatamente successiva al blocco di istruzioni fermato (che potrebbe anche essere l’esecuzione di un ulteriore ciclo di istruzione se nel programma abbiamo pi` u cicli uno dentro l’altro).

25


/* break */ #include <stdio.h> #include <string.h> #define MESSAGE "Hello world!\n" int main(void) { int i; for (i=0;i<strlen(MESSAGE);i++) { if (MESSAGE[i]==’ ’) break; else printf("%c",MESSAGE[i]); } printf("!\n"); } Nel programma appena mostrato, la stampa del noto messaggio viene interrotta non appena viene intercettato il primo carattere di spazio. L’interruzione avviene in questo caso con l’istruzione break, esistono ovviamente anche altri modi per interrompere un ciclo di istruzioni. Un semplice esercizio, allora, pu`o essere la scrittura dello stesso programma senza l’uso di break. L’istruzione continue `e invece opposta o complementare a break. Con continue possiamo far continuare un ciclo di istruzioni senza che l’attuale iterazione si soffermi sulle successive istruzioni che prevede il ciclo stesso. In altre parole, l’istruzione continue permette di continuare l’iterazione del ciclo di istruzioni passando immediatamente al successivo valore utile per l’iterazione. Un esempio chiarir` a sicuramente le idee: /* continue */ #include <stdio.h> #include <string.h> #define MESSAGE "Hello world\n" int main(void) { int i; printf("%s",MESSAGE); printf("01234456789\n"); for (i=0;i<strlen(MESSAGE);i++) { if (i%2==0) continue; else printf("%c",MESSAGE[i]); } } Nel ciclo di istruzione vengono stampati i caratteri di posto pari (quelli il cui indice di posizione diviso per due da resto uguale a zero), questo provocher`a la stampa della stringa el ol! Le istruzioni break e continue possono essere sostituite con altre strutture di controllo gi`a viste, come ad esempio if e switch. Il loro impiego nei programmi non `e eccessivo, fra le due istruzioni, break, `e di sicuro pi` u usata (essendo comunque richieste dal costrutto switch). Ricordatene, allora, l’esistenza!

26


3.9

goto e label

Le ultime due istruzioni per il controllo del flusso di esecuzione di un programma hanno uno scarso utilizzo. Il linguaggio le prevede e per completezza, quindi, osserveremo qualche esempio. Si tratta delle istruzioni di salto incondizionato: goto e label. Ricordano molto i salti incondizionati che si trovano nei sorgenti di molti programmi scritti in assembly (il linguaggio macchina parlato dal processore). L’istruzione goto indica un salto ad una istruzione. Quale? Quella immediatamente successiva all’etichetta label. Per il compilatore una label `e la riga del codice sorgente del programma caratterizzato da un nome seguito dal carattere :. La sottile ma importante differenza fra break e goto `e che mentre break termina un ciclo di istruzioni facendo riprendere l’esecuzione dal blocco di istruzioni pi` u esterno (che potrebbe ancora essere un ciclo di istruzioni), con l’istruzione goto possiamo terminare definitivamente pi` u cicli nidificati di istruzioni. Come? Sar` a sufficiente collocare all’esterno dei cicli nidificati l’etichetta e far puntare l’istruzione goto a questa etichetta! /* goto e label */ #include <stdio.h> #define ROWS 3 #define COLUMNS 3 int main(void) { int i,j,k,count; int mat[ROWS][COLUMNS]={0,1,0, 1,0,1, 0,0,0}; for (i=0;i<ROWS;i++) { count=0; for (j=0;j<COLUMNS;j++) { if (mat[i][j]==0) count++; if (count==(COLUMNS)) goto error; } } if (i==ROWS) return 1; error: printf("Errore!\n"); } Il programma nell’esempio interrompe l’esecuzione (la lettura di una matrice) se su una riga della stessa vengono rilevati tanti valori nulli per quanti sono i valori previsti per quella riga.

27


Capitolo 4

Il problema dell’input Negli esempi finora visti l’input dato ai programmi era gi`a contenuto in apposite variabili. Il programma, allora, esegue le istruzioni mostrandoci alla fine della computazione il risultato. Questo risolve in parte i nostri problemi, ad ogni esecuzione dobbiamo infatti modificare i valori delle variabili con nuovi valori (e ovviamente ricompilare il programma!). Per dare all’utente la possibilit`a di interagire con il programma, ad esempio digitando l’input e/o effettuando scelte impareremo a gestire il problema dell’input.

4.1

Input da tastiera: scanf()

La libreria stdio (standard input/output) implementa un utile funzione per l’input da tastiera, si tratta della funzione scanf(). Su ogni macchina ogni dispositivo pu` o assolvere a funzioni di input e/o di output. Alcuni di questi dispositivi sono usati di default dal sistema operativo. La tastiera, ad esempio, `e un dispositivo di input, detto anche stdinput (standard input). Il monitor, invece, `e un dispositivo di output, detto anche stdoutput (standard output). La funzione scanf(), dunque, legge i caratteri dallo stdinput e li inserisce a partire dall’indirizzo di memoria della variabile specificata. Quando usiamo scanf() bisogna sempre indicare l’input atteso, a tale proposito andranno usati i caratteri formattatori gi` a visti per printf(). /* scanf */ #include <stdio.h> int main(void) { unsigned int i; do { printf("Inserisci un valore da 1 a 10: "); scanf("%d",&i); } while (i<1 || i>10); } L’input verr` a ripetuto finch`e il valore inserito da tastiera non `e compreso fra 1 e 10! Notare anche l’uso dell’istruzione do-while(). Con scanf() i nostri programmi iniziano a dialogare con l’utente: 28


/* pari o dispari? */ #include <stdio.h> int main(void) { unsigned int i; do { printf("Inserisci un valore: "); scanf("%d",&i); } while (i<0); if(i%2==0) printf ("%d ` e pari!\n",i); else printf("%d ` e dispari!\n",i); return 1; } Anche per l’input di stringhe: /* Come ti chiami? */ #include <stdio.h> #define MAX_LENGTH 50 int main(void) { char name[MAX_LENGTH]; int length; printf("Come ti chiami?\n"); scanf("%s",name); for(length=0;name[length]!=’\0’;length++); printf("Ciao %s! Il tuo nome ` e lungo %d caratteri!\n",name,length); return 1; } Avete provato ad eseguire quest’ultimo programma? Cosa succede se alla domanda rispondete inserendo nome e cognome? La funzione scanf() effettua la lettura dell’input fino al primo carattere di spazio. Qualsiasi altra cosa dopo il primo carattere di spazio viene dunque ignorata! Pertanto, se vogliamo realizzare l’input di stringhe `e preferibile affidare il compito ad alcune funzioni della libreria string (header string.h).

4.2

Libreria string.h

Ogni sequenza di caratteri, raccolta in un array di char, costituisce una stringa. La libreria string.h implemente alcune utili funzioni per agevolare il lavoro con le stringhe.

4.2.1

strlen

La funzione strlen() ritorna la lunghezza della stringa data in input alla funzione stessa.

29


/* strlen */ #include <stdio.h> #define MAX_LENGTH 50 int main(void) { char name[MAX_LENGTH]; int length; printf("Come ti chiami?\n"); scanf("%s",name); length=strlen(name); printf("Ciao %s! Il tuo nome ` e lungo %d caratteri!\n",name,length); return 1; } Ovviamente nellâ&#x20AC;&#x2122;esempio sussiste sempre il problema evidenziato prima per scanf(). Risolveremo questo inconveniente alla fine di questo capitolo.

4.2.2

strcmp e strncmp

Il confronto fra due stringhe pu`o avvenire mediante la funzione strcmp(). Se A e B sono due stringhe, la funzione strcmp(A,B) ritorna: il valore 0 se A==B, un valore <0 se A<B oppure un valore >0 se A>B. /* strcmp */ #include <stdio.h> #define MAX_LENGTH 50 int main(void) { char A[MAX_LENGTH]; char B[MAX_LENGTH]; printf("A: "); scanf("%s",A); printf("B: "); scanf("%s",B); if(strcmp(A,B)==0) printf("A e B sono uguali!\n"); else printf("A e B sono diverse!\n"); return 1; } La funzione strncmp(A,B,n) confronta, invece, al massimo n caratteri di A con B. I valori tornati hanno lo stesso significato dei valori restituiti dalla funzione strcmp(). /* strncmp */ #include <stdio.h> #define MAX_LENGTH 50 int main(void) {

30


char A[MAX_LENGTH]; char B[MAX_LENGTH]; printf("A: "); scanf("%s",A); printf("B: "); scanf("%s",B); if(strncmp(A,B,3)==0) printf("A e B sono uguali!\n"); else printf("A e B sono diverse!\n"); return 1; } Provate, ad esempio, il programma con le stringhe Luca e Lucifero. Effettuando un confronto dei primi 3 caratteri di A (Luca) con la stringa B (Lucifero), il programma affermer` a che A e B sono uguali!

4.2.3

strcat e strncat

Siano A e B due stringhe, la funzione strcat(A,B) effettuer`a, allora, la concatenazione fra le due stringhe: /* strcat */ #include <stdio.h> #define MAX_LENGTH 50 int main(void) { char A[MAX_LENGTH]; char B[MAX_LENGTH]; printf("A: "); scanf("%s",A); printf("B: "); scanf("%s",B); printf("A+B: %s",strcat(A,B)); return 1; } La funzione ritorna la stringa concatenata (o meglio, un puntatore alla stringa concatenata). La funzione strncat(A,B,n) concatena al massimo n caratteri di B ad A: Attenzione, quando usate strcat() e strncat() verificate sempre la dimensione di A (ovvero della stringa di destinazione). Questa, infatti, deve essere necessariamente grande da poter contenere la stringa concatenata!

4.2.4

strcpy e strncpy

Date due stringhe, A e B, la funzione strcpy(A,B) effettua la copia di B in A (compreso il carattere terminatore). Ovviamente A deve essere sufficientemente grande per contenere B. /* strcpy */ #include <stdio.h> #define MAX_LENGTH 50 31


int main(void) { char A[MAX_LENGTH]; char B[MAX_LENGTH]; printf("A: "); scanf("%s",A); printf("B: %s",strcpy(B,A)); return 1; } La funzione restituisce un puntatore alla stringa copiata (nellâ&#x20AC;&#x2122;esempio, passato, poi, alla funzione printf() per la stampa). La funzione strncpy(A,B,n), invece, copia al massimo n caratteri della stringa B in A: /* strncpy */ #include <stdio.h> #define MAX_LENGTH 50 #define N_CHAR 4 int main(void) { char A[MAX_LENGTH]; char B[MAX_LENGTH]; printf("A: "); scanf("%s",A); printf("B: %s",strncpy(B,A,N_CHAR)); return 1; }

4.2.5

strstr

Siano A e B due stringhe. La funzione strstr(A,B) cerca la stringa B allâ&#x20AC;&#x2122;interno della stringa A e restituisce un puntatore alla prima occorrenze di B in A, in caso contrario, invece, ritorna NULL. /* strstr */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 50 int main(void) { char A[MAX_LENGTH]="Acer Aspire One 110L"; char B[MAX_LENGTH]; printf("Testo: %s\n",A); printf("Cerca: "); scanf("%s",&B); printf("Esito: "); if(strstr(A,B)) printf("trovato!\n"); else printf("non trovato!\n"); return 1; 32


} L’utilit` a di questa funzione, dunque, risiede nella possibilit`a di poter cercare particolari sottostringhe all’interno di una stringa.

4.2.6

strchr e strrchar

Sia A una sctringa e C un char. La funzione strchr(A,C) ritorna un puntatore alla prima occorrenza di C in A, altrimenti NULL qualora C non compare in A. /* strchr */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 50 int main(void) { char A[MAX_LENGTH]="Acer Aspire One 110L\n"; char *C; if ((C=strchr(A,’\n’))!=NULL) *C=’\0’; printf("Testo: %s\n",A); return 1; } Il programma mostrato nell’esempio torna il puntatore alla prima occorrenza del carattere new line all’interno della stringa A, tale puntatore viene poi usato per modificare lo stesso carattere new line nel token che indica la fine della stringa (che `e \0). La funzione strrchr(A,C) restituisce, invece, un puntatore all’ultima occorrenza di C in A.

4.2.7

strtok

La funzione strtok(A,C) cerca in A delle sottostringhe delimitate dai caratteri C. La prima chiamata ritorna il puntatore al primo token trovato. Per i successivi token, invece, la chiamata a strtok(NULL,C) (ben diversa dalla prima) attende, per la fine del ciclo, il valore NULL. Passando a strtok() il valore NULL si sta cio`e indicando alla funzione di continuare la ricerca dei token anche nei successivi caratteri rimasti nella stringa. /* strtok */ #include <stdio.h> #include <string.h> int main(void) { char A[] = "pane,latte,uova,caff` e"; char* C; printf("Stringa completa: %s\n",A); printf("Stringa spezzata: "); C=strtok(A,","); while (C!=NULL) 33


{ printf("%s ",C); C=strtok(NULL, ","); } printf("\n"); return 1; } Nellâ&#x20AC;&#x2122;esempio, una stringa contenente una lista di parole separate dal carattere , viene spezzata in tante sottostringhe. Ogni sottostringa delimitata dai caratteri , rappresenta, quindi, una parola della lista!

4.2.8

sprintf

Anche la libreria stdio (header stdio.h) definisce alcune utili funzioni per le stringhe. Alcune di queste verranno trattate nel presente e successivo paragrafo. La funzione sprintf(), come il nome stesso lascia intendere, ha qualcosa in comune con la funzione printf(). Entrambe le funzioni sono orientate alla stampa di stringhe. La funzione sprintf() effettua la stampa della stringa, formattata con i noti caratteri di formattazione, allâ&#x20AC;&#x2122;interno di una stringa (a differenza di printf() che stampa sullo standard output)! /* sprintf */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 50 int main(void) { char A[] = "Errore!\n"; char messaggio[MAX_LENGTH]; sprintf(messaggio,"%s",A); printf("Messaggio: %s",messaggio); return 1; } Attenzione, la variabile messaggio deve essere sufficientemente grande da contenere la stringa da stampare!

4.2.9

snprintf

Con snprintf() possiamo specificare il numero massimo di caratteri da stampare nella stringa! Attenzione, al numero di caratteri da stampare va aggiunto anche il token di terminazione per le stringhe. Se il limite di caratteri da stampare viene superato la funzione ritorna il valore -1. /* snprintf */ #include <stdio.h> #include <string.h>

34


#define MAX_LENGTH 50 int main(void) { char A[] = "Errore!\n"; char messaggio[MAX_LENGTH]; snprintf(messaggio,4,"%s",A); printf("Messaggio: %s",messaggio); return 1; }

4.2.10

sscanf

sscanf() effettua la lettura dell’input da una stringa passata alla funzione (primo parametro). La stringa da leggere pu`o essere descritta mediante i caratteri per formattare il testo (quelli gi`a visti per printf(), secondo parametro). I valori letti, infine, vengono assegnati alle variabili elencate (terzo parametro). /* sscanf */ #include <stdio.h> #include <string.h> int main(void) { char ora[6]; int h,m; int check; do { check=0; printf("Inserire ora: "); scanf("%s",&ora); if (sscanf(ora,"%d:%d",&h,&m)==2) if (h>=0&&h<=23) if (m>=0&&m<=59) check=1; } while (check!=1); return 1; }

4.2.11

gets

Eccoci, finalmente alla funzione gets(). Con questa funzione possiamo aggirare il limite finora imposto dalla funzione scanf(), ricordate? Con scanf() l’input termina non appena viene letto il primo carattere di spazio. La funzione gets() richiede come parametro l’array di caratteri da usare per la registrazione dell’input. Si tratta di una funzione che pu`o rivelarsi particolarmente pericolosa! Cos`ı come molte altre non effettua alcun controllo sul valore in input e ci`o pu`o portare facilemente ad una situazione di buffer overflow, un errore di programmazione assai frequente. Ogni volta che tentiamo di memorizzare una stringa di caratteri in un array che non dispone delle celle necessarie viene lanciato un errore di buffer overflow, il

35


programma non ha modo di proseguire e viene, quindi, terminato (segmentation fault)! Un esempio: saranno sufficienti 5 caratteri? /* gets */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 5 int main(void) { char input[MAX_LENGTH]; printf("Digita una stringa: "); gets(input); printf("Input: %s\n",input); return 1; } Come posso, allora, lavorare con le stringhe in tutta sicurezza? Vanno evitate le istruzioni che non effettuano un controllo sui caratteri stampati: scanf() e gets() ad esempio, ma anche strcpy() e strcat(). Meglio ricorrere a strncpy(), oppure strncat(). Per l’input di stringhe, infine, `e preferibile usare una nuova funzione, la funzione fgets(). fgets() `e usata per leggere una linea o riga di caratteri da uno stream di input. Useremo fgets() per leggere dallo standard input (uno stream o flusso di input), la tastiera! La riga letta viene memorizzata in un array di caratteri. Perch`e con fgets() possiamo evitare gli errori di buffer overflow? Perch`e con fgets() vengono letti al massimo n-1 caratteri, la stessa lettura, poi, continua finch`e non viene incontrato il carattere new line (oppure il simbolo EOF, che indica la fine del file, se fgets() viene usata per leggere da file! In caso di successo la funzione ritorna un puntatore alla stringa letta, altrimenti il valore NULL. Attenzione, fgets() memorizza nell’array anche il carettere new line e dopo quest’ultimo, quindi, il token che chiude la stringa. Nell’esempio, oltre a vedere come usare la funzione fgets(), vedremo come correggere la stringa letta: /* fgets */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 25 int main(void) { char name[MAX_LENGTH]; char surname[MAX_LENGTH]; char temp[MAX_LENGTH+MAX_LENGTH]; do { printf("Nome e Cognome: "); fgets(temp,sizeof(temp),stdin); temp[strlen(temp)-1]=0; } 36


while ((sscanf(temp,"%s %s",&name,&surname))!=2); printf("Nome:\t\t%s\n",name); printf("Cognome:\t%s\n",surname); return 1; } Il primo argomento passato alla funzione `e la stringa di destinazione, il secondo argomento specifica il massimo numero di caratteri da leggere (se superato, l’input viene troncato, sempre meglio di un buffer overflow), il terzo argomento indica il descrittore da cui leggere. Non avendo ancora trattato le funzioni (che vedremo nel successivo capitolo), pur avendole usate finora, `e necessario chiarire alcune cose. Una funzione svolge un compito, esegue cio`e un set di istruzioni. Il lavoro finito produce un risultato, restituito da un valore di ritorno. Per svolgere i proprio calcoli una funzione pu`o aver bisogno di un certo numero di variabili. Il passaggio delle variabili ad una funzione avviene elencando le variabili (argomenti) e separando ognuna di essa con una virgola. La funzione sizeof() usata nell’esempio legge la dimensione dell’array. Nei sistemi Linux-like ogni cosa `e modellata con un file. Anche lo standard input e lo standard output sono file. La lettura dallo standard input equivale alla lettura dalla tastiera. La scrittura sullo standard output, invece, equivale alla scrittura sul monitor. Ad ogni file `e associato un descrittore, il sistema operativo anzich`e fare riferimento al file attraverso il nome usa, allora, un descrittore, un numero. Allo standard input `e associato il descrittore avente numero 0 mentre allo standard output il descrittore avente il numero 1. Esiste poi, di default, un ulteriore stream sempre aperto all’avvio di un sistema Linux-like, si tratta dello standard error a cui `e associato il descrittore avente numero 2. Con i numeri si rischia facilmente di fare confusione e di scambiare, allora, lo standard input con lo standard output. Per questo motivo `e possibile fare riferimento ad uno dei tre stream appena citati chiamandoli per nome: • Per lo standard input: stdinput; • Per lo standard output: stdoutput; • Per lo standard error: stderr; Ritorniamo al programma, l’input dell’utente (una stringa con nome e cognome separati da un carattere di spazio) viene memorizzato, con fgets(), all’interno di una stringa temporanea (variabile temp). All’interno di questa stringa viene, poi, cercato il nome e il cognome digitato dall’utente. Con sscanf() viene verificato il numero di stringhe lette e assegnate (alle variabili name e surname), se l’input non `e corretto l’utente dovr`a ripetere la procedura, possibilmente fornendo due stringhe. Avete notato la cancellazione del carattere new line? Per le altre funzioni della libreria string (ne esistono tante altre), invito il lettore a cercare in rete.

37


Capitolo 5

Funzioni e visibilit` a di variabili Eâ&#x20AC;&#x2122; giunto il momento di scomporre i nostri problemi. La scrittura di un unico programma che risolve il nostro problema `e una tecnica di programmazione che va assolutamente evitata. La scomposizione del programma, invece, in tanti programmi componenti `e la via pi` u naturale per scrivere un buon codice sorgente, facilmente gestibile nel tempo. Una funzione realizza un preciso compito, se disponiamo di un certo numero di funzioni e ognuna di questa risolve ogni singola fase prevista dal programma abbiamo, allora, risolto il problema iniziale!

5.1

Struttura di una funzione

La struttura di una funzione rispecchia esattamente la struttura del programma, ovvero della funzione main(): tipo_ritornato nome_funzione(tipo arg1, tipo arg2, ...) { DICHIARAZIONI_VARIABILI ... ISTRUZIONE_1; ISTRUZIONE_2; ... ISTRUZIONE_N; return VALORE; } Ancora prima di scrivere il codice sorgente di una funzione occorre dichiarare la stessa al compilatore fornendo il cosiddetto prototipo della funzione. Il prototipo della funzione (o anche firma) suggerir`a al compilatore il tipo restituito dalla funzione, il nome della funzione e gli argomenti che occorrono ad essa (i tipi delle variabili che occorrono alla funzione). Tale riga va inserita prima di iniziare la scrittura della funzione main() e va sempre terminata con il carattere ;. In questo modo, nel codice del main(), possiamo richiamare la funzione dichiarata tutte le volte che vogliamo. Lâ&#x20AC;&#x2122;implementazione della funzione, invece, verr`a aggiunta alla fine del file. Pertanto, il prototipo della funzione equivale a scrivere 38


la prima riga della funzione stessa senza aggiungere, quindi, lâ&#x20AC;&#x2122;implementazione (che va aggiunta solo dopo). Gli esempi, in seguito, confermeranno quanto appena detto. /* programma */ #include <stdio.h> #include ... tipo_ritornato nome_funzione(tipo arg1, tipo arg2, ...); int main (void) { DICHIARAZIONI_VARIABILI ... istruzione_1; istruzione_2; ... nome_funzione(...); ... istruzione_N; } tipo_ritornato nome_funzione(tipo arg1, tipo arg2, ...) { DICHIARAZIONI_VARIABILI ... istruzione_1; istruzione_2; ... istruzione_N; return VALORE; } Il programma che fa uso della funzione e che quindi la richiama nel corso della sua esecuzione `e anche detto chiamante. Il main(), dunque, `e una particolare funzione (detta funzione principale). Negli esempi finora visti nessun argomento `e stato passato al main(). Quando una funzione non riceve alcun argomento si pu` o decidere di lasciare vuota la lista degli argomenti oppure marcare ancora di pi` u il fatto con la parola chiave void. Il valore restituito al chiamante `e ritornato attraverso la parola chiave return (a cui segue il nome della variabile il cui valore va riferito al chiamante). Se necessario, il valore da restituire viene convertito nel tipo dichiarato nel prototipo della funzione. Se una funzione non ritorna alcun valore, anzich`e scrivere il tipo restituito (che non `e quindi identificabile) va scritta, allora, la parola chiave void. Una funzione che non ritorna alcun valore al chiamante `e anche detta procedura. Attenzione, se la funzione termina lâ&#x20AC;&#x2122;esecuzione delle istruzioni senza prevedere la restituzione di un valore (con la parola chiave return), il programma chiamante raccoglier` a un valore sporco. Ovvero, privo di significato e quindi inutile. /* funzioni */ #include <stdio.h>

39


int sum(int a, int b); int main(void) { int a,b,total; printf("a: "); scanf("%d",&a); printf("b: "); scanf("%d",&b); total=sum(a,b); printf("%d + %d = %d\n",a,b,total); return 1; } /* restituisce la somma a+b */ int sum(int a, int b) { return a+b; } Nellâ&#x20AC;&#x2122;esempio mostrato sopra, la funzione sum() restituisce la somma dei due argomenti passati (due interi). In questo esempio, invece, si fa uso di procedure per la stampa di messaggi: /* login? */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 50 void show_success(void); void show_error(void); int main(void) { char user_name[MAX_LENGTH]; char password[MAX_LENGTH]; printf("User name: "); fgets(user_name,sizeof(user_name),stdin); user_name[strlen(user_name)-1]=0; printf("Password: "); fgets(password,sizeof(password),stdin); password[strlen(password)-1]=0; if(strcmp(user_name,"root")==0) { if(strcmp(password,"root")==0) show_success(); else show_error(); } else show_error(); return 1; } /* stampa un messaggio di benvenuto */ void show_success(void) { printf("Login effettuato con successo!\n"); 40


} /* stampa un messaggio di errore */ void show_error(void) { printf("Password o nome utente errato!\n"); } Notare l’assenza dell’istruzione return che in questo caso diventa opzionale (poich`e le procedure non ritornano alcun valore).

5.2

atoi e atof

La libreria stdlib offre al programmatore numerose funzioni. Alcune di queste funzioni si occupano della conversione dei numeri a partire da una stringa data (come parametro). La funzione atoi() converte una stringa in un int, la funzione atof(), invece, restituisce un double. Avremo modo di vedere e soprattutto usare (spero in futuro, nei vostri programmi) le funzioni presenti in stdlib in un successivo capitolo (dove verranno mostrate anche altre funzioni di libreria). Un utile esercizio, per prendere confidenza con le funzioni, `e la scrittura delle stesse funzioni descritte sopra. Per atoi() si potrebbe avere un’implementazione di questo tipo: /* atoi */ #include <string.h> #include <stdio.h> #include <ctype.h> #define MAX_LENGTH 15 int atoi(char a[]); int main(void) { char a[MAX_LENGTH]; int i; printf("Numero: "); fgets(a,sizeof(a),stdin); a[strlen(a)-1]=’\0’; i=atoi(a); printf("Numero: %d\n",i); return 1; } int atoi(char a[]) { int i=0,n,sign; while (a[i]==’ ’) i++; if (a[i]==’-’) { sign=-1; i++; } else sign=1; 41


for (n=0;isdigit(a[i]);i++) n=(10*n)+(a[i]-’0’); return sign*n; } L’input della stringa avviene con fgets(). La funzione atoi() riceve, dal chiamante, la stringa raccolta e inizia a togliere da questa, se presenti, ogni carattere di spazio. Questo processo va avanti, quindi, fino al primo carattere diverso dal carattere di spazio. Tale carattere, allora, pu`o essere o il segno del numero oppure l’inizio del numero stesso (se `e un numero positivo). Se si tratta del segno negativo l’indice che sta scorrendo la stringa viene fatto puntare al carattere successivo. La conversione pu` o avere inizio. La funzione isdigit(), definita in ctype, ci dir`a se il carattere `e una cifra (digit). La stringa verr` a letta finch`e l’indice punta a un digit. Ai valori letti, dunque ai char della stringa, viene sottratto il codice ascii dello zero. La differenza che si ottiene `e proprio l’intero cercato che andr`a aggiunto man mano ai precedenti valori letti. Ogni valore letto viene moltiplicato n volte per 10, in questo modo viene dato un opportuno peso ai valori fin qui letti. Per atof() si pu`o avere questa implementazione: /* atof */ #include <string.h> #include <stdio.h> #include <ctype.h> #define MAX_LENGTH 15 int main(void) { char a[MAX_LENGTH]; double f,atof(char a[]); printf("Numero: "); fgets(a,sizeof(a),stdin); a[strlen(a)-1]=’\0’; f=atof(a); printf("Numero: %f\n",f); return 1; } double atof(char a[]) { double val, power; int i=0,sign; while (a[i]==’ ’) i++; if (a[i]==’-’) { sign=-1; i++; } else sign=1; for (val=0.0;isdigit(a[i]);i++) val=(10.0*val)+(a[i]-’0’); if (a[i]==’.’) i++; for (power=1.0;isdigit(a[i]);i++) { 42


val=(10.0*val)+(a[i]-’0’); power=power*10.0; } return sign * (val/power); }

5.3

Variabili e funzioni: visibilit` a

Ogni variabile di programma o funzione gode di un certo livello di visibilit`a. Una variabile interna, locale cio`e ad una funzione che la dichiara nel corso della sua implementazione, ha una visibilit`a limitata alla sola funzione che la definisce. Una variabile esterna, invece, `e visibile a tutte le funzioni del programma. Attraverso l’uso di variabili esterne pi` u funzioni, dunque, possono comunicare tra di loro scambiandosi e passandosi i valori per le variabili depositandoli proprio nelle variabili esterne. Tuttavia, l’uso esagerato di variabili esterne in un programma ne pu` o condizionare inevitabilmente l’esecuzione! Se pi` u funzioni di programma, infatti, hanno la possibilit`a di modificare i valori delle variabili esterne, il risultato finale va allora elaborato con maggiore attenzione. Magari sincronizzando l’accesso alle variabili condivise! Ogni variabile interna ha un ciclo di vita che coincide con quello della funzione che dichiara la variabile stessa. Al termine della funzione, ogni variabile (interna) verr` a quindi cancellata. Possiamo dimostrare quanto appena detto sopra con un esempio (ormai noto a molti, credo), lo scambio (swap) di valori fra due variabili: /* swap? */ #include <stdio.h> void swap(int a, int b); int main(void) { int a=0; int b=1; swap(a,b); printf("Dopo aver chiamato la funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); return 0; } void swap(int a, int b) { int temp=a; a=b; b=temp; printf("Nella funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); }

43


Lâ&#x20AC;&#x2122;output del programma mostra un evidente fatto. Nel programma principale (funzione main()) la variabile a `e impostata a 0, mentre b vale 1. Nella funzione swap(), invece, la stampa dellâ&#x20AC;&#x2122;output ci dice che: Nella funzione swap(): a: 1 b: 0 Lo scambio `e avvenuto? Quando il controllo del programma ritorna alla funzione principale, che stampa i valori delle variabili a e b, ci accorgiamo della verit`a: Nella funzione swap(): a: 1 b: 0 Dopo aver chiamato la funzione swap(): a: 0 b: 1 Press ENTER to continue. Tutto sembra immutato! Le variabili a e b della funzione swap() vengono cancellate al termine della stessa funzione e i valori scambiati vengono persi. In altre parole le varibili a e b definite in swap() e in main() non sono collegate ma rappresentano variabili distinte, con visibilit`a limitata al blocco di istruzioni della funzione. Se invece vogliamo far diventare a e b variabili globali, visibili quindi anche da swap() `e sufficiente modificare il programma in questo modo: /* swap? */ #include <stdio.h> /* variabili globali */ int a=0; int b=1; void swap(); int main(void) { swap(a,b); printf("Dopo aver chiamato la funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); return 0; } void swap() { int temp=a; a=b; b=temp; printf("Nella funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); } 44


Dichiarando a e b al di fuori del main() viene garantita la loro visibilit`a anche alla funzione swap() che adesso non ha pi` u bisogno di ricevere parametri per lâ&#x20AC;&#x2122;input poich`e pu` o leggere immediatamente i valori che manipola! Provate a modificare il programma, dichiarando le variabili a e b dopo il main(). La compilazione del programma, in questo caso, verr`a interrotta poich´e a e b non saranno pi` u visibili alla funzione main()! Quando la complessit` a di un programma lo consente, `e possibile non solo scomporre il problema in tanti sottoprogrammi ma ognuno di questi pu`o essere scritto in un file separato dagli altri. Come indicare, allora, ai tanti file del programma la natura di una variabile: interna o esterna? Il linguaggio C mette a disposizione la parola chiave extern per indicare al compilatore se una variabile deve essere considerata interna o esterna al file. /* swap? */ #include <stdio.h> #include "5-12.c" /* variabili globali */ int a=0; int b=1; int main(void) { swap(a,b); printf("Dopo aver chiamato la funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); return 0; } Qui le variabili a e b vengono dichiarate ed hanno una visbilit`a globale, per tutti i file che parteciperanno alla composizione del programma. Per la funzione swap(), usata nel main(), viene inclusa la partecipazione al programma del file 5-12.c (con la parola chiave include) che `e strutturato in questo modo: /* variabile esterne */ extern int a; extern int b; void swap() { int temp=a; a=b; b=temp; printf("Nella funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); } Pertanto, poich`e nel primo file (quello con il metodo main() le variabili a e b sono state dichiarate al di fuori della funzione (principale), esse sono visibili a tutte le funzioni di quel file! Nel nostro caso, la funzione swap(), che pure fa uso delle variabili a e b, non ha bisogno di definire nuove variabili ma pu`o, allora, 45


fare riferimento a quelle stesse variabili gi`a definite nellâ&#x20AC;&#x2122;altro file. Per fare ci`o far` a precedere i tipi e i nomi delle variabili (esterne) dalla parola chiave extern. In definitiva, le variabili (e le funzioni) dichiarate prima di ogni altra funzione, incluso il main(), hanno una visibilit`a globale. Le stesse variabili possono essere poi usate dai altre funzioni di programma, scritte in altri file, se le stesse sono precedute dalla parola chiave extern. Tutte le variabili (e le funzioni), invece, dichiarate allâ&#x20AC;&#x2122;interno di una funzione hanno una visibilit`a locale.

5.3.1

Variabili static e auto

Per completare il discorso sulla visibilit`a delle variabili occorre adesso trattare la possibilit` a di dichiarare variabili statiche e/o automatiche. Le variabili automatiche sono le variabili dichiarate e usate nelle funzioni. Al termine della funzione esse vengono distrutte, come abbiamo gi`a visto per lâ&#x20AC;&#x2122;esempio della funzione swap(). Esse vengono allocate in memoria quando la funzione viene invocata e sono, successivamente, cancellate quando la funzione termina. Le variabili statiche, invece, vengono allocate in memoria quando il programma ha inizio e vengono, invece, cancellate quando il programma termina. Con la parola chiave auto possiamo allora indicare se una variabile deve essere automatica. Con la parola chiave static, infine, indichiamo se la variabile deve essere statica. Occorre subito precisare un importante fatto, tutte le variabili statiche hanno una visibilit` a ridotta. Esse vengono distrutte al termine del programma ma a differenza delle vere variabili globali sono visibili esclusivamente alle funzioni interne al file. Il concetto appena visto per le variabili statiche si applica anche alle funzioni statiche, il seguente file: /* file 5-13.c */ #include <stdio.h> static void funzione1() { printf("funzione1(): riesci a leggermi?\n"); } void funzione2() { printf("funzione2(): chiamo la funzione1()\n"); funzione1(); } definisce due funzioni. Una di queste, come si pu`o ben notare, `e dichiarata come static. Pertanto, solo funzione2() pu`o chiamare funzione1()! In questo file: /* file 5-14.c */ #include <stdio.h> #include "5-13.c" int main(void) { funzione2(); /* --> funzione1(); <-- */ } viene invocata funzione2() (che successivamente chiama funzione1()).

46


5.3.2

Variabili register e volatile

Le variabili di programma possono essere anche allocate nei registri del processore, anzich´e in memoria. Ci` o potrebbe essere necessario se la variabile deve essere letta o scritta con una certa rapidit`a. In questo caso una variabile deve allora essere dichiarata con la parola chiave register. Una variabile volatile, parola chiave volatile, `e una variabile che pu`o essere modificata anche da altri processi (programmi) in esecuzione. Meglio evitare queste dichiarazioni se non sono necessarie.

47


Capitolo 6

Array e puntatori Con l’introduzione dei puntatori possiamo scrivere in maniera assai pi` u chiara i nostri programmi. Alcune istruzioni, poi, risultano possibili solo se scritte attraverso l’uso di puntatori. La loro trattazione si accompagna al concetto di array e in questo capitolo ne capiremo le motivazioni.

6.1

Array

Abbiamo gi` a incontrato l’uso di array nel corso di queste note. Con la seguente definizione: int a[10]; viene definito un vettore (variabile a) di 10 interi. Un array `e una collezione di valori, appartenenti allo stesso tipo e indicizzato per consentire l’accesso, sia in lettura che in scrittura.

Un indice i, un intero, viene poi usato per l’accesso ad ogni singola casella dell’array. Ad esempio, con la notazione a[0] accediamo al primo elemento dell’array. Notate, infatti, che le celle dell’array sono numerate proprio a partire da 0 (fino alla massima dimensione definita durante la dichiarazione della variabile). Nel nostro esempio, allora, l’ultimo valore utile per l’array a `e l’intero a[9]!

6.2

Puntatori

Con i puntatori iniziamo a giocare con gli indirizzi di memoria. Ogni variabile allocata in memoria ha un proprio indirizzo. Pensate alla memoria del vostro calcolatore come al vettore descritto nel paragrafo precedente. Un puntatore `e una particolare variabile che contiene l’indirizzo di un’altra variabile. Siccome un puntatore punta ad uno dei tipi di variabili offerti dal linguaggio, 48


ogni puntatore, quando viene dichiarato, deve necessariamente indicare il tipo puntato! int *p; int a; p=&a; Osservate le istruzioni appena mostrate. Per denotare una variabile puntatore si ricorre al simbolo * (star, asterisco). Nell’esempio, *p `e un puntatore a una variabile di tipo int. Possiamo cio`e far puntare p a qualsiasi int. E infatti, nelle successive istruzioni, viene prima dichiarata la variabile a, quindi si procede all’assegnazione &a. Con l’operatore unario & si accede all’indirizzo della variabile a. Pertanto, essendo p un puntatore a int ed a una variabile di tipo int, l’istruzione p=&a assegna, dunque, l’indirizzo di a alla variabile p! Solitamente si dice che p punta ad a. Attenzione, l’operatore unario & si applica esclusivamente alle variabili definite in memoria (non va allora applicato alle costanti e alle variabili register, non si possono conoscere gli indirizzi dei registri di memoria del processore!). Con un puntatore possiamo sia raccogliere gli indirizzi delle variabili puntate e sia accedere realmente alle stesse variabili. L’accesso alla variabile puntata avviene con l’operatore unario *. Osservate queste istruzioni: int *p; int a=1, b=2, c[10]; p=&a; b=*p; *p=0; p=&c[0]

/* /* /* /*

p punta ad a*/ adesso b vale 1! */ adesso a vale 0! */ adesso p punta a c[0] */

Con l’istruzione p=&a il puntatore p sta puntando alla variabile a (con l’operatore unario & passiamo l’indirizzo di a a p!). Con l’istruzione b=*p, invece, assegnamo alla variabile b il contenuto della variabile puntata da p e quindi a, l’istruzione equivale a scrivere b=a e quindi b adesso vale 1! Con l’istruzione *p=0, il contenuto puntato da p (ancora la variabile a) viene messo a zero: `e come scrivere a=0. Infine, con l’istruzione p=&c[0] il puntatore p punta all’intero c[0].

6.3

Scambio di parametri

Ricordate la funzione swap(), e di come abbiamo aggirato il problema della condivisione per le variabili da scambiare? In quella circostanza, per riuscire nello swap, abbiamo dichiarato le stesse variabili come variabili globali. Con l’uso dei puntatori possiamo adesso perfezionare quella funzione: /* swap */ #include <stdio.h> void swap(int *a,int *b); int main(void) { 49


int a=0; int b=1; printf("Prima della funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); swap(&a,&b); printf("Dopo della funzione swap():\n"); printf(" a: %d\n",a); printf(" b: %d\n",b); return 0; } void swap(int *a,int *b) { int temp; temp=*a; *a=*b; *b=temp; } L’esecuzione del programma conferma l’avvenuto scambio di variabili. Poich`e il linguaggio C effettua il passaggio delle variabili per valore (ne effettua cio`e una copia, locale alla funzione!) l’unico modo per consentire alle stesse funzioni di poter modificare le variabili ´e l’uso dei puntatori ovvero di ricorrere al passaggio per riferimento. Attraverso l’uso dei puntatori non facciamo altro che passare alla funzione l’indirizzo (il riferimento, quindi) delle variabili. Il chiamante, nel nostro caso la funzione main(), deve passare alla funzione swap() gli indirizzi di a e di b, e lo fa usando l’operatore unario &. La funzione swap(), invece, deve dichiarare gli argomenti come puntatori (mediante l’operatore unario *). Da questo deduciamo che lo scambio di argomenti pu`o avvenire per valore (ed `e il metodo usato di default dal linguaggio), oppure per riferimento (usando i puntatori). Una funzione che deve manipolare un vettore potr`a farlo solo se l’array viene passato con un riferimento. Sia p un puntatore a interi ed a un vettore di interi. Con l’istruzione p=&a[0] la variabile p punta all’elemento a[0] del vettore a (il primo). Se b `e un nuovo intero, allora l’istruzione b=*p far`a in modo che b contenga a[0] (p puntava ad a[0], ricordate?). L’istruzione *(p+1), infine, fa puntare p all’elemento successivo e quindi ad a[1]. Anzich`e scrivere p=&a[0] possiamo anche scrivere p=a! Questo perch`e di default il nome di un array identifica il puntatore al primo elemento dell’array! Pertanto, quando passiamo ad una funzione un array come parametro si sta passando alla funzione il puntatore a quell’array! Ecco un esempio che sfrutta l’osservazione appena fatta: /* inverte un array */ #include <stdio.h> #define MAX_LENGTH 10 void swap(int a[]);

50


int main(void) { int i,a[MAX_LENGTH]={0,1,2,3,4,5,6,7,8,9}; printf("a[] prima della funzione swap():\n"); for (i=0;i<MAX_LENGTH;i++) printf("%d, ",a[i]); printf("\b\b.\n"); swap(a); printf("a[] dopo la funzione swap():\n"); for (i=0;i<MAX_LENGTH;i++) printf("%d, ",a[i]); printf("\b\b.\n"); return 0; } void swap(int a[]) { int temp,i=0; while(i<(MAX_LENGTH/2)) { temp=a[i]; a[i]=a[MAX_LENGTH-i-1]; a[MAX_LENGTH-i-1]=temp; i++; } } che genera questo output: a[] prima della funzione swap(): 0, 1, 2, 3, 4, 5, 6, 7, 8, 9. a[] dopo la funzione swap(): 9, 8, 7, 6, 5, 4, 3, 2, 1, 0. Press ENTER to continue. Con quest’altro esempio, invece, `e possibile vedere come far puntare un puntatore al successivo elemento: /* strlen oppure strcnt? */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 10 int strcnt(char *s); int main(void) { char s[MAX_LENGTH]; printf("Input: "); fgets(s,sizeof(s),stdin); s[strlen(s)-1]=’\0’; printf("Length: %d\n",strcnt(s)); return 0; 51


} int strcnt(char *s) { int i; for (i=0;*s!=’\0’;s++) i++; return i; } In quest’ultimo esempio va notata l’istruzione s++ che fa puntare s (un puntatore a char) al successivo elemento dell’array passato come argomento alla funzione strcnt() (che esegue, quindi, lo stesso compito di strlen() definita nella libreria string.h).

6.4

Gestire la memoria con i puntatori

In questo paragrafo vi propongo un interessante programma per gestire con i puntatori una porzione della memoria. Questo programma ci permetter`a di conoscere meglio due utili funzioni per gestire in maniera dinamica la memoria e che presenter` o, quindi, solo alla fine del paragrafo. Preferisco mostrarvi prima come possiamo implementare da soli questa funzionalit`a. Nel programma viene dichiarato un array di caratteri (un carattere occupa un byte), variabile buffer. La costante SIZE definisce la dimensione dell’array. Poich`e due funzioni dovranno poi gestire questo array, ho allora dichiarato lo stesso array con il qualificatore static. Un puntatore viene poi aggiornato dopo ogni operazione di allocazione, in questo modo punter`a sempre alla prima posizione libera nell’array. Anche questo puntatore sar`a comune a entrambe le funzioni di allocazione e deallocazione, la variabile *lastpointer `e dunque anch’essa static. Con la funzione *allocmem() vengono allocati n byte consevutivi in buffer. La funzione ritorna un puntatore alla posizione nell’array, utile poi a memorizzare una stringa. La funzione freemem(), invece, libera n byte in buffer. Poich`e nel main() viene simulata la scrittura in buffer, ho allora pensato a una terza funzione (o procedura, non avendo parametri di input) per stampare a video lo stato della memoria (showstatus()). Ecco il codice dell’allocatore di memoria (semplice): /* allocatore di memoria (per stack) */ #include <stdio.h> #include <string.h> #define SIZE 1024 static char buffer[SIZE]; static char *lastpointer=buffer; char *allocmem(int n); void freemem(char *p); void showstatus();

52


int main(void) { char input[SIZE]; char *freespace; char *old_freespace; int i; showstatus(); printf("Stringa da memorizzare: "); fgets(input,sizeof(input),stdin); input[strlen(input)-1]=0; printf("Spazio necessario: %d byte\n\n",strlen(input)); printf("Richiesta di spazio in memoria: "); if( (freespace=allocmem(strlen(input)))==0) printf("FALLITA\n"); else printf("OK\n"); old_freespace=freespace; for (i=0;i<strlen(input);i++) { freespace=input[i]; freespace++; } showstatus(); printf("Libero lo spazio in memoria.\n"); freemem(old_freespace); showstatus(); return 0; } /* Alloca n byte consecutivi in buffer */ char *allocmem(int n) { if(buffer+SIZE-lastpointer>=n) { lastpointer+=n; return lastpointer-n; } else return 0; } /* Libera gli ultimi n byte allocati in buffer*/ void freemem(char *pointer) { if(pointer>=buffer && pointer<buffer+SIZE) lastpointer=pointer; } /* Mostra lo stato della memoria, ovvero di buffer */ void showstatus() { printf("Stato della memoria:\n"); printf(" - dimensione: %d byte\n",SIZE); printf(" - memoria disponibile: %d byte\n",buffer-lastpointer+SIZE); printf(" - memoria occupata: %d byte\n\n",lastpointer-buffer); } Questo, invece, un esempio dellâ&#x20AC;&#x2122;output generato: Stato della memoria: 53


- dimensione: 1024 byte - memoria disponibile: 1024 byte - memoria occupata: 0 byte Stringa da memorizzare: luca Spazio necessario: 4 byte Richiesta di spazio in memoria: OK Stato della memoria: - dimensione: 1024 byte - memoria disponibile: 1020 byte - memoria occupata: 4 byte Libero lo spazio in memoria. Stato della memoria: - dimensione: 1024 byte - memoria disponibile: 1024 byte - memoria occupata: 0 byte Nel primo commento al programma ho scritto che questo `e un allocatore per una memoria stack. Uno stack `e una struttura dati, nel nostro caso un array, gestito con una politica last input first output (LIFO). L’ultimo valore immesso `e anche il primo ad essere prelevato. In altre parole le chiamate a freemem() devono avvenire in ordine inverso rispetto alle chiamate allocmem(). Nel main(), per permettere le chiamate a freemem() viene salvato nella variabile old freespace il valore del vecchio puntatore. In questo modo si pu`o ripristinare lo stato del puntatore, facendo ripercorrere all’inverso alla stessa variabile i valori che questa ha avuto man mano che veniva chiamata allocmem(). Vi consiglio di giocare con questo programma, magari eseguendo pi` u chiamate per l’allocazione della memoria ed eseguendo, al contrario, le chiamate a freemem(). Pensate a come bisogna fare per tenere traccia, man mano, dei valori assunti dal puntatore all’area di memoria libera. Se p `e un puntatore a un elemento di un array, allora p++ punta all’elemento successivo. Nelle due funzioni avviene un importante confronto fra puntatori. In allocmem(), ad esempio, per stabilire se in buffer ci sono n byte consecutivi viene effettuato il test buffer+SIZE-lastpoint>=n. La variabile buffer, un array di char, `e un puntatore al primo elemento. La differenza fra due puntatori `e un intero e pu` o essere quindi sommato ad altri valori interi. Il test per stabilire se esistono n byte consecutivi liberi `e quindi lecito. La libreria standard del linguaggio implementa due utili funzioni per la gestione dinamica della memoria: malloc() per l’allocazione e free() per liberare la memoria che non serve pi` u al programma. La funzione malloc(size t size) (memory alloc) alloca size byte. Se lo spazio in memoria `e sufficiente a garantire il buon esito dell’allocazione allora la funziona ritorna un puntatore al blocco di memoria allocato. Se l’allocazione non `e invece possibile viene restituito un puntatore nullo. In verit` a la funzione restituisce, in caso di successo, un puntatore a void. Per questo motivo, molto spesso, davanti alla funzione si trova un operatore di casting che riporta il puntatore a uno dei tipi noti al linguaggio. La funzione free(void pointer) rilascia il blocco di memoria puntato da pointer e precedentemente allocato da malloc(). Con queste due funzioni 54


possiamo eliminare dai nostri programmi alcune importanti limitazioni di spazio necessario ad una variabile e per questo motivo fissato finora in fase dichiarativa della stessa variabile. Ecco un esempio: /* array, quanto grande mi occorre? #include <stdio.h>

*/

int main(void) { int *values; int i,n,value; double total=0; printf("Letture effettuate: "); scanf("%d",&n); printf("Allocazione della memoria: "); values=malloc(n*sizeof(int)); if (values==NULL) printf("Errore!\n"); else { printf("OK\n"); for (i=0;i<n;i++) { printf("Valore n.ro %d: ",i+1); scanf("%d",&value); total=total+value; } printf("Valore medio: %.2f\n",total/n); free(values); } return 0; } Senza l’uso dei puntatori e della funzione malloc() avremo sicuramente scritto questo programma fissando la dimensione dell’array, ponendo quindi un vincolo al programma. Non conoscendo a priori la dimensione dell’array necessario a calcolare la media, il programma, allora, fa allocare in memoria lo spazio necessario per memorizzare n interi (spazio ricavato dalla funzione sizeof()) solo dopo aver raccolto questa informazione durante la prima fase di input. In questo modo il programma risulta assai pi` u flessibile.

6.5

Puntatori a funzioni

Le istruzioni, cos`ı come le variabili di un programma, vengono allocate in memoria principale (quando il programma `e in esecuzione). E’ quindi possibile far puntare un puntatore anche a una funzione! Questo esempio mostra come dichiarare e usare un puntatore a una funzione: /* puntatore a funzione */ #include <stdio.h> void message(void) { printf("Message: OK!\n"); }

55


int main(void) { void (*pointer)(void)=message; printf("La funzione message() ` e all’indirizzo: 0x%.8x\n",pointer); (*pointer)(); return 0; } Questo l’output generato: La funzione message() ` e all’indirizzo: 0x08048464 Message: OK! Un puntatore a funzione viene allora dichiarato in questo modo: tipo restituito (*nome funzione)(parametri funzione). Il nome della funzione viene preceduto dal carattere *, tutto viene poi racchiuso fra parentesi. Se la funzione prevede dei parametri `e possibile allora far seguire a quanto detto finora la lista degli argomenti, separati e racchiusi da nuove parentesi. L’uso dei puntatori a funzioni favorisce la scrittura di un codice sorgente generico ma ancor pi` u potente. L’impiego pi` u diffuso dei puntatori a funzioni `e la scrittura dei parser o delle barre dei menu dei programmi (dove ad ogni clic nel menu viene passata una precisa funzione, detta funzione di callback ).

56


Capitolo 7

Ordinamento di array Dopo aver approfondito la relazione fra array e puntatori, e dopo averli impiegati (spero) subito nei primi programmi che solitamente si scrivono, quasi inevitabilmente ci si imbatte in uno dei classici problemi per la gestione dell’informazione: il problema dell’ordinamento di un array. Esistono diversi modi di risolvere il problema, ogni algoritmo garantisce precise prestazioni. Molti di questi algoritmi vengono, ormai, integrati in numerose librerie. Magari proprio quella che utilizzerete in futuro. Tuttavia `e assai utile, almeno all’inizio, fare uno sforzo per cercare ci comprenderne il loro funzionamente (poi in futuro, sono sicuro, si richiamer` a la funzione senza farsi il minimo problema). L’implementazione, allora, non pu` o che accrescere le nostre capacit`a di programmazione.

7.1

Ordinamento

Come dicevo nell’introduzione del capitolo il problema dell’ordinamento `e un problema assai ricorrente nell’informatica e ci`o presuppone, per la sua realizzazione, almeno una relazione d’ordine. Per stabilirla, in questo capitolo, si far`a leva sulla natura dei numeri e quindi sulle relazioni logiche che ci permettono di stabilire se un dato numero `e maggiore, uguale oppure minore di un altro. Gli algoritmi che verranno descritti continuano infatti a valere pur cambiando la relazione d’ordine, ovviamente in tal caso sar`a necessario modificare il codice. Ricordate, la struttura dati che raccoglie l’informazione caratterizza le prestazioni dell’algoritmo. Per confrontare tra loro i tanti algoritmi si `e andata consolidando nel tempo l’uso di una particolare notazione (notazione asintotica) in grado di prevedere l’andamento degli stessi algoritmi (nei vari casi, peggiore, migliore, etc...). Tale notazione esprime la complessit`a dell’algoritmo. Non mi occuper`o di questo formalismo ma intendo lasciarvi qualche punto di riferimento (ognuno far` a il suo percorso di studi, approfondendo se necessario l’argomento): n n·log2 n n2

10 ≈ 33 100

100 ≈ 665 104

1000 ≈ 104 106

106 ≈ 2 · 107 1012

109 ≈ 3 · 107 1018

Nella tabella vengono mostrati il numero di passi per portare a termine un algoritmo quando la complessit` a dell’algoritmo stesso `e paragonabile alle funzioni

57


matematiche della prima colonna. Esse sintetizzano la complessit`a della maggior parte (non tutti) degli algoritmi. I primi algoritmi che vedremo hanno una complessit` a quadratica e si basano essenzialmente sul confronto numerico. I successivi algoritmi, invece, impiegano meno tempo a ordinare un array, hanno una complessit` a logaritmica, e si basano su un noto concetto (dividi et impera).

7.2

Selection sort

Il selection sort `e sicuramente uno degli algoritmi di ordinamento pi` u intuitivi e di semplice realizzazione e si basa sul confronto. A partire da un array, l’algoritmo effettua la ricerca del pi` u piccolo valore e ne scambia, poi, la posizione con quello puntato da un indice, che inizialmente viene fatto puntare al primo elemento dell’array. Al successivo tentativo, l’indice viene incrementato e la ricerca del valore pi` u piccolo viene allora ripetuta. Quanto appena detto va fatto fino a scandire l’intero array. /* selectionsort */ #include <stdio.h> #define LENGTH 10 void selectionSort(int *array,int length); int search_min(int *array,int begin,int end); void swap(int *x, int *y); int main(void) { int i,array[LENGTH]={1,8,4,0,2,5,6,3,7,9}; selectionSort(array, LENGTH); printf("array: "); for (i=0;i<LENGTH;i++) printf("%d, ",array[i]); printf("\b\b;\n"); return 0; } void selectionSort(int *array,int length) { int i,i_min; for (i=0;i<length;i++) { if ((i_min=search_min(array,i,length))==-1) printf("Errore nella chiamata alla funzione!"); else swap(&array[i],&array[i_min]); } } int search_min(int *array,int begin,int end) { int i,temp_i; if (begin<end) { temp_i=begin; for (i=begin+1;i<end;i++) { if (array[i]<array[temp_i]) temp_i=i; 58


} return temp_i; } else return -1; } void swap(int *x, int *y) { int temp=*x; *x=*y; *y=temp; } La complessit` a dell’inserction sort `e di tipo quadratico.

7.3

Bubble sort

L’algoritmo bubble sort prevede diversi confronti di coppie di valori adiacenti nell’array fino a ordinare l’intera sequenza. Per ogni scansione dell’array si confrontano dunque le coppie adiacenti e si procede a scambiarne l’ordine se questi non risultano tra loro ordinati. Alla fine di una singola scansione, l’elemento numerico pi` u grande presente nell’array occuper`a l’ultima posizione e i successivi confronti andranno arrestati prima! Il procedimento appena descritto va 59


poi iterato fintanto ch`e esistono coppie da confrontare, fino a giungere all’ultima coppia in testa all’array (presumibilmente, ancora da ordinare). I confronti possono essere inoltre arrestati se in una scansione dell’array il confronto delle coppie adiacenti non provoca nessuna inversione. In tal caso, infatti, `e evidente che il vettore `e gi` a ordinato! /* bubblesort */ #include <stdio.h> #include <stdbool.h> #define LENGTH 10 void bubbleSort(int *array,int length); void swap(int *x, int *y); int main(void) { int i,array[LENGTH]={1,8,4,0,2,5,6,3,7,9}; bubbleSort(array,LENGTH); printf("array: "); for (i=0;i<LENGTH;i++) printf("%d, ",array[i]); printf("\b\b;\n"); return 0; } void bubbleSort(int *array,int length) { int i,temp,step=0; bool change; do { change=false; for (i=0;i<length-1-step;i++) { if (array[i]>array[i+1]) { swap(&array[i],&array[i+1]); change=true; } } step++; } while(change==true); } void swap(int *x, int *y) { int temp=*x; *x=*y; *y=temp; } La complessit` a del bubble sort `e di tipo quadratico.

60


7.4

Quick sort

Il quick sort si basa sul paradigma dividi et impera. La versione qui presentata `e quella ricorsiva. Una funzione si dice ricorsiva quando nelle sue istruzioni `e prevista una chiamata alla funzione stessa! Il quick sort partiziona la sequenza da ordinare in due sequenze che vengono, poi, ordinate rispetto a un elemento (detto pivot, perno). Durante le prime istruzioni l’algoritmo si preoccupa di scegliere un elemento dell’array come pivot e partiziona la sequenza in due sottoinsiemi. Il primo sottoinsieme conterr`a tutti gli elementi dell’array che sono minori o uguali al pivot scelto. Il secondo sottoinsieme, invece, conterr`a tutti gli elementi dell’array che sono maggiori del pivot. A tale proposito una funzione per la partizione fa scorrere due indici (uno di questi parte dal primo elemento dell’array, il secondo dall’ultimo). Gli indici vengono incrementati (il primo) e decrementati (il secondo) finch`e viene rispettata la condizione d’ordine nei confronti del pivot. Non appena ci`o non `e possibile `e evidente che `e necessario scambiare i due elementi puntati dai rispettivi indici. Quando i due indici, a seguito dei rispettivi incrementi e decrementi, si incrociano la procedura per la partizione si arresta e va allora richiamata, ricorsivamente (la funzione chiamer`a se stessa!) sul primo sottoinsieme e sul secondo, fino a ordinare l’intero vettore. /* quicksort */ #include <stdio.h> #define LENGTH 10 void quickSort (int *array, int left, int right); void swap(int *x, int *y); int main (void) { int i,array[LENGTH]={1,8,4,0,2,5,6,3,7,9}; quickSort(array,0,LENGTH-1); printf("array: "); for (i=0;i<LENGTH;i++) printf("%d, ",array[i]); 61


printf("\b\b;\n"); } void quickSort (int *array, int left, int right) { int i=left, j=right; int pivot=array[(left+right)/2]; do { while (array[i]<pivot) i++; while (array[j]>pivot) j--; if (i<=j) { swap(&array[i],&array[j]); i++; j--; } } while (i<=j); if (left<j) quickSort(array,left,j); if (i<right) quickSort(array,i,right); } void swap(int *x, int *y) { int temp=*x; *x=*y; *y=temp; } La complessit` a del quick sort nel caso medio e migliore `e di tipo logaritmico che diventa, invece, quadratica nel caso peggiore.

7.5

Merge sort

A differenza dei precedenti algoritmi il merge sort ha una complessit`a logaritmica, anche nel caso peggiore. Il merge sort, che pure si basa sul paradigma dividi et impera, opera dividendo lâ&#x20AC;&#x2122;array da ordinare in due met`a e affina tale partizione fino ad isolare ogni singolo elemento. La successiva fase, quindi, prevede la costruzione di un array ordinato operando una vera e propria fusione degli insiemi che man mano vengono ordinati. /* mergesort */ #include <stdio.h> #define LENGTH 10 void mergeSort(int *array, int begin, int end); void merge(int *array,int begin, int middle, int end); int main(void) { int i,array[LENGTH]={1,8,4,0,2,5,6,3,7,9}; mergeSort(array,0,LENGTH); printf("array: "); for (i=0;i<LENGTH;i++) printf("%d, ",array[i]); 62


printf("\b\b;\n"); return 0; } void mergeSort(int *array, int begin, int end) { int middle; if (begin<end) { middle=(end+begin)/2; mergeSort(array,begin,middle); mergeSort(array,middle+1,end); merge(array,begin,middle,end); } } void merge(int *array,int begin, int middle, int end) { int i,j,k,temp[LENGTH]; i=begin; j=middle+1; k=0; while ((i<=middle) && (j<=end)) { if (array[i]<=array[j]) temp[k++]=array[i++]; else temp[k++]=array[j++]; } while (i<=middle) temp[k++]=array[i++]; while (j<=end) temp[k++]=array[j++]; for (k=begin;k<=end;k++) array[k]=temp[k-begin]; }

63


64


Capitolo 8

Strutture Le strutture dati ci permettono di organizzare meglio l’informazione, i nostri dati. Tutte le variabili fra loro correlate possono essere disposte e gestite in un’unica struttura dati, come se fosse un unica variabile. Pensate ad esempio allo stato di un sistema, descritto da pi` u variabili (velocit`a, accelerazione, coordinate, etc... etc...). Oppure alla situazione aziendale di un impegato, riassunta da pi` u voci che ne descrivono il profilo (nome e cognome, numero di matricola, progetti in gestione, competenze, etc... etc...). Se questa cosa vi interessa non posso che invitarvi a continuare la lettura di questo capitolo.

8.1

Definizione ed esempi

La dichiarazione di una struttura avviene per mezzo della parola chiave struct. Il programmatore deve far seguire alla suddetta parola chiave un nome che identifica la struttura, ogni variabile da inserire nella struttura va poi inserita (con tipo e nome di variabile da usare) in blocco di istruzioni: struct point { int x; int y; }; E’ come definire un nuovo tipo, nell’esempio abbiamo dichiarato, allora, il tipo point. Nella funzione main() si pu`o quindi dichiarare una variabile di tipo point. La fase di dichiarazione pu`o avvenire anche subito dopo quella di definizione: struct point { int x; int y; } a,b; Nell’esempio abbiamo prima definito la struttura point, poi le variabili a e b, due punti del piano cartesiano. Se le due fasi (definizione della struttura e dichiarazione delle variabili) vengono divise la dichiarazione delle variabili avviene, invece, in questo modo:

65


... struct point { int x; int y; }; int main(void) { ... struct point o={0,0}; ... } La variabile o (il centro del piano cartesiano) dellâ&#x20AC;&#x2122;esempio viene dichiarata e inizializzata. Le variabili che compongono una struttura sono anche dette membri della struttura. Lâ&#x20AC;&#x2122;accesso a un membro della struttura avviene facendo seguire al nome dato alla struttura lâ&#x20AC;&#x2122;operatore . (detto operatore di membro) che a sua volta precede il nome dato al membro della struttura. ... struct point { int x; int y; }; int main(void) { ... struct point o={0,0}; printf("Origine: %d,%d\n",o.x,o.y); ... } Il linguaggio favorisce la definizione di strutture dati nidificate. Avendo definito la struttura point possiamo allora utilizzare la stessa per la definzione di una nuova struttura, ad esempio un struttura rect per registrare le coordinare di un rettangolo: #include <stdio.h> int main(void) { /* descrive un punto */ struct point { int x; int y; }; /* descrive e dichiara un rettangolo */ struct rect { struct point a; struct point b; } sports_ground; int area; 66


printf("Angolo inferiore sinistro: "); scanf("%d %d",&sports_ground.a.x,&sports_ground.a.y); printf("Angolo superiore destro: "); scanf("%d %d",&sports_ground.b.x,&sports_ground.b.y); area=((sports_ground.b.x)-(sports_ground.a.x))* ((sports_ground.b.y)-(sports_ground.a.y)); printf("Area: %d\n",area); return 0; } L’accesso alle singole componenti dei punti che definiscono il rettangolo (angolo superiore destro e angolo inferiore sinistro) permette di applicare su queste le comuni operazioni dell’aritmetica nonch`e di richiamare, se ci`o occorre, funzioni pi` u complesse e definite quindi nell’ambito del programma. Le strutture non possono essere confrontate, occorre prima stabilire una regola che ne permette il confronto.

8.2

typedef

Abbiamo in precedenza detto che la dichiarazione di una struttura ne permette poi l’impiego all’interno del programma. Al compilatore, tuttavia, per segnalare l’uso della struttura da applicare come tipo di variabile, va segnalato che la variabile `e strutturata e va pertanto preceduta, durante la sua dichiarazione, dalla parola chiave struct. In altre parole, prima definiamo la struttura (con la parola chiave struct), poi la dichiariamo nel programma con una sintassi di questo tipo: struct NOME VARIABILE. Con typedef, invece, possiamo definire una volta per tutte un nuovo tipo di variabile (valido all’interno del programma che stiamo scrivendo). Per fare questo `e sufficiente far precedere alla parola chiave struct la parola chiave typedf. Ora possiamo dichiarare all’interno del programma la variabile strutturata senza ricorrere alla precedente sintassi: ... typedef struct { int x; int y; } point; int main(void) { ... point o={0,0}; printf("Origine: %d,%d\n",o.x,o.y); ... } In questo caso il nome della struttura, che definisce poi anche il nuovo tipo, va messo alla fine, prima di chiudere l’istruzione (con il solito simbolo ;).

67


8.3

Strutture e funzioni

Abbiamo gi` a visto come dichiarare una funzione e usarla, poi, nel corso del programma. Con l’introduzione delle strutture possiamo adesso definire per i nostri programmi delle apposite strutture per la raccolta dei dati, sia per l’input che per l’output. Una funzione pu`o accettare come argomento di input una struttura e restituire come output una struttura. Consiglio, ad esempio, di inventare una struttura dati per l’output di una funzione qualora al chiamante vanno restituiti, al termine della funzione, pi` u di un valore. #include <stdio.h> /* descrive un punto */ typedef struct { int x; int y; } point; /* costruisce un punto */ point make_point(int x, int y) { point temp; temp.x=x; temp.y=y; return temp; } /* calcola il punto medio */ int main(void) { point a,b,middle; printf("Coordinate del punto a: "); scanf("%d %d",&a.x,&a.y); printf("coordinate del punto b: "); scanf("%d %d",&b.x,&b.y); middle=make_point(((a.x+b.x)/2),((a.y+b.y)/2)); printf("Punto medio: %d,%d\n",middle.x,middle.y); return 0; } In questo programma point viene usato come un nuovo tipo di variabile! L’istruzione return ci consente di restituire al chiamante un solo valore di variabile. Dunque, se vogliamo restituire pi` u di un valore al chiamante possiamo allora realizzare una struttura dati e organizzare l`ı l’informazione utile.

8.4

Strutture dati, un esempio: lista

Una struttura dati pu` o condizionare enormemente le prestazioni di un programma. Alcune strutture dati hanno per questo motivo dei vantaggi e svantaggi. Finora i costrutti a nostra disposizione ci hanno permesso di utilizzare i tipi di variabili pi` u comuni e definiti dal linguaggio. In molti casi quando siamo interessati a collezionare una lista di valori ricorriamo quasi sempre a un array di tali 68


valori. L’array (che `e una sruttura dati statica) rende facili alcune operazioni (come l’accesso in lettura o scrittura) e ne complica invece altre (pensate ad esempio ai problemi di ricerca e rilocazione). Per questo motivo non sempre, quindi, si addice alla natura del programma. In questo paragrafo vorrei presentarvi una particolare struttura dati, la lista di dati. Si tratta di una struttura dati dinamica, lo spazio occupato in memoria varia a seconda dell’input. A differenza dell’array, che `e una struttura dati statica (la dichiarazione di un array alloca in memoria n caselle consecutive per i dati, con n fisso), la lista alloca lo spazio necessario per ogni elemento. Se occorre, allora, un nuovo elemento, verr`a allocato spazio in memoria. Se l’elemento viene rimosso dalla lista, la memoria precedentemente occupata dallo stesso viene resa nuovamente disponibile. Per realizzare una lista ci occorre una struttura dati adeguata. I puntatori spesse volte sono usati assieme alle strutture dati e a breve vedremo come. Esistono numerose strutture dati: liste, liste circolari, code, code con priorit`a, pile etc... I concetti alla base della lista di dati, cos`ı come alcune propriet`a strutturali, ci permettono di applicare molte delle cose che abbiamo finora imparato. Una lista di dati deve contenere una sequenza di variabili, sia semplici che a loro volta strutturati. Ogni elemento della lista viene allora modellato da una struttura dati (elementare) che definisce quindi un nuovo tipo. Cosa deve contenere questa struttura dati elementare? La prima cosa da aggiungere a tale struttura `e l’informazione che ci interessa collezionare, ad esempio uno o pi` u valori di tipo int. Per tenere collegati tutti gli elementi della lista va poi aggiunto un puntatore al prossimo elemento. Un disegno chiarir` a ogni dubbio:

La lista, allora, sar` a un puntatore alla variabile node (il primo nodo della lista). Quando viene chiesto al programma di aggiungere il primo elemento della lista viene chiamata la funzione make(). Quest’ultima alloca lo spazio necessario a memorizzare un elemento della lista, aggiunto con la chiamata alla funzione add(). Sia make() che add() ritornano al chiamante un puntatore a node. Un metodo sicuramente interessante `e quello che si occupa di eliminare dalla lista un valore precedentemente inserito. Se le precedenti funzioni aggiungevano il primo o i successivi nodi della lista, allocando spazio in memoria attraverso malloc(), la funzione del() libera, invece, lo spazio precedentemente impegnato attraverso free(). Nell’eliminazione di un elemento (il primo trovato all’interno della lista) vanno analizzati pi` u casi. Ogni volta che viene indicato al programma un valore da eliminare si procede alla dichiarazione di un puntatore, bad node. Questo punter` a, inizialmente, al primo elemento della lista e verr`a quindi aggiornato al successivo valore ad ogni iterazione. Se si tratta del valore da cancellare vanno allora valutati diversi casi. Potrebbe infatti trattarsi, dell’elemento in cima alla lista, dell’ultimo, oppure dell’elemento che occupa una posizione centrale nella lista. La funzione del() traduce proprio le riflessioni fin qui fatte. L’eliminazione di un valore comporta, poi, (oltre alla chiamata alla funzione free())

69


lâ&#x20AC;&#x2122;aggiornamento del puntatore al prossimo valore. A tale proposito un puntatore a node, detto previous, punta al nodo precedentemente analizzato a cui va evidentemente aggiornato il puntatore al successivo valore (poich`e quello appena letto deve essere rimosso)! /* gestione di una lista di dati */ #include <stdio.h> #include <stdlib.h> /* definisce il singolo nodo */ typedef struct { int value; struct node *next; } node; /* la lista ` e un puntatore al primo nodo */ typedef node* list; int show_menu(list current_root); node *make(); node *add(int value, list current_root); node *del(int value, list current_root); int search(int value, list current_root); int count(list current_root); void *print(list current_root); int main(void) { int choice,value,quit=0; list root=NULL; while (quit==0) { choice=show_menu(root); switch(choice) { case(1): /* aggiungi */ printf("Valore: "); scanf("%d",&value); if (root==NULL) root=make(); root=add(value,root); break; case(2): /* rimuovi */ printf("Valore: "); scanf("%d",&value); root=del(value,root); break; case(3): /* cerca */ printf("Valore: "); scanf("%d",&value); printf(" %d occorrenze del valore %d!\n", 70


search(value,root),value); break; case(4): /* stampa */ print(root); break; case(5): /* esci */ printf("Attenzione: la lista andr` a persa!"); free(root); quit=1; break; default: break; } printf("\n"); } return 0; } /* mostra le opzioni del menu e raccoglie lâ&#x20AC;&#x2122;input */ int show_menu(list current_root) { int choice=0; do { printf("Gestione della lista (%d elementi):\n", count(current_root)); printf(" 1 - Aggiungi;\n"); printf(" 2 - Rimuovi;\n"); printf(" 3 - Cerca;\n"); printf(" 4 - Stampa;\n"); printf(" 5 - Esci;\n"); printf("Scelta: "); scanf("%d",&choice); } while (choice>5); return choice; } /* crea una lista */ node *make() { node *root; root=malloc(sizeof(node)); if (root==NULL) { printf("ATTENZIONE: memoria non disponibile!\n"); exit(1); } else return root; } /* aggiunge un nodo alla lista */ node *add(int value, list current_root) { 71


node *new_node=malloc(sizeof(node)); new_node->value=value; if (current_root==NULL) new_node->next=NULL; else new_node->next=current_root; return new_node; } /* elimina un valore dalla lista */ node *del(int value, list current_root) { node *bad_node=current_root; node *previous=NULL; int found=0; if (current_root!=NULL) { /* lista non vuota... */ while (bad_node->next!=NULL && found!=1) { if (bad_node->value==value) { found=1; if (previous==NULL) { if (bad_node->next!=NULL) current_root=(node *)bad_node->next; else current_root=NULL; free(bad_node); return current_root; } else { if (bad_node->next!=NULL) previous->next=bad_node->next; else previous->next=NULL; free(bad_node); return current_root; } } else { previous=bad_node; bad_node=(node *)bad_node->next; } } } else return NULL; } /* conta gli elementi della lista */ int count (list current_root) { int count=0; if (current_root!=NULL) { while (current_root->next!=NULL) { count++; current_root=(node *)current_root->next; } } return count; 72


} /* conta il numero di occorrenze di un valore */ int search(int value, list current_root) { int count=0; if (current_root!=NULL) { while (current_root->next!=NULL) { if (current_root->value==value) count++; current_root=(node *)current_root->next; } } return count; } /* stampa la lista */ void *print(list current_root) { printf("Lista: "); if (current_root==NULL) printf("vuota!"); else { while (current_root->next!=NULL) { printf("%d ",current_root->value); current_root=(node *)current_root->next; } } printf("\n"); }

8.5

Le union

Le union nascono dalla necessit`a di dover gestire nella stessa area di memoria (allocata) una variabile il cui tipo, nel corso del programma, non `e ancora definito. Si tratta di una struttura dati vera e proprio. I membri della stessa, allora, prevedono un variabile per ogni tipo atteso. union position_x { int i_x; float f_x; double d_x; } position; Ovviamente alla variabile position verr`a assegnato uno solo dei tipi previsti e lo spazio necessario per la suddetta variabile coincider`a con quello pi` u grande fra i tipi presenti nella union. L’accesso ai memvri della union avviene, cos`ı come per le strutture, in due modi: nome union.nome membro oppure nome union->nome membro. E’ compito del programmatore tenere traccia del tipo usato o richiesto dall’utente nel corso del programma. Ci` o pu` o essere fatto (anche a tempo di esecuzione, runtime) dedicando all’evento una variabile, ecco un esempio: /* union */

73


#include <stdio.h> #define INT 1 #define FLOAT 2 #define DOUBLE 3 /* union per la posizione dell’auto */ typedef union u_xposition { int i_xposition; float f_xposition; double d_xposition; } xposition; int main (void) { int type_for_xposition=DOUBLE; /* tipo */ xposition car; car.d_xposition=5.5; if (type_for_xposition==INT) printf("L’auto ha percorso %d metri!\n",car.i_xposition); else if (type_for_xposition==FLOAT) printf("L’auto ha percorso %.1f metri!\n",car.f_xposition); else if(type_for_xposition==DOUBLE) printf("L’auto ha percorso %.3f metri!\n",car.d_xposition); else printf("Errore con un tipo!\n"); return 0; } Le union possono trovare posto all’interno di strutture dati, laddove il programmatore prevede per una variabile pi` u di un tipo.

74


Capitolo 9

File Ed eccoci ai file! Finora i programmi scritti raccoglievano l’input e restituivano l’output sugli standard definiti di default dal sistema operativo (che per i sistemi operativi Linux sono stdin e stdout). Dopo qualche programma si inizia a sentire la necessit` a di scrivere o leggere qualcosa da un file. Questa possibilit` a aumenta di molto la flessibilit`a del programma, soprattutto quando al riavvio dell’applicazione si riesce a ristabilire lo stato del programma (e delle sue variabili)! Le librerie del linguaggio implementano le funzioni necessarie a manipolare i file, non ci resta che studiarne i comportamenti e, cosa pi` u importante, iniziare ad utilizzarle!

9.1

Aprire e chiudere file

La gestione dei file avviene attraverso i puntatori. All’interno della libreria stdio.h `e stata dichiarata una struttura dati per la gestione dei file, detta FILE. Pertanto, se siamo interessati a manipolare un file non ci resta che dichiarare all’interno del nostro programma un puntatore a FILE. Tutte le operazioni su un file faranno riferimento al suddetto puntatore dichiarato. Prima di iniziare a leggere o a scrivere un file `e necessario aprire il file in questione. Tale operazione `e indispensabile poich`e oltre a restituire un puntatore al file ci dir` a se il file da usare esiste! Per aprire un file si ricorre alla funzione fopen(): FILE *fopen(char *filename, char *mode); che accetta come argomenti il nome del file (il file verr`a cercato nella stessa cartella che ospita il programma, per tutti gli altri file va invece dato il path al file), un puntatore a carattere, e la modalit`a con cui aprire il file, ancora un puntatore a carattere. Un file pu` o essere aperto in pi` u modi: • w: apre il file in scrittura permettendone la modifica. Attenzione, il precedente contenuto viene rimosso per fare posto a quello nuovo; • r: apre il file in lettura, nessuna modifica pu`o essere applicata al file; • a: apre il file in scrittura, ogni aggiunta al file verr`a collocata in fondo al file. Al precedente contenuto viene, allora, aggiunto, quello nuovo; 75


• b: se il sistema operativo `e in grado di operare la distinzione tra file di testo e file binari, oltre a una delle precedenti modalit`a occorre specificare anche questa possibilit` a; Ogni tentativo di aprire in scrittura un file inesistente provoca, se possibile, la creazione di un nuovo file (verr`a usato il nome passato alla funzione fopen()). La funzione fopen() ritorna, in caso di successo, un puntatore a FILE. Per le successive operazioni di lettura e/o scrittura si dovr`a usare il puntatore restituito da fopen(). In caso di errore la funzione restituisce, invece, il valore NULL. Quando sul file aperto non sono necessarie ulteriori operazioni di lettura e/o scrittura `e consigliabile chiudere il file. Anche questa operazione `e indispensabile, alcuni sistemi operativi impongono infatti un limite ai file che `e possibile aprire (contemporaneamente)! Pertanto, ad ogni apertura del file dovr`a poi seguire, dopo averlo usato, una chiusura dello stesso. La funzione per chiudere un file `e fclose(): int fclose(FILE *fp); Per chiudere un file occorre passare alla funzione fclose() il puntatore del file ottenuto in precedenza con fopen().

9.2

Scrivere su file

Per la scrittura su file esiste una funzione che si comporta proprio come printf(), si tratta di fprintf(): int fprintf(FILE *fp, char *line, ...); A differenza di printf(), la funzione fprintf() effettua la stampa su FILE. L’intero restituito dalla funzione coincide con il numero di caratterei scritti. In caso di errore il valore restituito sar`a -1. Per la stampa su file occorre passare alla funzione il puntatore al file (primo argomento) e la stringa da scrivere (secondo argomento).

9.3

Leggere da file

La lettura da file avviene per mezzo della funzione fscanf(): int fscanf(FILE *fp, char *line, ...); L’intero restituito dalla funzione coincide con il numero di variabili a cui `e stato assegnato un valore oppure EOF se si `e verificato un errore di lettura nell’input oppure se `e stata incontrata la fine del file. Se la funzione fscanf() ritorna il valore 0 esistono allora dei valori in input che non hanno tuttavia trovato alcuna corrispondenza con i caratteri specificati per la formattazione del testo presente in line. Per la lettura su file occorre passare alla funzione il puntatore al file (primo argomento), la stringa per la formattazione del’input (secondo argomento) e la lista di variabili da usare per l’assegnazione dei valori letti. E’ come usare scanf(), in fscanf() occorre aggiungere solo il puntatore a FILE!

76


9.4

Esempio: il file esiste?

Ecco come verificare se un file esiste: /* il file esiste? */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 100 int main(void) { char filename[MAX_LENGTH]; FILE *fp; printf("Nome del file: "); fgets(filename,sizeof(filename),stdin); filename[strlen(filename)-1]=0; if ((fp=fopen(filename,"r"))==NULL) printf("Il file %s non esiste!\n",filename); else { printf("Il file %s esiste!\n",filename); fclose(fp); } return 0; } In questo esempio `e importante accedere in lettura al file, per non cancellare il contenuto esistente. Se dunque la funzione fopen() restituisce un puntatore diverso da quello NULL, il file cercato esiste ed `e stato aperto in lettura. Solo in questo caso, prima di terminare il programma, `e indispendabile chiamare anche fclose()! Provate a verificare il funzionamento di questo programma escludendo la chiamata a fclose(), cosa succede?

9.5

Esempio: il comando cat

Il comando cat `e un comando Linux che, come descrivono le pagine del manuale di Linux (comando man cat), concatena i file passati come argomenti stampandone il risultato sullo standard output (nella maggior parte dei casi il monitor del computer). Se al comando viene fornito un solo nome di file sul monitor si legger` a, allora, il contenuto di quel file! Ecco una versione semplificata di cat: Il codice di questo esempio presenta due nuove funzioni: getc() e putc(). La funzione getc(), cos`Äą definita: int getc(FILE *fp); restituisce lâ&#x20AC;&#x2122;ultimo carattere letto dal file, quello cio`e puntato da fp (che `e un puntatore a FILE). Se si verifica un errore oppure il file `e finito ritorna il valore EOF (end of file).La funzione putc(), cos`Äą definita: int putc(int c, FILE *fp); scrive, invece, il carattere c (identificandolo attraverso il suo codice ascii) nel file puntato da fp. Le funzioni getc() e putc() sono, allora, le funzioni getchar() e putchar() per i file! 77


9.6

Esempio: lettura e scrittura

Come usare le funzioni fprintf() e fscanf()? Ecco un esempio, banale, che verificando l’esistenza di un file (creato durante una prima conversazione con il computer) `e in grado di stampare l’et`a precedentemente detta dall’utente (se nel frattempo non `e cambiata): /* mi ricordo di te */ #include <stdio.h> #include <string.h> #define MAX_LENGTH 50 int exist(char *filename); int main(int argc, char *argv[]) { FILE *fp; char username[MAX_LENGTH]; char filename[MAX_LENGTH+3]; int age; printf("Come ti chiami?\n"); fgets(username,sizeof(username),stdin); username[strlen(username)-1]=0; strcpy(filename,username); strcat(filename,".txt"); if ((exist(filename))==1) { strcpy(filename,username); strcat(filename,".txt"); fp=fopen(filename,"r"); fscanf(fp,"%d\n",&age); fclose(fp); printf("Dovresti avere %d anni!\n",username,age); } else { fp=fopen(filename,"w"); printf("Quanti anni hai?\n"); scanf("%d",&age); fprintf(fp,"%d\n",age); fclose(fp); printf("Ciao %s, il kernel mi sta chiamando!\n",username); } return 0; } /* se il file esiste ritorna 1, altrimenti -1 */ int exist(char *filename) { FILE *fp; if ((fp=fopen(filename,"r"))==NULL) return -1; else { fclose(fp);

78


return 1; } }

9.7

Altre funzioni per file

Quello che segue `e un elenco di altre funzioni di libreria (stdio) per la gestione dei file: • remove(): rimuove un file dal filesystem. int remove(const char *filename); Ritorna il valore 0 in caso di successo, altrimenti un valore diverso da 0; • rename(): rinomina un file con un nuovo nome. int rename(const char *oldname, const char *newname); Ritorna il valore 0 in caso di successo, altrimenti un valore diverso da 0; • setvbuf(): imposta il tipo di buffer da usare per il flusso (stream) di caratteri letti dal file. inr setvbuf(FILE *stream, char *buf, int mode, size_t size); Con la costante IOFBF l’intero file viene bufferizzato, con IOLBF il file viene bufferizzato a linee, con IONBF il file, invece, non viene bufferizzato. Ovviamente l’impostazione del buffer deve avvenire prima di ogni operazione sul file. Ritorna il valore 0 in caso di successo, altrimenti un valore diverso da 0; • fflush(): svuota i dati presenti nel buffer di lettura e non ancora scritti sullo stdout. int fflush(FILE *stream); In caso di successo la funzione ritorna il valore 0, altrimenti EOF;

79


Capitolo 10

Direttive per il preprocessore Il compilatore ha pi` u amici (programmi), uno di questi `e il preprocessore. Prima ancora di passare il file sorgente al compilatore avvengono delle importanti sostituzioni all’interno del file. Il preprocessore, in generale, sostituisce ogni chiamata alla libreria inclusa al file con la libreria vera e propria! Lo stesso preprocessore, poi, effettua altre sostituzioni che a breve vedremo. Tutte le istruzioni dirette al preprocessore iniziano con il carattere #. Visto il lavoro svolto dal preprocessore possiamo allora affermare che il preprocessora `e anche amico del programmatore.

10.1

Includere file di librerie

La direttiva pi` u usata e diretta al preprocessore `e la direttiva #include, usata per includere un file di libreria al sorgente del programma scritto. I file inclusi con #include vengono anche detti header. Si tratta di normali file scritti in linguaggio C, al loro interno vengono solitamente definite variabili e funzioni per programmi (poi richiamate dal chiamante, ovvero il programma con la funzione main() da noi scritto). Se nel programma viene invocata una funzione che non `e definita in nessun file di libreria il compilatore genera un errore. Le librerie usate dal compilatore vengono solitamente raccolte in alcune cartelle di sistema. E’ in tale cartella che il preprocessore cerca i file e li sostituisce con il loro contenuto. Ci` o avviene, come finora visto negli esempi, con l’istruzione #include <NOME LIBRERIA.h>. E se il file di libreria, ad esempio scritto da noi, si trova in una diversa cartella? E’ allora possibile richiamare un file di libreria passando alla direttiva #include l’intero path che punta al file, racchiudendo lo stesso fra i doppi apici: #include "/home/user/libreria.h" Attenzione, le istruzioni dirette al preprocessore non terminano mai con il punto e virgola.

80


10.2

Definire costanti e macro

La dichiarazione di costanti all’interno di un programma avviene attraverso la direttiva #define, gi` a usata nel corso di queste stesse note. Ecco un esempio: #define MAX_LENGTH 100 Cos`ı come accade per i file di libreria, anche per le costanti il preprocessore effettua, prima di passare il file sorgente al compilatore, la sostituzione delle stesse con il valore per esse definito. L’uso della direttiva #define non si esaurisce qui, `e infatti possibile definire anche delle macro. La macro `e un’istruzione del linguaggio C (a volte anche lunga) rappresentata da un etichetta definita in fase di dichiarazione della stessa macro. #define max(a,b) ((a) > (b) ? (a) : (b)) Nell’esempio mostrato sopra viene dichiarata una macro usata, poi, nel programma. Ogni occorrenza della macro max(a,b) viene allora sostituita con l’istruzione ((a) > (b) ? (a) : (b)), utile? Dipende dal programma. Questa direttiva pu` o farvi risparmiare non poche righe di codice se ad esempio, nel programma, l’istruzione in questione viene usata con una certa frequenza.

10.3

Macro predefinite

Esistono alcune macro definite di default dal linguaggio che permettono di accedere ad informazioni relative al file compilato. L’uso di tali macro avviene attraverso la direttiva #line seguita dal nome della macro predefinita che sono: • •

LINE : costante numerica che indica il numero di riga della linea di codice sorgente in esecuzione; FILE : stringa costante che contiene il nome del file in esecuzione;

DATE : stringa costante che contiene la data di compilazione del file in esecuzione;

TIME : stringa costante che contiene l’ora di compilazione del file in esecuzione;

STDC : un flag di stato, se `e uguale a 1 il programma implementato `e conforme allo standard;

/* direttive */ #include <stdio.h> int main(void) { printf("Nome file: %s\n",__FILE__); printf("Data di compilazione: %s %s\n",__DATE__,__TIME__); printf("Conforme allo standard: "); if(__STDC__==1 ) printf("si\n"); else printf("no\n"); printf("Questa ` e la riga numero %d del file!\n",__LINE__); return 0; } 81


Ecco l’output generato dal programma: Nome file: /home/light-kun/Documenti/10-4.c Data di compilazione: Apr 3 2010 16:49:20 Conforme allo standard: si Questa ` e la riga numero 10 del file!

10.4

Compilazione condizionata

La compilazione di un file sorgente pu`o avvenire includendo determinati file di libreria e impostando le variabili di programma in un preciso modo. Per permettere al programmatore di impartire questi ordini di compilazione il preprocessore mette a disposizione le seguenti direttive: • #if espressione; • #else espressione; • #elif espressione; • #endif espressione; • #ifdef espressione; • #ifndef espressione; • defined (espressione); • #undef espressione; La direttiva defined (espressione) dichiara un espressione evidentemente ricorrente e quindi usata pi` u volte dal preprocessore. Le direttive #if, #else e #endif definiscono la nota struttura basata su una condizione (espressione oppure da defined (espressione)) e avente, quindi, due possibili alternative. Ecco un esempio: #define POWER on #define SPEED 100 ... int speed; .. #if POWER==on int speed=SPEED+20; #else int speed=SPEED; #endif ... Con la diretta #elif possiamo poi contemplare pi` u casi: 82


#define POWER 1 #define SPEED 100 ... int speed; .. #if POWER==1 speed=SPEED+10; #elif POWER==2 speed=SPEED+20; #elif POWER==3 speed=SPEED+30; #endif ... La direttiva #ifdef pu` o essere usata per scrivere #if defined (espressione). La direttiva #ifndef, invece, equivale a scrivere #if !defined (espressione). La definizione di una macro pu`o essere annullata con la direttiva #undef. Con #undef ci assicuriamo che una funzione venga definita come funzione piuttosto che come macro. Un uso assai frequente delle direttive per il preprocessore si verifica in tutti quei programmi che hanno come target la generazione di un eseguibile per pi` u sistemi operativi: /* windows o linux? */ #include <stdio.h> const char* pulisci; #ifdef WIN32 pulisci="cls"; #else #ifdef LINUX pulisci="clear"; #endif #endif int main (void) { system(pulisci); printf("Hello world!\n"); return 0; } In questo esempio viene chiamata la funzione system() (che esegue la stringa passata come se questa fosse un comando esterno e quindi interpretabile dal sistema operativo). Per i sistemi operativi Linux il comando per pulire lo schermo `e clear, per i sistemi operativi Windows, invece, il comando `e cls. Dunque 83


la compilazione del programma attribuisce allâ&#x20AC;&#x2122;eseguibile il giusto valore per la variabile pulisci!

84


Capitolo 11

Funzioni di libreria In questo capitolo vengono elencate alcune funzioni di libreria. Le funzioni della libreria string.h sono state gi`a viste nel capitolo 4!

11.1

Libreria ctype.h

La libreria ctype.h prevede diverse funzioni per la classificazione e modifica dei caratteri. Tutte le funzioni in caso di successo ritornano al chiamante un valore diverso da 0, altrimenti 0. Il valore in input alle funzioni `e in tutti i casi un int o comunque un unsigned int (intero senza segno) che corrisponde al valore usato nella codifica del testo. • int isalnum(int c): verifica se il carattere passato sia alfanumerico; • int isalpha(int c): verifica se il carattere passato `e una lettera dell’alfabeto; • int isblank(int c): verifica se il carattere passato `e un carattere di spazio o di tabulazione; • int iscntrl(int c): verifica se il carattere passato `e un carattere di controllo; • int isdigit(int c): verifica se il carattere passato `e un numero (digit); • int islower(int c): verifica se il carattere passato `e minuscolo; • int isprint(int c): verifica se il carattere passato `e stampabile; • int ispunct(int c): verifica se il carattere passato `e un carattere di punteggiatura; • int isspace(int c): verifica se il carattere passato `e uno spazio; • int isupper(int c): verifica se il carattere passato `e maiuscolo; • int isxdigit(int c): verifica se il carattere passato `e esadecimale; • int tolower(int c): converte c in lettera minuscola; • int toupper(int c): converte c in lettera maiuscola; 85


11.2

Libreria math.h

La libreria math.h contiene funzioni (e costanti) matematiche. Tutte le funzioni ricevono come parametri uno o pi` u double (oppure int), il valore ritornato al chiamante `e, invece, sempre un double. • double sin(double x): funzione seno di x; • double cos(double x): funzione coseno di x; • double tan(double x): funzione tangente di x; • double asin(double x): funzione arcoseno di x; • double acos(double x): funzione arcocoseno di x; • double atan(double x): funzione arcotangente di x; • double sinh(double x): funzione seno iperbolico di x; • double cosh(double x): funzione coseno iperbolico di x; • double tanh(double x): funzione tangente iperbolica di x; • double exp(double x): calcola la funzione esponenziale di x; • double log(double x): calcola il logaritmo naturale di x; • double log10(double x): calcola il logaritmo in base 10 di x; • double pow(double x, double y): calcola valore di x elevato y; • double sqrt(double x): calcola la radice quadrata di x; • double fabs(double x): calcola il valore assoluto di x; Le funzioni che ritornano valori di angoli restituiscono gli stessi espressi in radianti!

11.3

Libreria stdlib.h

Nella libreria stdlib.h `e possibile trovare funzioni per: la conversione fra tipi, la gestione della memoria, il controllo dei processi, l’ordinamento e la ricerca di valori (che qui non riporto). • double atof(const char *s): converte s in double; • int atoi(const char *s): converte s in int; • long atol(const char *s): converte s in long; • int rand(void): genera un numero casuale compreso fra 0 e 32767; • void srand(unsigned int seed): usa seed come seme per generare una nuova sequenza di numeri casuali (usare srand() prima di rand();

86


• void *calloc(size t nobj, size t size): ritorna un puntatore allo spazio per un vettore di nobj oggetti di ampiezza size, altrimenti NULL; • void *malloc(size t size): ritorna un puntatore allo spazio per un oggetto di ampiezza pari a size, altrimenti NULL; • void *realloc(void *p,size t size): modifica l’ampiezza dell’oggetto puntato da p portando la stessa a size. Ritorna un puntatore alla nuova area di memoria, altrimenti NULL; • void free(void *p): rilascia lo spazio di memoria puntato (e quindi usato) da p; • void exit(int status): causa la fine (normale) del programma ritornando lo stato di chiusura che pu`o essere, ad esempio, pari a EXIT SUCCESS o EXIT FAILURE; • int system(const char *s): passa al sistema operativo la stringa s affinch`e essa venga eseguita come un comando esterno. Il valore ritornato dipende dall’implementazione di s;

11.4

Libreria time.h

La libreria time.h permette di dichiarare tipi e funzioni per la gestione di date e ore. • clock t clock(void): ritorna il tempo di cpu usato dal programma a partire dall’esecuzione dello stesso. Se l’informazione tornata non `e disponibile il valore restituito `e -1; • time t time(time t *tp): ritorna l’ora corrente (assegnata a tp), altrimenti -1; • double difftime(time t time2, time t time1): ritorna la differenza fra time2 e time1; Per altre funzioni vi suggerisco di cercare in Internet. Le informazioni restituite dalle funzioni mediante la struttuta time t vengono organizzate in pi` u membri, ecco quelli pi` u importanti: • int tm sec: secondi dopo il minuto; • int tm min: minuti dopo le ore; • int tm hour: ore dopo la mezzanotte; • int tm mday: giorno del mese; • int tm mon: mese dell’anno (attenzione, va da 0 a 11); • int tm year: anno solare; • int tm isdst: un flag per l’ora legale;

87


Capitolo 12

Licenza per la documentazione libera GNU 12.1

Preamble

The purpose of this License is to make a manual, textbook, or other functional and useful document free in the sense of freedom: to assure everyone the effective freedom to copy and redistribute it, with or without modifying it, either commercially or noncommercially. Secondarily, this License preserves for the author and publisher a way to get credit for their work, while not being considered responsible for modifications made by others. This License is a kind of copyleft, which means that derivative works of the document must themselves be free in the same sense. It complements the GNU General Public License, which is a copyleft license designed for free software. We have designed this License in order to use it for manuals for free software, because free software needs free documentation: a free program should come with manuals providing the same freedoms that the software does. But this License is not limited to software manuals; it can be used for any textual work, regardless of subject matter or whether it is published as a printed book. We recommend this License principally for works whose purpose is instruction or reference.

12.2

Applicability and definictions

This License applies to any manual or other work, in any medium, that contains a notice placed by the copyright holder saying it can be distributed under the terms of this License. Such a notice grants a world-wide, royalty-free license, unlimited in duration, to use that work under the conditions stated herein. The Document, below, refers to any such manual or work. Any member of the public is a licensee, and is addressed as you. You accept the license if you copy, modify or distribute the work in a way requiring permission under copyright law. A Modified Version of the Document means any work containing the Document

88


or a portion of it, either copied verbatim, or with modifications and/or translated into another language. A Secondary Section is a named appendix or a front-matter section of the Document that deals exclusively with the relationship of the publishers or authors of the Document to the Documentâ&#x20AC;&#x2122;s overall subject (or to related matters) and contains nothing that could fall directly within that overall subject. (Thus, if the Document is in part a textbook of mathematics, a Secondary Section may not explain any mathematics.) The relationship could be a matter of historical connection with the subject or with related matters, or of legal, commercial, philosophical, ethical or political position regarding them. The Invariant Sections are certain Secondary Sections whose titles are designated, as being those of Invariant Sections, in the notice that says that the Document is released under this License. If a section does not fit the above definition of Secondary then it is not allowed to be designated as Invariant. The Document may contain zero Invariant Sections. If the Document does not identify any Invariant Sections then there are none. The Cover Texts are certain short passages of text that are listed, as FrontCover Texts or Back-Cover Texts, in the notice that says that the Document is released under this License. A Front-Cover Text may be at most 5 words, and a Back-Cover Text may be at most 25 words. A Transparent copy of the Document means a machine-readable copy, represented in a format whose specification is available to the general public, that is suitable for revising the document straightforwardly with generic text editors or (for images composed of pixels) generic paint programs or (for drawings) some widely available drawing editor, and that is suitable for input to text formatters or for automatic translation to a variety of formats suitable for input to text formatters. A copy made in an otherwise Transparent file format whose markup, or absence of markup, has been arranged to thwart or discourage subsequent modification by readers is not Transparent. An image format is not Transparent if used for any substantial amount of text. A copy that is not Transparent is called Opaque. Examples of suitable formats for Transparent copies include plain ASCII without markup, Texinfo input format, LaTeX input format, SGML or XML using a publicly available DTD, and standard-conforming simple HTML, PostScript or PDF designed for human modification. Examples of transparent image formats include PNG, XCF and JPG. Opaque formats include proprietary formats that can be read and edited only by proprietary word processors, SGML or XML for which the DTD and/or processing tools are not generally available, and the machine-generated HTML, PostScript or PDF produced by some word processors for output purposes only. The Title Page means, for a printed book, the title page itself, plus such following pages as are needed to hold, legibly, the material this License requires to appear in the title page. For works in formats which do not have any title page as such, Title Page means the text near the most prominent appearance of the workâ&#x20AC;&#x2122;s title, preceding the beginning of the body of the text. The publisher means any person or entity that distributes copies of the Document to the public. A section Entitled XYZ means a named subunit of the Document whose title either is precisely XYZ or contains XYZ in parentheses following text that translates XYZ in another language. (Here XYZ stands for a specific section name 89


mentioned below, such as Acknowledgements, Dedications, Endorsements, or History.) To Preserve the Title of such a section when you modify the Document means that it remains a section Entitled XYZ according to this definition. The Document may include Warranty Disclaimers next to the notice which states that this License applies to the Document. These Warranty Disclaimers are considered to be included by reference in this License, but only as regards disclaiming warranties: any other implication that these Warranty Disclaimers may have is void and has no effect on the meaning of this License.

12.3

Verbatim copying

You may copy and distribute the Document in any medium, either commercially or noncommercially, provided that this License, the copyright notices, and the license notice saying this License applies to the Document are reproduced in all copies, and that you add no other conditions whatsoever to those of this License. You may not use technical measures to obstruct or control the reading or further copying of the copies you make or distribute. However, you may accept compensation in exchange for copies. If you distribute a large enough number of copies you must also follow the conditions in section 3. You may also lend copies, under the same conditions stated above, and you may publicly display copies.

12.4

Copying in quantity

If you publish printed copies (or copies in media that commonly have printed covers) of the Document, numbering more than 100, and the Documentâ&#x20AC;&#x2122;s license notice requires Cover Texts, you must enclose the copies in covers that carry, clearly and legibly, all these Cover Texts: Front-Cover Texts on the front cover, and Back-Cover Texts on the back cover. Both covers must also clearly and legibly identify you as the publisher of these copies. The front cover must present the full title with all words of the title equally prominent and visible. You may add other material on the covers in addition. Copying with changes limited to the covers, as long as they preserve the title of the Document and satisfy these conditions, can be treated as verbatim copying in other respects. If the required texts for either cover are too voluminous to fit legibly, you should put the first ones listed (as many as fit reasonably) on the actual cover, and continue the rest onto adjacent pages. If you publish or distribute Opaque copies of the Document numbering more than 100, you must either include a machine-readable Transparent copy along with each Opaque copy, or state in or with each Opaque copy a computernetwork location from which the general network-using public has access to download using public-standard network protocols a complete Transparent copy of the Document, free of added material. If you use the latter option, you must take reasonably prudent steps, when you begin distribution of Opaque copies in quantity, to ensure that this Transparent copy will remain thus accessible at the stated location until at least one year after the last time you distribute an Opaque copy (directly or through your agents or retailers) of that edition to the public.

90


It is requested, but not required, that you contact the authors of the Document well before redistributing any large number of copies, to give them a chance to provide you with an updated version of the Document.

12.5

Modifications

You may copy and distribute a Modified Version of the Document under the conditions of sections 2 and 3 above, provided that you release the Modified Version under precisely this License, with the Modified Version filling the role of the Document, thus licensing distribution and modification of the Modified Version to whoever possesses a copy of it. In addition, you must do these things in the Modified Version: • Use in the Title Page (and on the covers, if any) a title distinct from that of the Document, and from those of previous versions (which should, if there were any, be listed in the History section of the Document). You may use the same title as a previous version if the original publisher of that version gives permission. • List on the Title Page, as authors, one or more persons or entities responsible for authorship of the modifications in the Modified Version, together with at least five of the principal authors of the Document (all of its principal authors, if it has fewer than five), unless they release you from this requirement. • State on the Title page the name of the publisher of the Modified Version, as the publisher. • Preserve all the copyright notices of the Document. • Add an appropriate copyright notice for your modifications adjacent to the other copyright notices. • Include, immediately after the copyright notices, a license notice giving the public permission to use the Modified Version under the terms of this License, in the form shown in the Addendum below. • Preserve in that license notice the full lists of Invariant Sections and required Cover Texts given in the Document’s license notice. Include an unaltered copy of this License. • Preserve the section Entitled History, Preserve its Title, and add to it an item stating at least the title, year, new authors, and publisher of the Modified Version as given on the Title Page. If there is no section Entitled History in the Document, create one stating the title, year, authors, and publisher of the Document as given on its Title Page, then add an item describing the Modified Version as stated in the previous sentence. • Preserve the network location, if any, given in the Document for public access to a Transparent copy of the Document, and likewise the network locations given in the Document for previous versions it was based on. These may be placed in the History section. You may omit a network

91


location for a work that was published at least four years before the Document itself, or if the original publisher of the version it refers to gives permission. • For any section Entitled Acknowledgements or Dedications, Preserve the Title of the section, and preserve in the section all the substance and tone of each of the contributor acknowledgements and/or dedications given therein. • Preserve all the Invariant Sections of the Document, unaltered in their text and in their titles. Section numbers or the equivalent are not considered part of the section titles. • Delete any section Entitled Endorsements. Such a section may not be included in the Modified version. • Do not retitle any existing section to be Entitled Endorsements or to conflict in title with any Invariant Section. • Preserve any Warranty Disclaimers. If the Modified Version includes new front-matter sections or appendices that qualify as Secondary Sections and contain no material copied from the Document, you may at your option designate some or all of these sections as invariant. To do this, add their titles to the list of Invariant Sections in the Modified Version’s license notice. These titles must be distinct from any other section titles. You may add a section Entitled Endorsements, provided it contains nothing but endorsements of your Modified Version by various parties—for example, statements of peer review or that the text has been approved by an organization as the authoritative definition of a standard. You may add a passage of up to five words as a Front-Cover Text, and a passage of up to 25 words as a Back-Cover Text, to the end of the list of Cover Texts in the Modified Version. Only one passage of Front-Cover Text and one of Back-Cover Text may be added by (or through arrangements made by) any one entity. If the Document already includes a cover text for the same cover, previously added by you or by arrangement made by the same entity you are acting on behalf of, you may not add another; but you may replace the old one, on explicit permission from the previous publisher that added the old one. The author(s) and publisher(s) of the Document do not by this License give permission to use their names for publicity for or to assert or imply endorsement of any Modified Version.

12.6

Combining documents

You may combine the Document with other documents released under this License, under the terms defined in section 4 above for modified versions, provided that you include in the combination all of the Invariant Sections of all of the original documents, unmodified, and list them all as Invariant Sections of your combined work in its license notice, and that you preserve all their Warranty Disclaimers. The combined work need only contain one copy of this License, and multiple

92


identical Invariant Sections may be replaced with a single copy. If there are multiple Invariant Sections with the same name but different contents, make the title of each such section unique by adding at the end of it, in parentheses, the name of the original author or publisher of that section if known, or else a unique number. Make the same adjustment to the section titles in the list of Invariant Sections in the license notice of the combined work. In the combination, you must combine any sections Entitled History in the various original documents, forming one section Entitled History; likewise combine any sections Entitled Acknowledgements, and any sections Entitled Dedications. You must delete all sections Entitled Endorsements.

12.7

Collections of documents

You may make a collection consisting of the Document and other documents released under this License, and replace the individual copies of this License in the various documents with a single copy that is included in the collection, provided that you follow the rules of this License for verbatim copying of each of the documents in all other respects. You may extract a single document from such a collection, and distribute it individually under this License, provided you insert a copy of this License into the extracted document, and follow this License in all other respects regarding verbatim copying of that document.

12.8

Aggregation with independent works

A compilation of the Document or its derivatives with other separate and independent documents or works, in or on a volume of a storage or distribution medium, is called an aggregate if the copyright resulting from the compilation is not used to limit the legal rights of the compilationâ&#x20AC;&#x2122;s users beyond what the individual works permit. When the Document is included in an aggregate, this License does not apply to the other works in the aggregate which are not themselves derivative works of the Document. If the Cover Text requirement of section 3 is applicable to these copies of the Document, then if the Document is less than one half of the entire aggregate, the Documentâ&#x20AC;&#x2122;s Cover Texts may be placed on covers that bracket the Document within the aggregate, or the electronic equivalent of covers if the Document is in electronic form. Otherwise they must appear on printed covers that bracket the whole aggregate.

12.9

Translation

Translation is considered a kind of modification, so you may distribute translations of the Document under the terms of section 4. Replacing Invariant Sections with translations requires special permission from their copyright holders, but you may include translations of some or all Invariant Sections in addition to the original versions of these Invariant Sections. You may include a translation 93


of this License, and all the license notices in the Document, and any Warranty Disclaimers, provided that you also include the original English version of this License and the original versions of those notices and disclaimers. In case of a disagreement between the translation and the original version of this License or a notice or disclaimer, the original version will prevail. If a section in the Document is Entitled Acknowledgements, Dedications, or History, the requirement (section 4) to Preserve its Title (section 1) will typically require changing the actual title.

12.10

Termination

You may not copy, modify, sublicense, or distribute the Document except as expressly provided under this License. Any attempt otherwise to copy, modify, sublicense, or distribute it is void, and will automatically terminate your rights under this License. However, if you cease all violation of this License, then your license from a particular copyright holder is reinstated (a) provisionally, unless and until the copyright holder explicitly and finally terminates your license, and (b) permanently, if the copyright holder fails to notify you of the violation by some reasonable means prior to 60 days after the cessation. Moreover, your license from a particular copyright holder is reinstated permanently if the copyright holder notifies you of the violation by some reasonable means, this is the first time you have received notice of violation of this License (for any work) from that copyright holder, and you cure the violation prior to 30 days after your receipt of the notice. Termination of your rights under this section does not terminate the licenses of parties who have received copies or rights from you under this License. If your rights have been terminated and not permanently reinstated, receipt of a copy of some or all of the same material does not give you any rights to use it.

12.11

Future revision of this license

The Free Software Foundation may publish new, revised versions of the GNU Free Documentation License from time to time. Such new versions will be similar in spirit to the present version, but may differ in detail to address new problems or concerns. See http://www.gnu.org/copyleft/. Each version of the License is given a distinguishing version number. If the Document specifies that a particular numbered version of this License or any later version applies to it, you have the option of following the terms and conditions either of that specified version or of any later version that has been published (not as a draft) by the Free Software Foundation. If the Document does not specify a version number of this License, you may choose any version ever published (not as a draft) by the Free Software Foundation. If the Document specifies that a proxy can decide which future versions of this License can be used, that proxyâ&#x20AC;&#x2122;s public statement of acceptance of a version permanently authorizes you to choose that version for the Document.

94


12.12

Relicensing

Massive Multiauthor Collaboration Site (or MMC Site) means any World Wide Web server that publishes copyrightable works and also provides prominent facilities for anybody to edit those works. A public wiki that anybody can edit is an example of such a server. A Massive Multiauthor Collaboration (or MMC) contained in the site means any set of copyrightable works thus published on the MMC site. CC-BY-SA means the Creative Commons Attribution-Share Alike 3.0 license published by Creative Commons Corporation, a not-for-profit corporation with a principal place of business in San Francisco, California, as well as future copyleft versions of that license published by that same organization. Incorporate means to publish or republish a Document, in whole or in part, as part of another Document. An MMC is eligible for relicensing if it is licensed under this License, and if all works that were first published under this License somewhere other than this MMC, and subsequently incorporated in whole or in part into the MMC, (1) had no cover texts or invariant sections, and (2) were thus incorporated prior to November 1, 2008. The operator of an MMC Site may republish an MMC contained in the site under CC-BY-SA on the same site at any time before August 1, 2009, provided the MMC is eligible for relicensing.

95


Linguaggio C