Corso C. Lezione 3. ANSI C: Array. Funzioni. Stringhe. In questa lezione esamineremo prima gli ARRAY e poi introdurremo in modo piu' organico le funzioni, argomento gia' accennato nella prima lezione introduttiva. ARRAY. ------ Fino ad ora abbiamo visto le variabili come un contenitore per un solo valore del tipo che ci serve. In C esiste la possibilita' di dichiarare un array, cioe' un insieme di N valori tutti omogenei (quindi dello stesso tipo). Gli array, come le variabili, hanno un identificativo che viene indicato nella dichiarazione. In piu' negli array si usa un indice che ci permette di accedere, o meglio indirizzare, ogni singolo elemento. Quando dichiariamo un array dobbiamo specificare la sua "dimensione", cioe' il numero di elementi che potra' contenere. La sintassi che si usa nella dichiarazione di un array e' la seguente: TIPO nomearray[DIMENSIONE]; per esempio se volessi dichiarare un array di 4 interi: int mioarray[4]; Possiamo modellizzare l'array appena dichiarato in questo modo mioarray +------------------+ 0 | | +------------------+ 1 | | +------------------+ 2 | | +------------------+ 3 | | +------------------+ Come possiamo vedere abbiamo allocato lo spazio per contenere 4 variabili di tipo int, tutte sotto lo stesso nome mioarray. Per accedere ai singoli valori possiamo utilizzare gli indici da 0 a 3. Notare che in C tutti gli array hanno 0 come indice del primo elemento e questa caratteristica NON puo' essere cambiata in nessun modo (cosa che e' invece possibile in altri linguaggi strutturati come il Pascal). Per riferirci ad un elemento di un array usiamo la sintassi nomearray[ INDICE ] dove al posto di INDICE dobbiamo inserire un numero (o un espressione) che indica quale elemento vogliamo utilizzare. Per esempio, se voglio scrivere il valore 10 nel secondo elemento dell'array dichiarato prima, usero' il seguente comando: mioarray[1] = 10; Se invece volessi stampare il quarto valore scrivero': printf( "%d\n", mioarray[3] ); quindi utilizzo gli elementi dell'array esattamente come se fossero delle variabili. Cosa succede se provo ad accedere ad un elemento che non puo' essere contenuto nell'array? Se per esempio scrivo: int array[100]; array[105] = 10; il compilatore mi segnala l'errore? La risposta e': No, il compilatore non da nessun errore. Quando eseguo il programma il processore andra' a scrivere nella zona di memoria esattamente contigua a quella dell'array. Probabilmente quella zona di memoria sara' NON allocata e il mio programma crashera' andando in segmentation fault (perche' ho tentato di scrivere in una zona di memoria vietata o perche' ho sovrascritto altre informazioni importanti contenute in quella zona). Si dice quindi che il C non fa il "buond checking" (controllo sui bordi, tradotto brutalmente). Quindi deve essere cura del programmatore scrivere un programma che non faccia cose che non deve fare. Per la cronaca, molti server sono stati bucati perche' avevano dei problemi di sicurezza dovuti proprio al fatto che il C non effettua alcun buond checking. Ma questa e' una storia che vedremo' piu' avanti nel corso ;-). Vediamo un semplice esempio di utilizzo degli array. Questo programma prima riempie un array con 10 valori pseudo-casuali, gli stampa e poi trova il maggiore stampandolo a video: --------- File: ex13-array.c -------- 1: #include 2: 3: int main() 4: { 5: int array[10]; 6: int i, max, imax; 7: 8: for ( i=0; i<10; i++ ) 9: { 10: array[i] = rand() % 100; 11: printf( "Elemento %d = %d\n", i, array[i] ); 12: } 13: 14: max=0; imax=0; 15: for ( i=0; i<10; i++ ) 16: if ( max < array[i] ) 17: { 18: max = array[i]; 19: imax = i; 20: } 21: 22: printf( "Il massimo e' %d contenuto nell'elemento %d.\n", 23: max, imax ); 24: return(1); 25: } --------- File: ex13-array.c -------- Nella riga 10, ad ogni elemento dell'array assegno un numero casuale. Questo numero viene generato dalla funzione rand() ("man rand" per avere tutti i dettagli anche se sono sicuro che ormai non devo piu' dirvi queste cose e tutti andate automaticamente a guardare i dettagli delle funzioni sconosciute sull'help...). Di questo numero poi viene fatto il modulo con 100. In questo modo qualunque sia il risultato della rand alla fine avro' un valore compreso fra 0 e 99. Nello stesso ciclo for alla riga 11 stampo al volo il valore appena inserito nell'array. Per quanto riguarda la ricerca del massimo, dovro' nuovamente scorrere l'array con i valori appena inseriti e "salvarmi" in una variabile di supporto il valore massimo trovato fino a quel momento. La variabile max contiene il valore mentre la variabile imax contiene l'indice dell'elemento dell'array che contiene il massimo. Gli array visti finora si chiamano array monodimensionali, perche' per accedere ad un elemento utilizziamo un solo indice. Questi array sono l'equivalente dei vettori in matematica. Possiamo dichiarare anche degli array multidimensionali, per esempio delle matrici, in questo modo: int matrice[4][5]; in questo modo ho dichiarato un array in cui per accedere ad un elemento dovro' utilizzare una coppia di indici. La dimensione dell'array e 4x5 per un totale di 20 elementi cosi' rappresentabili: matrice 0 1 2 3 4 +-----+-----+-----+-----+-----+ 0 | | | | | | +-----+-----+-----+-----+-----+ 1 | | | | X | | +-----+-----+-----+-----+-----+ 2 | | | | | | +-----+-----+-----+-----+-----+ 3 | | | | | | +-----+-----+-----+-----+-----+ per accedere all'elemento indicato dalla X dovro' scrivere: matrice[1][3] = 10; Non c'e' limite al numero di dimensioni che posso utilizzare. Volendo posso creare un array a 5 dimensioni (non chiedetemi pero' l'utilita' pratica di una cosa del genere...). FUNZIONI. --------- Riprendiamo adesso un argomento piuttosto importante (non solo in C ma in qualsiasi linguaggio strutturato e non). Abbiamo detto la volta scorso che una funzione e' un "oggetto" che prende dei valori in input e ci restituisce un risultato in output. In C le funzioni si dichiarano con la seguente sintassi: TIPO_DEL_RISULTATO nomefunzione( TIPO_ARGOMENTO nome, ... ) { CORPO_DELLA_FUNZIONE; return( RISULTATO ); } Senza stare a riprende tutto cio' che e' stato gia' detto nella prima lezione, diciamo rapidamente il significato di ogni termine: TIPO_DEL_RISULTATO: Indica di che tipo sara' il valore restituito dalla funzione. Se voglio dichiarare una procedura (cioe' una funzione che NON restituisce nulla) dovro' utilizzare il tipo "void". nomefunzione: E' il nome della funzione. TIPO_ARGOMENTO: Indica il tipo del primo argomento della funzione. nome: Indica il nome del primo argomento della funzione. RISULTATO: E' un espressione che, una volta valutata, sara' il risultato vero e proprio della funzione. Gli argomenti sono i valori che la funzione prende in input. Posso dichiararne quanti ne voglio separandoli con la virgola ",". Se la funzione non prende nessun argomento dovro' inserire semplicemente la coppia di parentesi tonde "( )" senza scrivere nulla al loro interno. Gli argomenti possono essere usati all'interno della funzione come se fossero delle variabili dichiarate nel corpo della funzione stessa. Vediamo per esempio una funzione che dati due numeri interi mi calcola la loro somma: --------- File: ex14-funzioni.c -------- 1: #include 2: 3: int somma( int a, int b ) 4: { 5: int c; 6: c = a + b; 7: return(c); 8: } 9: 10: int main() 11: { 12: int ris; 13: 14: ris = somma( 5, 6 ); 15: printf( "ris=%d\n", ris ); 16: 17: printf( "7+8=%d\n", somma( 7, 8 ) ); 18: return(1); 19: } --------- File: ex14-funzioni.c -------- Questa funzione accetta due parametri interi e restituisce un valore intero pari alla somma dei due parametri. +---------+ a --->| | | somma |---> risultato b --->| | +---------+ Notare che le variabili a e b vengono inizializzate con i valori che io "passo" alla funzione quando la richiamo. Per esempio se ad un certo punto del programma scrivo: ris = somma( 15, 20 ); le variabili a e b vengono inizializzate con i valori 15 e 20 rispettivamente. I valori passati alla funzione vengono SEMPRE passati "per copia". Questo significa che se richiamo la funzione in questo modo ris = somma( c, d ); i valori di c e d vengono copiati in a e b, che sono due istanze diverse da c e d. Quindi se nel corpo della funzione modifico il contenuto di a o di b queste modifiche NON avranno ripercussioni sul contenuto di c e d che rimarra' inalterato. Lo scope (visibilita') dei parametri e' limitato al corpo della funzione, proprio come se questi fossero dichiarati nel corpo della funzione stessa. Posso passare un array come argomento dichiarando la funzione in questo modo: int somma( int mioarray[] ) { ... } quindi senza specificare la grandezza dell'array. Perche' non indico anche la grandezza dell'array? Semplicemente perche' e' un informazione ridondante in quanto il C non fa bound checking. E' sempre cura del programmatore (ripeto) fare in modo che il processore non vada a scrivere oltre il limite superiore dell'array. Gli array vengono passati "per riferimento". Se nel corpo della funzione modifico il contenuto dell'array verra' modificato anche il contenuto dell'array che ho usato nella chiamata. Ma come, non si era detto che in C gli argomenti sono passati sempre PER COPIA? In realta' qui la questione e' piu' sottile e verra' risolta quando introdurremo i puntatori nella prossima lezione. Per adesso, onde evitare inutili complicazioni, prendete il caso degli array come un eccezione alla regola. Alla luce di questo capiamo perche' non si specifica la dimensione dell'array nella dichiarazione della funzione: l'array prende la dimensione dell'array passato nella chiamata della funzione. Vediamo un esempio con il passaggio di array: --------- File: ex15-array.c -------- 1: #include 2: 3: int somma( int numeri[] ) 4: { 5: int totale, i; 6: totale = 0; 7: 8: for ( i=0; numeri[i]!=0; i++ ) 9: totale += numeri[i]; 10: 11: return(totale); 12: } 13: 14: int main() 15: { 16: int array[] = { 17: 10, 15, 20, 17, 5, 22, 40, 0 18: }; 19: 20: printf( "Totale=%d\n", somma(array) ); 21: } --------- File: ex15-array.c -------- Alle righe 16,17,18 dichiaro un array inserendo direttamente i valori al suo interno. In questo caso non ho bisogno di dichiarare la dimensione in quanto mi verra' creato automaticamente di 8 elementi. Posso comunque specificare la dimensione (possibilmente con un numero maggiore o uguale a 8....). La funzione somma calcola la somma di tutti i numeri contenuti nell'array. Se osservate bene la riga 8, la condizione di entrata nel ciclo e' "numeri[i]!=0" quindi il ciclo termina non appena viene incontrato un valore uguale a 0. In pratica per "terminare" l'array utilizzo il valore 0. Se passassi alla funzione un array senza questo valore, il ciclo for andrebbe avanti oltre la dimensione dell'array andando a pescare valori in zone di memoria di cui non abbiamo il controllo, dando risultati imprevedibili. Vediamo ora un esempio di funzione "ricorsiva". Una funzione ricorsiva in generale e' una funzione che al suo interno richiama se stessa. Generalmente le funzioni ricorsive si adattano perfettamente a risolvere problemi definiti in modo ricorsivo (pensate un po'... :). Per esempio, per calcolare il fattoriale di un numero posso scrivere qualcosa del genere: int fattoriale( int n ) { if ( n==0 || n==1 ) return 1; return( fattoriale(n-1) * n ); } perche' il fattoriale di n (n!) e' definito proprio cosi': CASO BASE: 0! = 1 1! = 1 CASO GENERALE: n! = n * (n-1)! Notare che la soluzione e' IDENTICA alla definizione del problema. Per questo motivo gli algoritmi ricorsivi risultano molto "eleganti" e piacciono particolarmente ai Matematici. In generale ogni algoritmo ricorsivo ha un equivalente iterativo (realizzato con dei cicli for/while). Gli algoritmi ricorsivi pero' in alcuni casi semplificano notevolmente la soluzione di una classe di problemi. Per contro gli algoritmi ricorsivi sono meno efficienti degli equivalenti iterativi. STRINGHE. --------- Le stringhe sono una sequenza di caratteri. Abbiamo usato piu' volte le stringhe come costanti, per esempio inserendole come argomento della funzione prinf(). Vediamo piu' in dettaglio questo tipo di dato. Intuitivamente, possiamo pensare un carattere di una stringa come il valore contenibile in una variabile di tipo char. Viene naturale pensare quindi che una stringa di n caratteri sia un array di char. In effetti e' cosi' che in C vengono gestite le stringhe. Un array di 10 char definito cosi': char stringa[10]; puo' contenere una stringa: 0 1 2 3 4 5 6 7 8 9 +-----+-----+-----+-----+-----+-----+-----+-----+------+---+ | 'I' | 'o' | ' ' | 'g' | 'i' | 'o' | 'c' | 'o' | '\0' | | +-----+-----+-----+-----+-----+-----+-----+-----+------+---+ L'ultima posizione rimane inutilizzata in questo esempio. Ricordiamo che scrivendo 'I' il compilatore converte automaticaticamente questo simbolo nel codice ASCII della lettera indicata tra apici. Il codice ascii della I e' 73, quindi 'I'==73. Notiamo che anche lo spazio e' un carattere. Inoltre notiamo che la stringa viene "terminata" dal carattere '\0'. Questo e' un carattere speciale denominato NUL il cui codice e' 0. Il codice ASCII definisce oltre alle lettere dell'alfabeto i numeri e alcuni segni di punteggiatura, anche altri caratteri speciali che vengono utilizzati per scopi di controllo. Alcuni esempi sono: LF (Line Feed = Accapo) '\n' == 13 NUL '\0' == 0 HT (Tabulazione) '\t' == 9 BS (Back Space) '\b' == 8 eccetera... "man ascii" per avere l'elenco completo... Quindi quando noi scrivevamo: printf( "Ciao\n" ); il compilatore creava "al volo" un array di 6 caratteri con la stringa specificata e lo passava alla funzione printf. Un altro modo di dichiarare una stringa e' il seguente char stringa[] = "Ciao"; in questo caso l'array mi viene automaticamente dimensionato a 5 (ricordate lo '\0' alla fine della stringa). Da notare inoltre la differenza quando scriviamo 'A' e "A". Il primo e' il carattere 'A' il secondo e' l'array che contiene la stringa "A". 'A' == 65 +-----+------+ "A" == | 'A' | '\0' | +-----+------+ Esistono vari comandi di libreria in C per la gestione delle stringhe (la maggiorparte dei quali e' dichiarato in string.h). Quando vogliamo stampare una stringa con printf dobbiamo utillizzare il formattatore "%s". Per esempio: char stringa[] = "MegaBug"; printf( "%s e' un gran figo.\n", stringa ); Se invece vogliamo stampare un carattere dobbiamo utilizzare il formattatore "%c". Per esempio: char st[] = "ABCDEF"; printf( "Il primo carattere di %s e': %c\n", st, st[0] ); printf( "Il terzo carattere di %s e': %c\n", st, st[2] ); Come ultimo esempio di questa lezione scriviamo una funzione che calcola la lunghezza di una stringa. In realta' esiste gia' una funzione di libreria che fa questo lavoro, la "strlen", definita in string.h, comunque puo' risultare interessante riscriverla. Vediamo: --------- File: ex16-strlun.c -------- 1: #include 2: #include 3: 4: int strlun( char s[] ) 5: { 6: int l = 0; 7: 8: while ( s[l] != '\0' ) 9: l++; 10: 11: return l; 12: } 13: 14: int main() 15: { 16: char str1[] = "Prova stringa 1"; 17: char str2[] = "Stringa molto lunga..... lunghissima......"; 18: char str3[] = "A"; 19: 20: printf( "Lunghezze calcolate con strlen: %d %d %d\n", 21: strlen(str1), strlen(str2), strlen(str3) ); 22: printf( "Lunghezze calcolate con strlun: %d %d %d\n", 23: strlun(str1), strlun(str2), strlun(str3) ); 24: 25: return(0); 26: } --------- File: ex16-strlun.c -------- Come potete vedere la funzione strlun e' piuttosto semplice: scorre tutto l'array s[] finche non incontra uno '\0'. A quel punto avro' in l proprio la lunghezza della stringa s, come si puo' facilmente notare da questo disegno: 0 1 2 3 4 5 +-----+-----+-----+-----+-----+------+----+----+ | 'P' | 'r' | 'o' | 'v' | 'a' | '\0' | | | +-----+-----+-----+-----+-----+------+----+----+ quando esco dal while avro' l==5 che e' la lunghezza della stringa "Prova".