Completo Linguaggio C

52
LINGUAGGIO C Le variabili Una variabile è un'area di memoria cui viene dato un nome, in grado di memorizzare un singolo valore (numerico o un carattere). Per poter utilizzare una variabile per salvare un valore è necessario dichiararla specificandone il nome ed il tipo di valore per i quali di desidera utilizzarla; dunque, prima di utilizzare una variabile è necessario dichiarla. Nel C ANSI la dichiarazione delle variabili viene messa all'inizio della funzione che ne farà uso. Ci sono cinque tipi "primitivi": int - numero intero. float - numero in virgola mobile, ossia un numero con una parte frazionaria. double - numero in virgola mobile in doppia precisione. char - un carattere. void - un tipo per usi speciali Uno degli aspetti meno chiari del linguaggio è la quantità di memoria che ciascuno di questi tipi richiede non è predefinita, in quanto dipende dall'architettura del calcolatore. Per individuare quanta memoria viene messa a disposizione dall'architettura del proprio calcolatore è possibile compilare ed eseguire il programma di seguito riportato, il quale visualizza il numero di byte utilizzati dai vari tipi, mediante l'uso dell'operatore sizeof. #include <stdio.h> #include <stdlib.h> int main() { char c; int a; float f; double d; printf("\n*** occupazione di memoria per ogni tipo di variabile ***\n"); printf("carattere (char): %d byte\n", sizeof(c)); printf("intero (int): %d byte\n", sizeof(a)); printf("reale singola precisione (float): %d byte\n", sizeof(f)); printf("reale doppia precisione (double): %d byte\n", sizeof(d)); } Variabile intera Una variabile di tipo int può memorizzare un valore compreso tra i -32768 e +32767 nel caso di un impiego di 2 byte per le variabili di questo tipo (i valori derivano da 2^15, numero con segno rappresentabile in complemento a 2 su 16 bit). Per dichiarare una variabile di tipo intero si utilizza l'istruzione seguente:

description

Linguaggio C

Transcript of Completo Linguaggio C

  • LINGUAGGIO C

    Le variabili

    Una variabile un'area di memoria cui viene dato un nome, in grado di memorizzare un singolo valore (numerico o un carattere). Per poter utilizzare una variabile per salvare un valore necessario dichiararla specificandone il nome ed il tipo di valore per i quali di desidera utilizzarla; dunque, prima di utilizzare una variabile necessario dichiarla. Nel C ANSI la dichiarazione delle variabili viene messa all'inizio della funzione che ne far uso.

    Ci sono cinque tipi "primitivi":

    int - numero intero. float - numero in virgola mobile, ossia un numero con una parte frazionaria. double - numero in virgola mobile in doppia precisione. char - un carattere. void - un tipo per usi speciali

    Uno degli aspetti meno chiari del linguaggio la quantit di memoria che ciascuno di questi tipi richiede non predefinita, in quanto dipende dall'architettura del calcolatore. Per individuare quanta memoria viene messa a disposizione dall'architettura del proprio calcolatore possibile compilare ed eseguire il programma di seguito riportato, il quale visualizza il numero di byte utilizzati dai vari tipi, mediante l'uso dell'operatore sizeof.

    #include #include int main() { char c; int a; float f; double d; printf("\n*** occupazione di memoria per ogni tipo di variabile ***\n"); printf("carattere (char): %d byte\n", sizeof(c)); printf("intero (int): %d byte\n", sizeof(a)); printf("reale singola precisione (float): %d byte\n", sizeof(f)); printf("reale doppia precisione (double): %d byte\n", sizeof(d)); }

    Variabile intera

    Una variabile di tipo int pu memorizzare un valore compreso tra i -32768 e +32767 nel caso di un impiego di 2 byte per le variabili di questo tipo (i valori derivano da 2^15, numero con segno rappresentabile in complemento a 2 su 16 bit). Per dichiarare una variabile di tipo intero si utilizza l'istruzione seguente:

  • int variable name;

    Ad esempio:

    int a;

    dichiara una variabile chiamata a di tipo intero. Per assegnare un valore alla variabile intera si usa l'espressione:

    a=10;

    Il linguaggio C utilizza il carattere = per l'operatore di assegnamento. Un'espressione della forma a=10; dovrebbe essere interpretata come prendi il valore numerico 10 e salvalo nella locazione di memoria associata alla variabile intera a. In generale, una variabile di tipo int occupa 2 byte di memoria.

    Variabile reale

    Il C ha due tipi per dichiarare variabili con una parte frazionaria: float e double, che differiscono per la quantit di memoria che occupano e per la precisione che consentono di garantire

    Una variabile di tipo float (da floating point, virgola mobile) consente di rappresentare numeri con una precisione di 7 cifre, per valori che vanno (pi o meno) da 1.E-36 a 1.E+36. In generale, una variabile di tipo float occupa 4 byte di memoria.

    Una variabile di tipo double (da double precision, doppia precisione) consente di rappresentare un numero con una precisione di 13 cifre, per valori che vanno da 1.E-303 a 1.E+303. In generale, una variabile di tipo float occupa 8 byte di memoria.

    Variabile carattere

    Alla base del C ci sono numeri e caratteri, anche se questi ultimi vengono in realt visti anch'essi come numeri, e pi precisamente come il valore intero del codice ASCII corrispondente al carattere trattato. Per dichiarare una variabile di tipo carattere si utilizza la parola chiave char. Un carattere viene memorizzato in un byte.

    Per esempio, la seguente una dichiarazione di una variabile di tipo carattere:

    char c;

    Per assegnare, o memorizzare, un carattere nella variabile c necessario indicare il carattere tra apici singoli, come segue:

    c = 'A';

    Si noti che in una variabile di tipo char si pu memorizzare un solo carattere.

  • NOTA Si faccia attenzione al fatto che i singoli caratteri sono indicati tra apici singoli e non tra doppi apici:

    char alfa, beta; alfa = 'x'; /* -1- */ beta = "y"; /* -2- */

    La prima istruzione corretta, la seconda semanticamente scorretta.

    La parte di dichiarazione del tipo specifica che tipi di valori, e quali comportamenti, sono supportati dall'entit che si sta dichiarando. Non vi differenza tra variabili dichiarate attraverso un'unica dichiarazione o mediante dichiarazioni multiple dello stesso tipo. Ad esempio:

    float x, y;

    equivalente a

    float x; float y;

    Ogni inizializzazione viene effettuata con l'assegnamento (utilizzando l'operatore =) di un'espressione del tipo appropriato. Ad esempio:

    x = 3.14; y = 2.81;

    Cambiamento automatico di tipo

    Nelle espressioni aritmetiche possibile mischiare variabili numeriche di tipo diverso. In quasi tutti i casi i valori con una precisione inferiore vengono convertiti ad una precisione superiore ed utilizzati dagli operatori. Si consideri lo stralcio ci codice seguente:

    int a; float b; ... ... = a*b;

    L'espressione a*b viene valutata dopo aver convertito il valore intero in un float (avr parte frazionaria nulla) e dopo aver svolto la moltiplicazione. Il risultato della moltiplicazione un valore di tipo float. Nel caso in cui questo venga assegnato ad una variabile float tutto funziona normalmente. Nel caso in cui venga assegnato ad una variabile di tipo int il valore verr automaticamente troncato (non arrotondato). Questa conversione automatica di tipo vale anche per i caratteri. Un carattere viene rappresentato come un carattere ASCII esteso o un altro codice con un valore tra 0 e 255. Un altro modo di vedere la cosa che una variabile di tipo char una variabile intera di un solo byte in grado di memorizzare un valore compreso tra 0 e 255, che pu anche essere interpretato come un carattere.

  • Tipi definiti dall'utente

    La parola chiave typedef viene utilizzata per assegnare un alias ad un qualsiasi tipo fondamentale oppure derivato (introdotto dall'utente e derivato dai tipi fondamentali). Con typedef non si definisce un nuovo tipo, ma si introduce un nome che corrisponde a un tipo definito. La sintassi la seguente:

    typedef nome_tipo nuovo_nome_tipo;

    A titolo di esempio si consideri la seguente dichiarazione:

    typedef int boolean;

    In tal modo possibile dichiarare variabili di tipo boolean che possono assumere tutti i valori interi:

    int a, b; boolean finito;

    Rinominare un tipo pu essere utile per rendere pi leggibile il programma e per evitare espressioni altrimenti complesse. Quando si ha a che fare con struct l'utilizzo di typedef risulta particolarmente comodo (Lezione 12)

    Espressione di assegnamento

    Una volta dichiarata una variabile possibile utilizzarla (se si omette la dichiarazione il compilatore lo segnala con un errore) per memorizzare un valore ed in seguito manipolarlo. possibile memorizzare un valore con la seguente sintassi:

    nome_variabile = valore;

    Si faccia attenzione a non scambiare la variabile a cui assegnare il valore con quella che contiene il valore da assegnare: ci che sta a sinistra dell'operatore = la destinazione dell'assegnamento e pu essere solo una variabile. Ci che sta a destra la sorgente e pu essere qualsiasi espressione che dia un valore. L'esempio seguente:

    a = 10;

    memorizza il valore 10 nella variabile int chiamata a. Sarebbe sbagliato sintatticamente scrivere l'espressione al contrario:

    10 = a;

    e come errore sintattico il compilatore lo segnalerebbe. Non cos nel caso in cui si desideri assegnare alla variabile a il valore contenuto nella variabile b:

    a = b;

    In questo caso girare l'espressione produrrebbe un effetto diverso e non sarebbe sintatticamente scorretto!

  • b = a;

    Espressioni e operatori aritmetici

    Si definisce espressione aritmetica un insieme di variabili, costanti e richiami di funzione connessi da operatori aritmetici. Il risultato di un'espressione aritmetica sempre un valore numerico. Nella tabella sottostante sono presentati gli operatori aritmetici e le loro priorit in ordine dalla pi alta alla pi bassa ...

    negazione (- unario)

    moltiplicazione (*) divisione (/) modulo (%)

    somma (+)

    sottrazione (-)

    assegnamento (=)

    L'unico operatore unario (e cio che si applica ad un solo operando) la negazione: Se x ha valore 5, l'espressione

    -x;

    ha valore -5. L'operatore modulo, %, calcola il resto della divisione intera tra i due operandi. L'assegnamento = anch'esso un operatore e ha una posizione all'interno della scala di priorit, ed la pi bassa. Data la priorit degli operatori necessario utilizzare le parentesi tonde () per ottenere la corretta sequenza di valutazione delle espressioni quando sono articolate e composte da pi operatori, come per esempio la seguente: a=10.0 + 2.0 * 5.0 - 6.0 / 2.0

    che equivale a:

    a=10.0 + (2.0 * 5.0) - (6.0 / 2.0)

    e non a:

    a=(10.0 + 2.0) * (5.0 - 6.0) / 2.0

    Gli operatori di incremento e decremento

    Gli operatori ++ e -- sono di incremento e decremento rispettivamente, e possono essere applicati a variabili numeriche. L'espressione i++ equivale a i = i + 1, a parte il fatto che la variabile i viene valutata solo una volta nella forma compatta. Gli operatori di incremento e decremento possono essere sia prefissi che postfissi: essi possono apparire sia prima sia dopo l'operando a cui si riferiscono. Se l'operatore viene posto prima (prefisso) l'operazione viene eseguita prima di restituire il valore dell'espressione; se viene posto dopo (postfisso) l'operazione viene eseguita dopo aver utilizzato il valore originale. Ad esempio:

    ... int i; i = 15; printf("%d, %d, %d", ++i, i++, i); ...

    LazyalexEvidenziato

    LazyalexEvidenziato

    LazyalexEvidenziato

  • Il risultato che si ottiene :

    16, 16, 17

    Infatti, l'espressione ++i incrementa i prima di stamparlo a video (quindi i 16); la successiva i++ viene valutata al valore corrente di i (16) che verr poi incrementato (a 17); infine l'espressione i il valore di i dopo il postincremento (17).

    opportuno evitare di modificare pi volte il valore di una variabile per evitare di rendere il codice illeggibile

    Applicando l'operatore di incremento (o decremento) ad una variabile di tipo char si passa al carattere successivo (o precedente).

    Espressioni e operatori logici

    Un'espressione logica un'espressione che genera come risultato un valore vero o falso e viene utilizzata dalle istruzioni di controllo del flusso di esecuzione. La valutazione delle espressioni logiche pu avere o un valore diverso da zero (interpretato come vero) oppure un valore pari a zero (interpretato come falso). Un semplice esempio di espressione logica una variabile: se il suo contenuto diverso da zero, allora l'espressione vera, altrimento l'espressione falsa.

    Le espressioni logiche possono contenere gli operatori relazionali usare per confrontare tra loro dei valori. Anche tra questi operatori ci sono delle priorit, riportate nella tabella sottostante.

    maggiore(>) maggiore-uguale(>=) minore(

  • esp1 && esp2 = !(!(esp1) || !(esp2)) esp1 || esp2 = !(!(esp1) && !(esp2))

    Mediante queste due leggi possibile girare le espressioni logiche in base alla propria capacit di esprimerle e al tipo di condizione che richiesta. Ad esempio, l'espressione per valutare se una variabile num sia compresa tra due valori espremi min e max - valori inclusi - pu essere scritta come:

    (num >= min) && (num

  • In questo modo, ad ogni esecuzione l'utente pu inserire valori diversi, producendo risultati diversi.

    Il passo ulteriore vedere il risultato, ossia fare stampare sullo standrd output (il video) il risultato dell'elaborazione. Questo viene fatto mediante la funzione printf: per visualizzare il valore memorizzato in una variabile necessario scrivere:

    printf("Il valore della variabile a e': %d", a);

    Il %d sia nella scanf, sia nella printf indica che il valore gestito un intero in base dieci.

    Nota: la funzione scanf non fa una richiesta all'utente, semplicemente aspetta che l'utente inserisca il dato. Per questo motivo, buona norma utilizzare prima una printf che informi l'utente sulla necessit di inserire un dato, come ad esempio:

    ... printf("Inserisci un numero intero: "); scanf("%d", &a);

    Le funzioni di ingresso e uscita nel dettaglio

    printf

    La funzione printf ha sempre come primo argomento tra le parentesi tonde una stringa (ossia una sequenza di caratteri delimitata dalle virgolette - ". Dopo la stringa possibile mettere un numero qualsiasi di altri argomenti, in base alla necessit. La forma pi generale :

    printf(stringa, espressione, espressione, espressione...)

    La stringa deve includere un segnaposto per ciascuna espressione successivamente elencata, per specificare in quale punto della stringa deve essere posizionato il valore dell'espressione e di che tipo di valore si tratta. Per questo motivo la stringa prende solitamente il nome di stringa di controllo o stringa di formato.

    Il funzionamento il seguente: la printf scorre la stringa da sinistra verso destra e scrive a video (il dispositivo d'uscita standard) tutti i caratteri che incontra. Quando trova un % allora identifica il tipo di dato che deve essere stampato mediante il carattere (o i caratteri) che seguono il %. Tale elemento (il %) un segnaposto ad indicare che l necessario stampare il valore di un'espressione che di un tipo ben preciso. La funzione printf utilizza tale informazione per convertire e formattare il valore ottenuto dall'espressione che segue la stringa di formato. Valutato, formattato e scritto a video il valore dell'espressione, la funzione continua nell'analisi della stringa, fino al prossimo % o alla fine della stringa. Per esempio:

    printf("Inserisci un numero intero:");

    C' solo la stringa di formato e tutti i caratteri vengono scritti sullo schermo, cosicch ci che vede l'utente appunto la scritta inserita tra le virgolette. Se si considera invece l'altro esempio:

  • printf("Il valore della variabile a e': %d", a);

    il %d specifica che si deve valutare l'espressione che segue la stringa come un numero intero decimale. Il risultato sar vedere a video la scritta:

    Il valore della variabile a e': xx

    dove xx il valore che in quel momento memorizzato nella variabile a. anche possibile scrivere invece della variabile a un'espressione, come ad esempio:

    printf("Il risultato e': %d", a+1);

    che scriver il valore della variabile a pi un'unit. Il valore della variabile resta per invariato, in quanto non stato riassegnato.

    La specifica %d no n solamente un identificatore di formato, ma uno specificatore di conversione: indica il tipo di valore risultante dall'espressione e come tale tipo di dato deve essere convertito in caratteri da visualizzare sullo schermo. Se per un qualsiasi motivo l'espressione indicata dopo la stringa di formato ha un valore reale (derivante da un float) verr comunque stampato qualcosa, che per non corrisponder al valore esatto. La ragione che un int utilizza la met dello spazio occupato da un float. Quindi verr visualizzato solamente il contenuto dei primi due byte. Questi due primi byte verranno interpretati come la rappresentazione in complemento a due di un numero intero dotato di segno. Tutto ci molto lontano dal corrispondere anche solo alla parte intera del numero reale, rappresentato in complemento a due ma notazione virgola mobile (formato standard IEEE 754).

    A parte i dettagli tecnici sono due le cose importanti da ricordare:

    1. L'identificatore che segue il % specifica il tipo di variabile che deve essere visualizzato e il formato dell'espressione che segue

    2. Nel caso ci sia una differenza tra l'identificatore indicato e il valore calcolato dell'espressione il dato visualizzato non necessariamente corretto e pu causare errori anche su gli altri elementi della printf.

    Gli identificatori di formato %

    Gli identificatori previsti dall'ANSI C sono:

    Tipo Espressione A video %c char singolo carattere %d (%i) int intero con segno %e (%E) float or double formato esponenziale %f float or double reale con segno %g (%G) float or double utilizza %f o %e in base alle esigenze %o int valore base 8 senza segno %p pointer valore di una variabile puntatore %s array of char stringa (sequenza) di caratteri %u int intero senza segno %x (%X) int valore base 16 senza segno

  • Caratteri di controllo

    Ci sono alcuni codici di controllo che non stampano caratteri visibili ma contibuiscono a formattare ci che viene stampato:

    \b cancella \f avanzamento carta \n nuova linea \r a capo (senza una nuova linea) \t tabulatore \' apice \0 null

    In generale solamente il codice \n viene utilizzato di frequente, per mandare a capo inserendo una nuova riga. Ad esempio:

    ... printf("prima riga ...\n"); printf("seconda riga ...\n");

    produrr a video la stampa di due righe, con il cursore poi sulla terza riga:

    prima riga ... seconda riga ...

    scanf

    La funzone scanf simile alla printf nella forma:

    scanf(stringa, variabile, variabile, ...)

    In questo caso la stringa di controllo specifica come i caratteri immessi dall'utente mediante la tastiera debbano essere convertiti e memorizzati nelle variabili. ci sono per alcune differenze significative:

    La prima che mentre la funzione printf valuta il valore di un'espressione, ad esempio il valore di una variabile, senza modificarlo, la funzione scanf deve modificare il valore di una variabile per memorizzarci il valore appena acquisito. La trattazione dettagliata di questi aspetti oggetto della Lezione 12, per il momento sufficiente ricordare questa necessit di modifica. Per indicare questa differenza, si antepone al nome della variabile un &.

    La seconda differenza che relativa alla stringa di controllo. La regola che la funzione scanf processa la stringa di controllo da sinistra a destra ed ad ogni segnaposto cerca di interpretare i caratteri ricevuti in ingresso in relazione all'identificatore (gli identificatori sono gli stessi della funzione printf). Se vengono specificati pi valori nella stringa di controllo, si presuppone che questi vengano immessi da tastiera in modo separato, usando come

  • separatore lo spazio, l' o il tabulatore. Questo significa che per inserire tre interi possibile digitare i dati in una delle due forme:

    3 4 5

    oppure

    3 4 5

    Per esempio, l'istruzione:

    scanf("%d %d",&i,&j);

    acquisisce due numeri interi e li memorizza rispettivamente nelle variabili i e j. I due valori possono essere inseriti separati da uno spazio (o un numero qualsivoglia di spazi) oppure andando a capo ogni volta.

    Unica eccezione il caso di %c: viene acquisito un carattere, qualsiasi esso sia, quindi qualsiasi tasto venga premuto sulla tastiera, quello l'unico carattere acquisito.

    Librerie standard

    Per potre utilizzare le funzioni printf e scanf necessario includere la libreria standard del C che le definisce. richiesta quindi la direttiva:

    #include

    che deve essere indicata prima del main. In alcuni ambienti di programmazione le librerie vengono incluse automaticamente e la direttiva risulta non necessaria. Se l'ambiente per non effettua l'inclusione in modo autonomo, la compilazione del programma non va a buon fine in quanto viene segnalato errore di "funzione sconosciuta" in corrispondenza delle printf e scanf. quindi buona norma utilizzare sempre la direttiva indicata.

    Riassumendo ....

    Si scriva un programma che chiede all'utente un numero che indica la temperatura in gradi centigradi e stampa a video l'equivalente in Fahrenheit

    #include main() { float celsius, fahr; printf("Inserisci la temperatura in gradi Celsius: "); scanf("%g", &celsius); fahr = (celsius * 9)/5 + 32; printf("%g gradi Celsius = %g gradi Fahrenheit\n", celsius, fahr); }

  • Le istruzioni e i blocchi

    Le istruzioni di espressione, come i++, o la chiamata di una funzione, sono istruzioni seguite da un punto e virgola, che indica la fine dell'istruzione. Di fatto anche il punto e virgola da solo un'istruzione che non fa nulla (istruzione vuota). Non tutte le espressioni possono diventare istruzioni, perch, ad esempio, x 1) for(i=0; i < h; i++)

  • if (numeri[i] > 0) somma += numeri[i]; else /* ATTENZIONE!!!! */ somma = numeri[0]; ....

    La clausola else sembra essere collegata al controllo sulla lunghezza dell'array, ma solo un effetto dell'indentazione (aspetto ignorato dal compilatore). La clausola else risulta essere collegata all'ultimo if che non possiede la clausola. Il codice precedente in realt equivalente il seguente:

    ... if (h > 1) for(i=0; i < h; i++) if (numeri[i] > 0) somma += numeri[i]; else somma = numeri[0]; ....

    Per poter collegare la clausola else all'ultimo if necessario utilizzare le parentesi graffe per creare blocchi:

    ... if (h > 1){ for(i=0; i < h; i++) if (numeri[i] > 0) somma += numeri[i]; } else somma = numeri[0]; ....

    switch

    Il costrutto switch consente di trasferire il controllo di esecuzione all'interno di un blocco di istruzioni, in cui sia presente un'etichetta. Il punto scelto determinato dal risultato della valutazione di una espressione intera. La forma generale del costrutto :

    switch (espressione) { case n: istruzioni case m: istruzioni case h: istruzioni ... default: istruzioni }

    I valori n, m, ... sono delle costanti intere. Se il valore dell'espressione combacia con il valore di una delle etichette case il controllo viene trasferito alla prima istruzione che segue tale etichetta. Se non vi alcuna etichetta che combacia, allora il controllo viene trasferito alla prima istruzione dopo l'etichetta default, se esiste, altrimenti si salta tutta l'istruzione switch.

    Una volta trasferito il controllo alla prima istruzione di quelle che seguono l'etichetta case che combacia, le istruzioni successive vengono eseguite una per volta, anche se sono associate ad un'altra etichetta case. Un'etichetta case o default non spinge ad uscire dallo switch. Se si desidera

  • arrestare l'esecuzione delle istruzioni all'interno del blocco switch necessario utilizzare l'istruzione break. All'interno di un blocco switch, l'istruzione break trasferisce il controllo all'esterno del blocco, alla prima istruzione che segue lo switch .

    Nello stralcio di codice sottostante riportato un esempio di utilizzo dell'istruzione switch senza il break. Nella stragrande maggioranza dei casi l'istruzione break necessaria per ottenere il comportamento desiderato.

    ... val = 0; switch(ch){ case 'f': case 'F': val++; case 'e': case 'E': val++; case 'd': case 'D': val++; case 'c': case 'C': val++; case 'b': case 'B': val++; case 'a': case 'A': val++; case '9': val++; case '8': val++; case '7': val++; case '6': val++; case '5': val++; case '4': val++; case '3': val++; case '2': val++; case '1': val++; break; default: val = -1; printf("carattere %c non appartenente all'alfabeto base16\n", ch); }

    L'espressione di switch deve essere di tipo char o int. Tutte le etichette case devono essere espressioni costanti. In tutte le istruzioni switch singole, ogni valore associato alle etichette case deve essere unico, e ci pu essere al pi una sola etichetta default.

    while e do-while

    Una struttura di iterazione consente di specificare un'azione, o un insieme di azioni, che dovr essere ripetuta pi volte. Si parla in questi casi di ciclo. Il ciclo while generalmente strutturato come segue:

    while (espressione) istruzione

    L'espressione viene valutata e, se ha valore diverso da 0 (vero) viene eseguita l'istruzione successiva (che pu anche essere un intero blocco di istruzioni, opportunamente delimitato dalle parentesi graffe). Una volta che quest'ultima terminata, l'espressione viene valutata

  • nuovamente e se nuovamente vera, si ripete l'istruzione. Ci si ripete fino a quando l'espressione ha valore 0 (falso), nel qual caso il controllo si trasferisce all'istruzione successiva al while.

    Un ciclo while pu essere eseguito 0 o pi volte, poich l'espressione potrebbe essere falsa gi la prima volta. Si tratta di un ciclo a condizione iniziale: prima di eseguire il ciclo si valuta la condizione.

    A volte si desidera eseguire il corpo di un ciclo almeno una volta. In tal caso si utilizza un ciclo do-while, ossia un ciclo a condizione finale. La struttura la seguente:

    do istruzione while (espressione);

    In questo caso, l'espressione viene valutata al termine dell'esecuzione dell'istruzione (o del blocco di istruzioni). Fino a quando l'espressione vera, l'istruzione viene ripetuta.

    Ecco due esempi di utilizzo dei costrutti di ciclo:

    printf("Inserire un intero positivo\n"); scanf("%d" ,&num); while(num > 0){ printf("*"); num--; } do{ printf("Inserisci un intero compreso tra 0 e 15, inclusi:"); scanf("%d", num); }while(i 15);

    for

    L'istruzione for utilizzata per effettuare un ciclo dal principio sino alla fine di un intervallo di valori. La sintassi la seguente:

    for (espressione-iniziale; espressione-booleana; espressione-incremento) istruzione

    L'espressione-iniziale permette di inizializzare le variabili di ciclo, e viene eseguita una volta sola, prima di qualsiasi altra operazione. Successivamente ad essa, viene valutata l'espressione del ciclo e, se questa ha valore diverso da 0 - vera -, viene eseguita l'istruzione che costituisce il corpo del ciclo. Al termine dell'esecuzione del corpo del ciclo, viene valutata l'espressione-incremento, di solito per poter aggiornare i valori delle variabili di ciclo. Quindi, si valuta nuovamente l'espressione del ciclo e cosi via. Il ciclo si ripete finch non si valuta come falsa l'espressione del ciclo - valore 0-. Questo modo di procedere praticamente equivalente a:

    { espressione-iniziale;

  • while(espressione-booleana) { istruzione espressione-incremento; } }

    se non fosse che si eseguirebbe sempre l'espressione-incremento se venisse incontrata un'istruzione continue all'intemo del corpo del ciclo. La parte di inizializzazione e quella di iterazione di un ciclo for possono essere costituite da elenchi di espressioni separate da virgole. Tali espressioni vengono valutate, come molti operatori, da sinistra a destra. Ad esempio, per poter far procedere due indici in direzioni opposte, sarebbe appropriato scrivere del codice come:

    for (i = 0, j = NumElem - 1; j >= 0; i++, j--) { /* ... */ }

    break

    Un'istruzione break pu essere utilizzata per uscire da qualunque blocco di codice appartenente ad un ciclo, controllato da uno switch, o anche da un for, while o do-while. In generale verr utilizzato solamente all'interno dello switch per indicare il termine della sequenza di istruzioni da eseguire una volta trovata l'etichetta che combacia con l'espressione dello switch. L'istruzione si presenta come:

    break;

    continue

    Un'istruzione continue pu essere utilizzata solamente all'interno di un ciclo (for, while oppure do-while) e trasfetisce il controllo alla fine del corpo del ciclo. Nel caso di cicli while e do-while, ci spinge a valutare l'espressione del ciclo immediatamente successivo. In un ciclo for, invece, quella che viene valutata per prima l'espressione-incremento e solo dopo si valuta l'espressione del ciclo. L'istruzione si presenta come:

    continue;

    Un'istruzione continue viene spesso utilizzata per saltare una parte del blocco di istruzioni sul quale si sta svolgendo il ciclo. Ad esempio:

    while(valore != -1){ if(valore == 0) continue; /* non considerare il valore 0 */ cont++; scanf("%d", &valore); }

    return

    Un'istruzione return termina l'esecuzione di un sottoprogramma e restituisce il controllo al chiamante. Il valore dell'espressione nell'istruzione return viene restituito al chiamate come valore restituito dal sottoprogramma (funzione).

  • Se il sottoprogramma non restituisce alcun valore - subroutine - allora si pu scrivere semplicemente:

    return;

    Se il sottoprogramma ha specificato un tipo per il valore da restituire - funzione -, l'istruzione return deve comprendere anche un'espressione di un tipo che possa essere assegnato al tipo da restituire. Ad esempio:

    int Lunghezza(char Frase[]) { int i; i=0; while(Frase[i] != '\0') i++; return i; /* anche return(i); */ }

    L'espressione dell'istruzione return pu essere omessa, e questo ha senso solo per sottoprogrammi di tipo void, ed in tali casi l'istruzinoe pu essere omessa completamente. In tal caso, il controllo viene restituito al chiamante in corrispondenza del termine del sottoprogramma stesso.

    exit

    L'istruzione exit termina l'esecuzione dell'intero programma. Si tratta di un'istruzione simile all'istruzione return, che per restituisce il controllo al programma chiamante del programma principale, dunque termina l'esecuzione del programma. Se il programma principale non ha specificato un tipo di dato da restituire - void main- allora si scriver semplicemente:

    exit;

    In alternativa, il programma main restituisce un intero - int main - al fine di caratterizzare il termine

    del programma. Nel caso in cui il programma arrivi al termine perch concluso il flusso di elaborazione senza l'insorgere di alcun problema, si restituir, ad esempio, il valore 0, in caso contrario, se si termina l'esecuzione a causa di una qualche condizione di anomalia, si restituir un valore diverso, arbitrariamente legato alla particolare situazione di anomalia:

    exit 1;

    I sottoprogrammi

    Un sottopramma un insieme di istruzioni identificate mediante un nome, ed accessibile tramite un'interfaccia, che consente di far comunicare il sottoprogramma con il (sotto)programma chiamante.

    In termini generali ci sono due tipi di sottoprogrammi: quelli che restituiscono un valore al

    (sotto)programma chiamante, e quelli che non lo fanno. Ai primi si d il nome di funzioni, ai secondi il nome

    di procedure (o subroutine). Quindi possiamo vedere un sottoprogramma come un ambiente che

    (eventualmente) riceve delle informazioni, svolge l'elaborazione richiesta ed eventualmente restituisce un

  • valore al (sotto)programma che lo ha chiamato.

    Graficamente le due tipologie di sottoprogrammi sono riportate nella figura seguente.

    In C solitamente questa distinzione viene persa per ci che concerne la terminologia, e ci si riferisce pi

    semplicemente ai sottoprogrammi con il termine funzione, presupponendo che questa resituisca o meno

    un valore in base alle specifiche ed al comportamento che si desidera ottenere.

    Quando viene chiamato un sottoprogramma il flusso di esecuzione abbandona il programma in cui viene fatta la chiamata ed inizia ad eseguire il corpo del sottoprogramma. Quando quest'ultimo termina la propria esecuzione, con l'esecuzione dell'istruzione return o con il termine del corpo del programma, il programma chiamante continua l'esecuzione del codice che segue la chiamata al sottoprogramma.

    Una funzione una porzione di codice (sequenza di istruzioni) che vengono raggruppati e a cui viene dato un nome. Il vantaggio di questa soluzione che la porzione di codice pu essere utilizzata pi volte semplicemente scrivendone il nome.

    Si supponga di voler scrivere una funzione che scriva a video un insieme di direttive per l'utente, che costituiscono il men del programma, come fatto nello stralcio di codice riportato:

    ... printf("Scegli la voce del men:\n"); printf("1. addizione\n"); printf("2. sottrazione\n"); printf("3. moltiplicazione\n"); printf("4. divisione\n"); scanf("%d", &scelta); ...

    Per rendere la parte di codice una funzione necessario racchiudere il codice tra un paio di parentesi graffe per renderle un blocco di codice e dare un nome alla funzione:

  • menu() { printf("Scegli la voce del men:\n"); printf("1. addizione\n"); printf("2. sottrazione\n"); printf("3. moltiplicazione\n"); printf("4. divisione\n"); }

    A questo punto possibile utilizzare la funzione chiamandola:

    void main() { menu(); scanf("%d", &scelta); ... }

    Nel programma, l'istruzione menu(); equivalente ad aver scritto direttamente tutte le istruzioni della funzione stessa. A parte il semplice esempio, le funzioni hanno lo scopo di rendere un lungo programma come una collezione di porzioni di codice separate su cui lavorare in modo isolato, suddividendo la soluzione di un problema grosso in tanti piccoli sottoproblemi, di pi facile soluzione.

    Funzioni e variabili locali

    Nella funzione utilizzata per chiarire la filosofia di fondo delle funzioni non ci sono variabili e questo un caso particolarmente semplice. Una funzione una sottounit di un programma completo: il main stesso una funzione, chiamata dal sistema. Quindi anche una funzione avr in generale delle variabili che verranno utilizzate al suo interno per effettuare le computazioni desiderate.

    Le variabili dichiarate all'interno di una funzione sono significative e visibili esclusivamente all'interno della funzione stessa, ed il programma chiamante non ne ha alcuna visibilit. Si tratta di variabili locali alla funzione stessa.

    Le variabili che una funzione dichiara sono create quando la funzione viene chiamata e vengono distrutte quando la funzione termina. Si consideri il seguente esempio:

    void contastampe() { int num_stampe; num_stampe = 0; printf("xxxxxxx"); num_stampe++; } void main() { contastampe(); ... contastampe();

    LazyalexEvidenziato

  • }

    Ad ogni chiamata della funzione contastampe() la variabile num_stampe viene ricreata e distrutta al termine.

    ...

    Se si desidera quindi che un valore computato da una funzione resti disponibile anche dopo il termine dell'esecuzione della funzione necessario che questo venga trasmesso al programma chiamante. In modo analogo, se la funzione deve svolgere un'elaborazione dei dati del programma chiamante necessario che i dati le vengano passati.

    A questo scopo vengono definite variabili speciali, chiamate parametri che vengono utilizzati per passare i valori alla funzione. I parametri vengono elencati nelle parentesi tonde che seguono il nome della funzione, indicando il tipo ed il nome di ogni parametro. La lista dei parametri ha come separatore la virgola. Per esempio:

    somma(int a, int b) { int risultato; risultato = a + b; }

    Questo codice definisce una funzione chiamata somma con due parametri a e b, entrambi di tipo intero. La variabile risultato dichiarata localmente alla funzione, nel corpo della funzione. I parametri a e b vengono utilizzati all'interno della funzione come normali variabili - si noti che non devono essere definite due variabili locali a e b. Inoltre, questi a e b non hanno nulla a che fare con altre variabili a e b dichiarate in altre funzioni.

    La differenza fondamentale tra i parametri e le variabili che i primi hanno un valore iniziale quando la funzione viene eseguita, mentre le variabili devono essere inizializzate. Quindi,

    somma(l, 2);

    una chiamata alla funzione somma in cui il parametro a vale 1 e b vale 2. anche possibile far assumere ai parametri il risultato di un espressione, come ad esempio:

    somma(x+2, z*10);

    che far assumere ad a il valore pari a x+2 (in base a quanto varr x al momento della chiamata e b pari a z*10. Pi semplicemente si pu fissare il valore di un parametro al valore di una variabile:

    somma(x, y);

    in cui a assume il valore di x e b di y.

    In modo duale anche necessario o possibile trasmettere al programma chiamante il valore calcolato dalla funzione. La via pi semplice la restituzione del valore attraverso il nome della

  • funzione, ossia come se il nome della funzione fosse una variabile dotata di un valore. Il valore viene restituito per mezzo della seguente istruzione:

    return(value);

    Questa istruzione pu essere posizionata in qualunque punto della funzione, tuttavia un'istruzione return causa il termine della funzione e restituisce il controllo al programma chiamante. necessario aggiungere un'informazione relativa al tipo di dato che la funzione restituisce. Sempre con riferimento alla funzione somma, il tipo restituito un intero; si scriver dunque:

    int sum(int a, int b) { ... }

    La funzione completa :

    int sum(int a, int b) { int risultato; risultato = a + b; return (risultato); }

    La chiamata assume quindi la seguente forma:

    r = somma(1, 2);

    istruzione che somma 1 a 2 e memorizza il risultato nella variabile r.

    Ovviamente, la situazione tra ingressi ed uscite di una funzione non uguale: possibile passare un numero di parametri d'ingresso qualsiasi, mentre la funzione pu restituire mediante l'istruzione return un singolo valore. Si noti che una funzione pu avere quante istruzioni return si desidera, ma ci non consente di restituire pi di un valore in quanto quando si esegue la prima return la funzione termina. (Per poter restituire pi dati necessario utilizzare un passaggio dell'indirizzo come discusso nella Lezione 11).

    Per riassumere, una funzione ha la seguente forma sintattica:

    tipo_restituito NomeFunzione(lista tipo-nome) { istruzioni }

    Nel caso in cui una funzione non restituisca alcun parametro, il tipo indicato void, che indica appunto tale eventualit. La funzione void menu() una funzione senza paramtri d'ingresso e che non restituisce alcun valore.

    void un tipo standard ANSI C.

  • Cosa succede quando un afunzione deve restituire al chiamante pi di un valore? Per semplicit esaminiamo il caso in cui una funzione riceva in ingresso due numeri interi a e b e debba restituire al chiamante sia il valore della divisione tra a e b sia il resto. Per il momento ci si limita a capire il problema, senza trovare una soluzione, allo scopo di focalizzare l'attenzione su alcuni aspetti fondamentali del passaggio dei parametri. Si considerino i seguenti aspetti:

    1. l'istruzione return consete di restituire una sola informazione, nella fattispecie un solo numero intero;

    2. una volta eseguita un'istruzione return il codice successivo non verr senz'altro eseguito; 3. stampare a video NON significa restituire.

    Queste osservazioni ci permettono di dire che NON potremo scrivere i seguenti stralci di codice:

    int ris_resto(int a, int b) { int r1, r2; r1 = a/b; r1 = a % b; return(a, b) } int ris_resto(int a, int b) { int r1, r2; r1 = a/b; r1 = a % b; return(a); return(b); }

    Ad evidenziare ulteriormente ci che comunque dovrebbe essere di immediata comprensione, il fatto che nel codice del programma chiamante, non si saprebbe come ricevere i due valori interi restituit, visto che l'assegnamento prevede che ci sia una variabile a sinistra come destinazione dell'assegnamento e a destra dell'uguale un'espressione che ne determina il valore:

    ... int risultato, resto; ... risultato = ris_resto(val1, val2); resto = ???; ...

    Funzioni e prototipi

    Dove va scritta la definizione di una funzione, prima o dopo il main()? L'unico requisito che la tipologia della funzione (tipo di dato restituito e tipo dei parametri) sia nota prima che la funzione venga usata. Una possibilit scrivere la definizione della funzione quindi prima del main(). Con questa soluzione per necessario prestare molta attenzione all'ordine con cui si scrivono le funzioni, facendo sempre in modo che una funzione sia sempre definita prima che qualche altra la chiami. In alternativa, la soluzione po pulita dichiarare la funzione prima del main, separatamente da dove viene poi definita. Per esempio:

    int somma();

  • void main() { ... }

    Qui si dichiara il nome della funzione somma e si indica che restituisce un int. A questo punto la definizione della funzione pu essere messa ovunque.

    Per quanto riguarda i parametri ricevuti in ingresso necessario (ANSI C) dichiararne la tipologia ma non il nome, come mostrato nel seguente esempio:

    int restodivisione(int, int);

    Gli array

    Le variabili semplici, capaci di contenere un solo valore, sono utili ma spesso insufficienti per numerose applicazioni. Quando si ha la necessit di trattare un insieme omogeneo di dati esiste un'alternativa efficiente e chiara all'utilizzo di numerose variabili dello stesso tipo, da identificare con nomi diversi: definire un array, ovvero una collezione di variabili dello stesso tipo, che costituisce una variabile strutturata. L'array costituisce una tipologia di dati strutturata e statica . La dimensione fissata al momento della sua creazione - in corrispondenza alla dichiarazione - e non pu essere mai essere variata.

    Array monodimensionali

    Intuitivamente un array monodimensionale - vettore - pu essere utilizzato come un contenitore suddiviso in elementi, ciascuno dei quali accessibile in modo indipendente. Ogni elemento contiene un unico dato ed individuato mediante un indice: l'indice del primo elemento dell'array 0, l''ultimo elemento di un array di N elementi ha indice N-1. Il numero complessivo degli elementi dell'array viene detto dimensione, e nell'esempio utilizzato pari a N.

    Per riassumere, un array una struttura di dati composta da un numero determinato di elementi dello stesso tipo, ai quali si accede singolarmente mediante un indice che ne individua la posizione all'interno dell'array.

    Per ogni array, cos come per ogni variabile semplice (o non strutturata) necessario definire il tipo di dati; inoltre necessario specificarne la dimensione, ossia il numero di elementi che lo compongono. Una dichiarazione valida la seguente:

    int numeri[6]; numeri[0] numeri[1] numeri[2] numeri[3] numeri[4] numeri[5]

    Viene indicato, come sempre, prima il tipo della variabile (int) poi il nome (numeri) ed infine tra parentesi quadre la dimensione (6): l'array consente di memorizzare 6 numeri interi. Tra parentesi quadre necessario indicare SEMPRE un'espressione che sia un valore intero costante. In base a quanto detto, errato scrivere:

  • int numeri[]; int i, numeri[i];

    Il vincolo di dover specificare in fase di dichiarazione la dimensione dell'array porta ad un sovradimensionamento dell'array al fine di evitare di non disporre dello spazio necessario durante l'escuzione del programma. Nel caso in cui sia dunque richiesto l'utilizzo di un array, la specifica dell'algoritmo dovr quindi prevedere o l'esatto numero di dati da gestire oppure un valore massimo di dati. Nel caso in cui non sia possibile stabilire in alcun modo un limite al numero di elementi senza violare i requisiti di generalit, sar necessario utilizzare una struttura dati diversa e pi precisamente una struttura dinamica, trattata in seguito (Lezione 14).

    Per accedere al singolo elemento dell'array necessario indicare il nome dell'array e l'indice dell'elemento posto tra parentesi quadre, ad esempio, per accedere al primo elemento si dovr scrivere:

    numeri[0]

    Si noti che gli elementi dell'array vanno dall'elemento di indice 0 a quello di indice 5. Accedere all'elemento di indice 6 (o superiore) non causa un errore di sintassi, quanto un errore semantico (non sempre di facile individuazione). In generale il singolo elemento di un array pu essere utilizzato come una semplice variabile.

    Spesso l'array viene utilizzato all'interno di iterazioni per accedere uno dopo l'altro ai suoi elementi, semplicemente utilizzando un indice che viene modificato ad ogni iterazione. Ad esempio:

    /* Inizializzazione di un array di numeri interi al valore nullo */ for (i = 0; i < 6; i++) numeri[i] = 0;

    L'indice i inizializzato a zero consente di accedere dal principio al primo elemento dell'array e di proseguire fino all'ultimo, con indice 5 (si noti che quando l'indice pari a 6 la condizione falsa ed il corpo del ciclo non viene eseguito).

    Come per le variabili non strutturate, il contenuto di una variabile prima che le venga assegnato un valore ignoto, quindi anche per gli elementi di un array opportuno prima assegnare un valore e poi leggerlo, a meno che non si sia alla ricerca di valori casuali.

    Array bidimensionali

    Gli array bidimensionali sono organizzati per righe e per colonne, come matrici. La specifica di un array bidimensionale prevede l'indicazione del tipo di dati contenuti nell'array, del nome e delle due dimensioni, numero di righe e numero di colonne, racchiusa ciascuna tra parentesi quadre. Ad esempio, la dichiarazione che segue specifica un array bidimensionale di numeri reali, organizzati su 4 righe e 6 colonne, per un totale di 24 elementi:

    float livelli[4][6]; livelli[0][0] livelli[0][1] livelli[0][2] livelli[0][3] livelli[0][4] livelli[0][5]

  • livelli[1][0] livelli[1][1] livelli[1][2] livelli[1][3] livelli[1][4] livelli[1][5]

    livelli[2][0] livelli[2][1] livelli[2][2] livelli[2][3] livelli[2][4] livelli[2][5]

    livelli[3][0] livelli[3][1] livelli[3][2] livelli[3][3] livelli[3][4] livelli[3][5]

    L'accesso ai singoli elementi dell'array bidimensionale avviene in modo analogo a quanto avviene per gli array monodimensionali, specificando gli indici della riga e della colonna dell'elemento di interesse, ad esempio:

    livelli[2][4]

    Come esempio di scansione degli elementi di un array bidimensionale si consideri lo stralcio di codice qui riportato, in cui si accede riga per riga ad ogni elemento dell'array, mediante due cicli annidati. Il ciclo pi esterno scandisce le righe, quello pi interno le colonne.

    /* Acquisizione da tastiera dei valori della matrice livelli[4][6] */ for (i = 0; i < 4; i++) for(j = 0; j < 6; j++){ printf("Inserisci l'elemento riga %d colonna %d: ", i, j); scanf("%f", &livelli[i][j]); }

    Approfondimento

    Gli elementi vengono memorizzati per righe, quindi pi veloce accedere per righe ai dati memorizzati.

    Passaggio a sottoprogrammi

    Il passaggio di array a sottoprogrammi viene sempre fatto per riferimento, passando per valore l'indirizzo dell'array, e lasciando quindi di fatto accessibile al sottoprogramma l'array con la possibilit di modificarne il contenuto. Per questo motivo necessario prestare estrema attenzione durante l'accesso agli elementi dell'array.

    Ogniqualvolta si passa un array pluridimensionale necessario indicare - tra parentesi quadre - tutte le dimensioni dell'array ad eccezione della prima. Ne consegue che per un array monodimensionale la dichiarazione dell'array come parametro viene fatta cos:

    void funzionex(int numeri[], ...){ ... }

    Nel caso di array pluridimensionali, o pi comunemente i bidimensionali trattati in precedenza, la dichiarazione deve essere fatta come segue:

    void funzioney(int livelli[][6], ...){ ... }

    LazyalexEvidenziato

  • La specifica del numero di colonne un'informazione di servizio e non pu essere utilizzata dal programmatore come specifica del numero di colonne dell'array. In tal senso, buona norma, passare sempre, mediante ulteriori parametri, il numero di elementi dell'array monodimensionale, o il numero di righe e di colonne in array bidimensionali, come mostrato nei seguenti stralci di codice:

    void funzionex(int numeri[], int num_elem){ ... } void funzioney(int livelli[][6], int num_righe, int num_colonne){ ... }

    Si consideri a titolo di esempio, il seguente problema:

    "scrivere una funzione che restituisca l'indice dell'elemento di valor minimo di un array di numeri interi"

    Decidendo di scrivere una funzione che riceva in ingresso l'array ed il numero di elementi si arriva una

    soluzione di questo tipo:

    int MinArrayInt(int v[], int l){ int i, imin; for(i=1, imin=0; i < l; i++) if(v[i] < v[imin]) imin = i; return(imin); }

    Nel caso si decida di optare per la soluzione che non prevede il passaggio anche del numero di elementi dell'array, si scriver il codice seguente:

    int MinArrayInt(int v[]){ int i, imin; for(i=1, imin=0; i < N; i++) if(v[i] < v[imin]) imin = i; return(imin); }

    possibile fare alcune considerazioni che motivano l'opportunit di passare ad un sottoprogramma anche le dimensioni dell'array, e fanno concludere che la prima soluzione costituisce un codice migliore. Eccole.

    In primo luogo ad un sottoprogramma potrebbe venir passato solo una porzione ridotta dell'intero array. Si ricordi infatti che dovendo specificare a priori la dimensione di un array in fase di dichiarazione, spesso si deve sovradimensionare l'array onde evitare di avere problemi di memoria. Non necessariamente tutti gli elementi verrranno quindi utilizzati. In tal caso, quando si passa l'array come parametro importante specificare le dimensioni su cui lavorare, anche tenendo presente che spesso impossibile distingere in base al contenuto dell'array quali elementi siano "validi" e quali non lo siano.

    Una seconda considerazione a favore del passaggio esplicito delle dimensioni dell'array mediante opportuni paramentri, la possibilit di riutilizzare i sottoprogrammi senza doverli "aggiustare" di volta in volta. Nel primo caso, indipendentemente dalla dimensione dell'array utilizzato, il

  • sottoprogramma funziona perfettamente cos come . Nella seconda soluzione invece, sar necessario accertarsi che l'array sia effettivamente di dimensione N, e nel caso in cui ci siano array di dimensione diverse, sar necessario scrivere funzioni diverse. La prima soluzione costituisce dunque una soluzione piu' flessibile e riutilizzabile.

    Questa considerazione purtroppo non universale. Il fatto che nel passaggio di array pluridimensionali sia

    necessario specificare le dimensioni ad eccezione della prima fa s che una funzione possa essere riutilizzata

    solo per array che hanno tutti le stesse dimensioni - ad eccezione della prima!

    Infine, un'ultima considerazione legata allo sviluppo di programmi complessi da parte di pi persone. La persona che deve scrivere la funzione MinArrayInt, affinch possa scrivere la funzione deve concordare i parametri che riceve in ingresso, e comunque, se non dovesse ricevere anche l'intero che rappresenta la dimensione dell'array, dovrebbe chiedere l'informazione a chi realizzer la funzione chiamante. Ha quindi bisogno dell'informazione circa la dimensione dell'array: vale quindi la pena pensare, che ogni informazione necessaria debba essere trasmessa tramite il meccanismo dei parametri!

    I puntatori

    Una variabile un'area di memoria a cui viene dato un nome.

    int x;

    La dichiarazione precedente riserva un'area di memoria che viene individuata dal nome x. Il vantaggio di questo approccio che possibile accedere al valore memorizzato mediante il suo nome. La seguente istruzione salva il valore 10 nell'area di memoria identificata dal nome x:

    x =10;

    Il calcolatore fa accesso alla propria memoria non utilizzando i nomi delle variabili ma utilizzando una mappa della memoria in cui ogni locazione viene individuata univocamente da un numero, chiamato indirizzo della locazione di memoria. Un puntatore una variabile che memorizza l'indirizzo di una locazione di memoria, l'indirizzo di una variabile.

    Un puntatore deve essere dichiarato come qualsiasi altra variabile, in quanto anch'esso una variabile. Per esempio:

    int *p;

    la dichiarazione specifica una variabile puntatore ad un intero. L'introduzione del carattere * davanti al nome della variabile indica che si tratta di un puntatore del tipo dichiarato. Si noti che l'asterisco si applica alla singola variabile, non al tipo. Quindi:

    int *p , q;

    dichiara un puntatore ad un intero (variabile p) ed un intero (variabile q).

    Prima di poter utilizzare un puntatore necessario discutere del significato di due operatori: & e *.

    LazyalexEvidenziato

  • L'operatore & restituisce l'indirizzo di una variabile. Si consideri lo stralcio di codice seguente:

    int *p , q, n; ... p = &q; /* -1- */ n = q; /* -2- */

    L'effetto dell'istruzione memorizzare l'indirizzo della variabile q nella variabile p. Dopo questa operazione, p punta a q. La seconda istruzione copia il valore di q nella variabile n, mentre la variabile p punta alla variabile q.

    L'operatore * ha la seguente capacit: se applicato ad una variabile puntatore restituisce il valore memorizzato nella variabile a cui punta: p memorizza l'indirizzo, o punta, ad una variabile, e *p restituisce il valore memorizzato nella variabile a cui punta p. L'operatore * viene chiamato operatore di derefereziazione.

    Per riassumere:

    1. Per dichiarare un puntatore mettere * davanti al nome. 2. Per ottenere l'indirizzo di una variabile utilizzare & davanti al nome. 3. Per ottenere il valore di una variabile utilizzare * di fronte al nome del puntatore.

    Si consideri lo stralcio di codice seguente:

    int *a , b , c; b = 10; a = &b; c = *a;

    Delle tre variabili dichiarate, a un puntatore ad intero, mentre b e c sono interi. La prima istruzione salva il valore 10 nella variabile b. La seconda istruzione (a = &b) salva in a il valore dell'indirizzo della variabile a. Dopo questa istruzione a punta a b. Infine, l'istruzione c = *a memorizza il valore della variabile puntata da a (ossia b) in c, quindi viene memorizzato in c il valore di b (10).

    Si noti che a un int e p un puntatore ad un int quindi

    a = p;

    un errore perch cerca di memorizzare un indirizzo in un int. In modo analogo:

    a = &p;

    cerca di memorizzare l'indirizzo di un puntatore in un int ed altrettanto errato.

    L'unico assegnamento sensato tra un int ed un puntatore ad un int :

    a = *p;

  • Passaggio di una variabile o del puntatore alla variabile

    Si consideri a titolo d'esempio il seguente problema: si scriva una funzione che scambia il contenuto di due variabili. In linea di massima l'operazione dovrebbe essere semplice: scrivere una funzione scambia(a, b) che scambia il contenuto di a e b.

    void scambia(int a , int b) { int temp; temp = a; a = b; b = temp; printf("funz: var1 = %d var2 = %d\n", a, b); /* solo per debug*/ }

    Questo non va. Il passaggio dei parametri in C viene fatto sempre per valore, facendo una copia del valore della variabile che viene passata e su questa copia agisce la funzione. Quindi se si considera lo stralcio di codice seguente che effettua la chiamata alla funzione scritta, otterremmo un risultato diverso da quello desiderato:

    ... int x, y; x = 18; y = 22; printf("prima: var1 = %d var2 = %d\n", x, y); scambia(x, y); printf("dopo: var1 = %d var2 = %d\n", x, y); ...

    A video si ottiene:

    prima: var1 = 18 var2 = 22 funz: var1 = 22 var2 = 18 dopo: var1 = 18 var2 = 22

    La soluzione al problema passare (sempre per valore) il riferimento alla variabile, ossia il loro indirizzo cosicch la funzione possa accedere direttamente alla memoria (tramite appunto l'indirizzo) e modificarne il valore. Alla funzione vengono quindi passati gli indirizzi delle variabili; la funzione corretta dunque: int scambia(int *a , int *b); { int temp; temp = *a; *a = *b; *b = temp; printf("funz2: var1 = %d var2 = %d\n", *a, *b); /* solo per debug*/ }

    Si noti che i due parametri a e b sono puntatori e quindi per scambiare il valore necessario utilizzare gli operatori di dereferenziazione per far s che i valori delle variabili a cui puntano vengano scambiati. Infatti *a il contenuto della variabile a cui punta a.

  • Naturalmente anche la chiamata della funzione deve essere adattata: necessario passare due indirizzi e non pi due interi. Quindi, il codice il seguente:

    ... int x, y; x = 18; y = 22; printf("prima: var1 = %d var2 = %d\n", x, y); scambia(&x, &y); printf("dopo: var1 = %d var2 = %d\n", x, y); ...

    A video si ottiene:

    prima: var1 = 18 var2 = 22 funz: var1 = 22 var2 = 18 dopo: var1 = 22 var2 = 18

    che ci che si desidera.

    La regola che ogniqualvolta si passa ad una funzione una variabile il cui contenuto deve essere modificato necessario passare l'indirizzo della variabile.

    Si faccia attenzione che chiamare la funzione passando il valore della variabile invece del suo indirizzo quando il parametro dichiarato l'indirizzo causa lo scambio di contenuto di due aree casuali della memoria, provocando danni possibilmente all'intero sistema e non solo al programma in esecuzione! Non si tratta infatti di un errore sintattico ma piuttosto di un errore semantico.

    Approfondimento

    La necessit di passare l'indirizzo ad una funzine spiega anche il perch le due funzioni di I/O printf e scanf

    sono diverse. La funzione printf non modifica il valore dei suoi parametri, quindi viene chiamata con

    printf("%d", a) ma la funzione scanf modifica il valore della variabile, per memorizzarci quello appena

    acquisito, quindi viene chiamata con scanf("%d", &a).

    Puntatori e array

    In C c' uno stretto legame tra puntatori e array: nella dichiarazione di un array si sta di fatto dichiarando

    un puntatore a al primo elemento dell'array:

    int a[10];

    Infatti a equivale a &a[0]. L'unica differenza tra a e una variabile puntatore che il nome dell'array un puntatore costante: non si modifica la posizione a cui punta (altrimenti si perde una parte dell'array). Quando si scrive un'espresisone come a[i] questa viene convertita in un'espressione a puntatori che restituisce il valore dell'elemento appropriato. Pi precisamente, a[i] equivalente a *(a + i) ossia il valore a cui punta a + i. In modo analogo *(a + 1) uguale a a[1] e cos via.

    LazyalexEvidenziato

  • La possibilit di sommare 1 ad un puntatore pu essere interessante ma necessario capire esattamente cosa succede. Ad esempio, in generale un int occupa due byte, un float quattro. Quindi se si dichiare un array di int e si somma uno al puntatore all'array, il puntatore si muover di due byte. D'altra parte, se si dichiara un array di float e si somma uno al puntatore all'array, questo si muove di quattro byte. In altre parole, sommare uno ad un puntatore ad array significa farlo passare al successivo elemento, che ,dopotutto, ci che si desidera. Ci che sufficiente ricordare che l'aritmetica dei puntatori si basa su unit del tipo di dati trattato. possibile utilizzare gli operatori di incremento e decremento (++ e --) con i puntatori, ma non con il nome dell'array perch quello un puntatore costante e non pu essere modificato.

    Per riassumere:

    1. Il nome di un array un puntatore costante al primo elemento dell'array, ossia a == &a[0] e *a == a[0].

    2. L'accesso agli array mediante indici equivalente all'aritmetica dei puntatori, ossia a+i = &a[i] e *(a+i) == a[i].

    Un ulteriore punto legato a array e funzioni il passaggio di un array ad una funzione: di default si passa il puntatore all'array. Questo consente di scrivere funzioni che possono accedere all'intero array senza dover passare ogni singolo valore contenuto nell'array: si passa il puntatore al primo elemento (e, in linea di massima, il numero degli elementi presenti).

    Si consideri il seguente esempio, con le possibili implementazioni: scrivere una funzione che riempie un array di interi con numeri casuali (mediante la funzione di libreria rand())

    void main() /* stralcio di programma chiamante */ { int numeri[NMAX], i; ... riempirandom(numeri, NMAX); ... } void riempirandom(int a[] , int n) { int i; for (i = 0; i< n ; i++) a[i] = rand()%n + 1; /* rand()%n + 1 genera il valore casuale */ }

    oppure, usando l'aritmetica dei puntatori:

    void riempirandom(int *pa , int n) { int i; for (i = 0; i< n ; ++i, ++pa) *pa = rand()%n + 1; /* rand()%n + 1 genera il valore casuale */ }

    Si faccia attenzione a non inserire l'espressione pa = 0 nella prima parte del for altrimenti si punta alla cella

    di memoria di indirizzo 0, cosa che provoca un errore durante l'esecuzione quando si tenta di scriverci un

    valore! Si pu scrivere anche:

    void riempirandom(int *pa , int n)

  • { int i; for(i = 0; i< n ; ++i) *(pa+i)=rand()%n+1; /* rand()%n + 1 genera il valore casuale */ }

    oppure, ancora

    void riempirandom(int *pa , int n) { int i; for( ; i< n ; ++pa, ++i) *(pa)=rand()%n+1; /* rand()%n + 1 genera il valore casuale */ }

    Le stringhe

    possibile definire array per gestire qualsiasi tipo di dato semplice, int, float, char, .... In genere una collezione di numeri interi comoda per tenere uniti tutti i dati, che per hanno un significato proprio. Quando si parla di caratteri, invece, pu essere interessante poter manipolare l'intero insieme di caratteri appartenenti ad un array, in quanto costituiscono nell'insieme un vocabolo o un'intera frase (con i debiti spazi). Il C consente quindi di interpretare una sequenza di caratteri come una singola unit, per una pi efficiente manipolazione e vi una apposita libreria standard - string.h - con funzioni di utilit di frequente utilizzo.

    Una stringa pu includere caratteri alfanumerici ('a'...'z' 'A'...'Z' '0'...'9'), caratteri speciali come +, -, $ ed altri. La caratteristica rilevante che ad indicare il termine della sequenza di caratteri c' un carattere terminatore: '\0'. In C una stringa un array di caratteri e come tale ne eredita le propriet ed il comportamento di base. Inoltre, possibile avere ulteriori vantaggi dovuti all'esistenza del terminatore ed alla sua interpretazione.

    Si consideri la seguente dichiarazione di un array di caratteri di 20 elementi:

    char Vocabolo[20];

    possibile accedere ad ogni elemento dell'array singolarmente, come si fa per ogni altro tipo di array. inoltre possibile manipolare l'intero array come un'unica entit purch esista un carattere terminatore '\0' in uno degli elementi dell'array, che ci sia stato messo dalle funzioni di manipolazione della stringa, oppure direttamente da programma durante l'esecuzione. importante ricordarsi di dimensionare opportunamente l'array includendo un elemento anche per contenere il terminatore. Ad esempio, se un algoritmo prevede che si debba gestire vocaboli "di al pi 20 caratteri" necessario dichiarare un array di 21 elementi.

    #define DIM 20 char Vocabolo[DIM+1];

    Funzioni di manipolazione delle stringhe

    Una stringa un array di carattere e la fine dei dati validi demarcata dal carattere nullo '\0'. Nella libreria string.h sono diponibili un certo insieme di funzioni per la manipolazione delle stringhe, che realizzano operazioni di frequente utilizzo. Tra queste c' ad esempio la funzione int strlen(char[])

    LazyalexEvidenziato

    LazyalexEvidenziato

    LazyalexEvidenziato

  • che restituisce il numero di caratteri presenti nella stringa ricevuta in ingresso come parametro. Questo viene sempicemente realizzato contando il numero di caratteri dal primo fino al carattere terminatore.

    Si potrebbe pensare che fosse possibile assegnare una stringa ad un'altra, direttamente, mediante un'espressione del tipo:

    char a[l0], b[10]; b = a;

    La parte di codice non copia ordinatamente i caratteri presenti nell'array a nei caratteri dell'array b. Ci che viene effettivamente fatto far in modo che b punti allo stesso insieme di caratteri di a senza farne una copia. Quindi, modificando poi i valori di b si modificano quelli di a. Il codice seguente esemplifica quanto detto.

    char a[l0], b[10]; scanf("%s", a); /* l'utente inserisce la stringa "abcdefghij" */ b = a; printf("inizio a: %s b: %s\n" a); for(i = 0; i < 10; i=i+2) b[i] = '-'; printf("fine a: %s b: %s\n" a);

    Ci che si ottiene a video :

    inizio a: abcdefghij b: abcdefghij fine a: -b-d-f-h-j b: -b-d-f-h-j

    Per copiare il contenuto di una stringa in un'altra necessaria la funzione char * strcopy(char[],char[]) che effettivamente effettua la copia elemento ad elemento dell'array a nell'array b> fino al carattere terminatore. Ci sono numerose altre funzioni, tra cui citiamo solo l'importate funzione di confronto tra stringhe. Infatti il confronto a == b darebbe esito positivo solamente se i due array puntassero allo stesso insieme di caratteri, e non se il loro contenuto fosse identico. La funzione int strcmp(char[],char[]) confronta due stringhe e restituisce 0 se il loro contenuto identico.

    Questa realt ha effetto anche sulla inizializzazione degli array. Non possibile scrivere:

    a = "prova";

    perch a indica l'inizio dell'array ( un puntatore) mentre "prova" una stringa costante. Si pu per scrivere:

    strcopy(a,"prova")

    Le strutture

    L'array un esempio di struttura dati. Utilizza dei tipi di dati semplici, come int, char o double e li organizza in un array lineare di elementi. L'array costituisce la soluzione in numerosi casi ma non tutti, in quanto c' la restrizione che tutti i suoi elementi siano dello stesso tipo.In alcuni casi per necessario poter gestire all'interno della propria struttura un mix di dati di tipo diverso. Si

    LazyalexEvidenziato

  • consideri a titolo d'esempio l'informazione relativa al nominativo, anni e salario. Il nominativo richiede una stringa, ossia un array di caratteri terminati dal carattere '\0', l'et ed il salario richiedono interi. Con le conoscenze acquisite fino ad ora possibile solamente dichiarare delle variabili separate, soluzione non altrettanto efficiente dell'utilizzo di una unica struttura dati individuata da un unico nome: il Linguaggio C a tale scopo dispone della struct.

    Definizione di una struct

    La dichiarazione di una struct un processo a due fasi. La prima la definizione di una struttura con i campi dei tipi desiderati, utilizzata poi per definire tutte le variabili necessarie con la suddetta struttura. Si consideri il seguente esempio: si desidera gestire i dati di un insieme di persone relativamente al loro nominativo, all'et e al loro salario. Per prima cosa si definisce la struct che consente di memorizzare questo tipo di informazioni, nel seguente modo:

    struct s_dipendente { char nominativo[40]; int anni; int salario; };

    A questo punto possibile definire una variabile con la struttura appena introdotta:

    struct s_dipendente dipendente;

    La variabile si chiama dipendente ed del tipo struct s_dipendente definito precedentemente.

    La sintassi per la definizione di una struct la seguente:

    struct nome_struttura { lista dei campi (tipo-nome) };

    In seguito possibile definire variabili come segue:

    struct nome_struttura nome_variabile;

    Accesso ai campi della struttura

    Per accedere ai campi della struttura, per scrivere un valore o per leggerlo, necessario indicare il nome della variabile seguito da quello del campo di interesse, separati da un punto .. Ad esempio, per la struttura precedentemente dichiarata:

    struct s_dipendente dipendente ... dipendente.anni = 30; ... printf("%s\n", dipendente.nominativo); ...

  • Una volta individuato il campo d'interesse mediante la sequenza nome_variabile.nome_campo si ha a che fare con una variabile normale, e nel caso di dipendente.anni ad una variabile di tipo intero.

    Si noti che non deve utilizzare il nome della struttura s_dipendente, ma il nome della variabile, anche perch

    ci possono essere pi variabili con la stessa struttura. Nel caso in cui una struct contenga campi costituiti da

    altre struct si utilizzano i nomi di ogni struttura separati da punti fino a quando non si arriva al campo finale

    della struttura.

    Si consideri il seguente esempio:

    struct s_azienda { char nome[30]; ... struct s_dipendente contabile; ... };

    Per accedere ai dati del contabile si segue il percorso: azienda.contabile.nominativo.

    Per riassumere, l'aspetto significativo delle struct determinato dalla possibilit di memorizzare informazioni di natura diversa al'interno di un'unica variabile. Una struct pu essere utilizzata per integrare un gruppo di variabili che formano un'unit coerente di informazione.

    Ad esempio il Linguaggio C non possiede un tipo fondamentale per rappresentare i numeri complessi: una soluzione semplice consiste nell'utilizzare una struct e nel definire un insieme di funzioni per la manipolazione di variabili. Si consideri a tale scopo il seguente esempio:

    struct s_complesso { float reale; float immaginaria; };

    A questo punto possibile definire due variabili:

    struct s_complesso a, b, c;

    possibile effettuare operazioni di assegnamento tra i vari campi della struct come ad esempio:

    a.reale = b.reale;

    D'altra parte non si pu scrivere un'espressione del tipo c = a + b, per la quale necessario invece scrivere:

    c.reale = a.reale + b.reale; c.immaginaria = a.immaginaria + b.immaginaria;

    A questo punto potrebbe quindi essere conveniente scriversi un insieme di funzioni che effettuino le operazioni elementari sui numeri complessi da richiamare ogni volta.

  • Strutture e funzioni

    La maggior parte dei compilatori C consente di passare a funzioni e farsi restituire come parametri intere strutture. Se si desidera che una funzione possa cambiare il valore di un parametro necessario passarne il puntatore.

    struct s_complesso somma(struct s_complesso a , struct s_complesso b) { struct s_complesso c; c.reale = a.reale + b.reale; c.immaginaria = a.immaginaria + b.immaginaria; return (c); }

    Definita la funzione somma possibile chiamarla, come nel seguente esempio:

    struct s_complesso x, y, z; ... x = somma(y, z);

    Si tenga presente che il passaggio di una struct per valore pu richiedere un elevato quantitativo di memoria.

    Puntatori a strutture

    Come per tutti i tipi fondamentali possibile definire un puntatore ad una struct.

    struct s_dipendente * ptr

    definisce un puntatore ad una struttura s_dipendente. Il funzionamento pressoch inalterato:

    (*ptr).anni

    il campo anni della struttura s_dipendente a cui punta ptr, ed un numero intero. necessario utilizzare le parentesi in quanto il punto '.' ha una priorit superiore all'asterisco '*'. Di fatto l'utilizzo di puntatori a struct estremamente comune e la combinazione della notazione '*' e '.' particolarmente prona ad errori; esiste quindi una forma alternativa pi diffusa che equivale a (*ptr).anni, ed la seguente:

    prt->anni

    Questa notazione d un'idea pi chiara di ci che succede: prt punta (i.e. ->) alla struttura e .anni "preleva" il campo di interesse.

    L'utilizzo di puntatori consente di riscrivere la funzione di somma di numeri complessi passando come parametri non le struct quanto i puntatori a queste.

    void s_complesso somma(struct s_complesso *a , struct s_complesso *b , struct s_complesso *c) { c->reale = a->reale + b->reale; c->immaginaria = a->immaginaria + b->immaginaria;

  • }

    In questo caso c un puntatore e la chiamata deve essere fatta cos:

    somma(&x, &y, &z);

    In questo caso si risparmia spazio nella chiamata alla funzione, in quanto si passano i tre indirizzi invece delle strutture intere

    Array di strutture

    Uno dei punti id forza del C la capacit di combinare insieme tipi fondamentali e tipi derivati per ottenere strutture dati complesse a piacere, in grado di modellare entit dati del mondo reale. Si consideri il seguente esempio:

    struct s_automobile { char marca[50]; char modello[70]; int venduto; }; typedef struct s_automobile auto;

    Per poter gestire le informazioni di un concessionario a questo punto necessario poter dichiarare delle variabili che memorizzino i dati relativi alle automobili vendute. Si ipotizzi che ci siano al pi cento diverse combinazioni di marche e modelli da dover gestire. Nell'ambito del programma sar quindi necessario disporre di 100 elementi di tipo auto e a tal scopo verr dichiarato un array, come segue:

    void main() { auto concessionario[100]; int i; ... }

    In questo modo si dichiara un array di struct s_automobile di cento elementi. Ogni elemento dell'array ha i suoi campi marca, modello e venduto, a cui si accede come segue:

    ... printf("inserisci il nome della marca: \n"); gets(concessionario[i].marca); concessionario[i].venduto=0; ...

    I campi delle struct dei singoli elementi dell'array vengono poi trattati normalmente, in base al loro tipo (nell'esempio rispettivamente come un array di caratteri ed un intero)

  • Definizione di un nuovo tipo per le strutture

    La dichiarazione della struct per poter gestire insiemi di dati non omogenei viene spesso completata introducendo un nuovo tipo, definito appunto dall'utente, che si va ad affiancare ai tipi fondamentali del C. Si consideri il caso della struct per la gestione dei numeri complessi. Al fine di evitare di dover scrivere ogni volta che si dichiara una variabile struct s_complesso possibile definire un nuovo tipo apposito:

    typedef struct s_complesso complesso;

    In questo modo abbiamo introdotto un nuovo tipo che si affianca ad int, char, ... ce si chiama complesso ed possibile utilizzarlo nella dichiarazione di variabili, come mostrato di seguito:

    struct s_complesso { float reale; float immaginaria; }; typedef struct s_complesso complesso; ... void main() { ... int a, b; complesso x, y; }

    Frequentemente la dichiarazione del tipo mediante la typedef viene fatta concorrentemente alla dichiarazione della struct, secondo la seguente sintassi:

    typedef struct nome_struttura { lista dei campi (tipo-nome) } nome_tipo_struttura;

    La dichiarazione pi compatta. Riportata all'esempio precedente, il codice che si ottiene il seguente:

    typedef struct s_complesso { float reale; float immaginaria; } complesso;

    Strutture e liste concatenate

    Le strutture vengono utilizzate per la realizzazione del tipo elementare che costituisce l'elemento di base delle liste dinamiche concatenate, discusse nella Lezione 15.

    I File

  • Il flusso - stream

    Sebbene il linguaggio C non abbia dei metodi nativi per gestire l'ingresso/uscita su file, la libreria standard contiene numerose funzioni per un approccio efficiente, flessibile epotente. Un concetto importante in C il flusso (stream), ossia un'interfaccia logica, comune a tutti i dispositivi periferici del calcolatore; nel caso pi comune uno stream l'interfaccia logica ad un file. In base a come il C definisce il termine "file", questo pu far riferimento ad un file su disco, allo schermo, alla tastiera, ad una porta, e via discendo. Anche se questi file differiscono nella forma e nelle loro capacit, vengono visti tutti in modo analogo. Uno stream viene collegato ad un file mediante un'operazione di apertura, e dualmente lo stream viene disassociato mediante una operazione di chiusura. La posizione corrente il punto in cui si far il prossimo accesso nel file. Ci sono due tipi di stream: quelli di testo (costituiti da una sequenza di caratteri ASCII, sebbene ci possa essere una discrepanza tra il contenuto del file e lo stream) e quelli binari (utilizzati per qualiasi tipo di dato). In linea di massima ci si riferir sempre a stream di testo. Uno stream di testo composto di linee. Ogni linea ha zero o pi caratteri ed terminata da un carattere di a-capo (carattere con codice ASCII 10) che l'ultimo carattere della linea. I caratteri sono esclusivamente caratteri stampabili, il carattere di tabulazione ('\t') e il carattere a-capo ('\n').

    Per aprire un file ed associarlo ad uno stream l'istruzione da utilizzarsi la fopen(), il cui prototipo mostrato di seguito:

    FILE *fopen(char *,char *);

    La funzione fopen(), come tutte le funzioni di sistema si trova nella libreria standard di sistema stdio.h . Essa riceve in ingresso due parametri, rispettivamente il nome del file da aprire e il modo in cui aprirlo (il tipo di accesso che si desidera fare). Per quest'ultimo aspetto, le modalit consentite sono:

    Modo Significato r apre un file di testo in lettura w crea un file di testo per scriverci a apre un file in scrittura e si posiziona alla fine (appende il testo) rb apre un file binario in lettura wb crea un file binario in scrittura ab apre un file binario e si posiziona alla fine r+ apre un file di testo in lettura/scrittura w+ crea un file di testo in lettura/scrittura a+ apre o crea un file di testo in lettura/scrittura r+b apre un file binario in lettura/scrittura w+b crea un file binario in lettura/scrittura a+b apre un file binario e si posiziona alla fine per lettura/scrittura

    Se l'operazione di apertura ha successo (il file c' e si hanno i permessi corretti, se si tratta di lettura, c' spazio a sufficienza e si hanno i permessi nel caso di scrittura) l'istruzione restituisce un puntatore a file (tipo FILE*) valido. Il tipo FILE viene definito nella libreria stdio.h. Si tratta di una struttura con diverse informazioni relative al file, tra cui ad esempio la dimensione. Il puntatore al file verr utilizzato da tutte le funzioni che lavorano con i file e non va esplicitamente manipolato (incrementato, decrementato,

  • ...). Se la funzione fopen() non va a buon fine, restituisce un puntatore a NULL, condizione che va verificata prima di procedere nell'accesso al contenuto del file. Lo stralcio di codice mostra l'utilizzo della funzione fopen() e il controllo del risultato prima di procedere:

    ... FILE *fp; char NomeFile[30]; ... if ((fp = fopen(NomeFile, "r")) == NULL) printf("Errore nell'apertura del file %s\n", NomeFile); else{ ... }

    Per chiudere un file disponibile la funzione di libreria fclose(), il cui prototipo il seguente:

    int fclose(FILE *);

    La funzione riceve in ingresso come parametro il puntatore al file da chiudere: il puntatore deve essere valido, ottenuto mediante una precedente fopen(). La funzione fclose() restituisce 0 se viene eseguita con successo, altrimenti restituisce EOF (end of file, fine del file) nel caso si verifichi un errore.

    Una volta aperto un file, in base al modo in cui stato aperto, possibile leggere e/o scrivere utilizzando le seguenti funzioni:

    int fgetc(FILE *); int fputc(int , FILE *);

    La prima funzione (getc()) legge il byte successivo dallo stream indicato dal puntatore al file e lo restituisce

    come intero (il valore ASCII corrispondente al carattere); se si verifica un errore o termina il file la funzione

    restituisce EOF (situazione indicata dal carattere stesso EOF). Il valore restituito dalla funzione fget() pu

    essere assegnato ad una variabile di tipo carattere.

    La funzione duale fput() scrive un byte corrispondente al carattere ricevuto in ingresso come primo parametro nel file associato al puntatore a file indicato come secondo parametro della funzione. Sebbene il tipo del primo parametro un intero la funzione pu ricevere in ingresso un carattere (si ricordi sempre il legame tra un carattere e il suo codice ASCII). La funzione fput() restituisce il carattere scritto nel caso non vi siano problemi, altrimenti restituisce EOF.

    Funzioni per file di testo

    Le librerie standard del C mettono a disposizione quattro funzioni per semplificare le operazioni di accesso ai file in grado di gestire una maggior quantit di dati rispetto al singolo byte. Le prime due funzioni hanno i seguenti prototipi:

    int fputs(char* , FILE*); char *fgets(char* , int , FILE*);

  • Le funzioni fputs() e fgets() scrivono e leggono una stringa da un file. La funzione fputs() scrive la stringa ricevuta come primo parametro, sul file indicato dal puntatore a file (il secondo parametro). La funzione restituisce EOF nel caso in cui si verifica un errore, altrimenti un valore non negativo. Il carattere terminatore della stringa non viene scritto e non viene neppure aggiunto automaticamente un ritorno a capo.

    La funzione fget() legge una sequenza di caratteri dal file puntato dall'apposito puntatore, che costiuisce il terzo parametro. La funzione legge al pi num-1 caratteri e li memorizza nella stringa ricevuta come primo parametro. Nel caso i caratteri siano meno oppure si incontri il carattere a-capo o EOF. La stringa letta viene terminata con il terminatore '\0'. La funzioe restituisce la stringa se non si verifcano problemi, altrimenti restituisce il puntatore nullo.

    Le altre due funzioni sono fprintf() e fscanf(). Queste funzioni operano esattamente come la funzione printf() e scanf() rispettivamente, solamente che accedono a file invece che allo standard input (tastiera) e output (video). I loro prototipi sono:

    int fprintf(FILE *, char *, ...); int fscanf(FILE *, char * ...);

    Invece di ridirigere le operazioni di ingresso/uscita verso la console, queste funzioni operano sul file specificato dal puntatore al file ricevuto come primo parametro. Per il resto queste operazioni sono analoghe alle printf() e scanf(). Il vantaggio di queste due funzioni che rendono estremamente semplice scrivere una grande quantit e variet di dati su un file di testo.

    La funzione feof() restituisce un valore diverso da zero se il puntatore al file che riceve come unico parametro ha raggiunto la fine del file, altrimenti restituisce 0. Il prototipo :

    int feof(FILE *fp);

    Funzioni del file system

    possibile accedere a due funzioni del file system per eliminare un file e per riportarsi all'inizio di un file aperto senza doverlo chiudere e riaprire. I prototipi sono i seguenti:

    int remove(char *); void rewind(FILE *);

    La funzione remove() riceve in ingresso il nome del file da cancellare mentre la funzione rewind() il puntatore al file da riposizionare.

    La memoria dinamica

    Tutte le variabili dichiarate ed utilizzate nelle precedenti lezioni venivano allocate in modo statico, riservando loro spazio nella porzione di memoria denominata Stack che destinata alla memoria statica. Il compilatore vede dal tipo della variabile, al momento della dichiarazione, quanti byte devono essere allocati. Per staticit si intende che i dati non cambieranno di dimensione nella durata del programma (si ricordi il vincolo di dimensionare opportunamento un array, eventualmente sovradimensionandolo).

  • Esiste una porzione di memoria denominata Heap ('mucchio' in italiano) dove possibile allocare porzioni di memoria in modo dinamico durante l'esecuzione del programma, a fronte di richieste di spazio per variabili.

    Allocazione dinamica

    Con questo metodo di allocazione possibile allocaren byte di memoria per un tipo di dato (n sta per la

    grandezza di byte che devono essere riservati per quel tipo di dato). A questo scopo esistono specifiche

    funzioni della libreria standard (malloc e free) per l'allocazione e il rilascio della memoria. Per identificare la

    dimensione della memoria da allocare dinamicamente, si utilizza l'operatore sizeof che prende come

    parametro il tipo di dato (int, float, ...) e restituisce il numero di byte necessari per memorizzare un dato di

    quel tipo.

    Si ricordi che il numero di byte necessari per memorizzare un numero intero dipende dall'architettura del

    calcolatore e dal compilatore stesso. In generale questo valore pari a 4 byte, ma potrebbe essere

    differente su architetture diverse. Per cui, onde evitare di riservare una quantit di memoria sbagliata,

    opportuno far uso di tale funzione.

    La funzione C per allocare dinamicamente uno spazio di memoria per una variabile la seguente:

    void * malloc(size_t);

    La funzione, appartenente alla libreria standardstdlib.h riserva uno blocco di memoria didim byte dalla memoria heap e restituisce il puntatore a tale blocco. Nel caso lo spazio sia esaurito, restituisce il puntatore nullo (NULL). Quindi, per poter sfruttare la possibilit di allocare dinamicamente della memoria necessario dichiarare delle variabili che siano dei puntatori, a cui verr assegnato il valore dell'indirizzo del blocco richiesto quando si allocher della memoria. Ad esempio, per poter allocare dello spazio per una variabile intera necessario aver dichiarato una variabile puntatore ad intero e poi nel corpo del programma aver chiesto lo spazio in memoria mediante la funzionemalloc, come mostrato nel seguito:

    ... int *numx; /*-1-*/ ... numx = (int *) malloc(sizeof(int)); /*-2-*/ ... *numx = 34; /*-3-*/

    Poich la malloc restituisce un puntatore genericovoid *, viene fatto un cast esplicito al tipo intero, (int *).

    La situazione della memoria, corrispondenti ai punti -1-, -2- e-3- mostrata nella figura seguente.

  • -1- -2- -3-

    abbastanza intuitivo che possibile allocare dinamicamente tutta la memoria che si desidera (pur di non esaurire la memoria heap) per poter gestire un numero di dati qualsivoglia, non noto a priori e pur di aver dichiarato dei puntatori per poter accedere alla memoria allocata dinamicamente. Nell'esempio fatto abbiamo bisogno di una variabile puntatore ad intero per poi poter gestire la memoria allocata dinamicamente. anche possibile allocare dinamicamente un numero di byte sufficienti a contenere pi dati dello stesso tipo, ossia un array allocato dinamicamente. Si consideri il seguente stralcio di codice, che dopo aver chiesto all'utente quanti dati intende inserire, alloca dinamicamente la memoria per poi procedere nell'elaborazione

    ... int *Numeri, n; ... printf("Quanti dati si desidera inserire?"); scanf("%d", &n); /* numero di dati - omesso controllo di validit*/ Numeri = (int *)malloc(n * sizeof(int)); /* vengono allocati n * numero_byte_per_un_intero */ for(i = 0; i < n; i++){ printf("Inserisci il dato %d: " i+1); scanf("%d", &Numeri[i]); } ...

    In questo modo possibile scrivere programmi in cui non sia noto a priori il numero di dati da trattare, anche se rimane il vincolo che tale informazione debba essere prima o poi fornita al programma. Per poter gestire situazioni in cui il numero dei dati non mai conosciuto, e pu variare durante l'esecuzione in base all'elaborazione necessario utilizzare delle strutture dati opportune, che siano in grado di allocare di volta in volta la memoria necessaria: si tratta delle liste concatenate, trattate nella Lezione 15.

    Rilascio della memoria dinamicamente allocata

    Quando la memoria allocata dinamicamente non serve pi opportuno (!) liberarla, in modo tale che possa essere riutilizzata, riducendo i rischi di esaurire la memoria. La funzione free effettua questa operazione; il prototipo il seguente:

    void free(void *);

  • La memoria riceve in ingresso un parametro: il puntatore alla memoria che deve essere liberata. Una volta eseguita l'istruzione fare accesso al puntatore senza prima riassegnarlo, se non per verifare che punti a NULL causa un errore durante l'esecuzione.

    Nota size_t un tipo utilizzato per le dimensioni dei tipi in memoria. definito nella libreria stddef.h

    Le liste concantenate

    Una lista concatenata una sequenza di nodi in cui ogni nodo collegato al nodo successivo: possibile aggiungere collegare nella lista un numero qualsivoglia di nodi, eliminarli, ordinarli in base ad un qualche criterio. Si accede alla lista concatenata mediante un puntatore al primo elemento della lista, da l in poi ogni elemento punter a quello successivo. Per convenzione, l'ultimo nodo punter a NULL ad indicare il termine della lista.

    Ogni nodo della lista, oltre a m