Traccia della lezione Lez18: Lucido 2: L'abitudine alla lettura ci fa dimenticare che una sequenza di caratteri non ha un significato ovvio e neppure una struttura ovvia, cioè non è ovvio che gli spazi bianchi e gli a capo separino unità concettuali diverse, che le cifre costituiscano numeri e le lettere parole, che i numeri contenenti una virgola (in C un punto) siano numeri reali anziché interi. Un programma che legge e interpreta i dati deve essere in grado di effettuare questa conversione, che ci pare ovvia, ma non è affatto elementare. I tre casi elencati (tastiera, stringa, file) sembrano molto diversi, ma in tutti e tre si ha una sequenza di caratteri che vanno acquisiti, scomposti in blocchi elementari e "interpretati", cioè devono ricevere dal programma un "significato", nel senso di essere usati in un certo modo anziché in un altro. Da questo punto di vista, non c'è differenza tra i tre casi. L'unica differenza è che tastiera e file sono flussi (stream) di caratteri che arrivano dall'esterno, mentre le stringhe sono già in memoria, per cui non vanno lette. Un'altra differenza è che le stringhe si possono anche trattare come vettori, accedendo in maniera diretta ai caratteri anziché procedere in ordine dal primo in avanti, mentre gli stream si leggono sempre in ordine. Tuttavia, quando si usa una stringa come stream, non si sfrutta l'accesso diretto ai caratteri, e quindi questa differenza non c'è. Il parsing è anche essenziale nella compilazione di programmi: il compilatore legge un file di testo, che contiene il codice C di un programma, e lo "interpreta", costruendo un file binario che contiene un codice macchina corrispondente al codice C. Lucido 3: La funzione scanf non fa rigorosamente parte del linguaggio C, ma della libreria standard: per usarla, bisogna includere la libreria . La funzione "scanf" confronta la stringa di formato e l'inizio dello stream in ingresso. Riconosce via via nello stream in ingresso elementi compatibili con quelli della stringa di formato e li salva nei p-1 oggetti passati come parametri dopo la stringa di formato. Si ferma quando termina la stringa o quando trova nello stream un elemento incompatibile con la stringa di formato. Alla fine, restituisce il numero di elementi riconosciuti e assegnati a oggetti. La ricerca di oggetti che combaciano con il pattern non avviene nell'intero stream di ingresso, ma in modo sequenziale a partire dalla posizione corrente. Quindi, se si sta cercando un numero intero e lo stream contiene prima lettere dell'alfabeto e poi un numero, la ricerca fallisce e si interrompe subito. La funzione scanf non termina alla fine dell'ingresso, ma alla fine della stringa di formato. Se l'ingresso contiene più oggetti di quelli chiesti dalla stringa, essi rimangono accodati e verranno passati alla successiva scanf. Se ne contiene di meno, il computer rimane in attesa dei successivi e non esegue la funzione scanf. Si noti che tutte le funzioni viste finora avevano un numero di parametri assegnato a priori nella loro dichiarazione. Si vedrà che la libreria stdarg.h consente di dichiarare funzioni con un numero di parametri non definito a priori, cioè diverso da una chiamata all'altra. Le funzioni di parsing, come scanf, sono il principale esempio di tali funzioni. I p-1 parametri che seguono la stringa di formato sono tutti puntatori, cioè indirizzi: in genere richiedono una & applicata al nome di una variabile (o elemento di vettore o campo di struttura). E' facile ricordarlo se si pensa che non sono dati, ma risultati, che vengono passati alla funzione come finti dati affinché essa li modifichi. Quindi devono essere passati per indirizzo. Lucido 4: Nella versione più elementare, la stringa di formato è costituita solo da caratteri, che vengono ricercati pari pari nello stream in ingresso, e la funzione procede finché i due sono perfettamente uguali: - se la stringa è "prova10" e l'utente scrive "prova10" con la tastiera, la chiamata scanf("prova10") scorre stringa e stream fino in fondo; - se la stringa è "prova10" e l'utente scrive "prova01" con la tastiera, la chiamata scanf("prova10") scorre stringa e stream fino alla 'a' compresa e si ferma; nello stream rimane accodata la stringa "01" ed eventuali future chiamate a scanf partono da lì; - se la stringa è "prova10" e l'utente scrive "prova" con la tastiera, la chiamata scanf("prova10") scorre lo stream fino in fondo e ignora la parte mancante della stringa di formato. Tutto questo è ovviamente inutile, perché l'obiettivo non è scorrere lo stream di ingresso, ma estrarne dati da caricare in celle di memoria. Prima di arrivare ai dati veri e propri, discutiamo il concetto di "separatore". Con "separatore" o "blank" si intende ogni carattere che divide "parole". Questi sono lo spazio bianco, la tabulazione e l'a capo. Per i separatori non è richiesta una corrispondenza perfetta fra stringa di formato e stream in ingresso: il numero di separatori nei due può essere diverso. Questo serve a non ingessare troppo il formato di ingresso: se la lettura fosse troppo pignola, sarebbe difficilissimo passare i dati. Quindi la stringa di formato "prova 10" è compatibile con gli stream "prova10", "prova 10", "prova 10", ecc... Permane una limitazione fondamentale: se la stringa di formato non indica separatori, non devono essercene neppure nello stream in ingresso. Così la stringa "Numero 10/ 5" è incompatibile con "Numero 10 /5", perché il carattere '/' deve immediatamente seguire il "10", mentre e' compatibile con "Numero 10/5" perché il separatore dopo il carattere '/' può scomparire. In breve: passando dalla stringa di formato allo stream in ingresso i separatori possono scomparire, ma non apparire. La comparsa di separatori è consentita prima delle specifiche di conversione. Così la stringa "Numero %d/%d" è compatibile con "Numero -6/ 0" perché la seconda specifica di conversione %d consente di inserire separatori prima del numero intero, mentre è incompatibile con "Numero -6 /0" perché non si possono aggiungere separatori fra il primo numero e il carattere '/'. La stringa di formato "Numero %d /%d" invece è compatibile con "Numero -6/0", "Numero -6/ 0", "Numero -6 /0" e "Numero -6 / 0" perché i separatori possono scomparire, e possono comparire prima delle specifiche di conversione. Il consiglio elementare è, in caso di dubbio, mettere i separatori nella stringa di formato; tanto, possono sparire. Il numero dei separatori è irrilevante, per cui tanto vale usarne uno solo. Il codice PARSING_INT.C illustra alcune caratteristiche della funzione scanf. Il programma chiede all'utente di inserire da tastiera due numeri interi (separati da un numero qualsiasi di "bianchi" intermedi), li stampa, chiede di inserirne altri due, li stampa, chiede di inserirne un quinto e lo stampa: - se nello stream sono presenti oggetti diversi da intero (numero reale o stringa), la lettura si interrompe e il controllo sul numero di oggetti letti rivela l'errore. - se si inserisce un numero reale, il codice interpreta la parte intera come intero, ma poi il punto impedisce di continuare il parsing, per cui il codice termina - se si inserisce un solo intero e si preme INVIO, il codice attende l'inserimento del secondo intero - se si inseriscono più di due interi, le successive richieste di inserimento usano gli interi inseriti in più Si noti che la stringa di formato nelle prime due chiamate è diversa: "%d%d" nella prima, "%d %d" nella seconda. Questo non provoca alcuna differenza, dato che nel caso "%d%d" le specifiche di conversione ammettono che gli interi nello stream siano separati (altrimenti non si potrebbero neppure distinguere tra loro); gli spazi potrebbero essere in numero qualsiasi sia nello stream di ingresso sia nella stringa di formato: quelli in più vengono semplicemente ignorati Si ricorda che, per interrompere un programma in esecuzione, in Windows basta premere contemporaneamente i tasti CTRL e C. Lucido 5: Il codice PARSING_DATA.C illustra l'esempio di lettura della data. Si noti che inserendo la data con i numeri separati da '-' anziché '/', la lettura si interrompe al primo carattere errato, per cui si legge solo il primo intero. Si noti che la specifica %s non si limita a copiare l'oggetto nella stringa, ma vi aggiunge il terminatore, per produrre una stringa C standard. Lo stesso fa la specifica %[set], che è un caso particolare della %s, dato che anch'essa riconosce stringhe. Il codice PARSING_STR.C illustra la lettura di stringhe. Il codice chiede di inserire la stringa "prova". Ovviamente, dato che la stringa di formato non contiene specifiche di conversione, non ci sono riconoscimenti di oggetti, e la funzione restituisce sempre 0. Quindi, non testiamo il valore restituito. Poi chiede due parole, le stampa, ne chiede altre due e le stampa. - se si inseriscono più di due parole, quelle eccedenti vengono passate alla lettura successiva - i numeri vengono trattati come parole - se nell'inserire "prova" si fa un errore, il parsing termina sul primo carattere errato, per cui le successive operazioni partiranno da lì; non ci sono segnalazioni di errore - il numero di separatori fra le parole nello stream è ininfluente: vengono saltati mentre si cercano le parole stesse; i separatori possono anche essere tabulazioni e a capo. Si noti che i parametri di scanf qui non sono dereferenziati con & perché sono stringhe, e quindi puntatori. Lo stesso succederebbe se la chiamata alla funzione scanf si trovasse all'interno di una funzione e le variabili da leggere dovessero essere passate all'esterno per indirizzo: siccome le variabili sarebbero già dei puntatori, non ci sarebbe bisogno di &. Per esempio: void LeggeInteri (int *pi1, int *pi2) { scanf("%d %d",pi1,pi2); } Si noti che le stringhe s1, s2, s3 e s4 devono avere allocato spazio sufficiente a contenere la parola dello stream (statico, come nell'esempio, o dinamico). In caso contrario, si ha sforamento dei limiti di memoria, con possibili errori. Il codice PARSING_CHAR.C mostra alcune conseguenze fastidiose delle regole di corrispondenza quando applicate alla lettura di caratteri. Il codice chiede di inserire due caratteri, li stampa (carattere e codice intero corrispondente), poi chiede di inserirne un terzo e lo stampa. - se si inserisce un solo carattere e si preme INVIO, il codice non attende il secondo, perché l'INVIO viene interpretato come un carattere - se si inseriscono più di due caratteri, le successive richieste di inserimento usano i caratteri inseriti in più - se si inseriscono due caratteri separati da uno spazio, questo viene interpretato come il secondo carattere, mentre il secondo carattere e l'INVIO finale vengono interpretati come i due caratteri successivi - la seconda richiesta ha stringa di formato "%c %c" anziché "%c%c"; questo consente di inserire i due caratteri separati da un numero qualsiasi di spazi bianchi, tabulazioni o a capi, che verranno ignorati (altrimenti sarebbero interpretati come il secondo carattere della coppia) Quasi sempre è più comodo usare la specifica di conversione "%s", che consente di leggere parole intere ignorando i caratteri separatori. Se occorrono i singoli caratteri, si possono estrarre in seguito dalle stringhe. Lucido 6: "%d%d" significa: numero intero (preceduto eventualmente da separatori) numero intero (preceduto eventualmente da separatori) Quindi vanno bene gli stream in ingresso: " 12 34" "12 34" "12 34" mentre con gli stream: "12,34" " 12 ,34" " 12, 34" scanf riconosce 12, salta gli eventuali separatori e si ferma alla virgola (era atteso un altro numero intero) "%d,%d" significa: numero intero (preceduto eventualmente da separatori), virgola (immediatamente e senza separatori), numero intero (preceduto eventualmente da separatori) Quindi vanno bene gli stream in ingresso: " 12, 34" "12,34" "12, 34" mentre con gli stream: " 12 ,34" "12 ,34" " 12 , 34" scanf riconosce 12 e si ferma al primo spazio successivo (era attesa la virgola) "%d ,%d" significa: numero intero (preceduto eventualmente da separatori), sequenza di separatori (anche zero), virgola, numero intero (preceduto eventualmente da separatori) Quindi vanno bene gli stream in ingresso: " 12 , 34" "12 ,34" "12, 34" mentre con gli stream: " 12 34" "12 34" scanf riconosce 12 e si ferma alla prima cifra di 34 (era attesa la virgola) "%d, %d" significa: numero intero (preceduto eventualmente da separatori), virgola (immediatamente senza separatori), sequenza di separatori (anche zero), numero intero (preceduto eventualmente da separatori) Quindi vanno bene gli stream in ingresso: " 12, 34" "12,34" "12, 34" mentre con gli stream: " 12 ,34" "12 ,34" " 12 , 34" scanf riconosce 12 e si ferma al primo spazio successivo (era attesa la virgola) "%d , %d" significa: numero intero (preceduto eventualmente da separatori), sequenza di separatori (anche zero), virgola, sequenza di separatori (anche zero), numero intero (preceduto eventualmente da separatori) Quindi vanno bene gli stream in ingresso: " 12 , 34" "12 ,34" "12, 34" "12,34" mentre con gli stream: " 12 34" "12 34" scanf riconosce 12 e si ferma alla prima cifra di 34 (era attesa la virgola) In linea di principio, aggiungendo separatori alla stringa di formato non si sbaglia, perché possono anche mancare nello stream in ingresso, mentre non vale il contrario. Lucido 7: La differenza fondamentale fra %3c e %3s è che con la seconda la funzione scanf inserisce il terminatore alla fine della stringa. char s1[6]; strcpy(s1,"prova"); scanf("%3s",s1); /* l'utente inserisce da tastiera "bra" */ printf("%s",s1); /* il programma stampa "bra" */ strcpy(s1,"prova"); scanf("%3c",s1); /* l'utente inserisce da tastiera "bra" */ printf("%s",s1); /* il programma stampa "brava" */ Ovviamente, nell'assegnare i valori agli oggetti, valgono le regole di conversione implicita. Un tipico errore è leggere un numero reale avendo specificato un intero nella stringa di formato. Il numero restituito da scanf è il numero di oggetti riconosciuti nell'ingresso e assegnati a puntatori. Gli oggetti riconosciuti, ma non assegnati a causa del soppressore (*) non entrano nel conteggio. Quindi, scanf("%*d",&i) restituisce 0 e lascia i invariato. Il soppressore serve a saltare dati non necessari. Lucido 8: ingresso "12 34" chiamata n = scanf("%*d%d",&i); Il primo intero (12) viene riconosciuto, ma non assegnato. Il secondo intero (34) viene riconosciuto, saltando i separatori, e assegnato alla variabile "i". Quindi "n" vale 1 e "i" vale 34. ingresso "Un due tre" chiamata n = scanf("%*s%s",s); La prima parola ("Un") viene riconosciuta, ma non assegnata. La seconda ("due") viene riconosciuta, saltando i separatori iniziali, e assegnata alla variabile "s". Quindi "n" vale 1 e "s" vale "due". Si noti che "s" viene passata per valore, dato che è una stringa, cioè un vettore (si modificano i singoli caratteri, non la stringa). ingresso "12345" chiamata n = scanf("%1d%2d%3d",&i,&j,&k); Il primo intero è di una sola cifra (1); viene riconosciuto e assegnato alla variabile "i". Il secondo è di due cifre (23); viene riconosciuto e assegnato alla variabile "j". Il terzo è al massimo di tre cifre, ma l'ingresso ne fornisce due (45); viene riconosciuto e assegnato alla variabile "k". Quindi "n" vale 3, "i" vale 1, "j" vale 23 e "k" vale 45. ingresso "123456" chiamata n = scanf("%2d%2s%2d",&i,s,&j); Il primo intero è di due cifre (12); viene riconosciuto e assegnato alla variabile "i". Segue una stringa di due caratteri ("34"); viene riconosciuta e assegnata alla variabile "s". Segue un intero di due cifre (56), che viene riconosciuto e assegnato alla variabile "j". Quindi "n" vale 3, "i" vale 12, "s" vale "34" e "j" vale 56. Lucido 9: Nel caso della specifica %[set]: - gli insiemi possono essere indicati anche attraverso i caratteri estremi (ad esempio [a-z] indica tutte le lettere minuscole, [0-9ab] indica tutte le cifre e i caratteri 'a' e 'b') - si può specificare un insieme di caratteri proibiti, anziché obbligatori: basta premettere all'elenco il carattere '^' (ad esempio [^dgt] indica tutte le stringhe che non contengono i caratteri 'd', 'g' e 't') ingresso "123abc" chiamata n = scanf("%[0-9]",s); L'insieme da riconoscere è costituito dalle cifre. Si scorre l'ingresso fino al primo carattere non riconosciuto ('a') e si assegna la stringa precedente alla variabile "s". Quindi "n" vale 1 e "s" vale 123". La stringa "abc" rimane nello stream di ingresso. ingresso "abc123" chiamata n = scanf("%[0-9]",s); L'insieme da riconoscere è costituito dalle cifre. L'ingresso comincia con un carattere non appartenente all'insieme ('a'). Quindi, la funzione si arresta subito, "n" vale 0, "s" è invariata e la stringa "abc123" rimane nello stream di ingresso. ingresso "abc123" chiamata n = scanf("%[^0-9]",s); L'insieme da riconoscere è costituito dai caratteri diversi dalle cifre. Il riconoscimento si arresta quindi alla prima cifra ('1'). Quindi, "n" vale 1, "s" vale "abc" e la stringa "123" rimane nello stream di ingresso. Il codice PARSING_SET.C illustra l'uso di queste specifiche. Si noti che, fornendo stringhe che violano la specifica, la parte che la rispetta viene assegnata alla variabile e la parte che la viola viene conservata nello stream in ingresso, a disposizione della scanf successiva. Quando anche il primo carattere non separatore viola la specifica, la scanf restituisce 0. Si noti che la seconda lettura, quella delle stringhe non contenenti [dgt], non termina premendo INVIO, perché INVIO non fa parte dell'insieme [dgt]: o si preme uno dei tre caratteri proibiti, oppure si aggiunge '\n' all'insieme dei caratteri proibiti. In questo secondo caso, tale carattere non verrà letto, e quindi resterà in coda per la lettura successiva. Infatti, la prima lettura termina premendo INVIO, ma lascia l'a capo nello stream in ingresso, e la stampa del risultato della seconda lettura comincia con l'a capo iniziale. Se si vuole eliminare l'a capo, occorre esplicitarlo al termine della stringa di formato: scanf("%[^dgt\n]\n",s); Un esempio notevole di uso della specifica di insiemi è la stringa di formato "[^\n]", che consente di leggere una riga, cioè qualsiasi sequenza di caratteri sino al primo a capo. Si noti che il carattere di a capo non viene letto, ma rimane nello stream di ingresso. Per leggere anche quello, ma senza assegnarlo alla stringa s, si può usare l'istruzione scanf("%[^\n]\n",s); Anche in questo caso i parametri di scanf non hanno la dereferenziazione & perché sono stringhe, e quindi puntatori. Per esercizio, si modifichi il codice PARSING_DATA.C in modo che accetti sia '/' sia '-' come separatori fra giorno e mese e fra mese ed anno. Lucido 11: Si noti che i parametri di printf non hanno la dereferenziazione, perché sono passati per valore, anziché per indirizzo. Infatti, scanf modifica i propri parametri (sono risultati, non dati!), mentre printf non li modifica (sono veri dati!). Lucido 13: Come alla funzione "scanf" si affianca la "printf", così alla funzione "sscanf" si affianca la "sprintf". Questa può essere usata per produrre stringhe che contengano al loro interno sottostringhe, numeri interi o reali, ecc...