Introduzione al linguaggio C: stringhe, input/output, puntatori
Tutorial Programmazione in C – Una stringa è composta da uno o più caratteri alfanumerici in sequenza, il che significa che con le stringhe è possibile rappresentare qualunque tipo
di frase o parola. Le stringhe sono tuttavia un’astrazione volta a rendere più semplice la gestione del programma allo sviluppatore, ed hanno meccanismi differenti nei differenti linguaggi di programmazione. In C una stringa è rappresentata da un array di variabili di tipo char (carattere), terminati dal
carattere . Questo carattere particolare viene detto NUL terminating byte e serve ad indicare che la stringa è finita; non può essere inserito da tastiera e non può essere stampato sullo schermo. Tuttavia la sua presenza è fondamentale e insostituibile; se una funzione non trova il NUL terminating byte, continuerà a prelevare/scrivere dati dalle/sulle aree di memoria successive al termine della stringa, con possibili effetti catastrofici.
Tuttavia l’analisi di questo argomento esula dallo scopo di questo articolo;
ci basti sapere che la presenza del byte in una stringa C è fondamentale.
Come detto, una stringa C è memorizzata in un array di caratteri, ossia in
un array di variabili di tipo char:
char stringa[20];
Con questa istruzione C creiamo una stringa che può contenere fino a 20 elementi,
incluso il NUL terminating byte. Una variabile di tipo char non è altro che
una variabile numerica di dimensioni ridotte (1 byte), il cui valore è associato
ad un carattere secondo lo standard di codifica ASCII (si veda ascii(7), fig.1).
Oltre ad usare la notazione appena vista per la dichiarazione di stringhe, possiamo
usare speciali variabili dette puntatori (o pointer). Al loro interno è
memorizzato un riferimento all’area di memoria in cui una variabile risiede.
La notazione è la seguente:
char* stringa;
Un puntatore prima di essere usato deve essere inizializzato, e vi sono
principalmente 2 modi di farlo. E’ possibile assegnargli un array:
char arr[10];
char* ptr = arr;
In questo caso potremo accedere al contenuto di arr sia tramite la variabile
arr sia tramite il puntatore ptr. Ad es. le espressioni:
ptr[3] = ‘X’
e
arr[3] = ‘X’
saranno completamente equivalenti. E’ possibile creare un pointer ad una
variabile non array, ma bisogna preventivamente ottenere l’indirizzo di detta
variabile mediante l’operatore di referenziazione &:
int i = 0;
int* ptri = &i;
Adesso, per accedere ad i mediante ptri, dovremo usare l’operatore di
dereferenziazione (*). I due statement successivi sono equivalenti:
i = 10;
*ptri = 10; // ptri e’ un puntatore ad i; *ptri e’ i
Un altro modo di creare un puntatore è usare la funzione malloc(), che
inizializza un’area di memoria e ne ritorna un puntatore. Ad es.,
char* ptr = malloc(32);
riserverà 32 byte nell’area di memoria detta heap (a differenza degli array,
che vengono memorizzati nell’area di memoria detta stack) e ritornerà un
puntatore all’inizio dell’area di memoria appena creata. Il processo appena
descritto è detto processo di allocazione di memoria. Al termine della
funzione, potremo accedere all’area di memoria a cui ptr punta, usando ptr
stesso come fosse un array (notare che la memoria allocata via malloc() non
è inizializzata e potrebbe contenere valori spazzatura frutto di un precedente
utilizzo di quell’area di memoria da parte di un altro processo).
In realtà, l’assegnazione che abbiamo visto poco sopra viene sì riconosciuta
dal compilatore, ma non è logicamente corretta. La funzione malloc(), in
realtà, ritorna un puntatore di tipo void. Una variabile di tipo void non può
avere nessun valore (infatti, la dichiarazione di una variabile void viene
segnalata dal compilatore come un errore). Tuttavia la notazione void* viene
usata frequentemente come “puntatore generico” e non è rifiutata dal
compilatore: un void* punta ad un’area di memoria generica; tuttavia attraverso
un void* non possiamo accedere alla memoria come tramite un array, ma dobbiamo
ricorrere a complicate acrobazie di codice per ottenere l’effetto desiderato.
void* ptr = malloc(32);
*(char*)(ptr+4) = ‘M’;
In questo caso, se ptr fosse un char pointer (char*), basterebbe semplicemente
usare la forma ptr[4] = ‘M’. Tuttavia è necessario che malloc() ritorni un
puntatore generico, perché, ad esempio, si potrebbe desiderare di allocare un
puntatore a interi (int*). Siccome alcuni sistemi possono essere intransigenti
sull’assegnazione dei puntatori, conviene utilizzare un cast in ogni caso:
char* ptr = (char*) malloc(32);
Un cast serve a forzare il tipo di una variabile o funzione. Per eseguire un
cast basta anteporre alla variabile che vogliamo convertire il tipo che vogliamo
ottenere racchiuso tra parentesi. Un ultimo cenno sui puntatori: stiamo attenti
all’utilizzo della funzione malloc() quando dobbiamo usare puntatori a int.
malloc(), infatti, alloca esattamente il numero di byte che gli vengono passati
per argomento; nel caso dei char* non c’è nessun problema, perché un char occupa
esattamente un byte di memoria. Gli int, però, occupano 4 byte, quindi lo
statement
int* ptr = (int*) malloc(32);
allocherebbe un array di 8 interi. Occupando un int 4 byte, infatti, avremmo
un array la cui dimensione è 32/4 = 8. Quindi facciamo attenzione. Per prevenire
questo problema e anche il problema dell’inizializzazione della memoria, possiamo
usare la funzione calloc():
int* ptr = calloc(sizeof(int), 32); // array di 32 interi
Questa prende come primo argomento la dimensione del tipo del puntatore che
verrà creato (in questo caso 4; l’operatore sizeof(nometipo) serve a ottenere
la dimensione del tipo “nometipo”), ed, inoltre, inizializza a 0 tutta l’area
di memoria allocata (eliminando l’annoso problema della memoria spazzatura).
Chiusa questa parentesi, torniamo ad occuparci delle stringhe.
Stringhe in pratica
L’assegnazione di un valore ad una stringa può avvenire in due modi: direttamente
o mediante funzioni esterne. L’assegnazione diretta di un array di caratteri è
fattibile solo durante la dichiarazione e non in un secondo momento. La motivazione
di ciò verrà spiegata in seguito. E’ possibile tuttavia variare il valore di un
elemento della stringa come fosse un normale array.
// valido
char stringa[20] = “ciao ciao”;
// non valido
char stringa[20];
stringa = “ciao ciao”;
// valido
char stringa[20] = “ciao ciao”;
stringa[2] = ‘A’; // diventa “ciAo ciao”
L’assegnazione mediante funzioni esterne è fattibile solo successivamente alla
dichiarazione. Queste funzioni sono descritte all’interno del file di intestazione string.h, quindi per usarle dovremo
scrivere la seguente istruzione nell’intestazione del nostro programma:
#include <string.h>
La principale funzione di assegnazione è strcpy(), che copia nel suo primo
argomento il valore del suo secondo argomento. Vediamone la firma:
char* strcpy(char* destinazione, char* sorgente)
Il puntatore ‘destinazione’ contiene l’indirizzo di memoria presso il quale
risiede la nostra variabile; strcpy() continuerà a prelevare caratteri dal
puntatore ‘sorgente’ e a copiarli sul puntatore ‘destinazione’ fin quando non
incontrerà il carattere . Vediamo un esempio di utilizzo di questa funzione:
char stringa[20];
strcpy(stringa, “ciao ciao”);
La funzione strncpy() differisce dalla strcpy() per il solo fatto che è
possibile specificare il numero massimo di byte da copiare.
char* strncpy(char* destinazione, char* sorgente, int quantiCaratteri);
L’utilizzo della funzione strncpy() è generalmente più sicuro dell’utilizzo
di strcpy(), perché limita il rischio di incorrere nei buffer-overflow [1].
L’esempio successivo copia solo i primi 10 caratteri di “quertyuiopasdf” in
stringa.
char stringa[10];
strncpy(stringa, “qwertyuiopasdf”, 10);
Input/Output
Gran parte dei programmi ha la necessità di interagire con gli utenti; ciò
significa acquisire delle informazioni dall’utente e quindi rielaborarle.
Le informazioni possono essere numeri o stringhe; esistono varie funzioni
che permettono di acquisire informazioni e restituire all’utente altre
informazioni derivate dall’elaborazione delle prime. Vediamone alcune.
int printf(char* format_string, …);
Questa funzione ha un numero di argomenti variabile e serve a dare un output
all’utente del nostro programma. L’unico argomento obbligatorio è la stringa
“format_string”, il cui contenuto descrive la formattazione dell’output
secondo particolari criteri. Una format string è composta da testo normale,
tutti i caratteri ordinari eccetto %, e da uno specificatore di conversione.
Uno specificatore di conversione (detto anche marcatore) inizia con un simbolo
di percentuale (%), ed è seguito da alcuni caratteri che descrivono la
conversione. La lettera ‘d’ minuscola, ad esempio, stampa numeri decimali con
segno, la ‘f’ numeri a virgola mobile (float e double), la ‘x’ numeri
esadecimali, la ‘s’ per le stringhe e così via.
Per ogni specificatore di conversione, la funzione printf() estrae un
argomento dalla lista degli argomenti; quindi dovremo passare alla printf() un
numero di argomenti uguale a quello del numero degli specificatori di
conversione. Si veda la pagina di manuale printf(3), che contiene
una spiegazione esaustiva dell’impiego delle stringhe di formattazione.
Il valore di ritorno della printf() è il numero di caratteri che sono andati
in output.
Ecco un esempio dell’utilizzo della funzione printf():
char stringa[16];
strcpy(stringa, “Ciao mondo”);
int num = 10;
printf(“Stringa = %s; numero = %dn”, stringa, num);
In queste linee di codice la funzione printf() sostituisce al marcatore %s la
stringa “Ciao mondo” ed al marcatore %d il numero decimale num. L’output
sarà quindi:
Stringa = Ciao mondo; numero = 10
Similmente lavora la funzione scanf(), che serve a ricevere dati in ingresso
dalla tastiera.
int scanf(char* format_string, …);
Il formato della format string è sempre lo stesso, la differenza è che per
acquisire informazioni non dovremo passare la variabile stessa, bensì un suo
puntatore; avendo a disposizione l’indirizzo di memoria della variabile, la
funzione scanf() può scrivere i dati ricevuti dalla tastiera a quell’indirizzo,
rendendo possibile l’input. Gli argomenti passati alla scanf() dovranno quindi
essere referenziati (preceduti dall’operatore &), se non sono nativamente
puntatori (come nel caso dei char*).
L’esempio seguente legge da tastiera un numero decimale e lo scrive
all’indirizzo # da questo momento la variabile “num” conterrà il valore
letto dalla tastiera.
int num;
scanf(“%d”, &num);
Input/Output: i file
Abbiamo appena visto come sia possibile interagire con schermo e tastiera.
Le funzioni appena descritte non servono solo a questo scopo, ma anche all’
interazione con i file.
Un file è un flusso (stream) di dati di vario genere che può venire letto
e interpretato da appositi programmi secondo particolari formati. Un file
in C è descritto dalla struttura FILE, e viene aperto e chiuso dal programma
mediante le istruzioni fopen() e fclose().
FILE* fp = fopen(char* nomefile, char* modo);
int fclose(FILE* fp);
La prima funzione prende due argomenti: il primo è il nome del file, completo
di estensione, che può essere un path relativo (es. “../file.txt”) o assoluto
(es. “/tmp/aaaaa”); il secondo è una stringa contenente alcuni caratteri che
descrivono la modalità di apertura di un file: sola lettura (“r”), sola
scrittura (“w”) e append mode (“a”). Se un file viene aperto con modalità
“a”, mediante operazioni di output sul file i dati verranno aggiunti in coda al
file, mentre con la modalita’ “w” il file, se esistente, verrà prima
cancellato, poi sovrascritto. Il risultato della chiamata a fopen verrà
memorizzato in un puntatore ad una variabile FILE (FILE*, che non necessita
di allocazione mediante malloc o simili).
FILE* fp = fopen(“/tmp/test”, “w”); /* apre il file */
/* … operazioni sul file… */
fclose(fp); /* chiude il file */
Per inviare dati in output su un file si può usare la funzione fprintf():
int fprintf(FILE* fp, char* format_string, …);
L’unica differenza rispetto alla printf() è che il primo argomento è
il FILE* aperto. Di default anche allo schermo e alla tastiera sono associate
alcune variabili FILE*: queste sono stdin, stdout e stderr. La prima è legata
alla tastiera, così che
fscanf(stdin, format, …)
equivale a
scanf(format, …)
stdout e stderr sono legate allo schermo ma sono differenti fra loro: stdout
è bufferizzata, stderr no. Questo significa che i dati scritti su stdout
verranno visualizzati solo dopo che una n sarà stata scritta sullo stream
oppure dopo che stdout sarà stato “flushato” mediante la funzione fflush(),
mentre i dati scritti su stderr verranno visualizzati immediatamente (per
questo motivo stderr viene usato per visualizzare gli errori).
La funzione fflush() serve a “svuotare” il buffer di un file; viene chiamata
automaticamente quando si chiude il file o può essere chiamata dal
programmatore e l’effetto che deriva dalla chiamata è la scrittura delle
modifiche sul file.
int fflush(FILE* fp);
Facciamo un esempio di quanto detto fin ora sui file:
FILE* fp = fopen(“/tmp/a.txt”, “w”);
if(fp == NULL) /* controlla se ci sono stati errori nell’apertura */
{
fprintf(stderr, “Non ho potuto aprire il filen”);
exit(1); /* la funzione exit() termina il programma */
}
int var;
printf(“Dammi un numero: “);
scanf(“%d”, &var);
fprintf(fp, “Ciao mondo. Hai scritto %dn”, var);
fclose(fp); // chiamata a fflush automatica
Vediamone l’output in figura 2.
Ancora input/output
Analizziamo ancora qualche funzione relativa all’input/output.
char* fgets(char* stringa, int n, FILE* fp);
La funzione fgets legge al massimo “n” bytes dal file fp e li memorizza
all’indirizzo a cui punta stringa (che deve essere un puntatore e non un
array). Esempio:
FILE* fp = fopen(“file.txt”, “r”); /* lettura */
char str[64];
fgets(&str, 64, fp);
fclose(fp);
int fgetc(FILE* fp);
Questa funzione legge un byte da fp e lo ritorna. Esempio:
// fp è un FILE* aperto
int car = fgetc(fp);
printf(“Carattere letto: %cn”, car);
int fputs(char* str, FILE* fp);
fputs() scrive la stringa str sul file fp, e si ferma quando incontra il NUL
terminating byte.
int fputc(int carattere, FILE* fp)
fputc() scrive il carattere specificato dal primo argomento sul file fp.
Possiamo inoltre controllare se il file contiene ancora altri dati o meno,
usando la funzione feof(). Questa è molto utile nei loop:
FILE* fp = … // inizializzazione in lettura
while(!feof(fp)) // mentre la EOF (end of file) non e’ raggiunta
{
int car = fgetc(fp);
// istruzioni …
}
Conclusioni
Abbiamo analizzato il funzionamento dell’input/output su file di testo; in una
prossima puntata parleremo della stesura delle funzioni, componenti fondamentali
della programmazione procedurale.
Buona programmazione!
Riferimenti
[1] Aleph1, Smashing the stack for fun and profit
http://www.phrack.org/show.php?p=49&a=14