94
Parte lxxxv C 532 Linguaggio C: introduzione ................................................ 49 532.1 Struttura fondamentale ................................................ 49 532.2 Ciao mondo! ......................................................... 50 532.3 Variabili e tipi ........................................................ 52 532.4 Operatori ed espressioni ............................................... 58 532.5 Strutture di controllo di flusso ......................................... 64 532.6 Funzioni ............................................................. 69 532.7 Struttura e campo di azione ............................................ 71 532.8 I/O elementare ....................................................... 73 532.9 Restituzione di un valore .............................................. 76 532.10 Suddivisione dei sorgenti e compilazione .............................. 76 532.11 Riferimenti ......................................................... 80 533 C: puntatori, array e stringhe ................................................. 81 533.1 Puntatori ............................................................ 81 533.2 Array ............................................................... 83 533.3 Stringhe ............................................................. 88 533.4 Puntatori e funzioni ................................................... 91 534 C: tipi di dati derivati ....................................................... 92 534.1 Strutture e unioni ..................................................... 92 535 C: oggetti dinamici e aritmetica dei puntatori .................................. 97 535.1 Oggetti dinamici ..................................................... 97 535.2 Aritmetica dei puntatori ............................................... 99 536 C: file .................................................................... 100 536.1 Macro «FILE» come tipo di dati ...................................... 100 536.2 Apertura e chiusura .................................................. 100 536.3 Lettura e scrittura ................................................... 102 536.4 Indicatore interno al file .............................................. 105 536.5 File di testo ......................................................... 107 536.6 I/O standard ........................................................ 108 537 C: istruzioni del preprocessore .............................................. 109 537.1 Linguaggio a sé stante ............................................... 109 47

Parte lxxxv C · ... puntatori, array e stringhe ... oggetti dinamici e aritmetica dei puntatori ... 538.1 Problemi elementari di programmazione

  • Upload
    lekien

  • View
    216

  • Download
    0

Embed Size (px)

Citation preview

Parte lxxxv

C532 Linguaggio C: introduzione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

532.1 Struttura fondamentale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 49

532.2 Ciao mondo! . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 50

532.3 Variabili e tipi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 52

532.4 Operatori ed espressioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 58

532.5 Strutture di controllo di flusso . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 64

532.6 Funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 69

532.7 Struttura e campo di azione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 71

532.8 I/O elementare . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 73

532.9 Restituzione di un valore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

532.10 Suddivisione dei sorgenti e compilazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 76

532.11 Riferimenti . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 80

533 C: puntatori, array e stringhe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

533.1 Puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 81

533.2 Array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 83

533.3 Stringhe . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 88

533.4 Puntatori e funzioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 91

534 C: tipi di dati derivati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92

534.1 Strutture e unioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 92

535 C: oggetti dinamici e aritmetica dei puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

535.1 Oggetti dinamici . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 97

535.2 Aritmetica dei puntatori . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 99

536 C: file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

536.1 Macro «FILE» come tipo di dati . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . .100

536.2 Apertura e chiusura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 100

536.3 Lettura e scrittura . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 102

536.4 Indicatore interno al file . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 105

536.5 File di testo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 107

536.6 I/O standard . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 108

537 C: istruzioni del preprocessore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

537.1 Linguaggio a sé stante . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 109

47

537.2 Macro predefinite . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 113

538 C: esempi di programmazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

538.1 Problemi elementari di programmazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

538.2 Scansione di array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

538.3 Algoritmi tradizionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

539 Automazione della compilazione: Make e file-make . . . . . . . . . . . . . . . . . . . . . . . . . . 136

539.1 Make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

539.2 File-make . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 136

539.3 Regole deduttive . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

539.4 File-make tipico . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 139

48

49Capitolo 532

Linguaggio C: introduzioneIl linguaggio C è il fondamento dei sistemi Unix. Un minimo di conoscenza di questolinguaggio è importante per sapersi districare tra i programmi distribuiti in forma sorgente.

Il linguaggio C richiede la presenza di un compilatore per generare un file eseguibile (o in-terpretabile) dal kernel. Se si dispone dei cosiddetti «strumenti di sviluppo», intendendo conquesto ciò che serve a ricompilare il kernel, si dovrebbe disporre di tutto quello che è necessarioper provare gli esempi di questi capitoli.

532.1 Struttura fondamentale

Il contenuto di un sorgente in linguaggio C può essere suddiviso in tre parti: commenti, di-rettive del preprocessore e istruzioni C. I commenti vanno aperti e chiusi attraverso l’uso deisimboli ‘/*’ e ‘*/’.

532.1.1 Direttive del preprocessore

Le direttive del preprocessore rappresentano un linguaggio che guida alla compilazione delcodice vero e proprio. L’uso più comune di queste direttive viene fatto per includere porzionidi codice sorgente esterne al file. È importante fare attenzione a non confondersi, dal momentoche tali istruzioni iniziano con il simbolo ‘#’: non si tratta di commenti.

Il programma C tipico richiede l’inclusione di codice esterno composto da file che terminanocon l’estensione ‘.h’. La libreria che viene inclusa più frequentemente è quella necessaria allagestione dei flussi di standard input, standard output e standard error; si dichiara il suo utilizzonel modo seguente:

|#include <stdio.h>

532.1.2 Istruzioni C

Le istruzioni C terminano con un punto e virgola (‘;’) e i raggruppamenti di queste si fannoutilizzando le parentesi graffe (‘{ }’).

||istruzione;

|

||{istruzione; istruzione; istruzione; }

|

Generalmente, un’istruzione può essere interrotta e ripresa nella riga successiva, dal momentoche la sua conclusione è dichiarata chiaramente dal punto e virgola finale. L’istruzione nullaviene rappresentata utilizzando un punto e virgola da solo.

50 volume XX Programmare 1

532.1.3 Nomi

I nomi scelti per identificare ciò che si utilizza all’interno del programma devono seguire regoledeterminate, definite dal compilatore C a disposizione. Per cercare di scrivere codice portabilein altre piattaforme, conviene evitare di sfruttare caratteristiche speciali del proprio ambiente.In particolare:

• un nome può iniziare con una lettera alfabetica e continuare con altre lettere, cifrenumeriche e il trattino basso;

• in teoria i nomi potrebbero iniziare anche con il trattino basso, ma questo è sconsigliabile;

• i nomi sono sensibili alla differenza tra lettere maiuscole e minuscole.

La lunghezza dei nomi può essere un elemento critico; generalmente la dimensione massimadovrebbe essere di 32 caratteri, ma ci sono versioni di C che ne possono accettare solo unaquantità inferiore. In particolare, C GNU ne accetta molti di più di 32. In ogni caso, il compi-latore non rifiuta i nomi troppo lunghi, semplicemente non ne distingue più la differenza oltreun certo punto.

532.1.4 Funzione principale

Il codice di un programma C è scomposto in funzioni, dove l’esecuzione del programma cor-risponde alla chiamata della funzione ‘main()’. Questa funzione può essere dichiarata senzaargomenti oppure con due argomenti precisi: ‘int main (int argc, char *argv[])’.

532.2 Ciao mondo!

Come sempre, il modo migliore per introdurre a un linguaggio di programmazione è di pro-porre un esempio banale, ma funzionante. Al solito si tratta del programma che emette unmessaggio e poi termina la sua esecuzione.

|/*| * Ciao mondo!

| */

||#include <stdio.h>

||/* La funzione main() viene eseguita automaticamente all’avvio. */

|int main ()

|{| /* Si limita a emettere un messaggio. */

| printf ("Ciao mondo!\n");

|}

Nel programma sono state inserite alcune righe di commento. In particolare, all’inizio, l’a-sterisco che si trova nella seconda riga ha soltanto un significato estetico, per guidare la vistaverso la conclusione del commento stesso.

Il programma si limita a emettere la stringa «Ciao Mondo!» seguita da un codice di interruzionedi riga, rappresentato dal simbolo ‘\n’.

Linguaggio C: introduzione 51

532.2.1 Compilazione

Per compilare un programma scritto in C si utilizza generalmente il comando ‘cc’, anche sedi solito si tratta di un collegamento simbolico al vero compilatore che si ha a disposizione.Supponendo di avere salvato il file dell’esempio con il nome ‘ciao.c’, il comando per la suacompilazione è il seguente:

$ cc ciao.c [ Invio ]

Quello che si ottiene è il file ‘a.out’ che dovrebbe già avere i permessi di esecuzione.

$ ./a.out [ Invio ]

|Ciao mondo!

Se si desidera compilare il programma definendo un nome diverso per il codice eseguibilefinale, si può utilizzare l’opzione standard ‘-o’.

$ cc -o ciao ciao.c [ Invio ]

Con questo comando, si ottiene l’eseguibile ‘ciao’.

$ ./ciao [ Invio ]

|Ciao mondo!

532.2.2 Emissione dati attraverso «printf()»

L’esempio di programma presentato sopra si avvale di ‘printf()’ per emettere il messaggioattraverso lo standard output. Questa funzione è più sofisticata di quanto possa apparire dal-l’esempio, in quanto permette di formattare il risultato da emettere. Negli esempi più semplicidi codice C appare immancabilmente questa funzione, per cui è necessario descrivere subito,almeno in parte, il suo funzionamento.

||int printf (stringa_di_formato [, espressione]...)

|

‘printf()’ emette attraverso lo standard output la stringa indicata come primo parametro,dopo averla rielaborata in base alla presenza di metavariabili riferite alle eventuali espressioniche compongono i parametri successivi. Restituisce il numero di caratteri emessi.

L’utilizzo più semplice di ‘printf()’ è quello che è già stato visto, cioè l’emissione di unasemplice stringa senza metavariabili (il codice ‘\n’ rappresenta un carattere preciso e non èuna metavariabile, piuttosto si tratta di una cosiddetta sequenza di escape).

| printf ("Ciao mondo!\n");

La stringa può contenere delle metavariabili del tipo ‘%d’, ‘%c’, ‘%f’,... e queste fannoordinatamente riferimento ai parametri successivi. Per esempio,

| printf ("Totale fatturato: %d\n", 12345);

fa in modo che la stringa incorpori il valore indicato come secondo parametro, nella posizionein cui appare ‘%d’. La metavariabile ‘%d’ stabilisce anche che il valore in questione deve

52 volume XX Programmare 1

essere trasformato secondo una rappresentazione decimale intera. Per cui, il risultato divieneesattamente quello che ci si aspetta.

|Totale fatturato: 12345

532.3 Variabili e tipi

I tipi di dati elementari gestiti dal linguaggio C dipendono molto dall’architettura dell’elabo-ratore sottostante. In questo senso, volendo fare un discorso generale, è difficile definire ladimensione delle variabili numeriche; si può solo dare delle definizioni relative. Solitamente,il riferimento è dato dal tipo numerico intero (‘int’) la cui dimensione in bit è data dalla di-mensione della parola, ovvero dalla capacità dell’unità aritmetico-logica del microprocessore.In pratica, con l’architettura x86 a 32 bit, la dimensione di un intero normale è di 32 bit.

532.3.1 Tipi primitivi

I tipi di dati primitivi rappresentano un valore numerico singolo, nel senso che anche il tipo‘char’ può essere trattato come un numero. Il loro elenco essenziale si trova nella tabella532.8.

|Tabella 532.8. Elenco dei tipi di dati primitivi elementari in C.

Tipo Descrizione

|char Carattere (generalmente di 8 bit).

|int Intero normale.

|float Virgola mobile a singola precisione.

|double Virgola mobile a doppia precisione.

Come già accennato, non si può stabilire in modo generale quali siano le dimensioni esatte inbit dei vari tipi di dati, si può solo stabilire una relazione tra loro.

Questi tipi primitivi possono essere estesi attraverso l’uso di alcuni qualificatori: ‘short’,‘long’ e ‘unsigned’. I primi due si riferiscono alla dimensione, mentre l’ultimo modifica ilmodo di valutare il contenuto di alcune variabili. La tabella 532.10 riassume i vari tipi primitivicon le combinazioni dei qualificatori.

|Tabella 532.10. Elenco dei tipi di dati primitivi in C assieme ai qualificatori.

Tipo Abbreviazione Descrizione

|char

|unsigned char Tipo ‘char’ usato numericamente senza segno.

|short int |short Intero più breve di ‘int’.

Linguaggio C: introduzione 53

Tipo Abbreviazione Descrizione

|unsigned short int |unsigned short Tipo ‘short’ senza segno.

|int Intero normale.

|unsigned int |unsigned Tipo ‘int’ senza segno.

|long int |long Intero più lungo di ‘int’.

|unsigned long int |unsigned long Tipo ‘long’ senza segno.

|float

|double

|long double Tipo a virgola mobile più lungo di ‘double’.

Così, il problema di stabilire le relazioni di dimensione si complica:

I tipi ‘long’ e ‘float’ potrebbero avere una dimensione uguale, altrimenti non è detto qualedei due sia più grande.

Il programma seguente, potrebbe essere utile per determinare la dimensione dei vari tipiprimitivi nella propria piattaforma.1

|/* dimensione_variabili */

||#include <stdio.h>

||int main ()

|{| printf ("char %d\n", (int)sizeof(char));

| printf ("short %d\n", (int)sizeof(short));

| printf ("int %d\n", (int)sizeof(int));

| printf ("long %d\n", (int)sizeof(long));

| printf ("float %d\n", (int)sizeof(float));

| printf ("double %d\n", (int)sizeof(double));

| printf ("long double %d\n", (int)sizeof(long double));

|}

Il risultato potrebbe essere quello seguente:

54 volume XX Programmare 1

|char 1

|short 2

|int 4

|long 4

|float 4

|double 8

|long double 12

I numeri rappresentano la quantità di caratteri, nel senso di valori ‘char’, per cui il tipo ‘char’dovrebbe sempre avere una dimensione unitaria.

I tipi primitivi di variabili mostrati sono tutti utili alla memorizzazione di valori numerici, avario titolo. A seconda che il valore in questione sia trattato con segno o senza segno, varia lospettro di valori che possono essere contenuti.

Nel caso di interi (‘char’, ‘short’, ‘int’ e ‘long’), la variabile può essere utilizzata per tuttala sua estensione a contenere un numero binario. In pratica, il massimo valore ottenibile è(2**n)−1, dove n rappresenta il numero di bit a disposizione. Quando invece si vuole trattareil dato come un numero con segno, il valore numerico massimo ottenibile è circa la metà.

Nel caso di variabili a virgola mobile, non c’è più la possibilità di rappresentare esclusivamentevalori senza segno; inoltre non c’è più un limite di dimensione, ma solo di approssimazione.

Le variabili ‘char’ sono fatte, in linea di principio, per contenere il codice di rappresentazionedi un carattere, secondo la codifica utilizzata nel sistema. Generalmente si tratta di un dato di8 bit (1 byte), ma non è detto che debba sempre essere così. A ogni modo, il fatto che questavariabile possa essere gestita in modo numerico, permette una facile conversione da lettera acodice numerico corrispondente.

Un tipo di valore che non è stato ancora visto è quello logico: Vero è rappresentato da unqualsiasi valore numerico diverso da zero, mentre Falso corrisponde a zero.

532.3.2 Costanti letterali

Quasi tutti i tipi di dati primitivi, hanno la possibilità di essere rappresentati in forma di costanteletterale. In particolare, si distingue tra:

• costanti carattere, rappresentate da un carattere alfanumerico racchiuso tra apici singoli,come ‘’A’’, ‘’B’’,...;

• costanti intere, rappresentate da un numero senza decimali, e a seconda delle dimensionipuò trattarsi di uno dei vari tipi di interi (escluso ‘char’);

• costanti con virgola, rappresentate da un numero con decimali (un punto seguito da altrecifre, anche se si tratta solo di zeri), che indipendentemente dalle dimensioni sono sempredi un tipo ‘double’.

Per esempio, 123 è generalmente una costante ‘int’, mentre 123.0 è una costante ‘double’.

Per quanto riguarda le costanti che rappresentano numeri con virgola, si può usare anche lanotazione scientifica. Per esempio, ‘7e+15’ rappresenta l’equivalente di 7 * (1015), cioè unsette con 15 zeri. Nello stesso modo, ‘7e-5’, rappresenta l’equivalente di 7 * (10−5), cioè0,00007.

Linguaggio C: introduzione 55

È possibile rappresentare anche le stringhe in forma di costante attraverso l’uso degli apicidoppi, ma la stringa non è un tipo di dati primitivo, trattandosi piuttosto di un array di caratteri.Per il momento è importante fare attenzione a non confondere il tipo ‘char’ con la stringa. Peresempio, ‘’F’’ è un carattere, mentre ‘"F"’ è una stringa, ma la differenza è notevole. Lestringhe vengono descritte meglio in seguito.

È stato affermato che si possono rappresentare i caratteri singoli in forma di costante, utiliz-zando gli apici singoli come delimitatore, e che per rappresentare una stringa si usano invecegli apici doppi. Alcuni caratteri non hanno una rappresentazione grafica e non possono essereinseriti attraverso la tastiera.

In questi casi, si possono usare tre tipi di notazione: ottale, esadecimale e simbolica. In tutti icasi si utilizza la barra obliqua inversa (‘\’) come carattere di escape, cioè come simbolo perannunciare che ciò che segue immediatamente deve essere interpretato in modo particolare.

La notazione ottale usa la forma ‘\ooo’, dove ogni lettera o rappresenta una cifra ottale. Aquesto proposito, è opportuno notare che se la dimensione di un carattere fosse superiore aifatidici 8 bit, occorrerebbero probabilmente più cifre (una cifra ottale rappresenta un gruppodi 3 bit).

La notazione esadecimale usa la forma ‘\xhh’, dove h rappresenta una cifra esadecimale.Anche in questo caso vale la considerazione per cui ci vogliono più di due cifre esadecimaliper rappresentare un carattere più lungo di 8 bit.

Dovrebbe essere logico, ma è il caso di osservare che la corrispondenza dei caratteri con irispettivi codici numerici dipende dalla codifica utilizzata. Generalmente si utilizza la codificaASCII, riportata anche nella sezione 417.1.

La notazione simbolica permette di fare riferimento facilmente a codici di uso comune, quali<CR>, <HT>,... Inoltre, questa notazione permette anche di indicare caratteri che altrimentiverrebbero interpretati in maniera differente dal compilatore. La tabella 532.14 riporta i varitipi di rappresentazione delle costanti carattere attraverso codici di escape.

|Tabella 532.14. Elenco dei modi di rappresentazione delle costanti carattereattraverso codici di escape.

Codice di escape Descrizione

|\ooo Notazione ottale.

|\xhh Notazione esadecimale.

|\\ Una singola barra obliqua inversa (‘\’).

|\’ Un apice singolo destro.

|\" Un apice doppio.

|\0 Il codice <NUL>.

|\a Il codice <BEL> (bell).

|\b Il codice <BS> (backspace).

|\f Il codice <FF> (formfeed).

56 volume XX Programmare 1

Codice di escape Descrizione

|\n Il codice <LF> (linefeed).

|\r Il codice <CR> (carriage return).

|\t Una tabulazione orizzontale (<HT>).

|\v Una tabulazione verticale (<VT>).

Nell’esempio introduttivo, è già stato visto l’uso della notazione ‘\n’ per rappresentarel’inserzione di un codice di interruzione di riga alla fine del messaggio di saluto.

| printf ("Ciao mondo!\n");

Senza di questo, il cursore resterebbe a destra del messaggio alla fine dell’esecuzione di quelprogramma, ponendo lì l’invito.

532.3.3 Campo di azione delle variabili

Il campo di azione delle variabili in C viene determinato dalla posizione in cui queste vengo-no dichiarate e dall’uso di particolari qualificatori. Per il momento basti tenere presente chequanto dichiarato all’interno di una funzione ha valore locale per la funzione stessa, mentrequanto dichiarato al di fuori, ha valore globale per tutto il file.

532.3.4 Dichiarazione delle variabili

La dichiarazione di una variabile avviene specificando il tipo e il nome della variabile, comenell’esempio seguente dove si dichiara la variabile ‘numero’ di tipo intero.

|int numero;

La variabile può anche essere inizializzata contestualmente, assegnandogli un valore, comenell’esempio seguente in cui viene dichiarata la stessa variabile ‘numero’ con il valore inizialedi 1000.

|int numero = 1000;

Una costante è qualcosa che non varia e generalmente si rappresenta attraverso una notazioneche ne definisce il valore. Tuttavia, a volte può essere più comodo definire una costante inmodo simbolico, come se fosse una variabile, per facilitarne l’utilizzo e la sua identificazioneall’interno del programma. Si ottiene questo con il modificatore ‘const’. Ovviamente, èobbligatorio inizializzala contestualmente alla sua dichiarazione. L’esempio seguente dichiarala costante simbolica ‘pi’ con il valore del P-greco.

|const float pi = 3.14159265;

Le costanti simboliche di questo tipo, sono delle variabili per le quali il compilatore nonconcede che avvengano delle modifiche.

È il caso di osservare, tuttavia, che l’uso di costanti simboliche di questo tipo è piuttostolimitato. Generalmente è preferibile utilizzare delle macro definite e gestite attraverso ilpreprocessore. L’utilizzo di queste viene descritto più avanti.

Linguaggio C: introduzione 57

Una caratteristica fondamentale del linguaggio C è quella di permettere di fare qualsiasi ope-razione con qualsiasi tipo di dati. In pratica, per esempio, il compilatore non si oppone difronte all’assegnamento di un valore numerico a una variabile ‘char’ o all’assegnamento diun carattere a un intero. Però ci possono essere situazioni in cui cose del genere accadonoaccidentalmente e il modo migliore per evitarlo è quello di usare una convenzione nella defini-zione dei nomi delle variabili, in modo da distinguerne il tipo. A puro titolo di esempio vieneproposto il metodo seguente, che non fa parte però di uno standard accettato universalmente.

Si possono comporre i nomi delle variabili utilizzando un prefisso composto da una o piùlettere minuscole che serve a descriverne il tipo. Nella parte restante si possono usare inizialimaiuscole per staccare visivamente i nomi composti da più parole significative.

Per esempio, ‘iLivello’ potrebbe essere la variabile di tipo ‘int’ che contiene il livello diqualcosa. Nello stesso modo, ‘ldIndiceConsumo’ potrebbe essere una variabile di tipo ‘longdouble’ che rappresenta l’indice del consumo di qualcosa.2

In questa fase non sono ancora stati mostrati tutti i tipi di dati che si possono gestire effetti-vamente; tuttavia, per completezza, viene mostrata la tabella 532.19 con tutti questi prefissiproposti.

|Tabella 532.19. Convenzione proposta per i nomi delle variabili.

Prefisso Tipo corrispondente

|c ‘char’

|uc ‘unsigned char’

|si ‘short int’

|usi ‘unsigned short int’

|i ‘int’

|ui ‘unsigned int’

|li ‘long int’

|uli ‘unsigned long int’

|f ‘float’

|d ‘double’

|ld ‘long double’

|a array

|ac array di ‘char’, o stringa

|auc array di ‘unsigned char’

|a... array di ...

|acz stringa terminata con ‘\0’

58 volume XX Programmare 1

Prefisso Tipo corrispondente

|t ‘struct’

|u ‘union’

|p puntatore

|pc puntatore a ‘char’

|puc puntatore a ‘unsigned char’

|p... puntatore a ...

|e enumerazione

532.4 Operatori ed espressioni

L’operatore è qualcosa che esegue un qualche tipo di funzione, su uno o due operandi, resti-tuendo un valore. Il valore restituito è di tipo diverso a seconda degli operandi utilizzati. Peresempio, la somma di due interi genera un risultato intero. Gli operandi descritti di seguitosono quelli più comuni e importanti.

532.4.1 Operatori aritmetici

Gli operatori che intervengono su valori numerici sono elencati nella tabella 532.20.

|Tabella 532.20. Elenco degli operatori aritmetici e di quelli di assegnamento relativia valori numerici.

Operatore eoperandi Descrizione

|++op Incrementa di un’unità l’operando prima che venga restituito il suo valore.

|op++ Incrementa di un’unità l’operando dopo averne restituito il suo valore.

|--op Decrementa di un’unità l’operando prima che venga restituito il suo valore.

|op-- Decrementa di un’unità l’operando dopo averne restituito il suo valore.

|+op Non ha alcun effetto.

|-op Inverte il segno dell’operando.

|op1 + op2 Somma i due operandi.

|op1 - op2 Sottrae dal primo il secondo operando.

|op1 * op2 Moltiplica i due operandi.

|op1 / op2 Divide il primo operando per il secondo.

Linguaggio C: introduzione 59

Operatore eoperandi Descrizione

|op1 % op2 Modulo: il resto della divisione tra il primo e il secondo operando.

|var = valore Assegna alla variabile il valore alla destra.

|op1 += op2 |op1 = op1 + op2

|op1 -= op2 |op1 = op1 - op2

|op1 *= op2 |op1 = op1 * op2

|op1 /= op2 |op1 = op1 / op2

|op1 %= op2 |op1 = op1 % op2

532.4.2 Operatori di confronto e operatori logici

Gli operatori di confronto determinano la relazione tra due operandi. Il risultato dell’espressio-ne composta da due operandi posti a confronto è di tipo booleano, rappresentabile in C come!0, o non-zero (Vero), e zero (Falso). È importante sottolineare che qualunque valore diversoda zero, equivale a Vero in un contesto logico. Gli operatori di confronto sono elencati nellatabella 532.21.

|Tabella 532.21. Elenco degli operatori di confronto. Le metavariabili indicaterappresentano gli operandi e la loro posizione.

Operatore eoperandi Descrizione

|op1 == op2 Vero se gli operandi si equivalgono.

|op1 != op2 Vero se gli operandi sono differenti.

|op1 < op2 Vero se il primo operando è minore del secondo.

|op1 > op2 Vero se il primo operando è maggiore del secondo.

|op1 <= op2 Vero se il primo operando è minore o uguale al secondo.

|op1 >= op2 Vero se il primo operando è maggiore o uguale al secondo.

Quando si vogliono combinare assieme diverse espressioni logiche, comprendendo in questeanche delle variabili che contengono un valore booleano, si utilizzano gli operatori logici (notinormalmente come: AND, OR, NOT, ecc.). Il risultato di un’espressione logica complessa èquello dell’ultima espressione elementare a essere valutata. Gli operatori logici sono elencatinella tabella 532.22.

60 volume XX Programmare 1

|Tabella 532.22. Elenco degli operatori logici. Le metavariabili indicaterappresentano gli operandi e la loro posizione.

Operatore eoperandi Descrizione

|! op Inverte il risultato logico dell’operando.

|op1 && op2 Se il risultato del primo operando è Falso non valuta il secondo.

|op1 || op2 Se il risultato del primo operando è Vero non valuta il secondo.

Un tipo particolare di operatore logico è l’operatore condizionale, che permette di eseguireespressioni diverse in relazione al risultato di una condizione. La sua sintassi si esprime nelmodo seguente:

||condizione ? espressione1 : espressione2

|

In pratica, se l’espressione che rappresenta la condizione si avvera, viene eseguita la primaespressione che segue il punto interrogativo, altrimenti viene eseguita quella che segue i duepunti.

532.4.3 Operatori binari

In C, così come non esiste il tipo di dati booleano, non esiste nemmeno la possibilità di gestirevariabili composte da un bit singolo. A questo problema si fa fronte attraverso l’utilizzo dei tipidi dati esistenti in modo binario. Sono disponibili le operazioni elencate nella tabella 532.23.

|Tabella 532.23. Elenco degli operatori binari. Le metavariabili indicaterappresentano gli operandi e la loro posizione.

Operatore eoperandi Descrizione

|op1 & op2 AND bit per bit.

|op1 | op2 OR bit per bit.

|op1 ^ op2 XOR bit per bit (OR esclusivo).

|op1 << op2 Spostamento a sinistra di op2 bit.

|op1 >> op2 Spostamento a destra di op2 bit.

|~op1 Complemento a uno.

|op1 &= op2 |op1 = op1 & op2

|op1 |= op2 |op1 = op1 | op2

|op1 ^= op2 |op1 = op1 ^ op2

Linguaggio C: introduzione 61

Operatore eoperandi Descrizione

|op1 <<= op2 |op1 = op1 << op2

|op1 >>= op2 |op1 = op1 >> op2

|op1 ~= op2 |op1 = ~op2

In particolare, lo spostamento può avere effetti differenti a seconda che venga utilizzato suuna variabile senza segno o con segno, dove in questo ultimo caso si possono ottenere risultatidiversi su piattaforme differenti. Per questo, viene mostrato solo l’esempio dello spostamentosu variabili senza segno.

Per aiutare a comprendere il meccanismo vengono mostrati alcuni esempi. In particolare siutilizzano due operandi di tipo ‘char’ (a 8 bit) senza segno:

• ‘a’, contenente il valore 42, pari a 001010102;

• ‘b’, contenente il valore 51, pari a 001100112.

AND

|c = a & b

‘c’ contiene il valore 34, come mostrato dallo schema seguente:

OR

|c = a | b

‘c’ contiene il valore 59, come mostrato dallo schema seguente:

XOR

|c = a ^ b

62 volume XX Programmare 1

‘c’ contiene il valore 25, come mostrato dallo schema seguente:

Spostamento a sinistra

|c = a << 1

‘c’ contiene il valore 84, come mostrato dallo schema seguente:

In pratica si è ottenuto un raddoppio.

Spostamento a destra

|c = a >> 1

‘c’ contiene il valore 21, come mostrato dallo schema seguente:

In pratica si è ottenuto un dimezzamento.

Complemento a uno

|c = ~a

‘c’ contiene il valore 213, corrispondente all’inversione dei bit di ‘a’.

Linguaggio C: introduzione 63

532.4.4 Conversione di tipo

Quando si assegna un valore a una variabile, nella maggior parte dei casi, il contesto stabilisceil tipo di questo valore in modo corretto. Di fatto, è il tipo della variabile ricevente che sta-bilisce la conversione necessaria. Tuttavia, il problema si può porre durante la valutazione diun’espressione.

Per esempio, ‘5/4’ viene considerata la divisione di due interi e, di conseguenza, l’espressionerestituisce un valore intero, cioè 1. Diverso sarebbe se si scrivesse ‘5.0/4.0’, perché in questocaso si tratterebbe della divisione tra due numeri a virgola mobile (per la precisione, di tipo‘double’) e il risultato è un numero a virgola mobile.

Quando si pone il problema di risolvere l’ambiguità si utilizza esplicitamente la conversionedel tipo, attraverso un cast.

||(tipo) espressione

|

In pratica, si deve indicare tra parentesi il nome del tipo di dati in cui deve essere convertital’espressione che segue. Il problema sta nella precedenza che ha il cast nell’insieme degli altrioperatori e in generale conviene utilizzare altre parentesi per chiarire la relazione che ci deveessere.

|int x = 10;

|long y;

|...

|y = (long)x/9;

In questo caso, la variabile intera ‘x’ viene convertita nel tipo ‘long’ (a virgola mobile) primadi eseguire la divisione. Dal momento che il cast ha precedenza sull’operazione di divisione,non si pongono problemi, inoltre, la divisione avviene trasformando implicitamente il 9 interoin un 9 di tipo ‘long’. In pratica, l’operazione avviene utilizzando valori ‘long’ e restituendoun risultato ‘long’.

532.4.5 Espressioni multiple

Un’istruzione, cioè qualcosa che termina con un punto e virgola, può contenere diverse espres-sioni separate da una virgola. Tenendo presente che in C l’assegnamento di una variabile èanche un’espressione, che restituisce il valore assegnato, si veda l’esempio seguente:

64 volume XX Programmare 1

|int x;

|int y;

|...

|y = 10, x = 20, y = x*2;

L’esempio mostra un’istruzione contenente tre espressioni: la prima assegna a ‘y’ il valore10, la seconda assegna a ‘x’ il valore 20 e la terza sovrascrive ‘y’ assegnandole il risultato delprodotto ‘x*2’. In pratica, alla fine la variabile ‘y’ contiene il valore 40 e ‘x’ contiene 20.

|y = x = 10;

In questo esempio ulteriore, si vede l’assegnamento alla variabile ‘y’ dello stesso valore cheviene assegnato alla variabile ‘x’. In pratica, sia ‘x’ che ‘y’ contengono alla fine il numero 10.

532.5 Strutture di controllo di flusso

Il linguaggio C gestisce praticamente tutte le strutture di controllo di flusso degli altri linguag-gi di programmazione, compreso go-to che comunque è sempre meglio non utilizzare e qui,volutamente, non viene presentato.

Le strutture di controllo permettono di sottoporre l’esecuzione di una parte di codice alla veri-fica di una condizione, oppure permettono di eseguire dei cicli, sempre sotto il controllo di unacondizione. La parte di codice che viene sottoposta a questo controllo, può essere una singolaistruzione, oppure un gruppo di istruzioni. Nel secondo caso, è necessario delimitare questogruppo attraverso l’uso delle parentesi graffe.

Dal momento che è comunque consentito di realizzare un gruppo di istruzioni che in realtà necontiene una sola, probabilmente è meglio utilizzare sempre le parentesi graffe, in modo daevitare equivoci nella lettura del codice. Dato che le parentesi graffe sono usate nel codice C,se queste appaiono nei modelli sintattici indicati, queste fanno parte delle istruzioni e non dellasintassi.

532.5.1 Struttura condizionale: «if»

La struttura condizionale è il sistema di controllo fondamentale dell’andamento del flusso delleistruzioni.

||if (condizione) istruzione

|

||if (condizione) istruzione else istruzione

|

Se la condizione si verifica, viene eseguita l’istruzione o il gruppo di istruzioni che segue; quin-di il controllo passa alle istruzioni successive alla struttura. Se viene utilizzata la sotto-strutturache si articola a partire dalla parola chiave ‘else’, nel caso non si verifichi la condizione, vieneeseguita l’istruzione che ne dipende. Sotto vengono mostrati alcuni esempi.

|int iImporto;

|...

|if (iImporto > 10000000) printf ("L’offerta è vantaggiosa\n");

Linguaggio C: introduzione 65

|int iImporto;

|int iMemorizza;

|...

|if (iImporto > 10000000)

| {

| iMemorizza = iImporto;

| printf ("L’offerta è vantaggiosa\n");

| }

|else| {

| printf ("Lascia perdere\n");

| }

|int iImporto;

|int iMemorizza;

|...

|if (iImporto > 10000000)

| {

| iMemorizza = iImporto;

| printf ("L’offerta è vantaggiosa\n");

| }

|else if (iImporto > 5000000)

| {

| iMemorizza = iImporto;

| printf ("L’offerta è accettabile\n");

| }

|else| {

| printf ("Lascia perdere\n");

| }

532.5.2 Struttura di selezione: «switch»

La struttura di selezione, che si attua con l’istruzione ‘switch’, è un po’ troppo complessa peressere rappresentata facilmente attraverso uno schema sintattico. In generale, questa strutturapermette di eseguire una o più istruzioni in base al risultato di un’espressione. L’esempioseguente mostra la visualizzazione del nome del mese, in base al valore di un intero.

66 volume XX Programmare 1

|int iMese;

|...

|switch (iMese)

| {

| case 1: printf ("gennaio\n"); break;

| case 2: printf ("febbraio\n"); break;

| case 3: printf ("marzo\n"); break;

| case 4: printf ("aprile\n"); break;

| case 5: printf ("maggio\n"); break;

| case 6: printf ("giugno\n"); break;

| case 7: printf ("luglio\n"); break;

| case 8: printf ("agosto\n"); break;

| case 9: printf ("settembre\n"); break;

| case 10: printf ("ottobre\n"); break;

| case 11: printf ("novembre\n"); break;

| case 12: printf ("dicembre\n"); break;

| }

Come si vede, dopo l’istruzione con cui si emette il nome del mese attraverso lo standardoutput, viene richiesta l’interruzione esplicita dell’analisi della struttura, attraverso l’istruzione‘break’, allo scopo di togliere ambiguità al codice, garantendo che sia evitata la verifica deglialtri casi.

Un gruppo di casi può essere raggruppato assieme, quando si vuole che ognuno di questiesegua lo stesso insieme di istruzioni.

|int iAnno;

|int iMese;

|int iGiorni;

|...

|switch (iMese)

| {

| case 1:

| case 3:

| case 5:

| case 7:

| case 8:

| case 10:

| case 12:

| iGiorni = 31;

| break;

| case 4:

| case 6:

| case 9:

| case 11:

| iGiorni = 30;

| break;

| case 2:

| if (((iAnno % 4 == 0) && !(iAnno % 100 == 0)) ||

| (iAnno % 400 == 0))

| iGiorni = 29;

| else

| iGiorni = 28;

Linguaggio C: introduzione 67

| break;

| }

È anche possibile definire un caso predefinito che si verifica quando nessuno degli altri siavvera.

|int iMese;

|...

|switch (iMese)

| {

| case 1: printf ("gennaio\n"); break;

| case 2: printf ("febbraio\n"); break;

| ...

| case 11: printf ("novembre\n"); break;

| case 12: printf ("dicembre\n"); break;

| default: printf ("mese non corretto\n"); break;

| }

532.5.3 Iterazione con condizione di uscita iniziale: «while»

||while (condizione) istruzione

|

L’iterazione si ottiene normalmente in C attraverso l’istruzione ‘while’, che esegue un’istru-zione, o un gruppo di queste, finché la condizione continua a restituire il valore Vero. Lacondizione viene valutata prima di eseguire il gruppo di istruzioni e poi ogni volta che terminaun ciclo, prima dell’esecuzione del successivo.

L’esempio seguente fa apparire per 10 volte la lettera «x».

|int iContatore = 0;

||while (iContatore < 10)

| {| iContatore++;

| printf ("x");

| }|printf ("\n");

Nel blocco di istruzioni di un ciclo ‘while’, ne possono apparire alcune particolari:

• ‘break’, che serve a uscire definitivamente dalla struttura del ciclo;

• ‘continue’, che serve a interrompere l’esecuzione del gruppo di istruzioni, riprendendoimmediatamente con il ciclo successivo (a partire dalla valutazione della condizione).

L’esempio seguente è una variante del calcolo di visualizzazione mostrato sopra, modifica-to in modo da vedere il funzionamento dell’istruzione ‘break’. All’inizio della struttura,‘while (1)’ equivale a stabilire che il ciclo è senza fine, perché la condizione è semprevera. In questo modo, solo la richiesta esplicita di interruzione dell’esecuzione della struttura(attraverso l’istruzione ‘break’) permette l’uscita da questa.

68 volume XX Programmare 1

|int iContatore = 0;

||while (1)

| {| if (iContatore >= 10)

| {| break;

| }| iContatore++;

| printf ("x");

| }|printf ("\n");

532.5.4 Iterazione con condizione di uscita finale: «do-while»

Una variante del ciclo ‘while’, in cui l’analisi della condizione di uscita avviene dopol’esecuzione del blocco di istruzioni che viene iterato, è definito dall’istruzione ‘do’.

||do blocco_di_istruzioni while (condizione);

|

In questo caso, si esegue un gruppo di istruzioni una volta, poi se ne ripete l’esecuzione finchéla condizione restituisce il valore Vero.

532.5.5 Ciclo enumerativo: «for»

In presenza di iterazioni in cui si deve incrementare o decrementare una variabile a ogni ciclo,si usa preferibilmente la struttura ‘for’, che in C permetterebbe un utilizzo più ampio di quellocomune.

||for (espressione1; espressione2; espressione3) istruzione

|

Questa è la forma tipica di un’istruzione ‘for’, in cui la prima espressione corrisponde all’as-segnamento iniziale di una variabile, la seconda a una condizione che deve verificarsi fino a chesi vuole che sia eseguita l’istruzione (o il gruppo di istruzioni) e la terza all’incremento o de-cremento della variabile inizializzata con la prima espressione. In pratica, potrebbe esprimersinella sintassi seguente:

||for (var = n; condizione; var++) istruzione

|

Il ciclo ‘for’ potrebbe essere definito anche in maniera differente, più generale: la primaespressione viene eseguita una volta sola all’inizio del ciclo; la seconda viene valutata all’iniziodi ogni ciclo e il gruppo di istruzioni viene eseguito solo se il risultato è Vero; l’ultima vieneeseguita alla fine dell’esecuzione del gruppo di istruzioni, prima che si ricominci con l’analisidella condizione.

L’esempio già visto, in cui veniva visualizzata per 10 volte una «x», potrebbe tradursi nelmodo seguente, attraverso l’uso di un ciclo ‘for’.

Linguaggio C: introduzione 69

|int iContatore;

||for (iContatore = 0; iContatore < 10; iContatore++)

| {| printf ("x");

| }|printf ("\n");

Anche nelle istruzioni controllate da un ciclo ‘for’ si possono collocare istruzioni ‘break’ e‘continue’, con lo stesso significato visto per il ciclo ‘while’

Sfruttando la possibilità di inserire più espressioni in una singola istruzione, si possono realiz-zare dei cicli ‘for’ molto più complessi, anche se questo è sconsigliabile per evitare di scriverecodice troppo difficile da interpretare. In questo modo, l’esempio precedente potrebbe essereridotto a quello che segue:

|int iContatore;

||for (iContatore = 0; iContatore < 10; printf ("x"), iContatore++)

| {| ;

| }|printf ("\n");

Il punto e virgola solitario rappresenta un’istruzione nulla.

532.6 Funzioni

Il linguaggio C offre le funzioni come mezzo per realizzare la scomposizione del codice insubroutine. Prima di poter essere utilizzate attraverso una chiamata, le funzioni devono esseredichiarate, anche se non necessariamente descritte. In pratica, se si vuole indicare nel codiceuna chiamata a una funzione che viene descritta più avanti, occorre almeno dichiararne ilprototipo.

Le funzioni del linguaggio C prevedono il passaggio di parametri solo per valore, con tipi didati primitivi (compresi i puntatori che sono descritti in un altro capitolo).

Il linguaggio C offre un gran numero di funzioni interne, che vengono importate nel codiceattraverso l’istruzione ‘#include’ del preprocessore. In pratica, in questo modo si importala parte di codice necessaria alla dichiarazione e descrizione di queste funzioni standard. Peresempio, come si è già visto, per poter utilizzare la funzione ‘printf()’ si deve inserire lariga ‘#include <stdio.h>’ nella parte iniziale del file sorgente.

532.6.1 Dichiarazione di un prototipo

||tipo nome ([tipo_parametro[,...]]);

|

Quando la descrizione di una funzione può essere fatta solo dopo l’apparizione di una suachiamata, occorre dichiararne il prototipo all’inizio, secondo la sintassi appena mostrata.

Il tipo, posto all’inizio, rappresenta il tipo di valore che la funzione restituisce. Se la funzionenon deve restituire alcunché, si utilizza il tipo ‘void’. Se la funzione richiede dei parametri,

70 volume XX Programmare 1

il tipo di questi deve essere elencato tra le parentesi tonde. L’istruzione con cui si dichiara ilprototipo termina regolarmente con un punto e virgola.

Lo standard C ANSI stabilisce che una funzione che non richiede parametri deve utilizzarel’identificatore ‘void’ in modo esplicito, all’interno delle parentesi.

Segue la descrizione di alcuni esempi.

• |int fattoriale (int);

In questo caso, viene dichiarato il prototipo della funzione ‘fattoriale’, che richiedeun parametro di tipo ‘int’ e restituisce anche un valore di tipo ‘int’.

• |void elenca ();

Si tratta della dichiarazione di una funzione che fa qualcosa senza bisogno di riceverealcun parametro e senza restituire alcun valore (void).

• |void elenca (void);

Esattamente come nell’esempio precedente, solo che è indicato in modo esplicito il fat-to che la funzione non riceve argomenti (il tipo ‘void’ è stato messo all’interno delleparentesi), come richiede lo standard ANSI.

532.6.2 Descrizione di una funzione

La descrizione della funzione, rispetto alla dichiarazione del prototipo, aggiunge l’indicazionedei nomi da usare per identificare i parametri e naturalmente le istruzioni da eseguire. Leparentesi graffe che appaiono nello schema sintattico fanno parte delle istruzioni necessarie.

||tipo nome ([tipo parametro[,...]]) {istruzione;... }

|

Per esempio, la funzione seguente esegue il prodotto tra i due parametri forniti e ne restituisceil risultato.

|int prodotto (int x, int y)

|{| return x*y;

|}

I parametri indicati tra parentesi, rappresentano una dichiarazione di variabili locali che con-tengono inizialmente i valori usati nella chiamata. Il valore restituito dalla funzione vienedefinito attraverso l’istruzione ‘return’, come si può osservare dall’esempio. Naturalmente,le funzioni di tipo ‘void’, cioè quelle che non devono restituire alcun valore, non hanno questaistruzione.

Le variabili dichiarate all’interno di una funzione, oltre a quelle dichiarate implicitamente co-me mezzo di trasporto dei parametri, sono visibili solo al suo interno, mentre quelle dichiarateal di fuori, dette globali, sono accessibili a tutte le funzioni. Se una variabile locale ha unnome coincidente con quello di una variabile globale, allora, all’interno della funzione, quellavariabile globale non è accessibile.

Linguaggio C: introduzione 71

Le regole da seguire per scrivere programmi chiari e facilmente modificabili, prevedono chesi debba fare in modo di rendere le funzioni indipendenti dalle variabili globali, fornendo lorotutte le informazioni necessarie attraverso i parametri della chiamata. In questo modo diventadel tutto indifferente il fatto che una variabile locale vada a mascherare una variabile globale;inoltre, ciò permette di non dover tenere a mente il ruolo di queste variabili globali.

In pratica, ci sono situazioni in cui può avere senso l’utilizzo di variabili globali per fornireinformazioni alle funzioni, tuttavia occorre giudizio, come in ogni cosa.

532.7 Struttura e campo di azione

Un programma scritto in linguaggio C può essere articolato in diversi file sorgenti, all’internodei quali si può fare riferimento solo a «oggetti» dichiarati preventivamente. Questi oggettisono variabili e funzioni: la loro dichiarazione non corrisponde necessariamente con la lorodescrizione che può essere collocata altrove, nello stesso file o in un altro file sorgente delprogramma.

532.7.1 Funzioni

Quando si vuole fare riferimento a una funzione descritta in un file sorgente differente, o inuna posizione successiva dello stesso file, occorre dichiararne il prototipo in una posizioneprecedente. Se si desidera fare in modo che una funzione sia accessibile solo nel file sorgentein cui viene descritta, occorre definirla come ‘static’.

|static void miafunzione (...)

|{|...

|}

532.7.2 Variabili e classi di memorizzazione

Quando si dichiarano delle variabili, senza specificare alcuna classe di memorizzazione (cioèquando lo si fa normalmente come negli esempi visti fino a questo punto), il loro campo diazione è relativo alla posizione della dichiarazione:

• le variabili dichiarate all’esterno delle funzioni sono globali, cioè accessibili da parte ditutte le funzioni, a partire dal punto in cui vengono dichiarate;

• le variabili dichiarate all’interno delle funzioni sono locali, cioè accessibiliesclusivamente dall’interno della funzione in cui si trovano.

Si distinguono quattro tipi di classi di memorizzazione, a cui corrisponde una parola chiaveper la loro dichiarazione:

• automatica, ‘auto’;

• registro, ‘register’;

• statica, ‘static’;

72 volume XX Programmare 1

• esterna, ‘extern’.

La prima, ‘auto’, è la classe normale: vale in modo predefinito e non occorre indicarla quandosi dichiarano le variabili (variabili automatiche).

Dichiarando una variabile come appartenente alla classe ‘register’, si richiede di utilizzareper questa un registro del microprocessore (ammesso che ciò sia possibile). L’utilizzo di unregistro per una variabile serve a velocizzare l’esecuzione di un programma che deve accederefrequentemente a una certa variabile, ma generalmente questa tecnica è sconsigliabile.

La classe di memorizzazione ‘static’ genera due situazioni distinte, a seconda della posizio-ne in cui viene dichiarata la variabile. Se si tratta di una variabile globale, cioè definita al difuori delle funzioni, risulta accessibile solo all’interno del file sorgente in cui viene descritta.Se invece si tratta di una variabile locale, cioè interna a una funzione, si tratta di una variabileche mantiene il suo valore tra una chiamata e l’altra. In questo senso, una variabile locale stati-ca, richiede generalmente un’inizializzazione all’atto della dichiarazione; tale inizializzazioneavviene una sola volta, all’avvio del programma.

Quando da un file sorgente si vuole accedere a variabili globali dichiarate in modo normalein un altro file, oppure, quando nello stesso file si vuole poter accedere a variabili dichiaratein una posizione più avanzata dello stesso, occorre una sorta di prototipo delle variabili: ladichiarazione ‘extern’. In questo modo si informa esplicitamente il compilatore e il linkerdella presenza di queste.

Segue la descrizione di alcuni esempi.

|int accumula (int iAggiunta)

|{| static int iAccumulo = 0;

| iAccumulo += iAggiunta;

| return iAccumulo;

|}

La funzione appena mostrata si occupa di accumulare un valore e di restituirne il livelloraggiunto a ogni chiamata. Come si può osservare, la variabile statica ‘iAccumulo’viene inizializzata a zero, altrimenti non ci sarebbe modo di cominciare con un valore dipartenza corretto.

|static int iMiaVariabile;

|...

|int miafunzione (...)

|{|...

|}|...

La variabile ‘iMiaVariabile’ è accessibile solo alle funzioni descritte nello stesso filein cui si trova, impedendo l’accesso a questa da parte di funzioni di altri file attraverso ladichiarazione ‘extern’.

Linguaggio C: introduzione 73

|extern int iMiaVariabile;

|...

|int miafunzione (...)

|{| iMiaVariabile = ...

|}|int iMiaVariabile = 123;

|...

In questo esempio, la variabile ‘iMiaVariabile’ è dichiarata formalmente in una posi-zione centrale del file sorgente; per fare in modo che la funzione ‘miafunzione’ possaaccedervi, è stata necessaria la dichiarazione ‘extern’ iniziale.

|extern int iTuaVariabile;

|...

|int miafunzione (...)

|{| iTuaVariabile = ...

|}|...

Questo caso rappresenta la situazione in cui una variabile dichiarata in un altro fi-le sorgente diventa accessibile alle funzioni del file attuale attraverso la dichiarazione‘extern’. Perché ciò possa funzionare, occorre che la variabile ‘iTuaVariabile’ siastata dichiarata in modo normale, senza la parola chiave ‘static’.

532.8 I/O elementare

Con il linguaggio C, l’I/O elementare si ottiene attraverso l’uso di due funzioni fondamentali:‘printf()’ e ‘scanf()’. La prima si occupa di emettere una stringa dopo averla trasformatain base a determinati codici di formattazione; la seconda si occupa di ricevere input (gene-ralmente da tastiera) e di trasformarlo secondo determinati codici di formattazione. Infatti, ilprimo problema che si incontra quando si vogliono emettere informazioni attraverso lo stan-dard output per visualizzarle sullo schermo, sta nella necessità di convertire in qualche modotutti i tipi di dati che non siano già di tipo ‘char’. Dalla parte opposta, quando si inserisce undato che non sia un semplice carattere alfanumerico, occorre una conversione adatta nel tipodi dati corretto.

Per utilizzare queste due funzioni, occorre includere il file di intestazione ‘stdio.h’, come ègià stato visto più volte.

532.8.1 Funzione «printf()»

||int printf (stringa_di_formato[, espressione]...)

|

La funzione ‘printf()’ emette attraverso lo standard output la stringa indicata come primoparametro, dopo averla rielaborata in base alla presenza di metavariabili riferite alle eventualiespressioni che compongono i parametri successivi. Restituisce il numero di caratteri emessi.

74 volume XX Programmare 1

In pratica, se viene fornito a ‘printf()’ un solo parametro di tipo stringa, questa viene emes-sa così com’è, senza trasformazioni. Se invece vengono forniti anche altri parametri, questivengono inclusi nella stringa attraverso una serie di metavariabili inserite nella stringa stessa:in corrispondenza dei punti in cui si trovano tali metavariabili, queste vengono sostituite dalcontenuto dei parametri corrispondenti. Si osservi l’esempio:

|printf ("Il capitale di %d al tasso %f ha fruttato %d", 1000, 0.05, 1050);

Si ottiene la frase seguente:

|Il capitale di 1000 al tasso 0.05 ha fruttato 1050

In pratica, al posto della prima metavariabile ‘%d’ è stato inserito il valore 1000 dopo averloconvertito in modo da essere rappresentato da quattro caratteri (‘’1’’, ‘’0’’, ‘’0’’, ‘’0’’),al posto della seconda metavariabile ‘%f’ è stato inserito il valore 0.05 dopo un’opportunaconversione in caratteri, infine, al posto della terza metavariabile ‘%d’ è stato inserito il valore1050.

La scelta della metavariabile corretta determina il tipo di trasformazione che il parametro cor-rispondente deve ricevere. La tabella 532.60 elenca alcune delle metavariabili utilizzabili. Ènecessario ricordare che per rappresentare il simbolo di percentuale si usa una metavariabilefasulla composta dalla sequenza di due segni percentuali: ‘%%’.

|Tabella 532.60. Alcune metavariabili utilizzabili per la formattazione di stringhe con‘printf()’.

Simbolo Corrispondenza

|%c Un carattere singolo.

|%s Una stringa.

|%d Un intero con segno a base 10.

|%u Un intero senza segno a base 10.

|%o Un intero senza segno in ottale.

|%x Un intero senza segno in esadecimale.

|%e Un numero a virgola mobile, in notazione scientifica.

|%f Un numero a virgola mobile, in notazione decimale fissa.

|%g Un numero a virgola mobile, secondo la notazione di ‘%e’ o ‘%f’.

Le metavariabili possono contenere informazioni aggiuntive tra il simbolo di percentuale e lalettera che definisce il tipo di trasformazione. Si tratta di inserire un simbolo composto daun carattere singolo, seguito eventualmente da informazioni aggiuntive, secondo la sintassiseguente:

||%[simbolo][ampiezza][.precisione][{h|l|L}]tipo

|

Linguaggio C: introduzione 75

Questi simboli sono rappresentati dalla tabella 532.61. In presenza di valori numerici, si puòindicare il numero di cifre decimali intere (ampiezza), aggiungendo eventualmente il numerodi decimali (precisione), se si tratta di rappresentare un numero a virgola mobile. Sempre nelcaso di trasformazioni di valori numerici, è anche possibile specificare il tipo particolare a cuiappartiene il dato immesso, attraverso una lettera: ‘h’, ‘l’ e ‘L’. Queste indicano rispettiva-mente che si tratta di un intero ‘short’, ‘long’ e ‘double’; se manca questa indicazione, siintende che si tratti di un intero normale (‘int’).

|Tabella 532.61. Elenco dei simboli utilizzabili tra il segno di percentuale e la letteradi conversione.

Simbolo Corrispondenzaspazio Il prefisso di un numero positivo è uno spazio.

|+ Il prefisso di un numero positivo è il segno ‘+’.

|- Allinea a sinistra rispetto al campo.

|0 Utilizza zeri, invece di spazi, per allineare a destra.

Nella stringa di formattazione possono apparire anche sequenze di escape come già mostratonella tabella 532.14.

Si veda anche la pagina di manuale printf (3).

532.8.2 Funzione «scanf()»

||int scanf (stringa_di_formato[, puntatore]...)

|

La funzione ‘scanf()’ potrebbe essere definita come l’inverso di ‘printf()’, nel sensoche riceve input dallo standard input interpretandolo opportunamente, secondo le metavaria-bili inserite nella stringa di formattazione (la stringa di formattazione deve contenere solometavariabili).

|printf ("Inserisci l’importo:");

|scanf ("%d", &iImporto);

L’esempio emette la frase seguente e resta in attesa dell’inserimento di un valore numericointero, seguito da [ Invio ].

|Inserisci l’importo:_

Questo valore viene inserito nella variabile ‘iImporto’. Si deve osservare il fatto che i para-metri successivi alla stringa di formattazione sono dei puntatori, per cui, avendo voluto inserireil dato nella variabile ‘iImporto’, questa è stata indicata preceduta dall’operatore ‘&’ in mododa fornire alla funzione l’indirizzo corrispondente.

Con una stessa funzione ‘scanf()’ è possibile inserire dati per diverse variabili, come si puòosservare dall’esempio seguente, ma in questo caso, per ogni dato viene richiesta la pressionedi [ Invio ].

76 volume XX Programmare 1

|printf ("Inserisci il capitale e il tasso:");

|scanf ("%d%f", &iCapitale, &iTasso);

Le metavariabili utilizzabili sono simili a quelle già viste per ‘printf()’; in particolare nonsi utilizzano simboli aggiuntivi, mentre è sempre possibile inserire la dimensione.

La funzione ‘scanf()’ restituisce il numero di elementi che sono stati letti con successo,intendendo con questo non solo il completamento della lettura, ma anche il fatto che i datiinseriti risultano corretti in funzione delle metavariabili indicate.

Si veda anche la pagina di manuale scanf (3).

532.9 Restituzione di un valore

I programmi, di qualunque tipo siano, al termine della loro esecuzione, restituiscono un valoreche può essere utilizzato da uno script di shell per determinare se il programma ha fatto ciòche si voleva o se è intervenuto qualche tipo di evento che lo ha impedito.

Convenzionalmente si tratta di un valore numerico, in cui zero rappresenta una conclusionenormale, ovvero priva di eventi indesiderati, mentre qualsiasi altro valore rappresenta un’ano-malia. A questo proposito si consideri quello «strano» atteggiamento degli script di shell, percui zero equivale a Vero.

Se nel sorgente C non si fa nulla per definire il valore restituito, questo è sempre zero, mentreper agire diversamente, conviene utilizzare la funzione ‘exit()’.

532.9.1 Funzione «exit()»

||exit (valore_restituito)

|

La funzione ‘exit()’ provoca la conclusione del programma, dopo aver provveduto a scari-care i flussi di dati e a chiudere i file. Per questo motivo, non restituisce un valore all’internodel programma, al contrario, fa in modo che il programma restituisca il valore indicato comeargomento.

Per poterla utilizzare occorre includere il file di intestazione ‘stdlib.h’ che tra l’altrodichiara già due macro adatte a definire la conclusione corretta o errata del programma:‘EXIT_SUCCESS’ e ‘EXIT_FAILURE’.3

|#include stdlib.h

|...

|...

|if (...)

| {

| exit (EXIT_SUCCESS);

| }

|else| {

| exit (EXIT_FAILURE);

| }

Linguaggio C: introduzione 77

L’esempio mostra in modo molto semplice come potrebbe essere utilizzata questa funzione.

532.10 Suddivisione dei sorgenti e compilazione

All’inizio del capitolo è descritto in modo semplice come compilare un programma compostoda un sorgente unico. Di solito i programmi di dimensioni normali sono articolati in più filesorgenti separati che vengono compilati in modo indipendente e infine collegati in un esegui-bile unico. Questo permette di ridurre i tempi di compilazione quando si fanno modifiche soloin uno o alcuni file sorgenti, in modo da non dover ricompilare sempre tutto.

532.10.1 Suddivisione in più file sorgenti

La suddivisione del codice in più file sorgenti richiede un po’ di attenzione nell’inclusionedei file di intestazione, nel senso che si deve ripetere l’inclusione dei file necessari in tuttii sorgenti. Se si utilizzano delle macro del preprocessore, queste devono essere dichiaratein tutti i sorgenti che ne fanno uso; per questo conviene solitamente predisporre dei file diintestazione aggiuntivi, in modo da facilitarne l’inclusione in tutti i sorgenti.

Un altro problema è dato dalle funzioni descritte in un file e utilizzate anche in altri. Ognifile sorgente, all’interno del quale si fa riferimento a una funzione dichiarata altrove, devecontenere una dichiarazione di prototipo opportuna. In modo analogo occorre comportarsi conle variabili globali. Anche queste definizioni possono essere inserite in un file di intestazionepersonalizzato, da includere in ogni sorgente.

532.10.2 Compilazione e link

Disponendo di diversi file sorgenti separati, la compilazione avviene in due fasi: la generazionedei file oggetto e il link di questi in modo da ottenere un file eseguibile. Fortunatamente, tuttoquesto può essere gestito tramite lo stesso compilatore ‘cc’.

Per generare i file oggetto si utilizza ‘cc’ con l’opzione ‘-c’, mentre per unirli assieme, siutilizza l’opzione ‘-o’. Si osservi l’esempio seguente:

|/* prova1.c */

|#include <stdio.h>

||/* una funzione banalissima */

|void messaggio (char *pc)

|{| printf (pc);

|}

|/* prova0.c */

|#include <stdio.h>

||/* prototipo della funzione banalissima */

|void messaggio (char *);

||int main ()

|{| messaggio ("saluti a tutti\n");

|}

78 volume XX Programmare 1

Si suppone che il primo file sia stato nominato ‘prova1.c’ e il secondo ‘prova0.c’. Siinizia dalla compilazione dei singoli file in modo da generare i file oggetto ‘prova1.o’ e‘prova0.o’.

$ cc -c prova1.c [ Invio ]

$ cc -c prova0.c [ Invio ]

Quindi si passa all’unione dei due risolvendo i riferimenti incrociati, generando il fileeseguibile ‘prova’.

$ cc -o prova prova1.o prova0.o [ Invio ]

Se si volesse fare una modifica su uno solo dei file sorgenti, basterebbe rigenerare il file oggettorelativo e riunire il tutto con il comando ‘cc -o’ appena mostrato.

532.10.3 Compilatore standard «cc»

Il compilatore C di GNU è GCC (‘cc’ GNU), tuttavia, le sue caratteristiche sono tali da ren-derlo conforme al compilatore standard POSIX. Per mantenere la convenzione, è presente ilcollegamento ‘cc’ che si riferisce al vero eseguibile ‘gcc’.

||cc [ opzioni | file ]...

|

La sintassi esprime in maniera piuttosto vaga l’ordine dei vari argomenti della riga di comandoe in effetti non c’è una particolare rigidità.

Opzione Descrizione

|-c Genera come risultato i file di oggetto, senza avviare il link dell’eseguibile finale.

|-g Aggiunge delle informazioni diagnostiche utili per il debug attraverso strumentiappositi come ‘gdb’.

|-o fileDefinisce il nome del file che deve essere generato a seguito della compilazione(indipendentemente dal fatto che si tratti di un file eseguibile o di un file oggettoo altro ancora).

|-llibreriaCompila utilizzando la libreria indicata, tenendo presente che, per questo, vienecercato un file che inizia per ‘lib’, continua con il nome indicato e termina con‘.a’ oppure ‘.so’.

Estensione tipica Descrizione

|.c Sorgente C.

|.o File oggetto.

|.a Libreria.

Linguaggio C: introduzione 79

532.10.4 Problemi con l’ottimizzazione di GCC

Il compilatore GCC consente di utilizzare diverse opzioni per ottenere un risultato più o menoottimizzato. L’ottimizzazione richiede una potenza elaborativa maggiore, al crescere del livel-lo di ottimizzazione richiesto. In situazioni particolari, può succedere che la compilazione nonvada a buon fine a causa di questo problema, interrompendosi con segnalazioni più o menooscure, riferite alla scarsità di risorse. In particolare potrebbe essere rilevato un uso eccessivodella memoria virtuale, per arrivare fino allo scarico della memoria (core dump).

È evidente che in queste situazioni diventa necessario diminuire il livello di ottimizzazionerichiesto, modificando opportunamente le opzioni relative. L’opzione in questione è ‘−On’,come descritto nella tabella 532.70. In generale, l’assenza di tale opzione implica la compila-zione normale senza ottimizzazione, mentre l’uso dell’opzione ‘−O0’ può essere utile alla finedella serie di opzioni, per garantire l’azzeramento delle richieste di ottimizzazione precedenti.

|Tabella 532.70. Opzioni di ottimizzazione per GCC.

Opzione Descrizione

|-O|-O1

Ottimizzazione minima.

|-O2 Ottimizzazione media.

|-O3 Ottimizzazione massima.

|-O0 Annullamento delle richieste precedenti di ottimizzazione.

Alle volte, compilando un programma, può succedere che a causa del livello eccessivo diottimizzazione prestabilito, non si riesca a produrre alcun risultato. In questi casi, può essereutile ritoccare i file-make, dopo l’uso del comando ‘configure’; per la precisione si devericercare un’opzione che inizia per ‘-O’. Purtroppo, il problema sta nel fatto che spesso sitratta di più di un file-make, in base all’articolazione dei file che compongono il sorgente.

Ammesso che si tratti dei file ‘Makefile’, si potrebbe usare il comando seguente per attuarela ricerca:

$ find . -name Makefile ←↩↪→-exec echo \{\} \; ←↩↪→-exec grep \\-O \{\} \; [ Invio ]

Il risultato potrebbe essere simile a quello che si vede qui di seguito:

80 volume XX Programmare 1

|./doc/Makefile|./backend/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./frontend/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./include/Makefile|./japi/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./lib/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./sanei/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./tools/Makefile|CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072

|./Makefile

In questo caso, si può osservare che i file ‘./doc/Makefile’, ‘./include/Makefile’ e‘Makefile’, non contengono tale stringa.

Questo problema può riguardare anche la compilazione di un kernel Linux. In tal caso, dovreb-be essere sufficiente modificare il solo file ‘sorgenti_linux/Makefile’, anche se non è l’unicoin cui appaia tale opzione. Le righe su cui intervenire potrebbero avere l’aspetto seguente:

|HOSTCFLAGS = -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer

|CFLAGS := $(CPPFLAGS) -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer

532.11 Riferimenti

• Alessandro Bellini, Andrea Guidi, Linguaggio C, McGraw-Hill, ISBN 88-386-3404-1

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

1 Come si può osservare, la dimensione è restituita dalla funzione ‘sizeof()’, che però nel-l’esempio risulta preceduta dalla notazione ‘(int)’. Si tratta di un cast, perché il valore resti-tuito dalla funzione è di tipo speciale, precisamente si tratta del tipo ‘size_t’. Il cast è soloprecauzionale perché generalmente tutto funziona in modo regolare senza questa indicazione.2 Di fatto, questa è la convenzione usata nel linguaggio Java, però si tratta di un’idea valida eperfettamente applicabile anche in C.3 In pratica, ‘EXIT_SUCCESS’ equivale a zero, mentre ‘EXIT_FAILURE’ equivale a uno.

81Capitolo 533

C: puntatori, array e stringheNel capitolo precedente sono stati mostrati solo i tipi di dati primitivi, cioè quelli a cui si fariferimento attraverso un nome. Per poter utilizzare strutture di dati più complesse, come unarray o altri tipi più articolati, si gestiscono dei puntatori alle zone di memoria contenenti talistrutture. L’idea può sembrare spaventosa a prima vista, ma tutto questo può essere gestito inmodo molto semplice se si comprende bene il problema dall’inizio.

Quando si ha a che fare con i puntatori, è importante considerare che il modello di memoria chesi ha di fronte è un’astrazione, nel senso che una struttura di dati appare idealmente continua,mentre nella realtà il compilatore potrebbe anche provvedere a scomporla in blocchi separati.

533.1 Puntatori

Una variabile, di qualunque tipo sia, rappresenta un valore posto da qualche parte nella me-moria del sistema. Quando si usano i tipi di dati normali, è il compilatore a prendersi cura ditradurre i riferimenti agli spazi di memoria rappresentati simbolicamente attraverso dei nomi.

Attraverso l’operatore di indirizzamento e-commerciale (‘&’), è possibile ottenere il puntatore(riferito alla rappresentazione ideale di memoria del linguaggio C) a una variabile normale.Tale valore, che comunque appartiene ai tipi di dati primitivi (anche se questo fatto non è statodefinito in precedenza), può essere inserito in una variabile particolare, adatta a contenerlo:una variabile puntatore.

Per esempio, se ‘p’ è una variabile puntatore adatta a contenere l’indirizzo di un intero,l’esempio mostra in che modo assegnare a tale variabile il puntatore alla variabile ‘i’.

|int i = 10;

|...

|p = &i;

533.1.1 Dichiarazione e utilizzo delle variabili puntatore

La dichiarazione di una variabile puntatore avviene in modo simile a quello delle variabili nor-mali, con l’aggiunta di un asterisco davanti al nome. L’esempio seguente dichiara la variabile‘p’ come puntatore a un tipo ‘int’. Si osservi che è assolutamente necessario indicare il tipodi dati a cui si punta.

|int *p;

Non deve essere interesse del programmatore il modo esatto in cui si rappresentano i puntatoridei vari tipi di dati, diversamente non ci sarebbe l’utilità di usare un linguaggio come il Cinvece di un semplice assemblatore di linguaggio macchina.

Una volta dichiarata la variabile puntatore, questa viene utilizzata normalmente, senzaasterisco, finché si intende fare riferimento al puntatore stesso.

L’asterisco usato nella dichiarazione serve a definire il tipo di dati, quindi, la dichiarazione‘int *p’, rappresenta la dichiarazione di una variabile di tipo ‘int *’.

82 volume XX Programmare 1

Attraverso l’operatore di «dereferenziazione», l’asterisco (‘*’), è possibile accedere alla zonadi memoria a cui la variabile punta. Per «dereferenziare» si intende quindi l’azione con cui sitoglie il riferimento e si raggiungono i dati a cui un puntatore si riferisce.

Attenzione a non fare confusione con gli asterischi: una cosa è quello usato nelladichiarazione e un’altra è l’operatore con cui invece si ottiene la moltiplicazione.

L’esempio accennato nella sezione introduttiva potrebbe essere chiarito nel modo seguente,dove si mostra anche la dichiarazione della variabile puntatore.

|int i = 10;

|int *p;

|...

|p = &i;

A questo punto, dopo aver assegnato a ‘p’ il puntatore alla variabile ‘i’, è possibile accede-re alla stessa area di memoria in due modi diversi: attraverso la stessa variabile ‘i’, oppureattraverso la traduzione ‘*p’.

|int i = 10;

|int *p;

|...

|p = &i;

|...

|*p = 20

Nell’esempio, l’istruzione ‘*p=20’ è tecnicamente equivalente a ‘i=20’. Per chiarire un po’meglio il ruolo delle variabili puntatore, si può complicare l’esempio nel modo seguente:

|int i = 10;

|int *p;

|int *p2;

|...

|p = &i;

|...

|p2 = p;

|...

|*p2 = 20

In particolare è stata aggiunta una seconda variabile puntatore, ‘p2’, solo per fare vedereche è possibile passare un puntatore anche ad altre variabili senza dover usare l’asterisco.Comunque, in questo caso, ‘*p2=20’ è tecnicamente equivalente sia a ‘*p=20’, sia a ‘i=20’.

533.1.2 Passaggio di parametri per riferimento

Il linguaggio C utilizza il passaggio dei parametri alle funzioni per valore; per ottenere ilpassaggio per riferimento occorre utilizzare i puntatori. Si immagini di volere realizzare unafunzione banale che modifica la variabile utilizzata nella chiamata, sommandovi una quantitàfissa. Invece di passare il valore della variabile da modificare, si può passare il suo puntatore; inquesto modo la funzione (che comunque deve essere stata realizzata appositamente per questoscopo) agisce nell’area di memoria a cui punta questo puntatore.

C: puntatori, array e stringhe 83

|...

|void funzione_stupida (int *x)

|{| (*x)++;

|}|...

|int main ()

|{| int y = 10;

| ...

| funzione_stupida (&y);

| ...

|}

L’esempio mostra la dichiarazione e descrizione di una funzione che non restituisce alcunvalore e riceve un parametro puntatore a un intero. Il lavoro della funzione è solo quello diincrementare il valore contenuto nell’area di memoria a cui si riferisce tale puntatore.

Poco dopo, nella funzione ‘main()’ inizia il programma vero e proprio; viene dichiarata lavariabile ‘y’ corrispondente a un intero normale inizializzato a 10, poi, a un certo punto vienechiamata la funzione vista prima, passando il puntatore a ‘y’.

Il risultato è che dopo la chiamata, la variabile ‘y’ contiene il valore precedente incrementatodi un’unità.

533.2 Array

Nel linguaggio C, l’array è una sequenza ordinata di elementi dello stesso tipo nella rappre-sentazione ideale di memoria che si ha di fronte. In questo senso, quando si dichiara un array,quello che il programmatore ottiene in pratica, è solo il riferimento alla posizione iniziale diquesto; gli elementi successivi si raggiungono tenendo conto della lunghezza di ogni elemento.

Questo ragionamento vale in senso generale ed è un po’ approssimativo. In contesti par-ticolari, il riferimento a un array restituisce qualcosa di diverso dal puntatore al primoelemento.

Visto in questi termini, si può intendere che l’array in C è sempre a una sola dimensione, tuttigli elementi devono essere dello stesso tipo in modo da avere la stessa dimensione e la quantitàdegli elementi è fissa.

Inoltre, dal momento che quando si dichiara l’array si ottiene solo il riferimento al suo inizio, ècompito del programmatore ricordare la dimensione massima di questo, perché non c’è alcunmodo per determinarlo durante l’esecuzione del programma.

Infatti, quando un programma tenta di accedere a una posizione oltre il limite degli ele-menti esistenti, c’è il rischio che non si verifichi alcun errore, arrivando però a dei risultatiimprevedibili.

84 volume XX Programmare 1

533.2.1 Dichiarazione e utilizzo degli array

La dichiarazione di un array avviene in modo intuitivo, definendo il tipo degli elementi e laloro quantità. L’esempio seguente mostra la dichiarazione dell’array ‘a’ di sette elementi ditipo ‘int’.

|int a[7];

Per accedere agli elementi dell’array si utilizza un indice, il cui valore iniziale è sempre zero e,di conseguenza, l’ultimo ha indice n−1, dove n corrisponde alla quantità di elementi esistenti.

|a[1] = 123;

L’esempio mostra l’assegnamento del valore 123 al secondo elemento.

In presenza di array monodimensionali e di piccole dimensioni, può essere sensato attribuireun valore iniziale agli elementi di questo, all’atto della dichiarazione.

Alcuni compilatori consentono l’inizializzazione degli array solo quando questi sono di-chiarati all’esterno delle funzioni, con un campo di azione globale, oppure all’interno dellefunzioni, ma dichiarati come statici.

|int a[] = { 123, 453, 2, 67 };

L’esempio dovrebbe chiarire il modo: non occorre specificare il numero di elementi, perchéquesti sono esattamente quelli elencati nel raggruppamento tra le parentesi graffe.

Questo fatto potrebbe fare supporre erroneamente che si possano rappresentare degli arraycostanti attraverso un elenco tra parentesi graffe, ma non è così, perché questa semplificazionevale solo nel momento della dichiarazione.

533.2.2 Scansione di un array

La scansione di un array avviene generalmente attraverso un’iterazione enumerativa, in pra-tica con un ciclo ‘for’ che si presta particolarmente per questo scopo. Si osservi l’esempioseguente:

|int a[7];

|int i;

|...

|for (i = 0; i < 7; i++)

| {

| ...

| a[i] = ...;

| ...

| }

L’indice ‘i’ viene inizializzato a zero, in modo da cominciare dal primo elemento dell’array;il ciclo può continuare fino a che ‘i’ continua a essere inferiore a sette, infatti l’ultimo ele-mento dell’array ha indice sei; alla fine di ogni ciclo, prima che riprenda il successivo, vieneincrementato l’indice di un’unità.

C: puntatori, array e stringhe 85

Per scandire un array in senso opposto, si può agire in modo analogo, come nell’esempioseguente:

|int a[7];

|int i;

|...

|for (i = 6; i >= 0; i--)

| {

| ...

| a[i] = ...;

| ...

| }

Questa volta l’indice viene inizializzato in modo da puntare alla posizione finale; il ciclo vieneripetuto fino a che l’indice è maggiore o uguale a zero; alla fine di ogni ciclo, l’indice vienedecrementato di un’unità.

533.2.3 Array multidimensionali

Gli array in C sono monodimensionali, però nulla vieta di creare un array i cui elementi sianoarray tutti uguali. Per esempio, nel modo seguente, si dichiara un array di sette elementi che aloro volta sono array di cinque elementi di tipo ‘int’. Nello stesso modo si possono definirearray con più di due dimensioni.

|int a[5][7];

Quando si creano array multidimensionali, si tende a considerare il contrario rispetto a quantoaffermato, cioè, per fare riferimento all’esempio precedente, che si tratti di un array di cinqueelementi che a loro volta sono array di sette interi. Non è molto importante stabilire qualesia la realtà dei fatti, quello che conta è che il programmatore abbia chiaro in mente comeintende gestire la cosa. L’esempio seguente mostra il modo normale di scandire un array a duedimensioni.

|int a[5][7];

|int i1;

|int i2;

|...

|for (i1 = 0; i1 < 5; i1++)

| {

| ...

| for (i2 = 0; i2 < 7; i2++)

| {

| ...

| a[i1][i2] = ...;

| ...

| }

| ...

| }

86 volume XX Programmare 1

533.2.4 Natura dell’array

Inizialmente si è accennato al fatto che quando si crea un array, quello che viene restituito inpratica è un puntatore alla sua posizione iniziale, ovvero all’indirizzo del primo elemento diquesto.

Si può intuire che non sia possibile assegnare a un array un altro array, anche se ciò potrebbeavere significato. Al massimo si può assegnare elemento per elemento.

Per evitare errori del programmatore, la variabile che contiene l’indirizzo iniziale dell’array,quella che in pratica rappresenta l’array stesso, è in sola lettura. Quindi, nel caso dell’arraygià visto, la variabile ‘a’ non può essere modificata, mentre i singoli elementi ‘a[i]’ sì:

|int a[7];

Data la filosofia del linguaggio C, se fosse possibile assegnare un valore alla variabile ‘a’, simodificherebbe il puntatore, facendo in modo che questo punti a un array differente. Ma perraggiungere questo risultato è meglio usare i puntatori in modo esplicito. Si osservi l’esempioseguente:

|#include <stdio.h>

||int main ()

|{| int ai[3];

| int *pi;

|| pi = ai; /* pi diventa un alias dell’array ai */

|| pi[0] = 10;

| pi[1] = 100;

| pi[2] = 1000;

|| printf ("%d %d %d \n", ai[0], ai[1], ai[2]);

|}

Viene creato un array, ‘ai’, di tre elementi di tipo ‘int’, e subito dopo una variabile puntatore,‘pi’, al tipo ‘int’. Si assegna quindi alla variabile ‘pi’ il puntatore rappresentato da ‘ai’; daquel momento si può fare riferimento all’array indifferentemente con il nome ‘ai’ o ‘pi’.

In modo analogo, si può estrapolare l’indice che rappresenta l’array dal primo elemento. Siveda la trasformazione dell’esempio appena visto, nel modo seguente:

C: puntatori, array e stringhe 87

|#include <stdio.h>

||int main ()

|{| int ai[3];

| int *pi;

|| pi = &ai[0]; /* pi diventa un alias dell’array ai */

|| pi[0] = 10;

| pi[1] = 100;

| pi[2] = 1000;

|| printf ("%d %d %d \n", ai[0], ai[1], ai[2]);

|}

533.2.5 Array e funzioni

Si è visto che le funzioni possono accettare solo parametri composti da tipi di dati elementari,compresi i puntatori. In questa situazione, l’unico modo per trasmettere a una funzione unarray attraverso i parametri, è quello di inviarne il puntatore. Di conseguenza, le modificheche vengono poi apportate da parte della funzione si riflettono nell’array di origine. Si osservil’esempio seguente:

|#include <stdio.h>

||void elabora (int *pi)

|{| pi[0] = 10;

| pi[1] = 100;

| pi[2] = 1000;

|}||int main ()

|{| int ai[3];

|| elabora (ai);

| printf ("%d %d %d \n", ai[0], ai[1], ai[2]);

|}

La funzione ‘elabora()’ utilizza un solo parametro, rappresentato da un puntatore a un tipo‘int’. La funzione presume che il puntatore si riferisca all’inizio di un array di interi e cosìassegna alcuni valori ai primi tre elementi (anche il numero degli elementi non può esseredeterminato dalla funzione).

All’interno della funzione ‘main()’ viene dichiarato l’array ‘ai’ di tre elementi interi, e subitodopo viene passato come parametro alla funzione ‘elabora()’. Così facendo, si passa ilpuntatore al primo elemento dell’array.

Infine, la funzione altera gli elementi come è già stato descritto e gli effetti si possonoosservare.

88 volume XX Programmare 1

|10 100 1000

L’esempio potrebbe essere modificato per presentare la gestione dell’array in modo piùelegante. Per la precisione si tratta di ritoccare la funzione ‘elabora’.

|void elabora (int ai[])

|{| ai[0] = 10;

| ai[1] = 100;

| ai[2] = 1000;

|}

Si tratta della stessa identica cosa, solo che si pone l’accento sul fatto che l’argomento è unarray di interi. Infatti, essendo un array di interi un puntatore a un intero, questa notazione fasì che la lettura del sorgente diventi più facile.

533.3 Stringhe

Le stringhe, nel linguaggio C, non sono un tipo di dati a sé stante; si tratta solo di un arraydi caratteri con una particolarità: l’ultimo carattere è sempre ‘\0’ (pari a una sequenza di bita zero). In questo modo, si evita di dover accompagnare le stringhe con l’informazione dellaloro lunghezza, dal momento che il C non offre un metodo per determinare la dimensione degliarray.

Con questa premessa, si può intendere che il trattamento delle stringhe in C non sia una cosatanto agevole; in particolare non si possono usare operatori di concatenamento. Per tutti i tipidi elaborazione occorre intervenire a livello di array di caratteri.

533.3.1 Array di caratteri e array stringa

Una stringa è un array di caratteri, ma un array di caratteri non è necessariamente una stringa:per esserlo occorre che l’ultimo elemento sia il carattere ‘\0’.

|char ac[20];

L’esempio mostra la dichiarazione di un array di caratteri, senza specificare il suo contenuto.Per il momento non si può parlare di stringa, soprattutto perché per essere tale, la stringa devecontenere dei caratteri.

|char ac[] = { ’c’, ’i’, ’a’, ’o’ };

Questo esempio mostra la dichiarazione di un array di quattro caratteri. All’interno delleparentesi quadre non è stata specificata la dimensione perché questa si determina dall’ini-zializzazione. Anche in questo caso non si può ancora parlare di stringa, perché manca laterminazione.

|char acz[] = { ’c’, ’i’, ’a’, ’o’, ’\0’ };

Questo esempio mostra la dichiarazione di un array di cinque caratteri corrispondente a unastringa vera e propria. L’esempio seguente è tecnicamente equivalente, solo che utilizza unarappresentazione più semplice.

|char acz[] = "ciao";

C: puntatori, array e stringhe 89

Pertanto, la stringa ‘"ciao"’ è un array di cinque caratteri perché rappresenta implicitamenteanche la terminazione.

In un sorgente C ci sono varie occasioni di utilizzare delle stringhe letterali (delimitate attraver-so gli apici doppi), senza la necessità di dichiarare l’array corrispondente. Però è importantetenere presente la natura delle stringhe per sapere come comportarsi con loro. Per prima cosa,bisogna rammentare che la stringa, anche se espressa in forma letterale, è un array di caratteri;come tale restituisce semplicemente il puntatore del primo di questi caratteri (salvo le stesseeccezioni che riguardano tutti i tipi di array).

|char *pc;

|...

|pc = "ciao";

|...

L’esempio mostra il senso di quanto affermato: non esistendo un tipo di dati «stringa», si puòassegnare una stringa solo a un puntatore al tipo ‘char’. L’esempio seguente non è valido,perché non si può assegnare un valore alla variabile che rappresenta un array.

|char ac[];

|...

|ac = "ciao"; /* non si può */

|...

533.3.2 Stringhe come parametri di una funzione

Quando si utilizza una stringa tra i parametri della chiamata di una funzione, questa riceve ilpuntatore all’inizio della stringa. In pratica, si ripete la stessa situazione già vista per gli arrayin generale.

|#include <stdio.h>

||void elabora (char *acz)

|{| printf (acz);

|}||int main ()

|{| elabora ("ciao\n");

|}

L’esempio mostra una funzione banale che si occupa semplicemente di emettere la stringa ri-cevuta come parametro, utilizzando ‘printf()’. La variabile utilizzata per ricevere la stringaè stata dichiarata come puntatore al tipo ‘char’, poi tale puntatore è stato utilizzato come para-metro per la funzione ‘printf()’. Volendo scrivere il codice in modo più elegante si potrebbedichiarare apertamente la variabile ricevente come array di caratteri di dimensione indefinita.Il risultato è lo stesso.

90 volume XX Programmare 1

|#include <stdio.h>

||void elabora (char acz[])

|{| printf (acz);

|}||int main ()

|{| elabora ("ciao\n");

|}

533.3.3 Caratteri speciali e sequenze di escape

Nel capitolo precedente, in occasione della descrizione delle costanti letterali per i tipi di datiprimitivi, è già stato descritto il modo con cui si possono rappresentare alcuni caratteri specialiattraverso delle sequenze di escape. La tabella 532.14 riporta l’elenco delle corrispondenzepiù comuni.

533.3.4 Parametri della funzione main()

La funzione ‘main()’, se viene dichiarata con i suoi parametri tradizionali, permette di acqui-sire la riga di comando utilizzata per avviare il programma. La dichiarazione completa è laseguente:

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

|{| ...

|}

Gli argomenti della riga di comando vengono convertiti in un array di stringhe (cioè di pun-tatori a ‘char’), in cui il primo elemento è il nome utilizzato per avviare il programma e glielementi successivi sono gli altri argomenti. Il primo parametro, ‘argc’, serve a contenerela dimensione di questo array e permette di conoscere il limite per la scansione del secondoparametro, ‘argv’, che come si vede è un array di puntatori al tipo ‘char’.

È il caso di annotare che questo array ha sempre almeno un elemento: il nome utilizzato peravviare il programma e, di conseguenza, ‘argc’ è sempre maggiore o uguale a uno.

L’esempio seguente mostra in che modo gestire tale array, attraverso la semplice riemissionedegli argomenti attraverso lo standard output.

C: puntatori, array e stringhe 91

|#include <stdio.h>

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

|{| int i;

|| printf ("Il programma si chiama %s\n", argv[0]);

|| for (i = 1; i < argc; i++)

| {| printf ("argomento n. %d: %s\n", i, argv[i]);

| }|}

533.4 Puntatori e funzioni

Nello standard C ANSI, la dichiarazione di una funzione è in pratica la definizione di unpuntatore alla funzione stessa, un po’ come accade con gli array. In generale, è possibiledichiarare dei puntatori a un tipo di funzione definito in base al valore restituito e ai tipi diparametri richiesti.

||tipo (*nome_puntatore)(tipo_parametro[,...]);

|

L’esempio seguente mostra la dichiarazione di un puntatore a una funzione che restituisce unvalore di tipo ‘int’ e utilizza due parametri di tipo ‘int’.

|int (*f)(int, int);

L’assegnamento del puntatore avviene nel modo più semplice possibile, trattando il nome dellafunzione nello stesso modo in cui si fa con gli array: come un puntatore.

|int (*f)(int, int);

|int prodotto (int, int); /* prototipo di funzione descritta più avanti */

|...

|f = prodotto; /* il puntatore f contiene il riferimento alla funzione */

Una volta assegnato il puntatore, si può eseguire una chiamata di funzione semplicementeutilizzando il puntatore, per cui,

|i = f (2, 3);

risulta equivalente a quanto segue:

|i = prodotto (2, 3);

Nel linguaggio C precedente allo standard ANSI, perché il puntatore potesse essere utilizzatoin una chiamata di funzione, occorreva indicare l’asterisco, in modo da dereferenziarlo.

|i = (*f)(2, 3);

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

92Capitolo 534

C: tipi di dati derivatiFino a questo punto sono stati incontrati solo i tipi di dati primitivi, oltre agli array di que-sti (incluse le stringhe). Nel linguaggio C, come in altri, è possibile definire dei tipi di datiaggiuntivi, derivati dai tipi primitivi.

534.1 Strutture e unioni

È già stata descritta l’organizzazione degli array: si tratta di una serie di elementi uguali, tuttiadiacenti nel modello di rappresentazione della memoria (ideale o reale che sia). In modosimile si possono definire strutture di dati più complesse in cui gli elementi adiacenti siano ditipo differente. In pratica, una struttura è una sorta di mappa di accesso a un’area di memoria.

La variabile contenente una struttura si comporta in modo analogo alle variabili di tipo pri-mitivo, per cui, la variabile che è stata creata a partire da una struttura, rappresenta tutta lazona di memoria occupata dalla struttura stessa e non solo il riferimento al suo inizio. Questadistinzione è importante, per non fare confusione con il comportamento relativo agli array, chein realtà sono solo dei puntatori.

534.1.1 Dichiarazione e accesso

La dichiarazione di una struttura si articola in due fasi: la dichiarazione del tipo e la dichiara-zione delle variabili che utilizzano quella struttura. Dal momento che il tipo di struttura è unacosa diversa dalle variabili che la utilizzano, è opportuno stabilire una convenzione nel mododi attribuirne il nome. Per esempio, si potrebbero utilizzare nomi con iniziale maiuscola.

|struct Data { int iGiorno; int iMese; int iAnno; };

L’esempio mostra la dichiarazione della struttura ‘Data’ composta da tre interi dedicati acontenere rispettivamente: il giorno, il mese e l’anno. In questo caso, trattandosi di tre elementidello stesso tipo, sarebbe stato possibile utilizzare un array, ma come è possibile vedere inseguito, una struttura può essere conveniente anche in queste situazioni.

È importante osservare che le parentesi graffe sono parte dell’istruzione di dichiarazione dellastruttura e non rappresentano un blocco di istruzioni. Per questo motivo appare il punto evirgola finale, cosa che potrebbe sembrare strana, specialmente quando la struttura si articolasu più righe come nell’esempio seguente:

|struct Data {| int iGiorno;

| int iMese;

| int iAnno;

|}; /* il punto e virgole finale è necessario */

La dichiarazione delle variabili che utilizzano la struttura può avvenire contestualmente con ladichiarazione della struttura, oppure in un momento successivo. L’esempio seguente mostrala dichiarazione del tipo ‘Data’, seguito da un elenco di variabili che utilizzano quel tipo:‘DataInizio’ e ‘DataFine’.

C: tipi di dati derivati 93

|struct Data {| int iGiorno;

| int iMese;

| int iAnno;

|} DataInizio, DataFine;

Tuttavia, il modo più elegante per dichiarare delle variabili a partire da una struttura è quelloseguente:

|struct Data DataInizio, DataFine;

Quando una variabile è stata definita come organizzata secondo una certa struttura, si accede aisuoi componenti attraverso l’indicazione del nome della variabile stessa, seguita dall’operatorepunto (‘.’) e dal nome dell’elemento particolare.

|DataInizio.iGiorno = 1;

|DataInizio.iMese = 1;

|DataInizio.iAnno = 1980;

|...

|DataFine.iGiorno = DataInizio.iGiorno;

|DataFine.iMese = DataInizio.iMese +1;

|DataFine.iAnno = DataInizio.iAnno;

534.1.2 Strutture anonime

Una struttura può essere dichiarata in modo anonimo, definendo immediatamente tutte le va-riabili che fanno uso di quella struttura. La differenza sta nel fatto che la struttura non vienenominata nel momento della dichiarazione e, dopo la definizione dei suoi elementi, devono es-sere elencate tutte le variabili in questione. Evidentemente, non c’è la possibilità di riutilizzarequesta struttura per altre variabili definite in un altro punto.

|struct {| int iGiorno;

| int iMese;

| int iAnno;

|} DataInizio, DataFine;

534.1.3 Assegnamento, inizializzazione, campo di azione epuntatori

Nella sezione precedente si è visto come accedere ai vari componenti della struttura, attraversouna notazione che utilizza l’operatore punto. Volendo è possibile assegnare a una variabile diquesto tipo l’intero contenuto di un’altra che appartiene alla stessa struttura.

|DataInizio.iGiorno = 1;

|DataInizio.iMese = 1;

|DataInizio.iAnno = 1999;

|...

|DataFine = DataInizio;

|DataFine.iMese++;

94 volume XX Programmare 1

L’esempio mostra l’assegnamento alla variabile ‘DataFine’ di tutta la variabile‘DataInizio’. Questo è ammissibile solo perché si tratta di variabili dello stesso tipo, cioèdi strutture di tipo ‘Data’ (come deriva dagli esempi precedenti). Se invece si trattasse di va-riabili costruite a partire da strutture differenti, anche se realizzate nello stesso modo, ciò nonsarebbe ammissibile.

Nel momento della dichiarazione di una struttura, è possibile anche inizializzarla utilizzandouna forma simile a quella disponibile per gli array.

|struct Data DataInizio = { 1, 1, 1999 };

Dal momento che le strutture sono tipi di dati nuovi, per poterne fare uso occorre che la dichia-razione relativa sia accessibile a tutte le parti del programma che hanno bisogno di accedervi.Probabilmente, il luogo più adatto è al di fuori delle funzioni, eventualmente anche in un filedi intestazione realizzato appositamente.

Ciò dovrebbe bastare a comprendere che le variabili che contengono una struttura vengonopassate regolarmente attraverso le funzioni, purché la dichiarazione del tipo corrispondente siaprecedente ed esterno alla descrizione delle funzioni stesse.

|...

|struct Data { int iGiorno; int iMese; int iAnno; };

|...

|void elabora (struct Data DataAttuale)

|{| ...

|}

Così come nel caso dei tipi primitivi, anche con le strutture si possono creare dei puntatori. Laloro dichiarazione avviene in modo intuitivo, come nell’esempio seguente:

|struct Data *pDataFutura;

|...

|pDataFutura = &DataFine;

|...

Quando si utilizza un puntatore a una struttura, diventa un po’ più difficile fare riferimentoai vari componenti della struttura stessa, perché l’operatore punto, quello che unisce il no-me della struttura a quello dell’elemento, ha priorità rispetto all’asterisco che si utilizza perdereferenziare il puntatore.

|*pDataFutura.iGiorno = 15; /* non è valido */

L’esempio appena mostrato, non è ciò che sembra, perché l’asterisco posto davanti viene va-lutato dopo l’elemento ‘pDataFutura.iGiorno’, che non esiste. Per risolvere il problema sipossono usare le parentesi, come nell’esempio seguente:

|(*pDataFutura).iGiorno = 15; /* corretto */

In alternativa, in sostituzione del punto si può usare l’operatore ‘->’, fatto espressamente per ipuntatori a una struttura.

|pDataFutura->iGiorno = 15; /* corretto */

C: tipi di dati derivati 95

534.1.4 Unioni

L’unione permette di definire un tipo di dati accessibile in modi diversi, gestendolo come sesi trattasse contemporaneamente di tipi differenti. La dichiarazione è simile a quella dellastruttura; quello che bisogna tenere a mente è che si fa riferimento alla stessa area di memoria.

|union Livello {| char cLivello;

| int iLivello;

|};

Si immagini, per esempio, di voler utilizzare indifferentemente una serie di lettere alfabetiche,oppure una serie di numeri, per definire un livello di qualcosa («A» equivalente a uno, «B»equivalente a due, ecc.). Le variabili generate a partire da questa unione, possono essere gestitenei modi stabiliti, come se fossero una struttura, ma condividendo la stessa area di memoria.

|union Livello LivelloCarburante;

L’esempio mostra in che modo si possa dichiarare una variabile di tipo ‘Livello’, riferi-ta all’omonima unione. Il bello delle unioni sta però nella possibilità di combinarle con lestrutture.

|struct Livello {| char cTipo;

| union {| char cLivello; /* usato se cTipo == ’c’ */

| int iLivello; /* usato se cTipo == ’n’ */

| };|};

L’esempio non ha un grande significato pratico, ma serve a chiarire le possibiltà. La variabile‘cTipo’ serve ad annotare il tipo di informazione contenuta nell’unione, se di tipo carattere onumerico. L’unione viene dichiarata in modo anonimo come appartenente alla struttura.

534.1.5 Campi

All’interno di una struttura è possibile definire l’accesso a ogni singolo bit di un tipo di datideterminato, oppure a gruppetti di bit. In pratica viene dato un nome a ogni bit o gruppetto.

|struct Luci {| unsigned char

| bA :1,

| bB :1,

| bC :1,

| bD :1,

| bE :1,

| bF :1,

| bG :1,

| bH :1,

|};

L’esempio mostra l’abbinamento di otto nomi ai bit di un tipo ‘char’. Il primo, ‘bA’, rap-presenta il bit più a destra, ovvero quello meno significativo. Se il tipo ‘char’ occupasse unadimensione maggiore di 8 bit, la parte eccedente verrebbe semplicemente sprecata.

96 volume XX Programmare 1

|struct Luci LuciSalotto;

|...

|LuciSalotto.bC = 1;

L’esempio mostra la dichiarazione della variabile ‘LuciSalotto’ come appartenente al-la struttura mostrata sopra, quindi l’assegnamento del terzo bit a uno, probabilmente per«accendere» la lampada associata.

Volendo indicare un gruppo di bit maggiore, basta aumentare il numero indicato a fianco deinomi dei campi, come nell’esempio seguente:

|struct Prova {| unsigned char

| bA :1,

| bB :1,

| bC :1,

| stato :4;

|};

Nell’esempio appena mostrato, si usano i primi tre bit in maniera singola (per qualche scopo) ealtri quattro per contenere un’informazione più consistente. Ciò che resta (probabilmente soloun bit) viene semplicemente ignorato.

534.1.6 Istruzione «typedef»

L’istruzione ‘typedef’ permette di definire un nuovo di tipo di dati, in modo che la sua di-chiarazione sia più agevole. Lo scopo di tutto ciò sta nell’informare il compilatore; ‘typedef’non ha altri effetti. La sintassi del suo utilizzo è molto semplice.

||typedef tipo nuovo_tipo

|

Si osservi l’esempio seguente:

|struct Data {| int iGiorno;

| int iMese;

| int iAnno;

|};|typedef struct Data;

|Data DataInizio, DataFine;

Attraverso ‘typedef’, è stato definito il tipo ‘Data’, facilitando così la dichiarazione dellevariabili ‘DataInizio’ e ‘DataFine’.

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

97Capitolo 535

C: oggetti dinamici e aritmetica dei puntatoriFino a questo punto è stato visto l’uso dei puntatori come mezzo per fare riferimento a zonedi memoria già allocata. È possibile gestire della memoria allocata dinamicamente durantel’esecuzione del programma, facendovi riferimento attraverso i puntatori, utilizzando funzioniapposite per l’allocazione e la deallocazione di questa memoria.

535.1 Oggetti dinamici

Nel file di intestazione ‘stdio.h’ è definita la macro ‘NULL’, che serve convenzionalmente arappresentare il valore di un puntatore indefinito. In pratica un puntatore di qualunque tipo checontenga tale valore, rappresenta il riferimento al nulla.

535.1.1 Dichiarazione e verifica di un puntatore

A seconda del compilatore, la dichiarazione di un puntatore potrebbe coincidere con la suainizializzazione implicita al valore ‘NULL’. Per essere sicuri che un puntatore sia inizializzato,lo si può fare in modo esplicito, come nell’esempio seguente:

|#include <stdio.h>

|...

|int main ()

|{| int *pi = NULL;

| ...

|}

Dovendo gestire degli oggetti dinamici, prima di utilizzare l’area di memoria a cui dovrebbefare riferimento un puntatore, è meglio verificare che questo punti effettivamente a qualcosa.

|if (pi != NULL)

| {

| /* il puntatore è valido e allora procede */

| ...

| }

|else| {

| /* la memoria non è stata allocata e si fa qualcosa di alternativo */

| }

535.1.2 Allocazione di memoria

L’allocazione di memoria avviene generalmente attraverso la funzione ‘malloc()’, oppu-re ‘calloc()’. Se queste riescono a eseguire l’operazione, restituiscono il puntatore allamemoria allocata, altrimenti restituiscono il valore ‘NULL’.

||void *malloc (size_t dimensione)

|

98 volume XX Programmare 1

||void *calloc (size_t quantità, size_t dimensione)

|

La differenza tra le due funzioni sta nel fatto che la prima, ‘malloc()’, viene utilizzata perallocare un’area di una certa dimensione, espressa generalmente in byte, mentre la seconda,‘calloc()’, permette di indicare una quantità di elementi e si presta per l’allocazione di array.

Dovendo utilizzare queste funzioni per allocare della memoria, è necessario conoscere la di-mensione dei tipi primitivi di dati, ma per evitare incompatibilità conviene farsi aiutare da‘sizeof()’.

Il valore restituito da queste funzioni è di tipo ‘void *’ cioè una specie di puntatore neutro,indipendente dal tipo di dati da utilizzare. Per questo, in linea di principio, prima di assegnare aun puntatore il risultato dell’esecuzione di queste funzioni di allocazione, è opportuno eseguireun cast.

|int *pi = NULL;

|...

|pi = (int *)malloc (sizeof (int));

||if (pi != NULL)

| {

| /* il puntatore è valido e allora procede */

| ...

| }

|else| {

| /* la memoria non è stata allocata e si fa qualcosa di alternativo */

| }

Come si può osservare dall’esempio, il cast viene eseguito con la notazione ‘(int *)’ cherichiede la conversione esplicita in un puntatore a ‘int’. Lo standard ANSI C non richiedel’utilizzo di questo cast esplicito, quindi l’esempio si può ridurre al modo seguente:

|...

|pi = malloc (sizeof (int));

|...

535.1.3 Deallocazione di memoria

La memoria allocata dinamicamente deve essere liberata in modo esplicito quando non servepiù. Infatti, il linguaggio C non offre alcun meccanismo di raccolta della spazzatura o garbagecollector. Per questo si utilizza la funzione ‘free()’ che richiede semplicemente il puntatoree non restituisce alcunché.

||void free (void *puntatore)

|

È necessario evitare di deallocare più di una volta la stessa area di memoria, perché ciòprovocherebbe risultati imprevedibili.

C: oggetti dinamici e aritmetica dei puntatori 99

|int *pi = NULL;

|...

|pi = (int *)malloc (sizeof (int));

||if (pi != NULL)

| {

| /* il puntatore è valido e allora procede */

| ...

| free (pi); /* libera la memoria */

| pi = NULL; /* per sicurezza azzera il puntatore */

| ...

| }

|else| {

| /* la memoria non è stata allocata e si fa qualcosa di alternativo */

| }

535.2 Aritmetica dei puntatori

Con le variabili puntatore è possibile eseguire delle operazioni elementari: possono essereincrementate e decrementate. Il risultato che si ottiene è il riferimento a una zona di memoriaadiacente, in funzione della dimensione del tipo di dati per il quale è stato creato il puntatore.

535.2.1 Array

Gli array sono una serie di elementi dello stesso tipo e dimensione. La dichiarazione di unarray è in pratica la dichiarazione di un puntatore al tipo di dati degli elementi di cui questo ècomposto. Si osservi l’esempio seguente:

|int ai[3] = { 1, 3, 5 };

|int *pi;

|...

|pi = ai;

In questo modo il puntatore ‘pi’ punta all’inizio dell’array.

|...

|*pi = 10; /* equivale a: ai[0] = 10 */

|pi++;|*pi = 30; /* equivale a: ai[1] = 30 */

|pi++;|*pi = 50; /* equivale a: ai[2] = 50 */

Ecco che, incrementando il puntatore si accede all’elemento successivo adiacente, in funzionedella dimensione del tipo di dati. Decrementando il puntatore si ottiene l’effetto opposto, diaccedere all’elemento precedente.

Deve essere chiaro che è compito del programmatore sapere quando l’incremento o il decre-mento di un puntatore ha significato. Diversamente si rischia di accedere a zone di memoriaestranee all’array, con risultati imprevedibili.Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

100Capitolo 536

C: fileLa gestione dei file offerta dal linguaggio C è elementare, a meno di fare uso di librerie spe-cifiche realizzate appositamente per gestioni più sofisticate dei dati. A parte questa considera-zione, il linguaggio C offre due tipi di accesso ai file; uno definibile come «normale» e un altroa basso livello, per permettere l’accesso a funzioni del sistema operativo. In questo capitoloviene mostrato solo l’utilizzo normale dei file.

536.1 Macro «FILE» come tipo di dati

Nel linguaggio C, i file vengono trattati come un tipo di dati derivato, cioè ottenuto dai tipielementari esistenti. In pratica, quando si apre e si gestisce un file, si ha a che fare con unpuntatore al file, o file pointer, che è una variabile contenente un qualche riferimento univocoal file stesso.

Per gestire i puntatori ai file in C, occorre dichiarare una variabile come puntatore al tipoderivato ‘FILE’, come nell’esempio seguente:

|#include <stdio.h>

|...

|int main ()

| {

| FILE *pf;

| ...

| }

Il fatto che il nome utilizzato per questo tipo di dati, ‘FILE’, sia scritto utilizzando solo letteremaiuscole, lascia intendere che si tratta di una macro che si traduce in qualcosa di adatto altipo particolare di piattaforma utilizzato (si veda la sezione 537.1.1). Per questo stesso motivo,l’utilizzo di tale tipo richiede l’inclusione del file di intestazione ‘stdio.h’.

536.1.1 Macro «EOF»

Oltre alla macro ‘FILE’, è bene considerarne un’altra, anch’essa molto importante: ‘EOF’. Sitratta di un valore, definito in base alle caratteristiche della piattaforma, utilizzato per rappre-sentare il raggiungimento della fine del file. Anche questa macro è definita all’interno del file‘stdio.h’.

536.2 Apertura e chiusura

L’apertura dei file viene ottenuta normalmente con la funzione ‘fopen()’ che restituisce ilpuntatore al file, oppure il puntatore nullo, ‘NULL’, in caso di fallimento dell’operazione. L’e-sempio seguente mostra l’apertura del file ‘mio_file’ contenuto nella directory corrente, conuna modalità di accesso in sola lettura.

C: file 101

|#include <stdio.h>

|...

|int main ()

| {

| FILE *pfMioFile;

| ...

| pfMioFile = fopen ("mio_file", "r");

| ...

| }

Come si vede dall’esempio, è normale assegnare il puntatore ottenuto a una variabile adatta,che da quel momento identifica il file, finché questo resta aperto.

La chiusura del file avviene in modo analogo, attraverso la funzione ‘fclose()’, che restitui-sce zero se l’operazione è stata conclusa con successo, oppure il valore rappresentato da ‘EOF’.L’esempio seguente ne mostra l’utilizzo.

| fclose (pfMioFile);

La chiusura del file conclude l’attività con questo, dopo avere scritto tutti i dati eventualmenteancora rimasti in sospeso (se il file è stato aperto in scrittura).

Normalmente, un file aperto viene definito come flusso, o stream; così, nello stesso modoviene identificata la variabile puntatore che vi si riferisce. In effetti, lo stesso file potrebbeanche essere aperto più volte con puntatori differenti, quindi è corretto distinguere tra filefisici su disco e file aperti, o flussi.

536.2.1 Funzione «fopen()»

||FILE *fopen (char *file, char *modalità)

|

La funzione ‘fopen()’ permette di aprire il file indicato attraverso la stringa fornita comeprimo parametro, tenendo conto che tale stringa può contenere anche informazioni sul percorsonecessario per raggiungerlo, secondo la modalità stabilita dal secondo parametro, anch’essouna stringa.

La stringa fornita come secondo parametro, contenente l’informazione della modalità, puòutilizzare i simboli seguenti:

Stringa Descrizione

|r apre il file in sola lettura, posizionandosi all’inizio del file;

|r+ apre il file in lettura e scrittura, posizionandosi all’inizio del file;

|w apre il file in sola scrittura, creandolo se necessario, o troncandone a zero il suocontenuto se questo esiste già;

|w+ apre il file in scrittura e lettura, creandolo se necessario, o troncandone a zero ilsuo contenuto se questo esiste già;

|aapre il file in scrittura in aggiunta (append), creandolo se necessario, o aggiun-gendovi dati a partire dalla fine e, di conseguenza, posizionandosi alla fine dellostesso;

102 volume XX Programmare 1

Stringa Descrizione

|a+apre il file in scrittura in aggiunta e in lettura, creandolo se necessario, o aggiun-gendovi dati a partire dalla fine e, di conseguenza, posizionandosi alla fine dellostesso.

La funzione restituisce un puntatore al tipo ‘FILE’, riferito al file aperto, oppure si tratta delpuntatore nullo (‘NULL’) in caso di insuccesso.

Vedere anche fopen(3).

536.2.2 Funzione «fclose()»

||int fclose (FILE *stream)

|

La funzione ‘fclose()’ chiude il file rappresentato dal puntatore indicato come parametrodella funzione. Se il file è stato aperto in scrittura, prima di chiuderlo vengono scaricatii dati eventualmente accumulati nella memoria tampone (i buffer), utilizzando la funzione‘fflush()’.

La funzione restituisce il valore zero se l’operazione si conclude normalmente, oppure ‘EOF’se qualcosa non funziona correttamente. In ogni caso, il file non è più accessibile attraverso ilpuntatore utilizzato precedentemente.

Vedere anche fclose(3) e fflush(3).

536.3 Lettura e scrittura

L’accesso al contenuto dei file avviene generalmente solo a livello di byte e non di record. Leoperazioni di lettura e scrittura dipendono da un indicatore riferito a una posizione, espressain byte, del contenuto del file stesso. A seconda di come viene aperto il file, questo indicatoreviene posizionato nel modo più logico, come è già stato descritto a proposito della funzio-ne ‘fopen()’. Questo viene spostato automaticamente a seconda delle operazioni di letturae scrittura che si compiono, tuttavia, quando si passa da una modalità di accesso all’altra,è necessario spostare l’indicatore attraverso le istruzioni opportune, in modo da non creareambiguità.

La lettura avviene normalmente attraverso la funzione ‘fread()’ che legge una quantità dibyte trattandoli come un array. Per la precisione, si tratta di definire la dimensione di ognielemento, espressa in byte, quindi la quantità di tali elementi. Il risultato della lettura vieneinserito in un array, i cui elementi hanno la stessa dimensione. Si osservi l’esempio seguente:

|...

| char ac[100];

| FILE *pf;

| int i;

| ...

| i = fread (ac, 1, 100, pf);

| ...

C: file 103

In questo modo si intende leggere 100 elementi della dimensione di un solo byte, collocandolinell’array ‘ac[]’, organizzato nello stesso modo. Naturalmente, non è detto che la lettura ab-bia successo, o quantomeno non è detto che si riesca a leggere la quantità di elementi richiesta.Il valore restituito dalla funzione rappresenta la quantità di elementi letti effettivamente. Sesi verifica un qualsiasi tipo di errore che impedisce la lettura, la funzione si limita a restituirezero, o al limite, in certe versioni del linguaggio C, restituisce un valore negativo.

Quando il file viene aperto in lettura, l’indicatore interno viene posizionato all’inizio del file;quindi, ogni operazione di lettura sposta in avanti il puntatore, in modo che la lettura successivaavvenga a partire dalla posizione immediatamente seguente:

|...

| char ac[100];

| FILE *pf;

| int i;

| ...

| pf = fopen ("mio_file", "r");

| ...

| while (1) /* Ciclo senza fine */

| {

| i = fread (ac, 1, 100, pf);

| if (i == 0)

| {

| break; /* Termina il ciclo */

| }

| ...

| }

| ...

In questo modo, come mostra l’esempio, viene letto tutto il file a colpi di 100 byte alla volta,tranne l’ultima in cui si ottiene solo quello che resta da leggere.

La scrittura avviene normalmente attraverso la funzione ‘fwrite()’ che scrive una quantitàdi byte trattandoli come un array, nello stesso modo già visto con la funzione ‘fread()’.

|...

| char ac[100];

| FILE *pf;

| int i;

| ...

| i = fwrite (ac, 1, 100, pf);

| ...

L’esempio, come nel caso di ‘fread()’, mostra la scrittura di 100 elementi di un solo byte,prelevati da un array. Il valore restituito dalla funzione è la quantità di elementi che sono statiscritti con successo. Se si verifica un qualsiasi tipo di errore che impedisce la scrittura, lafunzione si limita a restituire zero, o al limite, in certe versioni del linguaggio C, restituisce unvalore negativo.

Anche in scrittura è importante l’indicatore della posizione interna del file. Di solito, quandosi crea un file o lo si estende, l’indicatore si trova sempre alla fine. L’esempio seguente mostralo scheletro di un programma che crea un file, copiando il contenuto di un altro (non vieneutilizzato alcun tipo di controllo degli errori).

104 volume XX Programmare 1

|#include <stdio.h>

|#define BLOCCO 1024

|...

|int main ()

|{| char ac[BLOCCO];

| FILE *pfIN;

| FILE *pfOUT;

| int i;

| ...

| pfIN = fopen ("fileIN", "r");

| ...

| pfOUT = fopen ("fileOUT", "w");

| ...

| while (1) /* Ciclo senza fine */

| {

| i = fread (ac, 1, BLOCCO, pfIN);

| if (i == 0)

| {

| break; /* Termina il ciclo */

| }

| ...

| fwrite (ac, 1, i, pfOUT);

| ...

| }

| ...

| fclose (pfIN);

| fclose (pfOUT);

| ...

|}

536.3.1 Funzione «fread()»

||size_t fread (void *buffer, size_t dimensione, size_t quantità, FILE *stream)

|

La funzione ‘fread()’ permette di leggere una porzione del file identificato attraverso il pun-tatore riferito al flusso (stream), collocandola all’interno di un array. La lettura del file avvienea partire dalla posizione dell’indicatore interno al file, per una quantità stabilita di elementi diuna data dimensione. La dimensione degli elementi viene espressa in byte e deve coinciderecon la dimensione degli elementi dell’array ricevente, che in pratica si comporta da memoriatampone (buffer). L’array ricevente deve essere in grado di accogliere la quantità di elementiletti.

La funzione restituisce il numero di elementi letto effettivamente.

Il tipo di dati ‘size_t’ serve a garantire la compatibilità con qualunque tipo intero, mentre iltipo ‘void’ per l’array della memoria tampone, ne permette l’utilizzo di qualunque tipo.

C: file 105

536.3.2 Funzione «fwrite()»

||size_t fwrite (void *buffer, size_t dimensione, size_t quantità, FILE *stream)

|

La funzione ‘fwrite()’ permette di scrivere il contenuto di una memoria tampone (buffer),contenuta in un array, in un file identificato attraverso il puntatore riferito al flusso (stream). Lascrittura del file avviene a partire dalla posizione dell’indicatore interno al file, per una quantitàstabilita di elementi di una data dimensione. La dimensione degli elementi viene espressa inbyte e deve coincidere con la dimensione degli elementi dell’array che rappresenta la memoriatampone. L’array in questione deve contenere almeno la quantità di elementi di cui vienerichiesta la scrittura.

La funzione restituisce il numero di elementi scritti effettivamente.

Il tipo di dati ‘size_t’ serve a garantire la compatibilità con qualunque tipo intero, mentre iltipo ‘void’ per l’array della memoria tampone, ne permette l’utilizzo di qualunque tipo.

536.4 Indicatore interno al file

Lo spostamento diretto dell’indicatore interno della posizione di un file aperto è un’operazionenecessaria quando il file è stato aperto sia in lettura che in scrittura e da un tipo di operazionesi vuole passare all’altro. Per questo si utilizza la funzione ‘fseek()’ ed eventualmente anche‘ftell()’ per conoscere la posizione attuale. La posizione e gli spostamenti sono espressi inbyte.

‘fseek()’ esegue lo spostamento a partire dall’inizio del file, oppure dalla posizione attuale,oppure dalla posizione finale. Per questo utilizza un parametro che può avere tre valori, rispet-tivamente 0, 1 e 2, identificati anche da tre macro: ‘SEEK_SET’, ‘SEEK_CUR’ e ‘SEEK_END’.l’esempio seguente mostra lo spostamento del puntatore, riferito al flusso ‘pf’, in avanti di10 byte, a partire dalla posizione attuale.

|i = fseek (pf, 10, 1);

La funzione ‘fseek()’ restituisce zero se lo spostamento avviene con successo, altrimenti siottiene un valore negativo.

L’esempio seguente mostra lo scheletro di un programma, senza controlli sugli errori, che,dopo aver aperto un file in lettura e scrittura, lo legge a blocchi di dimensioni uguali, modificaquesti blocchi e li riscrive nel file.

|#include <stdio.h>

||/* ----------------------------------------------------------------- */

|/* Dimensione del record logico */

|/* ----------------------------------------------------------------- */

|#define RECORD 10

|||int main ()

|{| char ac[RECORD];

| FILE *pf;

106 volume XX Programmare 1

| int iQta;

| int iPosizione1;

| int iPosizione2;

|| pf = fopen ("mio_file", "r+"); /* lettura+scrittura */

|| while (1) /* Ciclo senza fine */

| {| /* Salva la posizione del puntatore interno al file */

| /* prima di eseguire la lettura. */

| iPosizione1 = ftell (pf);

| iQta = fread (ac, 1, RECORD, pf);

|| if (iQta == 0)

| {| break; /* Termina il ciclo */

| }|| /* Salva la posizione del puntatore interno al file */

| /* Dopo la lettura. */

| iPosizione2 = ftell (pf);

|| /* Sposta il puntatore alla posizione precedente alla */

| /* lettura. */

| fseek (pf, iPosizione1, SEEK_SET);

|| /* Esegue qualche modifica nei dati, per esempio mette un */

| /* punto esclamativo all’inizio. */

| ac[0] = ’!’;

|| /* Riscrive il record modificato. */

| fwrite (ac, 1, iQta, pf);

|| /* Riporta il puntatore interno al file alla posizione */

| /* corretta per eseguire la prossima lettura. */

| fseek (pf, iPosizione2, SEEK_SET);

| }|| fclose (pf);

|}

536.4.1 Funzione «fseek()»

||int fseek (FILE *stream, long spostamento, int punto_di_partenza)

|

La funzione ‘fseek()’ permette di spostare la posizione dell’indicatore interno al file a cui fariferimento il flusso (stream) fornito come primo parametro. La posizione da raggiungere siriferisce al punto di partenza, rappresentato attraverso tre macro:

• ‘SEEK_SET’ rappresenta l’inizio del file;

C: file 107

• ‘SEEK_CUR’ rappresenta la posizione attuale;

• ‘SEEK_END’ rappresenta la fine del file.

Il valore dello spostamento, fornito come secondo parametro, rappresenta una quantità di byteche può essere anche negativa, indicando in tal caso un arretramento dal punto di partenza.

Il valore restituito da ‘fseek()’ è zero se l’operazione viene completata con successo,altrimenti viene restituito un valore negativo: -1.

536.4.2 Funzione «ftell()»

||long ftell (FILE *stream)

|

La funzione ‘ftell()’ permette di conoscere la posizione dell’indicatore interno al file acui fa riferimento il flusso (stream) fornito come parametro. La posizione è assoluta, ovveroriferita all’inizio del file.

Il valore restituito in caso di successo è positivo, a indicare appunto la posizionedell’indicatore. Se si verifica un errore viene restituito un valore negativo: -1.

536.5 File di testo

I file di testo possono essere gestiti in modo più semplice attraverso due funzioni: ‘fgets()’ e‘fputs()’. Queste permettono rispettivamente di leggere e scrivere un file una riga alla volta,intendendo come riga una porzione di testo che termina con il codice di interruzione di riga.

La funzione ‘fgets()’ permette di leggere una riga di testo di una data dimensione massima.Si osservi l’esempio seguente:

|fgets (acz, 100, pf);

In questo caso, viene letta una riga di testo di una dimensione massima di 99 caratteri, dalfile rappresentato dal puntatore ‘pf’. Questa riga viene posta all’interno dell’array ‘acz[]’,con l’aggiunta di un carattere ‘\0’ finale. Questo fatto spiega il motivo per il quale il secondoparametro corrisponde a 100, mentre la dimensione massima della riga letta è di 99 caratteri.In pratica, l’array di destinazione è sempre una stringa, terminata correttamente.

Nello stesso modo funziona ‘fputs()’, che però richiede solo la stringa e il puntatore del fileda scrivere. Dal momento che una stringa contiene già l’informazione della sua lunghezza per-ché possiede un carattere di conclusione, non è prevista l’indicazione della quantità di elementida scrivere.

|fputs (acz, pf);

108 volume XX Programmare 1

536.5.1 Funzione «fgets()»

||char *fgets (char *stringa, int dimensione, FILE *stream)

|

La funzione ‘fgets()’ permette di leggere una stringa corrispondente a una riga di testo da unfile. ‘fgets()’ impone l’indicazione della dimensione massima della riga, dal momento chequesta deve poi essere contenuta nell’array indicato come primo parametro. La dimensionemassima, indicata come secondo parametro, rappresenta la dimensione dell’array di caratteriche deve riceverlo. Dal momento che per essere una stringa, questa deve essere terminata,allora la dimensione massima della riga è di un carattere in meno rispetto alla dimensionedell’array.

Pertanto, la lettura del file avviene fino al raggiungimento della dimensione dell’array (menouno), oppure fino al raggiungimento del codice di interruzione di riga, che viene regolarmenteincluso nella riga letta, oppure anche fino alla conclusione del file.

Se l’operazione di lettura riesce, ‘fgets()’ restituisce un puntatore corrispondente alla stes-sa stringa (cioè l’array di caratteri di destinazione), altrimenti restituisce il puntatore nullo,‘NULL’, per esempio quando il file ha raggiunto la fine.

536.5.2 Funzione «fputs()»

||int fputs (const char *stringa, FILE *stream)

|

La funzione ‘fputs()’ permette di scrivere una stringa in un file di testo. La stringa vienescritta senza il codice di terminazione finale, ‘\0’.

Il valore restituito è un valore positivo in caso si successo, altrimenti ‘EOF’.

536.6 I/O standard

Ci sono tre file che risultano aperti automaticamente:

• standard input, corrispondente normalmente alla tastiera;

• standard output, corrispondente normalmente allo schermo del terminale;

• standard error, anch’esso corrispondente normalmente allo schermo del terminale.

Come è già stato visto in parte, si accede a questi flussi attraverso funzioni apposite, masi potrebbe accedere anche attraverso le normali funzioni di accesso ai file, utilizzando peridentificare i flussi i nomi: ‘stdio’, ‘stdout’ e ‘stderr’.

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

109Capitolo 537

C: istruzioni del preprocessoreIl preprocessore è un programma, o quella parte del compilatore, che si occupa di pre-elaborareun sorgente prima della compilazione vera e propria. In pratica, permette di generare un nuo-vo sorgente prima che questo venga compilato effettivamente. L’utilità della presenza di unpreprocessore, tra le altre cose, sta nella possibilità di definire gruppi di istruzioni alternativi aseconda di circostanze determinate.

Il linguaggio C non può fare a meno della presenza di un preprocessore, tanto che anche le suedirettive sono state regolate con lo standard ANSI.

537.1 Linguaggio a sé stante

Le direttive del preprocessore rappresentano un linguaggio a sé stante, con le sue regoleparticolari. In generale:

• le direttive iniziano con il simbolo ‘#’, preferibilmente nella prima colonna;

• le direttive non utilizzano alcun simbolo di conclusione (non si usa il punto e virgola);

• una riga non può contenere più di una direttiva.

Nelle sezioni seguenti vengono descritte le direttive più importanti.

537.1.1 Direttiva «#include»

||#include <file>

|

||#include "file"

|

La direttiva ‘#include’ permette di includere un file. Generalmente si tratta di un cosiddettofile di intestazione, contenente una serie di definizioni necessarie al file sorgente in cui vengonoincorporate.

Come si vede dalla sintassi, il file può essere indicato delimitandolo con le parentesi angolari,oppure con gli apici doppi.

|#include <stdio.h>

|#include "stdio.h"

Nel primo caso si fa riferimento a un file che dovrebbe trovarsi in una posizione stabilitadalla configurazione del compilatore (nel caso del C GNU in GNU/Linux, dovrebbe trattarsidella directory ‘/usr/include/’); nel secondo si fa riferimento a una posizione precisa, cherichiede l’indicazione di un percorso se non si tratta della stessa posizione in cui si trova ilsorgente in questione.

110 volume XX Programmare 1

Quando si indica un file da includere, delimitandolo con gli apici doppi e senza indicarealcun percorso, se non si trova il file nella directory corrente, il file viene cercato nelladirectory predefinita, come se fosse stato indicato tra le parentesi angolari.

Un file incorporato attraverso la direttiva ‘#include’, può a sua volta includerne altri; na-turalmente, questa possibilità va considerata per evitare di includere più volte lo stessofile.

537.1.2 Direttiva «#define»

||#define macro [sequenza_di_caratteri]

|

La direttiva ‘#define’ permette di definire dei nomi, conosciuti come macro, oppure «costantisimboliche» o «costanti manifeste». Quando queste macro vengono utilizzate nel sorgente,sono sostituite automaticamente con la sequenza che appare dopo la loro definizione.

|#define SALUTO ciao come stai

L’esempio appena mostrato fa in modo che il preprocessore sostituisca tutte le occorrenze di‘SALUTO’ con ‘ciao come stai’. È molto importante comprendere questo particolare: tuttociò che appare dopo il nome della macro viene utilizzato nella sostituzione.

|#define SALUTO "ciao come stai"

Questo nuovo esempio è diverso dal caso precedente, perché ci sono in più gli apici dop-pi. Questa volta, la macro ‘SALUTO’ potrebbe essere utilizzata in un’istruzione come quellaseguente, mentre non sarebbe stato possibile quando la sostituzione è stata definita senza apici:

|printf (SALUTO);

Visto questo, si può osservare che questa direttiva può essere utilizzata in modo più complesso,facendo anche riferimento ad altre macro già definite.

|#define UNO 1

|#define DUE UNO+UNO

|#define TRE DUE+UNO

In presenza di una situazione come questa, utilizzando la macro ‘TRE’, si ottiene prima lasostituzione con ‘DUE+UNO’, quindi con ‘UNO+UNO+1’, infine con ‘1+1+1’ (dopo, tocca alcompilatore).

L’utilizzo tipico delle macro è quello con cui si definiscono le dimensioni di qualcosa,per esempio gli array, e la corrispondenza reale di valori determinati che dipendono dallapiattaforma.

Per convenzione, i nomi utilizzati per le macro sono espressi solo con lettere maiuscole.

Come è possibile vedere meglio in seguito, è sensato anche dichiarare una macro senzaalcuna corrispondenza. Ciò può servire per le direttive ‘#ifdef’ e ‘#ifndef’.

C: istruzioni del preprocessore 111

537.1.3 Direttiva «#define» con argomento

||#define macro(argomento) sequenza_di_caratteri

|

La direttiva ‘#define’ può essere usata in modo simile a una funzione, per definire dellesostituzioni che includono in qualche modo un argomento. Seguendo l’esempio seguente,l’istruzione ‘i = DOPPIO(i)’ si traduce in ‘i = (i)+(i)’.

|#define DOPPIO(a) (a)+(a)

|...

|...

|i = DOPPIO(i);

|...

Si osservi il fatto che, nella definizione, la stringa di sostituzione è stata composta utilizzan-do le parentesi. Questo permette di evitare problemi successivamente, nelle precedenze divalutazione delle espressioni.

537.1.4 Direttive «#if», «#else», «#elif» e «#endif»

||#if espressione

| espressione|#endif

|

Le direttive ‘#if’, ‘#else’, ‘#elif’ e ‘#endif’, permettono di delimitare una porzione dicodice che debba essere utilizzato o ignorato in relazione a una certa espressione che puòessere calcolata solo attraverso definizioni precedenti.

|#define DIM_MAX 1000

|...

|...

|int main ()

|{|...

|#if DIM_MAX>100

| printf ("Dimensione enorme.");

| ...

|#else| printf ("Dimensione normale.");

| ...

|#endif|...

|}

L’esempio mostra in che modo si possa definire questa espressione, confrontando la macro‘DIM_MAX’ con il valore 100. Essendo stata dichiarata per tradursi in 1000, il confronto èequivalente a 1000 > 100 che risulta vero, pertanto il compilatore include solo le istruzionirelative.

112 volume XX Programmare 1

In particolare, l’istruzione ‘#elif’, come si può intuire, serve per costruire una catena dialternative else-if .

Gli operatori di confronto che si possono utilizzare per le espressioni logiche sono i soliti, inparticolare, è bene ricordare che per valutare l’uguaglianza si usa l’operatore ‘==’.

|#define NAZIONE ita

|...

|...

|int main ()

|{|#if NAZIONE==ita

| char valuta[] = "LIT";

| ...

|#elsif NAZIONE==usa

| char valuta[] = "USD";

| ...

|#endif|...

|}

Queste direttive condizionali possono essere annidate; inoltre possono contenere anche altritipi di direttiva del preprocessore.

537.1.5 Direttive «#ifdef» e «#ifndef»

Le direttive ‘#ifdef’ e ‘#ifndef’ si aggiungono a quelle descritte nella sezione precedente;servono per definire l’inclusione o l’esclusione di codice in base all’esistenza o meno di unamacro.

|#define DEBUG

|...

|int main ()

|{|...

|#ifdef DEBUG

| printf ("Punto di controllo n. 1\n");

| ...

|#endif|...

|}

L’esempio mostra il caso in cui sia dichiarata una macro ‘DEBUG’ (che non si traduce inalcunché) e in base alla sua esistenza viene incluso il codice che mostra un messaggioparticolare.

C: istruzioni del preprocessore 113

|#define OK

|...

|int main ()

|{|#ifndef OK

| printf ("Punto di controllo n. 1\n");

| ...

|#endif|...

|}

L’esempio appena mostrato è analogo a quello precedente, con la differenza che la direttiva‘#ifndef’ permette la compilazione delle istruzioni che controlla solo se la macro indicatanon è stata dichiarata.

L’uso delle direttive ‘#else’ e ‘#endif’ avviene nel modo già visto per la direttiva ‘#if’.

537.1.6 Direttiva «#undef»

||#undef macro

|

La direttiva ‘#undef’ permette di eliminare una macro a un certo punto del sorgente.

|#define NAZIONE ita

|...

|/* In questa posizione, NAZIONE risulta definita */

|...

|#undef NAZIONE

|...

|/* In questa posizione, NAZIONE non è definita */

|...

537.2 Macro predefinite

Il compilatore C ANSI prevede alcune macro predefinite. Il loro scopo è quello di an-notare informazioni legate alla compilazione nel file eseguibile finale (evidentemente a finidiagnostici).

537.2.1 File sorgente

Il programma può accedere all’informazione sul nome del file sorgente e della riga originale.Questi dati sono contenuti, rispettivamente, nelle macro ‘__FILE__’ e ‘__LINE__’. Questidati possono essere alterati nel sorgente, utilizzando la direttiva ‘#line’.

||#line numero_riga "nome_file"

|

114 volume XX Programmare 1

537.2.2 Data di compilazione

La data e l’ora della compilazione sono accessibili attraverso le macro ‘__DATE__’ e‘__TIME__’. Il formato della prima macro è la consueta stringa «mese/giorno/anno» e quellodella seconda è «ore:minuti:secondi».

537.2.3 C standard

Se il compilatore C che si utilizza è «standard», allora la macro ‘__STDC__’ corrisponde alvalore 1. Qualunque altro valore indica che non si tratta di un compilatore standard.

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

115Capitolo 538

C: esempi di programmazioneQuesto capitolo raccoglie solo alcuni esempi di programmazione, in parte già descritti in altricapitoli. Lo scopo di questi esempi è solo didattico, utilizzando forme non ottimizzate per lavelocità di esecuzione.

538.1 Problemi elementari di programmazione . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

538.1.1 Somma tra due numeri positivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 115

538.1.2 Moltiplicazione di due numeri positivi attraverso la somma . . . . . . . . . . . . 117

538.1.3 Divisione intera tra due numeri positivi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 118

538.1.4 Elevamento a potenza . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 119

538.1.5 Radice quadrata . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 120

538.1.6 Fattoriale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 121

538.1.7 Massimo comune divisore . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 123

538.1.8 Numero primo . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 124

538.2 Scansione di array . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

538.2.1 Ricerca sequenziale . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 125

538.2.2 Ricerca binaria . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 126

538.3 Algoritmi tradizionali . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

538.3.1 Bubblesort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 128

538.3.2 Torre di Hanoi . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 130

538.3.3 Quicksort . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 131

538.3.4 Permutazioni . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . . 133

538.1 Problemi elementari di programmazione

In questa sezione vengono mostrati alcuni algoritmi elementari portati in C. Per la spiegazionedegli algoritmi, se non sono già conosciuti, occorre leggere quanto riportato nel capitolo 530.

538.1.1 Somma tra due numeri positivi

Il problema della somma tra due numeri positivi, attraverso l’incremento unitario, è descrittonella sezione 530.2.1.|/* ================================================================= */

|/* somma <x> <y> */

|/* Somma esclusivamente valori positivi. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* somma (<x>, <y>) */

116 volume XX Programmare 1

|/* ----------------------------------------------------------------- */

|int somma (int x, int y)

|{| int z = x;

| int i;

|| for (i = 1; i <= y; i++)

| {| z++;

| };|| return z;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int y;

| int z;

|| /* Converte le stringhe ottenute dalla riga di comando in

| numeri interi e li assegna alle variabili x e y. */

| sscanf (argv[1], "%d", &x);

| sscanf (argv[2], "%d", &y);

|| z = somma (x, y);

|| printf ("%d + %d = %d\n", x, y, z);

|| return 0;

|}

In alternativa si può tradurre il ciclo ‘for’ in un ciclo ‘while’.

|int somma (int x, int y)

|{| int z = x;

| int i = 1;

|| while (i <= y)

| {| z++;

| i++;

| };|| return z;

|}

C: esempi di programmazione 117

538.1.2 Moltiplicazione di due numeri positivi attraverso la somma

Il problema della moltiplicazione tra due numeri positivi, attraverso la somma, è descritto nellasezione 530.2.2.|/* ================================================================= */

|/* moltiplica <x> <y> */

|/* Moltiplica esclusivamente valori positivi. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* moltiplica (<x>, <y>) */

|/* ----------------------------------------------------------------- */

|int moltiplica (int x, int y)

|{| int z = 0;

| int i;

|| for (i = 1; i <= y; i++)

| {| z = z + x;

| }|| return z;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int y;

| int z;

|| /* Converte le stringhe ottenute dalla riga di comando in

| numeri interi e li assegna alle variabili x e y. */

| sscanf (argv[1], "%d", &x);

| sscanf (argv[2], "%d", &y);

|| z = moltiplica (x, y);

|| printf ("%d * %d = %d\n", x, y, z);

|| return 0;

|}

In alternativa si può tradurre il ciclo ‘for’ in un ciclo ‘while’.

|int moltiplica (int x, int y)

|{

118 volume XX Programmare 1

| int z = 0;

| int i = 1;

|| while (i <= y)

| {| z = z + x;

| i++;

| }|| return z;

|}

538.1.3 Divisione intera tra due numeri positivi

Il problema della divisione tra due numeri positivi, attraverso la sottrazione, è descritto nellasezione 530.2.3.|/* ================================================================= */

|/* dividi <x> <y> */

|/* Divide esclusivamente valori positivi. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* dividi (<x>, <y>) */

|/* ----------------------------------------------------------------- */

|int dividi (int x, int y)

|{| int z = 0;

| int i = x;

|| while (i >= y)

| {| i = i - y;

| z++;

| }|| return z;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int y;

| int z;

|| /* Converte le stringhe ottenute dalla riga di comando in

| numeri interi e li assegna alle variabili x e y. */

C: esempi di programmazione 119

| sscanf (argv[1], "%d", &x);

| sscanf (argv[2], "%d", &y);

|| z = dividi (x, y);

|| printf ("Divisione intera - %d:%d = %d\n", x, y, z);

|| return 0;

|}

538.1.4 Elevamento a potenza

Il problema dell’elevamento a potenza tra due numeri positivi, attraverso la moltiplicazione, èdescritto nella sezione 530.2.4.|/* ================================================================= */

|/* exp <x> <y> */

|/* Eleva a potenza. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* exp (<x>, <y>) */

|/* ----------------------------------------------------------------- */

|int exp (int x, int y)

|{| int z = 1;

| int i;

|| for (i = 1; i <= y; i++)

| {| z = z * x;

| }|| return z;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int y;

| int z;

|| /* Converte le stringhe ottenute dalla riga di comando in

| numeri interi e li assegna alle variabili x e y. */

| sscanf (argv[1], "%d", &x);

| sscanf (argv[2], "%d", &y);

|

120 volume XX Programmare 1

| z = exp (x, y);

|| printf ("%d ** %d = %d\n", x, y, z);

|| return 0;

|}

In alternativa si può tradurre il ciclo ‘for’ in un ciclo ‘while’.

|int exp (int x, int y)

|{| int z = 1;

| int i = 1;

|| while (i <= y)

| {| z = z * x;

| i++;

| };|| return z;

|}

È possibile usare anche un algoritmo ricorsivo.

|int exp (int x, int y)

|{| if (x == 0)

| {| return 0;

| }| else if (y == 0)

| {| return 1;

| }| else

| {| return (x * exp (x, y-1));

| }|}

538.1.5 Radice quadrata

Il problema della radice quadrata è descritto nella sezione 530.2.5.

|/* ================================================================= */

|/* radice <x> */

|/* Radice quadrata. */

|/* ================================================================= */

||#include <stdio.h>

|

C: esempi di programmazione 121

|/* ================================================================= */

|/* radice (<x>) */

|/* ----------------------------------------------------------------- */

|int radice (int x)

|{| int z = 0;

| int t = 0;

|| while (1)

| {| t = z * z;

|| if (t > x)

| {| /* È stato superato il valore massimo. */

| z--;

| return z;

| }|| z++;

| }|| /* Teoricamente, non dovrebbe mai arrivare qui. */

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int z;

|| sscanf (argv[1], "%d", &x);

|| z = radice (x);

|| printf ("radq(%d) = %d\n", x, z);

|| return 0;

|}

538.1.6 Fattoriale

Il problema del fattoriale è descritto nella sezione 530.2.6.

|/* ================================================================= */

|/* fatt <x> */

|/* Fattoriale. */

|/* ================================================================= */

|

122 volume XX Programmare 1

|#include <stdio.h>

||/* ================================================================= */

|/* fatt (<x>) */

|/* ----------------------------------------------------------------- */

|int fatt (int x)

|{| int i = (x - 1);

|| while (i > 0)

| {| x = x * i;

| i--;

| }|| return x;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int z;

|| sscanf (argv[1], "%d", &x);

|| z = fatt (x);

|| printf ("%d! = %d\n", x, z);

|| return 0;

|}

In alternativa, l’algoritmo si può tradurre in modo ricorsivo.

|int fatt (int x)

|{| if (x > 1)

| {| return (x * fatt (x - 1));

| }| else

| {| return 1;

| }|}

C: esempi di programmazione 123

538.1.7 Massimo comune divisore

Il problema del massimo comune divisore, tra due numeri positivi, è descritto nella sezione530.2.7.|/* ================================================================= */

|/* mcd <x> <y> */

|/* Massimo comune divisore. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* mcd (<x>, <y>) */

|/* ----------------------------------------------------------------- */

|int mcd (int x, int y)

|{| while (x != y)

| {| if (x > y)

| {| x = x - y;

| }| else

| {| y = y - x;

| }| }|| return x;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

| int y;

| int z;

|| sscanf (argv[1], "%d", &x);

| sscanf (argv[2], "%d", &y);

|| z = mcd (x, y);

|| printf ("Il massimo comune divisore di %d e %d è %d\n", x, y, z);

|| return 0;

|}

124 volume XX Programmare 1

538.1.8 Numero primo

Il problema della determinazione se un numero sia primo o meno, è stato descritto nella sezione530.2.8.|/* ================================================================= */

|/* primo <x> */

|/* Numero primo. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* primo (<x>) */

|/* ----------------------------------------------------------------- */

|unsigned int primo (int x)

|{| unsigned int primo = 1;

| int i = 2;

| int j;

|| while ((i < x) && primo)

| {| j = x / i;

| j = x - (j * i);

|| if (j == 0)

| {| primo = 0;

| }| else

| {| i++;

| }| }|| return primo;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int x;

|| sscanf (argv[1], "%d", &x);

|| if (primo (x))

| {| printf ("%d è un numero primo\n", x);

| }| else

| {

C: esempi di programmazione 125

| printf ("%d non è un numero primo\n", x);

| }|| return 0;

|}

538.2 Scansione di array

In questa sezione vengono mostrati alcuni algoritmi, legati alla scansione degli array, portatiin C. Per la spiegazione degli algoritmi, se non sono già conosciuti, occorre leggere quantoriportato nel capitolo 530.

538.2.1 Ricerca sequenziale

Il problema della ricerca sequenziale all’interno di un array, è descritto nella sezione 530.3.1.

|/* ================================================================= */

|/* ricercaseq <x> <v1>... */

|/* Ricerca sequenziale. */

|/* ================================================================= */

||#include <stdio.h>

|#include <stdlib.h>

||/* ================================================================= */

|/* ricercaseq (<lista>, <x>, <ele-inf>, <ele-sup>) */

|/* ----------------------------------------------------------------- */

|int ricercaseq (int lista[], int x, int a, int z)

|{| int i;

|| /* Scandisce l’array alla ricerca dell’elemento. */

| for (i = a; i <= z; i++)

| {| if (x == lista[i])

| {| return i;

| }| }|| /* La corrispondenza non è stata trovata. */

| return -1;

|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| /* int lista[argc-2]; */

126 volume XX Programmare 1

| int *lista = (int *) malloc ((argc - 2) * sizeof (int));

| int x;

| int i;

|| /* Acquisisce il primo argomento come valore da cercare. */

| sscanf (argv[1], "%d", &x);

|| /* Considera gli argomenti successivi come gli elementi */

| /* dell’array da scandire. */

| for (i = 2; i < argc; i++)

| {| sscanf (argv[i], "%d", &lista[i-2]);

| }|| /* Esegue la ricerca. */

| i = ricercaseq (lista, x, 0, argc-2);

|| /* Emette il risultato. */

| printf ("%d si trova nella posizione %d\n", x, i);

|| return 0;

|}

Esiste anche una soluzione ricorsiva che viene mostrata nella subroutine seguente:

|int ricercaseq (int lista[], int x, int a, int z)

|{| if (a > z)

| {| /* La corrispondenza non è stata trovata. */

| return -1;

| }| else if (x == lista[a])

| {| return a;

| }| else

| {| return ricercaseq (lista, x, a+1, z);

| }|}

538.2.2 Ricerca binaria

Il problema della ricerca binaria all’interno di un array, è descritto nella sezione 530.3.2.

|/* ================================================================= */

|/* ricercabin <x> <v1>... */

|/* Ricerca binaria. */

|/* ================================================================= */

||#include <stdio.h>

C: esempi di programmazione 127

|#include <stdlib.h>

||/* ================================================================= */

|/* ricercabin (<lista>, <elemento>, <inizio>, <fine>) */

|/* ----------------------------------------------------------------- */

|int ricercabin (int lista[], int x, int a, int z)

|{| int m;

|| /* Determina l’elemento centrale. */

| m = (a + z) / 2;

|| if (m < a)

| {| /* Non restano elementi da controllare: l’elemento cercato */

| /* non c’è. */

| return -1;

| }| else if (x < lista[m])

| {| /* Si ripete la ricerca nella parte inferiore. */

| return ricercabin (lista, x, a, m-1);

| }| else if (x > lista[m])

| {| /* Si ripete la ricerca nella parte superiore. */

| return ricercabin (lista, x, m+1, z);

| }| else

| {| /* m rappresenta l’indice dell’elemento cercato. */

| return m;

| }|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| /* int lista[argc-2]; */

| int *lista = (int *) malloc ((argc - 2) * sizeof (int));

| int x;

| int i;

|| /* Acquisisce il primo argomento come valore da cercare. */

| sscanf (argv[1], "%d", &x);

|| /* Considera gli argomenti successivi come gli elementi */

| /* dell’array da scandire. */

| for (i = 2; i < argc; i++)

| {| sscanf (argv[i], "%d", &lista[i-2]);

128 volume XX Programmare 1

| }|| /* Esegue la ricerca. */

| i = ricercabin (lista, x, 0, argc-2);

|| /* Emette il risultato. */

| printf ("%d si trova nella posizione %d\n", x, i);

|| return 0;

|}

538.3 Algoritmi tradizionali

In questa sezione vengono mostrati alcuni algoritmi tradizionali portati in C. Per la spiegazionedegli algoritmi, se non sono già conosciuti, occorre leggere quanto riportato nel capitolo 530.

538.3.1 Bubblesort

Il problema del Bubblesort è stato descritto nella sezione 530.4.1. Viene mostrata prima unasoluzione iterativa e successivamente la funzione ‘bsort’ in versione ricorsiva.|/* ================================================================= */

|/* bsort <valore>... */

|/* BubbleSort. */

|/* ================================================================= */

||#include <stdio.h>

|#include <stdlib.h>

||/* ================================================================= */

|/* bsort (<lista>, <inizio>, <fine>) */

|/* ----------------------------------------------------------------- */

|void bsort (int lista[], int a, int z)

|{| int scambio;

| int j;

| int k;

|| /* Inizia il ciclo di scansione dell’array. */

| for (j = a; j < z; j++)

| {| /* Scansione interna dell’array per collocare nella */

| /* posizione j l’elemento giusto. */

| for (k = j+1; k <= z; k++)

| {| if (lista[k] < lista[j])

| {| /* Scambia i valori. */

| scambio = lista[k];

| lista[k] = lista[j];

C: esempi di programmazione 129

| lista[j] = scambio;

| }| }| }|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| /* int lista[argc-1]; */

| int *lista = (int *) malloc ((argc - 1) * sizeof (int));

| int i;

|| /* Considera gli argomenti come gli elementi */

| /* dell’array da ordinare. */

| for (i = 1; i < argc; i++)

| {| sscanf (argv[i], "%d", &lista[i-1]);

| }|| /* Esegue il riordino. */

| bsort (lista, 0, argc-2);

|| /* Emette il risultato. */

| for (i = 0; i < (argc-1); i++)

| {| printf ("%d ", lista[i]);

| }| printf ("\n");

|| return 0;

|}

Segue la funzione ‘bsort’ in versione ricorsiva.

|void bsort (int lista[], int a, int z)

|{| int scambio;

| int k;

|| if (a < z)

| {| /* Scansione interna dell’array per collocare nella */

| /* posizione a l’elemento giusto. */

| for (k = a+1; k <= z; k++)

| {| if (lista[k] < lista[a])

| {| /* Scambia i valori. */

| scambio = lista[k];

| lista[k] = lista[a];

130 volume XX Programmare 1

| lista[a] = scambio;

| }| }|| bsort (lista, a+1, z);

| }|}

538.3.2 Torre di Hanoi

Il problema della torre di Hanoi è descritto nella sezione 530.4.3.

|/* ================================================================= */

|/* hanoi <n-anelli> <piolo-iniziale> <piolo-finale> */

|/* Torre di Hanoi. */

|/* ================================================================= */

||#include <stdio.h>

||/* ================================================================= */

|/* hanoi (<n-anelli>, <piolo-iniziale>, <piolo-finale>) */

|/* ----------------------------------------------------------------- */

|void hanoi (int n, int p1, int p2)

|{| if (n > 0)

| {| hanoi (n-1, p1, 6-p1-p2);

| printf ("Muovi l’anello %d dal piolo %d al piolo %d\n", n, p1, p2);

| hanoi (n-1, 6-p1-p2, p2);

| }|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| int n;

| int p1;

| int p2;

|| sscanf (argv[1], "%d", &n);

| sscanf (argv[2], "%d", &p1);

| sscanf (argv[3], "%d", &p2);

|| hanoi (n, p1, p2);

|| return 0;

|}

C: esempi di programmazione 131

538.3.3 Quicksort

L’algoritmo del Quicksort è stato descritto nella sezione 530.4.4.

|/* ================================================================= */

|/* qsort <valore>... */

|/* QuickSort. */

|/* ================================================================= */

||#include <stdio.h>

|#include <stdlib.h>

||/* ================================================================= */

|/* part (<lista>, <inizio>, <fine>) */

|/* ----------------------------------------------------------------- */

|int part (int lista[], int a, int z)

|{| /* Viene preparata una variabile per lo scambio di valori. */

| int scambio = 0;

|| /* Si assume che a sia inferiore a z. */

| int i = a + 1;

| int cf = z;

|| /* Inizia il ciclo di scansione dell’array. */

| while (1)

| {| while (1)

| {| /* Sposta i a destra. */

| if ((lista[i] > lista[a]) || (i >= cf))

| {| break;

| }| else

| {| i += 1;

| }| }| while (1)

| {| /* Sposta cf a sinistra. */

| if (lista[cf] <= lista[a])

| {| break;

| }| else

| {| cf -= 1;

| }| }| if (cf <= i)

| {| /* È avvenuto l’incontro tra i e cf. */

132 volume XX Programmare 1

| break;

| }| else

| {| /* Vengono scambiati i valori. */

| scambio = lista[cf];

| lista[cf] = lista[i];

| lista[i] = scambio;

|| i += 1;

| cf -= 1;

| }| }|| /* A questo punto lista[a..z] è stata ripartita e cf è la */

| /* collocazione di lista[a]. */

| scambio = lista[cf];

| lista[cf] = lista[a];

| lista[a] = scambio;

|| /* A questo punto, lista[cf] è un elemento (un valore) nella */

| /* giusta posizione. */

| return cf;

|}||/* ================================================================= */

|/* quicksort (<lista>, <inizio>, <fine>) */

|/* ----------------------------------------------------------------- */

|void quicksort (int lista[], int a, int z)

|{| /* Viene preparata la variabile cf. */

| int (cf) = 0;

|| if (z > a)

| {| cf = part (lista, a, z);

| quicksort (lista, a, cf-1);

| quicksort (lista, cf+1, z);

| }|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| /* int lista[argc-1]; */

| int *lista = (int *) malloc ((argc - 1) * sizeof (int));

| int i;

|| /* Considera gli argomenti come gli elementi */

| /* dell’array da ordinare. */

| for (i = 1; i < argc; i++)

C: esempi di programmazione 133

| {| sscanf (argv[i], "%d", &lista[i-1]);

| }|| /* Esegue il riordino. */

| quicksort (lista, 0, argc-2);

|| /* Emette il risultato. */

| for (i = 0; i < (argc-1); i++)

| {| printf ("%d ", lista[i]);

| }| printf ("\n");

|| return 0;

|}

538.3.4 Permutazioni

L’algoritmo ricorsivo delle permutazioni è descritto nella sezione 530.4.5.

|/* ================================================================= */

|/* permuta <valore>... */

|/* Permutazioni. */

|/* ================================================================= */

||#include <stdio.h>

|#include <stdlib.h>

||/* Variabile globale. */

|int iDimArray;

||/* ================================================================= */

|/* visualizza (<array>, <dimensione>) */

|/* ----------------------------------------------------------------- */

|void visualizza (int lista[], int dimensione)

|{| int i;

|| for (i = 0; i < dimensione; i++)

| {| printf ("%d ", lista[i]);

| }| printf ("\n");

|}||/* ================================================================= */

|/* permuta (<lista>, <inizio>, <fine>) */

|/* ----------------------------------------------------------------- */

|void permuta (int lista[], int a, int z)

|{

134 volume XX Programmare 1

| int scambio;

| int k;

|| /* Se il segmento di array contiene almeno due elementi, si */

| /* procede. */

| if ((z - a) >= 1)

| {| /* Inizia un ciclo di scambi tra l’ultimo elemento e uno */

| /* degli altri contenuti nel segmento di array. */

| for (k = z; k >= a; k--)

| {| /* Scambia i valori. */

| scambio = lista[k];

| lista[k] = lista[z];

| lista[z] = scambio;

|| /* Esegue una chiamata ricorsiva per permutare un */

| /* segmento più piccolo dell’array. */

| permuta (lista, a, z-1);

|| /* Scambia i valori. */

| scambio = lista[k];

| lista[k] = lista[z];

| lista[z] = scambio;

| }| }| else

| {| /* Visualizza l’array e utilizza una variabile dichiarata */

| /* globalmente. */

| visualizza (lista, iDimArray);

| }|}||/* ================================================================= */

|/* Inizio del programma. */

|/* ----------------------------------------------------------------- */

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

|{| /* int lista[argc-1]; */

| int *lista = (int *) malloc ((argc - 1) * sizeof (int));

| int i;

|| /* Considera gli argomenti come gli elementi */

| /* dell’array da permutare. */

| for (i = 1; i < argc; i++)

| {| sscanf (argv[i], "%d", &lista[i-1]);

| }|| /* Salva la dimensione dell’array nella variabile globale. */

| iDimArray = argc-1;

|

C: esempi di programmazione 135

| /* Esegue le permutazioni. */

| permuta (lista, 0, argc-2);

|| return 0;

|}

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉

136Capitolo 539

Automazione della compilazione: Make e file-makeLa compilazione di un programma, in qualunque linguaggio sia scritto, può essere un’opera-zione molto laboriosa, soprattutto se si tratta di aggregare un sorgente suddiviso in più parti.Una soluzione potrebbe essere quella di predisporre uno script che esegue sequenzialmentetutte le operazioni necessarie, ma la tradizione impone di utilizzare il programma Make.

Uno dei vantaggi più appariscenti sta nella possibilità di evitare che vengano ricompilati i filesorgenti che non sono stati modificati, abbreviando quindi il tempo di compilazione necessarioquando si procede a una serie di modifiche limitate.

539.1 Make

Make, per la precisione l’eseguibile ‘make’, viene utilizzato normalmente assieme a un file, ilfile-make (o makefile), il cui nome può essere generalmente ‘makefile’ o ‘Makefile’, dovetra i due si tende a preferire l’ultimo con l’iniziale maiuscola. Il file-make serve a elencare aMake le operazioni da compiere e le interdipendenze che ci sono tra le varie fasi.

Make può anche essere usato da solo, senza file-make, per compilare un solo sorgente; inquesto caso, tenta di determinare l’operazione da compiere più adatta, in base all’estensione delsorgente stesso. Per esempio, se esiste il file ‘prova.c’ nella directory corrente, il comando

$ make prova [ Invio ]

fa sì che ‘make’ avvii in pratica il comando seguente:

$ cc -o prova prova.c [ Invio ]

Se invece esistesse un file-make, lo stesso comando, ‘make prova’, avrebbe un significatodiverso, corrispondendo alla ricerca di un obiettivo con il nome ‘prova’ all’interno del file-make stesso.

539.2 File-make

Un file-make è uno script specializzato per l’automazione della compilazione attraverso Make.Contiene la definizione di macro, simili alle variabili di ambiente di uno script di shell, e diobiettivi che rappresentano le varie operazioni da compiere.

All’interno di questi file, il simbolo ‘#’ rappresenta l’inizio di un commento, cioè di una partedi testo che non viene interpretata da Make.

539.2.1 Macro

La definizione di una macro avviene in modo molto semplice, indicando l’assegnamento diuna stringa a un nome che da quel momento la rappresenta.

||nome = stringa

|

Automazione della compilazione: Make e file-make 137

La stringa non deve essere delimitata. Il funzionamento è molto simile alle variabili di ambien-te dichiarate all’interno di uno script di shell. L’esempio seguente definisce la macro ‘prefix’che da quel punto in poi equivale a ‘/usr/local’:

|prefix=/usr/local

La sostituzione di una macro si indica attraverso due modi possibili:

||$(nome)

|

Oppure:

||${nome}

|

Nell’esempio seguente, la macro ‘exec_prefix’ viene generata a partire dal contenuto di‘prefix’.

|prefix=/usr/local|exec_prefix=$(prefix)

Esistono alcune macro predefinite il cui contenuto può anche essere modificato. Le piùimportanti sono elencate nella tabella 539.3.

|Tabella 539.3. Elenco di alcune macro predefinite di Make.

Nome ContenutoMAKE makeAR arARFLAGS rwYACC yaccYFLAGSLEX lexLFLAGSLDFLAGSCC ccCFLAGSFC f77FFLAGS

Per verificare il contenuto delle macro predefinite, si può predisporre un file-make simile aquello seguente, eseguendo poi semplicemente ‘make’ (i vari comandi ‘echo’ sono rientraticon un carattere di tabulazione).

138 volume XX Programmare 1

|all:| @echo MAKE $(MAKE) ; \

| echo AR $(AR) ; \

| echo ARFLAGS $(ARFLAGS) ; \

| echo YACC $(YACC) ; \

| echo YFLAGS $(YFLAGS) ; \

| echo LEX $(LEX) ; \

| echo LFLAGS $(LFLAGS) ; \

| echo LDFLAGS $(LDFLAGS) ; \

| echo CC $(CC) ; \

| echo CFLAGS $(CFLAGS) ; \

| echo FC $(FC) ; \

| echo FFLAGS $(FFLAGS)

Oltre alle macro predefinite ne esistono altre, la cui utilità si può comprendere in seguito.

|Tabella 539.5. Elenco di alcune macro interne.

Macro Significato

|$< Il nome del file per il quale è stato scelto l’obiettivo per deduzione.

|$* Il nome dell’obiettivo senza suffisso.

|$@ L’obiettivo della regola specificata.

539.2.2 Regole

Le regole sono il fondamento dei file-make. Attraverso di esse si stabiliscono degli obiettiviabbinati ai comandi necessari per ottenerli.

||obiettivo... : [dipendenza...]|<HT>comando[; comando]...

|

La sintassi indica un comando che deve essere eseguito per raggiungere uno degli obiettivinominati all’inizio, con le dipendenze che devono essere soddisfatte. In pratica, non si puòeseguire il comando se prima non esistono i file indicati nelle dipendenze.

La dichiarazione inizia a partire dalla prima colonna, con il nome del primo obiettivo,mentre i comandi devono iniziare dopo un carattere di tabulazione.

L’esempio seguente mostra una regola attraverso cui si dichiara il comando necessario a ese-guire il link di un programma oggetto, specificando che questo può essere eseguito solo quandoesiste già il file oggetto in questione.

|mio_prog: prova.o

| cc -o prova prova.o

Il comando indicato in una regola, può proseguire su più righe successive, basta concluderela riga, prima del codice di interruzione di riga, con una barra obliqua inversa (nella sezione

Automazione della compilazione: Make e file-make 139

precedente è già stato mostrato un esempio di questo tipo). Quello che conta è che le righeaggiuntive inizino sempre dopo un carattere di tabulazione.

Il comando di una regola può iniziare con un prefisso particolare:

Prefisso Significato

|- fa in modo che gli errori vengano ignorati;

|+ fa in modo che il comando venga eseguito sempre;

|@ fa in modo che il testo del comando non venga mostrato.

539.3 Regole deduttive

Make prevede alcune regole predefinite, o deduttive, riferite ai suffissi dei file indicati comeobiettivo. Si distingue tra due tipi di regole deduttive: a suffisso singolo e a suffisso doppio.La tabella 539.8 ne riporta alcune per chiarire il concetto.

|Tabella 539.8. Elenco di regole deduttive a singolo e a doppio suffisso.

Obiettivo Comando corrispondente‘.c’ $(CC) $(CFLAGS) $(LDFLAGS) −o $@ $<‘.f’ $(FC) $(FFLAGS) $(LDFLAGS) −o $@ $<‘.c.o’ $(CC) $(CFLAGS) −o $<‘.f.o’ $(FC) $(FFLAGS) −o $<

539.4 File-make tipico

Il file-make tipico, permette di automatizzare tutte le fasi legate alla ricompilazione di unprogramma e alla sua installazione. Si distinguono alcuni obiettivi comuni, usati di frequente:

Prefisso Significato

|all utile per definire l’azione da compiere quando non si indicaalcun obiettivo;

|clean per eliminare i file oggetto e i binari già compilati;

|install per installare il programma eseguibile dopo la compilazione.

Si può ricordare che le fasi tipiche di un’installazione di un programma distribuito in formasorgente sono appunto:

# make [ Invio ]

che richiama automaticamente l’obiettivo ‘all’ del file-make, coincidente con i comandinecessari per la compilazione del programma, e

# make install [ Invio ]

che provvede a installare gli eseguibili compilati nella loro destinazione prevista.

140 volume XX Programmare 1

Supponendo di avere realizzato un programma, denominato ‘mio_prog.c’, il cui eseguibiledebba essere installato nella directory ‘/usr/local/bin/’, si potrebbe utilizzare un file-make composto come l’esempio seguente:

|prefix=/usr/local|bindir=${prefix}/bin||all:| cc -o mio_prog mio_prog.c

||clean:| rm -f core *.o mio_prog

||install:| cp mio_prog $(bindir)

Come si può osservare, sono state definire le macro ‘prefix’ e ‘bindir’ in modo da facilitarela modifica della destinazione del programma installato, senza intervenire sui comandi.

L’obiettivo ‘clean’ elimina un eventuale file ‘core’, generato da un errore irreversibile duran-te l’esecuzione del programma, probabilmente mentre lo si prova tra una compilazione e l’altra,quindi elimina gli eventuali file oggetto e infine l’eseguibile generato dalla compilazione.

Appunti di informatica libera 2007.02 --- Copyright 2000-2007 Daniele Giacomini -- 〈daniele (ad) swlibero·org 〉