Sei sulla pagina 1di 25

Le basi

I bit e il sistema binario


Le informazioni vengono misurate nei calcolatori dal bit. Il bit (acronimo di binary digit) è la misura minima
dell’informazione che distingue due eventi (0 e 1), usando quindi il sistema numerico binario per ogni cifra.
Per n bit, le possibili scelte sono 2n volte e i numeri naturali rappresentati in cifre naturali vanno da 0 a
n
2 −1 . 1 byte corrisponde a 8 bit.
La parola di memoria è un gruppo di bit che vengono gestiti dall’hardware come un’unità e la sua lunghezza
è importante per l’architettura del computer.

Rappresentazione di numeri in sistema binario


Per convertire un numero di base 10 in uno a base binaria bisogna dividere il numero per 2 fino a quando il
risultato sarà 0. Per convertire da base 2 a base 10 bisogna sommare tutti i prodotti ottenuti tra le cifre
binarie e le rispettive potenze generate dalla loro posizione nel numero.

Numeri razionali
Per rappresentare i numeri razionali bisognerà moltiplicare per 2 la frazione: se il risultato è maggiore di 1 si
scriverà 1 dopo la virgola; se il risultato è minore di 1 si scriverà 0 dopo la virgola.

Numeri relativi
Per rappresentare un numero relativo si possono usare tre metodi:

1. Anteporre un numero per segnalare la positività o negatività della parola (0 se è positivo, 1 se


negativo). Non viene utilizzato perché implica la perdita di un bit e una consistente capacità di
memoria;
2. Complemento a 1: si invertono tutti i bit della parola. È poco usato;
3. Complemento a 2: si invertono tutti i bit della parola (complemento a 1) e si aggiunge 1.

Numeri con la virgola


Vengono definite due classi di numeri con la virgola:

- Numeri a virgola fissa: hanno una divisione specifica tra i numeri interi (prima della virgola) e i
numeri compresi tra 0 e 1 (dopo la virgola). Non è specificata la presenza della virgola nella
conversione ai numeri binari.
- Numeri a virgola mobile: nel momento in cui si devono rappresentare numeri infinitamente grandi
o infinitamente piccoli si preferisce raffigurare i numeri a virgola mobile.

In informatica, la virgola mobile è regolata dallo standard IEEE 754. Lo standard si estende in diversi
formati, ma quelli più conosciuti sono a 32 bit (singola precisione) e a 64 bit (doppia precisione). Lo
standard divide la parola in 3 parti (le quantità si riferiscono alla versione a 32 bit)

1. Segno (1 bit): indica il segno del numero (0 se positivo, 1 se negativo);


2. Esponente (8 bit): indica l’esponente della potenza di 2 con cui bisogna moltiplicare la mantissa.
L’esponente è k+(2n-1-1) e sarà sempre positivo poiché l’eccesso è maggiore dell’esponente di 2;
3. Mantissa (25 bit): indica la parte dopo la virgola del numero binario.

Casi particolari
- Se l’esponente è 00000000:
o Con mantissa=0, il numero è 0,0;
o Con mantissa≠0, il numero è circa 0,00.
- Se l’esponente 11111111:
o Con mantissa=0, il numero è infinito;
o Con mantissa≠0, il numero è NaN (not a number) e non ha un valore definito.

Operazioni col sistema binario


Le operazioni col sistema binario sono addizione e sottrazione, ognuna con le proprie peculiarità:

- Nell’addizione si effettua il riporto quando A+ B=1+1 . Poiché il risultato è 10, 0 verrà scritto in
corrispondenza di 1+1 e 1 riportato alle cifre successive. A causa del riporto può verificarsi
l’overflow, dove il risultato ha dei bit maggiori rispetto a quelli disponibili, non permettendo quindi
alla macchina di registrare e visualizzare il risultato effettivo;
- Nella sottrazione si effettua il prestito quando A−B=0−1. Poiché il risultato è 1, la cifra
successiva cederà 1 a 0 (diventando così 10) e il risultato della sottrazione verrà posto in
corrispondenza di A e B. a causa del prestito possono verificarsi casi di underflow dove il risultato è
troppo piccolo per essere rappresentato.

Codice ASCII
Per codificare caratteri viene spesso utilizzato il codice ASCII (American Standard Code for Information
Interchange), un sistema binario con una parola di 7 bit che contiene tutti i caratteri ottenibili da essa. Poiché
i caratteri che si vogliono ottenere (simboli, lettere maiuscole e minuscole ordinate, …) sono tanti, viene
spesso usata una parola di 8 bit, dato che si possono avere 256 combinazioni a differenza delle 127 con 7 bit.

Architettura di Von Neumann


Il comparto hardware di un calcolatore segue ancora oggi la struttura ideata da Von Neumann agli inizi del
Novecento. Un computer è composto dai seguenti moduli collegati:

- Bus di sistema: collega i vari elementi del calcolatore e trasporta le informazioni tra i moduli. Essa è
divisa in tre parti:
o Bus dati;
o Bus indirizzi: trasporta l’indirizzo desiderato;
o Bus comandi: trasporta informazioni sui comandi da effettuare.
- Memoria centrale (RAM): accoglie i dati e i programmi in esecuzione. È una memoria volatile (si
cancella quando viene spento il computer), composta da celle ognuna con un suo indirizzo (a cui si
può accedere per leggere o scrivere) e una sua parola la cui lunghezza dipende dalla macchina. Si
differenzia dalla memoria ROM, dove sono collocati programmi e file;
- Processore (unità di controllo di processo, CPU): esegue un programma, lo fa procedere e controlla
che tutti i processi siano stati effettuati. È composto di diverse parti:
o Unità di controllo: controlla e gestisce i processi;
o ALU (unità aritmetico-logica): esegue i calcoli;
o Registro dati (data register, DR);
o Registro indirizzi (adress register, AR);
o Registro istruzione corrente (current instruction register, CIR): contiene una copia
dell’istruzione in esecuzione;
o Clock: misura in Hertz la velocità di sistema e sincronizza cronologicamente i processi che si
susseguono;
o PC (programme counter): contiene l’indirizzo dell’istruzione che si sta per eseguire;
o Controllore;
- Interfaccia di ingresso/uscita (input/output): acquisisce i dati e mostra i risultati.

Le fasi del processore


1. Prelievo: il PC contiene l’istruzione e la trasporta nel CIR;
2. Decodifica: l’istruzione viene decodificata;
3. Esecuzione: l’istruzione viene eseguita. Le azioni che il processore può fare sono:
a. Scrivere e leggere la memoria;
b. Dare comandi all’ALU;
c. Modificare i valori del PC;
d. Scrivere sui registri;
e. Trasferire dati.
Il linguaggio C: le basi
Affinché vengano risolti dei problemi da un calcolatore bisogna creare un algoritmo in un linguaggio che sia
comprensibile dal computer. Il linguaggio di codifica deve essere preciso e sintetico, comprensibile sia alla
macchina sia dal programmatore. Si inizierà quindi a parlare quindi di linguaggi di alto livello, molto vicini al
linguaggio umano.

Il linguaggio C è un linguaggio di alto livello, dove la macchina è pensata come composta da:

- Una memoria infinita composta da variabili, delle celle non uguali ognuna di esse indicata con un
identificatore e non con un indirizzo numerico. Le variabili possono contenere dati come numeri,
lettere e stringhe. Gli identificatori sono una successione di lettere e cifre che iniziano con una
lettera. Il linguaggio C distingue tra maiuscole e minuscole, quindi è possibile avere variabili
omonime, ma è sconsigliato usare dei nomi simili a causa delle ambiguità che si possono creare.
Esistono identificatori predefiniti e riservati che il programmatore non può usare per altri scopi al di
fuori di quello originario;
- Un’unità di input da cui ricevere informazioni;
- Un’unità di output su cui scrivere dati.

Un programma C è composto generalmente da 4 parti:

- Dichiarazioni: danno informazioni sul tipo di variabile utilizzata;


- Inizializzazione: le variabili assumono un valore definito dall’utente;
- Elaborazione: i comandi veri e propri che il programma deve eseguire, dove i dati in input
producono quelli in output.
- Conclusione: vengono enunciati i risultati.

Tutte queste parti vengono incluse in una serie di parentesi graffe. Alla fine di ogni riga deve essere
presente il punto e virgola per essere letta e compresa dal calcolatore.

Tipi di dato
È un nome che indica l'insieme di valori che una variabile, o il risultato di un'espressione, possono assumere
e le operazioni che su tali valori si possono effettuare. Esistono tipi di dato semplici e tipi di dato complessi.
Tipi di dato semplici sono:

- int: assegna un valore numerico intero. È un valore definitivamente finito che usa una sequenza di
bit limitata;
- float: assegna un valore numerico reale. Può essere rappresentato in decimali oppure col metodo
della virgola mobile;
- char: hanno sia valore alfabetico che valore numerico poiché vengono memorizzati come interi in
un byte. Tale intero è indicato dal codice ASCII e può essere restituito dalla macchina. Esistono
caratteri di controllo, che non vengono stampati a schermo ma eseguono operazioni sulla
visualizzazione a schermo;
- double: assegna un valore decimale in doppia precisione.

Il qualificatore const fa assumere ad un qualsiasi tipo di dato un valore immodificabile da dichiarare in fase
di inizializzazione, che provoca un errore di compilazione se si tenta di modificarlo.

A questi tipi di dati semplici vanno aggiunti i qualificatori di tipo, che regolano l’ampiezza dello spazio
allocato e gli estremi dell’intervallo:

- unsigned e signed: riferiti agli interi. Lavorano su numeri senza segno;


- short e long: riferiti agli interi. Gli short sono compresi tra +32767 e -32767 e i long tra
+2147483647 e -2147483647. Gli interi sono in un numero compreso tra gli short e i long.

L’enumerazione
Un tipo di dato fondamentale è l’enumerazione, che consiste in un insieme di costanti intere che vanno
nominate. L’enumerazione parte da 0, se non diversamente specificato, ed è incrementata di 1 per ogni
valore. Gli identificatori corrispondono ognuno ad un valore. Per far iniziare l’enumerazione da un valore
diverso da 0 è necessario inizializzare il valore durante la definizione; è possibile anche assegnare un valore
costante a diversi membri di un’enumerazione.

Istruzioni
Nel linguaggio C si eseguono diversi tipi di istruzioni.

- Istruzioni di assegnamento [¿]: la variabile riceve un valore. È l’unica istruzione da leggere da


sinistra verso destra;
- Istruzioni di input/output: riceve dei valori scritti dall’utente e stampa i risultati.
- Istruzioni condizionali: la macchina deve scegliere tra due opzioni.
- Istruzioni iterative: la macchina esegue dei compiti fino a quando non viene falsata una condizione.

Istruzioni condizionali
In C le condizioni vengono affidati ai seguenti comandi:

- if… else: il calcolatore valuta una condizione secondo cui il programma deve procedere. Non è
necessario l’utilizzo dell’else.
- else if: raramente utilizzato. Il calcolatore deve effettuare un’ulteriore verifica per un’altra
condizione. È preferibile utilizzare un’unica condizione if.
- switch, break, continue: il primo valuta diverse condizioni. Gli ultimi due sono comandi che
rispettivamente interrompono oppure fanno continuare un programma al raggiungimento di un
determinato valore. Non è consentito utilizzarli negli esercizi ed è preferibile utilizzare un comando
più elaborato.

Istruzioni iterative
Le iterazioni vengono controllate tramite un contatore oppure tramite un valore sentinella.

- I valori sentinella vengono utilizzati nei comandi iterativi quando non si ha la certezza della quantità
dei dati inseriti. Devono sempre coincidere con un valore che non può essere utilizzato nel
programma (ad esempio, un numero negativo se il programma deve calcolare la media dei voti
ottenuti dagli alunni in un compito);
- Un contatore è una variabile inizializzata dell’utente che deve avere un nome, aumentare (o
diminuire) a seguito di una determinata condizione ed eventualmente terminare quando tale
condizione è mutata. Quando viene enunciata occupa uno spazio in memoria ed ha il valore
inizializzato.

Esistono diversi comandi di iterazione:

- while: è il comando base per l’iterazione. Gli eventuali contatori vanno inizializzati prima di tale
comando.
- for: unifica in un solo passaggio l’inizializzazione e la condizione per cui il contatore deve
aumentare o diminuire. È rappresentato come for(inizializzazione; condizione; incremento). Non è
necessario enunciare tutte e tre le istruzioni:
o Può essere omessa la condizione: in tal caso si creerà un ciclo infinito poiché la condizione è
sempre vera;
o Può essere omessa l’inizializzazione, a patto che il contatore sia stato enunciato prima;
o Può essere omesso l’incremento: in questo caso l’incremento è enunciato all’interno del for
oppure non sono previsti incrementi.
- do… while: è simile al comando while, ma l’impostazione del comando permette di eseguire il ciclo
almeno una volta in quanto le condizioni sono disposte dopo le istruzioni.

Operatori di relazione
Vengono utilizzati per confrontare due variabili oppure una variabile e un valore. Hanno una priorità
maggiore rispetto agli operatori logici, il che significa che verranno eseguiti prima. Gli operatori sono:

- Operatore di uguaglianza [¿=¿];


- Operatore di disuguaglianza [!=¿];
- Operatori maggiore e minore [¿], [¿];
- Operatori maggiore uguale e minore uguale [≥], [≤].

Confondere uguaglianza e assegnamento


Indipendentemente dall’esperienza con C può capitare di confondere l’operatore uguaglianza ( ¿=¿) con
quello assegnamento (¿); questo errore non viene notato dal calcolatore, che anzi compila correttamente il
programma seppur con degli errori logici che non faranno ottenere i risultati sperati. Per evitare di
incappare in questi errori se ci si trova davanti a una variabile ed una costante, è consigliabile scrivere la
costante a sinistra e la variabile a destra: in C infatti le variabili sono dette left value (valore sinistro) e le
costanti right value (valore destro) perché con la condizione di assegnamento queste possono essere
utilizzate rispettivamente a sinistra e a destra e in caso si sia scritto assegnamento (=) anziché uguaglianza il
compilatore restituirà un errore di sintassi.

Operatori logici
Gli operatori logici sono AND (&&), OR (||) e NOT (!). Hanno una priorità minore degli operatori relazionali
e di uguaglianza e permettono l’utilizzo di più condizioni in un’istruzione, evitando l’uso di if e if… else
nidificati che renderebbero il programma più difficile da debuggare.

- L’operatore && ha risultato vero solo quando entrambe le condizioni sono vere. C valuta vera una
condizione quando questa assume un valore diverso da 0 (generalmente 1). È consigliabile mettere
una prima condizione tendenzialmente falsa per ridurre il tempo di esecuzione del programma.
- L’operatore || ha risultato vero quando almeno una delle variabili è vera. Ha una priorità minore di
&&. È consigliabile mettere una prima condizione tendenzialmente vera al fine di ridurre il tempo di
esecuzione del programma.
- L’operatore ! nega la condizione a cui è affidato. Ha una priorità superiore alla condizione di
uguaglianza e può essere scritto senza utilizzare parentesi. Tuttavia, a lui viene preferito l’uso della
condizione di disuguaglianza.

Teorema di Bohm-Jacopini
Il teorema di Bohm-Jacopini dice che per programmare si ha bisogno di solo tre forme di controllo:

- Sequenza;
- Selezione (if, if… else, switch);
- Iterazione (while, do… while, for).

Questo implica che per creare un programma si ha bisogno solo di queste tre forme che andranno
accatastate oppure nidificate.
Le funzioni
Per creare programmi complessi è più semplice dividere il programma in tanti piccoli moduli, chiamati
funzioni. In C esistono due tipi di funzione:

1. Quelle presenti nella libreria standard di C, che non devono essere implementate dal
programmatore;
2. Quelle che vengono implementate dal programmatore.

Per poter essere eseguita, una funzione deve essere chiamata. Nella chiamata, vanno specificati il nome
della funzione e gli argomenti di cui la funzione ha bisogno. Nelle funzioni, le variabili dichiarate sono locali
e non hanno valore in altre funzioni. L’utilizzo delle funzioni è da ricercarsi nella sua astrazione (dove la
funzione può essere riutilizzata in altri programmi), nella sua portabilità in altri linguaggi e nella sua
reiterazione in un programma, senza dover scrivere ex-novo il programma nella funzione main.

Struttura di una funzione


Una funzione è strutturata nel seguente modo

TipoDiDato NomeFunzione (TipoDatoParametri NomeParametri)

Dichiarazioni;
Istruzioni;

La prima riga è detta intestazione della funzione. Ogni funzione deve contenere il comando return e il tipo
di dato restituito può essere di qualsiasi tipo; un tipo di dato void non restituirà alcun valore ed è
considerato un errore scrivere return. Se omesso, il tipo di dato restituito dalla funzione sarà sempre un
intero; tuttavia, l’assenza di tipo di dato può portare ad errori inattesi.

I parametri sono separati da virgola e va specificato per ognuno il tipo di dato. Se si vuole che la funzione
non riceva alcun parametro bisogna scrivere void. Se omesso, il parametro è di tipo intero. È un errore
mettere il punto e virgola al posto della virgola e bisogna dichiarare ogni tipo di dato delle variabili.

Le dichiarazioni e le istruzioni sono contenuti nel blocco. È proibito dichiarare una funzione in un’altra
funzione.

Per restituire il risultato di una funzione bisogna usare return espressione. Nel caso in cui la funzione non
restituisca niente si possono utilizzare il comando return [senza espressione] oppure chiudere direttamente
la funzione con la parentesi graffa.

Prototipo di funzione
Il prototipo di una funzione è un comando posto nel preprocessore che dice al calcolatore il numero, il tipo
e l’ordine dei dati inseriti e il tipo di dato restituito. Fungono da chiamata legittima della funzione e
vengono messi prima del programma perché se dovessero essere utilizzate delle variabili globali queste
devono essere dichiarate nel programma.

Un’altra utilità dei prototipi di funzione è la conversione forzata degli argomenti al tipo adeguato. Bisogna
però specificare che non sempre la conversione produce dei risultati previsti: un intero può essere
convertito in float, ma il viceversa implica la perdita della parte decimale, così come la conversione da un
long a uno short potrebbe produrre valori completamente differenti. In generale, convertire in un valore
più basso può portare a valori inaspettati.
Chiamata di funzione
In C le chiamate di sistema seguono la struttura dello stack, dove la prima funzione ad essere inserita è
anche la prima a restituire un valore. L’insieme di variabili locali utilizzate ad ogni esecuzione è detto record
di attivazione della chiamata della funzione. Il record viene generato ogni volta che viene eseguito il
programma e, ogni volta che il programma termina, questo viene cancellato e non si ha più accesso.
Essendo la memoria finita, può succedere che quando vengano effettuate troppe chiamate ad una funzione
si produca un errore noto come overflow dello stack.

Per chiamare una funzione si può utilizzare la chiamata per valore oppure la chiamata per riferimento. Nella
chiamata per valore verranno create delle copie delle variabili e non si andranno ad intaccare i valori delle
variabili originali. Nelle chiamate per riferimento si può invece agire modificando direttamente i valori delle
variabili originali. Tutte le chiamate di funzione in C sono per valore, anche se si può simulare l’utilizzo della
chiamata per riferimento utilizzando operatori di indirizzo e di derifermento. È consigliabile utilizzare le
chiamate per riferimento solo per funzioni sicure.

Ricorsività di una funzione


La ricorsività di una funzione è la chiamata di una funzione direttamente oppure indirettamente tramite
un’altra funzione. L’algoritmo ricorsivo funziona suddividendo i problemi complessi in un caso semplice da
eseguire e in un secondo problema, simile al primo ma più semplice; il secondo problema viene risolto da
una copia della funzione stessa e verrà a sua volta diviso in un caso semplice e in un terzo problema, e così
via fino a quando la funzione non verrà chiusa (si arriverà al caso base).

Un esempio di algoritmo ricorsivo è il fattoriale: il fattoriale di un numero è il prodotto di tutti i numeri


naturali da un numero n a 1. Poiché una proprietà del prodotto fattoriale è n !=n∗( n−1 ) !, la ricorsività
può essere vista come il prodotto tra un numero n e il fattoriale ( n−1 ), a sua volta visto come il prodotto
tra ( n−1 ) e il fattoriale di ( n−2 ), e così via fino a 1.

Iterazione o ricorsività?
Iterazione e ricorsività sono i due metodi più utilizzati per progettare un programma. Hanno diversi punti in
comune, ma ognuno di loro ha le sue peculiarità e i suoi difetti che in determinate situazioni fanno preferire
un metodo anziché l’altro. Sono entrambe

- Strutture di controllo: la prima è un ciclo, mentre la seconda è una selezione.


- Cicli: la prima è controllato da un indice e finisce quando l’indice non soddisfa più la condizione. La
seconda è un ciclo nascosto, dove la funzione richiama sé stessa fino a quando non si arriva ad un
caso base.
- Possono essere entrambe infinite: l’iterazione sarà infinita se l’indice non falsificherà mai la
condizione, mentre l’algoritmo ricorsivo sarà infinito se non arriverà mai ad un caso base.

Un problema di tipo ricorsivo può essere risolto sia con la ricorsività che con l’iterazione

Nonostante l’algoritmo ricorsivo sia un processo meccanico e dispendioso della memoria è spesso preferito
all’iterazione e i motivi sono semplici: la prima può essere una soluzione più immediata dell’iterazione
oppure quest’ultima è una soluzione più difficile da comprendere, implementare e correggere di una
ricorsività.

La programmazione è fatta di compromessi


È difficile che si ottenga un programma che sia comprensibile ed allo stesso tempo efficiente, quindi si
preferisce un elemento anziché un altro. Un esempio può essere l’uso di programmi monolitici o funzioni:

- I programmi monolitici sono efficienti, ma difficili da implementare e correggere;


- I programmi che utilizzano funzioni sono facili da implementare, ma spesso richiedono uno sforzo
maggiore per la memoria che deve richiamare le funzioni.
I vettori
Un vettore è un gruppo di locazioni di memoria che sono omonimi e hanno lo stesso tipo di dato. I vettori
iniziano sempre dall’elemento di posizione 0 e vengono dichiarati come Nome_vettore[indice_elemento]. Il
nome del vettore può contenere lettere, numeri e trattini bassi ma non può iniziare con un numero.

Quando si punta ad un elemento bisogna considerare che l’i-esimo elemento puntato ha indice i−1.
L’indice è un numero intero oppure fa riferimento ad un intero (sia esso una variabile oppure
un’espressione). Le parentesi quadre usate per indicare un elemento del vettore sono operatori che hanno
la stessa priorità delle parentesi tonde usate per richiamare una funzione.

Un vettore viene dichiarato come TipoDato NomeVettore[Quantità]. Un vettore può essere di qualsiasi tipo.

Per popolare un vettore possono essere utilizzati 2 metodi:

1. Popolamento tramite ciclo for;


2. Popolamento tramite inizializzazione di lista.

Le stringhe
Un vettore di caratteri è chiamato stringa ed è terminato dal carattere nullo ‘\0’. È necessario, quando si
riempie la stringa, lasciare l’ultimo posto al carattere nullo (quindi l’indice deve essere sempre minore della
dimensione, MAI minore o uguale). Nel caso in cui gli elementi inseriti nella stringa siano di più della
dimensione della stessa, tutti quelli in eccesso verranno allocate in zone di memoria successive alla stringa,
sovrascrivendo i dati contenuti e magari producendo un segmentation fault.

Quando si vuole popolare una stringa si scrive il comando scanf(“%s”, Nome_Vettore). “%s” è una specifica
di conversione delle stringhe che, a differenza di %c, considera solo il carattere effettivamente scritto ed
esclude il tasto invio per inserire l’elemento nell’array.

Il vettore come parametro di funzione


Per passare un vettore ad una funzione bisogna inserire separatamente nome del vettore e numero di
elementi. I vettori sono automaticamente passati per riferimento e il nome si riferisce direttamente al
primo elemento della stringa; quindi, quando la funzione viene eseguita, si sta lavorando direttamente sugli
elementi contenuti nel vettore e si conosce la posizione in memoria di quest’ultimo. L’utilizzo dei
riferimenti anziché del valore è per semplice efficienza: se un vettore di dimensioni ragguardevoli venisse
passato per valore diverse volte andrebbe ad occupare un numero spropositato di celle di memoria per le
copie. Tuttavia, gli elementi singoli del vettore, detti scalari, vengono passati per valore.

Ordinare un vettore: il bubble sort


Per ordinare un vettore è possibile usare il bubble sort (ordinamento a bolle), chiamato anche sinking sort
(ordinamento con sprofondamento), che ordina il vettore in modo ascendente, con gli elementi più piccoli
sopra e quelli più grandi sotto, come se fosse una bolla che risale in superficie oppure una pietra che
affonda. Il metodo consiste in diversi passaggi iterativi sul vettore dopo che questo è stato scritto. Questo
metodo è facile da implementare, ma è molto lento e macchinoso, soprattutto se questo è effettuato su un
vettore molto lungo.

Ricerca di elementi di un vettore


È possibile effettuare ricerche di elementi in un vettore. I due principali tipi di ricerca sono quella binaria e
quella lineare, molto più semplice da implementare della prima ma anche più dispendiosa.

1. La ricerca lineare consiste nell’uso di una chiave di ricerca, passando in rassegna tutti gli elementi
del vettore e confrontandoli con la chiave. La probabilità di trovare un elemento conforme è la
stessa sia al primo che all’ultimo elemento. Tale metodo è utile per vettori piccoli e non ordinati.
2. La ricerca binaria è invece utile per vettori grandi (meglio se ordinati). Il metodo è molto semplice:
data una chiave di ricerca, il programma punterà al centro del vettore. Se questo è l’elemento
cercato il programma si fermerà, altrimenti continuerà la propria ricerca in una delle metà del
vettore: se la chiave richiesta è minore della metà sarà puntato la metà della prima metà, altrimenti
punterà alla metà della seconda metà. Il programma continuerà a dimezzare il vettore fino a
quando l’elemento da cercare non sarà trovato.

Per dimostrare l’efficienza della ricerca binaria basta dire che in un vettore di un miliardo elementi bastano
solo 30 confronti rispetto ai 500 milioni della ricerca lineare.

Vettori multidimensionali
I vettori in C possono avere più di una dimensione. I vettori bidimensionali vengono chiamati anche matrici
e sono formati da due indici: per convenzione, il primo indice indica le righe, mentre il secondo indica le
colonne. Possono esistere anche vettori che hanno più di due indici. Una matrice viene dichiarata come
Tipo_Dato Nome_Matrice[riga][colonna].

L’operatore sizeof
L’operatore sizeof(Nome) è utilizzato per conoscere la misura in byte di un vettore. Essendo applicabile in
compilazione, essa non produrrà alcuna occupazione di memoria e restituirà un intero che rappresenta i
byte utilizzati dal vettore. Per conoscere la quantità di elementi presenti nell’array si può eseguire un
rapporto tra la misura del vettore e quella dell’elemento. Le parentesi non sono necessarie quando si parla
di una variabile, mentre sono necessarie se si deve analizzare un tipo di dato.
I puntatori
I puntatori sono l’argomento più importante e difficile di programmazione in C e consentono di manipolare
le strutture dinamiche e simulare le chiamate per riferimento. I puntatori sono delle variabili che assumono
come valore l’indirizzo di una cella di memoria: ecco perché riferirsi ad un valore per mezzo di un puntatore
viene definito deriferimento. I puntatori vengono dichiarati come Tipo_Dato *Nome_Puntatore che va letta
come “Nome_Puntatore punta ad una variabile di tipo Tipo_Dato”. L’asterisco * fa riferimento ad una sola
variabile, quindi se si vogliono dichiarare più puntatori in una sola riga è bene che tutte le variabili abbiano
l’asterisco.

I puntatori sono inizializzati ad 0, NULL oppure ad un altro indirizzo. NULL e 0 hanno lo stesso valore, ma è
preferibile che sia utilizzato il primo in quanto bisogna convertire la variabile ad un puntatore adeguato. 0 è
l’unico valore intero che può essere assegnato direttamente ad un puntatore.

Operatori di puntatori
La e commerciale & è un operatore unario detto “di indirizzo” che restituisce l’indirizzo dell’operando. Si
può dire che una variabile puntatore punta ad una variabile. L’operatore di indirizzo è adatto solo a
variabili, quindi non si può utilizzare per costanti o espressioni.

L’operatore * è invece un operatore unario detto “di deriferimento” e restituisce il valore dell’elemento
puntato dal puntatore.

& e * sono complementari: indipendentemente dal proprio ordine restituiranno lo stesso risultato.

Simulare una chiamata di funzione per riferimento


Solitamente le funzioni in C vengono chiamate per valore, generando quindi una copia delle variabili della
funzione. Tuttavia, quando si maneggia una grande mole di dati copiare intere funzioni può essere un
grosso dispendio di memoria: per questo motivo, C permette di simulare una chiamata di funzione per
riferimento usando la chiamata per deferimento. Per permettere questo, la funzione deve ricevere come
parametro un puntatore ad un tipo di dato (operatore di deriferimento *). Quando viene chiamata nel
programma, la funzione riceverà l’indirizzo della variabile che deve essere modificata (operatore di indirizzo
&).

Qualora la funzione debba ricevere un vettore come parametro potrà essere dichiarato sia un puntatore al
nome del vettore sia il vettore stesso in quanto il compilatore vede il puntatore e il vettore allo stesso modo
(quindi Tipo_Dato *Vettore è la stessa cosa di Tipo_Dato Vettore[indice]).

Esistono 4 modi per passare con riferimento:

1. Puntatore variabile a dati variabili;


2. Puntatore costante a dati variabili;
3. Puntatore variabile a dati costanti;
4. Puntatore costante a dati costanti.

Esempi di utilizzo:

1. Convertire da minuscole a maiuscole una stringa di caratteri: il puntatore passa in rassegna tutti gli
elementi della stringa per controllare se vanno convertiti o meno e, in caso affermativo, sostituisce
l’elemento della stringa.
2. Il puntatore riceve un vettore in una funzione che accede ai suoi elementi solo con gli indici.
3. Stampare lettera per lettera una stringa di caratteri: il puntatore punta tutti gli elementi della
stringa e nel frattempo la stampa.
4. Il puntatore passa un vettore ad una funzione che non deve modificare alcun elemento contenuto
all’interno del vettore.

I puntatori costanti vengono dichiarati col qualificatore const.

Esistono sei modi (oltre ai già citati 4, ce ne sono 2 per passaggio di valore) per utilizzare const in una
funzione e se si preferisce un metodo anziché un altro è per il criterio del minimo privilegio (la funzione
può accedere solo ai dati di sua competenza).

Bubble sort con i puntatori


È possibile eseguire il bubble sort tramite l’utilizzo dei puntatori. Il vettore sarà dichiarato nella funzione del
bubble sort come un puntatore e la sua misura come costante per il principio del minimo privilegio. Inoltre,
sempre per il suddetto principio, sarà chiamata nella stessa funzione una funzione di scambio sempre per il
principio del minimo privilegio.

Aritmetica dei puntatori


Con i puntatori è possibile eseguire delle operazioni come se fossero delle semplici variabili. Le operazioni
effettuabili sono:

- Incremento e decremento per fare riferimento all’elemento successivo o precedente a quello


assegnato;
- Somma e differenza di un intero. Non rispecchiano sempre l’aritmetica a cui siamo abituati: solo nel
caso in cui si tratti di caratteri esse combaceranno, mentre in tutti gli altri casi sarà una somma tra il
valore iniziale e il prodotto tra la quantità che vogliamo aggiungere e il numero di byte che occupa il
tipo di dato;
- Somma e differenza con un altro puntatore. Il risultato rappresenta quanti elementi sono presenti
tra i puntatori.

Ovviamente, i puntatori devono puntare a degli elementi di uno stesso vettore affinché possano essere
eseguite le operazioni.

È possibile confrontare oppure uguagliare puntatori, a patto che non puntino allo stesso vettore. Il
confronto più utilizzato è per verificare se il puntatore punta a NULL.

Puntatori di puntatori
Un puntatore può puntare ad un altro puntatore a patto che siano dello stesso tipo di dato. L’unica
eccezione è il puntatore a void in quanto esso è generico e può rappresentare qualsiasi puntatore, mentre
non si può sapere i dati della cosa a cui punta void in quanto tipo di dato generico non definito.

Puntatori a funzione
I puntatori a funzione servono per invocare una funzione. Vengono spesso utilizzati nelle scelte o nei menu
in quanto richiamano determinate funzioni con determinati parametri.

Puntatori, vettori e stringhe


Il nome di un vettore può essere considerato un puntatore costante. Per puntare al primo elemento,
l’assegnamento Puntatore = NomeVettore è lo stesso di Puntatore = &NomeVettore[0].

L’offset (puntare ad un altro elemento del vettore che non sia il primo) può essere rappresentato
equivalentemente come:

1. *(Puntatore + n);
2. &NomeVettore[n];
3. *(NomeVettore + n);
4. Puntatore[n] (a patto che il puntatore assuma il valore del vettore).
Una stringa è un vettore di caratteri che termina con il carattere nullo ‘\0’. Il valore di una stringa
corrisponde all’elemento nella sua prima cella: ne consegue che le stringhe, come i vettori, possono essere
assimilate a puntatori.
Le librerie del linguaggio C
<ctype.h>
È la libreria che permette la manipolazione e il controllo dei caratteri. Ogni suo comando assume i caratteri
come interi e li restituisce come interi. Un intero particolare è EOF (end of file), che corrisponde a −1. Sono
compresi in tale libreria i comandi che permettono di eseguire:

- Riconoscimento di lettere, numeri interi ed esadecimali;


- Riconoscimento di lettere minuscole e maiuscole;
- Conversione da maiuscolo a minuscolo e viceversa;
- Distinzione tra caratteri di spazio bianco, caratteri di controllo e caratteri stampabili che non siano
lettere, numeri oppure spazi.

<stdio.h>
È la libreria che permette l’input e l’output di valori, ma permette anche di manipolare caratteri e stringhe.
Sono compresi in tale libreria i comandi che permettono di:

- Immagazzinare in un vettore anziché visualizzare a schermo;


- Leggere l’input da un vettore anziché da tastiera;
- Leggere un carattere e restituire l’intero corrispondente;
- Immagazzinare caratteri in un vettore fino a quando non si incontra un accapo o un EOF.

<string.h>
È la libreria che permette di eseguire diverse operazioni sulle stringhe. I comandi compresi in tale libreria
permettono di:

- Copiare intere o una parte di (tramite size_t) stringhe in altre stringhe

[char *strcpy (char *s1, char *s2)] [char *strncpy (char *s1, char *s2, size_t n)]

- Unisce intere o una parte di (tramite size_t) stringhe con altre stringhe

[char *strcat (char *s1, char *s2)] [char *strncat (char *s1, char *s2, size_t n)]

- Confrontare intere o una parte di (tramite size_t) stringhe e restituire 0 se le stringhe sono uguali,
un numero negativo se la prima è minore della seconda, un valore positivo se la prima è maggiore
della seconda

[int strcmp (const char *s1, const char *s2)] [int strncmp (const char *s1, const char *s2, size_t n)]

- Trovare un determinato carattere alla sua prima apparizione;


- Determinare la lunghezza di una stringa iniziale che non contiene gli elementi di una seconda
stringa oppure determinare la lunghezza di una stringa che contiene solo gli elementi comuni;
- Trovare un determinato carattere di una stringa alla prima comparsa in un’altra stringa;
- Suddividere la stringa in componenti logici separati da spazi (token) usando come separatori gli
elementi di una seconda stringa;
- Copiare un determinato numero di elementi di un oggetto (blocco di dati) contenuto in una stringa
in un altro oggetto contenuto in un’altra stringa;
- Confrontare un determinato numero di elementi di un oggetto contenuto in una stringa con un
altro oggetto contenuto in un’altra stringa;
- Individuare un determinato carattere nei primi n elementi di un oggetto contenuto in una stringa.
- Copiare un determinato carattere in un determinato numero di elementi di un oggetto di una
stringa;
- Determinare la lunghezza di una stringa;
- Stampare messaggi di errore dipendenti dal sistema ricavati da un numero.
Input e output
L’input e l’output sono eseguiti tramite stream, che sono sequenze di byte. Nell’esecuzione di un
programma gli stream connessi sono 3: quello dell’input (generalmente la tastiera), quello dell’output
(generalmente lo schermo) e quello dello standard error (messaggi di errore, generalmente associati allo
schermo).

Output: la funzione printf


Con printf è possibile regolare la formattazione dell’output (allineamento del testo, inserimento di caratteri,
arrotondamenti da virgola mobile a decimale oppure da virgola mobile ad esponenziale, visualizzazione di
dati secondo dimensioni prefissate).

Il comando è scritto come printf(stringa di controllo, altro). La stringa di controllo è composta da indicatori
di conversione, dimensioni di campo, caratteri letterari e precisioni che vengono precedute dal carattere %.
La seconda parte del comando è opzionale e contiene tutte le variabili indicate nella stringa di controllo.

Gli indici di conversione di printf


Gli indici di conversione utilizzati per stampare un intero sono:

- “%d” oppure “%i”: stampano interi decimali con segno;


- “%o”: stampa interi in base 8 senza segno;
- “%u”: stampa interi decimali senza segno;
- “%x” oppure “%X”: stampano interi in base 16 senza segno. X usa le lettere maiuscole, mentre x le
lettere minuscole;
- “%h” oppure “%l”: sono modificatori di lunghezza che vengono messi prima degli indicatori di
conversione. Corrispondono rispettivamente a short e long.

Gli indici di conversione utilizzati per stampare un numero a virgola mobile sono:

- “%e” oppure “%E”: stampano numeri in notazione esponenziale;


- “%f”: stampano numeri in virgola fissa;
- “%g” oppure “%G”: stampano numeri in virgola fissa e in notazione esponenziale, in base alla
grandezza del valore. Non mostrano gli zeri finali della parte decimale e mostrano
automaticamente i numeri in forma esponenziale se minori di 10−4 oppure maggiori 106 ;
- “%L”: è un modificatore di lunghezza che viene messo prima degli indicatori di conversione e viene
utilizzato per mostrare long double.

Gli indici “%c” ed “%s” servono per stampare rispettivamente i caratteri e le stringhe, ricevendo quindi un
carattere e un puntatore che punterà alla stringa fino a quando non incontrerà il carattere nullo. È
considerato un errore usare “%c” per una stringa e “%s” per un carattere.

Gli ultimi 3 indici di conversione sono:

- “%p”: mostra l’indirizzo di memoria di un puntatore;


- “%n”: memorizza la quantità di caratteri inviati in output. Non stampa niente a schermo;
- “%%”: stampa a schermo il carattere percentuale.

La notazione esponenziale corrisponde alla notazione decimale usata in matematica. Generalmente, i valori
con la virgola vengono stampati con 6 cifre decimali e i valori in notazione esponenziale hanno solo un
numero a sinistra della virgola.
Dimensione di campo
La dimensione del campo è un numero intero, posto tra l’indicatore di conversione e il segno di
percentuale, utilizzato per rappresentare la misura del campo in cui verranno visualizzati i dati. Se i dati da
visualizzare sono di meno rispetto alla dimensione del campo, verranno allineati automaticamente a partire
da destra. Anche il segno meno per i numeri negativi è considerato un dato da allineare. Nel caso in cui i
dati siano di più della dimensione del campo si potrebbero avere degli allineamenti sbagliati.

Precisione
La funzione printf garantisce di rappresentare la precisione di un numero e di una stringa. La precisione è
rappresentata da un punto seguito dall’intero, posto tra la percentuale e l’indice di conversione, che
rappresenta la precisione. I risultati variano a seconda del tipo di dato:

- Per gli interi, la precisione è rappresentata da degli zeri messi prima della cifra;
- Per i numeri decimali, la precisione è rappresentata dal numero di elementi da visualizzare dopo la
virgola;
- Per i numeri in notazione esponenziale, la precisione è rappresentata dai numeri significativi;
- Per le stringhe, la precisione è rappresentata dal numero di caratteri che devono essere stampati.

È possibile allineare a destra e rappresentare un numero con una data precisione mettendo tra
percentuale e indice di conversione prima la dimensione dello spazio e poi la precisione. Un altro modo
è utilizzare gli asterischi al posto delle dimensioni, dichiarandoli sempre in printf, dopo la stringa di
controllo.

I flag
Per allineare in altri modi è possibile usare in printf i cosiddetti flag (o segnalini), segni che devono essere
inseriti subito dopo il segno di percentuale. I flag sono 5:

- “-“: allinea il testo a sinistra;


- “+”: mostra i segni dei numeri positivi;
- “ “: mostra uno spazio per un numero positivo, a patto che non sia stato già utilizzato il flag +;
- “#”: ha diversi significati a seconda dell’utilizzo:
o Visualizza 0 se viene usato un indice di conversione ottale (%o);
o Visualizza 0x se viene usato un indice di conversione esadecimale (%x e %X);
o Forza la visualizzazione di una virgola decimale per tutti i numeri in virgola mobile che non
ce l’hanno.
- “0”: mostra degli zeri prima del numero visualizzato.

I caratteri di escape
Esistono dei caratteri problematici che è difficile rappresentare in printf. Per questo motivo sono state
create delle sequenze di escape, caratterizzate dall’uso di backslash. Esse sono:

- “ \’ ”: mostra gli apici;


- “ \” “: mostra le virgolette;
- “ \? ”: mostra il punto interrogativo;
- “ \\ ”: mostra il backslash;
- “ \a ”: produce un allarme acustico o visivo;
- “ \b ”: sposta il cursore indietro di una posizione in una riga;
- “ \f “: va ad una nuova pagina;
- “ \n “: va accapo;
- “ \r “: ritorna all’inizio della riga corrente;
- “ \t “: tabulazione orizzontale;
- “ \v “: tabulazione verticale.
Input: la funzione scanf
Con scanf è possibile formattare l’input da ricevere e permette di ricevere tutti i tipi di dato, prendere dei
caratteri da uno stream di input oppure ignorarne degli altri. La funzione è definita come scanf(stringa di
controllo, altro), dove nella stringa sono definiti i tipi di carattere da ricevere e in altro sono presenti dei
puntatori a valore alle variabili dove questi devono essere allocati.

Gli indici di conversione di scanf


Gli indici di conversione utilizzati per immettere interi sono:

- “%d”: legge decimali senza segno. È associato ad un puntatore ad intero;


- “%i”: legge decimali, ottali ed esadecimali senza segno. È associato ad un puntatore ad intero;
- “%o”: legge un ottale. È associato ad un puntatore ad intero senza segno;
- “%x” oppure “%X”: legge un esadecimale. È associato ad un puntatore ad intero senza segno;
- “%h” oppure “%l”: va affiancato ad un indicatore di conversione ed associa un puntatore ad intero
di tipo rispettivamente short e long.

Gli indici di conversione utilizzati per immettere numeri in virgola mobile sono;

- “%f”, “%e”, “%E”, “%g” oppure “%G”: legge un valore in virgola mobile. È associato ad un puntatore
ad una variabile in virgola mobile;
- “%l” oppure “%L”: va affiancato ad un indicatore di conversione ed inserisce un valore
rispettivamente double e long double. È associato ad un puntatore ad una variabile double o long
double.

Gli indici di conversione utilizzati per immettere caratteri sono:

- “%c”: legge un carattere. È associato ad un puntatore a carattere e non aggiunge il carattere nullo;
- “%s”: legge una stringa. È associato ad un puntatore a vettore di caratteri abbastanza grande e
aggiunge automaticamente il carattere nullo.
- [gruppo di scansione]: è un vettore che legge un input e considera solo determinati caratteri che
verranno immessi e si ferma al primo elemento che non corrisponde al valore cercato. È possibile
anche non considerare determinati elementi inserendo all’interno delle parentesi quadre, prima
del gruppo di scansione, il carattere ^.

Gli indice di conversione vari sono:

- “%p”: legge l’indirizzo di un puntatore;


- “%n”: immagazzina il numero di dati presi fino a quel momento. È associato ad un puntatore ad
intero;
- “%%”: ignora il segno di percentuale inserita.

Ignorare caratteri
La funzione scanf presenta inoltre la capacità di ignorare dei caratteri tramite l’uso del carattere di
soppressione dell’assegnamento *. Il carattere deve essere messo tra il simbolo percentuale e l’indice di
conversione: tale comando segnalerà a scanf che dovrà leggere ed ignorare un certo carattere immesso.
Le strutture di dati
Le strutture sono dei tipi di dato derivato da variabili (che non sono necessariamente stessi tipi di dato)
correlate da un unico nome, usate come record da salvare nei file. Insieme coi puntatori, con le strutture è
possibile creare strutture più complesse e organizzate come liste concatenate, code, pile ed alberi.

Una struttura viene dichiarata come

Struct Nome

Dichiarazioni;

};

Il nome, opzionale ed abbinato a struct, serve per dichiarare le variabili del tipo di struttura. Le dichiarazioni
all’interno della struttura sono detti membri. I membri di una stessa struttura devono essere univoci,
mentre possono avere nomi simili a membri di altre strutture, non creando conflitti.

Una struttura può contenere diversi tipi di dato, ma non può contenere un’istanza di sé stessa, se non
tramite un puntatore: in tal caso si ha una struttura ricorsiva. È possibile dichiarare la struttura dopo la
definizione, in una riga a parte oppure contestualmente alla definizione stessa.

Operazioni con le strutture


Le uniche operazioni possibili con le strutture sono:

- Assegnamento dello stesso tipo di struttura ad altre strutture;


- Puntare ad una variabile della struttura;
- Accedere ai membri;
- Utilizzare l’operatore sizeof.

Non è possibile confrontare due strutture in quanto i membri di essa non sempre sono immagazzinati in
celle di memoria consecutive: i calcolatori, infatti, immagazzinano i dati entro dei confini di memoria di due
oppure quattro byte detti parola.

L’inizializzazione di una struttura è simile a quella di un vettore, potendo quindi utilizzare delle istruzioni di
assegnamento, degli assegnamenti singoli oppure inizializzandoli durante la loro definizione.

Operatori di strutture
Per accedere ad un membro della struttura è possibile utilizzare sia l’operatore punto “.” (detto operatore
membro a struttura) sia l’operatore freccia “->” (detto operatore puntatore a struttura).

- L’operatore punto accede al membro della struttura tramite il nome della variabile a cui ci interessa
accedere;
- L’operatore freccia accede al membro della struttura dichiarando un puntatore che punta alla
struttura. L’operatore è equivalente a (*puntatore).Nome che utilizza l’operatore membro, le cui
parentesi sono necessarie in quanto l’operatore punto ha una priorità maggiore dell’operatore
riferimento.

Strutture come parametri di funzione


È possibile passare una struttura ad una funzione, sia essa l’intera struttura, un suo membro o un puntatore
ad un qualche membro. Il passaggio sarà automaticamente per valore, quindi i membri non verranno
modificati assolutamente; nel caso in cui si voglia usare il passaggio per riferimento, bisognerà passare
l’indirizzo della struttura.
È poi possibile passare un vettore ad una funzione per valore usando una struttura che ha come membro
tale vettore.

Il comando typedef
typedef è un comando che fornisce un sinonimo ad un tipo di dato definito in precedenza. È utile
soprattutto nella definizione di una struttura. Va precisato che typedef NON CREERÀ UN NUOVO TIPO DI
DATO, ma fornirà UN NUOVO NOME AD UN TIPO DI DATO per rendere il programma auto esplicativo: ciò
significa che è possibile utilizzarlo anche per un tipo di dato fondamentale, migliorandone così la portabilità
su altri sistemi.
I file
I file vengono utilizzati per immagazzinare grandi quantità di dati in modo permanente su memorie
secondarie. Per comprendere al meglio i file è bene pensare che esiste una gerarchia da rispettare:

- Le macchine leggono solo due valori (0 e 1). Un singolo valore è detto bit;
- Un insieme di 8 bit corrisponde ad un byte;
- Ad ogni byte è associato un codice ASCII che coincide con un carattere. L’insieme coincidente con i
caratteri utilizzabili è detto insieme dei caratteri;
- Quando un insieme di caratteri trasmette un’informazione è detto campo;
- L’insieme di diversi campi correlati viene detto record;
- L’insieme di diversi record correlati è detto file;
- L’insieme di diversi file correlati è detto database.

È bene utilizzare una chiave del record, ovvero un campo che aiuta a ricercare un determinato record in un
file. I record possono essere organizzati in modo sequenziale, ovvero mettendo in ordine crescente in base
ad un campo.

Il linguaggio C vede i file come una sequenza di dati, detto stream, terminata da un marcatore end of file
(EOF) oppure da una dimensione prestabilita. Ad ogni file aperto verrà associato uno stream. Ogni volta che
si esegue un programma vengono aperti automaticamente 3 file (e con essi 3 stream): standard input,
standard output e standard error. Ogni file aperto è associato ad un puntatore a FILE che contiene un
vettore che contiene una descrizione di file aperti e a cui ogni elemento associa un blocco di controllo che
amministra un determinato file.

Funzioni per la manipolazione di file


La libreria standard contiene diverse funzioni utilizzabili per manipolare un file, come fgetc() per ottenere
un carattere da un determinato posto, oppure fputc() per inserire un determinato carattere in una
posizione. Ne esistono diverse simili a questi due.

Le funzioni fopen, fclose, fprintf, fscanf, rewind


La funzione fopen permette di aprire un file. Tale funzione riceverà come argomenti il nome del file e il
modo di apertura del file. Il programma verrà eseguito fino a quando il puntatore a FILE non incontrerà
EOF. È possibile aprire più files nello stesso programma, ma è obbligatorio chiuderli dopo la loro apertura
tramite la funzione fclose. I modi per aprire un file sono:

- “w”: creerà un file oppure sovrascriverà un eventuale file già scritto;


- “r”: leggerà un file già scritto;
- “a”: aggiungerà alla fine del file dei record;
- “w+”: creerà un file in lettura e scrittura oppure sovrascriverà un eventuale file già scritto;
- “r+”: leggerà un file in lettura e scrittura;
- “a+”: accoda e scriverà alla chiusura del file.

Un file binario può essere manipolato con dei modi simili a questi, dove basta aggiungere b.

Le funzioni fprintf e fscanf sono simili a printf e scanf, ma agiscono solo sui file ricevendo il puntatore ad
essi.

La funzione rewind riporta il puntatore che legge il file all’inizio.

File ad accesso casuale


Un file ad accesso casuale ha una dimensione fissa e, a differenza di un file ad accesso sequenziale, non
sovrascrive alcun dato presente, potendo modificare ed eliminare i record presenti.
Funzioni per la manipolazione dei file ad accesso casuale
Per creare questo tipo di file bisogna utilizzare la funzione fwrite, che scrive i file partendo dal byte a cui
punta il puntatore al file, mentre fread sposta i byte da una posizione dell’offset ad un’altra che inizia da un
determinato utilizzo.

La funzione è organizzata come fwrite(&partenza, sizeof(tipo dato), NumeroElementiVettore, arrivo), e


analogamente è organizzata fread.

La funzione fseek permetterà di impostare l’offset in una posizione specifica.


Le strutture dinamiche di dati
Le strutture ricorsive si servono dell’allocazione dinamica della memoria e non hanno una dimensione
prestabilita, a differenza dei vettori e delle strutture. Esistono 4 tipi di strutture ricorsive:

- Le liste concatenate, messe in fila indiana, che permettono di eseguire inserimenti e cancellazioni in
qualsiasi posizione della lista;
- Le pile, dove gli inserimenti e le eliminazioni vengono effettuate solo alla testa della struttura;
- Le code, utili per gli elementi in attesa, dove vengono inseriti elementi dalla coda della struttura ed
eliminati dalla testa della struttura;
- Gli alberi, utili per dati ad alta velocità.

Le strutture ricorsive contengono al loro interno vari membri e un puntatore alla struttura stessa: tale
membro, detto link, lega ad un’altra struttura dello stesso tipo dichiarato in quel momento. È necessario
che il puntatore dell’ultima struttura punti a NULL: in caso contrario, possono essere provocati degli errori
durante l’esecuzione.

Le liste concatenate
Una lista concatenata è composta da nodi, ognuno contenente un puntatore al successivo nodo. Per
convenzione, l’ultimo nodo punta a NULL. I nodi possono contenere ogni tipo di dato. È molto più
vantaggiosa di un vettore soprattutto nel caso in cui non si è a conoscenza della quantità di dati da
immettere, visto che è possibile aumentare o diminuire la lunghezza della lista a nostro piacimento. Le liste
tuttavia non hanno un’allocazione continua e quindi l’accesso non è immediato. Poiché i nodi sono collegati
tramite puntatori, per aumentare o diminuire “l’indice” si utilizza l’operatore puntatore a struttura freccia.

Le pile
La pila è anch’essa composta da nodi, ma a differenza della lista, può essere riempita e svuotata solo dalla
testa. Per manipolare una pila si usano le funzioni push (per immettere nuovi elementi) e pop (per estrarre
elementi).

Le code
La coda è composta come una pila, ma viene riempita solo dalla coda e svuotata dalla testa, quindi gli
elementi seguono l’ordine di inserimento.

Gli alberi
Gli alberi non sono strutture lineari come le precedenti: un primo nodo (detto radice) contiene due
puntatori, che puntano a loro volta ad altri nodi (figlio sinistro e figlio destro), che formano dei sottoalberi.
Tutti i figli sono tra loro fratelli e un nodo senza sottoalbero è detto nodo foglia.

Le funzioni malloc, free, sizeof


Per l’allocazione dinamica della memoria è necessario utilizzare le funzioni malloc, free e sizeof.

La prima funzione acquisisce il numero di byte utili per la funzione e restituisce un puntatore a void che
punta alla memoria allocata. È spesso utilizzato insieme a sizeof. La memoria allocata non deve essere
inizializzata. Se non è disponibile memoria verrà restituito NULL.

La funzione free invece libera la memoria che era stata occupata precedentemente da malloc.
Le direttive del preprocessore
Nel preprocessore sono presenti le direttive che verranno utilizzate prima della compilazione del
programma. Tutte le direttive sono indicate col cancelletto # e non può essere presente alcun tipo di
carattere prima, ad eccezione dello spazio.

Le direttive #include
Le direttive #include fanno includere delle copie di un file nel programma. Tali file possono essere indicati
tra virgolette oppure tra parentesi ad angolo: le prime considereranno un file, solitamente definito dal
programmatore, che si trova nella stessa directory del programma, mentre le seconde parentesi
attingeranno ad un file indipendentemente dalla directory, ovvero i file delle librerie standard.

Le direttive #define
Le direttive #define definiscono delle costanti simboliche oppure delle macro. Il formato è #define Simbolo
Costante.

Quando viene definito, il simbolo sostituisce nel programma la costante ed è molto utile se si dovesse
modificare la costante: difatti, basterebbe cambiare solo la costante definita nella riga della direttiva per far
sì che tutti i simboli assumano il valore della costante.

Le macro invece sono vere e proprie operazioni che sono sostituite da un simbolo. La comodità delle macro
è la stessa delle costanti simboliche.

A volte alcune funzioni possono essere definite come macro anziché come funzioni che permette di non
usare una chiamata ad una funzione. Se una macro è troppo lunga bisogna terminare la riga con \.

È possibile oscurare una costante utilizzando la direttiva #undef che “dimenticherà” la definizione fino a
tale direttiva.

Le direttive #if e #endif


Le direttive condizionali #if e #endif servono per controllare le esecuzioni del preprocessore. Esistono
diverse versioni di tale comando ed è spesso utilizzato per debugging dei programmi.

Potrebbero piacerti anche