Fondamenti di Informatica
Appunti schematici con esempi eseguibili passo passo. Costruiti sul materiale del corso (introduzione, allegati sugli algoritmi, dichiarazioni/istruzioni/programmi) e pensati per crescere lezione dopo lezione.
Ogni riquadro con il pallino arancione è un esecutore simulato: cambia i dati in ingresso, premi Esegui e guarda evolvere i contenitori passo dopo passo. Il punto non è vedere il risultato, è vedere perché quella combinazione di passi lo produce.
Il bottone ◐ in alto a destra cambia il tema (chiaro / scuro / automatico).
Capitoli
Problemi, esecutori, algoritmi
La terna (I, O, R). Che cos'è un esecutore. Definizione di algoritmo e sue proprietà. Memorizzazione nei contenitori. Le tre strutture di controllo e i diagrammi di flusso.
Catalogo di algoritmi
Divisione per sottrazioni ripetute, logaritmo intero, massimo di una sequenza, Euclide, ricerca sequenziale e ricerca dicotomica — tutti eseguibili passo passo.
La macchina astratta C
Unità di controllo, memoria, variabili e identificatori. Perché servono le dichiarazioni di tipo. Espressioni, condizioni, operatori logici, conversioni di tipo.
Istruzioni e programmi in C
Assegnamento, scanf/printf, if/else, while, do-while, for. Programmi completi commentati, dall'algoritmo al codice.
Array e stringhe
Aggregati di dati omogenei, indici, ricerca in array, variabile sentinella, rappresentazione in base, massimi locali, stringhe e terminatore '\0'.
Il corso in breve
Orario
| Lun 10:15–13:15 | Lezione · aula 26.1.1 |
| Lun 14:15–17:15 | Laboratorio · aula B.2.4 |
| Mer 8:15–10:15 | Lezione · aula 2.1.2 |
| Gio 13:15–16:15 | Esercitazione · aula T.1.1 |
Laboratorio solo 5 incontri: 12/10, 9/11, 23/11, 30/11, 21/12. Frequenza non obbligatoria, ma valutata attraverso parti del tema d'esame.
Esame
Prova scritta su carta: 4/5 esercizi sull'intero programma. Chi supera lo scritto sostiene una prova orale confermativa. Esito: voto dello scritto oppure rimandato/riprovato.
5 appelli l'anno: due a gennaio/febbraio, due a giugno/luglio, uno a settembre. Iscrizione entro la scadenza, niente appunti né dispositivi, si consegnano solo bella copia e testo.
Materiale
Libro di testo
K. N. King, Programmazione in C, Apogeo
Eserciziario
V. Caglioti, G. Fontana, Costruire un programma: guida al ragionamento algoritmico, McGraw-Hill
… ma soprattutto appunti presi a lezione. Non ci sono lucidi ufficiali né registrazioni di quest'anno.
Docenti
Docente: Vincenzo Caglioti — DEIB, edificio 20A, primo piano — ricevimento su appuntamento.
Esercitatore: ing. G. Fontana · Responsabile di laboratorio: ing. N. Catalano.
L'avvertenza che conta
Tutte le nozioni sul linguaggio C sono semplici. Combinarle ad arte per inventare l'algoritmo che risolve il problema assegnato è difficile. È questo il motivo per cui il corso può sembrare facile a chi ha già programmato — ed è proprio lì l'insidia.
All'esame non basta un programma che funziona: deve essere il frutto di un ragionamento solido e coerente con le specifiche date (niente cicli inutili, struttura dati richiesta, ecc.).
Il corso insegna due cose distinte, che avanzano in parallelo ma restano indipendenti:
- Analizzare i problemi e inventare algoritmi che li risolvano → attività creativa;
- Codificare quegli algoritmi in programmi scritti in C89 → attività di codifica, con un certo grado di creatività.
Questi appunti tengono separate le due cose: i capitoli 1–2 sono sul ragionamento (pseudocodice e diagrammi di flusso), i capitoli 3–5 sulla codifica.
Stato degli appunti
Coperto finora, dal materiale disponibile al 21 settembre 2026:
| Argomento | Dove | Stato |
|---|---|---|
| Organizzazione del corso, obiettivi, esame | questa pagina | ✅ |
| Problema, esecutore, algoritmo, strutture di controllo | cap. 1 | ✅ |
| Algoritmi di riferimento (allegati 1–6) | cap. 2 | ✅ |
| Macchina astratta C, dichiarazioni, espressioni, condizioni | cap. 3 | ✅ |
| Istruzioni, cicli, casting, programmi d'esempio | cap. 4 | ✅ |
| Array e stringhe | cap. 5 | ✅ |
| Codifica binaria dell'informazione | — | ⏳ da fare |
| Sottoprogrammi (funzioni, passaggio parametri) | — | ⏳ da fare |
| Strutture, file, allocazione dinamica, liste | — | ⏳ da fare |
| Ricorsione | — | ⏳ da fare |
Problemi, esecutori, algoritmi
Le tre nozioni su cui poggia tutto il corso, e il modo di combinare passi elementari per passare da un problema alla sua soluzione automatica.
Che cos'è l'informatica
L'oggetto dell'informatica è l'informazione: come ricavare ciò che non si sa (e che interessa) a partire da ciò che si sa.
Due definizioni equivalenti, entrambe usate a lezione:
- scienza dell'elaborazione dell'informazione (e della sua rappresentazione);
- scienza degli algoritmi.
Il corso è progettuale, come tutta l'ingegneria: si impara a ragionare per ideare algoritmi e scrivere programmi che risolvano un problema assegnato. Servono pochissime conoscenze disciplinari: è ragionamento puro.
La nozione di algoritmo ne richiede altre due, che vanno definite prima: problema ed esecutore.
Il problema: la terna (I, O, R)
Un problema è una terna (I, O, R) dove:
- I: due numeri (validi se sono naturali);
- O: un numero (valido se è naturale);
- R: il risultato è uno dei due dati e il risultato non è maggiore dell'altro dato.
Notare che R non dice come si trova il minimo: dice solo quale proprietà lega risultato e dati. La terna descrive il problema, non la soluzione. Se scrivi in R un pezzo di procedimento, hai già smesso di analizzare e hai iniziato (male) a progettare.
L'esecutore
Un sistema capace di eseguire in modo deterministico ciascun passo elementare appartenente a un insieme esiguo di passi elementari.
Un esecutore tipico per i nostri esempi sa fare solo questo:
- acquisizione dei dati (lettura);
- produzione dei risultati (scrittura);
- addizione di due numeri;
- sottrazione fra due numeri;
- confronto fra due numeri (calcolo dell'esito vero / falso);
- memorizzazione di un valore in un contenitore (vedi più avanti).
Un algoritmo è sempre relativo a un esecutore. «Trova il massimo» non è un passo elementare se l'esecutore sa solo confrontare: va costruito. Quando svolgi un esercizio, la prima domanda è quali passi ho davvero a disposizione?
L'algoritmo
Dati un problema P e un esecutore E, un algoritmo è una combinazione finita di passi elementari, ciascuno eseguibile da E, in grado di risolvere P: cioè, per qualunque insieme valido di dati in ingresso, produce in uscita risultati validi che soddisfano la relazione R.
Le due parole pesanti sono combinazione (si combinano in modi precisi: sequenza, selezione, iterazione) e finita (l'esecuzione deve prima o poi raggiungere un passo TERMINA).
Esempio: l'opposto di un numero intero
1. Acquisisci un numero intero 2. Sottrai dal valore 0 il valore del numero acquisito 3. Produci in uscita il risultato della sottrazione 4. TERMINA
Qui i passi sono eseguiti uno dopo l'altro: è una pura sequenza.
Esempio: il massimo fra due numeri naturali
1. Acquisisci due numeri naturali in ingresso 2. Confronta il valore dei due numeri 3. Se il primo numero è maggiore del secondo 3.1 produci il primo numero come risultato in uscita Altrimenti 3.2 produci il secondo numero come risultato in uscita 4. TERMINA
Qui compare una selezione: i passi 3.1 e 3.2 sono alternativi, non verranno mai eseguiti entrambi.
La computazione di un algoritmo è la successione di passi effettivamente eseguita, cioè il percorso seguito. Un solo algoritmo, tante computazioni diverse a seconda dei dati. Quando verifichi un algoritmo non devi controllare «il codice»: devi seguire una computazione alla volta, scegliendo dati che facciano prendere percorsi diversi.
Le proprietà che rendono un algoritmo tale
| Proprietà | Significato | Errore tipico |
|---|---|---|
| Eseguibilità | Ogni passo appartiene all'insieme dei passi elementari dell'esecutore. | Usare «ordina la sequenza» come se fosse un passo. |
| Determinismo | Lo stesso passo, sugli stessi dati, dà sempre lo stesso esito. | Passi ambigui («prendi un elemento qualsiasi»). |
| Finitezza | Per ogni insieme valido di dati la computazione raggiunge TERMINA. | Cicli che non riducono mai lo spazio di ricerca (vedi cap. 2). |
| Generalità | Funziona per tutti i dati validi, non per un esempio. | Provare solo il caso «bello» e non i casi limite. |
La memorizzazione: i contenitori
Memorizzare un valore in un contenitore significa salvarlo e conservarlo inalterato durante l'esecuzione di altri passi. È un passo elementare a tutti gli effetti, e senza di esso non si va da nessuna parte.
La notazione usata a lezione è la freccia:
A ← B - C A ← B - 3
Sono due passi in uno:
- si calcola la sottrazione fra due valori (ciascuno può essere contenuto in un contenitore, come B e C, oppure un valore non memorizzato, come 3);
- si memorizza il risultato nel contenitore A. Da quel momento il valore resta lì, inalterato,
fino a un eventuale futuro
A ← …che lo sovrascrive: del vecchio valore si perde ogni memoria.
| Dove compare A | Che cosa significa |
|---|---|
A ← … (a sinistra) | il nome del contenitore su cui scrivere il nuovo valore |
… ← A + 2 (a destra) | il valore memorizzato dentro A, da usare nel calcolo |
Ecco perché A ← A + 2 ha perfettamente senso e non è un'equazione: significa
«prendi il valore che c'è in A, aggiungici 2, riscrivi il risultato in A».
Un algoritmo minimo che lo usa: lo scambio
Se scrivo subito M ← N perdo per sempre il vecchio valore di M. Serve un
terzo contenitore temporaneo T che faccia da appoggio. Il problema non è «scambiare»: è
non distruggere un'informazione che serve ancora.
Esercizio: e se provassi senza T?
Traccia a mano la computazione di M ← N; N ← M partendo da M=7, N=12. Alla fine ti ritrovi
M=12 e N=12: il valore 7 è stato sovrascritto al primo passo e nessun passo successivo può recuperarlo.
È lo stesso motivo per cui, nel capitolo 5, prima di azzerare A[i] bisogna salvarne il valore in prec.
Le tre strutture di controllo
I «modi» per combinare i passi elementari si chiamano strutture di controllo. Sono tre, e bastano tutte e sole queste per esprimere qualunque algoritmo:
1 · Sequenza
Un passo dopo l'altro, nell'ordine scritto. L'algoritmo dell'opposto è tutto qui.
2 · Selezione
Durante l'esecuzione viene esaminata una condizione. Se risulta vera si esegue una certa sequenza di passi, se risulta falsa se ne esegue una diversa. Le due sequenze sono alternative: non verranno mai eseguite entrambe. Il ramo «altrimenti» può anche essere vuoto (non fare nulla).
3 · Iterazione (ciclo)
Un ciclo è caratterizzato da una condizione e da una sequenza di passi. Quando la computazione raggiunge la condizione la valuta:
- se risulta falsa, il corpo non viene eseguito e si passa oltre;
- se risulta vera, esegue i passi del ciclo e poi torna a valutare la condizione.
Un ciclo può essere eseguito 0 volte, 1 volta o più volte, a seconda di quante volte la condizione risulta vera. Il caso «zero volte» è quello che gli studenti dimenticano di verificare, ed è quasi sempre il caso limite chiesto all'esame.
Per ogni ciclo che scrivi devi saper rispondere a tre domande:
- Inizializzazione: quali contenitori devono avere un valore prima di entrare?
- Invariante: che cosa è vero a ogni giro? (es. «MAX contiene il massimo dei valori letti finora»)
- Terminazione: quale quantità decresce a ogni giro, garantendo che la condizione prima o poi diventi falsa?
Diagrammi di flusso
Il diagramma di flusso rende visibile il percorso della computazione. Stessa cosa del pseudocodice, vista da un'altra angolazione: utile quando devi convincerti che tutti i cammini finiscono davvero in «Fine».
La versione eseguibile passo passo di questo algoritmo è nel capitolo 2.
Il livello di dettaglio è una scelta
Lo stesso algoritmo si può disegnare a granularità diverse, a seconda di che cosa l'esecutore sa fare.
Se l'esecutore non ha il confronto diretto ma solo la sottrazione, il blocco X > Y ? si espande in due passi:
D ← X - Y D > 0 ?
È la stessa idea del passaggio dal problema al programma: si decompone un passo complesso in sottopassi semplici finché ogni pezzo è eseguibile.
Il metodo di lavoro
- Scrivi la terna (I, O, R). Se non sai scrivere R, non hai capito il problema.
- Elenca i passi elementari dell'esecutore. Cosa hai diritto di usare.
- Trova l'IDEA. Una frase in italiano che spiega perché funzionerà (es. «conta quante volte riesco a sottrarre»). Questa frase vale metà dell'esercizio.
- Scrivi l'algoritmo in pseudocodice, numerando i passi e indentando le strutture.
- Traccia a mano la computazione su un caso normale e su tutti i casi limite.
- Solo allora codifica in C.
- sequenza vuota (il ciclo gira zero volte);
- sequenza con un solo elemento;
- dati uguali fra loro (M = N);
- il valore cercato è il primo / l'ultimo / assente;
- valori al bordo delle condizioni di validità (0, 1, il massimo).
Verifica rapida
A ← A + 2, la A che compare a destra della freccia indica…mentre (condizione)?Catalogo di algoritmi
Gli algoritmi visti a lezione (allegati 1–6), ciascuno con problema, idea, pseudocodice ed esecuzione passo passo. Non vanno imparati a memoria: vanno capiti gli schemi di ragionamento che contengono.
| Schema | Dove lo vedi |
|---|---|
| Contatore: una variabile conta quante volte è successo qualcosa | divisione, logaritmo, potenza |
| Accumulatore: una variabile accumula un risultato parziale | sommatoria, media |
| Ottimo corrente: una variabile conserva il migliore visto finora | massimo di una sequenza |
| Riduzione: a ogni giro il problema diventa più piccolo ma equivalente | Euclide, ricerca dicotomica |
| Sentinella: una variabile registra «è già successo?» | ricerche |
1 · Divisione intera per sottrazioni ripetute
Esecutore: lettura, scrittura, addizione, sottrazione, confronto fra naturali, memorizzazione. Non possiede la divisione: è proprio quello che dobbiamo costruire.
Produci in uscita il numero di volte che è possibile sottrarre N da un valore, inizialmente pari a M, senza che il risultato scenda sotto 0. Produci anche l'ultimo valore ottenuto: è il resto.
A ogni valutazione della condizione vale M = N·Q + R. All'uscita vale anche R < N: le due cose insieme sono esattamente la definizione di quoziente e resto. Un'invariante che si mantiene + una condizione di uscita = dimostrazione che l'algoritmo è corretto.
Che succede se N > M? E se M = 0?
Prova con M=3, N=5 nel simulatore: la condizione R >= N è subito falsa, il ciclo gira
zero volte e si producono Q=0, R=3. È corretto: 3 diviso 5 fa 0 con resto 3. Con M=0 si ottiene Q=0, R=0.
Con N=0 invece l'algoritmo non terminerebbe mai (sottrarre 0 non riduce nulla): ecco perché «N diverso da 0» sta nelle condizioni di validità dei dati, e non è un dettaglio pedante.
2 · Logaritmo intero in base B
Esecutore: come sopra, ma con la divisione intera fra i passi elementari.
Conta quante volte posso dividere N per B prima che il risultato scenda sotto 1. È la stessa struttura della divisione per sottrazioni ripetute, con la sottrazione sostituita dalla divisione: riconoscere questa parentela è ciò che permette di riusare un ragionamento già fatto.
Nel testo degli appunti il passo 4.1 è scritto R ← R / N: è un errore di trascrizione.
Deve essere R ← R / B — si divide per la base, non per il numero di partenza.
Con R / N il ciclo girerebbe una volta sola e produrrebbe sempre L=1.
3 · Massimo di una sequenza
Leggi in sequenza valori interi positivi, in quantità non specificata, fino al primo valore negativo (che segnala la fine della lettura), e stampa il massimo fra i valori positivi letti. Se non viene letto alcun valore positivo, stampa 0.
Tieni un contenitore MAX che, a ogni iterazione, contiene il massimo fra i numeri letti fino a quel momento. Inizializzarlo a 0 risolve gratis anche il caso «nessun valore positivo».
La lettura compare due volte: una prima del ciclo (per avere un valore su cui valutare la condizione) e una in coda al corpo. È lo schema standard di tutte le letture terminate da un valore sentinella. Se la lettura stesse solo in testa al corpo, il terminatore verrebbe elaborato come un dato.
Varianti tipiche d'esame
- Massimo e minimo insieme: serve una seconda variabile, ma il minimo non può partire da 0 — va inizializzato con il primo valore letto (e allora il caso «sequenza vuota» va trattato a parte).
- Massimo e la sua posizione: aggiungi un contatore di posizione e aggiornalo insieme a MAX.
- Secondo massimo: due variabili che «scalano» quando arriva un valore più grande. Attento all'ordine degli aggiornamenti: è lo stesso problema dello scambio con contenitore temporaneo.
- Sequenza che può contenere valori negativi: l'inizializzazione a 0 diventa sbagliata.
4 · Massimo comun divisore (Euclide)
Sfruttiamo una proprietà aritmetica: se D è divisore sia di M sia di N, allora D divide anche la loro somma M+N e la loro differenza M−N. E quindi divide anche il resto della divisione intera M % N (che è M meno un certo numero di volte N).
Conseguenza: la coppia (M, N) e la coppia (N, M%N) hanno esattamente gli stessi divisori comuni, quindi lo stesso MCD. Ma la seconda coppia è fatta di numeri più piccoli. Ripeto finché il resto diventa 0: a quel punto il numero più piccolo divide l'altro ed è il MCD.
Il resto di una divisione è sempre strettamente minore del divisore e mai negativo. Quindi il secondo numero della coppia decresce strettamente a ogni giro restando ≥ 0: non può farlo all'infinito. Questo è il modo di argomentare la terminazione di un ciclo.
Provalo con casi particolari
- M=54, N=144: lo scambio iniziale rimette i numeri in ordine. Nota che se non ci fosse lo scambio l'algoritmo funzionerebbe lo stesso, perché la prima iterazione lo farebbe «da sola» (54 % 144 = 54).
- M=12, N=12: il ciclo gira zero volte, risultato 12.
- M=13, N=7: numeri primi fra loro, il risultato è 1.
- M=100, N=1: il ciclo gira zero volte, risultato 1.
5 · Ricerca sequenziale
La ricerca «dello schedario»: esamina la prima scheda; se autore e titolo coincidono la ricerca è conclusa con successo, altrimenti passa alla scheda successiva; ripeti fino a conclusione o fino a raggiungere l'ultima scheda.
Su N schede servono fino a N confronti. Soprattutto: non sfrutta l'ordinamento dell'archivio. Se le schede sono ordinate, buttare via quell'informazione è uno spreco — ed è esattamente il tipo di «ciclo non necessario» che all'esame costa punti anche se il programma funziona.
6 · Ricerca dicotomica in una sequenza ordinata
Esecutore (descrizione specifica): accedere all'elemento in una determinata posizione, memorizzare, addizione/sottrazione/divisione intera, confronto.
Guarda l'elemento centrale della porzione attuale. Se è quello cercato, hai finito. Altrimenti, grazie all'ordinamento, sai in quale metà il valore può ancora trovarsi: l'altra metà viene scartata tutta in un colpo solo. Il problema si ripresenta identico, ma su una porzione grande la metà.
Prima di arrivarci: due versioni che NON funzionano
Vale la pena vedere gli errori, perché sono gli stessi che farai tu la prima volta.
Errore 1 — le due metà coprono tutta la porzione
Se la «prima metà» comprende l'elemento centrale (Fine ← Media) invece di escluderlo, la porzione
può smettere di rimpicciolirsi. Prova a cercare 3 nella sequenza 2 5:
Non è «un programma lento» o «un programma che sbaglia risultato»: è una sequenza di passi che non è un algoritmo, perché viola la finitezza. Il rimedio: escludere da entrambe le metà l'elemento centrale appena esaminato, che sappiamo già non essere quello cercato.
Errore 2 — non si gestisce la porzione vuota
Escludendo il centro si può benissimo arrivare a una porzione vuota: è il caso in cui l'elemento è assente. Bisogna accorgersene e terminare esplicitamente con insuccesso, altrimenti non si sa nemmeno quale sia «l'elemento centrale» di una porzione di zero elementi.
La versione corretta
0. Acquisisci la sequenza ordinata e il numero da cercare
1. Inizio ← posizione del primo elemento
2. Fine ← posizione dell'ultimo elemento
3. mentre (Inizio <= Fine) esegui /* porzione attuale non vuota */
3.1 Media ← (Inizio + Fine) / 2 /* divisione intera */
3.2 se (A[Media] == cercato)
3.2.1 produci Media e STOP
altrimenti
3.2.2 se (cercato < A[Media])
3.2.2.1 Fine ← Media - 1 /* prima metà, centro escluso */
altrimenti
3.2.2.2 Inizio ← Media + 1 /* seconda metà, centro escluso */
4. produci -1 /* numero non presente */
5. STOP
Quando l'elemento cercato è assente, la computazione termina con
Inizio > Fine: i due indici si sono scavalcati rispetto al loro ordine naturale.
Provalo cercando 20 nella sequenza di default. (Curiosità utile: alla fine Inizio indica
esattamente la posizione in cui il valore andrebbe inserito per mantenere l'ordinamento.)
Quanto conviene davvero
Ogni confronto dimezza la porzione: il numero di confronti è circa il logaritmo in base 2 di N — lo stesso algoritmo del §2 di questa pagina, con B = 2.
La ricerca dicotomica è corretta solo se la sequenza è ordinata. Se non lo è, l'algoritmo termina comunque (la porzione si dimezza sempre) ma può rispondere «assente» su un valore presente. Un algoritmo senza le sue precondizioni non risolve il problema: risolve un altro problema.
Riepilogo
| Algoritmo | Idea in una riga | Struttura | Terminazione garantita da… |
|---|---|---|---|
| Scambio | serve un appoggio per non distruggere un valore | sequenza | — |
| Divisione | conta le sottrazioni possibili | ciclo + contatore | il resto cala di N a ogni giro |
| Logaritmo | conta le divisioni possibili | ciclo + contatore | il valore viene diviso per B ≥ 2 |
| Massimo | conserva il migliore visto finora | ciclo + selezione | la sequenza è finita (terminatore) |
| Euclide | (M,N) e (N, M%N) hanno lo stesso MCD | ciclo + riduzione | il resto decresce strettamente |
| Ricerca sequenziale | guardale tutte finché trovi | ciclo + condizione doppia | l'indice cresce fino a MAX |
| Ricerca dicotomica | scarta metà archivio a ogni confronto | ciclo + riduzione | la porzione si dimezza e il centro è escluso |
Verifica rapida
Inizio <= Fine.La macchina astratta C
Il nostro esecutore concreto: com'è fatto, che cosa sa fare, e perché ogni contenitore deve dichiarare che tipo di informazione contiene.
Componenti della macchina C
| Componente | Ruolo |
|---|---|
| Unità di controllo | controlla l'esecuzione del programma: decide quale istruzione eseguire e in che ordine |
| Standard Input | unità di ingresso, dalla quale si leggono i dati (tipicamente la tastiera) |
| Standard Output | unità di uscita, sulla quale si scrivono i risultati (tipicamente lo schermo) |
| Memoria centrale | divisa in celle elementari, ciascuna contenente un dato |
Le celle di memoria sono i contenitori introdotti parlando di algoritmi. Le chiamiamo variabili perché il loro contenuto può cambiare durante l'esecuzione del programma. Le istruzioni del C corrispondono ai passi elementari: operazioni aritmetiche, confronti, memorizzazione, acquisizione dati, produzione risultati.
Le idealizzazioni: perché «astratta»
La macchina C che studiamo è una semplificazione di quella reale:
- non c'è nessun limite al numero delle celle (memoria infinita);
- non c'è nessun limite al valore numerico contenuto;
- i contenitori si raggiungono tramite un nome, non tramite un indirizzo numerico;
- un contenitore può occupare una o più celle, a seconda del tipo di dato, senza che ce ne occupiamo.
Su che cosa si può operare
- sui valori contenuti nelle variabili;
- su valori direttamente riconoscibili dal C, cioè scritti nel testo del programma:
- numeri interi relativi:
23,-64 - numeri reali:
5.0,2.34,-43.56 - caratteri:
'h','8',' '(spazio),'.' - stringhe:
"carta","MNZLSS34L03F205T"— sequenze di caratteri in celle adiacenti (cap. 5)
- numeri interi relativi:
E come non si perdono i risultati delle operazioni? Memorizzandoli, anche solo temporaneamente, in contenitori.
Identificatori e parole chiave
Una successione di lettere, cifre e _ con al primo posto una lettera.
| Validi | Non validi |
|---|---|
a x alfa pippo a1 xy_23 Giuseppe DopoDomani |
2abc 4x 7 x-y |
- Le maiuscole sono distinte dalle minuscole:
Var1,var1eVAR1sono tre identificatori diversi. - Per evitare ambiguità non si può usare lo stesso identificatore per elementi diversi, né identificatori diversi per lo stesso elemento.
Alcuni identificatori sono predefiniti (associati a priori a un elemento del linguaggio) e
riservati (non riutilizzabili con altro significato). Per esempio scanf e printf
indicano operazioni di ingresso/uscita e non possono diventare nomi di variabile.
Una parola chiave è una parola predefinita del linguaggio, anch'essa riservata:
int, float, char, double, if, else,
while, do, for, return, const…
Scegli nomi che dicano che cosa contiene il contenitore, non come è fatto:
massimo, contatore, numLetti valgono più di a, b, x1.
Un nome ben scelto è metà del commento che non devi scrivere.
Struttura di un programma C
direttive per il compilatore /* es. #include <stdio.h> */
definizioni di tipo /* … vedremo in seguito … */
int main() /* intestazione del programma principale */
{
dichiarazioni /* quali contenitori userò e di che tipo */
istruzioni /* i passi da eseguire */
return(0); /* finale convenzionale */
}
altri sottoprogrammi /* … vedremo in seguito … */
In C89 tutte le dichiarazioni vanno all'inizio del blocco, prima di qualunque istruzione. Dichiarare una variabile in mezzo al codice (come si fa in C99 o in C++) è un errore nel linguaggio veicolare di questo corso.
Contenitori: perché servono le dichiarazioni
Un contenitore, visto da vicino, è:
| Aspetto | Che cos'è |
|---|---|
| Dispositivo fisico | una o più celle di memoria |
| Indirizzo | la sua posizione in memoria (un numero) |
| Valore | il dato contenuto |
In linguaggio macchina si identifica un contenitore con l'indirizzo (come «via Metastasio 76»); in C con un nome, l'identificatore (come «Palazzo Madama»).
Un contenitore è una sequenza di circuiti bistabili (es. 32), ciascuno con una tensione che può assumere solo due valori stabili: convenzionalmente 0 oppure 1.
Informazioni di natura diversa (naturali, relativi, caratteri, reali, perfino istruzioni) vengono codificate in sequenze di bit. Per ciascun tipo di dato la codifica è univoca, ma tipi diversi hanno codifiche indipendenti.
→ Una stessa sequenza di 0 e 1 può rappresentare informazioni diverse. Guardare i bit non basta per sapere che cosa significano.
Per ciascun contenitore si dichiara il tipo di informazione che vi sarà rappresentata. Così la macchina sa come interpretare la sequenza di bit contenuta, e quante celle riservare. La dichiarazione non è burocrazia: è ciò che dà significato ai bit.
I tipi di dato
| Tipo | Che cosa contiene | Occupazione tipica | Formato I/O |
|---|---|---|---|
char | un carattere | 1 byte | %c |
int | un intero relativo | 1 parola (es. 32 bit) | %d |
unsigned int | un intero naturale (senza segno) | 1 parola | %u |
float | un reale in virgola mobile, singola precisione | 32 bit | %f |
double | un reale in virgola mobile, doppia precisione | 64 bit (2 parole) | %lf |
char carA; /* conterrà valori di tipo char: un carattere */
int n; /* conterrà valori di tipo int: un intero relativo */
float x; /* reale in virgola mobile, singola precisione */
double y; /* reale in virgola mobile, doppia precisione */
Costanti
Un contenitore può essere dichiarato costante: riceve un valore che il programma non potrà modificare.
const float Pi = 3.1415; /* il valore non sarà modificabile dal programma */
Esiste anche la direttiva #define, che non crea un contenitore ma dice al compilatore
di sostituire testualmente un nome con un testo, prima di compilare:
#define MAX 100 /* ovunque compaia MAX, il compilatore scrive 100 */
#define base 2
La dimensione di un array deve essere una costante nota al compilatore
(cap. 5): #define è il modo standard di dargliela. Nota che dopo #define non va il punto e virgola.
Espressioni
La definizione è ricorsiva: un'espressione è
<espressione> := <valore riconoscibile dal C> oppure
<nome di una variabile> oppure
<nome di una costante> oppure
<espressione> <operatore aritmetico> <espressione>
<operatore aritmetico> := + - * / %
Esempi:
| Espressione | Che cos'è |
|---|---|
3.25 | valore reale (float) |
33 | valore intero (int) |
'a' | valore di tipo carattere (char) |
Pi | nome di contenitore costante di tipo float |
x, m, n | nomi di contenitori variabili |
2.0 * Pi + x*x | composizione di espressioni |
(m+3) * (n - (23 * m)) | idem, con parentesi |
La complessità è in teoria illimitata; per le parentesi valgono le stesse regole della matematica.
Dove si trovano le espressioni
- Nelle istruzioni di assegnamento:
n = (m+3)*(n - 23*m); - Nelle istruzioni di uscita:
printf("meta' di n: %d", n/2); - Nelle condizioni (qui sotto).
Nella forma <nome di contenitore> = <espressione>; il contenitore a sinistra
riceve il valore dell'espressione a destra. L'espressione a destra va valutata: quando
vi si incontrano nomi di contenitori se ne usa direttamente il valore.
Esempio: se m vale 3 e n vale 4, durante
n = (m+3)*(n-23*n); si calcola (3+3)*(4-23·4) = 6·(4-92) = 6·(-88) = -528
e si scrive −528 nel contenitore n, al posto del precedente 4.
Condizioni
<valore logico> := false, true
<operatore di confronto> := < <= > >= == !=
<operatore logico> := && || !
<condizione elementare> := <valore logico> oppure
<espressione> <operatore di confronto> <espressione>
<condizione> := <condizione elementare> oppure
<condizione> <operatore logico> <condizione>
0è interpretato come FALSO;1è interpretato come VERO… ma non solo: qualunque valore diverso da 0 è vero, anche-2.
= contro ==
= è l'assegnamento, == è il confronto.
if (x = 0) assegna 0 a x e poi valuta la condizione «0», cioè sempre falsa:
il compilatore non protesta e il programma sbaglia in silenzio.
Operatori logici e tabelle di verità
!negazione (non): data una condizione C,!Cha valore di verità opposto;&&congiunzione (e):C1&&C2è vera solo quando entrambe sono vere;||disgiunzione (o):C1||C2è falsa solo quando entrambe sono false.
| C1 | C2 | C1 && C2 | C1 || C2 | !C1 |
|---|---|---|---|---|
| V | V | V | V | F |
| V | F | F | V | F |
| F | V | F | V | V |
| F | F | F | F | V |
Esempi di lettura:
!(x==4)→ x NON è uguale a 4(x<10)&&(y>=100)→ x minore di 10 e y maggiore o uguale a 100(y==1)||(z!=2)→ y è uguale a 1 oppure z è diverso da 2(n <= -7) || (n >= 7)→ il valore assoluto di n è almeno 7
Precedenze… e come non pensarci
Non è necessario conoscere le precedenze a memoria: usa le parentesi per eliminare i dubbi
(e per modificare le precedenze quando serve): !( (C4||C5) && C6 ).
Non usare condizioni complesse. Il numero di casi da analizzare esplode
con il numero di clausole. Catene omogenee (C1 || C2 || C3, tutte dello stesso significato) vanno bene;
configurazioni miste e annidate no.
La regola del risparmio (valutazione in corto circuito)
- se valuta un OR e trova la prima clausola VERA, non valuta le successive → condizione VERA;
- se valuta un AND e trova la prima clausola FALSA, non valuta le successive → condizione FALSA.
Non è una curiosità: è ciò che rende corretto (e non solo veloce) uno schema che userai continuamente:
while (i < MAX && Sequenza[i] != cercato)
i = i + 1;
/* se i < MAX è falsa, Sequenza[i] NON viene valutato:
non si accede a un elemento fuori dall'array */
Invertire le due clausole romperebbe il programma. L'ordine delle clausole, qui, è parte del ragionamento.
Tipi, espressioni e conversioni
Di norma un operatore deve essere applicato a dati dello stesso tipo, e in un assegnamento l'espressione a destra deve avere lo stesso tipo del contenitore a sinistra. Il C però ammette deroghe, e le gestisce con due regole precise.
Regola 1 — nelle espressioni: vince il tipo più ricco
Se in un'espressione compare un'operazione fra dati di tipo diverso, il C usa come operandi valori del tipo più ricco.
int n = 4;
float x = 2.13;
int y;
y = n * x;
Nell'espressione n * x si usa, invece dell'intero 4, il float 4.0 (stesso valore matematico):
l'operazione diventa 4.0 * 2.13 e il risultato è il float 8.52.
Questo non cambia in alcun modo il valore della variabile n,
che resta intera e continua a valere 4. La promozione riguarda il valore usato nel calcolo, non il contenitore.
Regola 2 — nell'assegnamento: vince il tipo del contenitore
Se il tipo della variabile e il tipo del risultato dell'espressione sono diversi, il C assegna un valore dello stesso tipo della variabile. Tale valore può:
- avere lo stesso valore matematico del risultato — es.
x = n;assegna 4.0 a un float; - essere un arrotondamento (troncamento) — es.
y = n * x;con y intero assegna(int)8.52, cioè 8.
Casting esplicito
Quando la conversione automatica non scatta ma ci serve, la si chiede esplicitamente con
(tipo)espressione. Caso tipico: la media di due interi.
int somma = 7, cont = 2;
float media;
media = somma / cont; /* 3.0 — divisione INTERA: 7/2 = 3, poi convertito */
media = (float)somma / (float)cont; /* 3.5 — divisione reale: CORRETTO */
In media = somma / cont; la divisione viene eseguita prima
dell'assegnamento, fra due int: il C non ha motivo di attivare la conversione, quindi calcola la
divisione intera e butta via la parte frazionaria. Dichiarare media come float non basta:
bisogna convertire gli operandi.
Provalo
char e int
- Gli operatori aritmetici possono essere applicati a valori
char, trattandoli come interi (si usa il loro codice ASCII). - Si possono eseguire operazioni fra
intechar, ma spesso non hanno alcun significato e derivano da errori. - Eccezione utile: la differenza fra la versione maiuscola e minuscola della stessa lettera
(es.
'R'e'r') è la stessa per tutte le lettere alfabetiche, e si può usare per convertire fra minuscolo e maiuscolo.
Verifica rapida
int a = 7, b = 2; float r; r = a / b; quanto vale r?(float)a / b.while (i < MAX && A[i] != x), cosa garantisce che non si acceda fuori dall'array?int o printf sono scritti correttamente ma non sono utilizzabili come nomi di variabile.Istruzioni e programmi in C
Le istruzioni del linguaggio, una per una, e i programmi completi visti a lezione: la codifica degli algoritmi del capitolo 2.
Assegnamento
<nome di variabile> = <espressione>;
n = 18;
A = (A * 15.3) - B / (f + 32.1);
cont++; /* equivale a cont = cont + 1; */
È la traduzione in C del passo A ← … del capitolo 1: prima si valuta tutta l'espressione
a destra, poi si scrive il risultato nel contenitore a sinistra.
Ingresso e uscita
scanf — lettura
scanf("%d", &n); /* legge da tastiera un valore e lo memorizza come intero in n */
scanf deve scrivere dentro il contenitore, quindi ha bisogno
di sapere quale contenitore: si passa &n, «l'indirizzo di n». Dimenticare la &
è l'errore numero uno del laboratorio: il programma compila e poi si comporta in modo imprevedibile.
(Unica eccezione che vedremo: le stringhe, cap. 5.)
printf — scrittura
printf(" n vale %d e \n la parola ha \n %d caratteri", n, n + 3);
Gli argomenti dopo la stringa di formato sono espressioni: possono essere calcolate sul posto,
come n + 3 o n/2.
| Segnaposto | Tipo | Note |
|---|---|---|
%d | int | intero in base 10 |
%f | float | %.2f = due cifre decimali |
%lf | double | obbligatorio in scanf per i double |
%c | char | un singolo carattere |
%s | stringa | array di char terminato da '\0' (cap. 5) |
\n \t | — | a capo, tabulazione |
Provalo: formati e argomenti
I segnaposto e gli argomenti si accoppiano in ordine, e i tipi devono
corrispondere. Il compilatore C89 non se ne accorge: se scrivi %d e passi un float, stampi spazzatura.
Istruzione condizionale (selezione)
if ( <condizione> ) {
<istruzioni da eseguire se la condizione è vera>
}
else {
<istruzioni da eseguire se la condizione è falsa>
}
Il ramo else è facoltativo.
Esempio: valore assoluto di un numero reale
#include <stdio.h>
int main(){
float x;
scanf("%f", &x);
if (x < 0.0)
x = -x;
printf("\n valore assoluto: %f", x);
return 0;
}
Qui il ramo «altrimenti» è vuoto: se il numero è già positivo non c'è niente da fare.
Esempio: massimo e minimo di due numeri
#include <stdio.h>
int main(){
int m, n;
int max, min;
scanf("%d", &m);
printf("\n");
scanf("%d", &n);
if (m > n) {
max = m;
min = n;
}
else {
max = n;
min = m;
}
printf("\n massimo: %d, minimo: %d", max, min);
return 0;
}
In if(){}, else{}, while(){}, do{},
for(){} si possono rimuovere le graffe se il blocco contiene una sola istruzione.
Consiglio: mettile comunque. Il giorno che aggiungi la seconda istruzione e dimentichi le graffe,
l'errore è invisibile a occhio nudo.
Iterazione: while
while ( <condizione> ) {
<istruzioni>
}
La condizione è valutata prima di ogni esecuzione del corpo: il corpo può essere eseguito zero volte.
Esempio: calcolo di potenze intere
#include <stdio.h>
int main(){
int base;
int esponente;
int potenza;
int contatore;
scanf("%d", &base);
scanf("%d", &esponente);
potenza = 1;
contatore = 0;
while (contatore < esponente) {
potenza = potenza * base;
contatore = contatore + 1;
}
printf("\n %d alla %d: %d", base, esponente, potenza);
return 0;
}
Modifica: la massima potenza che non supera una soglia
Calcolare la massima potenza di una base che non superi una data soglia. Se la base è 10 e la soglia 93017, la risposta è 10000.
Parto dal programma di elevamento a potenza e cambio solo la condizione di permanenza
nel ciclo: da contatore < esponente a potenza*base <= soglia.
Sparisce il contatore, perché non serve più contare: serve solo sapere se posso fare un'altra moltiplicazione.
#include <stdio.h>
int main(){
int base;
int soglia;
int potenza;
scanf("%d", &base);
scanf("%d", &soglia);
potenza = 1;
while (potenza*base <= soglia)
potenza = potenza * base;
printf("\n massima potenza di %d <= %d: %d", base, soglia, potenza);
return 0;
}
Si verifica potenza*base, cioè il valore che si otterrebbe,
non potenza. È la differenza fra «controllo prima di fare il passo» e «faccio il passo e poi mi pento»:
la prima è quasi sempre la strada giusta.
Esempio: logaritmo intero
Per quante volte posso dividere un numero per un valore senza che il risultato scenda sotto 1? (È l'algoritmo del cap. 2 §2.)
#include <stdio.h>
int main(){
int base;
int numero, numcorrente;
int logaritmo;
printf("\n digita una base intera > 1 \n");
scanf("%d", &base);
printf("\n digita il numero positivo di cui calcolare il logaritmo \n");
scanf("%d", &numero);
numcorrente = numero;
logaritmo = 0;
while (numcorrente >= base) {
numcorrente = numcorrente / base;
logaritmo = logaritmo + 1;
}
printf("\n log di %d in base %d: %d", numero, base, logaritmo);
return 0;
}
- Il dato letto viene copiato in
numcorrente: così il valore originale resta disponibile per la stampa finale. Non distruggere i dati in ingresso se ti servono ancora. - Si può far evolvere il contatore direttamente nella variabile
logaritmo, invece di usare uncontatoree copiarlo alla fine: una variabile in meno, stesso risultato.
Iterazione: do-while
do {
<istruzioni>
}
while ( <condizione> );
Qui la condizione è valutata dopo il corpo: il corpo viene eseguito almeno una volta.
Esempio: lettura di un dato con criterio di validità
Continua a chiedere il dato finché la condizione di validità non è soddisfatta.
#include <stdio.h>
int main(){
int dato;
do {
printf("\n digita un intero positivo < 10");
scanf("%d", &dato);
}
while (dato < 0 || dato >= 10);
/* qui dato è sicuramente valido */
return 0;
}
| Ciclo | Usalo quando… |
|---|---|
while | il corpo potrebbe non dover essere eseguito nemmeno una volta (il caso generale) |
do-while | il corpo va certamente eseguito almeno una volta (tipico: la lettura con controllo di validità) |
for | solo cicli a conteggio con condizione elementare |
Iterazione: for
for ( istruzione1 ; condizione ; istruzione2 ) {
<istruzioni>
}
- istruzione1: eseguita una sola volta, prima di iniziare il ciclo;
- condizione: se vera si reitera, se falsa si esce;
- istruzione2: eseguita al termine di ciascuna iterazione, subito prima di rivalutare la condizione.
for (cont = 0; cont < esponente; cont++)
potenza = potenza * base;
equivale esattamente a
cont = 0;
while (cont < esponente) {
potenza = potenza * base;
cont++;
}
Ogni for ha un while equivalente, ma non ogni while
merita di diventare un for. Il for va usato solo in cicli a conteggio con condizioni
elementari: se la condizione è composta o l'avanzamento non è regolare, il while è più chiaro
e all'esame vale di più.
Programmi completi
Sommatoria di interi letti (termina al primo negativo)
#include <stdio.h>
int main(){
int nletto;
int somma = 0; /* assegnamento in fase di dichiarazione */
scanf("%d", &nletto); /* prima lettura, fuori dal ciclo */
while (nletto >= 0) {
somma = somma + nletto; /* accumulatore */
scanf("%d", &nletto); /* lettura in coda al corpo */
}
printf("Sommatoria = %d", somma);
return(0);
}
Stesso schema «leggi–elabora–rileggi» del massimo di una sequenza: cambia solo che cosa si fa con il valore letto.
Media di interi letti — con casting esplicito
Parto dal programma della sommatoria e, mentre aggiorno somma,
conto i numeri con un contatore inizializzato a 0. Alla fine divido. Ma somma e
cont sono entrambi int: la conversione automatica non si attiva e otterrei una divisione
intera. Serve il casting esplicito.
#include <stdio.h>
int main(){
float media;
int nletto;
int somma, cont;
somma = 0;
cont = 0;
scanf("%d", &nletto);
while (nletto >= 0) {
somma = somma + nletto;
cont++; /* equivale a cont = cont + 1; */
scanf("%d", &nletto);
}
media = (float)somma / (float)cont;
printf("\n Media = %f", media);
return(0);
}
Se il primo valore letto è negativo, cont vale 0 e la divisione è per zero.
Un programma completo deve trattare a parte il caso «nessun dato»: è esattamente il tipo di controllo che all'esame
distingue un programma ragionato da uno scritto a memoria.
MCD, algoritmo di Euclide
#include <stdio.h>
int main(){
int m, n;
int resto;
int temp;
scanf("%d%d", &m, &n);
if (n > m) { /* scambio: metto il maggiore in m */
temp = m;
m = n;
n = temp;
}
while (m % n != 0) {
resto = m % n;
m = n;
n = resto;
}
printf("Massimo comune divisore = %d", n);
return(0);
}
Lo scambio iniziale è, riga per riga, l'algoritmo del cap. 1. La simulazione passo passo è nel cap. 2 §4.
Cifre binarie di un numero frazionario — casting automatico
Stampare le prime 8 cifre in base 2 di un numero frazionario x, con 0 < x < 1.
#include <stdio.h>
#define base 2
#define Max 8
int main(){
float x;
int cifra;
int cont;
printf("\n digita un valore reale tra 0 e 1 \n"); /* segnala all'utente l'attesa */
scanf("%f", &x);
cont = 0;
while (x > 0 && cont < Max) {
cifra = x * base; /* l'espressione vale x*2.0: il valore viene
troncato all'intero e assegnato a cifra */
printf("%d", cifra);
x = x * base - cifra; /* qui il risultato resta float: nessun troncamento */
cont++;
}
return(0);
}
La stessa espressione x * base viene troncata quando finisce
in un int e conservata intatta quando finisce in un float. Non è il calcolo a cambiare:
è il contenitore di destinazione a decidere. E la doppia condizione x > 0 && cont < Max
copre entrambi i motivi per fermarsi: cifre esaurite oppure limite raggiunto.
Conversione in maiuscolo — codici ASCII
Leggi caratteri alfabetici fino a un terminatore (un carattere non alfabetico) e converti ciascun carattere minuscolo in maiuscolo, lasciando inalterati i maiuscoli.
#include <stdio.h>
int main(){
char cletto;
char convertito;
scanf("%c%*c", &cletto);
while ((cletto >= 'a' && cletto <= 'z') ||
(cletto >= 'A' && cletto <= 'Z')) {
if (cletto >= 'a' && cletto <= 'z')
convertito = cletto + 'A' - 'a';
else
convertito = cletto;
printf("%c", convertito);
scanf("%c%*c", &cletto);
}
return(0);
}
La differenza 'A' - 'a' è una costante uguale per tutte le lettere:
sommarla a una minuscola dà la corrispondente maiuscola. Il %*c in scanf legge e
scarta un carattere (serve a consumare l'invio). Sperimenta con il widget del
cap. 3.
Linee guida di stile
- Niente cicli non necessari. Se un'informazione si può calcolare durante un ciclo che stai già facendo, non farne un secondo.
- Usa la struttura dati indicata dal testo, anche se ne conosci una migliore.
- Indentazione coerente: un livello per ogni struttura annidata.
- Commenti che spiegano il perché, non il cosa.
i++; /* incrementa i */è rumore;/* invariante: MAX contiene il massimo dei valori letti finora */è oro. - Inizializza sempre prima di usare: una variabile non inizializzata contiene spazzatura.
- Un contenitore, un ruolo. Non riciclare la stessa variabile per due scopi diversi.
Verifica rapida
while e do-while?do-while è la scelta naturale per la lettura con controllo di validità:
il dato va chiesto almeno una volta.scanf compare due volte?scanf("%d", n); — che cosa c'è che non va?for?while rende il ragionamento più
leggibile — ed è il ragionamento a essere valutato.Array e stringhe
Il primo aggregato di dati: molti contenitori dello stesso tipo, un nome solo, e un indice per scegliere quale.
Che cos'è un array
Un array è un aggregato di dati dello stesso tipo, memorizzati in contenitori consecutivi, identificati da un nome comune e da un indice intero che varia da contenitore a contenitore.
float SequenzaFloat[11]; /* ha undici (11) elementi */
char parola[30]; /* ha trenta (30) elementi */
int elenco[100]; /* ha cento (100) elementi */
I singoli elementi sono contenitori a tutti gli effetti:
parola[0] /* il PRIMO carattere nell'array parola */
SequenzaFloat[6] /* il SETTIMO float nell'array SequenzaFloat */
elenco[99] /* l'ULTIMO intero in elenco */
Un array dichiarato con dimensione N ha indici validi da 0 a N−1.
A[N] non esiste: è la cella successiva all'array, e scriverci sopra danneggia altri dati
senza che il C protesti. Metà degli errori con gli array vengono da qui.
Assegnare un valore a un elemento
La forma generale dell'assegnamento si estende così:
<nome di variabile> = <espressione>; <indicazione di contenitore> = <espressione>;
parola[0] = 'd';
SequenzaFloat[6] = 3.14 * 2.0;
SequenzaFloat[8] = SequenzaFloat[6] * 4.0;
Inizializzazione contestuale alla dichiarazione
int A[10] = {10, 7, 5, 5, 1, 7, 7, 8, 6, 9};
int trovato = 0;
1 · Non si copia un array con l'assegnamento.
int A[10];
int B[10];
A = B; /* NON SI PUO' FARE !!! */
for (i = 0; i < 10; i++) /* occorre copiare elemento per elemento */
A[i] = B[i];
2 · La dimensione di un array non può essere una variabile.
Deve essere un valore intero costante, o un parametro definito con #define:
#define MAX 100
int main(){
int A[50];
int B[MAX/2 + 1]; /* ok: il compilatore sa calcolarlo */
/* int C[n]; NO: n è una variabile */
}
Per dimensioni davvero variabili servono altre strutture (liste dinamiche, array dinamici): le vedremo più avanti nel corso.
Scorrere un array
#include <stdio.h>
#define MAX 10 /* direttiva: sostituisce MAX con 10 */
int main(){
int Sequenza[MAX];
int i;
for (i = 0; i < MAX; i++) /* lettura: dall'inizio alla fine */
scanf("%d", &Sequenza[i]);
for (i = MAX-1; i >= 0; i--) /* stampa: dalla fine all'inizio */
printf(" %d ", Sequenza[i]);
return 0;
}
La stampa in ordine inverso si può ottenere anche scorrendo in avanti e «riflettendo» l'indice:
for (i = 0; i < MAX; i++)
printf(" %d ", Sequenza[MAX-1-i]);
Quando i vale 0 si legge l'ultimo elemento, quando vale MAX−1 si legge il primo: stesso risultato,
schema di conteggio diverso. Verifica sempre i due estremi quando scrivi un'espressione di indice.
Ricerca in un array
Elenco non ordinato — con variabile sentinella
#include <stdio.h>
#define MAX 100
int main(){
int i;
int trovato = 0; /* sentinella: "l'ho già trovato?" */
float Sequenza[MAX];
float cercato;
/* … qui il popolamento dell'array … */
scanf("%f", &cercato);
i = 0;
while (i < MAX && trovato == 0) {
if (Sequenza[i] == cercato)
trovato = 1;
else
i = i + 1; /* avanzo solo se NON ho trovato */
}
if (trovato == 1)
printf("numero %f trovato in posizione %d", cercato, i);
else
printf("numero %f non presente", cercato);
return 0;
}
else
Se incrementassi i in ogni caso, all'uscita i indicherebbe
la posizione successiva a quella dell'elemento trovato, e la stampa sarebbe sbagliata.
La variabile che contiene il risultato deve restare ferma nel momento in cui il risultato è stato trovato.
Versione alternativa — senza sentinella
#include <stdio.h>
#define MAX 10
int main(){
int i;
float Sequenza[MAX] = {12, 5, 6, 1, 7, 9, 3, 11, 14, 8};
float cercato;
scanf("%f", &cercato);
i = 0;
while (i < MAX && Sequenza[i] != cercato)
i = i + 1;
if (i < MAX)
printf("numero %f trovato in posizione %d", cercato, i);
else
printf("numero %f non presente", cercato);
return 0;
}
- Sulla condizione: la regola del risparmio evita di accedere a elementi fuori
dall'array — se
i < MAXè falsa,Sequenza[i]non viene nemmeno guardato. - Sull'uscita dal ciclo: si esce per due motivi diversi, e bisogna capire quale.
Se all'uscita vale ancora
i < MAX, allora sono uscito perché è diventata falsaSequenza[i] != cercato, cioè perchéSequenza[i] == cercato: ho trovato l'elemento in posizionei.
Elenco ordinato — ricerca dicotomica
#include <stdio.h>
#define MAX 100
int main(){
int min, max, med;
int trovato = 0;
float Sequenza[MAX];
float cercato;
/* … qui il popolamento dell'array … */
min = 0;
max = MAX - 1;
scanf("%f", &cercato);
while (min <= max && trovato == 0) {
med = (min + max)/2;
if (Sequenza[med] == cercato) trovato = 1;
else if (cercato < Sequenza[med]) max = med - 1;
else min = med + 1;
}
if (trovato == 1)
printf("numero %f trovato in posizione %d", cercato, med);
else
printf("numero %f non presente", cercato);
return(0);
}
È la codifica dell'algoritmo del cap. 2 §6; qui l'uscita
dal ciclo è resa esplicita dalla variabile sentinella trovato, invece che da uno STOP
in mezzo al ciclo. Ripasso interattivo:
Rappresentazione di un numero in una base
Rappresentare un numero naturale in base B usando un array di cifre.
Il resto della divisione per la base dà la cifra meno significativa; la divisione intera per la base «toglie» quella cifra. Le cifre escono quindi in ordine inverso: per questo l'array viene riempito dal fondo, e poi stampato dall'inizio.
#include <stdio.h>
#define base 2
#define MAX 32
int main(){
int n, i;
int SequenzaCifre[MAX]; /* array di MAX cifre */
scanf("%d", &n);
if (n == 0) printf("%d", 0);
else {
i = MAX - 1; /* ultima posizione */
while (n > 0 && i >= 0) {
SequenzaCifre[i] = n % base;
n = n / base;
i = i - 1; /* retrocedo di una posizione */
}
if (n > 0) printf("valore eccede i limiti");
else {
i = i + 1;
while (i < MAX) {
printf("%d", SequenzaCifre[i]);
i = i + 1; /* avanzo di una posizione */
}
}
}
return(0);
}
n == 0: il ciclo girerebbe zero volte e non stamperebbe nulla → si stampa «0» esplicitamente;n > 0all'uscita: l'array è finito prima delle cifre → il valore eccede i limiti;i = i + 1dopo il ciclo:iera già retrocesso di una posizione di troppo, bisogna tornare sulla prima cifra scritta.
Massimi locali: quando modificare i dati rovina il confronto
Azzerare gli elementi di un array che non sono massimi locali (un elemento è massimo locale se è strettamente maggiore dei suoi vicini immediati).
Esaminando gli elementi da sinistra a destra, quando arrivo ad A[i+1]
devo confrontarlo con A[i]… ma A[i] potrebbe essere già stato azzerato.
Il confronto userebbe un valore falso. È lo stesso problema dello scambio del cap. 1:
prima di distruggere un valore che serve ancora, va salvato.
Una variabile ausiliaria prec conserva il valore originario
dell'elemento precedente. Va aggiornata qualunque sia l'esito del confronto (sia che l'elemento venga
azzerato, sia che resti invariato). Il primo e l'ultimo elemento hanno un solo vicino e vanno trattati a parte.
#define N 10
int main(){
int A[N] = {10,7,5,5,1,7,7,8,6,9};
int prec, i;
/* primo elemento: unico vicino è A[1] */
prec = A[0];
if (A[0] <= A[1]) A[0] = 0;
/* elementi intermedi */
for (i = 1; i < N-1; i++)
if (A[i] <= prec || A[i] <= A[i+1]) {
prec = A[i]; /* salvo PRIMA di azzerare */
A[i] = 0;
}
else prec = A[i];
/* ultimo elemento: unico vicino è prec */
if (A[N-1] <= prec) A[N-1] = 0;
for (i = 0; i < N; i++)
printf("\n A[%d] = %d", i, A[i]);
return 0;
}
L'alternativa
Invece di salvare il valore precedente si può salvare direttamente
il risultato del confronto fra A[i] e A[i+1], per riutilizzarlo
durante l'esame di A[i+1]. È una variabile in meno da aggiornare, ma richiede di ragionare
su un booleano invece che su un valore: scegli quella che tu riesci a giustificare.
Stringhe
Una stringa è un array di caratteri con un terminatore,
posto subito dopo l'ultimo carattere utile. Come terminatore si usa il carattere nullo '\0'
(il suo codice ASCII è 0).
Spesso ci sono elementi dell'array non utilizzati. Per un cognome di 7 caratteri, «Zanardi»,
in un array Arr di 20 elementi:
'\0'; i restanti non vengono assegnati e contengono spazzatura.- La lunghezza di una stringa non è la dimensione dell'array: va cercata, scorrendo fino al terminatore.
- L'array deve avere almeno un elemento in più del numero di caratteri utili, per il
'\0'. - Con
scanf("%s", Parola)non si mette la&: il nome dell'array è già l'indicazione della zona di memoria.
Inversione di una stringa
#include <stdio.h>
#define MAX 30
int main(){
int i;
char Parola[MAX];
scanf("%s", Parola);
i = 0;
while (i < MAX-1 && Parola[i] != '\0') /* cerco il terminatore */
i = i + 1;
if (Parola[i] == '\0')
i = i - 1; /* ultimo carattere utile */
while (i >= 0) {
printf("%c", Parola[i]);
i = i - 1;
}
return(0);
}
Le due fasi sono nettamente distinte: prima si trova dove finisce la stringa, poi si scorre all'indietro. Esercizio suggerito a lezione: seguire l'evoluzione della memoria durante l'esecuzione.
Stringhe palindrome
Due indici, uno che parte dall'inizio e uno dalla fine, che si avvicinano confrontando i caratteri. Si esce o perché i caratteri differiscono (non palindroma) o perché gli indici si incontrano (palindroma). Di nuovo: bisogna saper distinguere perché si è usciti dal ciclo.
int main(){
char Str[30];
int i, j;
scanf("%s", Str);
j = 0;
while (j < 30 && Str[j] != '\0') j++; /* cerco il terminatore */
j--; /* ultimo carattere utile */
i = 0;
while (i < j && Str[i] == Str[j]) {
j--;
i++;
}
if (i >= j) printf("palindroma");
else printf("non palindroma");
return 0;
}
Perché la condizione finale è i >= j e non i == j
Con un numero dispari di caratteri i due indici finiscono sullo stesso
carattere centrale (i == j); con un numero pari si scavalcano (i > j).
Coprire entrambi i casi con >= evita di trattarli separatamente. Provalo nel simulatore con
anna (pari) e onorono (dispari).
Verifica rapida
int A[10]; — quali indici sono validi?A[10] è fuori dall'array e il C
non lo segnala.A = B; fra due array non funziona?A == B non confronta i contenuti.prec?char S[30]?Prossimi argomenti
Dal programma del corso, non ancora coperti da questi appunti: la codifica binaria dell'informazione, i sottoprogrammi, la memorizzazione persistente mediante file, l'allocazione dinamica della memoria e la ricorsione come tecnica di risoluzione dei problemi. Man mano che le lezioni procedono, si aggiungono qui.