Caratteri e stringhe di caratteri Una (costante di) stringa è una qualsiasi sequenza di caratteri racchiusi tra doppi apici, ad es.: “Buon giorno!”. Una stringa è memorizzata come un vettore di caratteri che termina con uno speciale marcatore di fine di stringa, detto carattere NULL e rappresentato dalla sequenza di escape \0. Ad es., la stringa “Buon giorno!” verrebbe rappresentata in memoria come Essa usa 13 locazioni di memoria, e va pertanto dichiarata come char string[13] per riservare uno spazio anche per il carattere NULL, che funge da sentinella per denotare la fine della stringa. Inizializzazione. La inizializzazione di un vettore di caratteri può essere semplificata scrivendo una dichiarazione del tipo: char codici[] = “prova”; nella quale si usa la stringa prova per inizializzare il vettore codici, di 6 elementi, come indicato in figura. I primi 5 elementi consistono nelle lettere p r o v a, l’ultimo è la sequenza di escape \0 o carattere NULL, che il compilatore C aggiunge automaticamente alla fine di ogni stringa. Dato che una stringa è memorizzata come un vettore di caratteri, i suoi singoli caratteri possono essere immessi, manipolati o messi in uscita usando le tecniche standard di gestione dei vettori, tramite le notazioni di indice o di puntatore. Funzioni di ingresso e uscita Sebbene, per elaborare una stringa già in memoria, si possa usare sia una funzione di biblioteca sia una scritta dall’utente, l’immissione di una stringa da tastiera o la sua visualizzazione sullo schermo richiedono qualche supporto da funzioni di biblioteca standard. La tabella seguente elenca le funzioni di biblioteca comunemente disponibili per l’ingresso e l’uscita di una stringa, sia carattere per carattere sia come unità completa. Le definizioni delle funzioni indicate in tabella sono contenute nel file d’intestazione stdio.h, che pertanto va incluso in ogni programma che le utilizzi (con la solita direttiva #include <stdio.h>). Le due funzioni scanf() e printf() sono già state usate ampiamente. Vediamo perciò le altre quattro funzioni. Funzione getchar(). La funzione getchar() restituisce il prossimo carattere battuto alla tastiera. Ha come intestazione: int getchar() ed è usata per l’ingresso di singoli caratteri in un’istruzione quale car_in = getchar(); Essa memorizza il prossimo carattere battuto nella variabile car_in, ed equivale alla più lunga scanf(“%c”, &car_in); getchar() non si aspetta che le vengano passati argomenti, e restituisce un dato di tipo intero. Ciò per consentire che venga restituita la sentinella EOF (End-Of-File) già vista, che ha un codice intero (2610). getchar() risulta utile per immettere in continuazione stringhe di caratteri da file di dati, come vedremo più avanti. Funzione putchar(). La funzione di uscita corrispondente a getchar() è putchar(), che si aspetta un argomento costituito da un singolo carattere e lo visualizza sullo schermo. Ad es. l’istruzione putchar(‘a’); visualizza sullo schermo la lettera a, ed è equivalente alla più lunga printf(“%c”, ‘a’); Il programma seguente usa le funzioni getchar() e putchar() per “fare l’eco” sullo schermo dei caratteri battuti sulla tastiera. #include <stdio.h> int main() { int car_in; while ((car_in = getchar()) != EOF) putchar(car_in); } Funzioni gets(), puts(). Queste funzioni trattano le stringhe come unità complete, e sono scritte usando le routine più elementari getchar() e putchar() (che forniscono l’ingresso e l’uscita di singoli caratteri). Un’istruzione quale: gets(messaggio); accetta in continuazione i caratteri digitati da tastiera e li memorizza nel vettore di caratteri messaggio. La memorizzazione dei caratteri nel vettore termina quando si preme il tasto <Invio>. Esso genera un carattere di linea nuova (\n) che gets() traduce in un carattere NULL (\0), scrivendolo come ultimo carattere nel vettore messaggio. Un’istruzione quale: puts(messaggio); visualizza sullo schermo il vettore di caratteri messaggio, che contiene la stringa immessa. Come mostra la figura, puts() traduce il carattere NULL (\0) in una sequenza di escape di linea nuova (\n), che invia automaticamente al video dopo che la stringa è stata visualizzata. Il seguente programma Getsputs usa gets() e puts() per “fare l’eco” sullo schermo di una stringa (lunga fino a 80 caratteri) immessa da tastiera. #include <stdio.h> main() { char messaggio[81]; printf(“Scrivi una stringa:\n”); gets(messaggio); printf(“La stringa appena scritta è:\n”); puts(messaggio); } In esso osserviamo che: la funzione gets() accetta in continuazione i caratteri digitati da tastiera e li memorizza nel vettore di caratteri messaggio; la pressione del tasto <Invio> genera un carattere di linea nuova, \n, che gets() interpreta come un fine-carattere; tutti i caratteri incontrati da gets(), tranne quello di linea nuova, sono memorizzati nel vettore di caratteri messaggio; prima di restituire un valore, la funzione gets() aggiunge il carattere NULL al gruppo di caratteri memorizzati, come mostra la figura. B u o n g ... \n gets() • il programma usa quindi la funzione puts() per visualizzare la stringa. Equivalenza tra puts() e printf(). Si tenga presente che, in generale, una chiamata alla funzione puts() può sempre essere sostituita con una chiamata alla funzione printf() Ad es., l’istruzione puts(mesaggio) usata nel programma precedente può essere sostituita con l’istruzione printf(“%s\n”, messaggio); nella quale • la sequenza di controllo di conversione %s avverte la funzione che si accederà a una stringa; • la sequenza di escape di linea nuova \n sostituisce la linea nuova generata automaticamente da puts() dopo che la stringa è stata visualizzata. Non equivalenza tra gets() e scanf(). L’equivalenza tra le funzioni di uscita puts() e printf() non è sussiste tra le funzioni di ingresso gets() e scanf(). Ad es., e gets(messaggio); scanf(“%s”, messaggio); non sono equivalenti. Infatti • gets() legge un gruppo di caratteri fino a quando incontra un carattere di linea nuova. • scanf() legge un gruppo di caratteri fino a quando incontra un carattere di linea nuova oppure uno spazio vuoto. Perciò, se si tentasse di immettere nel vettore messaggio i caratteri Questa è una stringa usando l’istruzione scanf(“%s”, messaggio); solo la parola Questa verrebbe assegnata al vettore. Per immettere la linea completa usando una chiamata alla funzione scanf() sarebbe necessaria una istruzione del tipo: scanf(“%s %s %s %s”, mess1, mess2, mess3, mess4); che assegna la parola… alla stringa… Questa mess1 è mess2 una mess3 stringa mess4 Il fatto che scanf() accetti anche lo spazio vuoto come delimitatore, la rende poco utile per immettere stringhe di dati. Osservazione se si usa la funzione scanf() per immettere una stringa di dati, davanti al nome del vettore non si usa il carattere &. Dato che, come sappiamo, un nome di vettore è una costante puntatore, equivalente all’indirizzo della prima locazione di memoria riservata per il vettore, messaggio è equivalente a &messaggio[0]. Perciò abbiamo potuto usare la chiamata di funzione scanf(“%s”, messaggio); in luogo di scanf(“%s”, &messaggio[0]); Manipolazione di stringhe di caratteri Copia carattere per carattere. Le stringhe si possono manipolare usando o le tecniche di elaborazione standard dei vettori o le funzioni di biblioteca standard. Vediamo dapprima come si elabora una stringa carattere per carattere, il che farà capire come sono costruite le funzioni di biblioteca standard e come creare funzioni di biblioteca proprie. Costruiamo una funzione, strcopia(), che copi la stringa2 nella stringa1. void strcopia(char string1[], char string2[]) { int i = 0; while (string2[i] != ‘\0’) { string1[i] = string2[i]; ++i; } string1[i] = ‘\0’; return; } Sebbene questa funzione possa essere abbreviata e scritta in maniera più compatta, essa illustra le caratteristiche principali della manipolazione di stringhe. Nella linea d’intestazione, le due stringhe sono passate a strcopia() come vettori, quindi ciascun elemento di string2 è copiato nell’elemento corrispondente di string1 fino a che (while) s’incontra il marcatore di fine-stringa NULL (\0), la cui rilevazione fa terminare il ciclo while che copia gli elementi. Dato che il carattere NULL non è copiato da string2 a string1, l’ultima istruzione di strcopia() aggiunge a string1 un carattere di fine-stringa. Prima di chiamare strcopia(), si deve allocare spazio sufficiente per il vettore string1, in modo che possa accogliere gli elementi di string2. Il programma seguente alloca per ciascuno dei due vettori lo spazio di una linea di schermo completa (80 caratteri), più uno per il finestringa, dichiara la funzione strcopia() che si aspetta due vettori di caratteri e la chiama passandole gli indirizzi dei due vettori. #include <stdio.h> void main(void) { char messaggio[81]; char nuovo_mess[81]; int i; void strcopia(char [], char []); printf("Scrivi una frase:"); gets(messaggio); strcopia(nuovo_mess, messaggio); puts(nuovo_mess); } void strcopia(char string1[], char string2[]) { int i = 0; while (string2[i] != '\0') { string1[i] = string2[i]; ++i; } string1[i] = '\0'; return; } Immissione di stringhe carattere per carattere. Con la stessa tecnica con cui può esere copiata carattere per carattere, una stringa può essere anche immessa e visualizzata. Ad es., il programma seguente usa la funzione d’ingresso caratteri getchar() per fare immettere una stringa carattere per carattere. #include <stdio.h> void main(void) { char messaggio[81], c; int i; printf("Scrivi una frase:\n"); i = 0; while(i<80 && (c = getchar()) != '\n') { messaggio[i] = c; ++i; } messaggio[i] = '\0'; printf("La frase appena scritta è:\n"); puts(messaggio); } La parte di programma evidenziata in colore sostituisce l’istruzione gets(messaggio) usata nel programma Getsputs. L’istruzione while fa leggere i caratteri immessi purché il loro numero sia inferiore a 81 e il carattere restituito da getchar() non sia quello di linea nuova (\n). Osservazione. Le parentesi attorno all’espressione c = getchar() sono necessarie per assegnare il carattere restituito da getchar() alla variabile c prima di confrontarlo con una sequenza di linea nuova (\n). In loro assenza, dato che l’operatore di confronto != ha la precedenza su quello di assegnazione =, l’espressione sarebbe equivalente a c = (getchar() != ‘\n’) Essa confronta il carattere restituito da getchar() con ‘\n’, e il valore dell’espressione relazionale (getchar() != ‘\n’) è 0 o 1 a seconda che getchar() abbia ricevuto o no il carattere di linea nuova. Così anche il valore assegnato a c sarebbe 0 o 1, a seconda del risultato del confronto. Funzione scritta dall’utente. Il programma precedente illustra anche un’utile tecnica per sviluppare le funzioni. Le istruzioni evidenziate in colore costituiscono una unità autoconsistente per immettere una linea completa di caratteri da tastiera. Pertanto esse possono essere scorporate da main() e messe insieme in una nuova funzione, chiamata getlinea(), come illustrato nel programma seguente. #include <stdio.h> void main(void) { char messaggio[81]; int i; void getlinea(char []); printf("Scrivi una frase:\n"); getlinea(messaggio); printf("La frase appena scritta è:\n"); puts(messaggio); } void getlinea(char strin[]) { int i = 0; char c; while (i < 80 && (c = getchar()) != '\n') { strin[i] = c; ++i; } strin[i] = '\0'; return; } Una scrittura compatta. Possiamo anche riscrivere getlinea() in modo più compatto assegnando direttamente alle componenti del vettore strin i caratteri restituiti da getchar(), eliminando la variabile locale c. void getlinea(char strin[]) { int i = 0; while (i < 80 && (strin[i++] = getchar()) != '\n') ; strin[i] = '\0'; return; } Osservazione. L’istruzione di assegnazione (strin[i++] = getchar()) •assegna il carattere restituito da gethcar() direttamente al vettore strin, quindi •incrementa il subscritto i usando l’operatore postfisso ++. • la successiva istruzione NULL (;) soddisfa l’esigenza che un ciclo while contenga almeno una istruzione. Entrambe le versioni di getlinea() sono sostituti accettabili di gets(), e dimostrano la intercambiabilità delle funzioni di biblioteca del C con quelle scritte dall’utente. Puntatori e funzioni di biblioteca Nel costruire funzioni di gestione delle stringhe risultano particolarmente utili i puntatori: una notazione con puntatori dà luogo a istruzioni più compatte ed efficienti - rispetto agli indici - per accedere ai caratteri individuali di una stringa. Per vedere l’equivalenza tra indici e puntatori quando si accede ai singoli caratteri di una stringa riprendiamo in considerazione la funzione strcopia(), che copia i caratteri da un vettore a un altro, uno alla volta. void strcopia(char string1[], char string2[]) { int i = 0; while (string2[i] != '\0') { string1[i] = string2[i]; ++i; } string1[i] = '\0'; return; } Per scriverne una versione con puntatori, apportiamole due modifiche: 1. Osserviamo che l’espressione (string2[i] != '\0') • è vera (cioè ha un valore diverso da 0) fin tanto che si leggono i caratteri della stringa diversi da quello finale (\0), mentre • è falsa (cioè vale 0) solo quando si legge il carattere finale. Ma anche l’espressione string2[i] ha un valore diverso da 0 per tutti i caratteri della stringa tranne l’ultimo, dato che solo \0 ha il codice ASCII 0. Perciò si può sostituire l’espressione (string2[i] != '\0') con la più semplice string2[i] e scrivere: void strcopia(char string1[], char string2[]) { int i = 0; while (string2[i]) { string1[i] = string2[i]; ++i; } string1[i] = '\0'; return; } 2. L’istruzione di assegnazione string1[i] = string2[i]; ha come valore il codice ASCII del carattere via via copiato; esso è uguale a 0 solo dopo che il carattere NULL sia stato assegnato a string1. Perciò possiamo includere l’espressione di assegnazione string1[i] = string2[i] dentro la parte di test dell’istruzione while, scrivendo: void strcopia(char string1[], char string2[]) { int i = 0; while (string1[i] = string2[i]) ++i; return; } In tal modo non è necessario terminare esplicitamente la prima stringa con il carattere NULL, dato che l’assegnazione entro parentesi assicura che NULL sia copiato dalla seconda stringa nella prima, facendo terminare correttamente il ciclo while. Possiamo adesso convertire strcopia() dalla notazione con indici a quella con puntatori come segue: void strcopia(char *string1, char *string2) { while (*string1 = *string2) { string1++; string2++; } return; } Sia nella versione con indici, sia in quella con puntatori, la funzione strcopia() riceve il nome del vettore passato (ossia l’indirizzo della sua prima locazione). Nella versione con puntatori di strcopia() i due indirizzi passati sono memorizzati rispettivamente negli argomenti puntatore string1 e string2. La precedente dichiarazione (char *string1, char *string2) indica che string1 e string2 sono puntatori che contengono l’indirizzo di un carattere, e fa sì che gli indirizzi passati siano trattati come valori puntatori anziché come nomi di vettori. La dichiarazione è equivalente a: (char string1[], char string2[]) All’interno di strcopia() l’espressione puntatore *string1 (”l’elemento il cui indirizzo è in string1”), sostituisce l’espressione con subscritto equivalente string1[i]. Analogamente per *string2. L’espressione *string1 = *string2 fa sì che l’elemento puntato da string2 sia assegnato all’elemento puntato da string1. Dato che gli indirizzi di partenza di entrambe le stringhe sono passati a strcopia() e memorizzati rispettivamente in string2 e string1, l’espressione *string2 si riferisce inizialmente a string2[0], e *string1 a string1[0]. Gli incrementi consecutivi ai due puntatori in strcopia() con le istruzioni string1++; e string2++; fanno sì che ciascun puntatore punti al carattere successivo nella rispettiva stringa. Si può effettuare un ultimo cambiamento alla funzione includendo gli incrementi ai puntatori come operatori postfissi nella parte di test dell’istruzione while. La forma finale della funzione di copia stringa è allora: void strcopia(char *string1, char *string2) { while (*string1++ = *string2++) ; return; } Nell’espressione *string1++ = *string2++ non c’è ambiguità, anche se l’operatore di indirezione, *, ha la stessa precedenza di quello di incremento, ++, infatti essa fa accedere al carattere puntato prima che il puntatore sia incrementato. Solo dopo il completamento dell’assegnazione *string1++ = *string2++ i puntatori sono incrementati per puntare ai caratteri successivi nelle rispettive stringhe. Quasi tutti i compilatori C comprendono nella loro biblioteca standard una funzione di copia stringa, scritta esattamente come la nostra versione con puntatori di strcopia(). Funzioni di biblioteca di stringa. Quasi tutti i compilatori C dispongono di un gruppo di funzioni di biblioteca per: l’ingresso il confronto la manipolazione l’uscita di stringhe di caratteri. Un loro elenco è fornito nella tabella seguente: Esercizio. Inserire gli opportuni commenti nelle righe lasciate vuote del seguente programma, quindi indicare l’uscita prodotta dal programma, senza eseguirlo. #include <stdio.h> #include <string.h> int main() { char x[] = "Buon compleanno a te"; char y[25]; char z[15]; printf("%s%s\n%s%s\n", "La stringa nel vettore x è: ", x, "La stringa nel vettore y è: ", strcpy(y, x)); strncpy(z, x, 14); z[14] = '\0'; printf("La stringa nel vettore z è: %s\n", z); return(0); } #include <stdio.h> #include <string.h> int main() { char x[] = "Buon compleanno a te"; /* inizializza il vettore di caratteri x */ char y[25]; /* crea il vettore di caratteri y */ char z[15]; /* crea il vettore di caratteri z */ printf("%s%s\n%s%s\n", "La stringa nel vettore x è: ", x, "La stringa nel vettore y è: ", strcpy(y, x)); strncpy(z, x, 14); /* copia i primi 14 caratteri di x in z (non copia il carattere nullo) */ z[14] = '\0'; printf("La stringa nel vettore z è: %s\n", z); return(0); } Il programma precedente utilizza le funzioni: • strcpy() per copiare l’intera stringa x nel vettore y • strncpy() per copiare i primi 14 caratteri della stringa x nel vettore z. Al vettore z viene accodato un carattere NULL (\0) perché la chiamata di strncpy() nel programma non scrive un carattere nullo di terminazione, dato che il 3° argomento è inferiore alla lunghezza della stringa indicata dal 2° argomento. Esercizio. Inserire gli opportuni commenti nelle righe lasciate vuote del seguente programma, quindi indicare l’uscita prodotta dal programma, senza eseguirlo. #include <stdio.h> #include <string.h> int main() { char s1[20] = "Happy "; char s2[] = "New Year "; char s3[40] = ""; printf("s1 = %s\ns2 = %s\n", s1, s2); printf("strcat(s1, s2) = %s\n", strcat(s1, s2)); printf("strncat(s3,s1,6) = %s\n", strncat(s3,s1,6)); printf("strcat(s3,s1) = %s\n", strcat(s3,s1)); return(0); } #include <stdio.h> #include <string.h> int main() { char s1[20] = "Happy "; /* inizializza il vettore di caratteri s1 */ char s2[] = "New Year "; char s3[40] = ""; /* inizializza il vettore di caratteri s3 con la stringa vuota */ printf("s1 = %s\ns2 = %s\n", s1, s2); printf("strcat(s1, s2) = %s\n", strcat(s1, s2)); /* accoda s2 a s1 */ printf("strncat(s3,s1,6) = %s\n", strncat(s3,s1,6)); /* accoda i primi 6 caratteri di s1 a s3; inserisce \0 dopo l’ultimo carattere */ printf("strcat(s3,s1) = %s\n", strcat(s3,s1)); /* accoda s1 a s3 */ return(0); } Il programma precedente usa due funzioni: strcat() accoda il suo 2° argomento (una stringa) al 1°, che è un vettore di caratteri che contiene una stringa. Il 1° carattere del 2° argomento sostituisce il carattere NULL (\0) che termina la stringa del 1° argomento. Il vettore utilizzato per immagazzinare la prima stringa deve essere sufficientemente grande per contenere: • la prima stringa, • la seconda stringa, • il carattere nullo di terminazione che viene copiato dalla 2^ stringa. strncat() accoda un numero specificato di caratteri dalla 2^ stringa alla 1^. Al risultato viene accodato automaticamente un carattere NULL di terminazione. Funzioni per la conversione delle stringhe (atof(), atoi(), atol()) La biblioteca di utilità generiche <stdlib.h> del C contiene funzioni che convertono le stringhe formate da numeri in valori interi e in virgola mobile, indicate in Tabella. La funzione atof() restituisce il suo argomento, costituito da una stringa che rappresenta un numero in virgola mobile, convertito in un numero in doppia precisione. Nel caso in cui il valore non possa essere convertito (per esempio, se il primo carattere della stringa non è un numero), il comportamento di atof() sarà indefinito. Esempio di atof(). Scrivere un programma che converta una stringa in un numero in doppia precisione, e che produca la seguente uscita: La stringa “99.0” convertita in double è 99.00 Il valore convertito, diviso 2 è: 49.500 Un programma che soddisa la richiesta è il seguente: #include <stdio.h> #include <stdlib.h> int main() { double d; d = atof("99.0"); printf("%s%.3f\n%s%.3f\n", "La stringa \"99.0\" convertita in double è ", d, "Il valore convertito, diviso 2 è: ", d/2.0); } Esempio di atoi(). In modo analogo si comporta la funzione atoi(), che converte in un valore intero il suo argomento costituito da una stringa di cifre che rappresenta un intero. Consideriamo il seguente programma: #include <stdio.h> #include <stdlib.h> int main() { int i; i = atoi("2006"); printf("%s%d\n%s%d\n", "La stringa \"2006\" convertita in int è: ", i, "Il valore convertito, meno 6, è: ", i - 6); } Esempio di atol(). La funzione atol() restituisce il suo argomento, costituito da una stringa che rappresenta un numero intero lungo, convertito in un valore (intero) lungo. Qualora gli int e i long siano rappresentati entrambi mediante 4 byte, le funzioni atoi() e atol() funzionano in modo identico. Consideriamo il seguente programma: #include <stdio.h> #include <stdlib.h> int main() { long l; l = atol("12345678"); printf("%s%ld\n%s%ld\n", "La stringa \"12345678\" convertita in long è: ", l, "Il valore convertito, diviso 2, è: ", l / 2); } Funzioni di biblioteca di carattere (ctype.h) Vi sono anche funzioni per operare sui caratteri, alcune delle quali sono elencate nella tabella seguente: Queste funzioni sono inserite in un file standard di nome ctype.h, per cui per accedere a esse il programma dovrà contenere, prima di main(), l’istruzione #include <ctype.h> Esempio (toupper(), tolower()). Come primo esempio, consideriamo il programma seguente: #include <stdio.h> int main() { printf("%s%c\n %s%c\n %s%c\n", " u convertito in maiuscolo è ", toupper('u'), "7 convertito in maiuscolo è ", toupper('7'), "L convertito in minuscolo è ", tolower('L')); } Esso produce la seguente uscita: u convertito in maiuscolo è U 7 convertito in maiuscolo è 7 L convertito in minuscolo è l Esempio (isalpha(), isdigit()). Come altro esempio consideriamo il programma seguente, che chiede in continuazione all’utente di immettere un carattere, quindi stabilisce se esso sia una lettera o una cifra. Il programma contiene un ciclo do-while dal quale si esce quando si digita una f. Per non costringere l’utente a scrivere lettere minuscole o maiuscole, esso converte tutte le lettere scritte in minuscole. #include <stdio.h> #include <ctype.h> void main(void) { char car_ingr; do { printf("\nPremi un tasto qualsiasi (f per finire) "); car_ingr = getchar(); /*riceve il prossimo carattere*/ car_ingr = tolower(car_ingr); /*converte in minuscolo*/ getchar(); /*riceve e ignora il tasto Invio*/ if (isalpha(car_ingr)) /*un valore diverso da 0 è vero*/ printf("Il carattere immesso è una lettera.\n"); else if (isdigit(car_ingr)) printf("Il carattere immesso è una cifra.\n"); else printf("Il carattere non è né lettera né cifra.\n"); } while (car_ingr != 'f'); } Osserviamo che, poiché le funzioni restituiscono un valore, una funzione può essa stessa essere l’argomento di una funzione (compresa se stessa). Perciò le due istruzioni del programma precedente car_ingr = getchar(); car_ingr = tolower(car_ingr); possono essere fuse nell’unica istruzione: car_ingr = tolower(getchar());