Linguaggio C: Generalità Il C è un linguaggio procedurale
Transcript
Linguaggio C: Generalità Il C è un linguaggio procedurale
Univ. degli Studi di Cagliari u Linguaggio C: Generalità Il C è un linguaggio procedurale-sequenziale, appartiene cioè alla stessa classe di linguaggi a cui appartengono Pascal e Fortran. Il C è stato definito nei primi anni 70 da Brian W. Kernighan e Dennis M. Ritchie con l'obiettivo di fornire da supporto per l'implementazione del sistema operativo UNIX. Il sistema operativo (eccetto il nucleo), il compilatore C ed essenzialmente tutti i programmi applicativi di UNIX sono scritti in C. Molte delle caratteristiche del C discendono dal linguaggio BCPL (Richards), attraverso il linguaggio B (Thompson) sviluppato nel 1970 per il primo sistema operativo UNIX su un calcolatore DEC PDP-11. Come molti altri linguaggi, il C non si presenta sempre nella stessa forma, ha subito cioè modifiche nel corso della sua esistenza. In particolare, il linguaggio si può presentare ... ð nella forma che risale alla sua definizione da parte degli autori, che chiameremo per brevità K&R-C. ð nella forma proposta dall'ANSI (American National Standards Institute), che nel 1983 ha costituito un comitato con l'obiettivo di dare una definizione del linguaggio C non ambigua e non dipendente dalla macchina, che chiameremo ANSI-C. Docente G. Armano -1- Appunti sul linguaggio C Univ. degli Studi di Cagliari Il C ha estensioni standard anche verso paradigmi di programmazione alternativi. Ad esempio: ð Parallel-C, per la programmazione concorrente ð C++, per la programmazione object-oriented (OOP) Nel seguito, non verrà fatto alcun accenno alle possibili estensioni del linguaggio per la programmazione concorrente, mentre ricordiamo una volta per tutte che il linguaggio C++ ha avuto una notevole importanza per la definizione dello standard ANSI-C. Infatti, molte innovazioni introdotte dal C++ sono state recepite dal comitato ANSI-C ed incorporate nello standard corrispondente. L'ANSI-C sarà il linguaggio di riferimento anche per questi appunti. Docente G. Armano -2- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Che cosa si intende quando si dice che il C è un linguaggio proceduralesequenziale ? ð procedurale le istruzioni del programma specificano cosa il programma deve fare Esempio: A = 22 ; ð /* Assegna 22 alla var. A */ sequenziale le istruzioni del programma specificano vengono fornite ed eseguite in sequenza Esempio: A = 22 ; B = -3 ; Docente G. Armano /* PRIMA, assegna 22 ad A */ /* POI, assegna -3 a B */ -3- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Un semplice esempio di programma C: Hello, world ! Vediamo come si fa in C a visualizzare sul monitor del computer la fatidica frase “Hello, world !” #include <stdio.h> main() { printf("Hello, world !\n") ; } Docente G. Armano -4- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Hello, world ! [II] Qualche nota sul programma che stampa “Hello, world !” - #include <stdio.h> è una direttiva per il compilatore. Si richiede di includere (nel file corrente) il file di libreria "stdio.h" che contiene la specificazione dei prototipi di funzione e delle costanti che gestiscono l'I/O - main () è una tra le possibili specificazioni − la più semplice− dell'interfaccia della funzione main (da cui parte l'esecuzione del programma). Il corpo della funzione segue immediatamente ed è contenuto tra parentesi graffe - printf("Hello, world !\n") ; è una chiamata alla funzione di sistema printf, che scrive una stringa sullo standard-output (tipicamente il video). Vedremo in seguito come la funzione printf può essere utilizzata per effettuare output generalizzato (formattazione e scrittura di costanti, espressioni e variabili di vario tipo) Docente G. Armano -5- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Hello, world ! [III] Supponiamo di aver salvato il programma nel file Hello.c. Il suffisso “.c” serve per ricordarci che il contenuto del file è un programma sorgente scritto in C. Per compilare il programma sorgente, sotto il prompt del sistema operativo (sia ad esempio il carattere “$”), dovremo attivare il compilatore C digitando: $ cc Hello.c In assenza di errori, il programma cc tenta di generare anche l'eseguibile, che − per default− si chiamerà a.out. Per dare al file eseguibile un nome diverso, ad esempio Hello, dovremo scrivere: $ cc -o Hello Hello.c A questo punto, digitando: $ Hello si ottiene sul video la scritta: Hello, world ! Docente G. Armano -6- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Un altro esempio: azzeramento del contenuto di un vettore Supponiamo di dover scrivere una funzione che azzera il contenuto di un vettore di numeri interi (vett). La dimensione del vettore (NMAX) viene passata come parametro alla funzione. void azzeraVettore ( int vett[], int NMAX ) { int i=0 ; while ( i < NMAX ) { vett[i] = 0 ; i = i+1 ; } } Risultato dell'applicazione della funzione ad un vettore di interi qualunque: tutti gli elementi del vettore (dall'elemento di indice 0 a quello di indice NMAX-1) vengono azzerati. Docente G. Armano -7- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Azzeramento del contenuto di un vettore [II] Qualche nota sulla funzione che azzera il contenuto di un vettore di interi: - void azzeraVettore ( int vett[ ], int NMAX ) è l'interfaccia della funzione azzeraVettore. Tale interfaccia specifica che la funzione non ritorna nulla (void), si chiama azzeraVettore, e i suoi parametri sono − nell'ordine− un vettore di interi (vett) e la dimensione del vettore (NMAX). - int i=0 ; definisce una variabile intera i, locale alla funzione azzeraVettore, che assume come valore iniziale zero. - while ( i < NMAX ) { ... } forza la ripetizione del codice tra parentesi graffe finché è verificata la condizione i < NMAX. - while ( ... ) { vett[i]=0 ; i = i + 1 ; } finché la condizione del test tra parentesi rotonde è verificata, assegna zero all'elemento vett[i] (elemento del vettore con indice i) e poi incrementa i. Docente G. Armano -8- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Azzeramento del contenuto di un vettore [III] Un esempio di programma che utilizza la funzione azzeraVettore: #include <stdio.h> void azzeraVettore ( int vett[], int NMAX ) { int i=0 ; while ( i < NMAX ) { vett[i] = 0 ; i = i+1 ; } } void main ( void ) { int V1[10] ; /* Vettore V1 di 10 interi */ int V2[50] ; /* Vettore V2 di 50 interi */ ... /* Altre dichiarazioni */ azzeraVettore(V1,10) ; /* Azzera gli elem. di V1 */ azzeraVettore(V2,50) ; /* Azzera gli elem. di V2 */ ... /* Altre istruzioni */ } Docente G. Armano -9- Appunti sul linguaggio C Univ. degli Studi di Cagliari u Azzeramento del contenuto di un vettore [IV] Si noti che nella chiamata azzeraVettore(V1,10): - V1 è un parametro attuale che viene fatto corrispondere con il parametro formale vett - 10 è un parametro attuale che viene fatto corrispondere con il parametro formale NMAX Per il momento, annotiamo che, in C: - i vettori vengono passati per indirizzo (ovvero tutto va come se la funzione lavorasse direttamente sul parametro attuale). - tutti gli altri tipi di parametri vengono passati per valore (ovvero si calcola il valore del parametro attuale e si usa tale valore per inizializzare la variabile locale equivalente che ha il nome del parametro formale). Docente G. Armano - 10 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Un esame più dettagliato delle caratteristiche del linguaggio C PARTE I: 1. 2. 3. 4. 5. strutture dati operatori strutture di controllo procedure e funzioni input/output PARTE II: 6. gestione della memoria dinamica 7. funzioni di libreria 8. altre caratteristiche del linguaggio (macro e direttive) Docente G. Armano - 11 - Appunti sul linguaggio C Univ. degli Studi di Cagliari LINGUAGGIO C: PARTE I Docente G. Armano - 12 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati Per i linguaggi delle classe del C, una struttura dati è caratterizzata dalla sua specificazione (tipo), ed è rappresentabile tramite le sue istanze, che possono essere costanti o variabili. u Strutture Dati: Tipi, Costanti e Variabili Un tipo di dato (concreto) è la specificazione delle caratteristiche fisiche che tutte le istanze che apparterranno al tipo devono avere. Una costante (di un certo tipo) è un elemento che appartiene al dominio definito dal tipo corrispondente. Una costante non cambierà il suo valore nel corso dell'esecuzione del programma. Una variabile (di un certo tipo) è un identificatore a cui è associato un indirizzo di memoria e che può ospitare valori appartenenti al dominio definito dal tipo corrispondente. Una variabile potrà assumere diversi valori nel corso dell'esecuzione del programma (o meglio durante il suo tempo di vita). Vedremo in seguito una definizione più dettagliata del concetto di variabile. Docente G. Armano - 13 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi I tipi possono essere: - built-in (predefiniti) / user-defined (derivati) - semplici / strutturati I tipi built-in sono già noti al compilatore del linguaggio utilizzato per la codifica del programma. I tipi user-defined sono definiti dall'utente sulla base di regole di composizione prefissate (array, records, enumerazioni, ecc.). I tipi semplici sono caratterizzati dal fatto di avere un dominio non strutturato (ad esempio: interi, caratteri, enumerazioni). I tipi strutturati sono caratterizzati dal fatto di avere un dominio strutturato (ad esempio: array e records). La definizione di un tipo strutturato viene mappata sui tipi semplici, direttamente (facendo ricorso a tipi built-in) oppure indirettamente (facendo riferimento ad identificatori di tipi definiti dall'utente). Docente G. Armano - 14 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Built-In In C esiste un ristretto numero di tipi predefiniti fondamentali: - char, carattere int, intero float, floating-point in singola precisione double, floating point in doppia precisione Docente G. Armano - 15 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Qualificatori di Tipi Built-In Alcuni qualificatori (short / long, signed / unsigned) possono essere applicati ai tipi predefiniti fondamentali, generando restrizioni / estensioni di tali tipi. Tabella di applicabilità dei qualificatori short e long ai tipi predefiniti fondamentali: short long char int float double no si no no no si no si Le uniche restrizioni imposte dallo standard ANSI sono le seguenti: - short ≥ 16 bit int ≥ 16 bit long ≥ 32 bit Normalmente l'ampiezza di un int rispecchia quella degli interi nella macchina utilizzata. Spesso short indica un intero a 16 bit, long uno di 32, e int occupa 16-32 bit. Ogni compilatore è comunque libero di scegliere la dimensione degli interi in relazione all'hardware sul quale opera. Il tipo long double caratterizza una precisione tipicamente superiore a quella fornita dal tipo double. Le notazioni short int e long int possono essere rispettivamente abbreviate in short e long. L'aritmetica corrispondente è quella binaria pura per i numeri non segnati e quella complemento a due per i numeri con segno. signed unsigned Docente G. Armano char int short int long int si (default) (default) (default) (default) si si si - 16 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Costanti Built-In: Caratteri Le costanti built-in di tipo char costituiscono il dominio del tipo di dato char. La notazione tipica utilizzata è quella di porre il carattere tra apici. In alternativa si possono specificare (sempre tra apici) il valore ottale / esadecimale (entrambi preceduti dal carattere “\”) della codifica ASCII corrispondente. Esempi: 'A', 'a', '\065', '\0x41' Alcuni caratteri speciali sono codificati a parte (sequenze di escape): \0 \a \b \f \n \r \t \v \\ \? \' \" Docente G. Armano NULL (carattere nullo) bell (allarme) BS backspace FF Form Feed (salto pagina) LF Line Feed (salto riga) CR Carriage Return (ritorno di carrello) HT Horizontal TAB (tabulazione orizzontale) VT Vertical TAB (tabulazione verticale) \ ? ' " - 17 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Costanti Built-In: Numeri Interi Le costanti built-in numeriche intere costituiscono il dominio dei tipi di dato int / short / long (eventualmente unsigned). La notazione tipica utilizzata è la sequenza di cifre decimali, opzionalmente preceduta dagli operatori unari “+” e “-”. Alcuni prefissi e suffissi predefiniti consentono di specificare rispettivamente il sistema di numerazione utilizzato e il tipo di riferimento. In assenza di suffissi espliciti, il tipo di riferimento dipende dal numero rappresentato (int / long, unsigned int / long). Esempi: 0, 1, -1, +32512, -37 - prefisso 0x / 0X, utilizzato per specificare costanti esadecimali esempi: 0xA925, 0x1B, 0x2154 - prefisso 0, utilizzato per specificare costanti ottali esempi: 072, 0515 - suffisso l / L, utilizzato per specificare costanti di tipo long esempi: 0L, -1l, 072L, 0x453AL - suffisso u / U, utilizzato per specificare costanti unsigned esempi: 3589u, 190U, 034U, 0x49B6U - suffissi ul / UL, utilizzati congiuntamente per specificare costanti unsigned long. esempi: 1882591UL, 32ul, 06UL, 0xFFFF0000ul Docente G. Armano - 18 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Costanti Built-In: Numeri Reali Le costanti built-in numeriche reali costituiscono il dominio dei tipi di dato float / double / long double. La notazione tipica utilizzata è la sequenza di cifre decimali, separata dal punto decimale, ed opzionalmente preceduta dagli operatori unari “+” e “-”. Anche la notazione esponenziale è ammessa. L'esponente presuppone una base 10. Alcuni prefissi e suffissi predefiniti consentono di specificare rispettivamente il tipo di riferimento. Il tipo di default è sempre double (infatti nel calcolo delle espressioni floating, tutto viene trasformato in double). Esempi: 0., .1, -1.357, +325e-4, -37.1e2 - suffisso f / F, utilizzato per specificare costanti float esempi: -72.85F, 32e-3f - suffisso l / L, utilizzato per specificare costanti di tipo long double esempi: 0.1L, -1547.38e-2l Docente G. Armano - 19 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Costanti di Tipo Espressione e di Tipo Stringa Oltre alle costanti built-in, altri tipi di costanti meritano immediata attenzione: - costanti di tipo espressione sono espressioni calcolabili in tempo di compilazione, e possono essere inserite in ogni punto in cui può trovarsi una costante Esempi: 10*(356-31), -82e-3+71 - costanti di tipo stringa sono costanti costituite da sequenze di caratteri stampabili, con eventuali sequenze di escape (\n, \t, ecc.), il tutto racchiuso tra doppi apici. Esempi: “Hello world !\n”, “Pippo”, “Pluto”, “Pippo, Pluto, Paperino” Le costanti di tipo stringa sono memorizzate come vettori di caratteri e terminate con il carattere '\0'. Docente G. Armano - 20 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Costanti Simboliche (User-Defined) Il K&R-C mette a disposizione la direttiva #define. Esempi: #define pigreca 3.1415 #define NMAX 100 In pratica #define stabilisce una regola di riscrittura per cui il compilatore sostituisce ogni occorrenza di un certo simbolo con la corrispondente definizione (nell'esempio sopra, rispettivamente, ogni occorrenza di pigreca e NMAX con 3.1415 e 100). L'ANSI-C mette anche a disposizione costruttori di costanti che utilizzano le definizioni di tipo e la parola chiave const. Ad esempio, per i tipi semplici: const const const const double pigreca = 3.1515 ; double e = 2.71828182845905; int NMAX = 100 ; short NumChars = 255 ; Vedremo poi qualche esempio di uso dell'uso di const applicata anche a tipi derivati. Docente G. Armano - 21 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili In generale, ad una variabile vengono associati: - un nome - un tipo - una visibilità (scope) - un tempo di vita (extent) - un indirizzo di memoria - un valore Docente G. Armano - 22 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Nome e Tipo Il nome e il tipo di una variabile vengono specificati durante la sua dichiarazione. Una dichiarazione di variabile viene fatta specificando innanzitutto il tipo di riferimento, seguito da un nome (con associata un'eventuale espressione di inizializzazione). Esempio: int K ; char ch='A' ; Più variabili dello stesso tipo possono essere dichiarate insieme. In tal caso, alla specificazione del tipo di riferimento, segue una lista di nomi (con associate eventuali espressioni di inizializzazione) separati da virgole. Esempio: int X, Y = -1; /* Inizializzo soltanto Y */ float F1, F2 = 1.0e-2, F3 ; /* Inizializzo soltanto F2 */ Docente G. Armano - 23 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Visibilità La visibilità di una variabile è lo spazio di programma nel quale la variabile è accessibile. In C la visibilità di una variabile può essere relativa al blocco di istruzioni / alla funzione / al file / al programma nel quale la variabile stessa è stata dichiarata. Esempio: /* File pippo.c */ int X, Y=-1; /* X ed Y sono variabili globali */ static float F1 = 1.0 ; /* F1 nota solo in pippo.c */ void simpleF ( int NMAX ) { int I=0 ; /* I visibile in simpleF */ while ( I < NMAX ) { int J = I+1 ; /* J visibile nel ciclo while */ ... ; /* Istruzioni del ciclo while */ } } /* File pluto.c */ extern int X, Y; /* X ed Y sono definite in pippo.c */ static float F1 = 1.0 ; /* un'altra F1 in pluto.c ! */ void complexF ( int K, int NMAX ) { int I=0 ; /* I visibile in complexF */ int X=-1 ; /* Questa def.ne di X oscura l'altra */ if ( NMAX > 0 ) { int J = 0 ; /* J visibile solo in questo blocco */ ... ; /* Istruzioni del blocco */ } } Docente G. Armano - 24 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Visibilità [II] Dall'esempio precedente: X e Y sono variabili globali al programma (statiche e potenzialmente visibili ovunque). Nel file pippo.c sono visibili direttamente, mentre in pluto.c vanno dichiarate extern per renderle visibili. F1 è una variabile statica con visibilità all'interno del file nel quale è definita. Identiche dichiarazioni (con il qualificatore static) in file diversi identificano variabili diverse ! Le variabili I definite in simpleF e complexF sono automatiche. La loro visibilità è all'interno della funzione nella quale sono definite. Le variabili J definite in simpleF e complexF sono automatiche. La loro visibilità è all'interno del blocco nel quale sono definite. La dichiarazione della variabile X all'interno della funzione complexF oscura quella della variabile globale (extern) con lo stesso nome. Docente G. Armano - 25 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Tempo di Vita Il tempo di vita di una variabile è l'intervallo di tempo in cui la variabile esiste all'interno del programma. Può essere coincidente con l'intero periodo in cui il programma è in esecuzione (variabili statiche), oppure limitato al tempo in cui viene eseguito codice appartenente al blocco in cui è stata definita la variabile (variabili automatiche). Ogni volta che si entra in un blocco / funzione vengono dunque create nuove istanze delle variabili automatiche, mentre all’uscita del blocco / funzione tali istanze vengono distrutte. Esempio: void azzeraVettore ( int vett[], int NMAX ) { int i=0 ; while ( i < NMAX ) { ... ; } } Ogni volta che la funzione azzeraVettore viene chiamata viene creata una variabile automatica i (in questo caso, con valore iniziale zero). NB in realtà, ogni volta che si entra in azzeraVettore, oltre ad i, vengono create 2 variabili locali “equivalenti” addizionali: vett e NMAX. Come vedremo meglio in seguito, vett viene inizializzata con l'indirizzo del parametro attuale, mentre NMAX viene inizializzata con il valore del corrispondente parametro attuale (in generale un'espressione). Docente G. Armano - 26 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Tempo di Vita [II] Per default, le variabili definite all'interno di un blocco / una funzione sono automatiche, mentre quelle definite al di fuori sono statiche. Per modificare la scelta di default relativa ad una variabile locale ad un blocco / funzione, si può usare la parola chiave static che, senza cambiarne le regole di visibilità, le associa un extent statico. Esempio: /* Sequenza di Fibonacci */ int Fibonacci( void ) { static int F0=0, F1=0; int Fnext ; if ( F0 == 0 ) { F0 = 1; return F0 ; } if ( F1 == 0 ) { F1 = 1; return F1 ; } Fnext = F1 + F0 ; F0 = F1 ; F1 = Fnext ; return Fnext ; } La funzione Fibonacci utilizza due variabili statiche (F0 ed F1) per memorizzare gli ultimi due numeri della sequenza Fn-1 ed Fn-2. Le variabili F0 ed F1 hanno un tempo di vita che coincide con quello del programma e vengono esplicitamente inizializzate a zero (in realta − come vedremo− l'inizializzazione a zero è la scelta di default, quindi potrebbe essere omessa). Concettualmente, l'inizializzazione viene effettuata alla prima chiamata, concretamente − come tutte le variabili statiche− F0 ed F1 vengono inizializzate nel momento in cui il programma viene mandato in esecuzione. Il loro valore iniziale è stato posto a zero per poter effettuare il test relativo ai primi due valori. Alla prima chiamata il test sul valore di F0 ha esito positivo e la funzione ritorna 1. Alla seconda chiamata il test sul valore di F1 ha esito positivo e la funzione ritorna ancora 1. Alle successive chiamate la funzione ritorna Fn-1+Fn-2. Docente G. Armano - 27 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Indirizzo di Memoria L'indirizzo di memoria di una variabile non viene gestito dal programmatore. Le variabili statiche (globali o no) vengono create una sola volta e quindi il loro indirizzo è fisso. Le variabili automatiche vengono create ogni volta che si entra nel blocco in cui sono definite, e quindi ad ogni creazione riceveranno -in generale- un indirizzo di memoria diverso. Tipicamente, le variabili statiche vengono allocate in un'area di memoria riservata, mentre le variabili automatiche vengono allocate nell'area di stack del programma, oppure in un registro del processore. La decisione di allocare nello stack o in un registro viene presa dal compilatore (tipicamente in fase di ottimizzazione), anche se si può suggerire al compilatore stesso l'allocazione in un registro tramite il qualificatore register. Esempio: int azzeraVettore( int vett[], int NMAX ) { register int i=0 ; while ( i < NMAX ) { ... ; }} NB Nonostante il suggerimento, il compilatore rimane comunque libero di decidere la reale allocazione della variabile. Docente G. Armano - 28 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Valore (Inizializzazione) Il valore iniziale può essere specificato esplicitamente nella dichiarazione, oppure seguire delle regole di default. Le regole di default sono le seguenti: - le variabili globali o statiche vengono inizializzate a zero - le variabili automatiche vengono lasciate indefinite. Docente G. Armano - 29 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Variabili: Valore (Aggiornamento) Il valore di una variabile può essere aggiornato tramite un'istruzione di assegnazione o un'istruzione specializzata di incremento / decremento. La più semplice forma di assegnazione è quella che assegna un valore ad una variabile. Sintassi di una semplice assegnazione: <varIdentifier> = <expr> Semantica corrispondente: store E V(<expr>) into E A(<varIdentifier>) Dove E A(.) ed E V(.) sono due funzionali che restituiscono -rispettivamentel'indirizzo e il valore del loro argomento. I due funzionali sono stati da noi definiti per poter trattare in modo esplicito la semantica delle assegnazioni, e -più in generale- quella delle istruzioni del linguaggio. Vedremo in seguito che in realtà esiste una stretta correlazione tra tali funzionali e -rispettivamente- gli operatori & (indirizzo) e * (indirezione). Esempio: int azzeraVettore( int vett[], int NMAX ) { register int i=0 ; while ( i < NMAX ) { vett[i] = 0 ; i++ ; } } vett[i] = 0 assegna zero all'elemento del vettore vett di indice i. i++ incrementa il valore della variabile i (più semplicemente, diremo che incrementa i). Per il momento, possiamo considerarla equivalente all'istruzione di assegnazione i=i+1. Vedremo meglio in seguito la semantica delle istruzioni di incremento / decremento. Docente G. Armano - 30 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Dichiarazione di Variabili Definite su Tipi Built-In Esempi: int X, Y = -1; /* Inizial. soltanto Y */ short S1 = 25, S2 = -1 ; unsigned long distanza ; float F1 = -12.58e-3 ; float F2 = F1 * 2 ; long double raggio = 1.0 ; Docente G. Armano - 31 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati (User-Defined) In C i tipi derivati sono sostanzialmente: ð enumerazioni ð ð ð array (mono / multi-dimensionali) record puntatori Docente G. Armano [semplice] - 32 - [strutturato] [strutturato] [strutturato] Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Enumerazioni Il C mette a disposizione il costruttore di tipi enumerativi enum per definire costanti di tipo simbolico. Esempi: enum enum enum enum bool { Giorno Mese { Escape false, true } ; { dom, lun, mar, mer, gio, ven, sab } ; Gen=1, Feb, Mar, Apr, ..., Dic } ; { BELL = '\a', BACKSPACE = '\b', TAB = '\t', NEWLINE = '\n' } ; In pratica, enum stabilisce una regola di riscrittura e mappa i suoi valori sugli interi. Nel tipo enumerativo bool, false e true valgono rispettivamente 0 e 1, poiché l'assegnazione di default parte da 0 e procede per incrementi unitari. Nel tipo enumerativo Giorno, dom, lun, mar, ..., sab valgono rispettivamente 0, 1, 2, ..., 6. Nel tipo enumerativo Mese, Gen, Feb, Mar, ..., Dic valgono rispettivamente 1, 2, 3, ..., 12. Nel tipo enumerativo (sequenza di) Escape, a BELL, BACKSPACE, TAB e NEWLINE sono esplicitamente assegnati i valori dei corrispondenti caratteri speciali. Docente G. Armano - 33 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Array Monodimensionali Il C mette a disposizione il costruttore di strutture omogenee (vettori) monodimensionali. La definizione di strutture omogenee multidimensionali (matrici) avviene specificando la dimensione tra parentesi quadre. Esempi: #define FMAX 1000 typedef int vettInt [100] ; vettInt V1 ; float F[FMAX] ; vettInt è un tipo di dato che caratterizza un array di 100 int. V1 è una variabile di tipo vettInt array di int (di dimensione 100). F è una variabile di tipo array di float (di dimensione 1000). Gli indici di un array di dimensione N sono 0,1, ..., N-1. Per accedere all'elemento di indice i di un array, basta specificare l'indice tra parentesi quadre. Esempio: void copiaVettore ( int DST[], int SRC[], int NMAX ) { int i=0 ; while ( i < NMAX ) { DST[i] = SRC[i] ; i = i + 1 ; } } Docente G. Armano - 34 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Array Multidimensionali Il C mette a disposizione il costruttore di strutture omogenee (vettori) multidimensionali. La definizione di strutture omogenee multidimensionali (matrici) avviene specificando, nell'ordine, le varie dimensioni tra parentesi quadre. Esempi: #define RMAX 30 #define CMAX 10 typedef int matrInt [RMAX][CMAX] ; int V[30][10] ; matrInt V1 ; Per accedere all'elemento di indice i di un array, basta specificare l'indice tra parentesi quadre. Esempio: void azzeraMatrice ( int matr[][100], int NMAX ) { int i ; /* righe */ int j ; /* colonne */ i=0 ; while ( i < NMAX ) { j=0 ; while ( j < 100 ) { matr[i][j] = 0 ; j = j+1 ; } i = i+1 ; } } NB Come si può notare, nel passare una matrice come parametro, la prima dimensione può essere omessa. Infatti le matrici vengono memorizzate per righe, e quindi è necessario conoscere soltanto la dimensione di una riga (ovvero il numero di colonne). Docente G. Armano - 35 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Array Multidimensionali [II] Una dichiarazione alternativa del prototipo della funzione azzeraMatrice potrebbe essere: void azzeraMatrice ( int (*matr)[100], int NMAX ) che specifica che il parametro matr è un puntatore ad un vettore di interi di 100 posizioni. Si noti invece che la dichiarazione: void azzeraMatrice ( int *matr[100], int NMAX ) specifica che mat è un vettore di puntatori ad un intero ! Questo perche' la priorità dell'operatore [] è superiore a quella dell'operatore di * indirezione. Docente G. Armano - 36 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Record Il C mette a disposizione il costruttore di strutture composite (record). Esempi: /* K&R-C */ enum Mese { ... } ; struct Data { unsigned short giorno; enum Mese mese; unsigned short anno }; struct Persona { char cognome[25] ; char nome[20] ; struct Data dataNascita ; char codiceFiscale[16+1] ; } ; typedef struct persona PERSONA ; struct Persona P1 ; PERSONA P2, dataBase[100] ; Docente G. Armano - 37 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Record [II] Mese è un type-TAG che caratterizza un tipo enumerativo con i seguenti valori: Gen = 1, Feb = 2, ..., Dic = 12. Data è un type-TAG che caratterizza un record con i campi giorno (1-31), mese (1-12), anno. Persona è un type-TAG che caratterizza un record con i campi cognome, nome, dataNascita e codiceFiscale. PERSONA è un identificatore di tipo che corrisponde alla struttura di tipo (struct) Persona. P1 è una variabile di tipo (struct) Persona. P2 è una variabile di tipo PERSONA. dataBase è una variabile di tipo array di PERSONA (di dimensione 100). Docente G. Armano - 38 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Record [II] Per accedere ad un elemento di un record (struct), occorre specificare la variabile coinvolta ed il nome del campo. Esempi: /* ANSI-C */ #include <string.h> enum Mese { ... } ; struct Data { ... } ; struct Persona { ... } ; ... Persona P1, dataBase[100] ; ... strcpy(P1.cognome, "Dessi") ; strcpy(P1.nome, "Sandro") ; P1.dataNascita.giorno = 15; P1.dataNascita.mese = Nov ; P1.dataNascita.anno = 1968 strcpy(P1.codiceFiscale, "DSS SND 68S15 C006V") ; Dove strcpy è una funzione di libreria del C che copia una stringa sorgente in una destinazione, la cui interfaccia è definita (nel file string.h) come segue: char * strcpy ( char DST[], const char SRC[] ) ; La funzione restituisce il puntatore al primo carattere della stringa destinazione, e copia il contenuto del vettore SRC nel vettore DST (sino al carattere di fine stringa, ovvero '\0'). Docente G. Armano - 39 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Puntatori Un puntatore è un indirizzo di memoria. Tipicamente sono sufficienti 32 bit per caratterizzare un puntatore, anche se numero di bit riservati ad un indirizzo di memoria dipende ovviamente dall'hardware della macchina (e a volte dal compilatore utilizzato). Una variabile di tipo puntatore è una variabile il cui valore è un indirizzo di memoria, ma è caratterizzata anche dal tipo di dato puntato. Esempio: ... int X = 35, *Xptr = &X ; ... printf("Scrivo X: %d\n",X) ; printf("Scrivo ancora X: %d\n",*Xptr) ; ... Definisco una variabile intera X con valore iniziale 35. Definisco una variabile puntatore (ad int) di nome Xptr il cui valore iniziale è l'indirizzo della variabile X. Le istruzioni printf scrivono entrambe sullo standard-output il valore della variabile X. Scrivo X: 35 Scrivo ancora X: 35 Docente G. Armano - 40 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Puntatori [II] Per capire bene il meccanismo della dichiarazione ed inizializazione della variabile Xptr occorre conoscere la semantica degli operatori unari “&” ed “*”. L'operatore & (indirizzo) fornisce l'indirizzo del suo argomento. Cosi' &X fornisce l'indirizzo della variabile X. L'operatore * (indirezione) forza un livello di valutazione in più rispetto al suo argomento. Cosi' *Xptr non fornisce il valore di Xptr (che è un puntatore), bensì il valore intero allocato all'indirizzo di memoria il cui valore è contenuto in Xptr. Esempio: Xptr 1931E4 0A2248 X 35 0A2248 con riferimento alla figura, possiamo scrivere: E A(Xptr) = 0x1931E4 E V(Xptr) = 0x0A2248 /* indirizzo della variabile Xptr */ /* valore della variabile Xptr */ E A(X) = 0x0A2248 = E V(Xptr) E V(X) = 35 = E V(*Xptr) /* indirizzo della variabile X */ /* valore della variabile X */ NB Esiste uno stretto legame tra i funzionali E A(.) ed E V(.) da una parte, e gli operatori & ed * dall'altra. Con riferimento alle variabili X ed Xptr sopra definite, tale legame può essere esplicitato dalle seguenti uguaglianze: E A(*Xptr) = E V(Xptr) E V(&X) = E A(X) Docente G. Armano - 41 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Inizializzazione L'ANSI-C consente di inizializzare − in fase di definizione− non solo costanti e variabili appartenenti a tipi semplici, ma anche a tipi derivati. In particolare, per quanto riguarda i tipi strutturati, l'inizializzazione procede secondo la struttura dati di riferimento. Ad ogni livello di annidamento della struttura dati corrisponde un livello in più di parentesi graffe. L'inizializzazione può avvenire per definire il valore iniziale di una variabile o quello di una costante di programma (in tal caso dovremo premettere la parola chiave const). Docente G. Armano - 42 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Inizializzazione di Vettori Per inizializzare una variabile di tipo vettore, in fase di definizione, scriveremo, ad esempio: /* ANSI-C */ int V1[5] = { -2, -1, 0, 1, 2 } ; Inizializza V1[0], V1[1], ..., V1[4] rispettivamente a -2, -1, 0, 1, 2. Non è necessario specificare un valore iniziale per tutti gli elementi del vettore. Gli elementi di cui non viene specificato il valore iniziale seguono le regole di inizializzazione di default per le variabili. Per inizializzare una costante di tipo vettore scriveremo, ad esempio: /* ANSI-C */ const int nullVector[5] = { 0, 0, 0, 0, 0 } ; Inizializza tutti gli elementi del vettore a zero. Se la dimensione del vettore viene omessa, il compilatore ne calcola le dimensioni sulla base del numero di valori iniziali. /* ANSI-C */ int V1[] = { -2, -1, 0, 1, 2 } ; Dichiara un vettore di 5 elementi con i valori iniziali specificati tra parentesi graffe. Docente G. Armano - 43 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture Dati: Tipi Derivati: Inizializzazione di Record Per inizializzare una variabile di tipo record, in fase di definizione, scriveremo, ad esempio: /* ANSI-C */ enum Mese { ... } ; struct Data { ... } ; struct Persona { ... } ; ... Persona P1 = { "Dessi","Sandro", { 15, Nov, 1968 }, "DSS SND 68R15 C006V") ; Per inizializzare una costante di tipo record scriveremo, ad esempio: /* ANSI-C */ enum Mese { ... } ; struct Data { ... } ; struct Persona { ... } ; ... const Persona emptyPerson = { "", "", { 0,0,0 }, "") ; Se alcuni campi della struttura vengono omessi, il compilatore l'inizializzazione procede seguendo le regole di inzializzazione di default per le variabili. Ad esempio: /* ANSI-C */ struct Persona P1 = { "Dessi", "Sandro" } Alla variabile P1 di tipo (struct) Persona vengono assegnati esplicitamente soltanto i campi cognome e nome. Se la variabile è statica (globale o no), gli altri campi vengono inizializzati a zero, altrimenti il loro contenuto sarà − in generale− imprecisato. Docente G. Armano - 44 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori Gli operatori in C si dividono nei seguenti gruppi principali: ð ð ð ð ð operatori aritmetici operatori relazionali e logici operatori di incremento e decremento operatori che operano sui bit operatori di assegnamento ed espressioni Docente G. Armano - 45 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori aritmetici ð operatori aritmetici unari: “+”, “-” “+” introdotto dall’ANSI-C per motivi di “simmetria” rispetto alla notazione con il “-” unario “-” già presente anche nel K&R-C per specificare l’inversione di segno applicata a costanti / variabili / espressioni aritmetiche. ð operatori aritmetici binari: “+”, “-”, “*”, “/”, “%” sono tutti operatori infissi. “*” e “/” sono prioritari rispetto al “+” e “-” e rappresentano gli usuali operatori aritmetici binari. “%” è l’operatore di modulo che restituisce il resto della divisione tra il primo e il secondo operando. Esempio: X % 4 => 1 se X = 5 oppure 9 oppure 13 oppure ... Tabella di applicabilità degli operatori binari “+”, “-” “*”, “/” “%” int, short, long si si si float, double si si no Docente G. Armano - 46 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori relazionali e logici Sono operatori che si integrano perfettamente all’interno del meccanismo di valutazione di espressioni aritmetiche. Infatti, come vedremo, i test realizzati in linguaggio C sono considerati falliti (esito negativo) se e solo se il risultato della valutazione dell’espressione corrispondente e’ zero. Un risultato non nullo determina l’esito positivo del test corrispondente. I valori “logici” false e true ottenuti come risultato della valutazione di espressioni che coinvolgono operatori relazionali e/o logici vengono mappati, rispettivamente, su 0 e 1. ð operatori relazionali sono: “>“, “>=“,“<“,“<=“,“==“,“!=“ Si noti che l’operatore di (verifica di) uguaglianza e’ “==“, mentre quello di (verifica di) disuguaglianza e’ “!=“ “>“, “>=“, “<“, “<=“ hanno priorità superiore rispetto a “==“ e “!=“ Tutti gli operatori relazionali hanno priorità inferiore a quelli aritmetici. Esempio: l’espressione i < lim-1 equivale a i < (lim-1) ð operatori logici unari: “!” (negazione) ð operatori logici binari: “&&” (and) e “||” (or) le espressioni connesse da “&&” e/o “||” vengono valutate da sinistra a destra e la valutazione si blocca non appena si determina la verità o falsità dell’intera espressione Docente G. Armano - 47 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di incremento e decremento Il C offre operatori specializzati che realizzano l’operazione di incremento o decremento. Gli operatori sono “++” (incremento) e “--” (decremento) e possono essere applicati prima di valutare la variabile corrispondente (pre-incremento / decremento) o dopo (post-incremento / decremento). L’operazione di incremento o decremento va vista come un effetto collaterale che si scatena durante la valutazione di espressioni in cui sono coinvolte le variabili / locazioni di memoria corrispondenti. Esempio (uso dell’operatore di post-incremento): int N = 10 ; int X[10] ; ... while ( N-- > 0 ) X[N] = 0 ; ... Come risultato dell’esecuzione del ciclo while, tutte le componenti del vettore X sono state poste a zero. Perché ? R.: l’istruzione N-- specifica un’operazione di post-decremento, ovvero: prima si valuta N e poi la si decrementa. Si noti che la prima assegnazione viene fatta sull’elemento di indice 9 del vettore e l’ultima sull’elemento di indice 0. Docente G. Armano - 48 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di incremento e decremento [II] Un altro esempio (copia di un vettore): versione #1: void copiaVettore( int DST[], int SRC[], int NMAX ) { int i=-1 ; while ( ++i < NMAX ) DST[i] = SRC[i] ; } versione #2: void copiaVettore( int DST[], int SRC[], int NMAX ) { while ( NMAX-- ) *DST++ = *SRC++ ; } Si noti l’assegnazione: *DST++ = *SRC++ che sarà presto usata in molti esercizi. La semantica associata all’istruzione e’: “prima estrai il valore contenuto nella locazione puntata da SRC e poi incrementa SRC. Deposita tale valore nella locazione puntata da DST e poi incrementa DST”. Docente G. Armano - 49 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di incremento e decremento [III] Alcune osservazioni: - un'istruzione di assegnazione ritorna sempre come valore quello della sua parte sinistra. In questo caso, viene restituito l’indirizzo della locazione di memoria puntata da DST. Le operazioni di assegnazione e di incremento vanno considerate come effetti collaterali (side-effect); - DST e SRC sono trattati come se fossero dei puntatori. Questo e’ sempre possibile in C (almeno quando stiamo considerando parametri formali di una funzione); - sono i puntatori e non le locazioni puntate ad essere (post-)incrementati poiché l’operatore ++ (incremento) si applica soltanto a quello che compare immediatamente alla sua sinistra. Per incrementare le locazioni puntate avremmo dovuto scrivere (*DST)++ oppure (*SRC)++. Semantica dell'assegnazione: Dopo ogni operazione in cui si verifica un side-effect inseriremo in alto il valore ritornato e in basso il side-effect. Quando necessario si possono usare le indicazioni {before} e {after} per sottolineare che il side-effect interviene − rispettivamente− prima o dopo la corrispondente valutazione. Assumiamo che se non viene indicato nulla il side-effect intervenga dopo. E A (* DST + + ) store E V (* SRC + + ) into E A (* DST + + ) EV (* DST + + =* SRC + + )= EV (* SRC + + )= EV (* SRC ) = EV (EV (SRC )) inc SRC E A (* DST + + )= E A (* DST ) = EV (DST ) inc Docente G. Armano DST - 50 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori che operano sui bit Sono 6 operatori che si possono applicare soltanto ad operandi interi (ovvero char, short, int, e long segnati o no). ð operatori logici bit-a-bit unari: “~” (inversione, ovvero complem. a 1) ð operatori logici bit-a-bit binari: “&” (and), “|” (or), “^” (xor) ð operatori di shift bit-a-bit binari “<<” (shift sx), “>>” (shift dx) Esempi: int X1, X2 = -1 ; /* ogni bit di X2 è posto ad 1 */ char ch1 = '9' ; /* ch1 vale 0x39 = codif. ASCII di 9 */ X1 X1 X1 X1 X1 X1 = = = = = = ~X2 ; /* risultato 0 */ X1 | X2 ; /* risultato -1 */ ch1 & 0x0F ; /* risultato 9 */ ch1 ^ 0x1C ; /* risultato 0x25 */ X1 << 2 ; /* risultato 0x94 */ X1 >> 4 ; /* risultato 9 */ Docente G. Armano - 51 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di assegnazione ed espressioni L'operatore di assegnazione è stato illustrato in una forma semplificata trattando l'aggiornamento delle variabili. Più in generale la sintassi di un'assegnazione è la seguente: <multipleAssign> ← <mem> = <multipleExpr> <multipleExpr> ← <expr> | <multipleAssign> Si noti che la definizione è ricorsiva. Semantica corrispondente: store E V(<multipleExpr>) into E A(<mem>) Rispetto alla versione semplificata dell'assegnazione, sono stati introdotti due elementi di novità: - la "parte sinistra" dell'assegnazione può contenere riferimenti a variabili / indirizzi di memoria multipli - nella "parte sinistra" non è più necessario che ci siano riferimenti a variabili singole. È sufficiente che ci siano riferimenti ad indirizzi di memoria. Docente G. Armano - 52 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di assegnazione ed espressioni [II] Un esempio: ... int V[10], X, Y, Z=5 ; ... V[0] = X = Y = 3*Z - 1 ; /* Inizializzazione multipla */ ... /* A questo punto, V[0], X e Y valgono tutti 14 = 3*5-1 */ Semantica: EV (V [ 0 ] = EV ( X = E A ( X = Y = 3* Z − 1) store EV ( X = Y = 3 * Z − 1) intoE A (V [ 0 ] ) X = Y = 3* Z − 1)= E A (Y = 3* Z − 1) store EV (Y = 3 * Z − 1) intoE A ( X ) Y = 3* Z − 1)= EV (3* Z − 1) = 3* EV (Z ) − 1 = 14 store EV (3 * Z − 1) intoE A (Y ) EV (Y = 3* Z − 1)= Si noti che la semantica del C restituisce l’indirizzo della parte sinistra di un’espressione di assegnazione. È comunque opportuno non sfruttare –salvo rarissimi casi– questa caratteristica del linguaggio, per evitare di incorrere in forme difficili da interpretare. Ad esempio, il C consentirebbe di scrivere: (x=y=1)++ ; che come risultato finale assegna 1 alla variabile y e 2 alla variabile x ! L’espressione risulta però di difficile lettura e inutilmente complicata. Se ne sconsiglia quindi l’uso. Come regola pratica, tutto va come se la valutazione della parte più a destra di un’assegnazione multipla venisse effettivamente propagata a sinistra, effettuando separatamente lo store su tutti gli indirizzi di memoria specificati dall’assegnazione multipla. Docente G. Armano - 53 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di assegnazione ed espressioni [III] Un altro esempio: int X, Y, *Yptr = &Y ; int Z = 0; X = *Yptr = ++Z - 2 ; Semantica: ad X e Y viene assegnato il valore E V(++Z - 2) = -1. C'è un side-effect su Z che viene pre-incrementata. L'inizializzazione di Y avviene tramite l'uso della variabile puntatore Yptr, inizializzata all'indirizzo di Y. E V ( X = *Yptr = + +Z − E V (*Yptr = + + 2) = store E V (*Yptr = + + E V (+ + Z − 2 ) = E V (Z ) − E V (* Yptr = + + Z − 2 )= inc Z Docente G. Armano Z − 2) Z − 2 ) into E A ( X ) 2 = 1− 2 = −1 {before}, store E V (+ + Z − 2 ) into E A (* Yptr ) - 54 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di assegnazione ed espressioni [IV] Ci sono poi altri 10 operatori che costituiscono delle forme contratte per le assegnazioni che utilizzano operatori aritmetici o bit-a-bit binari. ð ð operatori aritmetici di assegnazione: “+=“, “-=“, “*=“, “/=“, “%=“ operatori bit-a-bit di assegnazione: “&=“, “|=“, “^=“, “<<=“, “>>=“ La loro semantica è quella della forma corripondente espansa. Ad esempio: X1 += 2 corrisponde a X1 = X1 + 2 Docente G. Armano - 55 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori di assegnazione ed espressioni [V] Importanti note sugli effetti collaterali: il linguaggio C non specifica l'ordine di valutazione degli argomenti di una funzione e neppure quello relativo all'esecuzione degli effetti collaterali. Diversi compilatori possono quindi produrre risultati diversi eseguendo − ad esempio− le seguenti istruzioni: int n=3; int i=0; int A[10]; ... printf("%d %d\n",++n,power(2,n)) ; /* power(2,3) o power(2,4) ? */ ... A[i] = i++ ; /* A[0] o A[1] ? */ ... Docente G. Armano - 56 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Espressioni Condizionali Sintassi: <expr> ? <expr1> : <expr0> Semantica: E V(<expr> ? <expr1> : <expr0>) = E V(<expr1>) se E V(<expr>) ≠ 0 = E V(<expr0>) altrimenti Esempi: int X, Y, Z ; char ch1 ; ... Z = ( X < Y ) ? X : Y ; /* assegna a Z min(X,Y) */ ... ch1 = ( ch1 >= 'a' && ch1 <= 'z' ) ? ch1-'a'+'A' : ch1 ; /* trasforma ch1 in char maiuscolo */ Docente G. Armano - 57 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Operatori: Tabella delle Priorità Operatore () [] -> . ! ~ ++ -- (tipo) * & sizeof */% +<< >> <<= >>= == != & bit-a-bit ^ | && || ?: = += -= ecc. , Docente G. Armano - 58 - Associatività da SX a DX da DX a SX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da SX a DX da DX a SX da DX a SX da SX a DX Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo Le strutture di controllo sono: - sequenza if_then_else switch while_do do_while for goto Docente G. Armano - 59 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: Sequenza In C le istruzioni vengono eseguite in sequenza. Il carattere ";" è il separatore di istruzioni semplici. Esempio di istruzioni semplici in sequenza: int X, Y, Z ; ... Z = X + Y ; X = 2 * Y ; Istruzioni complesse (blocchi) sono delimitate dai caratteri {} e vanno considerate a tutti gli effetti come istruzioni "singole". All'interno di un blocco possono essere dichiarate variabili la cui visibilità rimane confinata all'interno del blocco stesso (inoltre, variabili esterne al blocco con lo stesso nome vengono oscurate). Esempio di istruzione complessa: int X, Y ; ... { int tmp = X; X = Y; Y = tmp; } ... /* scambio X con Y */ NB Nel seguito, con istruzione denoteremo istruzioni semplici o complesse. Si ricordi che soltanto le istruzioni semplici devono essere sempre terminate dal carattere ";". Anche l'istruzione vuota (ovvero un semplice ";") è ammessa. Docente G. Armano - 60 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: if_then_else Sintassi: if ( <expr> ) <instr1> oppure if ( <expr> ) <instr1> else <instr0> Dove: <instr1> e <instr0> sono istruzioni. Semantica: se E V(<expr>) ≠ 0 allora E V(<instr1>) altrimenti E V(<instr0>) Esempio: int X, Y ; ... if ( X < Y ) X = Y ; else { int tmp=X; X=Y; Y=tmp; } ... Docente G. Armano - 61 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: switch Sintassi (semplificata): switch ( <expr> ) { case <expr-const1> : <instrList1> case <expr-const2> : <instrList2> ... case <expr-constN> : <instrListN> default : <instrListN+1> } Dove: <expr> è un'espressione intera (char, short, int). <expr-const> è un'espressione di costanti. <instrList> è una lista di istruzioni. Semantica: Si valuta E V(<expr>) e si cerca sequenzialmente la prima espressione costante <expr-const> (in corripondenza delle label case) tale che E V(<expr-const>) = E V(<expr>). Se tale espressione esiste, allora si eseguono le istruzioni associate, e tutte quelle successive sino (i) alla fine del blocco dello switch o (ii) al primo break (o return) incontrato. Altrimenti, se esiste l'opzione di default, si eseguono le istruzioni corrispondenti. Altrimenti si esce dal blocco senza eseguire nulla. Docente G. Armano - 62 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: switch [II] Esempio: int X, Y ; ... switch ( X ) { case 10 case 11 case 12 default ... : X++ ; Y = 2 * X ; break ; : : Y = X ; : Y++ ; } NB quando X = 10 eseguo le istruzioni corrispondenti e poi esco dal blocco switch (per la presenza dell'istruzione break). Quando invece X = 11 oppure X = 12 eseguo l'assegnazione Y = X e poi incremento Y (ovvero, dato che il “case 12” non è chiuso da un break, eseguo anche l'istruzione associata alla label default). Altrimenti incremento Y. Docente G. Armano - 63 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: while_do Sintassi: while ( <expr> ) <instr> Dove: <expr> è un'espressione, <instr> è un'istruzione. Semantica: Se E V(<expr>) ≠ 0 allora E V(<instr>). Si ripetono tali operazioni finche' non si verifica una delle due eventualità seguenti: (i) E V(<expr>) = 0, oppure (ii) nel corpo dell'istruzione <instr> viene eseguito un break (o un return). Una delle due eventualità sopra riportate forza l'uscita dal ciclo. Esempio: /* cerca un char in una stringa. Se trovato, ne restituisce l'indice, altrimenti -1 */ int cercaChar ( char *str, int ch) { int i=0 ; while ( str[i] != '\0' ) { if ( str[i] == ch ) break ; else i++ ; } return ( str[i] != '\0' ) ? i : -1 ; } Finche' non sono arrivato al fine stringa ('\0') verifico se il carattere corrente ' uguale a quello cercato. Se uguale, allora esco dal ciclo (break). Ritorno l'indice i del char cercato oppure -1 (se non trovato). Docente G. Armano - 64 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: while_do [II] Esiste anche la possibilità di forzare la ripetizione anticipata del test E V(<expr>). In tal caso occorre usare l'istruzione continue che − eseguita all'interno del blocco di istruzioni del ciclo while− rimanda ad effettuare il test senza eseguire le eventuali istruzioni successive. Esempio: /* cerca un char in una stringa. Se trovato, ne restituisce l'indice, altrimenti -1 */ int cercaChar ( char *str, int ch) { int i=-1 ; while ( str[++i] ) { if ( str[i] != ch ) continue ; return i ; } return -1 ; /* non trovato */ } Docente G. Armano - 65 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: do_while Sintassi: do <instr> while ( <expr> ) Dove: <expr> è un'espressione, <instr> è un'istruzione. Semantica: Come lo while_do, con la differenza che qui il test viene eseguito dopo. Docente G. Armano - 66 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: for Sintassi: for ( <init> ; <expr> ; <update> ) <instr> Dove: <init> è un'espressione (o una sequenza di espress. separate da virgole) <expr> è un'espressione. <update> è un'espressione (o una sequenza di espress. separate da virgole). <instr> è un'istruzione. Semantica (fornita in termini dello while_do): <init> while ( <expr> ) { <instr> ; <update> ; } Esempio: void azzeraVettore ( int vett[], int NMAX) { int i ; for ( i=0; i < NMAX; i++ ) vett[i] = 0 ; } NB In <init> e <update> è possibile specificare anche più di una istruzione semplice. In tal caso, le virgole sono usate come separatori. Docente G. Armano - 67 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Strutture di Controllo: goto Sintassi: goto label ; Semantica: È l'istruzione di salto incondizionato. L'etichetta (label) deve ovviamente essere stata definita. Esempio: /* cerca un char in una stringa. Se trovato, ne restituisce l'indice, altrimenti -1 */ int cercaChar ( char *str, int ch) { int i=0 ; while ( str[i] != '\0' ) { if ( str[i] == ch ) goto trovato ; else i++ ; } return -1 ; /* non trovato */ trovato: return i ; /* trovato */ } NB Si consiglia di utilizzare l'istruzione di goto soltanto per implementare strutture di controllo che il linguaggio non offre esplicitamente (ad es. repeat-exit a più livelli). Docente G. Armano - 68 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni Le procedure storicamente sono nate (anni 60) per evitare la duplicazione di codice, per poi diventare (anni 80) strumenti per realizzare delle astrazioni funzionali. Una procedura è un costrutto linguistico che specifica una trasformazione tipicamente non disponibile a livello del linguaggio scelto. La trasformazione è caratterizzata da un nome e da eventuali parametri, che possono essere di ingresso, di ingresso-uscita, o di uscita. I parametri presenti nella specificazione della procedura sono detti parametri formali e consentono di esplicitare operazioni senza dover anticipare su quali dati "effettivi" tali operazioni devono essere eseguite. L'esecuzione di una procedura procede attraverso la sua invocazione (call), in cui si specificano il nome della procedura e i suoi parametri attuali, cioè quei dati sui quali verrà eseguita la trasformazione. Docente G. Armano - 69 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni [II] Esempio: /* incrementa tutti gli elementi di un vettore */ void incVettore ( int vett[], int NMAX) { while ( NMAX-- > 0 ) vett[NMAX]++ ; } void main ( void ) { int V1[100], V2[10] ; ... incVettore(V1,100) ; /* call su V1 */ incVettore(V2,10) ; /* call su V2 */ ... } - incVettore è una procedura con due parametri formali: vett (di ingresso-uscita) e NMAX di ingresso. - la procedura incVettore viene invocata due volte: la prima su V1 e la seconda su V2. Docente G. Armano - 70 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni [III] Una funzione può essere vista come una procedura che restituisce un valore (tipicamente il risultato della computazione effettuata). Formalmente, in C non ci sono procedure, poiché esistono soltanto funzioni. Rovesciando l'interpretazione usuale, una procedura può quindi essere vista come una funzione che non restituisce alcun valore. Dichiarazione di funzione: sintassi semplificata <function-dcl> ::= <type> <function-id> ( <parList> ) <function-body> <type> ::= <type-id> | <type-dcl> <parList> ::= <parList1> | ε <parList1> ::= <par> , <parList1> | <par> <par> ::= <type> <par-id> <function-body> ::= { <varList> <instrList> } Dove <varList> e <instrList> rappresentano, rispettivamente, una o più dichiarazione di variabili e una o più istruzioni. Per evidenziare il fatto che una funzione è in realtà una procedura, è consigliato utilizzare la parola chiave void. Nell'esempio precedente incVettore è una procedura poiché “restituisce” un tipo void. Per omogeneità, riteniamo opportuno inglobare void all'interno dei tipi predefiniti, con il significato di "nessun tipo". La scelta di default per il tipo restituito da una funzione C è int. Ciononostante suggeriamo di specificare sempre il tipo restituito, anche quando è int. Docente G. Armano - 71 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: prototipi Quando l'uso di una funzione precede la sua definizione (o quando la definizione è riportata in un altro file), occorre specificarne il prototipo. Sostanzialmente, il prototipo di una funzione ne indica l'interfaccia, ovvero il tipo dei parametri e il tipo del valore restituito. Esempio: /* incrementa tutti gli elementi di un vettore */ void incVettore ( int vett[], int NMAX) ; void main ( void ) { int V1[100], V2[10] ; ... incVettore(V1,100) ; /* call su V1 */ incVettore(V2,10) ; /* call su V2 */ ... } void incVettore ( int vett[], int NMAX) { while ( NMAX-- > 0 ) vett[NMAX]++ ; } Docente G. Armano - 72 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: prototipi [II] Poiché il prototipo riguarda soltanto l'interfaccia di funzione, il nome dei parametri può anche essere omesso. Ad esempio, il prototipo di incVettore può essere definito come segue: void incVettore ( int [], int ) ; Per compatibilità con il K&R-C è stata mantenuta la possibilità di dichiarare l'esistenza di una funzione senza specificare la lista dei suoi parametri. Ad esempio: void incVettore () ; specifica che incVettore è una funzione che non restituisce nulla (ovvero è una procedura) e di cui − al momento− non si specifica l'elenco dei parametri (cosa che andrà comunque fatta successivamente). Si noti che, invece, la dichiarazione: void incVettore ( void ) ; specifica che la funzione incVettore non ha parametri. Contrariamente alla precedente, questa definizione sarebbe incompatibile con la dichiarazione di funzione fornita in precedenza. Docente G. Armano - 73 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri Il passaggio di parametri avviene in C secondo una scelta di default, ovvero: - i vettori vengono passati "per indirizzo" ciò significa che alla funzione viene passato l'indirizzo del primo elemento del vettore. Tale indirizzo viene caricato − come valore iniziale− nella variabile locale equivalente che ha il nome del parametro formale; - tutti gli altri parametri vengono passati “per valore” ciò significa che alla funzione viene passato il valore dell'espressione specificata nell'invocazione della funzione. Tale valore viene caricato − come valore iniziale− nella variabile locale equivalente che ha il nome del parametro formale. Docente G. Armano - 74 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri [II] Esempio: /* copia una stringa sino ad NMAX caratteri*/ char * stringCopyLim ( char DST[], const char SRC[], int NMAX) { char * ptr = DST ; while ( NMAX-- > 0 ) { if ( *ptr++ = *SRC++ ) continue ; return DST ; } /* stringa SRC completata */ *ptr = '\0' ; return DST ; /* copiati NMAX caratteri */ void main ( void ) { char S1[128], S2[] = "pippo e pluto" ; stringCopyLim(S1,S2,5) ; printf("Stringa copiata = %s\n",S1) ; } Output: Stringa copiata = pippo Docente G. Armano - 75 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri [III] - la funzione stringCopyLim prende in ingresso due stringhe, SRC e DST passate per indirizzo (sono due vettori di caratteri) e la lunghezza massima della stringa destinazione, NMAX, passata per valore. - durante l'attivazione di stringCopyLim vengono create le variabili automatiche: ptr (dichiarata esplicitamente), DST, SRC, e NMAX (parametri). - ptr, DST, SRC sono tutte variabili di tipo puntatore a carattere, mentre NMAX è una variabile intera. - i valori iniziali di DST, SRC, e NMAX dipendono dai parametri attuali specificati nell'invocazione della funzione. In questo caso, le inizializzazioni sono, rispettivamente: E A(S1), E A(S2), E V(5)=5 - per sottolineare che un parametro non cambierà nel corso dell'esecuzione della funzione si può usare il qualificatore const. Per questo motivo, nell'esempio precedente SRC è stato dichiarato const. Docente G. Armano - 76 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri [IV] Per modificare la scelta di default relativa alle modalità di passaggio dei parametri, occorre ricorrere agli operatori “&” (indirizzo) e “*” (indirezione). Esempio: /* scambia il contenuto di due variabili */ void floatExchange ( float *F1, float *F2) { float tmp = *F1; *F1=*F2; *F2 = tmp ; } void main ( void ) { float V1[] = { 1.0, 2.0, 3.0, 4.0 } ; float X = 1.0, Y = -1.0 ; int i ; ... floatExchange(&X,&Y) ; floatExchange(&V1[1],&V1[2]) ; printf("X = %4.1f, Y = %4.1f\n",X,Y) ; for ( i=0; i < 4; i++) printf("V1[%d] = %3.1f\n", i, V1[i]) ; } Output: X = -1.0, Y = V[0] = 1.0 V[1] = 3.0 V[2] = 2.0 V[3] = 4.0 Docente G. Armano 1.0 - 77 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri [V] L'uso degli operatori “&” e “*” è necessario per evitare di lavorare su copie dei parametri attuali che verrebbero distrutte all'uscita dalla funzione. Ecco come NON si deve fare per scambiare due variabili: /* NON scambia il contenuto di due variabili */ void wrongFloatExchange ( float F1, float F2) { float tmp = F1; F1=F2; F2 = tmp ; } void main ( void ) { float V1[] = { 1.0, 2.0, 3.0, 4.0 } ; float X = 1.0, Y = -1.0 ; int i ; ... wrongFloatExchange(X,Y) ; wrongFloatExchange(V1[1],V1[2]) ; printf("X = %4.1f, Y = %4.1f\n",X,Y) ; for ( i=0; i < 4; i++) printf("V1[%d] = %3.1f\n", i, V1[i]) ; } Output: X = V[0] V[1] V[2] V[3] 1.0, Y = -1.0 = 1.0 = 2.0 = 3.0 = 4.0 NB non è cambiato assolutamente nulla nei parametri attuali ! Tutte le modifiche sono state infatti effettuate su copie dei parametri attuali (che vengono distrutte all'uscita della funzione). Docente G. Armano - 78 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Procedure e Funzioni: passaggio di parametri [VI] Un esame più attento della corretta procedura di scambio dei valori di due variabili. void floatExchange ( float *F1, float *F2) { float tmp = *F1; *F1=*F2; *F2 = tmp ; } - la funzione prende in ingresso due puntatori a float. - durante l'attivazione di floatExchange vengono create le variabili automatiche: tmp (dichiarata esplicitamente), F1, F2 (parametri). - F1 ed F2 sono variabili di tipo puntatore a float. - i valori iniziali di F1 ed F2 dipendono dai parametri attuali specificati nell'invocazione della funzione. - ad esempio, nella prima chiamata (scambio dei valori di X e di Y), le inizializzazioni di F1 ed F2 sono, rispettivamente: E V(&X)=E E A(X), E V(&Y)=E E A(Y). In altri termini, tramite F1 ed F2, alla procedura vengono passati gli indirizzi delle variabili di cui si vuole scambiare il valore. Docente G. Armano - 79 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Parametri di tipo puntatore a funzione Le procedure e funzioni possono anche essere passate come parametri. In tal caso, la specificazione del parametro di tipo funzione dovrà comprendere anche il tipo dei parametri da passare alla funzione puntata. Esempio: Supponiamo di dover realizzare una funzione di sort che ordini un vettore, e di voler passare come parametri le funzioni che eseguono il confronto e lo swap. int compInt ( int *k, int *w ) ; /* comparaz. di interi */ int compDouble ( double *k, double *w ) ; /* comparaz. di double */ void swapInt ( void *ptr1, void *ptr2 ) ; /* swap di interi */ void swapDouble ( void *ptr1, void *ptr2 ) ; /* swap di double */ void sort ( void * array, int dim, int (*comp) (void *, void *), void (*swap) ( void *, void * ) ) { int i, j; for ( i=0; i < dim-1 ; i++ ) for ( j=i+1; j < dim; j++ ) if ( (*comp)(&array[i],&array[j]) ) (*swap)(&array[i],&array[j]) ; } void main ( void ) { int V1[100] ; /* array di interi */ double F1[200] ; /* array di double */ ... /* inizializzazione vettori */ sort ( V1, 100, compInt, swapInt ) ; /* sort di interi */ sort ( F1, 200, compDouble, swapDouble ) ; /* sort di double */ ... } Docente G. Armano - 80 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output Le primitive di I/O del C non fanno parte del linguaggio, ma vengono definite come funzioni di libreria. In C, come in Unix, l'I/O viene realizzato da / su file. La tastiera e il monitor sono infatti trattati come file speciali (standard input e standard output). Le funzioni C che realizzano l'I/O hanno, tipicamente, una versione generalizzata per i file e una versione specializzata per lo standard input / output. Il tipo FILE, insieme alle funzioni che realizzano l'I/O, viene definito nel file di include <stdio.h>. Docente G. Armano - 81 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output [II] L'I/O viene solitamente suddiviso in tre categorie: - caratteri stringhe generalizzato bufferizzato Docente G. Armano - 82 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output di caratteri [II] Esiste una particolarità del C che riguarda l'I/O di caratteri: quando si vuole realizzare l'I/O di caratteri occorre trattare i caratteri singoli (char) come se fossero interi (int). La motivazione di questa scelta risiede nel fatto che (storicamente) il carattere di EOF non era un carattere del set ASCII originario. Si noti che sulle macchine in cui sono stati implementati i primi compilatori C la lunghezza degli interi era tipicamente 16 bit, di cui la parte alta veniva lasciata a zero per tutti i caratteri eccetto che per il carattere di EOF (tipicamente codificato con -1 su 16 bit, ovvero 0xFFFF). int getchar(void) ; int putchar(int ch) ; /* Legge un char da stdin */ /* Scrive un char su stdout */ int getc(FILE *F) ; int putc(int ch, FILE *F) ; /* Legge un char da file */ /* Scrive un char su file */ int fgetc(FILE *F) ; int fputc(int ch, FILE *F) ; /* Legge un char da file */ /* Scrive un char su file */ Si noti che getchar e putchar possono essere definiti in termini di getc e putc come segue: #define getchar() getc(stdin) #define putchar(ch) putc(ch,stdout) NB getc e putc sono quasi equivalenti a fgetc e fputc. Si consulti un manuale C per ulteriori informazioni. In particolare, si veda la funzione ungetc che “restituisce” un carattere al file da cui si sta prelevando l'input. Docente G. Armano - 83 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output di stringhe /* Legge una stringa da stdin */ char * gets ( char * ) ; /* Scrive una stringa su stdout */ char * puts ( char * ); /* Legge sino a num char da file */ char * fgets ( char *str, int num, FILE *stream ) ; /* Scrive una stringa su file */ char * fputs ( char *str, FILE *stream ); Docente G. Armano - 84 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output generalizzato La specificazione dei prototipi delle primitive che realizzano l'I/O generalizzato viene fatta utilizzando la notazione ANSI-C "..." che indica un numero variabile di parametri. La notazione K&R-C corrispondente utilizza il parametro speciale arglist. /* Legge da stdin un numero variabile di param. */ int scanf ( char *format, ... ) ; /* Scrive su stdout un numero variabile di param. */ int printf ( char *format, ... ) ; /* Legge da file un numero variabile di param. */ int fscanf ( FILE *stream, char *format, ... ) ; /* Scrive su file un numero variabile di param. */ int fprintf ( FILE *stream, char *format, ... ) ; Esiste anche la possibilità di effettuare un I/O da / su stringa, con le stesse modalità viste per i file: /* Legge da file un numero variabile di parametri */ int sscanf ( char *str, char *format, ... ) ; /* Scrive su file un numero variabile di parametri */ int sprintf ( char *str, char *format, ... ) ; Docente G. Armano - 85 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output generalizzato: formattazione Il parametro format specifica il formato per eseguire l'operazione di I/O. All'interno della stringa di formattazione, le specificazioni di formato vengono fornite facendole precedere dal carattere “%”. format % d, o x, u c s f e, g, p n i X E G % Docente G. Armano conversione intero, notazione decimale con segno intero, notazione ottale priva segno intero, notazione esadecimale priva segno intero, notazione decimale priva segno char, dopo la conversione a unsigned char stringa, stampa sino al char speciale '\0' floating / double (default 6 cifre decimali) float / double notazione esponenziale float / double usa alternativamente %e o %f scrive un indirizzo di memoria memorizza nell'argomento corrispondente il numero di char scritti dalla printf (sino al momento corrente) stampa un % - 86 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Input/Output bufferizzato Viene utilizzato per realizzare I/O (tipicamente in formato binario) da / su file. /* Legge da file count elem.,ognuno di dimensione size */ int fread( void *buffer, int size, int count, FILE *stream ); /* Scrive su file count elem.,ognuno di dimensione size */ int fwrite( const void *buffer, int size, int count, FILE *stream ); /* Seek */ int fseek ( FILE * stream, long offset, int org ); NB la funzione fseek si sposta lungo il file a partire dall'origine, dalla posizione corrente o dalla fine del file. La codifica dei valori che può assumere il parametro org è la seguente: SEEK_SET Inizio file SEEK_CUR Posizione corrente SEEK_END Fine file Docente G. Armano - 87 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Gestione file Per accedere ad un file, occorre innanzitutto aprirlo, poi usarlo e poi chiuderlo. Per l'utilizzazione possono essere utilizzate le funzioni viste in precedenza. L'apertura e la chiusura vengono realizzate tramite le seguenti funzioni: /* Apre un file */ FILE * fopen ( char *name, char *mode ) ; /* Chiude un file */ int fclose ( FILE *stream ) ; Modalità di apertura di un file: mode "r", "w", "a" "rb", "wb", "ab" "r+", "w+", "a+" "r+b", "w+b", "a+b" Docente G. Armano commento lettura, scrittura, append (text file) lettura, scrittura, append (binary file) lettura / scrittura text file (open, create, append) lettura / scrittura binary file (open, create, append) - 88 - Appunti sul linguaggio C Univ. degli Studi di Cagliari LINGUAGGIO C: PARTE II Docente G. Armano - 89 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Gestione della memoria dinamica Oltre alle zone di memoria riservate ai dati statici e allo stack, esiste un'altra zona di memoria, chiamata heap, destinata a contenere strutture dati create dinamicamente durante l'esecuzione di un programma. La memoria dinamica viene gestita in C come un dato astratto; in altri termini, l'implementazione della struttura dati dove vengono allocate le variabili dinamiche viene nascosta (ovvero resa “trasparente”) disciplinandone l'accesso tramite opportune primitive. Sostanzialmente il programmatore può allocare strutture successivamente disallocarle (quando non servono più). dati e I prototipi delle primitive messe a disposizione dal linguaggio C per gestire la memoria dinamica sono definiti nel file di include stdlib.h. Tra esse ricordiamo le principali (size_t è un tipicamente coincide con il byte): void void void void tipo predefinito che * malloc ( size_t size ) ; * calloc ( size_t num, size_t size ) ; * realloc ( void * ptr, size_t size ) ; free ( void * ptr ) ; Docente G. Armano - 90 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u 1 Gestione della memoria dinamica [II] - malloc alloca uno spazio di memoria di dimensione size nello heap e ne restituisce il puntatore. Se l'operazione non va a buon fine (ovvero se non c'è memoria sufficiente per soddisfare la richiesta) viene restituito NULL. Lo spazio di memoria non viene inizializzato. - calloc è una versione specializzata di malloc per allocare vettori. Infatti, occorre specificare il numero di elementi (num) da allocare e la dimensione (size) del singolo elemento. 1 - realloc modifica, portandola a size, l'ampiezza della struttura dati puntata da ptr. I contenuti restano invariati per uno spazio pari al minimo tra la vecchia e la nuova ampiezza. L'eventuale nuovo spazio non viene inizializzato. La funzione ritorna il puntatore alla nuova area, oppure NULL (in caso di insuccesso). - free libera la memoria precedentemente allocata, rendendola di nuovo disponibile per eventuali successive richieste. La funzione prende in ingresso un puntatore che deve essere rigorosamente quello restituito da una delle primitive di allocazione. In caso contrario è molto probabile che − prima o poi− seguirà un crash del programma (terminazione anormale). Ovviamente: calloc(num,size) ≡ malloc(num*size), ma anche: malloc(size) ≡ calloc(1,size). Docente G. Armano - 91 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Gestione della memoria dinamica: qualche esempio Supponiamo di voler allocare lo spazio per un dato di tipo Persona e di inizializzare la struttura dati con il contenuto di una variabile automatica dello stesso tipo. Successivamente, quando il dato non serve più vogliamo disallocarlo. #include <stdlib.h> typedef struct persona { char cognome[25] ; char nome [20] ; ... } Persona ; Persona * allocaPersona ( char * cognome, char * nome ) { /* alloca una struttura di tipo Persona nello heap */ Persona * ptr = ( Persona * ) malloc ( sizeof(Persona) ) ; /* inizializza la struttura dati - non ci interessa come */ ... return ptr ; } void main ( void ) { Persona * ptr1 ; /* definisce un puntatore a Persona */ ... ptr1 = allocaPersona("Salis","Carlo") ; /* crea il dato */ ... /* usa il dato */ free(ptr1) ; /* disalloca il dato che non serve più */ ... } Supponiamo ora di voler allocare lo spazio per un vettore di 100 elementi tipo Persona. In tal caso, è consigliabile usare la calloc: #include <stdlib.h> typedef struct persona { char cognome[25] ; char nome [20] ; ... } Persona ; void main ( void ) { Persona * ptr1 ; /* definisce un puntatore a Persona */ ... /* crea un vettore di 100 elementi di tipo Persona */ ptr1 = (Persona *) calloc(100,sizeof(Persona)) ; ... } Docente G. Armano - 92 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Gestione della memoria dinamica: qualche esempio [II] Supponiamo ora che, dopo aver allocato un vettore di 100 elementi, ci si accorga che è invece necessario uno spazio superiore (ad es. 150). In tal caso, dopo aver usato calloc o malloc, si può usare realloc. #include <stdlib.h> typedef struct persona { char cognome[25] ; char nome [20] ; ... } Persona ; void main ( void ) { Persona * ptr1 ; /* definisce un puntatore a Persona */ ... /* crea un vettore di 100 elementi di tipo Persona */ ptr1 = (Persona *) calloc(100,sizeof(Persona)) ; ... /* alloca spazio per altre 50 persone */ ptr1 = ( Persona * ) realloc ( ptr1, 150 * sizeof(Persona) ) ; ... } Docente G. Armano - 93 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Altre funzioni di libreria Oltre alle funzioni per la gestione della memoria dinamica e a quelle per l'I/O, il linguaggio C mette a disposizione altre librerie di funzioni. In particolare, ricordiamo le seguenti: - libreria per la gestione delle stringhe e dei caratteri - libreria matematica - libreria per la gestione del tempo - libreria delle chiamate di sistema - miscellanea Docente G. Armano - 94 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria per la gestione delle stringhe e dei caratteri Tra le funzioni messe a disposizione per gestire caratteri (prototipi in ctype.h), ricordiamo le seguenti: int int int int int int int isalpha ( int ch ) ; isdigit ( int ch ) ; isxdigit ( int ch ) ; islower ( int ch ) ; isupper ( int ch ) ; tolower ( int ch ) ; toupper ( int ch ) ; /* /* /* /* /* /* /* verifica verifica verifica verifica verifica converte converte char alfabetico */ cifra decimale */ cifra esadecimale */ char minuscolo */ char maiuscolo */ in maiuscolo */ in minuscolo */ Tra le funzioni messe a disposizione per gestire stringhe (prototipi in string.h), ricordiamo le seguenti: int strlen ( const char *str1 ) ; char * strcat char * strcmp char * strcpy int strcspn ( ( char *str1, const char *str2 ) ; ( const char *str1, const char *str2 ) ; ( char *str1, const char *str2 ) ; const char *str1, const char *str2 ) ; char * strncat ( char *str1, const char *str2, size_t size ) ; char * strncmp ( const char *str1, const char *str2, size_t size ) ; char * strncpy ( char *str1, const char *str2, size_t size ) ; int strnspn ( const char *str1, const char *str2 ) ; char * strchr ( const char *str1, int ch ) ; char * strrchr ( const char *str1, int ch ) ; char * strstr ( const char *str1, const char *str2 ) ; Docente G. Armano - 95 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria per la gestione delle stringhe e dei caratteri [II] - strlen calcola la lunghezza di una stringa. - strcat concatena str2 a str1; strcmp confronta str1 e str2 secondo l'ordinamento alfabetico e ritorna -1 se str1 < str2, +1 se str2 < str1, 0 se le due stringhe sono uguali; strcpy copia str2 in str1; strcspn ritorna l'indice del primo carattere di str1 che appartiene a str2 (che viene usata come bag, ovvero come "contenitore" di caratteri). In caso di fallimento ritorna NULL. - - strncat, strncmp, strncpy, strnspn sono le versioni di strcat, strcmp, strcpy, strspn limitate ai primi size caratteri (ad es.: strncat concatena al più size caratteri di str2 a str1). - strchr (strrchr) ritorna il puntatore alla prima (all'ultima) occorrenza del carattere ch nella stringa str. In caso di fallimento ritorna NULL. - strstr ritorna il puntatore alla prima occorrenza della sottostringa str2 trovata in str1. In caso di fallimento ritorna NULL. Docente G. Armano - 96 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria matematica Tra le funzioni messe a disposizione dalla libreria matematica (prototipi in math.h), ricordiamo le seguenti: double cos ( double x ) ; double sin ( double x ) ; double tan ( double x ) ; /* coseno, seno, tangente */ double acos ( double x ) ; /* arcocoseno, arcoseno, arcotangente */ double asin ( double x ) ; double atan ( double x ) ; double atan2 ( double x, double y ) ; /* arcotangente di x/y */ double cosh ( double x ) ; /* coseno, seno, tangente iperbolici */ double sinh ( double x ) ; double tanh ( double x ) ; double ceil ( double x ) ; /* ritorna il più piccolo intero >= x */ double floor ( double x ) ; /* ritorna il più grande intero <= x */ double exp ( double x ) ; /* esponenziale */ double pow ( double x, double y ) ; /* x elevato ad y */ double sqrt ( double x ) ; /* radice quadrata */ double fabs ( double x ) ; /* valore assoluto (double) */ double fmod ( double x, double y ) ; /* modulo (double) */ double log ( double x ) ; /* logaritmo naturale */ double log10 ( double x ) ; /* logaritmo base 10 */ Docente G. Armano - 97 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria per la gestione del tempo Le strutture dati coinvolte nelle operazioni che riguardano il tempo sono (i) time_t, tipicamente espresso tramite un unsigned long / long, e (ii) struct tm, che riporta il tempo sotto forma di struttura (con anno, mese, giorno, ore, minuti, secondi, ecc.). Il tempo viene misurato a partire dal 1900. Vediamo meglio la forma della struct tm: struct tm { int tm_sec ; int tm_min ; int tm_hour ; int tm_mday ; int tm_mon ; int tm_year ; int tm_wday ; int tm_yday ; int tm_isdst ; } /* /* /* /* /* /* /* /* /* secondi dopo il minuto [0-59] */ minuti dopo l'ora [0-59] */ ore dopo la mezzanotte [0-23] */ giorno del mese [1-31] */ mese a partire da Gennaio [0-11] */ anno dopo il 1900 */ giorno dopo la Domenica [0-6] */ giorno a partire da Gennaio [0-365] */ ora legale 1=si, 0=no, -1=info non disp. */ Tra le funzioni messe a disposizione per gestire il tempo (prototipi in time.h), ricordiamo le seguenti: time_t time ( time_t * ptr ) ; struct tm * localtime ( const time_t * ptr ) ; struct tm * gmltime ( const time_t * ptr ) ; char * asctime ( struct tm * ptr ) ; char * ctime ( const time_t * ptr ) ; Docente G. Armano - 98 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria per la gestione del tempo [II] - time ritorna l'ora corrente, oppure -1 se l’ora non è disponibile. Se l'argomento ptr è diverso da NULL il valore di ritorno viene anche assegnato alla locazione puntata da ptr. - localtime converte in ora locale (in forma di struct tm) l'ora contenuta in ptr (in forma di time_t). - gmltime converte in ora assoluta (in forma di struct tm) l'ora contenuta in ptr (in forma di time_t). - asctime converte in ASCII un tempo espresso in forma di struct tm. - ctime converte in ASCII un tempo espresso in forma di time_t. Si noti che: ctime(ptr) ≡ asctime(localtime(ptr)) Docente G. Armano - 99 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Libreria delle chiamate di sistema Tra le funzioni messe a disposizione per effettuare chiamate al sistema operativo (prototipi in stdlib.h e in process.h), ricordiamo le seguenti: void abort ( void ) ; /* termina il programma in modo anormale */ void exit ( int status ) ; /* termina il programma normalmente */ NB exit può anche essere usata per terminare il programma in modo “anormale”. Infatti, per convenzione, status = 0 indica una terminazione normale, mentre altri valori indicano una terminazione anormale. int execl ( char *fname, char * arg0, ..., char * argN, NULL ) ; Il gruppo di funzioni exec (ce ne sono altre oltre a quella indicata) viene usato per far partire un altro processo (child process) durante l'esecuzione del programma. Il nome del file che contiene il nuovo programma da eseguire è puntato da fname e gli eventuali argomenti sono specificati di seguito (la lista termina con NULL). Docente G. Armano - 100 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Librerie: miscellanea Tra le altre funzioni di libreria ricordiamo le seguenti: int atoi ( const char * anInt ) ; /* conv. stringa in int */ long atol ( const char * aLong ) ; /* conv. stringa in long */ double atof ( const char * aFloat ) ; /* conv. stringa in double */ int rand ( void ) ; int random ( int num ) ; void randomize ( void ) ; /* genera num. casuale tra 0 e RAND_MAX */ /* genera num. casuale tra 0 e num-1 */ /* iniz. il generatore di numeri random */ Per gestire funzioni con un numero di parametri variabile occorre ricorrere alle seguenti macro (= macro istruzioni), definite in stdarg.h: void va_start ( va_list argPtr, lastParam ) ; void va_arg ( va_list argPtr, type ) ; void va_end ( va_list argPtr ) ; Le macro utilizzano il tipo va_list definito anch'esso in stdarg.h. Non occorre e non si deve conoscere l'effettiva implementazione di va_list. Le funzioni con numero variabile di parametri devono avere almeno un parametro che precede la lista dei parametri di cui non si conosce nome e numero. Un tipico esempio di funzione con numero variabile di parametri è la funzione di libreria di I/O printf. Il suo prototipo è il seguente: /* K&R-C */ int printf ( const char * format, arg_list ) ; /* ANSI-C */ int printf ( const char * format, ... ) ; Si noti che QUI la notazione "...", che fa parte del linguaggio C, indica un numero variabile di parametri. Nel resto di questi appunti è invece stata spesso usata per indicare qualcosa di non interessante ai fini dell'esempio considerato. Docente G. Armano - 101 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Librerie: miscellanea [II] Una versione semplificata di printf (per illustrare l'uso di va_start, va_arg, va_end) che stampa soltanto interi, double e stringhe di caratteri (si fa l’ipotesi di avere a disposizione funzioni specializzate per la stampa di interi, double e caratteri): #include <stdarg.h> extern void printInt(int value) ; /* stampa un int */ extern void printDouble(double value) ; /* stampa un float */ extern void printChar(int value) ; /* stampa un char */ void simplePrintf ( char * format, ... ) { va_list argPtr ; char *p, *sval ; int ival; double dval; va_start ( argPtr, format ) ; for ( p = format ; *p ; p++ ) { if ( *p != '%' ) { putchar(*p) ; continue ; } switch ( *++p ) { case 'd' : /* specifica di stampa di numero intero */ ival = va_arg ( argPtr, int ) ; printInt ( ival ) ; break ; case 'f' : /* specifica di stampa di numero float */ fval = va_arg ( argPtr, double ) ; printFloat ( fval ) ; break ; case 's' : /* specifica di stampa di una stringa */ sval = va_arg ( argPtr, char * ) ; while ( *sval ) printChar ( *sval++ ) ; break ; default : putchar(*p) ; break ; /* per stampare il % */ } } va_end(argPtr) ; /* altrimenti probabile crash del programma */ } Docente G. Armano - 102 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Altre caratteristiche del linguaggio (macro e direttive) Il linguaggio C consente di definire macro di utilizzo generale, ovvero costanti o sequenze di caratteri che prima della compilazione vera e propria vengono “espanse”. Esempi: #define #define #define #define #define pigreca 3.1415 currentPath "pippo/source/" max(x,y) ( ( (x) > (y) ) ? (x) : (y) ) New(type) (type *) malloc ( sizeof(type) ) NewVector(dim,type) (type *) calloc ( dim, sizeof(type) ) Si noti che max, New e NewVector hanno anche parametri. La loro “espansione” comporta la sostituzione della parte sinistra con la parte destra prima che venga effettuata la compilazione vera e propria. Docente G. Armano - 103 - Appunti sul linguaggio C Univ. degli Studi di Cagliari u Altre caratteristiche del linguaggio (macro e direttive) [II] Al compilatore possono essere anche fornite direttive condizionali, che possono fare cambiare una compilazione senza che il testo sorgente venga modificato. Esempio: void foo ( int value ) { #ifdef DEBUG printf("pluto: value = %d\n",value) ; #endif ... /* codice della funzione */ } In questo caso, l'effetto della direttiva #ifdef è quello di valutare se esiste una definizione di DEBUG oppure no. Se esiste, allora viene compilata anche la parte tra #ifdef e #endif, altrimenti si compila soltanto il (vero e proprio) codice della funzione. La DEBUG dell'esempio può essere definita una volta per tutte nel main, oppure specificata all'atto della compilazione. Facendo l'ipotesi che il nome del file contenente la funzione foo sia pippo.c, la sua compilazione potrebbe essere effettuata nel modo seguente (usando l'opzione -D del compilatore): $ cc -DDEBUG -c pippo.c Docente G. Armano - 104 - Appunti sul linguaggio C Univ. degli Studi di Cagliari ♦ Bibliografia sul Linguaggio C • B. W. Kernighan, D.M. Ritchie, Linguaggio C, Jackson • Davies, The Indispensable Guide to C, Addison Wesley • B. Gottfried, Programmare in C, McGraw Hill (Collana Schaum) • H. Schildt, Linguaggio C, McGraw Hill Docente G. Armano - 105 - Appunti sul linguaggio C