le basi della programmazione
Transcript
le basi della programmazione
ISTITUTO TECNICO E LICEO SCIENTIFICO TECNOLOGICO “ANGIOY” LE BASI DELLA PROGRAMMAZIONE in linguaggio C Prof. G. Ciaschetti 1) Problemi, algoritmi e programmi Programmare un computer, cioè scrivere programmi, significa in buona sostanza istruire il computer su alcuni compiti da eseguire. Ma perché scriviamo programmi? Quali compiti vogliamo che esso svolga? Sebbene al giorno d’oggi i programmi permettono di comunicare, ascoltare musica, vedere film, ecc., essi sono stati inizialmente concepiti per sollevare l’uomo dal compito ingrato di fare calcoli, essendo spesso noiosi, e dunque poco stimolanti per l’intelletto umano, oppure eccessivamente complicati. Si prenda ad esempio il Colossus, nato durante la II guerra mondiale, con cui Alan Turing conduceva le sue ricerche nella decifrazione dei codici nei messaggi dei tedeschi, e che doveva decifrarne circa 2000 al giorno. Con l’aiuto di questo robot calcolatore, l’uomo poté allora concentrarsi sugli aspetti più teorici della soluzione di un problema, delegando al computer la parte più pratica, quella di eseguire i calcoli. E’ esattamente quanto avviene anche oggi: l’uomo è la mente, e il computer è il braccio che esegue ciò che l’uomo gli dice di fare, per mezzo dei programmi. Quali problemi possiamo affrontare con l’aiuto del computer? Iniziamo col dire che preparare un tiramisù, calcolare l’area di un rettangolo, piantare fiori nel giardino, contare il numero delle stelle nel cielo, trovare i primi 100 numeri primi, sconfiggere la fame nel mondo sono tutti problemi, in cui partendo da una situazione iniziale si deve raggiungere una situazione finale desiderata. Alcuni sono più semplici, altri più complessi, altri ancora assurdi o non correttamente formulati. Per capire di cosa stiamo parlando, con precisione, proviamo a dare alcune definizioni: Un problema è un quesito che attende una risposta, detta soluzione. Dagli esempi fatti precedentemente, appare evidente che non a tutti i problemi è possibile dare una soluzione (ad esempio, contare il numero di stelle), e tra tutti quelli a cui è possibile dare una soluzione, non è detto che tale soluzione possa essere trovata tramite un computer (ad esempio, preparare il tiramisù). In particolare, possiamo dire che Un problema è risolvibile se ammette almeno una soluzione. Il fatto che una soluzione esista, non ci garantisce che sia facile trovarla. Intanto, occorre che il problema sia correttamente formulato: ad esempio, se qualcuno ci pone il problema di “trovare il minimo comune multiplo”, probabilmente noi risponderemmo “tra chi?”. Chi ci ha posto il quesito non l’ha posto in modo appropriato, in quanto mancano i dati del problema necessari per trovare la soluzione. Allo stesso modo, se ci si chiede di “trovare il minimo comune tra 4 e 5”, la nostra risposta sarà, con tutta probabilità, “multiplo?”. Di nuovo, il quesito non è stato posto correttamente, in quanto non è stato specificato con esattezza cosa ottenere. Un problema è ben posto o correttamente formulato se sono ben specificati i dati iniziali del problema e i risultati da ottenere, ed esiste un modo per ottenere la soluzione a partire dai dati iniziali. Abbiamo fatto, a questo punto, una prima cernita tra tutti i possibili problemi: siamo interessati a problemi risolvibili e ben posti, visto che agli altri non sappiamo dare soluzione. Tra questi, se ad alcuni riusciamo a dare una soluzione, è perché abbiamo individuato una sequenza di passi che ci porta dalla situazione iniziale (problema non risolto) alla situazione finale (problema risolto), dopo aver fatto alcune operazioni. Un processo risolutivo è una sequenza di passi da compiere per arrivare alla soluzione di un problema, partendo dai dati iniziali. Ad esempio, per la preparazione del tiramisù il processo risolutivo (detto ricetta) è quello che ci permette, passo dopo passo, di ottenere il tanto amato dolce (soluzione) a partire dagli ingredienti (dati iniziali). E’ ovvio che nessuno si aspetta di preparare un tiramisù con un computer: vedremo infatti che solo quei problemi che richiedono operazioni che il computer sa fare possono essere risolti con esso. Tuttavia, il concetto di processo risolutivo è abbastanza generale da comprendere ogni tipo di problema. Tornando alla metafora tra mente e braccio fatta precedentemente, possiamo dire che noi, la mente, siamo i risolutori del problema, mentre il computer, il braccio, è l’esecutore materiale, che troverà effettivamente la soluzione, seguendo i nostri ordini. Il risolutore definisce il processo risolutivo, e l’esecutore è colui che lo esegue. Questo concetto è espresso nella seguente figura Fino a questo punto non abbiamo ancora parlato di programmi. Ci arriveremo tra poco. Intanto, osserviamo che il linguaggio con cui noi (i risolutori) definiamo il processo risolutivo, può essere diverso da quello che usa il computer (l’esecutore). Nel mondo dei computer, il linguaggio usato dall’uomo è detto linguaggio naturale, ed è molto diverso dal linguaggio che usa il computer, il cui alfabeto è fatto invece di 0 e 1, cioè assenza o presenza di corrente elettrica. Occorre allora tradurre i comandi dal linguaggio naturale nel linguaggio del computer. Inoltre, immaginiamo una situazione in cui più persone sono impegnate nel trovare il processo risolutivo per un problema molto complesso: occorre che essi usino uno stesso formalismo per descrivere tale processo, in modo che sia compreso da tutti. Due possibili risposte al secondo dei problemi appena esposti sono i diagrammi di flusso (anche chiamati diagrammi a blocchi, per via del fatto che ogni diversa operazione è rappresentata da un diverso blocco) e la pseudo codifica. Noi useremo solo il primo di questi due formalismi, che si adatta meglio ai nostri scopi (la pseudo codifica è soprattutto usata da chi programma il computer usando il linguaggio Pascal, e non è il nostro caso). Un algoritmo è la descrizione di un processo risolutivo fatta mediante diagrammi di flusso In quanto segue, poiché rappresenteremo sempre un processo risolutivo con diagrammi di flusso, parleremo indistintamente di processo risolutivo e algoritmo. Dunque, possiamo tranquillamente dire che un algoritmo è una sequenza di passi per risolvere un problema. Vediamo ora un po’ più in dettaglio come sono fatti i blocchi che compongono i diagrammi di flusso: inizio dell’algoritmo fine dell’algoritmo assegnamento input output selezione Il significato di ognuno di questi blocchi sarà più chiaro successivamente, quando parleremo di cosa sa fare il computer e di istruzioni. Per ora diciamo solo che ogni algoritmo inizia con il blocco start, termina con il blocco end, e vengono disegnate delle frecce tra un blocco e l’altro per rappresentare il flusso (cioè la sequenza) delle operazioni. Nel seguente esempio, si chiede al computer di prendere in input un dato numerico (ad esempio dalla tastiera) e visualizzarlo in output (ad esempio sul monitor). esempio di algoritmo Torneremo sui diagrammi di flusso tra poco. Per riprendere il discorso del risolutore e dell’esecutore, diciamo che il risolutore è colui che analizza il problema da risolvere, identificandone i dati e i risultati da ottenere, e disegna con un diagramma di flusso un algoritmo per la sua soluzione, cioè i passi per arrivare alla soluzione. Nel mondo dell’informatica, la figura professionale del risolutore è spesso indicata con il termine analista. Scritto l’algoritmo, bisogna tradurlo nella lingua del computer: occorre, cioè, che l’algoritmo venga tradotto in istruzioni vere e proprie in linguaggio binario. Chi effettua questa traduzione? Il compito è svolto in due fasi: nella prima, un programmatore prende l’algoritmo, cioè il diagramma di flusso, e lo traduce in un programma scritto in un opportuno linguaggio di programmazione, come ad esempio i linguaggi C, Java, Visual Basic, Pascal, ecc. Se si usa il linguaggio C, ad esempio, questo programma risulterà scritto in un file di testo con estensione .cpp (che sta per C plus plus, un estensione del linguaggio C). Nella seconda fase un apposito software, detto compilatore, traduce il programma scritto in linguaggio C (cioè il file .cpp) nel linguaggio binario, ottenendo così un programma eseguibile (un file con estensione .exe), quindi utilizzabile. Poiché stiamo imparando a essere dei buoni analisti, oltre che degli ottimi programmatori, occorre che sappiamo distinguere quando un algoritmo è un buon algoritmo e quando invece no: ogni buon algoritmo dovrebbe avere le seguenti caratteristiche: - - - - finito: il numero di passi (blocchi) che compongono l’algoritmo (diagramma di flusso) deve essere finito, eventualmente un numero alto, ma finito. deterministico: il risultato dell’elaborazione non deve dipendere da fattori casuali, come il lancio di una moneta o di un dado; in altre parole, non deve essere affetto da probabilità. Lo stesso concetto può essere espresso in questo modo: l’algoritmo deve produrre sempre gli stessi risultati a partire dagli stessi dati iniziali. generale: l’algoritmo deve poter risolvere un’intera classe di problemi simili tra loro, e non un singolo problema. Ad esempio, un algoritmo che sa sommare due numeri, qualsiasi essi siano, è generale, mentre non lo è un algoritmo che sa sommare solo 2 e 5. completo: l’algoritmo deve tener conto di tutti i casi possibili che possono verificarsi, senza incappare in errori o situazioni spiacevoli. Ad esempio, se si vogliono dividere tra loro due numeri, occorre considerare il fatto che il denominatore non può essere uguale a 0, altrimenti si verifica un errore. di durata finita: sebbene il numero di passi sia finito, questo non vuol dire che siamo certi che l’algoritmo finirà dopo un certo tempo quando verrà eseguito. Come vedremo, ci sono delle istruzioni che permettono di “ciclare”, cioè di ripetere alcuni passi più volte, e se non si prevede in questi casi una possibilità di uscita dal ciclo, si ciclerà all’infinito sempre sugli stessi passi (si dice in questi casi che si entra in un loop infinito). 2) Analisi dei dati Concentriamoci per il momento sulla figura dell’analista: innanzitutto, egli deve saper individuare tutti i dati del problema da risolvere, indicando di che tipo di dati si tratta, e quali sono i risultati da ottenere. In altre parole, egli deve identificare tutte le informazioni importanti del problema, e sapere che dentro al computer esse non avranno più nessuna distinzione semantica: il numero dei giocatori di una squadra di calcio, 11, nel computer è rappresentato con il solo numero 11, e non con il significato che questo numero ha. Oppure, ad esempio, se dobbiamo scrivere un programma che calcola il 10% di sconto su un prezzo, il dato “grezzo” che usa il computer è 10, ma noi sappiamo che si tratta di una percentuale da applicare. Questo aspetto è molto importante quando si sviluppa un programma: noi umani associamo informazioni ai dati, il computer no! E’ necessario allora, come dicevamo, individuare i diversi tipi di dati che possiamo avere nel computer che si andrà a programmare, ben sapendo che ognuno di essi avrà un ben preciso significato per noi e il nostro algoritmo. Una prima classificazione dei dati possiamo farla in base al tipo: esistono dati numerici e alfanumerici. Tra i primi, distinguiamo dati interi e dati reali (che a differenza degli interi, hanno una parte frazionaria e sono rappresentati, come sappiamo, in virgola mobile). A loro volta, i dati interi possono essere suddivisi in interi senza segno (solo positivi) e interi con segno (positivi e negativi), e per ognuno di questi due tipi il linguaggio C ci mette a disposizione tre tipi di dato: short int o interi corti, rappresentati su due byte, long int o interi lunghi, rappresentati su quattro byte, e int che possono essere rappresentati su due o 4 byte, a seconda dei computer. Tra i dati alfanumerici, abbiamo invece i caratteri (come le lettera ‘A’, ‘B’, o i segni di punteggiatura, o le parentesi, o ogni altro simbolo che possiamo digitare sulla tastiera) e le stringhe, che sono sequenze di caratteri (un po’ come le nostre parole, ad esempio “pippo”, “ciao mondo”, ecc.). Per i caratteri, il linguaggio C ci mette a disposizione il tipo char, che rappresenta ogni diverso carattere su 1 byte in codifica ASCII. Per le stringhe, esse vanno definite sempre con il tipo char, ma specificando il numero di catatteri che compongono la stringa (lo so, è poco chiaro detto così, tranquilli, ci torneremo più avanti). Nella seguente figura possiamo riassumere l’intero arco dei tipi di dato disponibili in linguaggio C. Per una trattazione più ampia dei tipi di dato in linguaggio C (il linguaggio che noi useremo per programmare) si veda la dispensa sulla rappresentazione delle informazioni. Una seconda classificazione dei dati è invece fatta in base all’utilizzo: distinguiamo così dati di input, dati di output, e dati di lavoro. I dati di input sono i dati del problema da risolvere, da immettere nel computer (infatti, il termine input viene dall’inglese to put in, che significa appunto mettere dentro). I dati di output sono i risultati da ottenere, ossia la soluzione del problema che intendiamo risolvere, che il computer dovrà fornirci (infatti, il termine output viene dall’inglese to put out, che significa appunto mettere fuori). Tutti i rimanenti dati su cui il computer dovrà lavorare, che serviranno per memorizzare calcoli intermedi o altre importanti informazioni sono i dati di lavoro. Un esempio aiuterà a capire meglio questa classificazione: supponiamo di dover risolvere il seguente problema: Dato il perimetro di un quadrato, trovare la sua area. Il perimetro è il dato iniziale del nostro problema, quindi il dato di input. L’area è il risultato da ottenere, e perciò il dato di output. Per ottenere l’area a partire dal perimetro, però, dovremo prima dividere il perimetro per 4, ottenendo così un altro dato: il lato. Non essendo questo dato né di input né di output, si tratta di un dato di lavoro. Basterà a questo punto moltiplicare il lato per sé stesso, per ottenere la soluzione al problema, cioè l’area. Per completezza, riportiamo l’algoritmo che risolve il problema dell’esempio Una terza e ultima classificazione dei dati possiamo farla in base alla loro rappresentazione: troviamo i dati costanti, le costanti e le variabili. I dati costanti I dati costanti sono quelli che vengono usati così come sono, e non sono memorizzati in memoria RAM (la memoria di lavoro del computer). Alcuni esempi ci aiuteranno a capire di che si tratta: dati costanti di tipo numerico – intero 3 129 dati costanti di tipo numerico – reale 1.55 -12.1 -48 717 0.0009 5.0 dati costanti di tipo alfanumerico – carattere ‘A’ dati costanti di tipo alfanumerico – stringa “ciao” ‘Z’ ‘+’ ‘{‘ ‘9’ “io sono il prof” ‘?’ “p_12” Si noti che i dati costanti di tipo carattere sono sempre rappresentati tra singoli apici, mente i dati costanti di tipo stringa sono rappresentati tra doppi apici. Le costanti e le variabili A differenza dei dati costanti, costanti e variabili sono dati memorizzati nella memoria RAM e sono caratterizzati da un identificatore (un nome), mediante il quale possiamo richiamarli dalla memoria. In particolare, le costanti sono dati memorizzati che non possono variare il loro valore durante l’esecuzione di un programma: viene assegnato all’identificatore di costante un valore all’inizio del programma e questo resta lo stesso fino alla fine del programma. Le variabili, invece, sono dati memorizzati ai quali è possibile cambiare il valore durante l’esecuzione di un programma: all’identificatore di costante può essere assegnato un nuovo valore, che sarà quindi memorizzato in RAM. Sia gli identificatori di costante che gli identificatori di variabile devono essere dichiarati prima di poterli usare: la dichiarazione di un identificatore ha l’effetto di riservare memoria per quel dato e chiamare quella zona di memoria con il nome che le abbiamo attribuito. Per dichiarare una costante, si usa in linguaggio C la seguente direttiva #define nome valore Ad esempio, con la dichiarazione #define pigreco 3.14 stiamo riservando una zona di memoria fatta di tanti byte quanti ne servono per memorizzare il dato 3.14 (che essendo di tipo float, sarà di 4 byte), e chiamando quella zona di memoria pigreco. Ogni volta che dovremo usare questo dato, nel nostro programma, sarà sufficiente scrivere il nome pigreco e il computer provvederà automaticamente a recuperare dalla memoria RAM il suo valore, come ad esempio nell’espressione 2*pigreco*R. Per la dichiarazione di una variabile, invece, si scrive in C tipo identificatore; Ad esempio, con la dichiarazione short int perimetro; stiamo riservando una zona di memoria fatta di tanti byte quanti ne servono per memorizzare un dato di tipo short int (quindi 2 byte) e chiamando quella zona di memoria perimetro. Ogni volta che dovremo usare questo dato, nel nostro programma, sarà sufficiente scrivere il nome perimetro e il computer provvederà automaticamente a recuperare dalla memoria RAM il suo valore. Inoltre, essendo una variabile, è anche possibile modificare il valore della variabile durante il programma, (mentre questo non è possibile con le costanti) come ad esempio fissare il suo valore a 100 perimetro = 100; E’ possibile anche dichiarare più variabili dello stesso tipo in un’unica dichiarazione, nel seguente modo tipo identificatore1, identificatore2, …, identificatoreN; Ad esempio, se dobbiamo memorizzare le misure della base e dell’altezza di un rettangolo, e vogliamo che entrambi i numeri possano avere anche una parte decimale, potremmo usare le variabili float base, altezza; Facciamo ancora qualche esempio di dichiarazione di variabili: long int anno_di_nascita; dichiara la variabile anno_di_nascita di tipo long int (4byte) double d1, d2, d3; dichiara le variabili d1, d2 e d3 di tipo double (8byte) char c; dichiara la variabile c di tipo char (1 byte) char nome[20]; dichiara la variabile nome di tipo sequenza di 20 caratteri (20 byte) Per la scelta degli identificatori il programmatore dovrà seguire alcune regole di buon senso: Non iniziare un identificatore con un numero (altrimenti il compilatore crederà che si tratta di un dato costante di tipo numerico); Non includere caratteri speciali, come simboli di operazioni o parentesi o lettere accentate (ad esempio lato-1 per indicare un lato fa confondere il compilatore, che crede che intendiamo sottrarre 1 all’identificatore lato); Non includere spazi (lato 1 non va bene); Usare solo lettere e numeri, con una lettera all’inizio, ed eventualmente il solo carattere _ (underscore) per separare parole diverse (ad esempio, lato_1, lato_2, ma andavano bene anche lato1 e lato2); Eventualmente usare le maiuscole e le minuscole per separare le parole, poiché il C distingue le due: si dice che è un linguaggio case sensitive (ad esempio, avremmo potuto usare gli identificatori LatoUno e LatoDue, che sono diversi da latouno e latodue); Usare degli identificatori che ci ricordano l’informazione associata al dato che stiamo memorizzando: se nell’esempio di prima al posto di P, A e L per indicare il perimetro, l’area e il lato del quadrato, avessimo usato IDENTIF1, IDENTIF2 e IDENTIF3 avremmo avuto maggiori difficoltà a ricordarci quale identificatore memorizzava cosa. E’ naturale chiedersi: “Perché dobbiamo usare le costanti, al posto dei dati costanti?”. Nell’esempio della costante pigreco fatto sopra, il valore sarà sempre 3.14, quindi perché andare a memorizzarlo, e non usare direttamente 3.14? Proviamo a illustrare il vantaggio offerto dalle costanti con un esempio: supponiamo che un programma per la contabilità aziendale di una grande azienda deve, in più punti del programma, calcolare l’IVA (Imposta sul Valore Aggiunto) sui prezzi dei prodotti. Ora, al giorno d’oggi sappiamo che l’IVA viene pagata nella percentuale del 20%, quindi potremmo trovare in tutti i punti del programma dove serve, l’espressione prezzo*20/100 Immaginiamo adesso che il governo, con una nuova legge, cambi la percentuale dell’imposta e la fissi al 22%. Dovremmo allora andare a modificare il nostro programma in tutti i punti dove compare il calcolo di questa imposta, e riscrivere l’espressione come prezzo*22/100 Per evitare di dover modificare l’intero programma, sarebbe più conveniente usare una costante, come segue: #define IVA 20 … prezzo*IVA/100 Al cambiare dell’imposta, basterà quindi solo modificare la riga in cui viene dato il valore alla costante IVA, mentre tutto il resto del programma potrà continuare a funzionare senza modifiche. #define IVA 22 … prezzo*IVA/100 3) Cose che il computer sa fare Per capire quali comandi possiamo dare al computer, occorre innanzitutto sapere esattamente cosa esso è in grado di fare: certo non possiamo chiedergli di prepararci un toast o il tiramisù, ma nemmeno di calcolarci il 150esimo numero primo o il massimo comune divisore tra due numeri: non esiste nessun comando del genere per la CPU. I comandi che si possono dare al computer sono elementari (i programmi, infatti, sono insiemi di comandi elementari per risolvere problemi complessi). In dettaglio, il computer è in grado di: A. B. C. D. E. F. eseguire calcoli aritmetici e logici memorizzare un dato recuperare un dato dalla memoria acquisire un dato in input produrre un dato in output fare confronti Iniziamo con il punto A. Le operazioni aritmetiche che il computer è in grado di eseguire sono: Addizione Sottrazione Moltiplicazione Divisione Modulo + * / % (solo per dati di tipo intero) L’operatore / che effettua la divisione ha un doppio comportamento, a seconda che si applichi a dati di tipo intero o reale: la divisione tra dati di tipo intero produce un risultato intero, cioè il quoziente della divisione, mentre la divisione tra dati di tipo reale produce un risultato reale, cioè con una parte frazionaria. Ad esempio, 15/6 = 2 15.0/4.0 = 3.75 perché sia 15 che 6 sono dati di tipo intero, mentre perché sia 15.0 che 4.0 sono dati di tipo reale Per avere il resto della divisione tra interi, si usa l’operatore modulo %. 15%6 = 3 il resto della divisione tra 15 e 6 è 3 In realtà il computer sa fare molto più di una semplice operazione alla volta: sa valutare espressioni. Ricordiamo che un’espressione è un insieme di termini (numeri e identificatori) collegati tra loro con delle operazioni, ed ha un valore che può essere calcolato effettuando le operazioni. Se scriviamo, ad esempio, (3+2)*5 il computer farà entrambe le operazioni ottenendo il valore 25. E se scriviamo 2*pigreco*R il computer recupererà il valore degli identificatori pigreco e R dalla RAM, e calcolerà il valore dell’espressione. Per quanto riguarda le operazioni logiche, in linguaggio C è possibile far fare al computer le seguenti: AND OR NOT && || ! Per quanto riguarda il punto B, la memorizzazione di un dato, essa avviene assegnando a una variabile il risultato di un’espressione, oppure mediante un’operazione di input. Ne parleremo più in dettaglio tra poco, quando parleremo più specificatamente di istruzioni. Relativamente al punto C, cioè recuperare un dato dalla memoria, entrano in gioco gli identificatori di costante e di variabile di cui abbiamo parlato nel paragrafo precedente, nel modo che abbiamo già visto negli esempi: i termini che possono comparire in un’espressione sono dati costanti, ma anche costanti e variabili, come nei seguenti esempi: ((3+2)*5)/4) P/4 espressione con soli dati costanti espressione con un identificatore (di costante o variabile) e un dato costante Quando il computer incontra un identificatore all’interno di un’espressione, va a recuperare nella memoria RAM il valore associato a quell’identificatore, e lo sostituisce nell’espressione al posto dell’identificatore. Per il punto D, acquisire un dato in input, esistono delle istruzioni che permettono al computer di acquisire un dato dalla tastiera o da un altro dispositivo di input, e memorizzarlo in una zona di memoria precedentemente allocata, che dovrà quindi essere una variabile (il suo valore infatti cambierà a seguito dell’immissione del dato). Analogamente, per il punto E, vedremo tra breve che esistono delle istruzioni per ordinare al computer di visualizzare un dato o più dati sul monitor, o inviarli a qualche altro dispositivo di output (stampante, etc.). Analizziamo ora in dettaglio il punto F: il confronto tra espressioni. Il computer dispone di un certo numero di operatori di confronto, anche detti operatori relazionali, che gli permettono di ottenere un’espressione booleana (vera o falsa) mediante confronto di due espressioni aritmetiche. Gli operatori relazionali sono: < > <= >= == != minore maggiore minore o uguale maggiore o uguale uguale diverso (not uguale) Ad esempio, 5 < 4 è un’espressione booleana falsa ottenuta mediante confronto con l’operatore relazionale < tra l’espressione 5 e l’espressione 4. Giusto per fare un altro esempio, (5+1) != (2-3) è un’espressione booleana vera, in quanto il valore dell’espressione 5+1 (cioè 6) è diversa dal valore dell’espressione 2-3 (cioè -1). APPROFONDIMENTO: Prima di passare a parlare di istruzioni e di programmi in linguaggio C, ancora qualche considerazione di approfondimento sul punto A: le operazioni aritmetiche e logiche. Abbiamo detto che l’operatore di divisione / ha un diverso comportamento a seconda che lo si applichi a dati di tipo intero o a dati di tipo reale (ma anche gli altri operatori, a ben pensarci, hanno questo doppio comportamento: l’addizione tra byte che rappresentano dati interi, ad esempio, funziona in modo diverso dall’addizione tra byte che rappresentano dati in virgola mobile, anche se noi non ce ne accorgiamo). Ma cosa succede se proviamo a fare un’operazione tra dati di tipo diverso? Ad esempio, se vogliamo effettuare la seguente divisione tra un dato reale e un dato intero? 15.5/4 Poiché ogni intero è anche un reale, ma non è vero il viceversa (4 è uguale a 4.0), il computer trasforma automaticamente il dato intero in un dato reale, senza perdita di informazione, effettuando così la divisione 15.5/4.0 Appare evidente come non si può effettuare l’altra conversione, da reale a intero, in quanto si avrebbe una perdita di informazione. Infatti, 15/4 non dà lo stesso risultato. La conversione di un dato da un tipo a un altro è chiamata casting, e può anche essere invocata esplicitamente dal programmatore, scrivendo (nel nostro esempio) 15.5/float(4) Se l’operazione di casting non ha perdita di informazione, come nell’esempio precedente, non ci sono problemi. Altrimenti, il compilatore ci avverte del rischio che si corre. 4) Il ciclo di vita del software Abbiamo parlato finora dei compiti dell’analista, che fa, a quanto sembra, il grosso del lavoro. E’ lui che deve individuare i dati, e descrivere i passi per la soluzione. Quando poi passa il suo algoritmo al programmatore, sembra che quest’ultimo debba limitarsi solo a tradurre l’algoritmo in un programma. Non è affatto semplice! Bisogna conoscere a fondo il linguaggio di programmazione che si usa, le potenzialità offerte dal linguaggio e dalle sue librerie, l’ambiente di programmazione e gli strumenti che questo mette a disposizione. Come se non bastasse, il programmatore è colui che deve anche verificare che poi il programma non contenga errori, che funzioni sempre, che faccia esattamente quello che ci si aspetta. Questo aspetto alle volte è estremizzato: si pensi a una casa produttrice di videogiochi che, non potendo impiegare i propri dipendenti tutto il tempo a giocare, ingaggia i migliori giocatori di tutto il mondo per testare un nuovo gioco. Insomma, un lavoraccio! Che però si fa con passione, se si sa cosa si sta facendo, e quando si vogliono vedere i risultati! Proviamo allora a descrivere tutte le fasi della vita di un programma, dalla sua ideazione fino alla sua ultima realizzazione. Lo facciamo con un algoritmo, essendo anche questa una sequenza di passi per partire da una situazione iniziale (ideazione del programma) a una situazione finale (programma realizzato), anche per prendere maggiore confidenza con i diagrammi di flusso Come si vede dalla figura, la parte di programmazione è piuttosto complessa. Per fortuna, l’ambiente di programmazione che useremo, Dev-C++ prodotto dalla Bloodshed e distribuito gratuitamente (il link per scaricarlo è nel nostro sito web) ci risolve gran parte del lavoro: fa tutta la parte evidenziata in giallo! Cominciamo a illustrare la figura con ordine. All’inizio, come già sappiamo, l’analista esegue l’analisi dei dati e sviluppa l’algoritmo, cioè il processo risolutivo. Il programmatore a questo punto con le sue conoscenze di informatica e del linguaggio C scrive il programma, e chiede al compilatore di tradurlo in binario. Si ottiene così il codice oggetto, che deve però essere collegato a tutti gli altri codici oggetto che sono stati utilizzati nel programma Questa operazione è svolta da un altro software, diverso dal compilatore: il linker. Esso mette assieme i diversi codici oggetto (il nostro e quello delle librerie che abbiamo utilizzato) che devono comporre il programma, e produce il programma nella forma di un file eseguibile. Il compilatore funziona effettuando una scansione del file .cpp che costituisce il programma, riconoscendo i vari token: comandi, direttive, identificatori, dati costanti, e tutto quanto compare in un programma in linguaggio C. Come vedremo, le istruzioni vanno date in modo preciso, quindi il compilatore si accorge anche di eventuali errori di sintassi che possiamo aver commesso nella scrittura di un programma, cioè gli errori che non rispettano le regole grammaticali del C. In questo caso, ci avverte con una notifica del tipo di errore che ha riscontrato, dandoci così indicazione su come risolverlo. Il fatto che un programma sia sintatticamente corretto però non vuol dire che faccia esattamente quanto vogliamo fargli fare. Ad esempio, se intendevamo scrivere un programma per il calcolo del minimo comune multiplo tra due numeri, e questo calcola il massimo comune divisore, non possiamo accontentarci del fatto che “…però funziona”. Gli errori logici sono dovuti ad errori di analisi del problema e dei passi necessari per risolverli, quindi è l’algoritmo che deve essere rivisto. Sfortunatamente, non c’è software che possa darci una mano in questo: dobbiamo necessariamente effettuare il test del programma nei diversi casi che possono presentarsi, per essere certi del suo corretto funzionamento. In generale sono difficili da scovare, a meno che non si tenga una traccia dello stato della memoria, istante per istante durante l’esecuzione del programma, per essere certi che tutte le quantità su cui lavora il computer sono esattamente quelle che intendiamo. APPROFONDIMENTO: L’ambiente di programmazione Dev-C++ che utilizziamo per scrivere programmi ci mette a disposizione oltre a un editor (il programma di scrittura, simile al Notepad di Windows), il compilatore, il linker, e uno strumento chiamato debugger per effettuare l’esecuzione del programma passo passo, allo scopo di poter controllare istante per istante lo stato del programma e della memoria). 5) Istruzioni e programmi in linguaggio C Ogni programma in linguaggio C è composto da un insieme di direttive per il compilatore un insieme di dichiarazioni di identificatore una sequenza di istruzioni, una per ogni riga, che vengono racchiuse tra parentesi graffe in un costrutto che si chiama main commenti per documentare il lavoro come segue: …direttive per il compilatore… …dichiarazioni di identificatore di costante …dichiarazioni di identificatore di variabile main() { istruzione1; istruzione2; … istruzioneN; } I commenti possono comparire ovunque all’interno del programma, iniziano con la coppia di caratteri /* e terminano con la coppia di caratteri */. Tutto quello che compare tra queste due coppie di caratteri non sarà interpretato dal compilatore, quindi possiamo scriverci proprio tutto quello che vogliamo. Le direttive per il compilatore servono a indicare quali librerie devono essere aggiunte, dal linker, al nostro codice oggetto per ottenere l’eseguibile, oltre ad altre informazioni per la compilazione (che al momento non ci interessano). Vedremo, in particolare, che ci serviranno due particolari librerie, la stdio.h e la stdlib.h per poter effettuare le istruzioni di input/output standard e le chiamate di sistema, rispettivamente. Delle dichiarazioni di costante e di variabile abbiamo già parlato: ricordiamo solo che ogni identificatore che verrà usato nel programma dovrà essere stato prima dichiarato (il compilatore segnala un errore qualora incontri un identificatore non precedentemente dichiarato), per avere il relativo spazio di memoria allocata. Parliamo dunque di istruzioni: i comandi che possiamo dare al computer. Istruzione di input standard (da tastiera) scanf(“specificatore di formato”, &variabile); Questa istruzione pone il programma in attesa che l’utente digiti un dato sulla tastiera, quindi andrà a memorizzarlo nella zona di memoria associata all’identificatore di variabile indicato. Lo specificatore di formato serve a informare il computer di che tipo di dato si tratta; abbiamo i seguenti possibili valori: %d decimal %f floating point dato numerico di tipo reale %c character dato alfanumerico di tipo char (codifica ASCII) %s string dato alfanumerico di tipo stringa (sequenza di caratteri in codifica ASCII) dato numerico di tipo intero Ad esempio, se dobbiamo inserire in input la misura del perimetro del quadrato e memorizzarla nella variabile P, come nell’algoritmo di pagina 6, supposto che la variabile P sia di tipo float (che è più generale di un tipo intero – questo ci permetterà di risolvere anche problemi con perimetri che hanno una parte frazionaria), potremmo scrivere scanf(“%f”, &P); L’operatore & presente nell’istruzione scanf è l’operatore indirizzo di, che data una variabile, ne calcola il suo indirizzo di memoria. L’istruzione scanf, infatti, richiede di conoscere esattamente l’indirizzo di memoria (a che numero di byte) andare a memorizzare il dato inserito. Vediamo ora qualche altro esempio: scanf(“%d”, &eta); /* inserimento di un dato numerico intero memorizzato nella variabile eta */ scanf(“%c”, &sesso) /* inserimento di un dato alfanumerico di tipo carattere memorizzato nella variabile sesso (potremmo dare alla variabile, ad esempio, i valori M o F) */ scanf(“%f”, &lato); /* inserimento di un dato numerico reale memorizzato nella variabile lato */ scanf(“%s”, nome); /* inserimento di un dato alfanumerico di tipo stringa memorizzato nella variabile nome */ Si noti come per i dati di tipo stringa non è necessario l’operatore &. Il motivo sarà chiaro il prossimo anno, quando parleremo di vettori e puntatori (che sono argomenti troppo complicati per ora). Istruzione di output standard (da tastiera) printf(“stringa di output”, espressione1, espressione2, …, espressioneN); Questa istruzione visualizza sul monitor la stringa di output racchiusa tra doppi apici. La presenza delle (eventuali) espressioni dopo la virgola serve per inserire all’interno della stringa di output da visualizzare dei valori numerici. Per ogni espressione che compare dopo la virgola, dovrà esserci uno specificatore di formato all’interno della stringa di output, al cui posto sarà visualizzato il valore dell’espressione. Facciamo un po’ di esempi: printf(“ciao mondo”); /* visualizza la scritta ciao mondo sul monitor */ printf(“%f”, area); /* visualizza il valore della variabile area sul monitor */ printf(“il risultato e’ %d”, ris); printf(“2 piu’ 2 fa %d”, 2+2); /* visualizza la scritta con il valore della variabile ris */ /* visualizza la scritta 2 piu’ 2 fa 4 */ printf(“ciao io sono %s e ho %d anni”, nome, eta); /* visualizza la scritta con il valore della variabile nome di tipo stringa e il valore della variabile eta di tipo intero */ All’interno della stringa di output possono essere anche inseriti dei caratteri speciali, come il carattere di a capo o newline, indicato con \n, o come una tabulazione, indicata con \t. Ad esempio, l’ istruzione printf(“io\nsono\nil\nprof”); produce il seguente risultato sul monitor: io sono il prof Le istruzioni scanf e printf fanno riferimento a input e output standard, cioè, la tastiera per l’input e il monitor per l’output. Tuttavia, è possibile che un nostro programma debba utilizzare altri dispositivi di input o output: si pensi, ad esempio, a un programma per un non vedente; in questo caso, l’output dovrà essere diretto verso una stampante Braille, anziché sul monitor. Occorrerà allora usare altre istruzioni di input e output, diverse da quelle viste. Per avere il massimo della flessibilità, il C non comprende direttamente le istruzioni di I/O, che costituiscono invece l’interfaccia del programma con l’esterno. Esse sono definite in una libreria di funzioni. Entrambe le istruzioni scanf e printf sono definite nella libreria standard di input/output che si chiama stdio. Dovremo allora dire al nostro programma di includere questa libreria, per disporre di queste due istruzioni. All’inizio del nostro programma scriveremo allora #include <stdio.h> Qualora volessimo usare altri tipi di input e output, dovremo includere altre librerie diverse da quella standard. Istruzione di assegnamento variabile = espressione; Serve per memorizzare un dato nella RAM. Per accedere alla RAM, come abbiamo visto, serve un identificatore. In particolare, poiché dobbiamo dare un valore all’identificatore durante l’esecuzione del programma, occorre che questo sia un identificatore di variabile. Quando il computer deve eseguire l’istruzione di assegnamento, esso prima valuta l’espressione a destra del segno = (ATTENZIONE: il simbolo = rappresenta l’assegnamento a una variabile, mentre il simbolo == è l’operatore per il confronto dell’uguaglianza tra due espressioni), eseguendone le operazioni, quindi va a scrivere nella memoria RAM, all’indirizzo che era stato riservato in fase di dichiarazione a quell’identificatore di variabile. Qualche esempio aiuterà a capire meglio: perimetro = lato*4; Il computer prima valuta l’espressione a destra del segno =. Cioè, recupera dalla RAM il valore dell’identificatore lato (supponiamo, ad esempio, 12), quindi lo moltiplica per 4. Il risultato dell’espressione, (48, nel nostro esempio) sarà memorizzato nella RAM in corrispondenza dell’identificatore perimetro. Possiamo, in altri termini, dire che la variabile perimetro ha ora il valore 48 (se lato ha valore. 12, come nel nostro esempio). nome = “pippo”; L’espressione a destra è stavolta costituita da un solo dato costante (e nessuna operazione) di tipo stringa, che verrà assegnato alla variabile nome. ipotenusa = sqrt(cateto1*cateto1+cateto2*cateto2 ); In questa istruzione di assegnamento, l’espressione contiene una chiamata alla funzione matematica che esegue la radice quadrata (sqrt sta per square root, radice quadrata in inglese, appunto). Questa funzione è definita nella libreria matematica math.h, che va inclusa, se intendiamo usarla, esattamente come la libreria di input/output standard stdio.h, con la direttiva include. Il computer esegue prima il calcolo dell’argomento della sqrt, ossia la somma dei quadrati dei cateti, quindi richiama il calcolo della radice quadrata. Il risultato dell’espressione sarà assegnato alla variabile ipotenusa. Ovviamente, perché un assegnamento sia possibile, occorre che il tipo del valore dell’espressione sia lo stesso del tipo della variabile a cui questo valore deve essere assegnato. Non è possibile, ad esempio, assegnare un valore numerico a una variabile di tipo stringa, o un valore alfanumerico a una variabile di tipo float, ad esempio. In alcuni casi, qualora possibile, il computer effettua un’operazione di casting (si veda l’approfondimento a pag. 10) ma avverte quando questa potrebbe causare una perdita di informazione. 5) Buone pratiche di programmazione Quando si programma, bisogna sempre tener a mente alcune regole di buon senso, che ci permetteranno di rendere più leggibili e amichevoli i nostri programmi: Istruire sempre l’utente su ciò che deve fare, quando è richiesta un’interazione: ad esempio, prima di chiedere un dato in input (scanf), mandare un messaggio (printf) che chiede all’utente di inserire un dato, possibilmente spiegandogli di che dato si tratta. Inserire una pausa alla fine del programma, per impedire che il sistema operativo (windows, o mac o linux) chiuda la sua finestra al termine dell’esecuzione del programma, non dando il tempo di vedere i risultati. Per inserire una pausa, occorre fare una chiamata al sistema nel seguente modo system(“pause”); Il computer arresterà l’esecuzione del programma, inserirà nel video il messaggio “Premi un tasto per continuare…” e aspetterà la digitazione di un tasto per proseguire (e terminare) l’esecuzione del programma. La funzione system per le chiamate al sistema è definita nella libreria standard, stdlib.h, che va inclusa come le altre. Allineare a sinistra le istruzioni all’interno del costrutto main, una su ogni riga e un paio di spazi più all’interno delle parentesi graffe. Allineare tra loro le parentesi graffe. Inserire nel codice dei commenti per documentare il lavoro che si sta svolgendo, nel modo più funzionale (né privo di documentazione, né con documentazione eccessiva o ridondante, che rischia di non essere di nessun aiuto). Usare i commenti per descrivere il tipo di problema risolto, la data del programma, il significato dei diversi identificatori, i calcoli più importanti. I commenti possono essere inseriti racchiudendoli tra /* ..commento… */. Salvare continuamente il file del programma che si sta scrivendo (.cpp), per evitare che guasti accidentali al computer possano compromettere ore e ore di lavoro.