Dispense su: Linguaggio di programmazione C a cura di Angela Scaringella e Sofia Bochicchio 1 Prefazione Questa dispensa costituisce il testo di riferimento, in aggiunta agli argomenti trattati nel libro di testo, per la preparazione all’esame di base di Informatica, sia per gli studenti di Sociologia Nuovo Ordinamento, sia per gli studenti di Goru, sia per gli studenti di Sociologia Vecchio Ordinamento. Si tratta di una introduzione elementare al linguaggio C che riporta le lezioni e le esercitazioni tenute durante il corso. E’ destinata soprattutto agli studenti che non hanno potuto frequentare il corso e permette di acquisire gli elementi iniziali della programmazione che sono richiesti per sostenere l’esame. Introduzione I linguaggi di programmazione sono alla base dell’Informatica, perché consentono di generare il software, sia quello di base (sistemi operativi), sia quello applicativo utilizzato nei vari aspetti dell’elaborazione di dati, come ad esempio operazioni eseguite da un programma, sequenze di passi eseguiti da una macchina per l’automazione di processi produttivi, controlli e protocolli di telecomunicazione, le interfacce utente, raccolta di dati da Internet, ecc. I linguaggi di programmazione occupano una posizione intermedia tra il linguaggio naturale (tipicamente inglese), comprensibile dalle persone, e quello binario (composto da sequenze delle sole cifre 0 e 1), comprensibili solo dal computer, l'unico linguaggio che pilota direttamente le unità fisiche dell'elaboratore (memoria centrale e cpu). Il linguaggio C, come qualsiasi altro linguaggio di programmazione (confrontare il Cap. 10 del libro di testo), si compone di parole e regole; le prime generate da un dato alfabeto o simboli secondo un metodo prefissato, le seconde costituiscono la portante grammaticale e sintattica del linguaggio che consente di aggregare le parole per formare frasi di senso compiuto, cioè ammissibili. I linguaggi di programmazione sono costituiti da un alfabeto, da una sintassi e da una semantica. La sintassi riguarda l’insieme di regole che devono essere rispettate per scrivere programmi corretti. Il linguaggio dei programmi C è costituito da tutte le stringhe (insiemi di caratteri o simboli) che soddisfano la proprietà di poter essere analizzate da un compilatore C, senza che venga rilevato alcun errore sintattico. La costruzione delle stringhe avviene secondo una metodologia che, in informatica è nota con il nome “Grammatiche formali”. Una grammatica formale (come introdotta da Chomsky) è un sistema che comprende: • variabili terminali, • variabili, • un insieme di regole di produzione e • un insieme di assiomi. La grammatica permette di operare su stringhe di simboli attraverso le regole di produzione che fanno passare da una stringa ad un’altra a partire dagli assiomi. Il linguaggio generato dalla grammatica è l’insieme di stringhe composte solo da variabili terminali che si possono ottenere a partire dagli assiomi operando con le regole di produzione. In seguito, ogni argomento introdotto è supportato da esempi e da esercizi svolti. 2 Relazione tra i linguaggi di programmazione e il concetto di algoritmo Si richiama il concetto di algoritmo e dalla sua rappresentazione mediante i diagrammi di flusso, questo ci permetterà di gettare una buona base sulla logica sottostante ai linguaggi di programmazione (confrontare il Cap. 1 del libro di testo). I computer, conosciuti anche con il nome di elaboratori elettronici o calcolatori o sistemi di elaborazione dati, sono strumenti d’ausilio alla risoluzione di una vasta gamma di problemi (non tutti!!, cfr cap.1, par. 1.4 del libro di testo) ed hanno la caratteristica di svolgere un gran numero di operazioni in maniera rapida e corretta. Questa capacità deriva dal fatto che sono macchine programmabili e cioè che accettano istruzioni e comandi imposti dall’uomo secondo modalità ben precise. I linguaggi di programmazione permettono di tradurre algoritmi interpretabili da un sistema di elaborazione. Un algoritmo scritto in un linguaggio di programmazione viene chiamato programma e il processo di scrittura del programma, a partire dall’algoritmo, viene chiamato codifica. Svolgere un’attività di programmazione (o, brevemente, programmare) significa quindi scrivere programmi che risolvano problemi destinati ad essere eseguiti in maniera automatica su un calcolatore. In generale, è necessario definire il problema nel modo più preciso possibile: tanto più precisa sarà la sua descrizione, tanto più facile risulterà scrivere il programma che lo risolve. E’ necessario quindi specificare in maniera rigorosa i dati noti e i risultati attesi. Esempio: Dato un importo, denominato imponibile, si deve calcolare un secondo importo, denominato lordo, derivante dall’applicazione dell’IVA al 20%. Di seguito è riportata la soluzione: 1. Si esegue la moltiplicazione imponibile*20; 2. La cifra ottenuta al passo 1 si divide per 100; 3. Il risultato cercato è dato dalla somma della cifra ottenuta al passo 2 con il valore imponibile. Dati iniziali: imponibile e aliquota IVA; risultato atteso: importo lordo. Si osservi che, dato un qualsiasi problema, i dati iniziali vengono sempre trasformati (o meglio,elaborati) al fine di produrre il risultato. Inoltre, con l’elencazione dei 3 passi precedenti, abbiamo dato origine ad un algoritmo. Si richiama la definizione di algoritmo: a) E’ una successione finita di azioni, o passi, che determinano la risoluzione di un problema. Si intuisce che, dati due algoritmi che risolvono lo stesso problema, viene preferito quello col minor numero di passi. b) Le azioni specificate devono essere univocamente interpretabili ovvero devono essere comprensibili senza ambiguità da colui che le esegue (solitamente un computer). c) Se la successione di azioni viene ripetuta in momenti diversi ma con gli stessi dati iniziali, allora deve essere restituito sempre lo stesso risultato; si dice che l’algoritmo deve essere non casuale. d) In ogni istante, la prossima azione da intraprendere deve essere univocamente determinabile a partire dall’analisi del valore di particolari dati. Esistono vari modi di rappresentare un algoritmo e precisamente: - in forma discorsiva per punti (elenco di passi); - mediante uno pseudo-linguaggio di programmazione; - in forma grafica (diagramma a blocchi ) utilizzando simboli standard. 3 Esempio_1 : Consideriamo un gioco con un dado: “una volta lanciato un dado, il giocatore vince solo se è uscito un numero n maggiore di 3, altrimenti perde. In caso di vincita verrà pagato un premio di euro 5*n ”. - In termini di algoritmo si ha la seguente successione di passi elementari: 1. Calcola un numero naturale n tra 1 e 6; 2. Se n appartiene all’insieme {1,2,3} allora vai al passo 4; 3. n appartiene all’insieme {4,5,6} ed è stata vinta la somma di euro 5*n; 4. Il gioco è terminato. - In pseudo-codice (simile al linguaggio Pascal) : program Gioco Con Dado: begin {inizio dell’algoritmo} n:=CalcolaUnNumeroNaturaleTra1e6: if n>=4 then {è uscito un numero maggiore di 3} begin premio:=n*5{calcola il premio vinto} end end {fine dell’algoritmo} - Per la forma grafica dobbiamo introdurre i simboli fondamentali dell’algoritmo. La struttura logica della soluzione di un problema può essere visualizzata mediante un diagramma a blocchi (diagramma di flusso). Simbolo terminale (Inizio e Fine) Simbolo di ingresso (Input) * Simbolo di annotazione (Commento) Simbolo di collegamento 4 Simbolo di elaborazione (Processo) Simbolo di decisione (il computer può decidere solo se vero o falso) Tornando all’ esempio_1, possiamo così graficamente rappresentare lo sviluppo dell’algoritmo: Inizio Estrai n n >= 4 ? si Hai vinto n*5 euro! n o Fine gioco Vediamo subito un altro esempio per chiarire alcuni punti. 5 Esempio_2: “Uno studente immette il suo voto: se il voto è uguale o superiore a 18 lo studente viene promosso, se è minore di 18 viene bocciato”. Inizio Leggi voto voto >=18 Si Promosso n o Bocciato Fine Da notare che non abbiamo dovuto specificare, con un altro controllo, se il voto fosse minore di 18. Se volessimo inserire più voti ? Mettiamo il caso che: “uno studente inserisca 3 voti e che voglia trovare la media.” Esempio_3: 6 Inizio somma=0; voto; voti ins=0; Leggi voto si somma= somma + voto voti_ins = voti_ins + 1 voti_ins < 3? no Media = somma / 3 Fine Questa è la logica lineare da seguire se si vuole sviluppare un algoritmo, ora siamo pronti per introdurre la storia del linguaggio C. LE BASI DELL’AMBIENTE C Il C e' stato progettato da Dennis Ritchie su UNIX, e discende dal B, ideato nel 1970 da Ken Thompson, e il B a sua volta discende dal BCPL, sviluppato da Martin Richards. Nel 1983 l'Istituto Nazionale Americano per gli Standard (ANSI) ha iniziato una standardizzazione del C, terminata nel 1989 con la definizione dell'ANSI C, che differisce di poco dal C di K&R (Kernigan & Ritchie). In seguito e' stato aggiornato secondo la filosofia della programmazione orientata agli oggetti, in questo caso si parla di C++, e il suffisso dei file solitamente e'.CPP. Il C consente la gestione di bit, byte e indirizzi di memoria, a differenza di altri linguaggi di alto livello come basic e pascal, per questo alle volte e' definito un linguaggio di medio livello, ossia piu' vicino al basso livello, ossia all'assembler. Tra l'altro ha solo 32 parole chiave, ben poche al confronto del basic, che ne ha solitamente piu' di 150. Inoltre il C e' un linguaggio portabile, ossia un listato scritto in ANSI C puo' essere compilato su ogni compilatore standard di ogni sistema operativo. I compilatori C richiedono un listato scritto in formato testo, che si puo' scrivere con un editor qualsiasi o con quello integrato nel compilatore. Il file di testo del programma, che di solito ha come suffisso “.c” e viene chiamato “programma sorgente”, viene compilato, cioè tradotto e 7 trasformato in un nuovo file chiamato “programma oggetto” (o modulo), per distinguerli l’uno dall’altro, il programma trasformato e reso comprensibile solo alla macchina assume un nuovo nome cui bisogna assegnare il suffisso .OBJ. Tutti i sistemi C consistono generalmente di tre parti: l’ambiente, il linguaggio e la libreria standard del C. I programmi scritti in C passano tipicamente attraverso 6 fasi, prima di essere eseguiti. Queste sono: editare, preelaborare, compilare, linkare (collegare), caricare ed eseguire. La prima fase consiste nella scrittura del codice (editing) in un file: questa si esegue con un programma chiamato editor. Il programmatore scrive un programma in C con l’editor e, se necessario, eseguirà delle correzioni. Il programma sarà quindi immagazzinato in un dispositivo di memoria di massa, come un disco. Il nome del file del programma C dovrà terminare con l’estensione .c. Ad esempio il blocco note, il “vi” e l’emacs sono editor largamente utilizzati sui sistemi windows e UNIX. In seguito, il programmatore immetterà il comando di compilazione del programma. Il compilatore tradurrà il programma C nel codice di un linguaggio macchina (detto anche codice oggetto). In un sistema C, prima che incominci la fase di traduzione, sarà eseguito automaticamente il programma preprocessore. Il preprocessore del C obbedisce a comandi speciali, chiamati direttive del preprocessore, con le quali si indica che sul programma dovranno essere eseguite determinate manipolazioni,prima della compilazione vera e propria. Tali manipolazioni consistono generalmente nella inclusione di altri file in quello da compilare,e nella sostituzione di simboli speciali con un testo del programma. La quarta fase è chiamata linking (collegamento). I programmi scritti in C contengono tipicamente dei riferimenti a funzioni definite altrove,per esempi nelle librerie standard o in quelle di un gruppo di programmatori che lavorano su un particolare progetto. Di conseguenza, il codice oggetto prodotto dal compilatore conterrà tipicamente dei “buchi” dovuti a queste parti mancanti. Il linker collega il codice oggetto con quello delle funzioni mancanti per produrre una immagine eseguibile (senza pezzi mancanti). La fase quinta è chiamata caricamento. Prima che possa essere eseguito,un programma dovrà essere caricato nella memoria. Questa operazione sarà eseguita dal loader (caricatore) che prenderà l’immagine eseguibile dal disco e la trasferirà nella memoria. Finalmente, sotto il controllo della sua CPU, il computer eseguirà il programma, una istruzione per volta. Quando si scrive il codice è buona abitudine indentarlo, ovvero, ad ogni istruzione si va a capo e si lascia una tabulazione orizzontale, questo permette al programmatore una più chiara lettura di quello che si sta facendo; per commentare più righe si usano i simboli: /* (Slash asterisco), per aprire il commento, e */ (asterisco slash) per chiudere; // doppio slash per commentare una sola riga. Vediamo subito un programma che visualizzi "Buon giorno!" sullo schermo: #include <stdio.h> /* Include la libreria standard, che non e' altro che un file di testo con delle strutture e macro predefinite. Questi file sono detti Header, infatti terminano con il suffisso .h */ main( ) /* Definiamo la funzione main, che non riceve nessun valore come argomento. Il programma iniziera' l'esecuzione a partire da questa funzione, che deve essere presente in ogni programma */ { /* Le istruzioni della funzione sono racchiuse tra aperta e chiusa parentesi graffa */ 8 printf(“Buon giorno!\n”); /* Chiamiamo la funzione di libreria printf che si occupa di ,visualizzare sullo schermo. Da notare che \n indica il newLine, ossia serve per andare a capo. Fondamentale: ogni istruzione termina con ; punto e virgola/ /* chiudiamo la funzione principale main( ) */ } In questo programma si notano già degli accorgimenti fondamentali. Innanzitutto la funzione main(), è la funzione principale, viene eseguita per prima ed è sempre presente in ogni programma. Inoltre si nota che gli argomenti da passare alle funzioni vanno messi tra le parentesi ( ) che si trovano dopo il nome della funzione stessa. Nel caso di main( ), non viene messo alcun argomento, infatti non richiede argomenti. La parentesi graffa aperta { apre il corpo di ogni funzione , la parentesi graffa chiusa } la chiude. La porzione di programma racchiusa in esse è anche detta blocco. Quando si chiama la funzione printf( ) della libreria <stdio.h> immettiamo come argomento il testo da stampare tra “ ” (doppi apici ), come vuole la sua sintassi. All'interno della stringa di caratteri abbiamo la sequenza di escape \n, che equivale al carattere newline, posiziona il cursore all’inizio della riga successiva. Sono disponibili altri caratteri non editabili, ad esempio \t , tabulazione orizzontale, muove il cursore alla tabulazione successiva; \r, ritorno carrello, posiziona il cursore all’inizio della riga corrente; \a, allarme, fa suonare il cicalino del sistema; \\ backslash,visualizza un carattere backslash in una istruzione printf; \’, apice singolo; \” virgolette. Dichiarazione di variabili Gli elementi di base di un programma sono le variabili e le costanti. In C le variabili devono essere dichiarate prima di essere usate e all’inizio del programma. Si possono definire molti tipi di variabili numeriche, ad esempio con int si indicano variabili che possono contenere solo numeri interi, con float variabili che possono contenere numeri con la virgola. Ecco una tabella riassuntiva dei tipi di dati definibili: Tipo Lungh. unsigned char char unsigned int short int int unsigned long long float double long double 8 bits 8 bits 16 bits 16 bits 16 bits 32 bits 32 bits 32 bits 64 bits 80 bits Range (valori minimi e massimi) 0 fino 255 -128 "" 127 0 "" 65,535 -32,768 "" 32,767 -32,768 "" 32,767 0 "" 4,294,967,295 -2,147,483,648 "" 2,147,483,647 3.4 * (10**-38) "" 3.4 * (10**+38) 1.7 * (10**-308) "" 1.7 * (10**+308) 3.4 * (10**-4932) "" 1.1 * (10**+4932) 9 Come si vede i tipi interi (char, int, long) possono essere unsigned, ossia solo positivi, in questo modo contengono il doppio di valori positivi, risparmiandosi di supportare quelli negativi. I tipi a virgola mobile, ossia float, double e long double, sono comunque signed. Se si va fuori del raggio di azione di una variabile, ad esempio se si aggiunge 100 ad una variabile int che vale gia' 32700, il risultato e' -32736! Questo per l'aritmetica in complemento a 2, che per semplificare fa ricominciare dal numero più basso quando si supera quello più alto, dato che non e' segnalato alcun errore e il risultato erroneo può essere usato, attenzione a non far eccedere mai i tipi! Per dichiarare una variabile basta scrivere il tipo seguito dal nome che decidiamo per la variabile stessa, ad esempio: int numero; float numero; /*nel caso in cui volessi inserire un numero intero */ /*nel caso in cui volessi inserire un numero con la virgola */ Nel primo caso abbiamo dichiarato una variabile di tipo intero e di nome numero. Da notare che abbiamo terminato la dichiarazione con un ;. Il nome da dare alla variabile e' a piacere, le uniche limitazioni sono: non può iniziare con un numero, ne con simboli come &,/,!,... , non si possono dare come nomi le parole chiave di comandi del C, come if, else, int, float... Il C è case sensitive, ovvero, le lettere maiuscole e quelle minuscole sono differenti in C, per cui NUMERO e' diverso da Numero o da NumeRO. Le parole chiave come printf() devono essere scritte in minuscolo, e solitamente anche le variabili sono scritte con caratteri minuscoli, nonostante si possano usare anche caratteri maiuscoli. La regola quindi sarebbe di scrivere le variabili a lettere minuscole, e se si vuole si può usare il carattere “_”. Per esempio, si può fare: int numero_1; int prova1_prova; Come assegnare un valore ad una variabile: numero = 1000; abbiamo introdotto l'operatore di assegnamento “ = ”, che assegna il valore ad una variabile. In questo caso abbiamo dato a numero il valore 1000. Da notare che scrivere: numero = 500+500; numero = (250+250)+500; è equivalente, infatti si può mettere anche un’espressione, che sarà risolta in fase di precompilazione, dato che si assegna sempre 1000. A questo punto ci manca di sapere come stampare il valore di una variabile a video con la funzione printf( ). Abbiamo già visto che per stampare un testo normale basta includerlo tra “ ” ,virgolette. Per inserire una variabile nella stringa occorre aggiungere un %d nel punto che ci interessa, e mettere il nome della variabile dopo la chiusura delle “ ”, in questo modo: printf (“Il valore di Numero e' %d.”,numero); 10 Al momento della stampa il %d sarà sostituito dal valore della variabile numero. Da notare la virgola che separa i doppi apici “ ” dalla variabile. La specifica di conversione %d è usata in una stringa di controllo del formato di una funzione printf per indicare che sarà visualizzato un intero, mentre in una stringa di controllo del formato di una funzione scanf per indicare che sarà immesso un intero. La funzione scanf della libreria standard stdio.h è utilizzata per leggere i dati della tastiera. Andiamo ora a semplificare le cose con un esempio: #include<stdio.h> main() { int num1; //dichiarazioni variabili int num2; int somma; num1=5; //inizializzazione num2=0; somma=0; printf(“Inserisci un numero:\n”); //prompt scanf(“%d”, &num2); /* legge un intero, il valore digitato sarà immesso nella posizione di memoria alla quale il nome num2 è stato assegnato */ somma=num1+num2; //assegnamento dell’addizione alla somma printf(“La somma del numero inserito da tastiera e del numero dato dal computer è : %d”,somma); //visualizza la somma return ; /* indica che il programma è terminato con successo */ } L’istruzione scanf (“%d”,&num2); utilizza scanf per ottenere un valore dall’utente. La funzione scanf prende i dati in ingresso dallo standard input che è di solito la tastiera. Questa scanf ha due argomenti: “%d” e &num2. Il primo argomento, la stringa di controllo del formato, indica il tipo di dato che dovrà essere immesso dall’utente. La specifica di conversione %d indica che il dato dovrà essere un intero ( d sta per intero decimale). Il secondo argomento incomincia con una E commerciale &, detta in C operatore di indirizzo, seguita dal nome della variabile. La E commerciale, o ampersand, quando è combinata con il nome della variabile , indica alla scanf la locazione di memoria in cui è immagazzinata la variabile num2. Il computer quindi immagazzinerà il valore della variabile num2 in quella locazione. Ogni qualvolta un valore è sistemato in una posizione di memoria, esso si sostituisce al valore contenuto in precedenza in quella locazione. Dato che questa informazione precedente sarà distrutta, il processo di scrittura in una locazione di memoria è detto scrittura distruttiva. Così come abbiamo visto per la variabile somma, prima inizializzata a 0 e poi sostituita dal valore dato dalla somma di num1 e num2. Quando il valore sarà letto da una locazione di memoria, il valore in quella locazione sarà preservato, questo processo è detto lettura non distruttiva. Per semplicità, immaginate la locazione della memoria come un cassettone, ad ogni cassetto (la dimensione dipende dal tipo della variabile) corrisponde un nome, all’interno troveremo il nostro valore. E’ buona abitudine inizializzare a 0 la variabile (come se volessimo ripulire il cassetto) per evitare risultati poco probabili durante l’esecuzione. 11 L’istruzione return 0; restituisce il valore 0 all’ambiente del sistema operativo in cui il programma è stato eseguito. In questa tabella sono riportate le specifiche di conversione per printf e scanf: Specifiche di conversione per printf per scanf Tipi di dato long double double float unsigned long int long int unsigned int int short char %Lf %f %f %lu %ld %u %d %hd %c %Lf %lf %f %lu %ld %u %d %hd %c L’aritmetica del C Gli operatori aritmetici sono: Operazione Operatore Parentesi Moltiplicazione Divisione Modulo () * / % Addizione Sottrazione + - Ordine di valutazione (priorità) Sono valutate per prime Sono valutate per seconde Sono valutate per seconde Resto di una divisione intera. Sono valutate per seconde Sono valutate per ultime Sono valutate per ultime Gli operatori di uguaglianza e relazionali: Operatori di uguaglianza == != uguale diverso (not uguale) Operatori relazionali > < >= <= maggiore minore maggiore o uguale minore o uguale 12 Tutti questi operatori, a eccezione di quello di assegnamento =, associano da sinistra a destra. L’operatore di assegnamento associa da destra a sinistra. Es.: somma = num1 + num2 ; Gli operatori di assegnamento Il C fornisce diversi operatori di assegnamento per abbreviare le relative espressioni. Per esempio l’istruzione a=a+b; può essere abbreviata con l’operazione di assegnamento addizione += come in: a+=b; L’operatore += aggiunge il valore dell’espressione alla destra dell’operatore alla variabile alla sua sinistra, immagazzinando il risultato in quest’ultima. += -= *= /= %= Addiziona Sottrae Moltiplica Divide Da il resto Gli operatori logici Gli operatori logici dell’algebra booleana (confronta Cap. 3 del libro di testo, par. 3.2) possono essere utilizzati per formare condizioni più complesse, combinando quelle semplici. Questi sono: && ( AND logico), || (OR logico) e ! (NOT logico detto anche negazione logica). (condizione 1) && (condizione 2) , la condizione sarà vera se e solo se entrambe le condizioni semplici saranno vere. (condizione 1) || (condizione 2) , la condizione sarà vera se una o entrambe le condizioni semplici saranno vere. Gli operatori di incremento ++ e di decremento - Il C fornisce anche l’operatore di incremento ++ unario e l’operatore di decremento – unario. Quindi, qualora volessi incrementare di 1 una variabile, anziché scrivere: a= a+1 o a+=1 Si può scrivere a++ ++a Incrementa la variabile a di 1 e quindi utilizza il nuovo valore di a nell’espressione in cui essa occorre. a++ Utilizza il valore corrente di a nell’espressione in cui essa occorre e quindi incrementa a di 1. --a Decrementa a di 1 e quindi utilizza il nuovo valore di a nell’espressione in cui essa occorre. a-Utilizza il valore corrente di a nell’espressione in cui essa occorre e quindi decrementa a di 1. Quando gli operatori di incremento o di decremento sono sistemati prima di una variabile, si dicono rispettivamente operatori di preincremento o di predecremento. Quando sono sistemanti dopo una variabile, si dicono rispettivamente operatori di postincremento o di postdecremento. 13 STRUTTURE DI CONTROLLO La maggior parte dei programmi richiede delle iterazioni o cicli. Un ciclo (loop) è un gruppo di istruzioni che il computer eseguirà ripetutamente, finché una certa condizione di continuazione del ciclo rimarrà vera. Tutti i programmi possono essere scritti in termini di tre sole strutture di controllo: la struttura di sequenza, la struttura di selezione, e la struttura di iterazione. Queste strutture di controllo con un ingresso e una uscita singoli semplificano la costruzione dei programmi. Le strutture di controllo possono essere attaccate l’una all’altra, collegando il punto di uscita di una di esse con quello di entrata con una successiva: accatastamento delle strutture di controllo; oppure possono essere innestate: nidificazione delle strutture di controllo. Strutture di sequenza: le istruzioni sono eseguite nell’ordine in cui sono state scritte. Strutture di selezione: sono di tre tipi: - IF , nel caso in cui una condizione sia vera eseguirà una certa azione, mentre la ignorerà nel caso sia falsa. Chiamata struttura di selezione singola. - IF/ELSE , nel caso in cui una condizione sia vera eseguirà una certa azione, nel caso in cui sia falsa eseguirà un’altra differente azione. Detta struttura di selezione doppia. - SWITCH , sceglierà ed eseguirà una tra tante differenti azioni secondo il valore assunto da una data espressione. Consiste di una serie di etichette case (caso) e di un caso opzionale default. Detta struttura di selezione multipla. Strutture di iterazione: sono di tre tipi: - WHILE , consente al programmatore di specificare che una azione dovrà essere ripetuta finchè alcune condizioni rimarranno vere. - DO/WHILE , stesso controllo del WHILE fatto però dopo aver eseguito almeno una volta il corpo del loop (ciclo). - FOR , gestisce automaticamente tutti i dettagli di una iterazione controllata da un contatore. Utilizziamo ora i diagrammi di flusso per visualizzare il funzionamento delle strutture di sequenza, selezione e ripetizione con un ingresso e una uscita singoli . Sequenza Selezione Struttura if (selezione singola) vero fa ls o 14 Struttura if/else (selezione doppia) falso vero Struttura switch (selezione multipla) vero f vero f vero f 15 Iterazione Struttura do/while vero f Struttura while vero f 16 Struttura for vero f Vediamo due tipi di iterazione: • L’iterazione controllata da un contatore; • L’iterazione controllata da un valore sentinella. L’iterazione controllata da un contatore è detta a volte iterazione definita, perchè conosciamo esattamente in anticipo il numero di volte che il ciclo sarà eseguito. L’iterazione controllata da un valore sentinella è detta a volte iterazione indefinita, perché non è noto in anticipo il numero di volte che il ciclo sarà eseguito. I valori sentinella sono utilizzati per controllare una iterazione quando il numero preciso delle iterazioni non è noto in anticipo e il ciclo include delle istruzioni che otterranno dei dati ogni volta che quest’ultimo sarà eseguito. Il valore sentinella indica la “fine dei dati” e sarà immesso dall’utente dopo che tutti i veri elementi informativi saranno stati forniti dal programma. Nella iterazione controllata da un contatore, è utilizzata una variabile di controllo per contare il numero delle iterazioni. La variabile di controllo sarà incrementata (di solito di 1) ogni volta che sarà eseguito il gruppo di istruzioni. Il ciclo terminerà quando il valore della variabile di controllo indicherà che sarà stato eseguito il numero corretto di iterazioni; a quel punto, il computer potrà continuare con l’istruzione successiva alla struttura di controllo. Una iterazione controllata da un contatore richiede: il nome di una variabile di controllo; il valore iniziale della stessa; l’incremento (o decremento) con cui la variabile di controllo sarà modificata ogni volta nel corso del ciclo; la condizione che verificherà il valore finale della variabile di controllo (ovverosia, quella che determinerà se il ciclo dovrà continuare). 17 Esempio_4 Iterazione controllata da un contatore (while): #include<stdio.h> main( ) { int contatore = 1; /* dichiarazione e inizializzazione, posso dargli un qualsiasi nome e valore */ while ( contatore < = 10 ) // condizione di iterazione { printf (“%d Buon giorno!\n”,contatore); contatore++; // incremento } return ; } Questo programma conta quante volte ( 10 nel nostro esempio) ho richiamato la funzione printf e ripete la frase : Buon giorno! ogni volta. La condizione di iterazione la posso prendere dall’esterno del programma e quindi richiederla all’utente e memorizzarla in una variabile d’appoggio. Vedremo come nel prossimo esempio. La struttura di iterazione for gestisce automaticamente tutti i dettagli di una iterazione controllata da un contatore. Riscriviamo il programma dell’esempio_4 con il for aggiungendo anche una variabile d’appoggio: Esempio_5 (for): include<stdio.h> main( ) { int i ; // contatore int appoggio = 0; // variabile d’appoggio printf (“Digita quante volte vuoi ripetere la frase Buon giorno: ”); scanf (“%d”,&appoggio); /* inizializazione, condizione di iterazione e incremento sono tutti inclusi nella intestazione della struttura for */ for (i=1; i <= appoggio; i++) { printf (“%d Buon giorno\n”,i ); } return ; } 18 Proseguiamo con altri esempi sulle strutture di controllo. Esempio_6 (if/else): “Inserire un numero e vedere se è pari o dispari.” #include<stdio.h> void main(void) /* void sta a significare che la funzione main non riceve e non restituisce alcun valore, come abbiamo già visto, si può omettere */ { int numero; // dichiaro variabile printf("Inserisci un numero:\n"); // visualizzo su schermo scanf("%d",&numero); // leggo numero if(numero%2==0) // condizione printf("Il numero inserito è pari"); else printf("Il numero inserito è dispari"); } Esempio_7 (if/else if/else): “Inserire un voto. Se il voto dello studente è >= 90 visualizza "a", altrimenti se il voto è >= 80 visualizza "b", altrimenti se il voto è >= 70 visualizza "c", altrimenti visualizza "d".” #include<stdio.h> void main(void) { int voto; printf("Inserisci voto: "); scanf("%d",&voto); if(voto>=90) printf("\n a"); /* if appeso */ /* costruzione annidata */ else if (voto>=80) printf("\n b"); /* avendo una sola istruzione non ha bisogno delle parentesi graffe*/ else if(voto>=70) printf("\n c"); else printf("\n d"); } 19 Esempio_8 (while/if/else): “ Inserisci un numero e vedere se è un numero primo.” #include<stdio.h> void main (void) { int num, i=2, div=0; // posso dichiarare ed inizializzare più variabili insieme printf("Inserisci un numero:\n"); scanf("%d",&num); while(i<num) // due strutture di controllo l’una dentro l’altra { if(num%i==0) { div=div+1; //div++; i=i+1; } else { i++; // uguale a i=i+1 } } if(div==0) printf("\n%d è un numero primo",num); else printf("\n%d non è un numero primo",num); } Esempio_9 (if/while/while): “Scrivere un programma C che legga il lato di un quadrato e lo disegni sullo schermo utilizzando degli "*".Il programma dovrà funzionare con tutti i quadrati con lati compresi tra 1 e 20”. esempio lato: 3 *** *** *** se lato < 1 o lato > 20 visualizzare "errore" #include<stdio.h> void main(void) { int lato,contatore1,contatore2; printf("Inserisci il lato del quadrato: "); scanf("%d",&lato); if((lato>=1) && (lato<=20)) /* && sta per l’operatore and, se entrambe le condizioni sono vere il programma seguita la lettura nella funzione */ { contatore1=0; while(contatore1<lato) { printf("\n*"); 20 contatore1++; contatore2=1; while(contatore2<lato) { printf("*"); contatore2++; } } } else printf("\nErrore!"); } Esempio_10 (switch) “Scrivere un programma che calcoli l'area del quadrato, del rettangolo, del triangolo e del cerchio.” #include<stdio.h> void main (void) { int figura; float base, altezza, area, raggio, p=3.14159; printf("Per calcolare l'area del:\n quadrato digita 1\n rettangolo digita 2\n triangolo digita 3\n cerchio digita 4"); printf("\n\n Digita numero: "); scanf("%d",&figura); switch(figura) { case 1: printf("Inserisci base: "); scanf("%f",&base); area=base*base; break; //esce dallo switch case 2: printf("Inserisci base e altezza separati da spazio: "); scanf("%f %f",&base,&altezza); area=base*altezza; break; case 3: printf("Inserisci base e altezza separati da spazio: "); scanf("%f %f",&base,&altezza); area=(base*altezza)/2; break; case 4: printf("Inserisci il raggio: "); scanf("%f",&raggio); area=raggio*raggio*p; break; 21 default: printf("Che ho detto? o 1 o 2 o 3 o 4...Guarda che hai inserito!"); } printf("L'area della figura geometrica che hai scelto è: %.4f ",area); // %.4f mi visualizza un numero float con solo 4 cifre dopo la virgola } Le istruzioni break e continue sono utilizzate per alterare il flusso di controllo. L’istruzione break, qualora sia eseguita in una struttura while, for, do/while o switch, provocherà l’uscita immediata da quella struttura. L’esecuzione del programma continuerà con la prima istruzione successiva alla struttura. L’utilizzo tipico dell’istruzione break è per anticipare l’uscita da un ciclo, oppure per ignorare la parte rimanente di una struttura switch. In assenza del break sarebbero eseguiti insieme tutti i case di una istruzione switch. Qualora non sia stata riscontrata nessuna occorrenza, sarà eseguito il caso default e sarà visualizzato un messaggio di errore. Ogni case può contenere una o più azioni. La struttura switch è differente da tutte le altre strutture, poiché in un case di switch non sono necessarie le parentesi graffe intorno alle azioni multiple. Qualora sia eseguita in una struttura while, for o do/while, l’istruzione continue farà in modo che quelle rimanenti nel corpo della struttura siano ignorate e che sia eseguita l’iterazione successiva del ciclo. Esempio: for ( x=1; x<=10; x++) { if ( x ==5) continue; /* senza parentesi per la if perché contiene un’unica istruzione */ printf (“%d ”,x); } Il risultato sarà: 1 2 3 4 6 7 8 9 10 Se avessi messo break al posto di continue il risultato sarebbe stato: 1234 Il C fornisce l’operatore condizionale ( ? : ) che è strettamente correlato con la struttura if/else. Quello condizionale è l’unico operatore ternario del C, in altre parole, accetta tre operandi. Gli operandi, insieme all’operatore condizionale, formano una espressione condizionale. Il primo operando è una condizione, il secondo operando è il valore che assumerà l’intera espressione condizionale, qualora la condizione sia vera, mentre il terzo operando è il valore che assumerà l’intera espressione condizionale, qualora la condizione sia falsa. Per esempio l’istruzione printf printf ( “%s”, voto >= 18 ? “Promosso” : “Bocciato”); contiene un’espressione condizionale che restituirà la stringa letterale Promosso, qualora la condizione voto >= 18 risulti vera, mentre restituirà la stringa letterale Bocciato, qualora la condizione risulti falsa. La specifica di conversione %s serve per visualizzare una stringa di caratteri. I valori in una espressione condizionale possono anche essere delle azioni da eseguire. Otteniamo lo stesso risultato se scriviamo: voto >= 18 ? printf (“Promosso”) : printf (“Bocciato”); 22 LE FUNZIONI Il modo migliore per sviluppare e amministrare un programma corposo è di costruirlo partendo da pezzi più piccoli o moduli ognuno dei quali sia più maneggevole del programma originale. Questa tecnica è detta dividi e conquista (divide et impera). I moduli in C sono chiamati funzioni. Le funzioni possono essere: predefinite, quelle “preconfezionate” disponibili nella libreria standard del C (es. #include<stdio.h> , file già compilato, #include<math.h>,…); o definite dall’utente. Le funzioni sono invocate da una chiamata di funzione. Questa specifica il nome della funzione e fornisce delle informazioni (gli argomenti ) di cui la funzione chiamata ha bisogno. Un capo (la funzione chiamante) chiede a un operaio (la funzione chiamata) di eseguire un compito e tornare indietro a riferire, quando il lavoro sarà stato eseguito. Il prototipo di funzione indica al compilatore il tipo del dato restituito dalla funzione, il numero dei parametri che quella funzione si aspetta di ricevere, il tipo dei parametri e l’ordine in cui questi sono attesi. Il compilatore utilizzerà i prototipi per convalidare le chiamate di funzione. Il formato di una definizione di funzione è: tipo_del_valore_di_ritorno nome_della_funzione (lista_dei_parametri) { dichiarazioni; istruzioni; return; } Esempio_11: “Trovare la radice quadrata di un numero chiesto all’utente. Utilizzare una funzione predefinita contenuta nella libreria matematica.” #include<stdio.h> #include<math.h> /* includo il file di intestazione matematico, utilizzando la direttiva del preprocessore #include<math.h>, quando utilizzo le funzioni della libreria matematica. */ void main (void) { double x=0; printf("Inserisci il valore: "); scanf("%lf",&x); printf("La radice quadrata di %.2f è %.2f",x , sqrt(x)); /* chiamo la funzione all'interno della printf , in alternativa avrei potuto dichiarare un’altra variabile, assegnarle il risultato della radice quadrata e poi visualizzarla a video. */ } 23 Esempio_12: “Trovare il quadrato di un numero chiesto all’utente. Utilizzare una funzione personalizzata.” #include<stdio.h> int funzione_1 (int valore); /* prototipo di funzione. Nel prototipo posso omettere il nome della variabile che vado a passare, avrei potuto scrivere: int funzione_1 (int);*/ main( ) { int numero=0; /* numero e risultato sono variabili locali. int risultato=0; printf("Inserisci un numero:\n"); scanf("%d",&numero); risultato=funzione_1(numero); /*chiamo la funzione passandogli un argomento, la variabile “numero”, il valore intero di ritorno lo assegno alla variabile d’appoggio “risultato” */ printf(" %d elevato al quadrato è %d", numero, risultato); return; } int funzione_1 (int valore) { int num; /* Se chiudessi la funzione con ; il programma non leggerebbe il blocco di istruzioni. */ /* Nota: non ho bisogno di inizializzare la varibile “valore” Potrei utilizzare lo stesso nome della variabile numero, ma è bene capire che sono due variabili distinte! */ num=valore*valore; /* avrei potuto ottimizzare la scrittura del codice con un’unica istruzione: return valore * valore; senza così utilizzare la variabile d’appoggio “num”. */ return num; } Posso scegliere un qualsiasi nome per la funzione. Il prototipo di funzione: int funzione_1 (int valore); sta ad indicare che la funzione_1 riceverà come argomento una variabile di tipo int e che mi restituirà un’altra variabile di tipo int. Se alla funzione non passassi alcun argomento e se non mi restituisse alcun valore scriverei: void funzione_1 (void); Tutte le variabili dichiarate nelle definizioni di funzione sono variabili locali : esse sono note soltanto in quella in cui sono state definite. Quelle dichiarate all’esterno delle funzioni, anche dal main, sono dette variabili globali : sono note a tutto il programma. 24 Esempio_13: Vedo come varia il valore di una variabile utilizzando due diverse funzioni. #include<stdio.h> void funz1 (int); //prototipo di funzione int funz2 (int, int); //prototipo di funzione void main (void) //funzione principale { int num; num=999; printf("Sono nel main.num vale %d\n",num); funz1(num); //chiamata di funz1, passo un solo argomento num=funz2(10,12); // chiamata di funz2, passo 2 argomenti (valori) da me scelti. printf ("Sono di nuovo nella funzione main! num vale %d",num); } int funz2 (int a, int b) //funz2 { printf("Sono nella funzione 2. a=%d e b=%d\n",a,b); return a+b; } void funz1 (int x) //funz1 { printf("Sono nella funzione 1. num vale %d\n",x); } Invocare le funzioni: chiamata per valore e per riferimento Esistono due modi per invocare le funzioni: la chiamata per valore e la chiamata per riferimento. Quando gli argomenti saranno passati in una chiamata per valore, sarà preparata una copia dei loro valori e questa sarà passata alla funzione chiamata. Le modifiche effettuate alla copia non interessano il valore originale della variabile definita nel chiamante. Quando un argomento sarà passato in una chiamata per riferimento, il chiamante consentirà effettivamente alla funzione chiamata di modificare il valore originale della variabile. Le classi di memoria Per dichiarare una variabile abbiamo sempre utilizzato il nome, il tipo e il valore, questi sono detti attributi. Una variabile è una locazione (posizione) della memoria in cui un valore può essere immagazzinato perché possa essere utilizzato da un programma . Un nome di variabile in C è qualsiasi identificatore valido. Un identificatore è una serie di caratteri,comprendente lettere,numeri e caratteri di sottolineatura (_),che non comincia con un numero. Un identificatore può avere qualsiasi lunghezza, ma soltanto i primi 31 caratteri devono essere riconosciuti dal compilatore C, in accordo con lo standard dell’ 25 ANSI C. Il C è case sensitive,ovvero, le lettere maiuscole e quelle minuscole sono differenti in C, perciò a1 e A1 sono identificatori distinti. Ora utilizzeremo gli identificatori anche come nomi per le funzioni definite dall’utente. In realtà, ogni identificatore in un programma ha anche altri attributi che includono: la classe di memoria, la permanenza (o durata) in memoria, la visibilità e il collegamento. Il C fornisce quattro classi di memoria indicate dalle specifiche di classe di memoria: auto, register, extern e static. La permanenza in memoria di un identificatore è il periodo durante il quale quell’identificatore esiste in memoria. Alcuni identificatori esistono per un tempo limitato, altri sono creati e distrutti ripetutamente, mentre altri ancora esistono durante l’intera esecuzione del programma. La visibilità di un identificatore è il punto del programma in cui si può far riferimento all’identificatore. Il collegamento di un identificatore determina se un identificatore sia noto solo in quello corrente o in qualsiasi altro file sorgente che abbia le dichiarazioni appropriate. Le quattro specifiche di classe di memoria possono essere suddivise in due tipi di permanenza: la permanenza automatica in memoria e la permanenza statica in memoria. Le parole chiave auto e register sono utilizzate per dichiarare variabili con permanenza automatica in memoria, queste ultime saranno create ogniqualvolta si entrerà nel blocco in cui sono state dichiarate,esisteranno finchè quello resterà attivo e saranno distrutte quando si uscirà dallo stesso. Le variabili locali hanno per default una permanenza automatica nella memoria. I dati per i calcoli e le altre elaborazioni,nella versione in linguaggio macchina di un programma, sono caricati normalmente nei registri. La specifica di classe di memoria register potrà essere inserita prima della dichiarazione di una variabile automatica,per suggerire al compilatore di conservare la variabile in uno dei registri hardware ad alta velocità del computer. Esistono due tipi di identificatori con permanenza statica nella memoria: gli identificatori esterni (come le variabili e i nomi di funzione globali) e le variabili locali dichiarate con la specifica di classe di memoria static. Le variabili e i nomi di funzioni globali sono definiti per default con la classe di memoria extern. La visibilità di un identificatore è la porzione del programma in cui quello potrà essere oggetto di riferimenti. I quattro tipi di visibilità possibili per un identificatore sono: prototipo di funzione, visibilità nella funzione, visibilità nel file, visibilità nel blocco e visibilità nel prototipo di funzione. Generazione di numeri casuali L’elemento della casualità può essere introdotto nelle applicazioni per computer utilizzando la funzione rand della libreria standard del C. L’istruzione: i = rand ( ) ; genera un numero compreso tra 0 e RAND_MAX (costante simbolica definita nel file di intestazione <stdlib.h> ). Lo standard dell’ANSI stabilisce che il valore di RAND_MAX debba essere almeno 32767,che è anche il valore massimo per un intero di due byte. Il prototipo per la funzione rand può essere ritrovato in <stdlib.h>. Utilizzeremo l’operatore modulo (%) in congiunzione con rand nel modo seguente: rand ( ) % 6 ; per generare degli interi compresi nell’intervallo da 0 a 5. Questa operazione si chiama riduzione in scala,mentre il numero 6 è detto fattore di scala. 26 La funzione rand genera in realtà dei numeri pseudocasuali. Richiamando ripetutamente rand, si produrrebbe una sequenza di numeri che sembra essere casuale. In realtà, la sequenza si ripeterà ogni volta che il programma sarà eseguito. Una volta che il programma sarà stato completamente messo a punto,potrà essere condizionato in modo da produrre una sequenza diversa di numeri casuali ad ogni sua esecuzione. Questa operazione è detta in gergo randomizzazione, ed è eseguita con la funzione srand della libreria standard. La funzione srand riceve un argomento intero unsigned e insemina la funzione rand, in modo da indurla a generare una diversa sequenza di numeri casuali ad ogni esecuzione del programma. ( Un unsigned int di due byte può contenere soltanto valori positivi compresi nell’intervallo da 0 a 65535.) L’istruzione: srand ( time ( NULL ) ) ; porta il computer a leggere il suo orologio interno per ottenere automaticamente un valore per il seme. La funzione time restituisce l’ora corrente del giorno espressa in secondi. Questo valore sarà convertito in un intero senza segno e utilizzato come seme per il generatore di numeri casuali. La funzione time riceverà un argomento NULL (normalmente time è in grado di fornire al programmatore una stringa che rappresenta l’ora del giorno; il NULL disabilita appunto questa capacità per una chiamata specifica di time). Il prototipo di funzione per time è in <time.h>. (esempio: dado.c) E se volessimo finalmente scrivere il nostro esempio_1 in linguaggio C? #include<stdio.h> #include<stdlib.h> #include<time.h> main( ) { // librerie standard del C // funzione principale // apro blocco istruzioni int numero; srand(time(NULL)); numero=1+rand( )%6; // dichiaro la variabile int /* Con l’aggiunta di +1 estraggo un numero casuale da 1 a 6 */ printf(“E’ uscito il numero %d\n”,numero); if(numero>=4) /* struttura di controllo. Tra parentesi c’è la condizione da rispettare. if, simbolo di decisione: se il numero che io utente ho inserito è > o = 4 la lettura del codice seguìta all’interno del blocco istruzioni e salta la else */ { printf(“Hai vinto %d*5 euro”,numero); /*In questo caso abbiamo una sola istruzione*/ } else /* Altrimenti, implica quindi che il numero è < di 4, salta la if e seguìta la lettura dell’else */ { printf(“Non hai vinto!”); } 27 } printf(“Fine gioco!”); /*A fine programma visualizzerà la scritta: Fine gioco! */ return ; // Fine programma I VETTORI Un vettore è un gruppo di posizioni (o locazioni) di memoria correlate dal fatto che tutte hanno lo stesso nome e tipo di dato.Il primo elemento di ogni vettore è l’elemento zero. I vettori sono entità statiche, giacchè manterranno le proprie dimensioni durante l’esecuzione del programma. Il numero di posizione contenuto all’interno delle parentesi quadre è più formalmente noto come indice. Un indice deve essere un intero o un’espressione intera. DICHIARAZIONE DEI VETTORI int vett[10]; questa riga di istruzioni specifica il tipo elementare di ogni elemento della struttura dati di tipo vettore e il numero richiesto per la grandezza del vettore in modo tale che il computer possa riservare l’appropriata quantità di memoria. Invece la sequente riga di istruzioni: int vett[ ]={2,4,7,4,8}; creerà un vettore di tipo int di nome vett di 5 elementi. int vett[SIZE]; Nella dichiarazione posso utilizzare la costante simbolica SIZE , direttiva del preprocessore #define. Una costante simbolica è un identificatore che sarà sostituito con il testo di sostituzione dal preprocessore C, prima che il programma sia compilato.Utilizzare le costanti simboliche per specificare le dimensioni dei vettori renderà i programmi più scalabili. Una costante simbolica non è una variabile, il compilatore non le riserva nessuno spazio di memoria. int vett[5]={1,2,3,4,5,6}; provocherà un errore di sintassi. INIZIALIZZARE A ZERO GLI ELEMENTI DI UN VETTORE Tre modi: int v[10]={0,0,0,0,0,0,0,0,0,0}; int v[10]={0}; for(i=0;i<10;i++) v[i]=0; Esempio_14: “Chiedere all'utente 10 valori interi, il programma deve visualizzare il max.” #include <stdio.h> void main (void) { int i, vettore[10], max = 0; for(i=0;i<10;i++) 28 { printf("Inserisci un numero: "); scanf("%d",&vettore[i]); if(vettore[i]>max) max=vettore[i]; } printf("Il maggiore è %d",max); } Esempio_15: “Tabellina del 2.” #include<stdio.h> #define N 10 main() { int v[N]={0},i; for(i=0;i<N;i++) v[i]=2+2*i; printf("%s%13s\n","Elemento","Valore"); for(i=0;i<N;i++) printf("%7d%13d\n",i,v[i]); return; } Come esercizio, si potrebbe chiedere all’utente di introdurre un qualsiasi numero e poi visualizzare la tabellina. Esempio_16: “Scrivere un programma che chieda all’utente di introdurre 10 valori. Visualizzare prima tutti i valori pari e poi tutti i valori dispari.” #include<stdio.h> main() { int v[10]={0},i; printf("Digita 10 numeri:\n"); for(i=0;i<10;i++) scanf("%d",&v[i]); printf("Valori pari:\n"); for(i=0;i<10;i++) if(v[i]%2==0) printf("%d ",v[i]); 29 printf("\nValori dispari:\n"); for(i=0;i<10;i++) if(v[i]%2!=0) printf("%d ",v[i]); return; } Esempio_17: “Scrivere un programma che chieda all’utente di introdurre 10 valori. Visualizzare prima tutti i valori di posizione pari e poi tutti i valori di posizione dispari.” #include<stdio.h> main() { int v[10]={0},i; printf("Digita 10 numeri:\n"); for(i=0;i<10;i++) scanf("%d",&v[i]); printf("Valori di posizione pari:\n"); for(i=0;i<10;i+=2) printf("%d ",v[i]); printf("\nValori di posizione dispari:\n"); for(i=1;i<10;i+=2) printf("%d ",v[i]); return; } PASSARE I VETTORI ALLE FUNZIONI Indicherete il nome del vettore senza parentesi quadre per passare ad una funzione un argomento di quel tipo. Per esempio, se il vettore vett fosse stato dichiarato come int vett[10]; l’istruzione per la chiamata della funzione somma(vett,10); passerebbe il vettore vett, il nome, e la sua dimensione alla funzione somma. Quando si passa a una funzione un vettore, spesso sarà passata anche la sua dimensione, in modo che la funzione possa elaborare il numero specifico degli elementi inclusi nel vettore. Il C passa i vettori alle funzioni utilizzando automaticamente una chiamata per riferimento simulata: la funzione chiamata potrà modificare i valori degli elementi inclusi nel vettore originale del chiamante. Il nome del vettore è in realtà l’indirizzo del suo primo elemento! vett e &vett[0] hanno l’indirizzo equivalente. 30 Di conseguenza, quando all’interno del suo corpo la funzione chiamata modificherà gli elementi del vettore, essa starà modificando effettivamente quelli del chiamante direttamente nelle loro locazioni di memoria originarie. funz( vett[3]); così ho passato un solo elemento del vettore ad una funzione. Il C fornisce lo speciale qualificatore di tipo const per prevenire, all’interno di una funzione, la modifica dei valori contenuti in un vettore. Quando un parametro di tipo vettore sarà preceduto dal qualificatore const, i suoi elementi diventeranno delle costanti nel corpo della funzione e ogni tentativo di modificarli, in quel contesto, provocherà un errore in fase di compilazione. Esempio_18: “Inserisco 10 numeri e trovo il massimo.” Stesso esercizio dell’esempio_14 ma utilizzando una funzione per trovare il max. #include<stdio.h> #define N 10 int maxvet (int v[ ]); main() { int i,vet[N]; printf("Inserisci %d numeri:\n",N); for(i=0;i<N;i++) scanf("%d",&vet[ i ]); printf("Il massimo è : %d",maxvet(vet)); return; } int maxvet (int v[ ]) { int j, max =0; for(j=0;j<N;j++) { if(v[ j ]>max) max=v[ j ]; } return max; // restituisco un intero } Esempio_19: “Cerco un valore all’interno di un vettore di 10 elementi.” #include<stdio.h> #define N 10 void inserimento (int vet[ ]); void cerca (int vettore[ ], int numero); void stampa (int vettoreee[ ]); main() { int v[N],num=0; inserimento(v); 31 printf("\nDigita un numero:"); scanf("%d",&num); cerca(v,num); stampa(v); return; } void inserimento (int vet[ ]) { int i; printf("Inserisci %d numeri\n",N); for(i=0;i<N;i++) scanf("%d",&vet[i]); } void cerca (int vettore[ ], int numero) { int i,f=0; for(i=0;i<N;i++) if(vettore[i]==numero) { f=1; printf("Il numero %d è nella posizione %d\n",numero,i+1); } if(f==0) printf("Il numero %d non è nel vettore\n",numero); } void stampa (int vettoreee [ ]) { int i; for(i=0;i<N;i++) printf("%d\n",vettoreee[i]); } La Struttura Dati MATRICE I vettori in C possono anche avere più di un indice. Un utilizzo tipico dei vettori multidimensionali è la rappresentazione di tabelle di valori formate da informazioni organizzate in righe e colonne. Per identificare un particolare elemento della tabella, dovremo specificare due indici: il primo identificherà per convenzione la riga dell’elemento, mentre il secondo ne identificherà la colonna. Le tabelle o i vettori che per identificare un particolare elemento richiedono due indici sono detti vettori bidimensionali o matrici. Osservate che i vettori multidimensionali possono avere più di due indici. Lo standard ANSI stabilisce che i sistemi ANSI C debbano supportare per i vettori un minimo di 12 indici. Colonna 0 Riga 0 M[0][0] Riga 1 M[1][0] Riga 2 M[2][0] Colonna 1 M[0][1] M[1][1] M[2][1] Colonna 2 M[0][2] M[1][2] M[2][2] Colonna 3 M[0][3] M[1][3] M[2][3] 32 Matrice M con 3 righe e 4 colonne, matrice 3 per 4. Esempio_20: /* MATRICE */ #include<stdio.h> #define R 4 #define C 6 /* righe */ /* colonne */ void riempiMx (int Mat[R][C]); /* utilizzo le funzioni */ void stampaMx (int Matrice[R][C]); void main (void) { int Mx[R][C]={0}; riempiMx(Mx); stampaMx(Mx); } /* dichiaro e inizializzo matrice */ /* chiamata funzione */ void riempiMx (int Mat[R][C]) /* inserisco i valori nella matrice */ { int i,j; printf("Inserisci valori:\n"); for(i=0;i<R;i++) /* utilizzo un doppio ciclo for */ for(j=0;j<C;j++) scanf("%d",&Mat[i][j]); } void stampaMx (int Matrice[R][C]) /* stampo i valori */ { int i,j; for(i=0;i<R;i++) { printf("\n"); /* va a capo dopo aver visualizzato una riga */ for(j=0;j<C;j++) printf("%4d",Matrice[i][j]); } } La Struttura Dati PUNTATORE I puntatori sono delle variabili che come valore contengono degli indirizzi di memoria. Una variabile contiene direttamente un valore specifico; un puntatore, invece, contiene l’indirizzo di una variabile nella quale è immagazzinato quel valore specifico. 33 Es.: num 7 num, nome variabile, fa direttamente riferimento ad una variabile il cui valore è 7. numPtr X numPtr fa indirettamente riferimento ad una variabile il cui valore è 7. Per questo motivo il riferimento a un valore per mezzo di un puntatore è detto deriferimento. DICHIARAZIONE int num, *numPtr ; * (asterisco) indica che la variabile dichiarata sarà un puntatore. Possono essere dichiarati in modo da far riferimento a oggetti di qualsiasi tipo di dato. Posso inizializzare il puntatore nella dichiarazione o con una istruzione di assegnamento, come una variabile normale. & operatore di indirizzo, restituisce l’indirizzo del suo operando int y = 5 ; int *yPtr ; yPtr = &y ; assegnerà al puntatore yPtr l’indirizzo della variabile y * operatore di deriferimento o operatore di risoluzione del riferimento, restituisce il valore dell’oggetto puntato dal suo operando ( cioè dal puntatore ) printf ( “ %d “, *yPtr ) ; visualizzerà il valore della variabile y, 5. Esempio_21: “Inserisco un numero e lo modifico mediante un passaggio alla funzione del suo puntatore:” #include<stdio.h> void modifico (int *); // riceve come argomento un puntatore main() { int a; printf("Inserisci un valore intero:\n"); scanf("%d",&a); modifico(&a); // chiamo la funzione e le passo un puntatore printf("Il valore di a è = %d\n ",a); /* ho modificato la variabile a senza che la funzione mi restituisse un valore */ return; } void modifico (int *x) { if(*x>10) (*x)-=5; /* se il valore è > di 10 gli sottraggo 5 (numero preso a 34 else caso */ // se il valore è < di 10 gli addiziono 5 (*x)+=5; } La Struttura Dati STRINGA Nel linguaggio C una stringa è in realtà un vettore di caratteri che termina con il carattere nullo (‘\0’). Per accedere a una stringa si utilizzerà un puntatore che farà riferimento al suo primo carattere. Il valore di una stringa corrisponde all’indirizzo del suo primo carattere. Ne consegue che nel linguaggio C è corretto affermare che una stringa è un puntatore: in effetti, è un puntatore al primo carattere della stringa. In questo senso le stringhe sono come i vettori, poiché anche questi sono puntatori al loro primo elemento. Una stringa potrà essere utilizzata in una dichiarazione per inizializzare un vettore di caratteri: char colore [ ] = “rosso”; uguale è scrivere: char colore [ ] = {‘r’,’o’,’s’,’s’,’o’,’\0’}; // i caratteri sono tra apici singoli Assicurarsi che il vettore sia grande a sufficienza per immagazzinare la stringa e il carattere NULL di terminazione. La dichiarazione creerà il vettore colore di 6 elementi. char colore [6]; FUNZIONI PER LA MANIPOLAZIONE DELLE STRINGHE TRATTE DALLA LIBRERIA PER LA GESTIONE DELLE STRINGHE Libreria: <stdlib.h> strcpy ( s1 , s2 ) strcat ( s1 , s2 ) strcmp ( s1 , s2 ) copia la stringa s2 nella stringa s1 accoda s2 a s1 confronta le stringhe s1 e s2 La funzione restituirà 0 se sono uguali < 0 se s1 è minore di s2 > 0 se s1 è maggiore di s2 N.b. Per comprendere < o > si consideri il processo di sistemazione in ordine alfabetico (es. per cognomi ). strlen ( s ) determina la lunghezza della stringa s. Restituisce il numero di caratteri che precedono il NULL di terminazione. strerror ( nomestringa ) traduce in una stringa di testo dipendente dal sistema printf ( “ %s \n “ , strerror ( Sbagliato! ) ; visualizzerà: Error Sbagliato! 35 Nel caso di strncpy, strncat, strncmp posso copiare, accodare, comparare un massimo di ‘ n ‘ caratteri dalla stringa s2 nel vettore s1. Es.: strncpy ( s1 , s2 , 4 ) ; Per strcpy s1 deve avere uguale o maggiore dimensione di memoria di s2. Dopo aver copiato una stringa bisogna aggiungere il carattere nullo di terminazione s1 [ strlen ( s1 ) ] = ‘\0’ ; Esempio_22: “Chiedo all’utente di inserire delle parole, il programma termina se si inserisce la parola "fine" e visualizza le stringhe fino ad ora inserite.” #include<stdio.h> #include<string.h> main() { char s[5]="fine"; char s1[20]=" ",s2[20]=" ",s3[200]=" "; //dichiaro ed inizializzo a 0 tre stringhe printf("Digita basta per terminare\n\n"); printf("Inserisci una parola:\n"); gets(s1); //stessa funzione dello scanf, legge in input. while(strcmp(s,s1)<0 || strcmp(s,s1)>0) { strcat(s3,s1); strcat(s3," "); /*inserisco uno spazio tra una parola e l'altra*/ printf("Inserisci una parola:\n"); gets(s2); strcpy(s1,s2); s1[strlen(s1)]='\0'; } printf("%s",s3); return; } 36