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.