CORSO DI C Scritto da : Mariani Angelo, Carullo R. Corretto da : Giammarco Paolo Elaborato da : Giammarco Paolo INDICE. INDICE. 2 Simbologia. 4 1. Il linguaggio C : panoramica e cenni storici. 5 2. Il compilatore : uno sguardo più approfondito. 7 3. Il linker. 9 4. Introduzione al C : prime funzioni. 13 Esercizi. 15 5. Include e file-header (.H). 16 6. Le variabili. 22 Esercizi. 25 Gli Array. 28 Il tipo di dato “Float”. 29 7. I Sottoprogrammi. 30 8. L’istruzione “printf” : qualcosa in più. 35 ESERCIZI. 38 9. L’istruzione “printf” : ancora di più. 40 ESERCIZI 44 10. Alcune curiosità sull’istruzione “printf”. 46 11. I Puntatori. 47 12. I Cicli (While - For). 48 13. La direttiva #DEFINE 49 Esercizi. 51 14. Passaggio di parametri alle funzioni. 53 Esercizi. 56 15. Cura di bellezza ai programmi. 57 Esercizi. 60 16. I record. 62 17. Nuovi elementi di programmazione. 65 18. I File. 70 19. Programmazione orientata agli oggetti (OOP). 75 20. La libreria “iostream”. 84 Simbologia. “//” Indica delle note dove vengono descritti i parallelismi con altri linguaggi.(solo BASIC e Pascal, dato che non credo interesserebbe a qualcuno se considerassi robe tipo Ada o Eiffel). Sappiano, i principianti totali, che potranno saltare queste parentesi senza alcun timore. “Ö” Indica delle note importanti ed utili per il lettore. 1. Il linguaggio C : panoramica e cenni storici. “Il linguaggio C fu realizzato ed implementato per la prima volta negli anni settanta da Dennis Ritchie su di un DEC PDP-11 con il sistema operativo UNIX. Il C è il risultato di un processo iniziato con la creazione di un primo linguaggio chiamato BCPL, sviluppato da Martin Richards. Da questo è poi nato un secondo linguaggio realizzato da Ken Thompson, chiamato B, che a sua volta ha portato allo sviluppo del C” (Herbert Schildt, introduzione a “Programmare in C++”, McGraw-Hill). Alcune persone che hanno letto la storia del C e di Unix si potrebbero chiedere : “Il sistema operativo Unix è scritto in Linguaggio C, come ha fatto quindi Ritchie a implementare C su Unix se Unix ancora non poteva esistere?” Ecco la risposta : “L’inizio dello sviluppo del sistema operativo UNIX risale al 1969 e venne compiuto nei Laboratori Bell, su un sistema DEC PDP-7 che era stato scartato. Ken Thompson, con il supporto e le idee di Rudd Canaday, Doug McLlroy, Joe Ossanna e Dennis Ritchie scrisse un sistema time sharing adatto ad ogni tipo di applicazione, di modeste dimensioni, abbastanza valido da suscitare l’entusiasmo degli utenti e, in ultima analisi, da creare sufficiente credibilità ai fini dell’acquisto di un sistema di maggiori capacità : il PDP 11/20. Uno dei primi utenti fu Ritchie, che favorì la migrazione al PDP-11 nel 1970. Egli progettò e scrisse anche un compilatore per il linguaggio di programmazione C. Nel 1973, Ritchie e Thompson riscrissero in C il nucleo di UNIX, rompendo la tradizione che voleva il software di sistema scritto in linguaggio assembler. Da tale scrittura, essenzialmente, nacque UNIX com’è oggi.” (Kernighan, Pike: “UNIX”, Zanichelli, 1985). Già, ma cos’è un linguaggio di programmazione ? Generalizzando fortemente, è un insieme di regole e di parole che noi, UOMINI, possiamo usare per comunicare a lui, l’ELABORATORE, COSA fare ma soprattutto COME farlo. L’elaboratore è molto, molto, molto scemo. Sa fare quattro o cinque cose (sufficienti per ottenere risultati anche molto complessi) ma non sa come combinarle insieme a tale scopo. È come un cuoco in grado di accendere un fornello, spostare una pentola su di un fornello, vuotare il contenuto di un uovo in un contenitore e controllare un contaminuti. Nonostante queste azioni siano sufficienti per cuocere un uovo in chereghino, lui non è in grado di concatenarle nella giusta sequenza. Se quindi gli chiedessimo “uei, tu, fammi un uovo in cereghino” lui resterebbe attonito. Dovremmo invece dirgli “ora tu devi 1) accendere il fornello 2) rompere l’uovo nel tegame 3) sposta il tegame sul fuoco 4) attendi cinque minuti, e scodella tutto”. Dovremmo quindi fornirgli la sequenza di azioni necessarie per la cottura dell’uovo. Tale sequenza di azioni è il “programma” che il cuoco deve eseguire (ed eseguirà sempre) qualora voglia cuocere l’uovo in cereghino. Se poi noi scrivessimo la sequenza su di un foglio, tutte le volte che volessimo l’uovo potremmo passare al cuoco il foglio, senza dover di nuovo dargli a voce tutti i comandi. Come già detto, l’elaboratore elettronico si comporta esattamente come il nostro cuoco, con un’aggravante: non parla italiano. Parla un linguaggio particolare, molto elementare e formato da pochissime parole. Non potremmo quindi dirgli “accendi il fornello”, ma ad esempio dovremmo dirgli “schiaccia gas, accendi fiammifero, avvicina fiammifero”. Tale linguaggio elementare negli elaboratori ha un nome: si chiama “linguaggio macchina” (o anche “assembler”: attenzione, non è la stessa cosa, ma per semplicità si supponga di sì, e spesso li useremo come sinonimi). È un linguaggio elementare, come già detto, molto distante da quello da noi normalmente utilizzato, e quindi si dice che è un “linguaggio di basso livello” (in contrapposizione al nostro, di “altissimo livello”). Ohibò , e come ci si capisce allora ? Niente ci vieta di dare ordini al nostro elaboratore direttamente in “linguaggio macchina” (anzi, moltissimi lo fanno). Ma come detto non è il massimo della praticità (anche se potremmo ottenere il pieno controllo del cuoco: se per motivi oscuri volessimo fargli premere sette volte di fila il gas, basterebbe dirgli: “schiaccia gas, schiaccia gas, schiaccia gas...”), e quindi ecco perché è preferibile usare uno dei cosiddetti “linguaggi di alto livello”: è un buon compromesso, non possiamo parlargli in italiano però cerchiamo di venirgli incontro e gli parliamo in un linguaggio che non è il nostro (altissimo livello) ma non è comunque il suo (basso livello), ed è quindi abbastanza semplice da imparare. Ci vuole perciò qualcuno (un “traduttore”) che traduca i nostri ordini (dati nella lingua intermedia di alto livello da noi scelta (C, Pascal, Basic, eccetera)) nella sua lingua. Se dicessimo al traduttore “accendi il fuoco”, il traduttore ascolterebbe il nostro ordine, lo tradurrebbe nella sua mente, e direbbe al cuoco “schiaccia gas, accendi fiammifero, avvicina fiammifero”. Il traduttore può però operare in due modi: potrebbe ascoltare tutti i nostri ordini, aspettare a tradurli, tradurli solo quando noi avessimo finito e passarli a quel punto al cuoco. Oppure potrebbe tradurre gli ordini MENTRE noi li pronunciamo, in “tempo reale”, quindi. Negli elaboratori, tale compito di traduzione viene effettuato da due “programmi” particolari. Quelli che funzionano secondo la prima modalità si chiamano “compilatori”, quelli che funzionano secondo la seconda si chiamano “interpreti”. I primi sono molto più efficienti (ovvero fanno si che il cuoco lavori molto più velocemente): il cuoco non deve aspettare che il traduttore ascolti l’ordine, lo traduca e glielo comunichi, ma alla fine della traduzione ha pronta tutta la lista di ordini nel suo linguaggio, e quindi può lavorare praticamente senza pause. Il C è sempre utilizzato in unione con un compilatore. Anche il Pascal, prendendo ad esempio alcuni linguaggi “famosi”. Il Basic invece spesso è “interpretato”. Le motivazioni di tali scelte vanno imputate a caratteristiche intrinseche dei linguaggi, che non prenderemo in considerazione. In teoria infatti qualunque linguaggio potrebbe essere sia interpretato che compilato. Il C è spesso considerato un linguaggio di “medio livello”, situato tra il linguaggio macchina (basso livello) e il Pascal (alto livello). Uno degli obiettivi del C era quello di fornire al programmatore un linguaggio di alto livello che potesse sostituire il linguaggio macchina (come detto più difficile, ma molto versatile e potente (vedi l’esempio sulle sette pressioni del gas)). Una istruzione in Pascal non ha virtualmente alcun rapporto con la sequenza di istruzioni in linguaggio macchina che vengono infine eseguite. Il C invece, pur possedendo strutture di controllo ad alto livello come quelle del Pascal, permette al programmatore di manipolare bit, byte e indirizzi in modo più strettamente legato alla macchina rispetto ad altri linguaggi ad alto livello. Per questo motivo il C è stato a volte definito “codice assembler ad alto livello”. Grazie alla sua duplice natura, il C consente di creare programmi efficaci e molto veloci senza costringere al ricorso del linguaggio macchina. Secondo la filosofia su cui è stato sviluppato il C, il programmatore è padrone delle proprie azioni. Di conseguenza il linguaggio non interferisce mai con il lavoro del progettista. Ecco perché non esiste quasi controllo di errore in esecuzione. Per esempio, se per qualche strano motivo si volesse scrivere nella parte di memoria in cui risiede al momento il programma, niente impedisce di farlo. Tutta la responsabilità per quanto riguarda controlli e cose simili ricade sul programmatore stesso. 2. Il compilatore : uno sguardo più approfondito. Dobbiamo introdurre qualche nuovo termine, per facilitarci le cose. Si chiama “sorgente” (o anche “programma sorgente”, meno comunemente “listato”) la sequenza di comandi scritti nel linguaggio di programmazione che abbiamo scelto, PRIMA che venga tradotto in un linguaggio comprensibile dal calcolatore. Così, il “sorgente” della ricetta dell’uovo in cereghino è il seguente: ---------- INIZIO SORGENTE ------------ passo 1: rompere l’uovo nel tegame. passo 2: accendere il fornello. passo 3: spostare il tegame sul fornello. passo 4: controllare il contaminuti. passo 5: SE il tempo trascorso è minore di cinque minuti ALLORA torna al passo 4, ALTRIMENTI continua passo 6: spegni il fornello. passo 7: togli l’uovo dal tegame. passo 8: fine. ---------- FINE SORGENTE ------------- (Le parole in maiuscolo sono così perché vi abituiate fin d’ora a notare queste strutture decisionali (SE questa cosa è vera, ALLORA faccio questo, ALTRIMENTI faccio quest’altro), così comuni anche nella vita di tutti i giorni: vedremo in seguito quanto sono importanti nella programmazione). Ricordate: il cuoco NON capisce questo linguaggio. Bisogna passarlo al traduttore (i traduttori più diffusi sono quelli “che fanno tutto in un colpo” e non comunicano al cuoco una riga subito dopo averla tradotta; li chiameremo “compilatori”). Il traduttore riceverà il sorgente, ci lavorerà un pò sopra e sfornerà un altro foglio, contenente il “programma eseguibile” (la sequenza di azioni, cioè , che il cuoco è in grado di eseguire perché scritta nel suo linguaggio). Questo potrebbe essere un esempio di “programma eseguibile” (comunemente detto “eseguibile”) sfornato dal nostro traduttore: --------- INIZIO ESEGUIBILE -------- passo 1: prend uov. passo 2: prend tegam. passo 3: spost uov su tegam. passo 4: romp uov. passo 5: sping gas. passo 6: ruot gas vers destr. passo 7: accend fiammifer. passo 8: spost fiammifer su gas. passo 9: spost tegam su gas. passo 10: accend contaminut. passo 11: guard contaminut. passo 12: SE tempo passat minor di cinque. ALLORA vai al passo 11, ALTRIMENTI continua. passo 13: sping gas. passo 14: ruot gas vers sinistr. passo 15: spost tegam da gas. passo 16: ruot tegam vers destr. passo 17: fine. ---------FINE ESEGUIBILE---------- Avete visto ? L’eseguibile è ovviamente più lungo del sorgente, in quanto ad una istruzione del sorgente (ad esempio “accendi fornello”) corrispondono più istruzioni nell’eseguibile (“sping gas”, “ruot gas vers destr”, “accend fiammifer”). Questo perché il linguaggio che il cuoco capisce è molto elementare, proprio perché sono elementari i compiti che il cuoco è in grado di eseguire. Riassumiamo cosa abbiamo fatto. Abbiamo scritto un sorgente che svolge il compito “uovo in cereghino”. Lo abbiamo passato al traduttore o compilatore, che lo ha tradotto ed ha costruito l’eseguibile. Se dessimo ora l’eseguibile al cuoco, il cuoco sarebbe in grado di cuocerci l’uovo così come noi vogliamo. Nella programmazione funziona tutto esattamente così. Una volta capito cosa dobbiamo fare, e capito COME possiamo dire al computer come farlo (nel linguaggio di programmazione scelto), scriviamo il sorgente. Il sorgente viene passato al compilatore, che lo traduce (si dice che lo “compila”, e così diremo d’ora in poi) e crea un eseguibile, che come tale è eseguibile dal computer. Tutto chiaro ? Restando nel campo dei computer, il sorgente è un semplice file di testo che contiene le istruzioni da noi scritte. Il compilatore è un “programma eseguibile” particolare, che prende il sorgente, lo elabora, e crea un altro “file eseguibile” o “file binario”, che noi possiamo eseguire sul nostro computer. Ecco quindi come si svolgerebbe una tipica sessione di programmazione: EDITA nomesorgente COMPILA nomesorgente CREANDO nomeeseguibile ESEGUI nomeeseguibile. 3. Il linker. Il compilatore, come si è detto, è il programma che opera sul nostro sorgente (cioè sul programma scritto nel linguaggio di alto livello da noi scelto) traducendolo in una versione in linguaggio macchina, che il computer è in grado di eseguire. O meglio, DOVREBBE essere in grado di eseguire. Nella realtà infatti il frutto del compilatore non è direttamente eseguibile: deve subire un’ultima elaborazione. Tale elaborazione è effettuata da un secondo programma particolare, il “linker” o “collegatore”. Dobbiamo introdurre un ulteriore termine: è chiamato “programma oggetto” (o “oggetto”) il frutto del compilatore, quando ancora non è stato smanacciato dal linker. Ma perché questo passo supplementare ? Tale passo è necessario per due motivi principali. · In primo luogo perché il compilatore in realtà NON traduce completamente il sorgente. Alcune espressioni infatti non vengono tradotte: per motivi vari è preferibile lasciare che una terza persona (il “linker”, appunto) aggiunga alla traduzione del sorgente alcune parti “già tradotte in precedenza” che vengono conservate in appositi archivi chiamati “librerie”. Si può immaginare che il traduttore delle nostre ricette per il cuoco non traduca, ad esempio, l’istruzione “accendi il gas” perché questa ricorre molto spesso: la traduzione di questa frase verrà effettuata in un secondo tempo dal “linker”, che andrà a cercare nell’archivio (nella “libreria”) la traduzione dell’istruzione e la aggiungerà alla traduzione generata dal traduttore (cioè al “programma oggetto”). Riassumiamo: “programma sorgente” (o “sorgente”) è il programma così come noi lo scriviamo, nel nostro linguaggio di alto livello. “Programma oggetto” (o “oggetto”) è il frutto del compilatore: è ciò che diventa il “programma sorgente” dopo che il compilatore l’ha tradotto. “Programma eseguibile” (o “eseguibile”) è infine il frutto del linker, finalmente eseguibile dal computer. ------ SORGENTE ------ Accendi il gas. Rompi l’uovo nel tegame. Metti il tegame sul fuoco. ... Spegni il gas. ---- FINE SORGENTE --- Proviamo ad esemplificare il tutto con la nostra ricetta dell’uovo in cereghino. Il SORGENTE viene passato al compilatore, che lo traduce parzialmente e crea l’OGGETTO. ------ OGGETTO ------- Accendi il gas. Prend uov. Spost tegam sott uov. Romp uov. ... Sping gas. Ruot gas vers destr. ---- FINE OGGETTO ---- L’OGGETTO è il SORGENTE parzialmente tradotto dal compilatore: le istruzioni non tradotte sono state marcate dal compilatore in modo che il linker sappia quali sono le istruzioni non ancora tradotte (nel nostro caso la sola “Accendi il gas”, contrassegnata simbolicamente da un asterisco). ----- ESEGUIBILE ------ *Sping gas. *Ruot gas vers sinistr. *Prend fiammif. *Accend fiammif. *Spost fiammif su gas. Prend uov. Spost tegam sott uov. Romp uov. ... Sping gas. Ruot gas vers dest. --- FINE ESEGUIBILE --- * sono una traduzione aggiunta dal linker dell’istruzione “Accendi il gas”. Passiamo ora l’OGGETTO al linker. Il linker analizzerà l’OGGETTO, e cercherà in una sua libreria di istruzioni già tradotte ogni istruzione marcata trovata. Aggiungerà quindi all’OGGETTO la traduzione di questa istruzione, creando un ESEGUIBILE ora completamente tradotto. · In secondo luogo il linker può fare anche altro: può collegare tra di loro le traduzioni (incomplete) di più sorgenti diversi. Noi infatti, proponendoci come scopo il cuocere una pizza, potremmo decidere di separare il nostro programma per il cuoco in più parti (invece di scrivere un sorgentone che faccia tutto quanto in una botta sola). Potremmo dividere la ricetta in due parti: una parte che si occupa di preparare la pasta(SORGENTE_1), ed una che si occupa di stendere la pasta nel tegame, guarnirla e cuocerla(SORGENTE_2). ------ SORGENTE_1 ------ Rompi uova. Metti 1kg di farina. Impasta. ... Finisci lievitazione. --- FINE SORGENTE_1 --- ------ SORGENTE_2 ----- Metti pasta nel tegame. Stendi con le dita. Metti pomodoro. ... Spegni gas. Togli pizza da forno. ---- FINE SORGENTE_2 --- Come potremmo ottenere da questi due sorgenti un UNICO eseguibile ? Dovremmo passare entrambi i sorgenti al compilatore, ottenendo DUE oggetti (OGGETTO 1 e OGGETTO 2). Il linker è in grado di UNIRE tra di loro questi due oggetti, creando un UNICO programma eseguibile. Ovviamente anche in questo caso il linker, prima di unire i due oggetti, dovrebbe occuparsi di completare le traduzioni cercando nella libreria le traduzioni delle istruzioni marcate dal compilatore ed aggiungendole ad ogni oggetto. Ecco come, quindi, avverrebbe la sessione di lavoro: EDITA SORGENTE_1 EDITA SORGENTE_2 COMPILA SORGENTE_1 ottenendo OGGETTO_1 COMPILA SORGENTE_2 ottenendo OGGETTO_2 LINKA OGGETTO_1 ed OGGETTO_2 ottenendo PIZZA_ESEGUIBILE Ricordo: NIENTE E NESSUNO CI VIETA di scrivere UN UNICO sorgente per la pizza. Potremmo infatti fare così, nel solito modo: EDITA SORGENTE COMPILA SORGENTE ottenendo OGGETTO LINKA OGGETTO ottenendo PIZZA_ESEGUIBILE Perché allora dividere in più parti il sorgente, per poi compilarle separatamente ed unirle tramite il linker ottenendo un eseguibile che effettuerebbe esattamente lo stesso compito ? Perché addirittura l’esistenza del “linker”, quando sarebbe tanto più semplice utilizzare un compilatore che eviti il passaggio intermedio dell’oggetto” e da un sorgente produca, senza nessuna fase di link, un eseguibile fatto e finito ? Per non meno di tre ottimi motivi. Eccoli. 1) Potremmo scrivere ricette “a più mani” in maniera semplice ed ordinata. Io potrei scrivere la ricetta della pasta, e Peppino ‘o Pizzettaro la ricetta per guarnirla e cuocerla. Certo, potremmo anche lavorare a quattro mani sullo stesso sorgente, ma ad esempio io non capisco il napoletano. 2) Potrei riutilizzare ricette già scritte in precedenza. Ammettendo per assurdo che la stessa pasta per la pizza potesse andar bene anche per gli gnocchi, io potrei riutilizzare la ricetta per la pasta della pizza (senza doverla quindi riscrivere da capo) e scrivere dal nulla solo la parte relativa alla fabbricazione e alla cottura degli gnocchi. In “breve” tempo (certo inferiore a quello che sarebbe necessario se dovessi scrivere tutto da zero) otterrei un perfetto eseguibile per gli gnocchi. 3) Posso risparmiare molto “tempo di compilazione”. Supponiamo di star lavorando ad una ricetta MOLTO elaborata. Se decido di dividere la stessa in più ricettine separate, nel caso volessi effettuare una modifica su una sola di queste (ad esempio usare olive verdi invece delle olive nere) mi basterebbe modificarne il sorgente, RICOMPILARE SOLO QUESTO ottenendo un nuovo oggetto e “linkare” insieme tutti gli oggetti (i vecchi e il nuovo), ottenendo un nuovo eseguibile che usa le olive verdi invece delle nere. Il compilatore dunque lavora SOLO su un limitato pezzo della ricetta. Se invece decidessi di scrivere la ricettona in un sorgente unico, qualsiasi modifica volessi effettuare mi costringerebbe a ricompilare TUTTO il sorgente, nonostante la microscopica modifica. Un’altra definizione: è detto “modulo” ogni “pezzo” in cui si suddivide il sorgente. Si dice solitamente, ad esempio, che “il sorgente è composto da sei moduli”. Ö : il linker il più delle volte è SVINCOLATO dal linguaggio del sorgente. Uno stesso programma linker può essere in grado di lavorare su oggetti risultati di compilazioni di sorgenti in C, in Basic, in Cobol, in Pascal, eccetera. È di conseguenza possibile addirittura collegare tra di loro vari oggetti che siano il risultato di compilazioni diverse effettuate da sorgenti scritti in linguaggi diversi (a patto ovviamente che i files oggetto seguano un certo standard). Nell’ambito ad esempio dei linguaggi della Borland è possibile compilare moduli scritti in C, in Pascal ed in Prolog (compilati rispettivamente con Turbo C, Turbo Pascal e Turbo Prolog), ed unire tra di loro gli oggetti prodotti mediante l’uso del linker. È altresì possibile unire oggetti provenienti da compilazioni effettuate dal compilatore C della Microsoft (o di altre case, ma con determinati vincoli) con oggetti provenienti da compilazioni effettuate con Clipper, e così via. È ovvio che quando si acquista un “compilatore”, non si ottiene solo il compilatore in senso stretto (perché altrimenti, come abbiamo visto, non saremmo in grado di produrre eseguibili funzionanti ma solo files oggetto senza nessuna utilità ). Le scatole del “compilatore Borland C++ 3.1” o del “compilatore Microsoft C 6.0” quindi non contengono solo il compilatore, ma anche il linker e le librerie da questo utilizzate. Esistono poi alcuni compilatori detti “ad ambiente integrato” che sembrano far tutto in un colpo solo: si scrive il sorgente, si preme un tasto e magicamente dopo qualche secondo si ha l’eseguibile pronto e scattante. E la compilazione ? E il link ? Dov’è il file oggetto ? Attenzione: queste fasi esistono ancora. L’ambiente integrato però è talmente in gamba da occuparsi di tutte le cose noiose, lasciandovi liberi di concentrarvi a fondo sulla mera programmazione. Durante il nostro corso C utilizzeremo raramente la suddivisione in più moduli del sorgente. Si lavorerà quasi sempre su di un unico sorgente, che per convenzione è un file di qualsiasi nome ma con estensione “.C”. Ad esempio “prova.c”. Chi di noi sarà così fortunato da avere a disposizione un compilatore ad ambiente integrato, come detto non dovrà far altro che scrivere il sorgente, premere un tasto e godersi il risultato in pochi secondi (o piangere sugli errori, a seconda. Gli altri invece dovranno utilizzare un compilatore “tradizionale”, che obbliga ad effettuare manualmente ed in tempi diversi compilazione e linking. Ma non si preoccupino: il vero programmatore da sempre lavora e per sempre lavorerà così. 4. Introduzione al C : prime funzioni. Ö : In C TUTTE le istruzioni (tranne in alcuni casi particolari, che vedremo in seguito) DEVONO terminare con un carattere di “punto e virgola”, (“;”). Ricordatevelo, stampatevelo in testa, abituatevi a rispondere a vostra madre dicendo alla fine “punto e virgola”. Il punto e virgola NON SI METTE però dopo i nomi dei sottoprogrammi (non va ad esempio dopo il “main()”. // : In BASIC si usa un carattere di “due punti” (“:”) per separare più istruzioni SOLO se tali istruzioni compaiono su di una stessa linea. Se le istruzioni sono su diverse linee, non è necessario il “due punti”. In C invece è SEMPRE necessario inserire il “punto e virgola”, anche se l’istruzione cui è riferito si trova da sola su di una linea. Possiamo dire che in C il “punto e virgola” identifica la FINE dell’istruzione, mentre in Basic il “due punti” svolge le funzioni di separatore tra due istruzioni adiacenti. Anche in Pascal è sempre necessario il “punto e virgola” dopo ogni istruzione tranne nel caso in cui l’istruzione non sia l’ultima in un blocco “begin-end”. In C invece è SEMPRE necessario, anche se l’istruzione è alla fine di un blocco “{ }”. Il C non differisce dagli altri linguaggi se non perché‚ - essendo più vicino alla logica del calcolatore - risulta all’inizio un pò astruso. Il programma deve iniziare con alcuni, #include, che servono a far sapere al compilatore che useremo certe librerie. Quella che si include sempre nei programmi è la: che contiene routine di input ed output. Si devono poi dichiarare le variabili che si useranno: interi (int), reali (float), vettori e matrici. Il programma vero e proprio inizia con la parola main(), che va sempre accompagnata dalle parentesi () per eventuali dichiarazioni. Poi si apre una parentesi graffa, che verrà chiusa alla fine del main stesso. Generalmente il main si chiude con: return 0;. Le istruzione che useremo nel nostro primo programma sono le seguenti (ricorda che ogni istruzione deve sempre essere seguita dal punto e virgola): clrscr(); Pulisce lo schermo printf(); Scrive sull’output standard (schermo) scanf(); Legge dall’ input standard (tastiera) getch(); Legge un carattere da tastiera Vediamole brevemente una per una. La clrscr() pulisce, con i parametri di colore settati al momento, tutto lo schermo (da 1,1 a 80,25). Naturalmente se si apre una finestra (vedi lezioni successive) pulirà solo quella. La printf() scrive una stringa di caratteri: printf(“Quando si parte per le vacanze ?\n”) I caratteri: \n servono a mandare a capo il cursore. Se si vogliono lasciare alcune righe vuote si può scrivere: printf(“\n\n\n”); Per scrivere un numero si deve dichiarare il formato: d (intero decimale), f (reale), s(stringa): printf(“%d”, intero); printf(“%f”, reale); La scanf() legge da tastiera una espressione e il formato va dichiarato: scanf(“%d”, &intero); prima della variabile si scrive una & (vedremo in seguito il significato). La getch() permette di leggere un carattere e può essere usata come trucco per tenere fermo il programma fino alla pressione di un tasto. Vediamo ora il semplice programma che può essere compilato ed eseguito (dopo aver cancellato o commentato le righe precedenti): #include Include la libreria di input, output int n; Definizione dell’intero n float s; Definizione del reale s main() Inizio programma principale { clrscr(); Pulisce lo schermo printf(“ PROGRAMMA CHE LEGGE E SCRIVE DUE NUMERI \n\n”); printf(“\n\n\n Scrivi un numero intero: ”); scanf(“%d”, &n); Legge un intero printf(“ Intero: %d”, n); printf(“\n\n\n Scrivi un numero reale: ”); scanf(“%f”, &s); Legge un reale printf(“ Reale: %f\n”, s); getch(); Legge un carattere return 0; } Esercizi. 1. Realizzare una calcolatrice capace di fare la somma di due numeri. R: main() {float r1, r2; printf(“\n C A L C O L A T R I C E \n”); printf(“\n Scrivi il primo addendo: ”); scanf(“%f”, &r1); printf(“\n Scrivi il secondo addendo: ”); scanf(“%f”, &r2); printf(“\n La somma vale: %f”, r1+r2); getch(); } 2. Realizzare un breve prg che calcoli l’ IVA. R: main() { const float iva =19.50; Definizione di una costante float float prezzo, prezzo_iva; printf(“\n C A L C O L O D E L L ‘ I V A \n”); printf(“\n Scrivi il prezzo netto: ”); scanf(“%f”, &prezzo); prezzo_iva = prezzo + prezzo*iva/100; printf(“\n Prezzo finale: %f lire”, prezzo_iva); getch(); } 3. Scrivere un programma per determinare le soluzioni di una equazione di secondo grado. R: #include Libreria di funzioni matematiche il prg utilizza la radice quadrata (sqrt) main() { float a, b, c, delta, x1, x2; printf(“\n RADICI DI UNA EQUAZIONE DI SECONDO GRADO \n”); printf(“\n Coefficiente a: ”); scanf(“%f”, &a); printf(“\n Coefficiente b: ”); scanf(“%f”, &b); printf(“\n Coefficiente c: ”); scanf(“%f”, &c); delta=b*b-4*a*c; Calcolo del delta if(delta<0) { printf(“\n\n ATTENZIONE : delta negativo!”); getch(); return;} Se il delta è negativo il prg si ferma x1=(-b+sqrt(delta))/(2*a); x2=(-b-sqrt(delta))/(2*a); printf(“\n delta= %f x1= %f x2= %f”, delta,x1,x2); getch(); } 5. Include e file-header (.H). Le istruzioni #include, si trovano all’inizio del programma, non è in realtà una funzione ma una “direttiva”: impone al compilatore, prima di iniziare a tradurre il programma (infatti si dice che la #include e le altre direttive (#ifdef, #define, ...) sono direttive rivolte al “precompilatore”), di “includere” nel sorgente a partire da quel punto il file il cui nome si trova tra parentesi angolari (<>) o tra virgolette (“”). Il file, una volta incluso, entrerà a far parte del sorgente in tutto e per tutto, ed insieme a questo verrà compilato. Ma con quale criterio devo mettere le parentesi angolari e le virgolette ? Quando si dà una direttiva di inclusione come: #include “ope.h” o , in un programma, il preprocessore cerca il file secondo regole prestabilite, che dipendono in parte dal compilatore utilizzato. Come regola generale, la ricerca si divide in due fasi: 1. nella prima il file viene cercato nella directory che contiene il programma che si sta compilando; 2. nella seconda fase la ricerca si estende ad altre directory, tipicamente quella che contiene gli header-file (nel Turbo-C ad esempio e la directory : TC\INCLUDE). Se si vuole che vengano eseguite ambedue le ricerche bisogna usare le virgolette nella direttiva di inclusione del file, #Include “ope.h” se invece si vuole che venga effettuata solo la seconda si possono usare le parentesi angolari, #Include In pratica conviene usare le virgolette per i file “my.h” contenuti nella stessa directory del programma principale, e le parentesi angolari per i file di libreria che si includono sempre nei programmi: , , ... Nei file-header si trovano principalmente due cose: i prototipi delle funzioni (che in C “liscio” si potrebbero volendo anche omettere) e le importantissime dichiarazioni delle costanti principali (ad esempio “EOF”, o “NULL”). Quando si utilizza una delle cosiddette “funzioni di libreria”, è sempre meglio “scoprire” (il modo più facile per farlo è guardare nell’ help online, se c’è) in quale header file si trovi il suo prototipo (può trovarsi in più di un header file). Ad esempio, il prototipo della “printf” è nell’ header file “stdio.h”, quello della “atof” in “stdlib.h” e “math.h”, quello della “malloc” in “stdio.h” e in “alloc.h”. Si può anche provare a compilare un programma senza includere niente. Solo se non utilizzano costanti o macro definite in qualche header (e questo accade solo in programmi estremamente semplici) funzionerà senza problemi. I file-header possono essere creati separando in un file-header (.h), tutte le routine, e poi eseguire la normale procedura di compilazione. Per esempio ecco l’ implementazione di una calcolatrice : lo dividi in un main che chiami per esempio “calc.c” e un file header che chiami “ope.h”, e poi compili normalmente il main. Inizio file main : CALC.C #include “ope.h” main() {printf(“ CALCOLATRICE\n”); printf(“>”); atprompt(); ctinue(); exit(); } FINE FILE : CALC.C INIZIO FILE HEADER : OPE.H #include #include float accu,opnd; char opr; atprompt() {scanf(“%f”,&accu);scanf(“%c”,&opr); if((opr!=‘r’)&&(opr!=‘i’))scanf(“%f”,&opnd); cmpt(); } ctinue() {scanf(“%c%f”,&opr,&opnd); if(opr == ‘q’) {exit();} cmpt(); } cmpt() {switch(opr) {case ‘+’: accu = accu+opnd;break; case ‘-’: accu = accu-opnd;break; case ‘*’: accu = accu*opnd;break; case ‘/’: accu = accu/opnd;break; case ‘r’: accu = sqrt(accu);break; case ‘^’: accu = pow(accu,opnd);break; case ‘i’: accu = 1/accu;break; case ‘c’: accu = 0;printf(“> ”);atprompt(); default : printf(“Errore nei dati in input”); exit(); } printf(“= %f”,accu); ctinue(); } FINE FILE OPE.H Oppure si può chiamare il secondo file ope.c e allora si dovranno compilare tutti e due i prg (il main e ope.c) e poi linkare il file con le funzioni al main. Allora: · Le variabili sono cassetti, destinati a contenere dati; · Ogni cassetto ha uno ed un solo nome; · In ogni cassetto può entrare uno ed un solo tipo di dato. Sviluppiamo meglio i concetti appena espressi: · Un cassetto. Una variabile È in tutto e per tutto un cassetto, che possiamo utilizzare per memorizzare temporaneamente dati, in attesa di elaborarli. · Come possiamo “usare” questo cassetto ? Tramite il nome. Immaginate che sul davanti di ogni cassetto, sotto la maniglia, stia scritto un nome (diverso per ogni cassetto). Per mettere un dato in quel cassetto diremo “metti il dato dentro il cassetto Pippo”. · Non esiste il cassetto universale, che può contenere di volta in volta tutto quanto. In C dovremo DICHIARARE all’inizio di ogni routine tutti i cassetti che vorremo usare all’interno della routine stessa, e dichiarare anche il tipo di dati che saranno destinati a contenere. I tipi di dati che abbiamo già visto parlando dell’istruzione “printf” sono tre: il tipo di dato “intero” (sequenza speciale “%d”), il tipo di dato “carattere” (sequenza speciale “%c”) e il tipo di dato “stringa” (sequenza speciale “%s”). In questa lezione considereremo solo i primi due. Cominciamo considerando il tipo di dato “intero”. Come detto, dobbiamo dichiarare all’inizio di ogni procedura i cassetti che utilizzeremo nel corpo della stessa. Dichiarare una variabile significa materialmente scriverne il nome, preceduto dal tipo di dati che dovrà contenere. Supponiamo di voler utilizzare, all’interno del nostro programmino, tre variabili di tipo intero, dal nome “a”, “b” e “c”. Ecco come le dovremo dichiarare: main() { int a; int b; int c; ...resto del programma... } Visto ? “int a;” dichiara la variabile “a”, di tipo intero. “int b;” e “int c;” fanno lo stesso per le variabili “b” e “c”. Attenzione: si può scrivere una dichiarazione più concisa, ma dall’identico significato: main() { int a,b,c; ...resto del programma... } Le tre variabili “a”, “b” e “c” sono dichiarate contemporaneamente di tipo intero: per dichiarare più variabili dello stesso tipo è infatti sufficiente scrivere il tipo seguito dalla lista di variabili, separate da una virgola. Come metto qualcosa nel cassetto ? Con l’operatore di assegnamento “=”. L’istruzione “a=1;” significa “metti il numero 1 nel cassetto di nome ‘a’. È possibile anche “copiare” il contenuto di un cassetto in un altro (a patto che i due cassetti siano dello stesso tipo, attenzione !). L’istruzione “a=b;” significa metti una copia del contenuto del cassetto ‘b’ nel cassetto ‘a’. Con queste due regolette di base, ci si può sbizzarrire. Si può mettere in un cassetto qualsiasi cosa, anche il risultato di una qualsiasi operazione matematica. Cosa fa la seguente istruzione ?: “a=b+c-2;” Semplice. Nel cassetto ‘a’ mette il risultato della somma dei contenuti dei cassetti ‘b’ e ‘c’, meno 2. Supponendo che ‘b’ contenga 7 e ‘c’ contenga 3, nel cassetto ‘a’ metterà 8 (7+3-2=8). Un caso particolare potrebbe risultare strano. Considerate la seguente istruzione: “a=a+1;” Orpo ! Un matematico potrebbe strabuzzare gli occhi. “Ma come, a uguale ad a + 1 è un’equazione senza soluzione !”. Attenti: non considerate le istruzioni di assegnamento come se fossero delle equazioni, perchè NON lo sono. L’istruzione precedente significa semplicemente nel cassetto ‘a’ metti il vecchio contenuto di ‘a’ sommato ad uno. Non fa altro, quindi, che incrementare di uno il contenuto del cassetto/variabile “a”. Siamo quindi ora in grado di scrivere un programmino che dichiara variabili e con queste effettua dei simpatici giochetti. Bene. Ma a che serve, se non posso vedere il risultato di questi giochetti ? Possiamo, possiamo. Usiamo l’istruzione “printf”, finalmente in modo utile. Nelle lezioni precedenti avevamo imparato come stampare numeri utilizzando l’istruzione “printf”, ricordate ? printf (“Il risultato di 4+3 è %d” ,7); Avevamo detto che l’istruzione “printf” si attende, per la sequenza “%d”, un numero intero. Ora possiamo aggiungere che l’istruzione attende un numero intero O una variabile intera. Quindi il seguente programma dovrebbe esservi chiaro: main() { int a,b,c; a=3; b=4; c=a+b; printf(“Il risultato di 3+4 è %d”, c); } Bello, eh ? Tutto chiaro, vero ? Ö : Come nome di variabile si può utilizzare qualsiasi combinazione di numeri E lettere (maiuscole o minuscole) E segni “_” (chiamati “underscore”). Il nome di una variabile però può iniziare solo con un underscore o con una lettera (maiuscola o minuscola), insomma NON può iniziare con un numero. Insomma: SÌ a “_absdabs”, “asd_asda_asd”,”asd123_123asd”,”____asadsadad123123”. NO a “1asdassaa”, “21312jjashd__asd”, “12asaaa”, ed ancor meno ovviamente a porcherie come “asd====????”, “?(/jjjj” e via sproloquiando. Ö : Il nome può essere un bel pò lungo. I compilatori più avanzati addirittura non hanno una lunghezza massima; le prime versioni di Turbo C comunque consideravano significanti solo i primi 32 caratteri (e sfido chiunque ad usare una variabile dal nome più lungo di dieci caratteri...). Ö : LE MAIUSCOLE SONO DIFFERENTI DALLE MINUSCOLE !!! Quindi il cassetto ‘A’ è DIVERSO dal cassetto ‘a’ ! Ö : Non possono esistere due cassetti (anche se di tipo diverso) con lo stesso nome. Una particolarità del C: esistono due operatori aritmetici “unari” (nel senso che hanno bisogno di un solo argomento): gli operatori “++” (di “autoincremento”) e “--” (di “autodecremento”). Le istruzioni: ++a; o a++; equivalgono all’istruzione “a=a+1”, ovvero incrementa di uno il contenuto del cassetto ‘a’. Le istruzioni: --a; o a--; equivalgono all’istruzione “a=a-1”, ovvero decrementa di uno il contenuto del cassetto ‘a’. Mettere l’operatore prima o dopo la variabile cui si riferisce non è sempre ininfluente (come vedremo in seguito). Per ora però consideratelo tale. Abbiamo visto il tipo di dato “intero”. Consideriamo ora brevemente il tipo di dato “carattere”. Per dichiarare una variabile di tipo carattere occorre ovviamente dichiararla, utilizzando il dichiaratore di tipo “char”. Il seguente programma dichiara tre variabili intere ‘a’, ‘b’ e ‘c’, e due variabili carattere, ‘d’ ed ‘e’: assegna quindi il carattere ‘A’ alla variabile carattere ‘d’, e ne stampa il contenuto. main() { int a,b; int c; char d,e; d=‘A’; printf(“Il contenuto del cassetto ‘d’ è %c”, d); } Con le variabili carattere si possono fare cose abbastanza carine. Ad esempio, considerate le seguenti istruzioni: char a; a=‘C’; a=a+1; Cosa pensate contenga al termine dell’esecuzione il cassetto ‘a’ ? Dunque: la seconda istruzione mette nel cassetto di tipo carattere ‘a’ il carattere ‘C’. La terza istruzione SOMMA UNO al contenuto del cassetto ‘a’. Ma come posso sommare un numero ad un carattere ? Il C è molto abile nel confondere le cose, e riesce a farlo. Per il C sommare un al carattere ‘C’ significa sommare il numero al codice ASCII del carattere ‘C’, facendolo quindi diventare(in questo caso) una ‘D’ (perché il codice ASCCI di C è 67 quindi 67+1=68 e il codice ASCII corrispondente al numero 68 è la D). Il cassetto ‘a’ quindi conterrà il carattere ‘D’. E la seguente istruzione ? a=‘z’-‘C’; Orpo ! Allora, il codice ASCII di ‘z’ è 122. Il codice ASCII di ‘C’ è 67. 122-67=55. Il carattere corrispondente al codice ASCII 55 è ‘7’. Quindi il cassetto ‘a’ conterrà alla fine il carattere ‘7’. Possiamo addirittura sommare tra di loro cassetti di tipo carattere e cassetti di tipo intero ! Il C convertirà automaticamente il risultato, a seconda del tipo di cassetto destinato a contenerlo. Ad esempio: main() { int intero,risultato_1; char carattere,risultato_2; intero=5; carattere=‘A’ ; risultato_1=intero+carattere; risultato_2=intero+carattere; printf(“Il risultato della prima somma è %d”, risultato_1); printf(“Il risultato della seconda somma è %c”, risultato_2); } Potete sicuramente immaginare cosa stamperanno le due istruzioni “printf”. La prima stamperà il codice ASCII di ‘A’ (65) più 5 (ovvero il numero 70, dato che la variabile “risultato_1” è di tipo intero). La seconda stamperà (attenti) il carattere corrispondente al codice ASCII risultante dalla somma del codice ASCII di ‘A’ più 5 (ovvero il carattere ‘F’, corrispondente al codice ASCII 70). 6. Le variabili. Allora: · Le variabili sono cassetti, destinati a contenere dati; · Ogni cassetto ha uno ed un solo nome; · In ogni cassetto può entrare uno ed un solo tipo di dato. Sviluppiamo meglio i concetti appena espressi: · Un cassetto. Una variabile È in tutto e per tutto un cassetto, che possiamo utilizzare per memorizzare temporaneamente dati, in attesa di elaborarli. · Come possiamo “usare” questo cassetto ? Tramite il nome. Immaginate che sul davanti di ogni cassetto, sotto la maniglia, stia scritto un nome (diverso per ogni cassetto). Per mettere un dato in quel cassetto diremo “metti il dato dentro il cassetto Pippo”. · Non esiste il cassetto universale, che può contenere di volta in volta tutto quanto. In C dovremo DICHIARARE all’inizio di ogni routine tutti i cassetti che vorremo usare all’interno della routine stessa, e dichiarare anche il tipo di dati che saranno destinati a contenere. I tipi di dati che abbiamo già visto parlando dell’istruzione “printf” sono tre: il tipo di dato “intero” (sequenza speciale “%d”), il tipo di dato “carattere” (sequenza speciale “%c”) e il tipo di dato “stringa” (sequenza speciale “%s”). In questa lezione considereremo solo i primi due. Cominciamo considerando il tipo di dato “intero”. Come detto, dobbiamo dichiarare all’inizio di ogni procedura i cassetti che utilizzeremo nel corpo della stessa. Dichiarare una variabile significa materialmente scriverne il nome, preceduto dal tipo di dati che dovrà contenere. Supponiamo di voler utilizzare, all’interno del nostro programmino, tre variabili di tipo intero, dal nome “a”, “b” e “c”. Ecco come le dovremo dichiarare: main() { int a; int b; int c; ...resto del programma... } Visto ? “int a;” dichiara la variabile “a”, di tipo intero. “int b;” e “int c;” fanno lo stesso per le variabili “b” e “c”. Attenzione: si può scrivere una dichiarazione più concisa, ma dall’identico significato: main() { int a,b,c; ...resto del programma... } Le tre variabili “a”, “b” e “c” sono dichiarate contemporaneamente di tipo intero: per dichiarare più variabili dello stesso tipo è infatti sufficiente scrivere il tipo seguito dalla lista di variabili, separate da una virgola. Come metto qualcosa nel cassetto ? Con l’operatore di assegnamento “=”. L’istruzione “a=1;” significa “metti il numero 1 nel cassetto di nome ‘a’. È possibile anche “copiare” il contenuto di un cassetto in un altro (a patto che i due cassetti siano dello stesso tipo, attenzione !). L’istruzione “a=b;” significa metti una copia del contenuto del cassetto ‘b’ nel cassetto ‘a’. Con queste due regolette di base, ci si può sbizzarrire. Si può mettere in un cassetto qualsiasi cosa, anche il risultato di una qualsiasi operazione matematica. Cosa fa la seguente istruzione ?: “a=b+c-2;” Semplice. Nel cassetto ‘a’ mette il risultato della somma dei contenuti dei cassetti ‘b’ e ‘c’, meno 2. Supponendo che ‘b’ contenga 7 e ‘c’ contenga 3, nel cassetto ‘a’ metterà 8 (7+3-2=8). Un caso particolare potrebbe risultare strano. Considerate la seguente istruzione: “a=a+1;” Orpo ! Un matematico potrebbe strabuzzare gli occhi. “Ma come, a uguale ad a + 1 è un’equazione senza soluzione !”. Attenti: non considerate le istruzioni di assegnamento come se fossero delle equazioni, perchè NON lo sono. L’istruzione precedente significa semplicemente nel cassetto ‘a’ metti il vecchio contenuto di ‘a’ sommato ad uno. Non fa altro, quindi, che incrementare di uno il contenuto del cassetto/variabile “a”. Siamo quindi ora in grado di scrivere un programmino che dichiara variabili e con queste effettua dei simpatici giochetti. Bene. Ma a che serve, se non posso vedere il risultato di questi giochetti ? Possiamo, possiamo. Usiamo l’istruzione “printf”, finalmente in modo utile. Nelle lezioni precedenti avevamo imparato come stampare numeri utilizzando l’istruzione “printf”, ricordate ? printf (“Il risultato di 4+3 è %d” ,7); Avevamo detto che l’istruzione “printf” si attende, per la sequenza “%d”, un numero intero. Ora possiamo aggiungere che l’istruzione attende un numero intero O una variabile intera. Quindi il seguente programma dovrebbe esservi chiaro: main() { int a,b,c; a=3; b=4; c=a+b; printf(“Il risultato di 3+4 è %d”, c); } Bello, eh ? Tutto chiaro, vero ? Ö : Come nome di variabile si può utilizzare qualsiasi combinazione di numeri E lettere (maiuscole o minuscole) E segni “_” (chiamati “underscore”). Il nome di una variabile però può iniziare solo con un underscore o con una lettera (maiuscola o minuscola), insomma NON può iniziare con un numero. Insomma: SÌ a “_absdabs”, “asd_asda_asd”,”asd123_123asd”,”____asadsadad123123”. NO a “1asdassaa”, “21312jjashd__asd”, “12asaaa”, ed ancor meno ovviamente a porcherie come “asd====????”, “?(/jjjj” e via sproloquiando. Ö : Il nome può essere un bel pò lungo. I compilatori più avanzati addirittura non hanno una lunghezza massima; le prime versioni di Turbo C comunque consideravano significanti solo i primi 32 caratteri (e sfido chiunque ad usare una variabile dal nome più lungo di dieci caratteri...). Ö : LE MAIUSCOLE SONO DIFFERENTI DALLE MINUSCOLE !!! Quindi il cassetto ‘A’ è DIVERSO dal cassetto ‘a’ ! Ö : Non possono esistere due cassetti (anche se di tipo diverso) con lo stesso nome. Una particolarità del C: esistono due operatori aritmetici “unari” (nel senso che hanno bisogno di un solo argomento): gli operatori “++” (di “autoincremento”) e “--” (di “autodecremento”). Le istruzioni: ++a; o a++; equivalgono all’istruzione “a=a+1”, ovvero incrementa di uno il contenuto del cassetto ‘a’. Le istruzioni: --a; o a--; equivalgono all’istruzione “a=a-1”, ovvero decrementa di uno il contenuto del cassetto ‘a’. Mettere l’operatore prima o dopo la variabile cui si riferisce non è sempre ininfluente (come vedremo in seguito). Per ora però consideratelo tale. Abbiamo visto il tipo di dato “intero”. Consideriamo ora brevemente il tipo di dato “carattere”. Per dichiarare una variabile di tipo carattere occorre ovviamente dichiararla, utilizzando il dichiaratore di tipo “char”. Il seguente programma dichiara tre variabili intere ‘a’, ‘b’ e ‘c’, e due variabili carattere, ‘d’ ed ‘e’: assegna quindi il carattere ‘A’ alla variabile carattere ‘d’, e ne stampa il contenuto. main() { int a,b; int c; char d,e; d=‘A’; printf(“Il contenuto del cassetto ‘d’ è %c”, d); } Con le variabili carattere si possono fare cose abbastanza carine. Ad esempio, considerate le seguenti istruzioni: char a; a=‘C’; a=a+1; Cosa pensate contenga al termine dell’esecuzione il cassetto ‘a’ ? Dunque: la seconda istruzione mette nel cassetto di tipo carattere ‘a’ il carattere ‘C’. La terza istruzione SOMMA UNO al contenuto del cassetto ‘a’. Ma come posso sommare un numero ad un carattere ? Il C è molto abile nel confondere le cose, e riesce a farlo. Per il C sommare un al carattere ‘C’ significa sommare il numero al codice ASCII del carattere ‘C’, facendolo quindi diventare(in questo caso) una ‘D’ (perché il codice ASCCI di C è 67 quindi 67+1=68 e il codice ASCII corrispondente al numero 68 è la D). Il cassetto ‘a’ quindi conterrà il carattere ‘D’. E la seguente istruzione ? a=‘z’-‘C’; Orpo ! Allora, il codice ASCII di ‘z’ è 122. Il codice ASCII di ‘C’ è 67. 122-67=55. Il carattere corrispondente al codice ASCII 55 è ‘7’. Quindi il cassetto ‘a’ conterrà alla fine il carattere ‘7’. Possiamo addirittura sommare tra di loro cassetti di tipo carattere e cassetti di tipo intero ! Il C convertirà automaticamente il risultato, a seconda del tipo di cassetto destinato a contenerlo. Ad esempio: main() { int intero,risultato_1; char carattere,risultato_2; intero=5; carattere=‘A’ ; risultato_1=intero+carattere; risultato_2=intero+carattere; printf(“Il risultato della prima somma è %d”, risultato_1); printf(“Il risultato della seconda somma è %c”, risultato_2); } Potete sicuramente immaginare cosa stamperanno le due istruzioni “printf”. La prima stamperà il codice ASCII di ‘A’ (65) più 5 (ovvero il numero 70, dato che la variabile “risultato_1” è di tipo intero). La seconda stamperà (attenti) il carattere corrispondente al codice ASCII risultante dalla somma del codice ASCII di ‘A’ più 5 (ovvero il carattere ‘F’, corrispondente al codice ASCII 70). Esercizi. Quali saranno gli output dei seguenti programmini ? 1) main() { int t; t=10; t++; ++t; printf(“Il valore di ‘t’ è %d\n”,t); } R: Output: Il valore di ‘t’ è 12 Commento: le due istruzioni “t++” e “++t” incrementano entrambe di uno il contenuto della variabile ‘t’. Ricordate: per ora è ininfluente mettere l’operatore unario “++” prima o dopo la variabile cui è applicato. Vedremo come in realtà non sia cosi’. 2) main() { int t=10; char c; c=‘A’; c++; t=20; --t; printf(“Beccati questo valore: %d\n”,t+c); R: Output: Beccati questo valore: 85 Commento: all’entrata nella funzione “printf”, la variabile intera ‘t’ contiene il valore 19 (il v4alore iniziale di 20 viene decrementato di uno con l’operatore “--”), mentre la variabile carattere ‘c’ contiene il valore ‘B’ (il valore iniziale ‘À viene successivamente incrementato di uno tramite l’operatore “++”). La somma tra 19 e ‘B’ (“t+c”) è possibile, come abbiamo visto: del carattere viene considerato il valore ASCII (‘A’ = 65, ‘B’ = 66, ‘C’ = 67 e così via), quindi ‘B’ + 19 = 66 + 19 = 85. Questo numero viene stampato dall’istruzione “printf” dato che la specifica di formato è “%d”. Se fosse stata “%c”, sarebbe stato stampato il carattere il cui codice ASCII è 85, ovvero la lettera ‘U’ . 3) main() { int t; char c; t=65; c=t; printf(“Contenuto di c: %c”,c); printf(“, ma anche c: %d\n”,c); printf(“Invece t: %c”,t); printf(“, ma anche t: %d\n”,t); } R: Output: Contenuto di c: A, ma anche c: 65 Invece t: A, ma anche t: 65 Commento: questo esercizio non ha lo scopo di confondervi ancora di più le idee. Vuole solo evidenziare l’estrema flessibilità del C nel trattamento delle variabili: sembrerebbe che tra una variabile di tipo intero ed una di tipo carattere non ci sia molta differenza... In effetti una differenza c’è , ma non così decisiva (un carattere occupa un byte, e quindi permette valori da -128 a 127; un intero ne occupa due, valori da -32768 a +32767). Il vero errore nell’usare una variabile di tipo carattere per contenere un valore intero (e viceversa) è soprattutto concettuale: un cassetto nato per contenere un certo tipo di dato DEVE contenere solo quel tipo di dato. Il fatto che il C ci permetta di pasticciare le cose non ci autorizza ad abusare della sua magnanimità.4 Gli Array. Ö : Abbiamo visto che possiamo utilizzare, all’ interno dei nostri programmi, dei “cassetti” (le variabili) nei quali possiamo conservare due tipi diversi di dati: caratteri (cassetti/variabili di tipo “char”) e numeri interi (cassetti/variabili di tipo “int”). Questi cassetti però andranno preventivamente “dichiarati”: dovremo cioè , all’ inizio del nostro programma, informare il compilatore che desideriamo utilizzare alcuni cassetti di un certo tipo, che identificheremo per mezzo di un nome. Al compilatore non interessa sapere quali cassetti desidereremo utilizzare per semplice curiosità ; nel BASIC ad esempio non dobbiamo dichiarare un bel niente: ci serve una variabile a metà del programma ? Bene, la materializziamo immediatamente dal nulla, senza doverla dichiarare all’ inizio. Perchè allora il C pretende che gli si dica tutto all’ inizio ? Non sarebbe cento volte più comodo se potessi evitare la dichiarazione delle variabili e utilizzarle dove e quando più mi aggrada ? Certo ! Ma non sarebbe assolutamente efficiente. Il compilatore C, sapendo in anticipo quante e quali variabili utilizzeremo, riserverà tutto e solo lo spazio in memoria che effettivamente serve. Dichiarare tutto, quindi ! Ecco un’ altra regoletta che, affiancata alla “un punto e virgola alla fine di OGNI istruzione” comincia a comporre il piccolo “decalogo del buon programmatore C” che alla fine del corso dovrete ricordare meglio del vostro nome. Praticamente in tutti i linguaggi di programmazione di medio/alto livello esiste una struttura dati fondamentale: l’ array. Un array non è altro che uno “schieramento” (in inglese “array”, guarda caso di variabili *dello stesso tipo*, ognuna delle quali è identificata da un *nome* (uguale per tutte) e da un *numero* (l’ “indice”, ovviamente diverso per tutte). Tornando al paragone cassetto/variabile, un array è una cassettiera. La cassettiera ha un nome unico (ad esempio “BOBI”), ed ogni cassetto è identificato da un numero. Supponendo che la cassettiera abbia quattro cassetti, avremo il cassetto 1, il cassetto 2, il cassetto 3 e il cassetto 4. La sintassi del C prevede che il cassetto/variabile appartenente ad una cassettiera/array venga identificato nel seguente modo: nome_cassettiera[numero_cassetto] L’ indice/numero del cassetto, cioè , deve essere racchiuso tra parentesi quadre, e deve seguire il nome dell’array/cassettiera. La cassettiera BOBI quindi avrà i cassetti BOBI[1], BOBI[2], BOBI[3] e BOBI[4]. Come detto, ogni cassettiera/array può ospitare solo cassetti tutti destinati a contenere un solo tipo di dato: potremo avere quindi cassettiere con cassetti di tipo intero o cassettiere con cassetti di tipo carattere, ma non cassettiere contenenti ad esempio due cassetti di tipo intero e due cassetti di tipo carattere. Bene. Per utilizzare una cassettiera, per prima cosa dovremo ovviamente dichiararla. In C un array va dichiarato nel seguente modo: tipo_cassetti nomecassettiera[numero_massimo_di_cassetti]; La cassettiera del nostro precedente esempio, quindi, supponendo debba contenere solo interi, andrà dichiarata nel seguente modo: int BOBI[4]; Chiaro, no ? Occorre a questo punto fare una precisazione essenziale: in C il primo cassetto di una cassettiera ha SEMPRE indice 0. Se abbiamo definito la cassettiera BOBI come formata da quattro cassetti, i quattro cassetti si chiameranno quindi nel seguente modo: BOBI[0] BOBI[1] BOBI[2] BOBI[3] I cassetti sono in totale quattro, come da dichiarazione, ma gli indici vanno da 0 a 3. State molto attenti a questo apparentemente strano aspetto, responsabile del maggior numero di errori nei programmi dei principianti. In C non è possibile, a differenza di altri linguaggi, definire un valore iniziale per l’indice di un array. Un array inizia SEMPRE con l’indice 0. Dichiarando una cassettiera come composta da dieci cassetti, il cassetto numero dieci NON ESISTE ! Esistono dieci cassetti, i cui indici vanno da zero a nove. Tentare di accedere al cassetto numero dà risultati non prevedibili (ma certamente indesiderati. Perfetto: sappiamo quasi tutto quanto ci serve per lavorare con gli array. Una variabile appartenente ad un array è in tutto e per tutto una variabile normalissima, identica alle semplici variabili di tipo “int” e “char” viste nelle lezioni precedenti. Ora dichiarerò un array di cinque elementi, lo riempirò con cinque caratteri a caso e li stamperò utilizzando l’ istruzione “printf”. main() { char nome[5]; nome[0]=“‘M”; nome[1]=“‘A”; nome[2]=“‘G”; nome[3]=“‘O”; nome[4]=“‘O”; printf(“%c %c %c %c %c\n”, nome[0], nome[1], nome[2], nome[3], nome[4]”); } Semplice, vero ? A questo punto del corso si potrebbe essere tentati di chiedersi: “beh, ma perchè mai dovrei complicarmi la vita utilizzando un array invece di N variabili normali ? Gli indici, le parentesi quadre, tutto inizia con lo zero, uffa, che barba...”. Un sogghigno sardonico increspa l”‘espressione dello scafato programmatore. Fin quando si lavora con sei, sette, dieci, venti variabili può essere (opinabilmente) piùcomodo lavorare senza array. Ma quando le variabili cominciano ad essere cento e passa, va da sè che anche solo dichiarare cento variabili tutte dal nome diverso (senza scomodare l”‘enorme comodità consentita dall”‘uso dei cicli, che non abbiamo ancora esaminato) non è esattamente un compito gradevole... Riassumo quanto fin qui detto sugli array: 1) un array è un raggruppamento di variabili, tutte dello stesso tipo. 2) ogni variabile appartenente all”‘array è identificata dal nome dell”‘array e da un indice numerale racchiuso tra parentesi quadrate. 3) un array si dichiara scrivendo il tipo di variabile che è destinato a contenere, il nome dell”‘array e il numero massimo di variabili che conterrà racchiuso tra parentesi quadre. 4) la prima variabile dell”‘array ha SEMPRE indice 0. Quindi se viene dichiarato un array da N variabili, l”‘indice delle singole variabili varia da 0 ad N-1. 5) una variabile appartenente ad un array è in tutto e per tutto una semplice variabile. Il tipo di dato “Float”. Introduciamo l’ ultimo tra i tipi “semplici” di dato: il tipo “float”. “Float” sta per “floating point”, ovvero “virgola mobile” (a causa della normalizzazione effettuata nelle operazioni riguardanti tali numeri): il tipo di dato “float” è utilizzato per variabili destinate a contenere numeri dotati di parte decimale. Così , la variabile main() { float valore; } potrà contenere un valore decimale, ad esempio “3.2”: main() { float valore; valore=3.2; } Come si stampa una variabile di tipo “float” ? Per mezzo della sequenza speciale “%f”: main() { float valore; valore=3.2; printf(“Il contenuto di ‘valorè’ è : %f”, valore); } Attenzione, però . In C l’ operatore divisione “/” tronca i decimali se avviene tra valori interi. La divisione 5/2 dà come risultato 2, anche se per contenerlo si destina una variabile di tipo float. Il seguente programma: main() { float valore; valore=5 / 2; printf(“5 / 2 = %f”, valore); } stamperà il numero 2. Come si può fare, allora ? Semplice: se si vuole ottenere anche la parte decimale di una divisione, bisogna che almeno uno tra dividendo e divisore sia un numero float, ovvero che abbia il punto decimale (anche se la parte decimale è 0). Per ottenere il risultato esatto di 5/2 bisogna quindi tramutare uno dei due numeri (o anche tutti e due) in numero float. Perciò sia valore=5.0 / 2; sia valore=5 / 2.0; sia valore=5.0 / 2.0; riempiono la variabile “valore” col giusto risultato (2.5). È ovviamente possibile dichiarare array di tipo float, non c’è bisogno di specificarlo, vero ? 7. I Sottoprogrammi. Un “programma” di una certa importanza non è mai composto da una serie di istruzioni eseguite in maniera rigorosamente sequenziale. In genere, oltre alla frammentazione “in grande” del sorgente in più moduli (cioè in più files separati) di cui abbiamo parlato nel capitolo precedente, si divide ulteriormente il singolo sorgente in più “pezzi”, che risiedono nello stesso file ma che sono logicamente separati. Le istruzioni cioè che svolgono un determinato e preciso compito vengono raggruppate e raccolte in una struttura che, tecnicamente, viene chiamata “sottoprogramma” (o “subroutine”, più semplicemente “routine”, spesso anche “procedura”). Ecco quindi come solitamente è organizzato un programma di medie dimensioni, diviso in due moduli. ----------MODULO_1------------- Sottoprogramma_1 istruzione 1 istruzione 2 ... istruzione N Fine sottoprogramma_1 Sottoprogramma_2 istruzione 1 istruzione 2 ... istruzione N Fine sottoprogramma_2 ... Sottoprogramma_n ... Fine sottoprogramma_n ---------FINE MODULO_1--------- ----------MODULO_2------------- Sottoprogramma_1 istruzione 1 istruzione 2 ... istruzione N Fine sottoprogramma_1 Sottoprogramma_2 istruzione 1 istruzione 2 ... istruzione N Fine sottoprogramma_2 ... Sottoprogramma_m ... Fine sottoprogramma_m ---------FINE MODULO_2--------- e così via. Ricordo che “MODULO_1” e “MODULO_2” sono due file separati, che compileremo separatamente ottenendo due file oggetto che verranno uniti insieme dal linker in modo da formare il file eseguibile. I sottoprogrammi possono “chiamarsi” l’un l’altro: cioè , ad un certo punto della loro esecuzione, possono chiedere che un altro programma venga eseguito interamente, prima di poter proseguire con l’esecuzione. Ma come fanno a chiamarsi ? Come farebbe una squadra di cuochi: per nome. Possiamo infatti immaginare che in ogni modulo risieda un certo numero di cuochi, ognuno responsabile di un compito preciso. Se il cuoco che sta facendo la pizza ha bisogno di qualcuno che gli faccia la pasta, e supponendo che nel modulo esista il cuoco Giacomino che è (guarda caso) un esperto di impasti, il primo cuoco può ad un certo punto urlare “Vai, Giacomino!”, aspettare che Giacomino gli scodelli mezzo chilo di pasta fresca e proseguire col proprio compito. In C ogni sottoprogramma è delimitato da questi due caratteri: “{” e “}”. Ogni gruppo di istruzioni racchiusa tra parentesi graffe è un sottoprogramma (non è sempre vero, ma per il momento supponete che lo sia). Ad ogni sottoprogramma inoltre è NECESSARIO dare un nome, che va scritto prima delle parentesi graffe. Infine (cosa che vedremo più approfonditamente in seguito, ma è necessario che ne anticipi adesso qualcosa altrimenti non potremmo scrivere i primi programmini) occorre evidenziare COSA passeremo al cuoco al momento della chiamata. Potremmo infatti chiamare Giacomo e passargli un chilo di farina e di uova, che lui utilizzerà per far la pasta, o possiamo dare per scontato che Giacomo possieda tutti gli ingredienti, e quindi chiamarlo senza passargli niente. Le cose che passiamo ai sottoprogrammi (che si chiamano “parametri”, ma come detto ne parleremo meglio in seguito) vanno scritte tra parentesi tonde, e vanno messe dopo il nome e prima delle parentesi graffe. Per ora scriveremo sottoprogrammi ai quali non passeremo mai niente, e quindi non dovremo mettere niente tra le parentesi tonde. In sintesi: ecco come va scritto un sottoprogramma in C. Nome_programma () { istruzioni varie } Ecco il primo modulo dell’esempio precedente scritto in pseudo-sintassi C, dopo aver chiamato in vari modi i cuochi/sottoprogrammi. ----------MODULO_1------------- Giacomo () { istruzione 1 istruzione 2 ... istruzione n } Carlino () { istruzione 1 istruzione 2 ... istruzione N } ... Giuseppino () { ... } ---------FINE MODULO_1--------- Un sorgente C è SEMPRE E SOLO una collezione di sottoprogrammi, dai vari nomi, che si chiamano vicendevolmente. Già , ma qual è il cuoco che “parte per primo” ? Quando viene lanciato l’eseguibile, a quale cuoco viene PER PRIMO ceduto il controllo (visto che in C non è possibile che più sottoprogrammi procedano parallelamente) ? Risposta: il controllo viene ceduto al cuoco che si chiama “main” (“principale”, in italiano). Deve SEMPRE esistere un sottoprogramma che si chiami “main” in un sorgente C, e deve sempre esistere UN SOLO sottoprogramma che si chiami “main”, altrimenti non sarà possibile ottenere l’eseguibile. Un sorgente C quindi è l’insieme di un sottoprogramma principale, chiamato “main” (il capocuoco), e di una serie di sottoprogrammi secondari (i sottocuochi). Ci sono alcune regole che vincolano il lavoro della nostra squadra di cuochi. Il cuoco può lavorare ed anche chiamare i sottocuochi. Non può ovviamente chiamare sè stesso. I sottocuochi possono chiamarsi tra di loro, ma non possono chiamare il capocuoco. Un programma può anche essere composto dal solo cuoco “main”, che svolge tutto il lavoro senza bisogno di nessun sottocuoco. Ora alcune precisazioni stilistiche sulla scrittura dei programmi in C: il C è un linguaggio detto “case sensitive”, ovvero considera come differenti lettere minuscole e lettere maiuscole. Quando dico che il cuoco principale si deve chiamare “main”, intendo proprio “main”. Non “MAIN”, nè “Main”, nè “MaIn”. Solo ed esclusivamente “main”, tutto in minuscolo. Se ad un cuoco/sottoprogramma dò come nome”Giacomo”, quando ne vorrò invocare l’esecuzione dovrò sempre scrivere “Giacomo”. Se chiamassi “giacomo”, o “GiAcOmO” non otterrei risposta. Questa è l’unica restrizione importante, che varrà sempre, anche quando in futuro comincerò a parlarvi dei comandi. Quando parlerò della parola chiave “while”, voi dovrete usare solo ed esclusivamente “while”, tutto in minuscolo. Per il resto siete liberi di usare tutti gli spazi, tutti gli “a capo” e tutti i caratteri di tabulazione che volete, a patto che non li usiate all’interno delle parole. Questi sono tutti esempi di sottoprogrammi C perfettamente leciti: -----ESEMPIO_1------ Giacomo ( ) { istruzioni } ---FINE_ESEMPIO_1--- -----ESEMPIO_2------ Giacomo () { istruzioni } ---FINE_ESEMPIO_2--- -----ESEMPIO_3------ Giacomo ( ) { istruzioni } ---FINE_ESEMPIO_3--- Questo invece è un esempio di sottoprogramma NON lecito: ---ESEMPIO_NON_LECITO--- Gia como () { } ------FINE_ESEMPIO------ Siete assolutamente liberi di adottare lo stile che più preferite. È però consuetudine scrivere i sottoprogrammi in questo formato: nome_sottoprogramma () { istruzione_1 istruzione_2 .... istruzione_N } ovvero scrivere sulla stessa linea il nome del sottoprogramma e le parentesi tonde (che conterranno i parametri, quando avremo imparato ad usarli), andare a capo, scrivere la parentesi graffa aperta e nient’altro, andare a capo, scrivere le istruzioni indentate di una tabulazione ed infine scrivere la parentesi graffa chiusa da sola. Attenzione: nè gli spazi, nè gli “a capo”, nè le tabulazioni influiscono sulla lunghezza finale dell’eseguibile. Usatene pure quanti ne volete ! Non crediate che meno ampio sia il sorgente, più corto sarà l’eseguibile ! Niente e nessuno vi vieta di scrivere i vostri sottoprogrammi così : nome_sottoprogramma(){istruzione_1istruzione_2...istruzione_N} ma non è necessario che si spendano altre parole per convincersi che non è esattamente la maniera giusta per scrivere sorgenti comprensibili. Riscriviamo il programma precedente del Capitolo 1, spezzandolo in tre funzioni, che in C si indicano facendo precedere il loro nome dalla dichiarazione del tipo (void, int, ...): void video() void leggi_intero() void leggi_reale() N.B. La parola void indica che le funzioni eseguono delle operazioni senza ritornare alcun valore al main. La prima pulisce lo schermo e scrive una intestazione, la seconda legge e scrive un intero e la terza un decimale. Dopo il nome della funzione si devono inserire sempre le parentesi che possono contenere delle variabili. Le funzioni che vengono chiamate dal main devono essere dichiarate all’inizio del programma, in modo che il compilatore le possa riconoscere. L’unica nuova istruzione che useremo è la: gotoxy(x,y) che posiziona il cursore alle coordinate (x,y) dello schermo. Questo è il semplice programma che può essere compilato ed eseguito. #include void video(); Dichiarazione delle funzioni void leggi_intero(); void leggi_reale(); main() { video(); Chiama la funzione che scrive il titolo leggi_intero(); Chiama la funzione che legge e scrive un numero intero leggi_reale(); Chiama la funzione che legge e scrive un numero reale getch(); return 0; } void video() Scrive l’intestazione {clrscr(); gotoxy(23,1); printf(“PROGRAMMA CHE LEGGE E SCRIVE DUE NUMERI \n\n”);} void leggi_intero() Legge e scrive un intero {int n; printf(“\n\n\n Scrivi un numero intero: ”); scanf(“%d”, &n); printf(“ Intero: %d”, n);} void leggi_reale() Legge e scrive un reale {float s; printf(“\n\n\n Scrivi un numero reale: ”); scanf(“%f”, &s); printf(“ Reale: %f\n”, s); } 8. L’istruzione “printf” : qualcosa in più. Scrivere sullo schermo qualcosa, è una delle più semplici ed utili azioni che un proramma può effettuare. In C si ottiene la stampa di qualcosa sullo schermo utilizzando l’istruzione “printf”. // : In BASIC si usa la “print”, ma non servono le parentesi; in Pascal la “write” [NON la writeln, attenti]). Ciò che si desidera stampare va scritto tra due parentesi tonde, e tra due virgolette. Ecco un semplicissimo programma che stampa la scritta “Ciao a tutti!” sullo schermo. main() { printf(“Ciao a tutti !”); } (provate ad ottenere un eseguibile da questo sorgente !) Notate il punto e virgola alla fine dell’istruzione, assolutamente necessario, come già detto. Avete visto che l’istruzione è racchiusa nella procedura di nome “main”: ricorderete dalla precedente lezione che questo è il nome della procedura che per default viene mandata in esecuzione per prima (e in questo caso per ultima. Avete provato ad ottenere l’eseguibile ? Bello, eh ? Provate a scrivere programmi che scrivono cose sullo schermo utilizzando più istruzioni “printf”; ad esempio: main() { printf(“Ciao a tutti !”); printf(“Come state ?”); printf(“Io benissimo !”); } (ricordate: ATTENTI al “punto e virgola” alla fine di ogni istruzione!). Se avete provato ad eseguire quest’ultimo sorgente, avrete notato che TUTTO viene stampato dall’eseguibile di seguito, sulla stessa linea. Ovvero, questo è il risultato sullo schermo derivante dall’esecuzione del programma: Ciao a tutti!Come state ?Io benissimo! È un comportamento normalissimo. Non importa che le istruzioni di stampa siano tre diverse, e su tre linee diverse (questo, se ricorderete quanto detto nelle lezioni precedenti riguardo all’estrema libertà di cui godete nel posizionamento delle istruzioni all’interno del sorgente, importa ancora di meno): l’istruzione “printf” inizia a stampare dalla posizione in cui la precedente istruzione “printf” ha smesso di stampare ! La prima istruzione stampa “Ciao a tutti!”, la seconda inizia a stampare “Come state ?” esattamente dal punto in cui la2 prima ha smesso il proprio lavoro, ovvero alla fine di “Ciao a tutti !”. Come “andare a capo”, quindi ? Come è possibile stampare su diverse linee ? Occorre utilizzare il simbolo speciale “\n” (una “barra” seguita dal carattere “n” minuscolo. Attenzione: QUELLA “barra”, e una “n” MINUSCOLA ! Attenti a non usare l’altra barra, la “/”, oppure a non scrivere in maiuscolo la “N” !). La “n” sta per “Newline”, la “barra” serve ad indicare che quella “n” non è una “n” normale, ma è una “n” che ordina al programma “ok, cuoco, ora vai a capo”. Proviamo ad eseguire il seguente sorgente: main() { printf(“Ciao a tutti !\n”); printf(“Come state ?”); } Ecco l’output del programma: Ciao a tutti ! Come state ? Il programma ha stampato la prima stringa, è andato a capo, ed ha stampato la seconda stringa. Evviva ! A questo punto sappiamo praticamente fare tutto. Non credo vi sia il bisogno di specificare che il simbolo “\n” NON viene stampato. E se voleste inserire una linea vuota tra le due stringhe ? Se voleste, cioè , che l’output del vostro programma fosse: Ciao a tutti ! Come state ? come dovreste fare ? Semplice: aggiungere un altro simbolo di newline. Ecco il sorgente che dà come output ciò che vogliamo: main() { printf(“Ciao a tutti!\n\n”); printf(“Come state ?”); } Visto ? Due simboli di newline di seguito: “\n\n”. È ovvio che potete anche metterne in file tre, oppure quattro, o addirittura cinque, o perfino settecentosettantaquattro ! Attenzione ! L’OUTPUT (ovviamente !) NON CAMBIA se mettete il simbolo di newline nella seconda istruzione: main() { printf(“Ciao a tutti !\n”); printf(“\nCome state ?”); } Potete giocare col newline come meglio preferite ! Potete metterlo dove volete, potete anche utilizzare una nuova istruzione: main() { printf(“Ciao a tutti!”); printf(“\n\n”); printf(“Come state ?”); } Visto ? Avete la massima libertà , da questo punto di vista. Dato che il simbolo di newline può essere usato come se fosse un qualsiasi altro carattere (seppur dagli effetti particolari), e quindi può essere seguito da qualsiasi sequenza di caratteri, a volte è possibile che l’istruzione che lo racchiuda diventi poco leggibile; ad esempio, non è molto chiaro ad un primo sguardo che la seguente istruzione: printf(“mia\nmamma”); stampa la parola “mia” e, su di una nuova linea, la parola “mamma”. Si sarebbe tentati di dire che l’istruzione stampa la parola “mia” e, dopo qualcosa di non ben definito, la parola “nmamma” (ad una primissima occhiata, certo). E invece no ! Noi, oramai acuti e scaltri programmatori, non mancheremo di notare la “barra”, prima della “n”, che identifica quella “n” come simbolo di “newline”. Cominciate a capire perché i sorgenti C hanno la sinistra fama di essere poco leggibili ? // : In BASIC l’istruzione PRINT va sempre a capo automaticamente dopo aver stampato la stringa passatagli come argomento, A MENO CHE non si metta, subito dopo un “punto e virgola”. In Pascal si usano due istruzioni diverse: la “write” non va a capo, dopo aver stampato la stringa passatagli come argomento. La “writeln” invece sì ). Ed ora forza ! Sperimentate, scrivete qualche sorgente, compilatelo ed eseguitelo ! Datevi da fare ! ESERCIZI. 1) Dov’è l’errore nei seguenti programmi ? a) main() { printf(“Ciao, Andrea !\n”) printf(“Come va la vitaccia ?”); } R: Manca il punto e virgola alla fine della prima istruzione “printf”. Il punto e virgola va messo alla fine di TUTTE le istruzioni. Ecco il programma corretto: main() { printf(“Ciao, Andrea !\n”); printf(“Come va la vitaccia ?”); } b) main(); { printf(“Ciao, Pippo !”); } R: Come ricorderete, il punto e virgola NON VA dopo i nomi delle procedure. Quindi il punto e virgola dopo “main()” è sbagliato: il compilatore si infurierebbe. Ecco il programma corretto: main() { printf(“Ciao, Pippo !”); } c) main() { Printf(“Ciao, Ciccio !”); } R: La “P” maiuscola di “Printf” !!! Per il compilatore C “Printf” non corrisponde a nessuna istruzione. Ricorderete che il C è “case sensitive”, ovvero considera le maiuscole diverse dalle minuscole, e viceversa. Ecco il programma corretto: main() { printf(“Ciao, Ciccio !”); } d) main() {printf(“Ciao, Marco !);} R: L’errore è SOLO nelle virgolette “non chiuse”: dopo “padellone !” ci vogliono le virgolette. Non importa che sia “tutto su di una riga”. Ecco il programma corretto: main() {printf(“Ciao, Marco !”);} 2) Quale sarà l’output del seguente programma: main() { printf(“Fra Martino campa”); printf(“naro, dormi tu, dormi”); printf(“tu\n\nsuona le campane, suona”); printf(“ le campane, din\ndon\ndan.”); } R: Fra Martino campanaro, dormi tu, dormitu suona le campane, suone le campane, din don dan. Notate il “dormitu”. Infatti la terza printf incomincia a scrivere nel punto in cui la seconda ha smesso. La seconda termina con “dormi”, la terza inizia con “tu”. Quindi apparirà “dormitu”. Se avessimo voluto le due parole separate da uno spazio, avremmo dovuto mettere lo spazio alla fine della seconda stringa (Es.: printf(“(...) dormi “);),oppureall’inizio della terza stringa (Es.: printf(“ tu (...)”);). 9. L’istruzione “printf” : ancora di più. Premessa: nel passato e nel futuro ho detto e spesso impropriamente dirò , ad esempio, che “il compilatore, quando trova l’istruzione <>, stampa quello che trova tra virgolette”. È una semplificazione, perché evidentemente (se ricordate quanto detto nelle prime lezioni) non è il compilatore che ESEGUE materialmente il programma. Il compilatore semplicemente TRADUCE l’istruzione in codice macchina, e sarà il computer che effettuerà materialmente l’azione. Dovrei quindi dire “il computer, quando trova l’istruzione <>, stampa...”, ma neanche questo va bene perché nemmeno: il computer non incontra MAI l’istruzione printf, ma la sua traduzione in codice macchina. La forma corretta dovrebbe essere “il compilatore, quando incontra l’istruzione <>, genera del codice macchina che quando verrà eseguito dal computer stamperà quanto contenuto tra virgolette”. Mi sembra chiaro che incolpando di tutto il compilatore risparmio un bel pò di caratteri. Vediamo ora qualcosa in più riguardo alla fondamentale istruzione “printf”. Metteremo molta carne al fuoco, in questa sezione, quindi molto attenti ! È possibile dire all’istruzione “printf” cosa stampare anche in un altro modo. Al momento potrebbe sembrarvi oscuro il motivo di questo strano metodo, perché sembrerebbe complicare inutilmente le cose: vedremo in seguito quanto sia invece fondamentale. Possiamo addirittura dire che se l’istruzione “printf” non funzionasse così , non servirebbe praticamente a niente... Poniamo di voler far stampare questa stringa di caratteri: Io ho 10 anni. Procedendo come abbiamo imparato a fare nella lezione scorsa, dovremmo scrivere il seguente programma: main() { printf (“Io ho 10 anni.”); } Ma otterremo lo stesso output anche se facessimo così : main() { printf (“Io ho %d anni.”, 10); } Argh ! Che roba è ? Niente di tanto brutto quanto sembra. Basta considerare i due caratteri “%d” come “sequenza speciale” (un pò come il “\n”): quando il compilatore C incontra questa coppia di caratteri, NON li stampa. Lui sa che quando trova quei due caratteri, deve prendere la roba che trova DOPO la stringa tra virgolette (separata da questa da una virgola) e stamparla. Nel nostro caso il compilatore stampa “Io ho “. Poi trova il “%d”. “Orpo”, pensa tra sé , “devo andare a cercare alla fine della stringa qualcosa da stampare in questo punto”. Dopo la stringa trova un 10. “Perfetto: stampo il 10”. Dopo aver stampato il 10, continua a stampare il resto della stringa: stamperà quindi “ anni.” e si fermerà . Qualcuno, pensando a come funzionano le istruzioni equivalenti in altri linguaggi, potrebbe chiedere: “ma alla fine non dovrebbe stampare un’altra volta il 10, dato che lo trova dopo la stringa ?”. La risposta è ovviamente no. La “f” di “printf” significa “formatted”: “formatted print”, “stampa formattata”, o meglio “stampa su formato”. Il “formato” è rappresentato dalla PRIMA STRINGA (che chiameremo “stringa-formato”). TUTTO CIÒ CHE SEGUE SONO DATI, che verranno stampati dove previsto dalla stringa-formato. Vediamo un esempio più complesso, ma che dovrebbe chiarire ulteriormente le cose (mostrerò solo l’istruzione, tralasciando di includerla nella procedura principale “main() { }”). printf (“Io ho %d anni, sono alto %d metro e %d centimetri”, 10, 1, 5); L’output sarà : Io ho 10 anni, sono alto 1 metro e 5 centimetri. I dati che seguono la stringa-formato vengono “sostituiti” ordinatamente alle sequenze speciali “%d”, non appena incontrate. Chiaro, no ? La sequenza speciale “%d” non deve essere seguita né preceduta da caratteri particolari. Il compilatore C è abbastanza in gamba per riuscire a “vederla” anche in mezzo ad una stringa molto confusa Questa istruzione: printf(“Io ho %d%d anni.”, 1, 0); darà come output: Io ho 10 anni. Notate le due sequenze “%d%d” attaccate ? (Qualcuno a questo punto potrebbe pensare: “Puff, bel linguaggio del cavolo, il C. Quindi, se io volessi stampare sullo schermo PROPRIO i due caratteri “%d”, oppure i due “\n”, non potrei ?”. “Ma non scherziamo”, la risposta c’è ma la vedremo in seguito). Ö : possiamo anche far sì che il compilatore ottenga il dato da stampare dopo un calcolo ! Calcolo che può essere complesso quanto si vuole, anche usando parentesi. La seguente istruzione: printf (“Quattro più venti meno sette per due è uguale a %d”, (4+20-7)*2); darà come output: Quattro più venti meno sette per due è uguale a 34 Notevole, eh ? Possiamo ovviamente fornire come dati da stampare più formule in sequenza, sempre separate da una virgola: printf (“Due più due uguale a %d, mentre due per due uguale a %d.”, 2+2, 2*2); Ö : In C l’operatore aritmetico di moltiplicazione “per” è l’asterisco. // : Anche nella maggior parte dei linguaggi di programmazione l’operatore “per” è l’asterisco. Bene. Ma perché ho usato sempre dei numeri ? Se come dato fornissi qualche altra cosa, ad esempio una lettera ? Non otterremmo il risultato voluto. La sequenza “%d” stamperà SEMPRE un numero INTERO (i decimali li vedremo in seguito), ANCHE se passassimo un carattere (il numero in questo caso ne sarebbe il codice ASCII ! Ma considereremo meglio questo aspetto in seguito). La “d” di “%d” sta per “digit” (cifra). Se volessimo stampare UN carattere, dovremmo usare la sequenza “%c”. ATTENZIONE: perché sia riconosciuto come tale, il carattere (che, al solito, va messo DOPO la stringa-formato) VA INCLUSO TRA DUE APOSTROFI. Questo ad esempio è un carattere che il C riconosce come tale: ‘A’ . Il carattere deve essere UNO SOLO. ‘AB’ NON è un “carattere”. (Non è nemmeno una stringa: è semplicemente un errore). Vediamo un esempio. printf (“Le prime %d lettere dell’alfabeto sono %c, %c e %c.”, 3, ‘A’, ‘B’, ‘C’); Quale sarà l’output ? Semplice: il compilatore incontra in questo ordine sequenze speciali e dati: %d %c %c %c 3 ‘A’ ‘B’ ‘C’ ed in tale ordine sostituirà i secondi alle prime. L’output quindi sarà : Le prime 3 lettere dell’alfabeto sono A, B e C. Anche per la sequenza speciale “%c” vale ovviamente quanto detto per la “%d”: non deve essere né seguita né preceduta da nessun carattere particolare. L’istruzione seguente, perfettamente valida: printf(“%c%c%c%c%c”, ‘C’, ‘i’, ‘a’, ‘o’, ‘!’); stamperà : Ciao! La “c” di “%c” sta per “carattere” (“character”). Abbiamo quindi visto due tipi di dati fondamentali: il tipo di dato “numero intero” (d’ora in avanti semplicemente “intero”) (sequenza speciale “%d”) e il tipo di dato “carattere” (sequenza speciale “%c”). Attenzione: anche i numeri sono “caratteri” ! Es.: ‘1’, SE scritto così , è un carattere ! Se invece è scritto senza apostrofi, è un numero. Non date molto peso a questa affermazione, notate solo che le due istruzioni seguenti: printf(“Io mangio %d pizze.”,7); printf(“Io mangio %c pizze.”, ‘7’); pur se diverse nella forma, danno lo stesso output: Io mangio 7 pizze. Nella prima istruzione il 7 è stampato come “intero”, nella seconda è stampato come “carattere”. Esaminiamo un ultimo tipo di dato: il tipo “stringa”, associato alla sequenza speciale “%s” (“s”, da “stringa”). I dati di tale tipo sono formati da sequenze di caratteri alfanumerici, racchiuse tra virgolette. Facile (spero) capire quale output dia la seguente istruzione: printf (“Il mio nome è %s, ficcatelo bene in mente.”, “Nessuno”); Ecco l’output: Il mio nome è Nessuno, ficcatelo bene in mente. Notate: l’istruzione “printf” ha come argomenti due stringhe, separate da una virgola. Ma la prima è la stringa-formato, la stringa che decide DOVE E COME la seconda verrà stampata. Ecco una istruzione bella zeppa di roba (notate la sequenza dei dati su righe diverse, come d’uso in casi di questa complessità ): printf(“I primi %d nomi dei %c nani sono:\n%s, %s e %s.”, 3, ‘7’, “Eolo”, “Pisolo”, “Mammolo”); L’output sarà : I primi 3 nomi dei 7 nani sono: Eolo, Pisolo e Mammolo. Come vedete è ormai difficile restare attaccati all’esempio del cuoco. Ma non temete, lo ritroveremo presto. ESERCIZI Prescindendo da quanto il compilatore possa dire, trovare gli errori nelle seguenti istruzioni. 1) printf(“Il mio %s si chiama %c%c%c%c%c.”, “cane”, “M”, “A”, “G”, “O”, “O”); R: Molto facile. Il gruppo di “%c%c%c%c%c” attende altrettanti CARATTERI (racchiusi tra apostrofi), e non STRINGHE (racchiuse tra virgolette), come nell’esempio. Ricordate: “M” secondo il C NON è un carattere. È una stringa lunga 1. Perciò l’istruzione corretta è : printf(“Il mio %s si chiama %c%c%c%c%c.”,”cane”,’M’,’A’,’G’,’O’,’O’ ); “cane” è correttamente tra virgolette: è una stringa, e proprio una stringa era attesa dal “%s”. Ma NESSUN PROGRAMMATORE C, per scrivere “MAGOO”, userebbe cinque caratteri in sequenza. 2) printf(“Io posseggo %c computer.\n”, ‘2’); R: Trabocchetto. In questa istruzione non ci sono errori. Spero non siate caduti nel tranello (pensando, ad esempio: “orpo, 2 è un numero, quindi ci vuole “%d” e non “%c””). Attenzione 2 è un numero, ma “2” è un carattere ! L’istruzione è corretta, come altrettanto corretta sarebbe stata la seguente: printf(“Io posseggo %d computer.\n”,2); SENZA gli apostrofi. 3) printf(“L’altro giorno ho mangiato %c bastoncini di pesce.\n”, ‘23’); R: Facile. “23” NON è un carattere. Se il numero da stampare è proprio 23, si può: · O stampare una stringa “...mangiato %s bastoncini...” , “23” · O stampare un intero “...mangiato %d bastoncini...” , 23 · O stampare due caratteri di seguito “...mangiato %c%c bastoncini...” ,”2”,”3” 4) printf(“Il mio %s si chiama %s”, “Magoo”, “cane”); R: L’istruzione è formalmente corretta, ma logicamente sbagliata. L’output (assurdo) sarebbe: Il mio Magoo si chiama cane perché , come sapete, le stringhe “figlie” vengono passate alla stringa formato nell’ordine in cui sono elencate all’interno dell’istruzione. Per ottenere un output non assurdo, l’istruzione corretta dovrebbe essere la seguente: printf(“Il mio %s si chiama %s”, “cane”, “Magoo”); 5) printf(“Spero che %d anni basteranno...”, “due”); R: Questa è scandalosa. “due” ovviamente NON è un numero. “%d” attende un intero. Noi dobbiamo passargli un intero, non la descrizione di un intero. 10. Alcune curiosità sull’istruzione “printf”. · La seguente istruzione: printf(“%d”,7/2); Restituisce il valore 3 perché la sequenza “%d” stampa SEMPRE un numero intero. Se gli passiamo un decimale, questo verrà troncato (senza alcun tipo di arrotondamento) e sarà stampata solo la parte intera. · Anticipando un argomento si può dire che è possibile inserire anche variabili. Supponendo che “vartest” sia una variabile di tipo stringa, l’istruzione: printf(“Ecco il contenuto della variabile: %s.”, vartest); stamperà il contenuto della variabile “vartest”. 11. I Puntatori. Con questa lezione entriamo nel vivo del C introducendo i puntatori. Un puntatore è una variabile che contiene l’indirizzo di una variabile. Se per esempio definisco l’intero x: int x; e gli assegno il valore 3: x = 3; per stabilire un collegamento fra la variabile x ed un puntatore devo prima definire il puntatore: int *p; (si dichiara, come per una normale variabile il tipo (int, float, etc.) e si fa precedere il nome del puntatore da una stella) e poi dichiarare il collegamento tra variabile e suo puntatore: p=&x; si scrive una & prima della variabile per indicare che si tratta di un indirizzo. A questo punto *p equivale effettivamente alla x e posso usarlo per scriverne il valore numerico: printf(“%d”, *p); Nel caso di un vettore la corrispondenza è ancora più significativa. Per esempio posso scrivere: int v[10]; int *p; p=v; in questo modo si è stabilita una corrispondenza tra il puntatore ed il vettore e da ora in poi si potrà praticamente usare il puntatore per tutte le operazioni. Per indicare le varie componenti del vettore basterà specificare il giusto puntatore: *(p+3)2 fornisce la quarta componente del vettore v. Attenzione al fatto che la scrittura: *p+3 non incrementa il puntatore di 3 unità, ma semplicemente aggiunge il numero 3 al valore della componente puntata in quel momento! Tutto quanto detto sui puntatori risulterà più chiaro in seguito con il loro uso 8effettivo. 12. I Cicli (While - For). Introduciamo ora i cicli for e while che ci saranno molti utili. Il ciclo for si realizza così: for(i=1; i<=10; i++) {printf(“%d ”, i)}; indicando il valore di partenza della variabile i=1, quello di arrivo i<=10, e il tipo di variazione della i, che in questo caso è l’incremento di una unità alla volta i++. Le istruzioni eseguite ad ogni ciclo sono quelle tra parentesi graffe. (Ricordiamo che per comodità in C l’incremento di una variabile si indica con l’espressione i++ e il decremento con i--). Un ciclo while si scrive: while(i!=10) {printf(“%d ”,i); i++} il ciclo continua fin tanto che l’espressione logica tra parentesi tonde risulta vera (nel nostro caso fino a quando i è diversa da 10). (Ricordiamo che la negazione logica in C si indica con: != e l’uguaglianza con ==. Il simbolo = serve invece per le normali assegnazioni, ha cioè il valore di uguale consueto). 13. La direttiva #DEFINE L’istruzione #define che permette di assegnare una costante numerica o di tipo stringa all’inizio del programma, per esempio: #define titolo “PUNTATORI” #define vero 1 #define falso 0 in questo modo sarà molto più facile gestire quelle costanti che si ripetono spesso nel programma, avendole identificate con un nome simbolico appropriato (titolo, vero, falso...) e facile da ricordare. Diamo ora di seguito il programma che illustra con esempi pratici quanto detto sui puntatori e sui cicli for e while e la direttiva #define. #include void video(); main() { int x, y, i; int *px, *pv; Dichiarazione di due puntatori di tipo intero int v[]={3,6,9,12,15}; Assegnazione delle componenti di un vettore #define titolo “PUNTATORI, FOR e WHILE” Assegnazione di una stringa video(); x=13; px=&x; px è il puntatore della variabile x printf(“\n\n Valore di x indicato da *px: ”); printf(“%d”, *px); *px+=5; Sommo 5 al valore puntato da px printf(“\n\n Somma del valore di x indicato da *px e di 5: ”); printf(“%d”, *px); printf(“\n\n Stampa del vettore con un ciclo for: ”); for(i=0; i<=4; i++) printf(“%d ”,v[i]); pv=v; pv è il puntatore del vettore v printf(“\n\n Stampa della quarta componente di v: ”); printf(“%d ”, *(pv+3)); printf(“\n\n Somma della prima componente (3) e del numero 2: ”); printf(“%d ”, *pv+2); i=0; printf(“\n\n Stampa di v tramite il puntatore: ”); while(i!=5) {printf(“%d ”, *pv++); i++;}; getch(); return 0; } void video() Scrive l’intestazione {clrscr(); gotoxy(26,1); printf(titolo); printf(“\n\n”); } Esercizi. 1. Scrivere i primi dieci interi con un ciclo for. R: main() {int i; for(i=1; i<=10; i++) printf(“ %d”,i); } 2. Scrivere i primi dieci interi in ordine inverso con un ciclo while. R: main() {int i; i=10; while(i!=0) {printf(“ %d”,i);i--;} } 3. Costruire con due cicli for innestati la tabellina delle moltiplicazioni. R: main() {int i, j; for(i=1; i<=9; i++){ for esterno per le righe for(j=1; j<=9; j++) printf(“%3d ”,i*j); for interno per le colonne printf(“\n”); } } N.B. Nella printf il 3, fra il % e la d, lascia uno spazio minimo 3 tra un numero e l’altro, il che permette l’allineamento verticale. 4. Realizzare un ciclo for che legga cinque numeri interi e li copi in un apposito vettore. R: main() {int i, v[10]; for(i=0; i<5; i++){printf(“Dato %d = ”,i+1); scanf(“%d”, &v[i]); } for(i=0; i<5; i++)printf(“%d “, v[i]); } 5. Scrivere un programma per il calcolo del valor medio di n numeri. R: main() {float valore_medio, somma; int n_dati, i; float v[50]; Dichiarazione di un vettore printf(“\n Numero dati: ”); scanf(“%d”, &n_dati); for(i=0; i static int ns; Dichiarazione della variabile intera ns come static (cioè globale) void video(); void scrivi_reale(float x); Funzione void con parametro in entrata void scrivi_intero(int x); Funzione void con parametro in entrata int leggi_intero(); Funzione int con parametro in uscita void leggi_reale(float *); Funzione void con parametro float in uscita void leggi_intero_2(); Funzione void senza parametri main() { #define titolo “GLI ARGOMENTI DELLE ROUTINE” Assegnazione di una stringa const float pi_greco = 3.141592; Assegnazione di una costante float radice_due, s; int n; video(); scrivi_reale(pi_greco); Chiama la funzione che scrive radice_due = 1.414213; Assegnazione di un reale scrivi_reale(radice_due); Passa alla void il reale n = leggi_intero(); Riceve dalla int() un intero scrivi_intero(n); Passa l’intero alla funzione leggi_reale(&s); Riceve un reale dalla funzione scrivi_reale(s); leggi_intero_2(&ns); ns è static ed è disponibile scrivi_intero(ns); getch(); return 0; } void video() Scrive l’intestazione {clrscr(); gotoxy(24,1); printf(titolo); printf(“\n\n”); } int leggi_intero() Legge un intero {int n; printf(“\n\n\n Scrivi un numero intero: ”); scanf(“%d”, &n); return n; } void scrivi_intero(int n) Scrive un intero {printf(“\n\n Intero: %d”, n);} void scrivi_reale(float x) Scrive un reale {printf(“\n\n Reale: %f”, x);} void leggi_reale(float *s) Legge un reale {printf(“\n\n\n Scrivi un numero reale: ”); scanf(“%f”, &*s);} void leggi_intero_2() Legge un intero { printf(“\n\n\n Scrivi un numero intero: ”); scanf(“%d”, &ns); } Esercizi. 1. Scrivere una funzione che faccia l’elevazione a potenza di un numero intero. R: int Potenza(int n, int esponente); main() {int p; p=Potenza(2, 7); printf(“\nLa potenza vale: %d”, p); } int Potenza(int n, int esponente) Fa l’elevazione a potenza {int i, p; if(esponente==0) return 1; else{ p=1; for(i=1; i<=esponente; i++) p=p*n; return p; } } 2. Stesso esercizio ma con tecnica ricorsiva (S una funzione che chiama se stessa ripetutamente). R: int Potenza(int n, int esponente); int main() {int p; p=Potenza(2, 7); printf(“\nLa potenza vale: %d”, p); } int Potenza(int n, int esponente) Fa l’elevazione a potenza {if(esponente==0) return 1; else return(n*Potenza(n,esponente-1));} 3. Scrivere una funzione che calcoli il valor medio di n numeri. R: void Valor_medio(float v[50], int n, float *s); main() {int i; float v[]={1.25, 2.34, 3.00, 4.56, 0.76}; float *s, media; Valor_medio(v, 5, &media); printf(“\nValore medio= %f”, media); } void Valor_medio(float v[50], int n, float *s) Fa la media di n valori {int i; float sum; sum=0; for(i=0; i=‘a’) && (c<=‘z’)) { . . .} cerca i caratteri compresi tra la a e la z. (In C l’operatore logico AND si indica con : && e quello OR con : ||) Ancora sulle stringhe: abbiamo voluto migliorare la routine di scrittura dello header (titolo di testa). In particolare abbiamo scritto una breve routine che calcola la lunghezza di una stringa in modo che la solita funzione “schermo” è ora in grado di centrare la scritta. Infine, per vignettare lo schermo abbiamo costruito una semplice funzione con i caratteri ascii: ¸ (184), ¾ (190), Õ (213), Ô (212) Ä (196), Í (205), ³ (179), ´ (180) ¿ (191), À (192), Ù (217), Ú (218) Ecco il solito programmino. #include void schermo(int n); void screenview(); PROGRAMMA PRINCIPALE int main() { #define testo_titolo “ STRINGHE” int i, c, ch, ln, n, n1, n2, n3; int v[80]; n=lunghezza_stringa(testo_titolo); Chiamata della routine che calcola la lunghezza di una stringa schermo(n); gotoxy(5,3); printf(“Scrivi un carattere e poi premi enter: ”); c=getchar(); Trasferisce un carattere in c gotoxy(5,5); printf(“Il carattere è: ”); putchar(c); Scrive il carattere c c=getchar(); Questa è una getchar di comodo i=0; Azzeramento del contatore i gotoxy(5,7); printf(“Scrivi una stringa di pochi caratteri: ”); while((c=getchar())!=‘\n’) Legge uno dopo l’altro i caratteri {v[i]=c; i++;} e li copia nel vettore v ln=i; Lunghezza della parola immessa gotoxy(5,9); printf(“La lunghezza della stringa è: %d”,ln); gotoxy(5,11); printf(“I codici ascii dei caratteri sono:”); gotoxy(5,12); for(i=0; i=97)&&(v[i]<=122)) n1++; if((v[i]>=65)&&(v[i]<=90 )) n2++; if((v[i]>=48)&&(v[i]<=57 )) n3++; } gotoxy(5,15); printf(“Lettere minuscole %d”,n1-1); gotoxy(5,16); printf(“Lettere maiuscole %d”,n2-1); gotoxy(5,17); printf(“Numeri %d”,n3-1); gotoxy(5,18); printf(“Altro %d”,ln-n1-n2-n3+3); getch(); return 0; } FINE DEL PROGRAMMA PRINCIPALE int lunghezza_stringa(char s[]) Calcola la lunghezza di s {int i; i=0;while(s[i]!=‘\0’) i++; return i; } void schermo(n) Scrive l’intestazione {clrscr(); screenview(); gotoxy(40-n/2,1); printf(testo_titolo); } void printat(int x,int y, char s1[]) Scrive un msg { gotoxy(x,y);cprintf(“%s”,s1);} void screenview() Disegna il quadro { int j,x0,x1,y0,y1; x0=1;y0=1;x1=80;y1=24; printat(x0,y0,”Õ”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“¸”); for(j=1;j<=y1-4;j++){printat(x0,y0+j,“³”); gotoxy(x0+x1-1,y0+j);cprintf(“³”);} printat(x0,y0+y1-4,“Ô); for(j=1;j<=x1-2;j++) cprintf(“Ä”);cprintf(“´”); printat(x0,y0+y1-3,“³”);printat(x0+x1-1,y0+y1-3,“³”) ; printat(x0,y0+y1-2,“³”);printat(x0+x1-1,y0+y1-2,“³”); printat(x0,y0+y1-1,“À”); for(j=1;j<=x1-2;j++) cprintf(“Ä”);printat(x0+x1-1,y0+y1+1,“Ù”);} Esercizi. 1. Assegnare una stringa con #define e scriverla carattere per carattere. R: main() {#define prova “LE STRINGHE” int i; i=0; while(prova[i]!=‘\0’){printf(“\n%c”, prova[i]);i++;}} 2. Stesso esercizio ma scrivendo le componenti del vettore stringa e i rispettivi codici ascii indicando esplicitamente la componente del vettore. R: main() {#define s “PIPPO” printf(“\n%c codice ascii= %d”, s[0], s[0]); printf(“\n%c codice ascii= %d”, s[1], s[1]); printf(“\n%c codice ascii= %d”, s[2], s[2]); printf(“\n%c codice ascii= %d”, s[3], s[3]); printf(“\n%c codice ascii= %d”, s[4], s[4]); } 3. Assegnare una stringa carattere per carattere. R: main() {int i; char s[5]; s[0]=‘M’; s[1]=‘I’; s[2]=‘N’; s[3]=‘N’; s[4]=‘I’; for(i=0; i<=4; i++) printf(“\n%c”, s[i]);i++;} 4. Far leggere una stringa dal programma carattere per carattere e assegnarne l’ultimo con ‘\0’. R: main() {int i; char c, s[10]; i=0; printf(“Scrivi una stringa: ”); while((c=getchar())!=‘\n’) {s[i]=c; i++;} s[i]=‘\0’; printf(“%s”, s); } 5. Scrivere una routine capace di contare i vari tipi di caratteri contenuti in una stringa. R: main() {int i, n1, n2, n3; #define s “abABC1234” n1=0;n2=0;n3=0; i=0; while(s[i]!=‘\0’) { if((s[i]>=‘a’) && (s[i]<=‘z’)) n1++; if((s[i]>=‘A’) && (s[i]<=‘Z’)) n2++; if((s[i]>=‘0’) && (s[i]<=‘9’)) n3++; i++;} printf(“\nLettere minuscole %d”,n1); printf(“\nLettere maiuscole %d”,n2); printf(“\nNumeri %d”,n3); } 6. Scrivere un prg che calcoli la lunghezza di una stringa. R: main() {int i, n; #define s “LE STRINGHE SONO VETTORI” i=0; while(s[i]!=‘\0’) i++; printf(“\nLunghezza della stringa %d”, i); } 7. Stesso esercizio ma realizzando una funzione. R: main() {int n; #define testo “LE STRINGHE SONO VETTORI” n=lunghezza_stringa(testo); printf(“\nLunghezza della stringa %d”, n); } int lunghezza_stringa(char s[]) {int i; i=0;while(s[i]!=‘\0’) i++; return i; } 8. Scrivere un prg per l’inversione di una stringa. R: main() {int i, n; char s1[50]; char s[] = “LE STRINGHE SONO VETTORI”; n=lunghezza_stringa(s); for(i=0; i #include struct persona{ Dichiarazione di una struttura char nome[30];2 char cognome[30]; char indirizzo[100]; char telefono[20]; }; struct persona amico; Amico è una struttura di tipo persona void printat(int x,int y, char s1[]); void schermo(); PROGRAMMA PRINCIPALE int main() { #define testo_titolo “ STRUTTURE (Struct) ” int i, n; char c, v[100]; schermo(); printat(5,3,”Inserisci i dati dell’utente”); i=0; printat(5,5,”Cognome: ”); while((c=getchar())!=‘\n’) Viene assegnato al membro cognome {amico.cognome[i]=c; i++;} della struct amico una stringa v[i]=‘\0’; L’ultimo carattere di una stringa deve essere \0 i=0;gotoxy(5,6); cprintf(“Nome: ”); while((c=getchar())!=‘\n’) Lettura di amico.nome {amico.nome[i]=c; i++;} v[i]=‘\0’; i=0;gotoxy(5,7); cprintf(“Indirizzo: ”); while((c=getchar())!=‘\n’) Lettura di amico.indirizzo {amico.indirizzo[i]=c; i++;} v[i]=‘\0’; i=0;gotoxy(5,8); cprintf(“Telefono: ”); while((c=getchar())!=‘\n’) Lettura di amico.telefono {amico.telefono[i]=c; i++;} v[i]=‘\0’; printat(45,3,”Scheda utente inserita”); Stampa della scheda inserita gotoxy(45,5); cprintf(“%s %s”, amico.nome, amico.cognome); gotoxy(45,6); cprintf(“%s”, amico.indirizzo); gotoxy(45,7); cprintf(“%s”, amico.telefono); Seconda parte del programma: qual’è il codice di F1, F2...? gotoxy(3,10); cprintf(“Scrivi cinque caratteri (a, Q...) o chiavi (F1, F2...)”); gotoxy(3,11); Viene usata la routine getkey per scrivere il codice ascii di 5 caratteri o chiavi (F1, F2...). i=1; while(i<=5) {cprintf(“asci_code %d ”, getkey());i++;} getch(); return 0; } FINE DEL PROGRAMMA PRINCIPALE void printat(int x,int y, char s1[]) Scrive un msg { gotoxy(x,y);cprintf(“%s”,s1);} int lunghezza_stringa(char s[]) Calcola la lunghezza di s {int i; i=0;while(s[i]!=‘\0’) i++; return i; } void screenview() Disegna il quadro { int j,x0,x1,y0,y1; x0=1;y0=1;x1=80;y1=24; printat(x0,y0,“Õ”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“¸”); for(j=1;j<=y1-4;j++){printat(x0,y0+j,“³”); gotoxy(x0+x1-1,y0+j);cprintf(“³”);} printat(x0,y0+y1-4,“Ô); for(j=1;j<=x1-2;j++) cprintf(“Ä”);cprintf(“´”); printat(x0,y0+y1-3,“³”);printat(x0+x1-1,y0+y1-3,“³”); printat(x0,y0+y1-2,“³”);printat(x0+x1-1,y0+y1-2,“³”); printat(x0,y0+y1-1,“À”); for(j=1;j<=x1-2;j++) cprintf(“Ä”);printat(x0+x1-1,y0+y1+1,“Ù”);} void schermo() Scrive l’intestazione {int n; n=lunghezza_stringa(testo_titolo); clrscr(); screenview(); gotoxy(40-n/2,1); cprintf(testo_titolo); } int getkey(void) Legge dal buffer di tastiera il tasto battuto {int key, lo, hi; key = bioskey(0); Istruzione che legge dal buffer lo = key & 0X00FF; hi = (key & 0XFF00) >> 8; return((lo == 0) ? hi + 256 : lo); } Aggiunge 256 alle chiavi 17. Nuovi elementi di programmazione. Acquisita una minima conoscenza del linguaggio C conviene ora dedicarsi alla costruzione di un primo programma che permetta di introdurre in modo naturale qualche altro elemento di programmazione fondamentale. Lo spunto ci è dato dalle strutture viste nella lezioni precedenti, mediante le quali è possibile realizzare un elementare programma di gestione di archivio. Il primo passo (quello che svilupperemo in questa lezione) potrebbe essere quello di realizzare delle funzioni capaci di gestire l’acquisizione dei dati mediante dei “campi protetti”, cioè dei riquadri dentro i quali scriveremo le stringhe di input. Un modo istruttivo per fare questo è quello di costruire un piccolo editor capace di trasferire i caratteri battuti sulla tastiera in una matrice e poi riversarli nella struttura. Dovremo essere capaci di riprodurre alcune carattersitiche degli editor, come il movimento a destra e a sinistra del cursore, la cancellazione di un carattere, etc. Cominciamo col progettare a grandi linee il software. Questo dovrà essere in grado di “catturare” il carattere da noi battuto sulla tastiera, scriverlo sullo schermo, memorizzarlo da qualche parte, spostarsi di un passo a destra. Inoltre dovrà essere capace almeno di interpretare le “freccette” destra e sinistra e spostare il cursore di conseguenza. (Esattamente come accade mentre scrivo questo testo). Quando avrò terminato di inserire i dati, dovrà essere possibile memorizzare il contenuto della memoria in vettori e poi nella struttura. Scendiamo ancora di più nel dettaglio. Avremo bisogno di un’area protetta dalla quale il cursore non possa uscire e questa la possiamo creare con l’istruzione window: window(x0, y0, x1, y1); che apre una finestra di vertici (x0,y0), (x1,y1). A questo punto posizioneremo il cursore nel vertice in alto a sinistra della finestra con: gotoxy(1,1); Dovremo poi stabilire se il tasto premuto è un carattere accettabile come dato (lettera, numero, etc), da ignorare (chiave come F1...), oppure si tratta di una istruzione di spostamento del cursore (freccette). Possiamo a questo scopo scrivere una funzione int che ci ritorna il valore -1 se il carattere è alfa-numerico, int alfa_numerico(int lt) {int n; n=0; if((lt>=91)&&(lt<=122)) n=-1; if((lt>=32)&&(lt<=63 )) n=-1; if((lt>=65)&&(lt<=90)) n=-1; if(n==0) return lt; else return n; } Ora il controllo del programma deve passare ad una struttura logica capace di gestire tutte le possibili opzioni: 1. pressione di un tasto generico per scrivere una lettera; 2. freccetta a destra; 3. freccetta a sinistra; 4. . . . una gestione del genere è detta di “scelta plurima” e l’operatore più indicato è lo switch: switch(cmd) { case 13 : . . . ;break; case 27 : . . . ;break; } che valuta l’espressione tra parentesi tonde ed esegue le istruzioni del “case” che ha valore uguale a quello della espressione. Quello che dobbiamo fare è dunque: catturare il carattere da tastiera con la getkey che già conosciamo ed aprire uno switch con tutte le possibili scelte che vogliamo prevedere: lt=getkey(); Cattura il carattere battuto cmd=alfa_numerico(lt); cmd=-1 se è un carattere switch(cmd) { Apre una scelta plurima case LEFT: gotoxy(wherex()-1,wherey());break; Passo a sinistra case RIGHT: gotoxy(wherex()+1,wherey());break; Passo a destra case UP: gotoxy(wherex(),wherey()-1);break; Passo in alto case DOWN: gotoxy(wherex(),wherey()+1);break; Passo in basso case -1: putchar(lt); Scrive il carattere a[wherex()][wherey()]=lt;break; e lo copia in a } } dove abbiamo usato le istruzioni: wherex(); Fornisce la posizione orizzontale del cursore wherey(); Fornisce la posizione verticale del cursore gotoxy(wherex()-1, wherey()); Sposta di un passo a sinistra (LEFT) gotoxy(wherex(), wherey()+1); Sposta di un passo in alto (UP) Il nostro software è ora definito. Per dare rilievo alla finestra utilzzeremo infine delle istruzioni che modificano il colore dello sfondo (background) e del testo: textbackground(); Definisce il colore dello sfondo textcolor(); Definisce il colore del testo Nel programma sono state definite per comodità alcune costanti con un nome che le ricorda. Ecco il programma relativo. #include #include Definizione di costanti di comodo #define UP 328 #define DOWN 336 #define LEFT 321 #define RIGHT 333 #define CR 13 #define ESC 27 #define DEL 339 #define TAB 9 #define BSP 8 struct persona{ Definizione di un tipo struttura char nome[30]; char cognome[30]; char indirizzo[100]; char telefono[20]; }; struct persona amico; amico è una variabile di tipo persona int getkey(void); void closewin(int x1,int y1,int x2,int y2); void inverte(); void printat(int x,int y, char s1[]); void schermo(); void cancella(int a[10][100]); void message(char st1[]); void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30]); PROGRAMMA PRINCIPALE int main() { #define testo_titolo “ CAMPI PROTETTI” int cmd, lt, i ,j; int a[10][100]; Matrice che contiene la scheda di input inverte(); Inverte i colori dello sfondo e del testo schermo(); cancella(a); Inizializza la matrice a message(“ Inserisci ”); Messaggio a fondo schermo campi_protetti(a,20,10,40,4, “Cognome”, “Nome”, “Indirizzo”, “Telefono”); Chiamata della void che crea la mascherina: a Matrice 20,10 Posizione sheda 40 Lunghezza max voci 4 Numeo voci “Cognome”, ”Nome”, ... Nomi campi Trasferimento della matrice a in amico for(i=0;i<=20;i++)amico.cognome[i]=a[1][i+1]; for(i=0;i<=20;i++)amico.nome[i]=a[2][i+1]; for(i=0;i<=40;i++)amico.indirizzo[i]=a[3][i+1]; for(i=0;i<=20;i++)amico.telefono[i]=a[4][i+1]; closewin(28,3,75,7); Preparazione finestra per stampa dati window(28,3,75,7); printat(1,1,“ Scheda utente inserita”); Stampa della scheda inserita gotoxy(1,2); cprintf(“%s”, amico.cognome); gotoxy(1,3); cprintf(“%s”, amico.nome); gotoxy(1,4); cprintf(“%s”, amico.indirizzo); gotoxy(1,5); cprintf(“%s”, amico.telefono); message(“ Esci ”); getch(); return 0; } FINE PROGRAMMA PRINCIPALE void printat(int x,int y, char s1[]) Scrive un msg { gotoxy(x,y);cprintf(“%s”,s1);} void message(char st1[]) Scrive un msg a fondo schermo {closewin(2,25,78,25);gotoxy(2,25);cprintf(st1);} int lunghezza_stringa(char s[]) Calcola la lunghezza di s {int i; i=0;while(s[i]!=‘\0’) i++; return i; } void inverte() Inverte lo sfondo e il testo { window(1,1,80,25);textbackground(WHITE);textcolor(BLACK); clrscr();} void closewin(int x1,int y1,int x2,int y2) Chiude una finestra {textbackground(WHITE); textcolor(BLACK);window(x1,y1,x2,y2);clrscr(); textbackground(WHITE);textcolor(BLACK);window(1,1,80,25);} void openwin(int x1,int y1,int x2,int y2) Apre una finestra {textbackground(BLACK);textcolor(GREEN); window(x1,y1,x2,y2);clrscr();} void screenview() Disegna il quadro { int j,x0,x1,y0,y1; x0=1;y0=1;x1=80;y1=24; printat(x0,y0,“Õ”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“¸”); for(j=1;j<=y1-4;j++){printat(x0,y0+j,“³”); gotoxy(x0+x1-1,y0+j);cprintf(“³”);} printat(x0,y0+y1-4,“Ô); for(j=1;j<=x1-2;j++) cprintf(“Ä”);cprintf(“´”); printat(x0,y0+y1-3,“³”);printat(x0+x1-1,y0+y1-3,“³”); printat(x0,y0+y1-2,“³”);printat(x0+x1-1,y0+y1-2,“³”); printat(x0,y0+y1-1,“À”); for(j=1;j<=x1-2;j++) cprintf(“Ä”);printat(x0+x1-1,y0+y1+1,“Ù”);} void schermo() Scrive l’intestazione {int n; n=lunghezza_stringa(testo_titolo); clrscr(); screenview(); gotoxy(40-n/2,1); cprintf(testo_titolo); } int getkey(void) Legge dal buffer di tastiera il tasto battuto {int key, lo, hi; key = bioskey(0); lo = key & 0X00FF; hi = (key & 0XFF00) >> 8; return((lo == 0) ? hi + 256 : lo); } void cancella(int a[10][100]) Cancella la matrice {int i,j; for(i=0;i<10;i++){for(j=0;j<100;j++)a[i][j]= ‘ ’;}} int alfa_numerico(int lt) Il carattere è alfanumerico ? {int n; n=0; if((lt>=91)&&(lt<=122)) n=-1; if((lt>=32)&&(lt<=63 )) n=-1; if((lt>=65)&&(lt<=90)) n=-1; if(n==0) return lt;else return n; } void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30]) {int lt,cmd; window(3,y0,23,y0+4); gotoxy(1,1); cprintf(“%s”,voce_1); gotoxy(1,2); cprintf(“%s”,voce_2); gotoxy(1,3); cprintf(“%s”,voce_3); gotoxy(1,4); cprintf(“%s”,voce_4); window(1,1,80,25); openwin(x0,y0,x0+max_len,y0+max_voci-1); gotoxy(1,1); while(1==1) { lt=getkey(); cmd=alfa_numerico(lt); switch(cmd) { case ESC: window(1,1,80,25);return; case CR: gotoxy(1,wherey()+1);break; case TAB: gotoxy(1,wherey()+1);break; case LEFT: gotoxy(wherex()-1,wherey());break; case RIGHT: gotoxy(wherex()+1,wherey());break; case UP: gotoxy(wherex(),wherey()-1);break; case DOWN: gotoxy(wherex(),wherey()+1);break; case BSP: gotoxy(wherex()-1,wherey());putchar(‘ ’); a[wherey()][wherex()]=‘ ’; gotoxy(wherex()-1,wherey());break; case -1: if(wherex()<=max_len){ putchar(lt); a[wherey()][wherex()]=lt;}break; } } } 18. I File. Per aprire un file si può utilizzare la funzione di libreria fopen che restituisce un puntatore al nome del file. Basterà dunque dichiarare un puntatore di tipo FILE nel seguente modo: FILE *fp; e poi aprire il file: fp=fopen(pippo,“a”); la prima voce “pippo” è il nome del file e la seconda è il modo nel quale viene aperto il file. Se si vuole creare un file si usa “w” (Write), se si vuole aggiungere qualcosa ad un file già esistente si usa “a” (Append) se si vuole solo leggere si usa “r” (Read). Per chiudere il collegamento col file si scrive: fclose(fp); Per scrivere sul file ci sono molte possibilità: fprintf(fp, “ ”, . . .); che è l’analoga della printf, oppure la fputs(line, fp); dove “line” è una variabile char, che permette di scrivere una stringa di caratteri. Per la lettura c’è l’inversa della fputs : fgets(line, nmax, fp); nella quale bisogna dichiarare il numero massimo di caratteri da leggere, nmax, oppure l’analoga della scanf, fscanf(fp, “ ”, . . .); Un’ultima considerazione riguarda le strutture. Abbiamo visto come si possa individuare una voce della struct attraverso il punto (.): amico.nome è la voce nome nella struttura amico. Se si usano i puntatori e la struct è stata denominata persona, si può dichiarare un puntatore di tipo persona: struct persona *pp; e allora la scrittura: pp = &amico; copia tutto il contenuto della variabile amico in pp. Se ci si vuole riferire alla voce nome si dovrà scrivere: (*pp.)nome Con le parentesi perché altrimenti il punto viene eseguito prima della * Dato l’uso comune di queste dichiarazioni è stata introdotta nel C la scrittura equivalente alla precedente: pp->nome che è più semplice. Per fare un esempio se volessimo far scrivere la voce pp.nome: printf(“%s”, pp->nome); Il programma che presentiamo amplia quello della lezione precedente prevedendo anche la scrittura della scheda inserita in un apposito file. Viene dato anche un file libreria necessario alla compilazione del programma. PROGRAMMA DI ARCHIVIO. È necessario il file cutil.h #include #include #include “cutil.h” Inclusione del file personale di libreria struct persona{ char nome[30]; char cognome[30]; char indirizzo[100]; char telefono[20]; }; struct persona amico; struct persona *pp; Puntatore di tipo struct persona void trasf(int a[10][100], struct persona*); PROGRAMMA PRINCIPALE int main() { #define testo_titolo “ ARCHIVIO” #define name “agenda.dat” Nome file FILE *fp; Puntatore al file int a[10][100]; inverte(); schermo(testo_titolo); fp=fopen(name,“a”); Apertura file di archivio cancella(a); message(“ Inserisci ”); campi_protetti(a,20,10,40,4, “Cognome”, “Nome”, “Indirizzo”, “Telefono”); trasf(a,&amico); Trasferimento matrice a in amico fprintf(fp,“%s%s%s%s\n”,amico.cognome,amico.nome,amico.indirizzo, amico.telefono); Disegna il quadro fclose(fp); Chiusura file message(“ Esci ”); getch(); return 0; } FINE DEL PROGRAMMA PRINCIPALE void trasf(int a[10][100], struct persona *pp) Trasferisce a in pp {int i; for(i=0;i<=20;i++) pp->cognome[i]=a[1][i+1]; for(i=0;i<=20;i++) pp->nome[i]=a[2][i+1]; for(i=0;i<=40;i++) pp->indirizzo[i]=a[3][i+1]; for(i=0;i<=20;i++) pp->telefono[i]=a[4][i+1]; } Inizio file da separare dal resto Il nome di questo file dovrà essere : CUTIL.H Libreria personale CUTIL.H #include #include Definizione di costanti di comodo #define UP 328 #define DOWN 336 #define LEFT 321 #define RIGHT 333 #define CR 13 #define ESC 27 #define DEL 339 #define TAB 9 #define BSP 8 int getkey(void); void closewin(int x1,int y1,int x2,int y2); void inverte(); void printat(int x,int y, char s1[]); void schermo(char testo_titolo[]); void message(char st1[]); void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30]); void printat(int x,int y, char s1[]) Scrive un msg { gotoxy(x,y);cprintf(“%s”,s1);} void message(char st1[]) Scrive un msg a fondo schermo {closewin(2,25,78,25);gotoxy(2,25);cprintf(st1);} int lunghezza_stringa(char s[]) Calcola la lunghezza di s {int i; i=0;while(s[i]!=‘\0’) i++; return i; } void inverte() Inverte lo sfondo e il testo { window(1,1,80,25);textbackground(WHITE);textcolor(BLACK); clrscr();} void closewin(int x1,int y1,int x2,int y2) Chiude una finestra {textbackground(WHITE); textcolor(BLACK);window(x1,y1,x2,y2);clrscr(); textbackground(WHITE);textcolor(BLACK);window(1,1,80,25);} void openwin(int x1,int y1,int x2,int y2) Apre una finestra {textbackground(BLACK);textcolor(GREEN); window(x1,y1,x2,y2);clrscr();} void screenview() Disegna il quadro { int j,x0,x1,y0,y1; x0=1;y0=1;x1=80;y1=24; printat(x0,y0,“Õ”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“¸”); for(j=1;j<=y1-4;j++){printat(x0,y0+j,“³”); gotoxy(x0+x1-1,y0+j);cprintf(“³”);} printat(x0,y0+y1-4,“Ô); for(j=1;j<=x1-2;j++) cprintf(“Ä”);cprintf(“³”); printat(x0,y0+y1-3,“³”);printat(x0+x1-1,y0+y1-3,“³”); printat(x0,y0+y1-2,“³”);printat(x0+x1-1,y0+y1-2,“³”); printat(x0,y0+y1-1,“À”); for(j=1;j<=x1-2;j++) cprintf(“Ä”);printat(x0+x1-1,y0+y1+1,“Ù”);} void schermo(char testo_titolo[]) Scrive l’intestazione {int n; n=lunghezza_stringa(testo_titolo); clrscr(); screenview(); gotoxy(40-n/2,1); cprintf(testo_titolo); } int getkey(void) Legge dal buffer di tastiera il tasto battuto {int key, lo, hi; key = bioskey(0); lo = key & 0X00FF; hi = (key & 0XFF00) >> 8; return((lo == 0) ? hi + 256 : lo); } void cancella(int a[10][100]) Cancella la matrice {int i,j; for(i=0;i<10;i++){for(j=0;j<100;j++)a[i][j]=‘ ‘;}} int alfa_numerico(int lt) Il carattere è alfanumerico ? {int cmd; cmd=0; if((lt>=91)&&(lt<=122)) cmd=-1; if((lt>=32)&&(lt<=63 )) cmd=-1; if((lt>=65)&&(lt<=90)) cmd=-1; return cmd; } void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30]) {int lt,cmd; cancella(a); window(3,y0,23,y0+4); gotoxy(1,1); cprintf(“%s”,voce_1); gotoxy(1,2); cprintf(“%s”,voce_2); gotoxy(1,3); cprintf(“%s”,voce_3); gotoxy(1,4); cprintf(“%s”,voce_4); window(1,1,80,25); openwin(x0,y0,x0+max_len,y0+max_voci-1); gotoxy(1,1); while(1==1) { lt=getkey(); if((cmd=alfa_numerico(lt))==0) cmd=lt; switch(cmd) { case ESC: window(1,1,80,25);return; case CR: gotoxy(1,wherey()+1);break; case TAB: gotoxy(1,wherey()+1);break; case LEFT: gotoxy(wherex()-1,wherey());break; case RIGHT: gotoxy(wherex()+1,wherey());break; case UP: gotoxy(wherex(),wherey()-1);break; case DOWN: gotoxy(wherex(),wherey()+1);break; case BSP: gotoxy(wherex()-1,wherey());putchar(‘ ’); a[wherey()][wherex()]=‘ ’; gotoxy(wherex()-1,wherey());break; case -1: if(wherex()<=max_len){ putchar(lt); a[wherey()][wherex()]=lt;}break; } } } Fine file cutil.h 19. Programmazione orientata agli oggetti (OOP). Il procedimento che abbiamo utilizzato nel progettare il programma di acquisizione dati (Cap. 7) è consistito nello schematizzare a grandi linee il problema (scrivere un mini editor) per poi scendere sempre più nel dettaglio, fino a scrivere il codice (programma) vero e proprio. Si tratta di una strategia classica adottata dai programmatori e indicata spesso col nome di TOP-DOWN. Il limite naturale di tale approccio è che spesso il codice realizzato risulta pesantemente legato al problema per il quale è stato scritto e non si presta ad una generalizzazione che ne permetta l’applicazione a classi di problemi più ampie. In altre parole il software nasce e muore col problema per il quale è stato creato. Il procedimento appena descritto si basa in gran parte sull’analisi del problema dal punto di vista delle procedure che dovremo adottare per la sua risoluzione: cioè lo sviluppo delle funzioni o subroutine che costuiranno in certo senso i mattoni del nostro programma. Un secondo aspetto, che abbiamo cominciato a introdurre con le strutture, è quello della natura stessa dei dati. I dati sono gli “oggetti” sui quali il programma lavora e, a seconda del problema analizzato, cambia la loro natura e forma (si pensi ai dati di un archivio, a quelli di un prg di grafica etc.) Una strategia alternativa, nello sviluppo di software, potrebbe quindi essere quella di preoccuparsi in particolar modo degli oggetti sui quali il programma dovrà lavorare piuttosto che dei dettagli del codice da scrivere. Un simile approccio è detto “programmazione orientata agli oggetti” (OOP). L’ambizione di questa diversa filosofia di programmazione è quella di realizzare del software di grande generalità, capace di trattare problemi di natura anche molto diversa fra loro, senza che sia necessario riscrivere ogni volta tutto il programma. Ovviamente le struct viste in precedenza (Cap. 7), non sono ancora adatte a questo scopo: occorre una struttura ancora più articolata capace di descrivere non soltanto la struttura dei dati (oggetti) ma anche le funzioni atte a manipolare i dati stessi. Una organizzazione simile dei dati è realizzata nelle cosiddette “classi”. Una CLASSE è di fatto un tipo di dato definito dal programmatore secondo le sue esigenze che è identificata da alcuni attributi: · il suo nome, che la identifica come tipo definito dal programmatore; · un certo numero di dati o oggetti che configurano la struttura dati tipica del problema analizzato; · alcune funzioni che permettono la manipolazione dei dati e che costituiscono l’interfaccia della classe con l’esterno; · i livelli di accesso alla classe (private, public, protected) che definiscono le modalità di accesso da parte del programma agli elementi di quella classe. Come si vede si tratta di una organizzazione articolata che definisce in modo consistente una specie di piccolo mondo col quale si può colloquiare, ma solo secondo certe regole stabilite dal programmatore nella progettazione della classe stessa. Anche se può sembrare complicato tutto quanto detto, una volta che si è adottato questo nuovo approccio (sistemico) alla risoluzione dei problemi, si apprezzerà subito il vantaggio di potersi dedicare alla costruzione del programma al livello più alto, senza doversi preoccupare di come poi saranno realizzate le singole funzioni. Cerchiamo ora di applicare quanto detto estendendo le potenzialità del nostro programma di archivio. Avremo bisogno di una classe per la gestione dell’archivio che potrà all’inzio comprendere solo due funzioni: una di aggiornamento e una di ricerca : class archivio_persone Dichiarazione di una classe { public: archivio_persone(); Costruttore della classe ~archivio_persone(); Distruttore della classe void aggiungi(); Funzione membro per aggiornamento void trova(); Funzione membro di ricerca }; Essa comprende due funzioni membro: void aggiungi(); void trova(); i cui codici dovranno essere scritti e che svolgeranno le funzioni di aggiornamento e di ricerca. In più si possono definire due funzioni con lo stesso nome della classe, nel nostro caso esse sono: archivio_persone(); (costruttore) ~archivio_persone(); (distruttore) che vengono chiamate rispettivamente: costruttore e distruttore della classe (il distruttore ha una tilde ~ prima del nome). Queste servono a creare e inizializzare una “variabile” di tipo classe e a cancellarla (non sono obbligatorie). Il tipo o modalità di accesso alle funzioni membro può essere dichiarato nella definizione della funzione all’interno della classe e può essere: public private Ad un elemento public si potrà accedere da qualunque punto del prg mentre ad un elemento private si potrà accedere solo attraverso le funzioni della classe. Naturalmente tutte le funzioni dichiarate nella definizione della classe, compresi il costruttore ed il distruttore, dovranno poi essere materialmente scritte come delle normali routine. I loro nomi saranno composti dal nome della classe e da quello della funzione separati da due coppie di due punti. Per esempio nel nostro caso: archivio_persone::aggiungi() { } archivio_persone::trova() { } dove tre le parentesi graffe va scritto il codice. Il vantaggio è nel fatto che tutte le routine relative a quella classe sono comprese nella classe stessa in una maniera ben coordinata e leggibile. Per definire una variabile di tipo archivio_persone basterà ora scrivere : archivio_persone pippo; Questa istruzione, attraverso il costruttore, inizializza la variabile pippo oltre a definirla (per esempio azzera un vettore). A questo punto possiamo chiamare le singole funzioni come delle normali routine: pippo.aggiungi(); pippo.trova(); Possiamo poi definire una seconda classe utile per il programma di archivio che sostituirà la struct persona: class persona{ public: persona(); ~persona(); char nome[40]; char cognome[40]; char indirizzo[40]; char telefono[40]; }; Anche la libreria per la gestione dello schermo potrà essere riscritta usando una unica classe Screen: class Screen { public: Screen(); void Setcursor(unsigned int shape); void inverte(); void msg_titolo(char *msg_header); void screenview(); void doppio_screen(); void scrivi_messaggio(char *messaggio); void openwin(int x1,int y1,int x2,int y2); void closewin(int x1,int y1,int x2,int y2); void oldwin(); private: int x0,y0; int x1,y1; }; che definisce come private le coordinate del rettangolo e contiene tutte le funzioni utili per le gestire le finestre ed il cursore. Per finire introduciamo due nuove funzioni standard di libreria, fread ( st , l , n , fp ) ; che legge un numero n stabilito di voci, di lunghezza l, dal file fp e, strcmp(s1 , s2 ); che confronta le due stringhe s1 e s2. Ritorna 0 se sono uguali, un valore negativo se s1 è minore di s2 e positivo nel caso opposto. P R O G R A M M A P E R A G E N D A Questo programma permette di archiviare in un file le schede personali che possono essere richiamate usando come chiave il cognome della persona. Il programma utilizza due file-header per la gestione dello schermo e dell’input delle schede: schermo.h e campi.h. Qui inizia il file del programma il cui nome è : AGENDA.CPP #include #include #include #include “schermo.h” Inclusione della libreria “schermo” #include “campi.h” Inclusione della libreria “campi” #define titolo “OOP - Programmazione Orientata agli Oggetti” #define name “amici.dat” FILE *fp; int a[10][100]; class persona{ Definizione di una classe public: persona(); ~persona(); char nome[40]; char cognome[40]; char indirizzo[40]; char telefono[40]; }; class archivio_persone{ public: archivio_persone(); ~archivio_persone(); void aggiungi(); void trova(); }; void trasf(int a[10][100], persona*); persona amico; persona *pp; INIZIO DEL PROGRAMMA PRINCIPALE int main() {int cd; clrscr(); Screen myscreen; Inizializza una classe myscreen myscreen.inverte(); myscreen.screenview(); myscreen.msg_titolo(titolo); myscreen.Setcursor(nocursor); archivio_persone file; Inizializza una classe archivio_persone while(1==1) { myscreen.scrivi_messaggio(“ Esci <+> Aggiungi <*> Trova ”); cd=toupper(getch()); switch(cd) { case ‘+’ : file.aggiungi(); break; Oggetto della classe file case ‘*’ : file.trova();break; Oggetto della classe file case 27 : return 0; } } } archivio_persone::archivio_persone() Costruttore {} archivio_persone::~archivio_persone() Distruttore {} void archivio_persone::aggiungi() Aggiunge un nuova voce all’archivio {Screen myscreen; myscreen.Setcursor(oldcursor); persona amico; fp=fopen(name,“a”); cancella(a); campi_protetti(a,20,10,40,4,“Cognome”,“Nome”,“Indirizzo”, “Telefono”); trasf(a,&amico); fprintf(fp,“%s%s%s%s\n”,amico.cognome,amico.nome,amico.indirizzo ,amico.telefono); fclose(fp); myscreen.Setcursor(nocursor);} void archivio_persone::trova() Ricerca una voce nell’archivio {int x0,y0,max_len,max_voci; Screen myscreen; myscreen.Setcursor(oldcursor); persona chiave;persona amico; fp=fopen(name,“r”); cancella(a); campi_protetti(a,20,10,40,4,“Cognome”,“Nome”,“Indirizzo”, “Telefono”); trasf(a,&chiave); do{ fread(amico.cognome,31,1,fp); fread(amico.nome,31,1,fp); fread(amico.indirizzo,31,1,fp); fread(amico.telefono,32,1,fp); if(strcmp(amico.cognome,chiave.cognome)==0){ myscreen.Setcursor(nocursor); gotoxy(1,1); cprintf(amico.cognome); gotoxy(1,2); cprintf(amico.nome); gotoxy(1,3); cprintf(amico.indirizzo); gotoxy(1,4); cprintf(amico.telefono); getch(); myscreen.Setcursor(oldcursor); } }while(!feof(fp)); fclose(fp); myscreen.Setcursor(nocursor);} persona::~persona(){} Distruttore persona::persona(){ Costruttore for(int i=0;i<40;i++) cognome[i]=‘\0’; for(i=0;i<40;i++) nome[i]=‘\0’; for(i=0;i<40;i++) indirizzo[i]=‘\0’; for(i=0;i<40;i++) telefono[i]=‘\0’;} void trasf(int a[10][100], struct persona *pp) Trasferisce a in pp {int i; for(i=0;i<=30;i++) pp->cognome[i]=a[1][i+1]; for(i=0;i<=30;i++) pp->nome[i]=a[2][i+1]; for(i=0;i<=30;i++) pp->indirizzo[i]=a[3][i+1]; for(i=0;i<=30;i++) pp->telefono[i]=a[4][i+1]; } Fine programma : AGENDA.CPP Inizio file header : SCHERMO.H Libreria personale - SCHERMO.H Libreria per funzioni di gestione dello schermo #include #include #include #include unsigned int oldcursor,shortcursor,tallcursor,nocursor=0x2000; class Screen { public: Screen(); void Setcursor(unsigned int shape); void inverte(); void msg_titolo(char *msg_header); void screenview(); void doppio_screen(); void scrivi_messaggio(char *messaggio); void openwin(int x1,int y1,int x2,int y2); void closewin(int x1,int y1,int x2,int y2); void oldwin(); private: int x0,y0; int x1,y1; }; unsigned int getcursor(void) Returns the shape of the current cursor { union REGS reg; reg.h.ah = 3; reg.h.bh = 0; int86(0X10, ®, ®); return(reg.x.cx);} void initcursor(void) Inizializza i tipi di cursore {struct text_info ti; gettextinfo(&ti); oldcursor = getcursor(); if (ti.currmode == MONO) {shortcursor = 0x0A0C; tallcursor = 0x090C;} else{shortcursor = 0x0607; tallcursor = 0x0507;} } void Screen::Setcursor(unsigned int shape) Setta il prompt {initcursor(); union REGS reg;reg.h.ah = 1;reg.x.cx = shape; int86(0X10, ®, ®);} void Screen::inverte() Inverte lo sfondo e il testo { window(1,1,80,25);textbackground(WHITE); textcolor(BLACK);clrscr();} void Screen::msg_titolo(char msg_header[]) Scrive i msg iniziali {int n; n=strlen(msg_header); gotoxy(40-n/2,2); cprintf(msg_header);} void Screen::scrivi_messaggio(char st1[]) Scrive un msg {Screen win; win.closewin(2,25,78,25); gotoxy(2,25);cprintf(st1);} void Screen::closewin(int x1,int y1,int x2,int y2) Chiude una finestra {textbackground(WHITE); textcolor(BLACK);window(x1,y1,x2,y2);clrscr(); textbackground(WHITE);textcolor(BLACK);window(1,1,80,25);} void Screen::openwin(int x1,int y1,int x2,int y2) Apre una finestra {textbackground(BLACK);textcolor(GREEN); window(x1,y1,x2,y2);clrscr();} void Screen::oldwin() Ripristina la window {textbackground(WHITE);textcolor(BLACK);window(1,1,80,25);} void printat(int x,int y, char s1[]) Scrive un msg { gotoxy(x,y);cprintf(“%s”,s1);} Screen::Screen() {x0=1;y0=1;x1=80;y1=24;} void Screen::screenview() Disegna il quadro { int j; printat(x0,y0,“Õ”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“¸”); for(j=1;j<=y1-4;j++){printat(x0,y0+j,“³”); gotoxy(x0+x1-1,y0+j);cprintf(“³”);} printat(x0,y0+y1-4,“Ô); for(j=1;j<=x1-2;j++) cprintf(“Ä”);cprintf(“´”); printat(x0,y0+y1-3,“³”);printat(x0+x1-1,y0+y1-3,“³”); printat(x0,y0+y1-2,“³”);printat(x0+x1-1,y0+y1-2,“³”); printat(x0,y0+y1-1,“À”); for(j=1;j<=x1-2;j++) cprintf(“Ä”);printat(x0+x1-1,y0+y1+1,“Ù”);} void Screen::doppio_screen() Quadro per help { int j; printat(x0,y0,“É”); for(j=1;j<=x1-2;j++) cprintf(“Í”);cprintf(“»”); for(j=1;j<=y1-2;j++){printat(x0,y0+j,“º”); gotoxy(x0+x1-1,y0+j);cprintf(“º”);} printat(x0,y0+y1-1,“È”);for(j=1;j<=x1-2;j++) cprintf(“Í”); cprintf(“¼”);printat(x0,y0+y1-1,“È”); for(j=1;j<=x1-2;j++) cprintf(“Í”);} Fine file SCHERMO.H Inizio file CAMPI.H Libreria di funzioni per la gestione degli input con campi protetti #include #include #include #include Definizione di costanti di comodo #define UP 328 #define DOWN 336 #define LEFT 331 #define RIGHT 333 #define CR 13 #define ESC 27 #define DEL 339 #define TAB 9 #define BSP 8 #define HOME 327 #define END 335 #define PGUP 329 #define PGDN 337 #define INS 338 #define BSP 8 #define F1 315 #define F2 316 #define F3 317 #define F4 318 #define F5 319 #define F6 320 #define F7 321 #define F8 322 #define F9 323 #define F10 324 void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30],...); int getkey(void) Legge dal buffer di tastiera il tasto battuto {int key, lo, hi; key = bioskey(0); lo = key & 0X00FF; hi = (key & 0XFF00) >> 8; return((lo == 0) ? hi + 256 : lo); } void cancella(int a[10][100]) Cancella la matrice {int i,j; for(i=0;i<10;i++){for(j=0;j<100;j++)a[i][j]=‘ ’;}} int alfa_numerico(int lt) Il carattere è alfanumerico ? {int cmd; cmd=0; if((lt>=91)&&(lt<=122)) cmd=-1; if((lt>=32)&&(lt<=63 )) cmd=-1; if((lt>=65)&&(lt<=90)) cmd=-1; return cmd; } void campi_protetti(int a[10][100],int x0, int y0, int max_len, int max_voci, char voce_1[30], char voce_2[30], char voce_3[30], char voce_4[30],...) {int lt,cmd; cancella(a); window(3,y0,23,y0+4); gotoxy(1,1); cprintf(“%s”,voce_1); gotoxy(1,2); cprintf(“%s”,voce_2); gotoxy(1,3); cprintf(“%s”,voce_3); gotoxy(1,4); cprintf(“%s”,voce_4); window(1,1,80,25); Screen win; win.scrivi_messaggio(“ Inserisci”); win.openwin(x0,y0,x0+max_len,y0+max_voci); gotoxy(1,1); while(1==1) { lt=getkey(); if((cmd=alfa_numerico(lt))==0) cmd=lt; switch(cmd) { case F10: return; case CR: gotoxy(1,wherey()+1);break; case TAB: gotoxy(1,wherey()+1);break; case LEFT: gotoxy(wherex()-1,wherey());break; case RIGHT: gotoxy(wherex()+1,wherey());break; case UP: gotoxy(wherex(),wherey()-1);break; case DOWN: gotoxy(wherex(),wherey()+1);break; case BSP: gotoxy(wherex()-1,wherey());putchar(‘ ’); a[wherey()][wherex()-1]=‘ ’; gotoxy(wherex()-1,wherey());break; case -1: if(wherex()<=max_len){ putchar(toupper(lt)); a[wherey()][wherex()-1]=toupper(lt);}break;} } } Fine file CAMPI.H 20. La libreria “iostream”. Il File-header della libreria iostream è la iostream.h Per stream si intende ogni flusso di informazioni di qualunque provenienza o destinazione: schermo, tastiera, file definiti dall’utente etc. L’uso delle funzioni e degli operatori di questa classe facilita le operazioni di input\output. Per esempio per scrivere e leggere da schermo potremo usare i comodi operatori << e >>. Grazie alla “sovrapponibilità” di questi operatori si potrà scrivere qualunque variabile o stringa, cout<<“Che tempo fa ?”; cout<>stringa; cin>>intero; Un’altra classe di stream della quale faremo uso è la che gestisce gli input\output per i file definiti dall’utente. Per usarla basterà definire una variabile fstream al solito modo: fstream pippo; e da quel momento pippo è inizializzata come appartenente alla classe fstream. Vediamo alcune funzioni membro appartenenti a questa classe. La open permette di aprire un file: pippo.open(name,ios::in); nella prima voce tra parentesi si scrive il nome del file, e con la funzione ios si definisce la modalità di accesso: ios::out per scrivere ios::in per leggere ios::app per appendere Per chiudere la connessione col file: pippo.close(); Si può scrivere e leggere da file con gli operatori << e >>: pippo<<“ ”; pippo>>stringa; oppure con apposite funzioni: file.write(variabile,10); file.read(variabile,10); dove la prima voce è il nome della variabile da leggere e la seconda il campo in caratteri da essa occupato. Per individuare la fine del file c’è un’apposita funzione eof : pippo.eof(); Introduciamo, per finire, la modalità di accesso alle classi: friend, che si va ad aggiungere alla public ed alla private. Le funzioni definite friend hanno accesso a tutti i campi, compresi quelli privati! In realtà le funzioni friend sono funzioni “esterne” che hanno accesso privilegiato a tutti i campi privati della classe nella quale sono definite, ma NON sono membri di quella classe. Per esempio, nel nostro programma di archivio, potremo ampliare la classe persona, per includervi le routine necessarie alla gestione degli input\output sia da schermo che da file: class persona{ friend void fin(class persona & b); Legge da file friend void fout(class persona & b); Scrive su file friend void in(class persona & b); Legge da tastiera friend void out(class persona & b); Scrive su video public: persona(); ~persona(); char nome[40]; char cognome[40]; char indirizzo[40]; char telefono[40]; }; Riscriviamo dunque il main del nostro programma, lasciando invariate i due file header. P R O G R A M M A D I A R C H I V I O #include #include #include #include #include #include #include “schermo.h” #include “campi.h” #define titolo “IO - Classi di stream per input-output” #define name “elenco.dat” static int a[10][100]; class archivio_persone{ public: archivio_persone(); ~archivio_persone(); void aggiungi(); void trova(); }; class persona{ friend void fin(class persona & b); friend void fout(class persona & b); friend void in(class persona & b); friend void out(class persona & b); public: persona(); ~persona(); char nome[40]; char cognome[40]; char indirizzo[40]; char telefono[40]; }; persona amico; persona *pp; archivio_persone gestione; fstream file; INIZIO PROGRAMMA PRINCIPALE int main() {int cd; clrscr(); Screen myscreen; myscreen.inverte(); myscreen.screenview(); myscreen.msg_titolo(titolo); myscreen.Setcursor(nocursor); while(1==1) { myscreen.scrivi_messaggio(“ Esci <+> Aggiungi <*> Trova ”); cd=toupper(getch()); switch(cd) { case ‘+’ : gestione.aggiungi(); break; case ‘*’ : gestione.trova(); break; case 27 : return 0; } } } archivio_persone::archivio_persone() {} archivio_persone::~archivio_persone() {} void archivio_persone::aggiungi() Aggiunge una nuova voce all’archivio {Screen myscreen; myscreen.Setcursor(oldcursor); persona amico; file.open(name,ios::app); File aperto per append in(amico); Lettura da schermo file.seekg(0,ios::end); Fine file fout(amico); Scrittura su file file.close(); myscreen.Setcursor(nocursor);} void archivio_persone::trova() Ricerca una voce nell’archivio {int x0,y0,max_len,max_voci; Screen myscreen; myscreen.Setcursor(oldcursor); persona chiave;persona amico; file.open(name,ios::in); in(chiave); Lettura da schermo do{ fin(amico); Lettura da file if(strcmp(amico.cognome,chiave.cognome)==0){ myscreen.Setcursor(nocursor); out(amico); Scrittura su video getch(); myscreen.Setcursor(oldcursor); } }while(!file.eof()); file.close(); myscreen.Setcursor(nocursor);} persona::~persona(){} Distruttore persona::persona(){ Costruttore for(int i=0;i<40;i++) cognome[i]=‘\0’; for(i=0;i<40;i++) nome[i]=‘\0’; for(i=0;i<40;i++) indirizzo[i]=‘\0’; for(i=0;i<40;i++) telefono[i]=‘\0’;} void fout(class persona &b) Scrive sul file { file.write(b.cognome,31); file.write(b.nome,31); file.write(b.indirizzo,31); file.write(b.telefono,31); file<<‘\n’;} void fin(class persona &b) Legge dal file {file.read(b.cognome,31); file.read(b.nome,31); file.read(b.indirizzo,31); file.read(b.telefono,32);} void out(class persona &b) Scrive sullo schermo {gotoxy(1,1);cout<