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:
- 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)
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.
- 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.
- 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.
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.
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:
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 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.
- 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:
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.
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.
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
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à.
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.
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.
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.
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]).
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.
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).
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.
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:
<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:
<string.h>
È la libreria che permette di eseguire diverse operazioni sulle stringhe. I comandi compresi in tale libreria
permettono di:
[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)]
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 utilizzati per stampare un numero a virgola mobile sono:
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.
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:
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:
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.
- “%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 ^.
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.
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.
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.
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.
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.
- 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.
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.