Corso C. Lezione 2. ANSI C: Le variabili e i costrutti. Dopo aver introdotto (nella prima lezione) alcuni concetti del C, per avere una visione d'insieme delle principali potenzialita' del linguaggio, andiamo ad esaminare in dettaglio i vari aspetti, definendoli in modo un po' piu' formale. LE VARIABILI. ------------- Le variabili sono lo "strumento" che ci mette a disposizione il linguaggio per "memorizzare" dei valori, fare delle "operazioni" su di essi e "leggere" eventuali risultati. I valori che posso memorizzare in una variabile possono essere di vari TIPI. Questo e' un concetto piuttosto importante, perche' il tipo di una variabile indica al compilatore come dovra' interpretare il contenuto della zona di memoria riservata a quella variabile e quanti byte di questa memoria dovranno essere riservati. Per esempio una variabile di tipo char conterra' un carattere alfanumerico e la dimensione di questa variabile sara' di 1 byte. In realta' in quel byte di memoria il contenuto "grezzo" e' sempre un insieme di bit, ma il compilatore interpretera' quell'insieme di bit secondo la codifica ASCII in quanto gli abbiamo detto che la variabile e' di tipo char. Detto questo vediamo i tipi che il C ci mette a disposizione: TIPO DIMENSIONE CONTENUTO ----------------(in bit)------------------------ short 16 Intero con segno int 32 Intero con segno long 32 Intero con segno unsigned short 16 Intero senza segno unsigned int 32 Intero senza segno unsigned long 32 Intero senza segno float 32 Numero in virgola mobile double 64 Numero in virgola mobile char 8 Carattere ASCII void 8 Vuoto Il C mette a disposizione il comando sizeof(...) che ci permette di sapere quanto spazio occupa una variabile. Per sapere per esempio quanti byte occupa una variabile di tipo int posso scrivere: printf( "%d\n", sizeof(int) ); il risultato sara' la dimensione in byte del tipo int. Provate come esempio il seguente programma: --------- File: ex7-sizeof.c -------- 1: #include 2: 3: int main() 4: { 5: printf( "Dimensione short : %d\n", sizeof(short) ); 6: printf( "Dimensione int : %d\n", sizeof(int) ); 7: printf( "Dimensione long : %d\n", sizeof(long) ); 8: printf( "Dimensione uns. short : %d\n",sizeof(unsigned short)); 9: printf( "Dimensione uns. int : %d\n", sizeof(unsigned int) ); 10: printf( "Dimensione uns. long : %d\n", sizeof(unsigned long)); 11: printf( "Dimensione float : %d\n", sizeof(float) ); 12: printf( "Dimensione double : %d\n", sizeof(double) ); 13: printf( "Dimensione char : %d\n", sizeof(char) ); 14: printf( "Dimensione void : %d\n", sizeof(void) ); 15: } --------- File: ex7-sizeof.c -------- La dimensione dei tipi interi puo' variare in funzione del sistema operativo e del computer sul quale gira il compilatore. Per esempio nei vecchi sistemi operativi a 16-bit la dimensione dell'int era di 16 bit (ecco spiegata l'esistenza del tipo long che potrebbe sembrare un inutile duplicato di int). Per poter utilizzare una variabile bisogna DICHIARARLA, bisogna cioe' dire esplicitamente al compilatore che utilizzeremo all'interno del nostro codice una variabile di un certo tipo e ci riferiremo ad essa con un certo nome (identificativo). Per fare la dichiarazione si usa la seguente sintassi: TIPO nomevariabile; oppure se ne voglio dichiararne piu' di una: TIPO nomevariabile1, nomevariabile2, ... , nomevariabilen; al posto di TIPO dobbiamo mettere uno dei tipi visti prima. La dichiarazione delle varibili va obbligatoriamente fatta all'inizio di ogni blocco di istruzioni. Un blocco di istruzioni e' racchiuso tra parentesi graffe '{' '}'. if ( ... ) { int a, b; a = 10; b = 20; } Per esempio in questo caso dichiaro due variabili int a cui mi riferisco usando gli identificativi "a" e "b". Esistono poi delle regole di "scoping" nel linguaggio che mi definiscono la "visibilita'" delle varibili all'interno del programma. Queste regole dicono che una variabile e' visibile solo all'interno del blocco in cui e' dichiarata e all'interno dei sottoblocchi che esso contiene. Quando dico che una variabile e' visibile, intendo che una variabile puo' "essere utilizzata", oppure che la variabile "esiste". Quindi: una variabile esiste e puo' essere utilizzata SOLO nel blocco in cui e' dichiarata. Al di fuori del blocco la variabile NON ESISTE (lo so che puo' sounare strano ma e' proprio cosi'). Vediamo un esempio: --------- File: ex8-scoping.c -------- 1: int main() 2: { 3: int a, b, n; 4: a = 10; 5: b = 20; 6: 7: if ( a == 10 ) 8: { 9: int b, c; 10: c = a; 11: b = 30; 12: printf( "b=%d c=%d\n", b, c ); 13: } 14: 15: printf( "a=%d b=%d n=%d\n", a, b, n ); 16: } --------- File: ex8-scoping.c -------- Esaminiamo lo scope delle variabili una per una. La variabile a puo' essere vista ovunque nel main, infatti viene settata (impostata) a 10 nella riga 4 e poi ne viene copiato il valore in c nella riga 10, puo' essere quindi letta anche all'interno del blocco if perche' questo e' interno al blocco main. Discorso analogo per la variabile n. La variabile b viene dichiarata prima nel main e poi viene "ridichiarata" nell'if. In questo caso esistono DUE istanze della variabile b. L'istanza di b nell'if e' visibile solo all'interno di questo blocco. L'istanza di b del main e' visibile nel main ma non all'interno dell'if perche' la seconda istanza "nasconde" la prima. La variabile c e' visibile solo all'interno dell'if. L'output del programma e' quindi il seguente: 1: b=30 c=10 2: a=10 b=20 n=1074732248 Come detto prima la variabile b all'uscita dell'if riprende il suo vecchio valore (20) quindi e' come avere due variabili separate. Che e' successo alla variabile n? Se osservate bene non abbiamo assegnato ad n nessun valore all'interno del codice. Si dice che in questo caso la variabile non e' "inizializzata" (vedo gia' le notti insonni che passere per questo tipo di errori... ;)) quindi il suo contenuto e', a tutti gli effeti, aleatorio: prende il valore che si ritrova nella cella di memoria del computer in quel momento.... GLI OPERATORI. -------------- Gli operatori sono dei costrutti del C che ci permettono di manipolare il contenuto delle variabili. Ne esistono vari, ma procediamo con ordine: - Operatore di assegnamento: VARIABILE = VALORE; quest'operatore copia (assegna, imposta, "setta"...) un valore all'interno di una variabile. Notare che il valore deve essere dello stesso TIPO della variabile. Non posso (o meglio non dovrei) assegnare, per esempio, un numero in virgola mobile ad una variabile intera. Il valore puo' essere una costante (ad esempio il numero 15) oppure il risultato di un'espressione (ad esempio a+b/2). - Operatori aritmetici: servono per costruire delle espressioni aritmetiche. Gli operatori aritmetici del C sono + Somma - Sottrazione * Moltiplicazione / Divisione % Modulo Le divisioni fra interi troncano eventuali parti decimali nel risultato. L'operatore di modulo da' come risultato il resto della divisione fra i due operandi. Per esempio 13%5 da' come risultato 3 (il resto della divisione fra 13 e 5). Gli operatori *, % e / hanno la stessa priorita' (precedenza) che e' superiore a quella deglio operatori + e -. - Operatori relazionali e logici: Servono per costruire delle espressioni booleane, cioe' delle espressioni che hanno come risultato un valore di verita' (vero o falso). Gli operatori relazionali in C sono > Maggiore >= Maggiore o uguale < Minore <= Minore o uguale == Uguale (non confondere con = che e' un assegnamento!) != Diverso con questi operatori posso creare dei confronti che posso utilizzare nelle parti condizionali dei vari costrutti del C (if, while, for...). Esistono poi gli operatori logici che permettono di costruire delle espressioni piu' complesse && "AND" logico. || "OR" logico. ! "NOT" logico. Per esempio posso scrivere ESPRESSIONE1 && ESPRESSIONE2 il risultato sara' vero solo se entrambe le espressioni sono vere, falso altrimenti. Per esempio se voglio sapere quando una variabile assume un valore compreso fra 20 e 30 posso scrivere if ( (a>20) && (a<30) ) { ... il corpo dell'if sara' eseguito solo se a e' maggiore di 20 e (and) a e' minore di 30. Gli operatori logici hanno una precedenza piu' bassa rispetto agli altri, quindi posso eliminare le parentesi senza correre il rischio che il programma esegua prima l'&&: if ( a>20 && a<30 ) { ... Se invece voglio sapere quando a NON e' compreso tra 20 e 30, posso semplicemente negare l'espressione precedente usando l'operatore !: if ( !(a>20 && a<30) ) { ... L'operatore || ha una precenza piu' bassa rispetto a && quindi la scrittura if ( a>20 && a<30 || a==40 ) { ... e' equivalente a if ( (a>20 && a<30) || a==40 ) { ... in questo caso l'if verra' eseguito solo se a e' compreso tra 20 e 30 OPPURE se a e' uguale a 40. - Operatori di incremento e decremento. Servono a incrementare (sommare 1) o decrementare (sottrarre 1) dalle variabili ++ Incrementa -- Decrementa questi operatori si chiamano "unari" in quanto agiscono solo su UN argomento. Per esempio il comando: a++; incrementa il contenuto della variabile a. Inoltre possono essere utilizzati con notazione prefissa (++a) o postfissa (a++). Il risultato sulla variabile a e' lo stesso. La differenza e' quando utilizziamo questi operatori all'interno di una espressione, per esempio: CASO 1: CASO 2: a = 10; a = 10; b = ++a; b = a++; Nel primo caso il valore di b sara' 11, mentre nel secondo sara' 10. Quindi nella notazione prefissa prima viene prima incrementata la variabile a e poi si utilizza il suo valore all'interno dell'espressione. Viceversa nella notazione postfissa prima si utilizza il valore di a e poi questo viene incrementato. - Operatori sui BIT. Questi agiscono direttamente su valori a livello di bit. Non stiamo a discutere molto su questi, anche perche' la loro utilita' (a questo punto del corso...) e' ancora molto limitata. Gli operatori che agiscono sui bit sono & AND bit a bit | OR inclusivo bit a bit ^ OR esclusivo bit a bit << shift a sinistra >> shift a destra ~ complemento a uno - Altri operatori di assegnamento. Questi operatori sono delle scritture un po' piu' compatte per le assegnazioni Il comando puo' essere sostituito con -------------------------------------------------- a = a + 5; a += 5; a = a - 5; a -= 5; in generale: VAR = VAR OPER ESPR; VAR OPER= ESPR; gli operatori che possono essere "compressi" sono: + - * / % << >> & ^ | - Operatore per la conversione di tipo (CAST). Questo e' un operatore molto importante perche' ci permette di "convertire" il risultato di un'espressione nel tipo che vogliamo. Possiamo quindi fare per esempio delle operazioni su interi e assegnare il risultato ad una variabile in virgola mobile, dopo averto opportunamente convertito. La sintassi del costrutto di cast (conversione) e' la seguente: ( TIPO ) ESPRESSIONE Questa scrittura indica al compilatore che deve convertite l'ESPRESSIONE nel tipo TIPO. Per esempio: float a; int b; b = 20; a = (float) b; in questo caso il valore di b viene convertito in un float e poi assegnato ad a. Potrei fare il contrario, cioe' convertire un valore float in un int, pero' in questo caso avrei una "perdita di informazione" perche' eventuali parti frazionarie verrebbero troncate. Il casting e' cmq. un'operazione delicata, perche' presenta in certi casi alcune controindicazioni. Riprenderemo comunque questo argomento piu' avanti nel corso. La seguente tabella indica la precedenza di tutti gli operatori. In una espressione saranno valutati prima gli operatori che stanno piu' in alto in questa tabella. Naturalmente si puo' modificare l'ordine di valutazione inserendo le parentesi "(" ")" opportune. OPERATORE -------------------------------------------- ! ++ -- (tipo) sizeof * / % + - << >> < <= > >= & ^ | && || = += -= *= /= %= &= ^= |= <<= >>= -------------------------------------------- Bene, visto che siete arrivati fin qui nella lettura senza morire di noia, passiamo ad esaminare qualcosa di piu' leggero. I COSTRUTTI DI CONTROLLO. ------------------------- Il C e' un linguaggio strutturato. Questo significa che possiamo scrivere tutti i nostri programmi utilizzando un piccolo insieme di comandi che realizzano i cosiddetti "costrutti". I costrutti possono essere di tre tipi: - Sequenza. - Alternativa. - Iterazione o ciclo. La sequenza non e' altro che una lista di comandi che vengono eseguiti in ordine uno alla volta. In pratica e' quello che abbiamo fatto finora quando scrivevamo i comandi all'interno di un blocco delimitato da parentesi graffe. L'alternativa ci permette di eseguire una sequenza di comandi oppure un'altra a seconda se si verifica una condizione. Per realizzare questo in C abbiamo gia' visto il comando if nella prima lezione: if ( CONDIZIONE ) { BLOCCO; } oppure la forma con l'else: if ( CONDIZIONE ) { BLOCCO1; } else { BLOCCO2; } non ripetero' qui quello che gia' ho spiegato nella prima lezione. Voglio solo far notare il fatto che un blocco di istruzioni puo' essere scritto in modo piu' compatto se il blocco stesso e' formato da UNA SOLA istruzione. In questo caso infatti possiamo omettere di delimitare il blocco con le parentesi graffe, per esempio: if ( a==10 ) { printf( "a vale 10.\n" ); } printf( "questo e' fuori dall'if...\n" ); puo' essere scritto in modo piu' compatto cosi': if ( a==10 ) printf( "a vale 10.\n" ); printf( "questo e' fuori dall'if...\n" ); l'istruzione if viene terminata dal ; alla fine della printf. Ecco un altro esempio: if ( a==10 ) printf( "a vale 10.\n" ); else printf( "a NON vale 10.\n" ); Come posso realizzare un'alternativa multipla? cioe' cosa devo scrivere per fare qualcosa che faccia delle cose diverse a seconda del contenuto di una variabile? Ci sono vari modi. Uno puo' essere scrivere una catena di if fatta in questo modo: if ( a==1 ) { BLOCCO1; } else { if ( a==2 ) { BLOCCO2; } else { BLOCCO3; } } Questa scrittura e' chiaramente poco pratica, dato che gia' una selezione fra due valori mi crea due livelli di indentazione. Una selezione fra piu' di due valori mi appesantira' notevolmente la scrittura del codice a causa della "profondita'" dell'indentazione che si potrebbe raggiungere. Ma facciamo un'attimo un'osservazione: il blocco else piu' grosso contiene al suo interno una sola istruzione, l'if. Questo, per quanto detto prima, mi permette di eliminare le parentesi graffe: if ( a==1 ) { BLOCCO1; } else if ( a==2 ) { BLOCCO2; } else { BLOCCO3; } Se adesso riscrivo in modo un po' piu' "furbo" questo pezzo di codice posso ottenere il seguente risultato: if ( a==1 ) { BLOCCO1; } else if ( a==2 ) { BLOCCO2; } else { BLOCCO3; } Che risulta gia' piu' gradevole e soprattutto non presenta l'incoveniente dell'indentazione. Notare che "else if" non e' un comando del C, bensi' le singole parole "else" ed "if" sono comandi del C. Quindi io non ho usato il comando "else if" ma ho usato in modo piu' accorto i comandi "else" ed "if". Il C in realta' ha un comando che permette di fare la stessa cosa: il comando "switch/case". La sintassi del comando switch/case e' la seguente: switch ( ESPRESSIONE ) { case VALORE1: AZIONE; AZIONE; break; case VALORE2: AZIONE; AZIONE; break; default: AZIONE; }; La struttura e' abbastanza semplice. Prima di tutto viene valutato il valore di ESPRESSIONE (che realisticamente sara' una variabile) che viene confrontato con i vari VALORE1, VALORE2, ecc. Nel caso uno di questi valori corrisponda vengono eseguite le azioni fra il case relativo e il primo break. Nel caso non ci siano corrispondenze con i valori indicati vengono eseguite le azioni scritte dopo il default. Per esempio: --------- File: ex9-switch.c -------- 1: #include 2: 3: int main( int argc, char **argv ) 4: { 5: int a; 6: a = atoi( argv[1] ); 7: 8: switch ( a ) 9: { 10: case 1: 11: printf( "a vale 1\n" ); 12: break; 13: case 2: 14: printf( "a vale 2\n" ); 15: break; 16: case 3: 17: case 4: 18: printf( "a vale 3 o 4\n" ); 19: break; 20: case 5: 21: printf( "a vale 5\n" ); 22: case 6: 23: printf( "a vale 5 o 6\n" ); 24: break; 25: default: 26: printf( "a non so quanto vale!\n" ); 27: }; 28: return(1); 29: } --------- File: ex9-switch.c -------- I casi 1 e 2 non credo abbiano bisogno di ulteriori commenti. Fra "case 3:" e "case 4:" non c'e' nessun break. Questo significa che il compilatore nel caso di a uguale a 3 eseguira' anche la parte relativa ad a uguale a 4. Nel caso di a uguale a 5 verra' eseguita prima la "printf( "a vale 5\n" );" e poi le istruzioni relative al caso a uguale a 6. In tutti gli altri casi viene eseguito il default. In generale verranno eseguiti i comandi compresi fra il "case" corrispondente e il primo "break" che segue. Vediamo ora i comandi che ci permettono di implementare cicli. Per fare un ciclo in C si puo' utilizzare il comando "while"/"do...while" oppure il comando "for". La sintassi del while e' la seguente: while ( CONDIZIONE ) { BLOCCO; } Quando l'esecuzione del programma arriva all'inizio del while viene subito controllata la CONDIZIONE. Se questa risulta verificata viene eseguito il BLOCCO di istruzioni. Arrivati alla fine del blocco di istruzioni viene nuovamente valutata la CONDIZIONE e se risulta ancora verificata il BLOCCO di istruzioni viene ripetuto. Questo finche' CONDIZIONE e' vero. In pratica il ciclo while ripete il blocco di istruzioni fintanto la CONDIZIONE e' vera. Vediamo un esempio: --------- File: ex10-while.c -------- 1: #include 2: 3: int main() 4: { 5: int a = 1; 6: 7: while ( a<=10 ) 8: { 9: printf( "%d\n", a ); 10: a++; 11: } 12: 13: return(1); 14: } --------- File: ex10-while.c -------- All'inizio del programma a vale 1, a<=10 e' vera percui viene eseguito il while la prima volta. All'interno del while a viene prima stampata su video e poi incrementata. A questo punto viene nuovamente controllata la condizione di entrata nel while e risulta 2<=10 -> vero. Quindi viene eseguito ancora il ciclo, viene stampata e incrementata a e cosi' via, finche' all'ultimo giro quando a e' arrivata al valore 10, viene stampato 10, viene incrementata a facendola diventare 11 e viene verificata la condizione di entrata al while cioe' 11<=10 -> falso. Quindi il ciclo finisce e l'esecuzione passa avanti (in questo caso c'e' il return che fa finire il programma). Notare che il while controlla la condizione solo ALLA FINE di ogni iterazione e all'INIZIO della PRIMA iterazione. Se la condizione di entrata al while diventa falsa DURANTE un iterazione ma poi ridiventa VERA prima della fine della stessa, il ciclo prosegue comunque. Lo stesso vale per gli altri tipi di cicli che andremo a vedere. Una variante al "while" e' il "do...while". La sintassi e' la seguente: do { BLOCCO; } while ( CONDIZIONE ); Notare il ; alla fine. La differenza in questo ciclo sta nel fatto che la condizione di entrata non viene verificata all'inizio. Quindi il BLOCCO di istruzioni verra' eseguito almeno una volta. Vediamo adesso il comando for: for ( INIZIALIZZAZIONE; CONDIZIONE; AVANZAMENTO ) { BLOCCO; } INIZIALIZZAZIONE e' un'istruzione che viene eseguita solo una volta all'inizio del ciclo, quindi prima di qualunque altra cosa. Poi viene controllata la CONDIZIONE e, se questa e' verificata, viene eseguito il BLOCCO di istruzioni. AVANZAMENTO e' un'istruzione che viene eseguita ALLA FINE di ogni iterazione. Inoltre dopo aver eseguito AVANZAMENTO viene verificata nuovamente la CONDIZIONE ed eventualmente rieseguito il ciclo se questa risulta vera. Vediamo lo stesso esempio di prima fatto con il for: --------- File: ex11-for.c -------- 1: #include 2: 3: int main() 4: { 5: int a; 6: 7: for ( a=1; a<=10; a++ ) 8: { 9: printf( "%d\n", a ); 10: } 11: 12: return 1; 13: } --------- File: ex11-for.c -------- La variabile a non viene inizializzata nella dichiarazione come prima, ma all'interno del ciclo for. Non credo ci sia bisogno di ulteriori commenti a parte il fatto che il codice e' molto piu' compatto. Qualcuno potrebbe chiedersi quale tipo di ciclo conviene utilizzare fra i tre disponibili. La risposta e' che non ce n'e' uno migliore degli altri, quindi rimane a discrezione del programmatore scegliere il piu' adatto. Naturalmente quello che posso fare con un ciclo di un tipo lo posso fare anche con un ciclo di tipo diverso (per come sono concepiti in C), percui la scelta e' influenzata esclusivamente da fattori di estetica e comprensibilita' del codice. Per concludere vediamo due comandi che ci saranno molto utili quando realizzeremo dei cicli un po' piu' complessi. I comandi sono "break" e "contintue". Il "break" serve per uscire immediatamente da un ciclo indipendentemente dal fatto che la condizione di uscita sia verificata e indipendentemente dal fatto che l'iterazione attuale si sia conclusa. In poche parole, se all'interno di un ciclo viene eseguita l'istruzione break questa fa terminare il ciclo stesso immediatamente. L'istruzione "continue" non termina il ciclo, bensi' termina l'iterazione corrente. Questo significa che se non sono ancora arrivato al termine dell'iterazione corrente e trovo un continue l'esecuzione riprendera' dall'inizio del ciclo. Probabilmente un esempio chiarira' meglio il funzionamento di questi comandi: --------- File: ex12-breakcontinue.c -------- 1: #include 2: 3: int main() 4: { 5: int a = 0; 6: 7: while ( a<=50 ) 8: { 9: a++; 10: if ( a>10 && a<20 ) 11: continue; 12: if ( a==30 ) 13: break; 14: printf( "%d\n", a ); 15: } 16: 17: return 1; 18: } --------- File: ex12-breakcontinue.c -------- Questo e' ancora il classico contatore ma con delle varianti. La prima e' che non voglio che mi stampi i numeri compresi fra 10 e 20. Per fare questo c'e' un continue alla riga 11 che mi termina l'iterazione se a e' compreso tra 10 e 20. Ho usato un continue perche' non voglio che il programma esca dal ciclo, dato che voglio stampare anche i numeri superiori a 20. La seconda variante e che voglio che il programma termini se a==30, per questo ho aggiunto un break alla riga 13. Ma non potevi mettere a!=30 come condizione del while? La risposta e' si'... chiaramente in questo programma volevo dimostrare che break esce comunque dal ciclo, anche se la condizione del while e' ANCORA verificata. In tutti i cicli che abbiamo visto e' possibile omettere le parentesi graffe se il ciclo stesso e' formato da una sola istruzione, esattamente come avviene per l'if. Per esempio for ( a=1; a<=10; a++ ) { printf( "%d\n", a ); } e' equivalente a for ( a=1; a<=10; a++ ) printf( "%d\n", a ); Questo e' tutto per quanto riguarda i costrutti. Preparatevi moralmente per la prossima lezione... ;-)