** INTERMEZZO ** Fino ad ora abbiamo introdotto alcuni concetti basilari del linguaggio C. Con quanto appreso possiamo acquisire dei caratteri, stamparli a video, eseguire cicli e condizioni, ma vi sono ben altre cose che si possono fare con un linguaggio di programmazione quale e' il C. Introdurremo nei prossimi capitoli argomenti quali gli array, le strutture, le union, i tipi enum, la gestione dei file, i puntatori, la memoria, le funzioni ed inizieremo a prendere in considerazione maggiormente le caratteristiche del C del QL, le sue peculiarita' e gli eventuali limiti. ** CAPITOLO 6 ** - ARRAY - Abbiamo visto nel capitolo 3 che e' possibile scrivere sul terminale dei caratteri acquisendoli da tastiera o generandoli da programma. Ma la domanda che ci si pone subito e': e se volessimo introdurre qualcosa di piu' grande di un carattere come possiamo fare? Si potrebbe utilizzare un ciclo for che attenda la pressione di un tasto per poi visualizzarlo ripetendo il ciclo fino al limite previsto, ma cio' non sarebbe molto utile (a parte il fatto che possa essere un buon esercizio di programmazione) anche perche' alla fine avremmo una serie di caratteri a video senza alcuna possibilita' di ulteriore gestione degli stessi. Il C, cosi' come altri linguaggi, permette di definire una zona di memoria in cui contenere una serie di dati omogenei (tutti char, int, long o altro) identificati univocamente da un valore posizionale o, meglio, un array. La definizione di un array in C si effettua nel seguente modo: funzione() { char un_array_di_char[10]; ... un_array_di_char identifica un array di caratteri ad una sola dimensione in grado di contenere al massimo 10 caratteri ed e', percio', un vettore o array monodimensionale di tipo char che occupera' in memoria la dimensione di 8 bit * 10 = 80 bit = 10 caratteri. Nel C gli array hanno origine 0 per cui avremo la possibilita' di accedere ai singoli caratteri del vettore numerandoli da 0 a n (e non da 1 a n come nella DIM del Basic). A questo punto abbiamo dichiarato l'array ma il suo valore non e' ancora definito per cui proviamo ad inizializzarlo. Non si puo' fare una cosa di questo tipo: funzione() { char un_array_di_char[10]; un_array_di_char = "SincalirQL"; mentre e' lecito: funzione() { char un_array_di_char[10] = "SinclairQL"; oppure char un_array_di_char[] = {"SinclairQL"}; Questo perche' gli array possono essere assegnati con il simbolo = soltanto al momento della loro dichiarazione. L'uso della parentesi graffa permette un'ulteriore finezza: la possibilita' di definire un array e lasciare al compilatore il compito di dimensionarlo. Nel C l'array di caratteri deve terminare con il carattere ASCII 0 (NULL) affinche' venga trattato come stringa di caratteri correttamente stampabile e gestibile con le funzioni di libreria. Nell'esempio precedente abbiamo dichiarato un array di 10 caratteri e vi abbiamo inserito un testo lungo esattamente 10 caratteri. In questo caso il compilatore C, se esente da bugs interni, inserisce uno zero binario al termine della stringa ma, in caso l'assegnazione fosse avvenuta in altro modo (es.: la copia diretta da un'area di memoria ad un'altra), il terminatore di stringa potrebbe mancare e, in tal caso, se provassimo a stampare con la funzione di libreria printf la nostra stringa potremmo ottenere in output qualcosa tipo: SinclairQLd oppure SinclairQL?^& o altri simboli spuri al termine della stringa: questo perche', come gia' detto, manca il terminatore di stringa e vengono stampati anche i contenuti della memoria consecutiva alla locazione dove termina l'array fino ad incontrare il primo valore binario 0. (In caso piu' grave possiamo anche causare un crash di sistema che obblighera' a resettare la macchina!). E' percio' utile dichiarare l'array di dimensioni almeno di un'unita' superiore al massimo numero di elementi che si prevede di utilizzare e, in ogni caso, inizializzare l'array con tutti gli elementi a 0. funzione() { char stringa[11]; int i; for( i = 0; i < 11; i++ ) stringa[i] = 0; ... In questo modo ci siamo assicurati che la nostra stringa sia inizializzata correttamente ad un valore nullo. Il C prevede alcune utili funzioni di libreria che consentono di effettuare un'inizializzazione di un'area di memoria ad un valore predefinito. Utilizzando la memset il precedente pezzo di codice diverra': funzione() { char stringa[11]; memset( stringa, 0, 11 ); ... memset permette di inizializzare l'area di memoria referenziata dal primo parametro con il valore del secondo parametro per il numero di bytes definito dal terzo parametro ed e' definita nel file di includeDopo aver inizializzato a zero la stringa possiamo assegnare una serie di valori ai suoi elementi. #include #include void main( void ); void main( void ) { char stringa[15]; int i; memset( stringa, 0, 15 ); for( i = 0; i < 14; i++ ) stringa[i] = (char)getchar(); printf( "stringa = %s\n", stringa ); } Definiamo una stringa di 15 elementi, la inizializziamo a 0 e tramite un ciclo for introduciamo da tastiera 14 caratteri. Tramite la funzione printf stampiamo la stringa. La printf permette si stampare informazioni a video ed ha la seguente sintassi generale: int printf( const char *format [, argomenti]... ); Ritorna il numero di caratteri stampati o un valore negativo in caso di errore. La const char *format e' la stringa di formattazione dell'output e si compone di una serie di testo e caratteri di formattazione (quali %s %c %d %l %x, ecc...) che indicano al compilatore il modo in cui stampare gli argomenti che seguono la stringa di formattazione. Nel nostro esempio la stringa di formattazione e': "stringa = %s\n" che significa: stampa la parola stringa seguita da uno spazio (blank), da un segno di uguale ed un altro spazio, quindi interpreta il primo parametro come una stringa terminata da 0, il %s, ed emetti un carattere di newline, \n. Alcuni altri caratteri di formattazione sono: %c carattere singolo minuscolo %C carattere singolo maiuscolo %d numero decimale intero %x numero esadecimale con lettere minuscole %X numero esadecimale con lettere maiuscole %02X numero esadecimale di 2 cifre maiuscole, se il numero e' inferiore alle due cifre stampa uno 0 davanti alla cifra per ottenere il numero di cifre specificate (in questo caso 2 ma se ne possono usare quante necessarie). %s stringa di caratteri stampabili terminata da 0. %lf numero long float. %- allineamento a sinistra del valore stampato %+ se il valore e' del tipo con segno ed e' negativo il segno meno precedera' la cifra. Nota: per una completa definizione di tutti i possibili caratteri di formattazione fare riferimento alla documentazione acclusa al compilatore C. L'esempio ultimo, pur essendo sintatticamente corretto, e' ancora un sistema abbastanza stupido di inizializzare una stringa di caratteri in quanto obbliga ad inserire tutti i caratteri, non permette correzioni in caso di errore di battitura e necessita di un ciclo di lettura. Utilizziamo la funzione scanf cosi' definita in stdio.h: int scanf( const char *format [,argomenti]... ); sostituiamo il ciclo for con la seguente istruzione: scanf( "%s", stringa ); La scanf e' simile alla printf ma assegna agli argomenti il tipo indicato dalla const char *format. In questo caso assegna all'array stringa i valori letti da tastiera fino a quando si preme il tasto di INVIO o viene raggiunta la dimensione massima dell'array destinazione. Anche per la scanf valgono gli stessi caratteri di formattazione della printf ma bisogna considerare una cosa molto importante: se la destinazione (argomento) e' un tipo differente dall'array (ovvero carattere singolo, intero ecc... ) il nome dell'argomento deve essere preceduto dal simbolo & (indirizzo di). Vedremo in seguito il significato degli operatori * (puntatore o indirezione) e & (indirizzo di) ed il loro uso. Naturalmente gli array possono essere di tipi differenti dal char e si avranno array di qualunque tipo predefinito o definibile. Gli array possono avere anche piu' dimensioni per formare tabelle o strutture piu' complesse; si possono avere array di array, array di puntatori, array di puntatori a funzioni, array di puntatori a puntatori di array, ecc... Un esempio di array tabellare: int tabella[10][10]; definisce una tabella di 10 righe per 10 colonne di interi. Ogni singolo elemento sara' identificato da: tabella[x][y]; ed in questo caso non occorre settare a zero l'ultimo elemento dell'array in quanto non si tratta di una stringa di caratteri Esercizio. Definire una tabella 10*10 di interi e creare una tavola pitagorica. #include #include main() { int tabella[10][10]; int x, y; char nome[20] ; memset( nome, 0, 20 ); printf( "Introdurre il vostro nome (max 19 caratteri): " ); scanf( "%s", nome ) ; printf("Tavola Pitagorica programmata da %s\n\n", nome ); for( x = 0 ; x < 10 ; x++ ) for( y = 0 ; y < 10 ; y++ ) tabella[x][y] = (x+1)*(y+1); for( x = 0 ; x < 10 ; x++ ) { for( y = 0 ; y < 10 ; y++ ) printf( "%3d\t", tabella[x][y] ); printf( "\n" ); } } Questo esercizio riassume quanto finora esposto sugli array. Provate a modificarlo per ottenere una tavola che raggiunga anche il 12 o che ritorni il risultato di un solo valore scelto dall'utente tra quelli possibili (Suggerimento: il risultato di 3*5 e' ... Calcolate prima tutta la tabella ed usate in seguito un ciclo che richieda un identificatore di riga e di colonna. Visualizzate poi il risultato contenuto nell'elemento indicato. Verificate che i valori introdotti da tastiera rientrino nei limiti della tabella.)