Sei sulla pagina 1di 15

Fondamenti di Informatica

Anno Accademico 2022/2023 – I semestre

13/9/2022
Introduzione agli algoritmi

L’informatica è una scienza. Si definisce come la conoscenza sistematica di metodi e


tecniche utili a rappresentare ed elaborare informazioni (intese come oggetti ->
collezioni strutturate di dati che rappresentano il mondo reale, o la parte di esso che ci
interessa).
Tre parole chiave:
1. Informazione
2. Rappresentazione
3. Elaborazione -> il fine è quello di estrarre ulteriori informazioni, attraverso un
processo automatizzato (tramite un calcolatore elettronico) -> il calcolatore si pone
tra input ed output; è solo un esecutore, dotato della velocità computatoria, della
vasta memoria e di una facile riprogrammabilità, ma non dotato di autocoscienza,
inferenza e relegato al mero compito di eseguire delle istruzioni, assegnate
dall’uomo

L’algoritmo è una sequenza finita di passi elementari non ambigui e


comprensibili da uno specifico esecutore, la cui esecuzione consente di
risolvere un dato problema. Esso è quindi un modo per insegnare ad un calcolatore
come svolgere un compito, in maniera deterministica e senza ambiguità.

La struttura di un calcolatore (architettura di Von Neumann) è ancora valida tutt’oggi,


ed è così articolata:

 La CPU è l’unità centrale, che esegue le istruzioni (somme, sottrazioni ecc) e


conosce l’istruzione successiva
 L’input e l’output interagiscono attraverso le periferiche, rispettivamente tastiera e
terminale
 La memoria è organizzata come una tabella a celle, dove in ogni cella vi è un
elemento base. La variabile identifica una riga della memoria e viene utilizzata per
memorizzare i valori intermedi (mantiene tali valori finchè non viene riscritta). Una
istruzione impartita al calcolatore scrive un valore nella variabile

Istruzioni di base da impartire al calcolatore:

-> punto di ingresso dell’algoritmo

-> punto di fine dell’algoritmo, ogni passo è stato svolto


-> la CPU si blocca, nell’attesa di un input -> l’informazione inserita viene
scritta nella riga x

-> legge dalla variabile x il valore e lo scrive sullo schermo

-> legge i contenuti delle variabili y,z; li somma e scrive il risultato nella
variabile x (l’uguale va qui inteso come un assegnamento)

Abbiamo inoltre affrontato due esempi in classe:

Nel primo esempio abbiamo dovuto calcolare area e circonferenza di un cerchio dato il
raggio. Si può schematizzare nel seguente modo (tenere presente che π non è una variabile,
ma una costante, quindi si inserisce direttamente nei calcoli):
Inizio -> leggi (r) -> a=3.14*r*r -> c=3.14*2*r -> scrivi (a,c) -> fine

Nel secondo esempio abbiamo dovuto calcolare come restituire una quantita di soldi (r),
avendo a disposizione solamente banconote da 5€ (b5) e monete da 1€ (m1). Per far ciò
abbiamo introdotto la divisione intera ed il modulo (o resto della divisione intera). Si può
schematizzare nel seguente modo:
Inizio -> leggi (r) -> b5=r/5 (divisione intera); m1=r%5 (resto intero della divisione intera)
-> scrivi (b5, m1) -> fine

15/9/2022
Algoritmi

L’algoritmo è, per definizione, una sequenza di istruzioni. La sua peculiarità è che si tratta
di un processo generale, quindi l’utente può variare a sua discrezione i dati inseriti.

Se, per esempio, volessimo creare un algoritmo che chiede un numero e ne calcola il valore
assoluto, dovremmo introdurre un nuovo strumento, ovvero il blocco condizionale di
seguito rappresentato:

Il blocco condizionale, nel caso citato, distingue i due casi nei quali n ha segno positivo o
negativo.
Se volessimo progettare l’algoritmo sopra descritto, potremmo farlo nel seguente modo:

È importante anche specificare la differenza tra il flusso di controllo, ovvero l’insieme di


tutti i possibili passaggi, e il flusso di esecuzione, che comprende solo il percorso
intrapreso.

In alternativa, posso progettare lo stesso algoritmo in modo diverso. Nel caso che segue
viene usata una sola variabile n, che quindi sarà necessariamente sovrascritta.

Passiamo ora al caso di un algoritmo che chiede tre numeri naturali e verifica che siano
una terna pitagorica:
Il seguente è invece un algoritmo che chiede un numero intero, verifica se è positivo,
negativo o nullo, e stampa un messaggio:

Introduciamo ora un nuovo concetto: quello di iterazione, o ciclo. Attraverso tale concetto,
noi abbiamo la possibilità di far tornare la freccia verso l’inizio (non necessariamente
proprio in esso). Quando la condizione è vera viene detta di permanenza nel ciclo (perché
ci costringe a iterare, e quindi a cambiare la variabile di controllo).

Tale concetto si può vedere all’opera nel seguente algoritmo che chiede all’utente un valore
positivo, se non lo riceve lo chiede nuovamente, e poi stampa il risultato:

In questo caso il valore può essere chiesto da 1 a + infinite volte, in base al suo segno.

In questo caso, leggermente diverso, l’algoritmo, in caso non riceva un numero positivo,
stampa un messaggio di errore:
Il teorema di Jacopini-Bohm afferma che qualunque algoritmo può essere progettato
solamente con tre strutture: sequenza, selezione e iterazione.

Progettiamo dunque un algoritmo che comprenda tutte e tre le strutture citate. Esso riceve
un numero intero positivo o nullo e ne calcola il fattoriale:

Passiamo ora ad un altro caso. L’algoritmo riceve due numeri naturali e ne calcola e
visualizza il massimo comun divisore:

In questo caso bisogna aprire una riflessione sugli operatori logici utilizzati. Le scritture di
seguito proposte hanno infatti la stessa valenza, sebbene solo una delle due sia stata
utilizzata nell’algoritmo:

Per verificare ciò, possiamo costruire la tabella delle verità. Prima dobbiamo riscrivere la
seconda (poiché contiene !=, che non ci piace), e lo facciamo scrivendo:
not (a%MCD=0) or not (b%MCD=0)
Ora possiamo scrivere la nostra tabella:
Come già ipotizzato, il risultato è equivalente. Questo è possibile grazie al teorema di De
Morgan, che ci dice:
not (a) and not (b) == not (a or b)

16/9/2022
Rappresentazione dell’informazione

L’obiettivo è capire come sono codificati i numeri (interi, con virgola) nel calcolatore.

Nella storia abbiamo assistito innanzitutto alla rappresentazione unaria, poi a quella
romana (di tipo additivo) e infine a quella araba (di tipo posizionale). In quest’ultima,
l’alfabeto è infatti composto da 10 simboli.

Il computer, invece, usa i transistor, che impiegano un alfabeto di due simboli (binario
naturale), quindi operano in base 2 (da cui il nome rappresentazione binaria).

Nel numero 01011 ad ogni posizione associo una potenza del 2, partendo da destra con 2 0.
Per convertirlo in base 10 dovrò quindi fare 0*24+1*23+0*22+1*2+1*1=11.
È interessante notare come gli zeri posti davanti al numero non aggiungano informazione,
quindi se ne potrebbero mettere infiniti.

Un bit è quindi una cifra binaria. Se la riga della memoria è lunga n bit posso codificare
numeri in n bit. La rappresentazione è finita, posso immagazzinare 2n numeri diversi in n
bit (da 0 a 2n -1).

Come si passa agevolmente da base 10 a base 2? Col metodo dei resti:

Il risultato ha quindi bisogno di 4 bit per essere


immagazzinato. Il numero di bit necessari si può
sempre calcolare arrotondando per eccesso il
log 2 ( A+ 1), dove A è il numero in base 10.

Per sommare i numeri binari si usa il seguente metodo:


 0+0=0
 1+0=1
 0+1=1
 1+1=0 con riporto di 1 nella colonna successiva

Se ottengo un riporto dopo l’ultima colonna vuol dire che non posso immagazzinare tale
numero nei bit previsti, quindi si ha un overflow.

Con i relativi abbiamo numero e segno: in n bit abbiamo un bit di segno (0 è +, 1 + -) ed n-1
bit di modulo.
11000 potrebbe essere sia 24 che -8, dipende da come interpretiamo i dati.

In n bit posso quindi, con modulo e segno, rappresentare da -2 n-1 +1 a 2n-1 -1.
Se avessi bisogno di aggiungere degli zeri (per esempio se avessi più bit a disposizione)
dovrei ovviamente farlo dopo il modulo.

Per rappresentare un numero A in base 10 con la base 2 modulo e segno ho bisogno di


log 2 (| A|+1 ) +1bit.

Nella rappresentazione base 2 complemento 2 la prima cifra raccoglie sia modulo che
segno (quindi il numero, rispetto alla base 2, varia solo se negativo). In n bit posso quindi
rappresentare da -2n-1 a +2n-1 numeri.

Per convertire in base 2 complemento 2 un numero negativo si procede nel seguente


modo:
 Si prende il suo valore assoluto
 Lo si converte in binario
 Si prende il suo opposto (basta porre uno 0 davanti)
 Si inverte tale numero e si aggiunge 1

In 2C2, inoltre, esiste solo la somma:


A-B diventa A+(-B)

20/9/2022
La virgola nel sistema binario

L’interpretazione del numero binario 100100 cambia in base al sistema numerico di


riferimento. Corrisponde a
 22+25=3610 in binario naturale
 -410 in binario MS
 -25+22=-2810 in binario 2C2

In generale, alle quantità K (kilo), M (mega), G (giga), T (Tera) si associano


rispettivamente 103, 106, 109 e 1012. In informatica invece si associano potenze del 2, ovvero,
rispettivamente, 210, 220, 230, 240.

Per rappresentare i numeri con la virgola (in questo caso i decimali compresi tra 0 e 1,
escluso), dopo di essa usiamo una notazione posizionale al contrario (10 -1, 10-2, …). Nel
sistema binario abbiamo che:
0,10112=1*2-1+0*2-2+1*2-3+1*2-4=0,5+0,125+0,0625=0,6875

Con n bit rappresentiamo quindi 2n numeri, compresi tra 0 e 1 escluso, quindi tra 0 e 1-2-n.

Il nostro sistema numerico di riferimento generale è però R, che, come sappiamo, è denso.
Di conseguenza dobbiamo capire se sia possibile rappresentare tutti i numeri di R
attraverso questa trasformazione. Proviamo col numero 0,6875:
Moltiplico ripetutamente per 2 ciò che c’è dopo la virgola -> 1,375 -> 0,75 -> 1,5 -> 1,0
Mi fermo quando ottengo ,0 oppure quando sono finiti i bit.

Prendendo gli 0 e gli 1 prima della virgola da sopra a sotto riesco a dedurre il numero in
binario:
Ottengo quindi che 0,687510 corrisponde a 0,10112.

Per un numero come quello visto in precedenza ci siamo resi conto che non c’è problema
ad operare una trasformazione di sistema numerico. Ma per 0,1 per esempio?

Ci accorgiamo della sua periodicità, che però non è accettata dal calcolatore, il quale è
dotato di un preciso numero di bit. Il risultato è che si perde sempre dell’informazione, e la
quantità di essa dipende dal numero di bit che abbiamo a disposizione.
(Per generalizzare, si può dire che un numero non è periodico in binario quando si può
esprimere come n/2m con n ed m numeri interi).

Per convertire, per esempio, un numero come 19,687510 in binario, bisogna scomporlo in
due parti:
 Quella prima della virgola: 1910=100112
 Quella dopo la virgola: 0,687510=10112
Il risultato che si ottiene è quindi 10011,10112. La domanda è: cosa facciamo della virgola?
 Prima ipotesi: Prefissiamo n bit per la parte intera ed m per quella decimale (la
virgola non ci serve più perché sappiamo ora la sua esatta posizione). Tale
rappresentazione viene detta “a virgola fissa” (fixed point) e ci consente di
rappresentare numeri da 0 a 2n-2-m (per i numeri negativi si usa la rappresentazione
modulo e segno, con quest’ultimo compreso nella parte intera).
 Seconda ipotesi: deriva dall’inadeguatezza del sistema fixed point, che vincola il
calcolatore ad un preciso intervallo di valori. Si introduce quindi il calcolo “a virgola
mobile” (floating point), nel quale si moltiplica il numero per una potenza del 2
(analogamente a come si moltiplica il numero per una potenza del 10 nella
notazione scientifica). Si avrà uno schema del seguente tipo (standard IEEE 754):
+/- 1,M * 2E, dove +/- definisce il segno, M è detta mantissa ed E è l’esponente.

Lo standard può essere a singola precisione o a doppia precisione:


 Singola precisione (32 bit): necessito di 1-23-8 bit rispettivamente per segno,
mantissa ed esponente. Lo schema è quindi il seguente:
 Doppia precisione (64 bit): necessito di 1-52-11 bit rispettivamente per segno,
mantissa ed esponente. Lo schema è analogo a quello sopra presentato ma con
valori ovviamente diversi.

Nello schema precedente si può notare come abbiamo posto, definendo l’esponente,
20=127. Le potenze rappresentabili in 8 bit sono 28=256, quindi i possibili esponenti vanno
da 0 a 255. Per rappresentare anche gli esponenti minori di 0 si pone quindi che
l’esponente 0 è codificato dal numero 127, a metà tra 0 e 255.

Proviamo quindi questo standard:


+19,687510=+10011,10112=1,001110112*24

S=02
E=4+127 (per il motivo spiegato qualche riga sopra)=13110=100000112
M=001110110000000000000002

Il risultato si rappresenta in tale maniera:

Considerando solamente gli esponenti compresi tra 1 e 254, si possono rappresentare le


potenze da 2-126 a 2127.

Vediamo ora alcuni casi particolari di rappresentazioni:

+0=

-0=

+inf=

-inf=

Nan (not a number)=

Spesso si converte dalla base 2 alla base 16, poiché occupa 4 volte meno spazio. L’alfabeto
HEX va da 0 a F (passando per 9 ed A). Il numero binario da convertire viene processato 4
cifre alla volta.
22/9/2022
Introduzione al C

In questo corso lavoriamo sullo standard C89.

Un programma è un algoritmo scritto nel linguaggio di programmazione. Il compilatore


prende il codice sorgente e lo trasforma nel linguaggio macchina.

C è uno dei linguaggi più utilizzati poiché non fornisce informazioni troppo elaborate.

In questa lezione vediamo come scrivere il seguente algoritmo, già affrontato in


precedenza, con C.

Innanzitutto scrivo inizio e fine, nel seguente modo:


int main (){
return 0;
}

Poi devo dichiarare le variabili di cui ho bisogno. Per ogni variabile devo specificare il
dominio:
 float=floating point, ovvero valori col punto (2.0).
 int=interi, e si rappresentano col complemento a 2. Si può usare divisione intera e
modulo (solo sui valori positivi). Per il numero di bit da usare non c’è uno standard,
potrebbe essere 32 tale numero, ma non è sempre così.
 char=caratteri, che si indicano tra apici (‘a’) e possono essere soggetti a operazioni
aritmetiche
Ricordiamoci molto bene che 2.0 float ≠ 2 int ≠ ‘2’.
Per dichiarare le variabili, in questo caso, scriviamo:
float r, a;
oppure
float r;
float a;

Passiamo quindi all’acquisizione dei dati. Come passano dalla tastiera alla memoria? Lo
schema generale, visto graficamente, è il seguente:
I dati passano dalla tastiera al buffer dello standard input, dove vengono immagazzinati
uno dietro l’altro e dove “\n” indica l’invio. Si passa quindi alla CPU, dove scanf acquisisce
materialmente i dati e li scrive in memoria.
Per acquisire i dati devo quindi scrivere:
scanf(“%f”, &r);
dove “%f” è la stringa di formato (con la f rappresenta un float, se sostituisco con la d
rappresenta un intero, mentre con la c un carattere) e &r indica il salvataggio in memoria
nella variabile r.
Se volessi scrivere più variabili, in questo caso 2 interi e un float, dovrei scrivere:
scanf(“%d%d%f”, &a, &b, &c);

Devo poi svolgere il calcolo vero e proprio, in questo caso:


a=r*r*3.14;
ma 3.14, essendo un numero, non ci piace. Possiamo quindi assegnargli un simbolo,
usando l’istruzione define in questo modo:
#define PI 3.14
Usiamo il maiuscolo in modo che le costanti siano immediatamente riconoscibili nel nostro
codice.

Il dato processato passa dalla memoria alla CPU, dove printf lo fa transitare attraverso il
buffer dello standard output. La printf si usa nel seguente modo:
printf(“area=%f\n”, a);
Va tenuto a mente che nella printf possiamo scrivere anche delle costanti (come “area” in
questo caso).

Inoltre il codice deve cominciare specificando la libreria che si va ad usare, in questo


modo:
#include<stdio.h>

L’ultimo passaggio da svolgere è nominare il programma, scrivendo il nome in questo


modo:
/*Nome*/

Il codice risulta, in questo caso, il seguente:

Infine, parlando degli errori, perché tanto prima o poi ne farai, possiamo distinguerli tra:
 sintattici: il compilatore capisce che potrebbe esserci un errore
 semantici: sta a noi capire dove sia l’errore
23/9/2022
Primi programmi in C – Numeri binari

Abbiamo affrontato tre esercizi:

1. Nel primo, inserito un carattere dell’alfabeto, dobbiamo stampare il successivo.


Associamo 0 ad a e così via fino a z=25. Il carattere successivo si definisce come:
car – ‘a’ + 1 (dove 1 è il passo). Ma dobbiamo gestire il caso in cui viene inserito z,
perché da 25 bisogna tornare indietro a 1. Per tornare da 25+1 a 1 consideriamo il
modulo (resto) della divisione intera tra 26 e 25, ovvero 1. Ciò viene formalizzato
così come possiamo osservare nel codice qui di seguito:

2. Nel secondo dobbiamo fare operazione tra elementi eterogenei (float + int). Uno dei
due viene convertito. Più in generale si può dire che la priorità è:
double>float>int>char
Il codice dell’esercizio è il seguente:

3. Nel terzo bisogna specificare, attraverso il cast specifico, uno dei valori da
interpretare con virgola mobile. Questo strumento si usa come nel codice ora
riportato:

27/9/2022
Programmazione in C – Blocco condizionale

In C non esiste il tipo booleano, ma vengono usati gli interi. 0 rappresenta il falso, l’1 (o
qualsiasi altro valore diverso da 0) rappresenta il vero.
Per realizzare il programma presentato di seguito non abbiamo bisogno del blocco
condizionale:

Nel seguente caso avremo però bisogno di tale funzione:

Nell’if introduco l’istruzione del ramo vero (nelle parentesi graffe posso introdurre un
blocco di codice, quindi più istruzioni in sequenza), mentre nell’else l’istruzione del ramo
falso. Da notare che l’else, a differenza dell’if, è facoltativo.

Nel seguente esempio introduco l’else if:

Nel seguente esempio verifichiamo quanto sia importante l’uso delle parentesi graffe, al
fine di subordinare gli else agli if nel modo che desideriamo:
29/9/2022
Cicli in C

Un costrutto che abbiamo accennato, usato per sostituire l’if-else, è lo switch.

I cicli possono essere di due tipi:


1. A condizione iniziale (while)
2. A condizione finale (do while)

Abbiamo anche introdotto due operatori, ++ e --, rispettivamente di incremento e


decremento di un valore, specificando che è vietato usarli in espressioni più grosse.

Nell’esercizio 4 usiamo %10 per stampare l’ultima cifra, mentre /10 per calcolare il nuovo
numero. Se cambio 10 con 2 ottengo il programma per il metodo dei resti.

Nell’esercizio 6 abbiamo anche introdotto il ciclo for, equivalente al while, che mi dice
quanto contare.

4/10/2022
Array (vettore)

L’array (o vettore) è un contenitore di valori omogenei. Si usa nel seguente modo:

La funzione array crea quindi un numero DIM righe di memoria, che sono numerate (i
numeri sono tra le parentesi quadre, e vanno da 0 a n-1). Per usare l’array devo anche
introdurre un contatore i.

13/10/2022
Stringhe in C

Per le stringhe usiamo \0 come valore terminatore. È un carattere non stampabile.

%s ci consente, nella scanf, di non usare un ciclo for per l’acquisizione di una stringa. Va
allocato, nella stringa, uno spazio in più per il terminatore.

Nella stringa va sempre posto il terminatore in fondo (vedi riga 14 dell’esercizio 3.c).

gets(s1) permette di acquisire una intera stringa, compresi gli invio. Il suo utilizzo genera
un warning perché è una pratica in disuso.
18/10/2022
Strutture in C

Con typedef vado a creare un nuovo tipo. Non alloco spazio in memoria, sto solo creando
un formato che userò successivamente.

Nell’esercizio 1 la memoria conterrà due righe per gli elementi a,b e somma, ovviamente a
causa della natura del typedef specificato in precedenza.
Per accedere alla specifica riga dobbiamo usare la formula variabile.campo, quindi per
inizializzare il numeratore di a scriviamo a.num=0.

21/10/2022
Sottoprogrammi in C

Per creare un sottoprogramma in C si usa il prototipo:


int *nome sottoprogramma*(int n);
dove n è l’input.

Il sottoprogramma (funzione) per calcolare il fattoriale di un numero è il seguente:

Nel sottoprogramma dell’esempio la n non va inizializzata: ciò viene fatto prendendo il


valore in esame.

Quando il sottoprogramma arriva alla return si elimina tutta la memoria che gli era stata
allocata.

Le varabili locali sono visibili solamente al singolo sottoprogramma. Se, nell’esempio di


prima, avessi messo n al posto di num1 avrei avuto due righe chiamate n in memoria, ma
distinte.

25/10/2022
Puntatori in C

Andiamo a lavorare sulle celle di memoria utilizzando il loro indirizzo.


Il puntatore è una variabile che non contiene un valore ma un indirizzo (si indica con
l’asterisco: *p. Per assegnare i valori a p scrivo p=&a).

Scrivendo *p=10 assegno 10 alla cella puntata da p.

Potrebbero piacerti anche