Sei sulla pagina 1di 602

C GUIDA ALLA PROGRAMMAZIONE

Pellegrino Principe
© Apogeo - IF - Idee editoriali Feltrinelli s.r.l.
Socio Unico Giangiacomo Feltrinelli Editore s.r.l.

ISBN edizione cartacea: 9788850333288

Il presente file può essere usato esclusivamente per finalità di carattere personale. Tutti i
contenuti sono protetti dalla Legge sul diritto d’autore.

Nomi e marchi citati nel testo sono generalmente depositati o registrati dalle rispettive case
produttrici.

L’edizione cartacea è in vendita nelle migliori librerie.

Sito web: www.apogeonline.com

Scopri le novità di Apogeo su Facebook

Seguici su Twitter @apogeonline

Rimani aggiornato iscrivendoti alla nostra newsletter


A mia moglie Paola, una donna paziente e sorridente.

A mio figlio Vittorio, un bambino speciale oggi,


un uomo straordinario domani.
Prefazione
Imparare il linguaggio C è come fare viaggio in una terra lontana e sconosciuta; è dunque
sia un viaggio meraviglioso, affascinante e gratificante, ricco di sorprese e scoperte, sia un
viaggio non semplice, che richiede perciò tanta pazienza e il giusto tempo.
In ogni caso, al termine del viaggio, la ricompensa ricevuta sarà elevata: si sarà appreso
non un linguaggio di programmazione qualsiasi ma il linguaggio di programmazione per
eccellenza che già antecedentemente alla sua prima standardizzazione, avvenuta nel lontano
1989 (ANSI C), aveva iniziato a entusiasmare una vasta platea di programmatori.
Come avremo modo di verificare durante tutto il percorso di apprendimento che il libro
intende offrire, C è un linguaggio estremamente espressivo e sintetico che dà grande
“fiducia” e libertà operativa al programmatore, il cui unico limite potrà essere dato solo
dalla poca fantasia o dalla scarsa preparazione sulle regole sintattiche dei costrutti o sulla
semantica delle operazioni.
Dal punto di vista più pratico, e forse meno filosofico, imparare C consente di sviluppare
programmi di uso generale, davvero a 360 gradi, programmi cioè utili per qualsiasi ambito
applicativo (sistemi operativi, robotica, database, networking, grafica e così via).
Esso quindi si dimostra, ancora oggi, dove è presente una pletora di altri linguaggi di
programmazione che promettono di essere più easy e più safe, lo strumento di eccellenza
adoperato da milioni di programmatori che apre le porte del mondo della programmazione
reale, sicuramente più hard ma anche più appagante.
Last but not least, C è un impressionante e imprescindibile strumento didattico usato
soprattutto dalle università più giudiziose per insegnare sia i fondamenti della
programmazione ma anche come è fatto, a “basso livello”, un calcolatore elettronico (si
pensi ai puntatori, la cui disamina non può prescindere da una spiegazione approfondita di
cos’è e come è organizzata la memoria di un elaboratore).
Desideriamo, inoltre, spendere qualche parola sui principi ispiratori che hanno guidato
l’autore nella scrittura del presente testo.
Gli argomenti propri di ogni capitolo hanno un preciso e chiaro ordine. Ogni capitolo
esprimerà compiutamente il relativo obiettivo didattico e non si sovrapporrà o
comprenderà contenuti di altri capitoli. Per esempio, se nel Capitolo 2 si parlerà delle
variabili, solo nel successivo Capitolo 3 si parlerà degli array; allo stesso modo solo
dopo aver trattato anche delle funzioni nel Capitolo 6, si parlerà dei puntatori nel
Capitolo 7 e di tutte le loro relazioni e utilizzi con le variabili, gli array e le funzioni.
Quanto detto può apparire abbastanza ovvio ma non lo è. Infatti oggi si sta assistendo
alla proliferazione di testi con argomenti scritti “a spirale”, dove cioè un argomento
può contenere riferimenti iniziali ad altri argomenti i quali saranno poi trattati
approfonditamente solo nel capitolo di pertinenza. Questo, a parere dell’autore,
laddove non strettamente necessario e comunque non legato ai costrutti del linguaggio
(è evidente che per mostrare il valore di un variabile bisogna dire qualcosa di
propedeutico sull’istruzione printf), induce a distrazioni e fa perdere inutilmente tempo
nella corretta comprensione della corrente unità didattica.
Il modo espositivo seguito è rigoroso, laddove necessario piuttosto formale, e si è
prestata molta attenzione al corretto uso della terminologia propria del linguaggio.
Questo non è un libro del tipo “Impariamo C in 24 ore” oppure “C For Dummies”. I
libri che pretendono di insegnare C in quel modo molto probabilmente sono solo uno
specchio per le allodole; illudono, dando false promesse. C è un linguaggio complesso
e ricco di sfumature; per insegnarlo, ci vogliono “serietà” e il giusto rigore; per
impararlo, ci vogliono pazienza e disciplina.
I listati e gli snippet di codice sono stati pensati in modo che diano una chiara
indicazione pratica dei relativi argomenti teorici; sono piuttosto brevi, autoconclusivi e
supportati da ulteriori commenti che danno, talune volte, ulteriori spiegazione teoriche.
Organizzazione del libro
Il libro è organizzato nei capitoli elencati di seguito.
Capitolo 1, “Introduzione al linguaggio C”: introduciamo il lettore ad alcuni concetti
propedeutici del calcolatore elettronico e dello sviluppo di un programma in C.
Redigiamo anche un primo programma e mostriamo come compilarlo ed eseguirlo.
Capitolo 2, “Variabili, costanti, letterali e tipi”: parliamo delle variabili, delle costanti e
dei tipi di dato fondamentali del linguaggio. Chiudiamo con una trattazione delle
conversioni di tipo e di un confronto tra typedef e #define.
Capitolo 3, “Array”: analizziamo l’importante struttura dati array nella sua forma
monodimensionale e multidimensionale. Parliamo altresì degli array di lunghezza
variabile, degli array costanti e dell’applicazione dell’operatore sizeof per ottenere la
dimensione di un array.
Capitolo 4, “Operatori”: mostriamo tutti gli operatori che il linguaggio mette a
disposizione per manipolare i dati; dai semplici operatori aritmetici a quelli complessi
bitwise. Chiude un’utile tabella di precedenza degli operatori.
Capitolo 5, “Strutture di controllo”: vediamo come gestire il flusso di esecuzione di un
programma attraverso le istruzioni di selezione, di iterazione e di salto.
Capitolo 6, “Funzioni”: trattiamo del fondamentale costrutto di funzione. Vediamo
come essa si dichiara e si definisce una funzione, cosa sono i parametri formali, come
si invoca e cosa sono i parametri attuali o argomenti. Analizziamo in dettaglio
l’importante istruzione return e cos’è la ricorsione.
Capitolo 7, “Puntatori”: parliamo in grande dettaglio dei puntatori e della loro
relazione con gli array. Vediamo anche cosa sono i puntatori a puntatori, i puntatori a
funzione, i puntatori a void e i puntatori nulli. Infine illustriamo in che modo le
keyword const e restrict influenzino un puntatore e la conversione tra puntatori.
Capitolo 8, “Strutture, unioni ed enumerazioni”: descriviamo le strutture, le unioni e le
enumerazioni evidenziando le loro differenze e quali sono i comuni casi di utilizzo.
Capitolo 9, “Dichiarazioni”: approfondiamo i concetti legati alle variabili, ai blocchi,
allo scope e al linkage. Diamo uno sguardo di insieme agli specificatori della classe di
memorizzazione, ai qualificatori di tipo, agli specificatori di tipo, agli specificatori di
funzione e agli specificatori di allineamento.
Capitolo 10, “Il preprocessore”: parliamo di una delle peculiarità di C, ossia il suo
preprocessore, indicando tutte le direttive che mette a disposizione, dalla più comune
#define a quella più esoterica #pragma.
Capitolo 11, “La libreria standard”: analizziamo tutte le funzionalità della libreria
standard del linguaggio attraverso un excursus completo di tutti i suoi header, anche
qui da quelli più usati come <string.h>, <stdio.h> e <stdlib.h> a quelli più particolari
come <fenv.h>, <locale.h> e <signal.h>.
Appendice A, “Installazione e utilizzo di GCC”: mostriamo come installare la suite di
compilazione GCC e come effettuare le comuni operazioni di compilazione, linking e
dubugging del codice.
Appendice B, “Installazione e utilizzo di NetBeans”: trattiamo di NetBeans, un potente
IDE multipiattaforma che consente di creare, compilare ed eseguire codice C in un
ambiente di facile utilizzo e ricco di funzionalità.
Appendice C, “Sistemi numerici: cenni”: forniamo un’introduzione ai sistemi numerici
decimale, ottale, esadecimale e binario e a come eseguire le conversioni tra di essi.
Mostriamo anche come effettuare le operazioni aritmetiche binarie di addizione,
sottrazione, moltiplicazione e divisione.
Struttura del libro e convenzioni
Gli argomenti del libro sono, ovviamente, organizzati in capitoli. Ogni capitolo è
numerato in ordine progressivo e denominato significativamente nel suo obiettivo didattico
(per esempio, Capitolo 2, “Variabili, costanti, letterali e tipi”). I capitoli sono poi suddivisi
in paragrafi di pertinenza.
All’interno dei paragrafi possiamo avere dei blocchi di testo o di grafica, a supporto alla
teoria, denominati, per esempio, come segue:
Listato NrCapitolo.NrProgressivo Descrizione... per i listati del codice sorgente;
Snippet NrCapitolo.NrProgressivo Descrizione... per un frammento di codice sorgente;
Sintassi NrCapitolo.NrProgressivo Descrizione... per la sintassi di un costrutto del
linguaggio;
Shell NrCapitolo.NrProgressivo Descrizione... per un comando di shell;
Output NrCapitolo.NrProgressivo Descrizione... per l’output di un programma;
Figura NrCapitolo.NrProgressivo Descrizione... per una figura;
Tabella NrCapitolo.NrProgressivo Descrizione... per una tabella.
Per esempio, il blocco denominato “Listato 6.10 VariableArgumentsList.c
(VariableArgumentsList)” indica il listato di codice numero 10 del Capitolo 6 avente come
descrizione il nome del file .c di codice sorgente e tra parentesi il nome del progetto
NetBeans.
Per quanto attiene ai listati, abbiamo adottato la seguente convenzione: i puntini di
sospensione (…) eventualmente presenti indicano che in quel punto sono state omesse alcune
parti del listato. Ovviamente, le medesime parti sono presenti nei relativi file .c allegati al
libro. Gli stessi caratteri possono talvolta trovarsi anche negli output di un programma
eccessivamente lungo.
Codice sorgente e progetti
All’indirizzo http://www.apogeonline.com/libri/9788850333288/scheda è possibile scaricare un
archivio ZIP che contiene tante cartelle quanti sono i capitoli del libro. Ciascuna cartella,
denominata Cap01, Cap02 e così via, ha le seguenti cartelle strutturate e denominate come
segue.
Listati
CON_NetBeans
— Linux
• [nome cartella del progetto; es. PrimoProgramma]
• ...
— Windows
• [nome cartella del progetto; es. PrimoProgramma]
• ...
SENZA_NetBeans
• [nome file .c del listato; es. PrimoProgramma.c]
• ...

Snippet
CON_NetBeans
— Linux
• [nome cartella del progetto; es. 2.1]
• ...
— Windows
• [nome cartella del progetto; es. 2.1]
• ...
SENZA_NetBeans
• [nome file .c dello snippet; es. 2.1.c]
• ...

All’interno, dunque, della cartella SENZA_NetBeans sono presenti, in modo indipendente,


tutti i file sorgente .c del capitolo di pertinenza con le eventuali risorse complementari; nella
cartella CON_NetBeans sono invece presenti, sia per Linux sia per Windows, delle sottocartelle
direttamente correlate a specifici progetti caricabili con l’IDE NetBeans.
Compilare ed eseguire direttamente i listati e
gli snippet di codice
Per rendere agevole la compilazione e l’esecuzione dei listati e gli snippet di codice,
indipendentemente dall’IDE NetBeans, riteniamo utili i consigli riportati di seguito.
Se si utilizza Windows, creare le seguenti strutture di directory:
per i sorgenti, C:\MY_C_SOURCES;
per gli eseguibili, C:\MY_C_BINARIES;
per i file oggetto, C:\MY_C_OBJECTS;
per i file include, C:\MY_C_INCLUDE;
per i file delle librerie statiche, C:\MY_C_STATIC_LIBRARIES;
per i file delle librerie dinamiche, C:\MY_C_SHARED_LIBRARIES;
per altri file, C:\MY_C_FILES.

Se si utilizza GNU/Linux, creare le seguenti strutture di directory, dove $HOME rappresenta


la propria home directory (per esempio /home/thp):

per i sorgenti, $HOME/MY_C_SOURCES;


per gli eseguibili, $HOME/MY_C_BINARIES;
per i file oggetto, $HOME/MY_C_OBJECTS;
per i file include, $HOME/MY_C_INCLUDE;
per i file delle librerie statiche, $HOME/MY_C_STATIC_LIBRARIES;
per i file delle librerie dinamiche, $HOME/MY_C_SHARED_LIBRARIES;
per altri file, $HOME/MY_C_FILES.

Leggere altresì e in via preliminare l’Appendice A.


Compilare ed eseguire con NetBeans i listati e
gli snippet di codice
Se lo si desidera, è possibile utilizzare l’IDE NetBeans per editare, compilare, eseguire e
“debuggare” il codice sorgente presente nel libro. A tal fine consultare l’Appendice B, per
una spiegazione in merito all’uso introduttivo di tale IDE e alla creazione e all’impiego dei
progetti.
NOTA
I progetti NetBeans degli snippet di codice hanno delle opzioni di compilazione “pedanti” in
modo che il lettore possa essere avvisato su cosa riporta il compilatore durante la fase di
compilazione. È anche importante avvisare che se da Windows si spostano le cartelle dei
progetti NetBeans in GNU/Linux, sarà apposto in automatico su un file denominato .dep.inc,
presente nella root di ogni progetto, l’attributo “Sola lettura”, il quale darà problemi di
compilazione sotto Windows se le stesse cartelle saranno spostate nuovamente su sistemi
Microsoft (o se saranno spostate direttamente da GNU/Linux in caso sia avvenuta qui una
loro prima copia). Lo stesso problema, comunque, non sarà mai rilevato in GNU/Linux.
Capitolo 1
Introduzione al linguaggio C

Il linguaggio C è ancora oggi, nonostante siano passati all’incirca quarant’anni dalla sua
prima apparizione, uno straordinario linguaggio di programmazione; ricco, espressivo,
potente e flessibile. Esso ha influenzato moltissimi altri linguaggi di programmazione che
hanno preso in “prestito” molti aspetti della sua filosofia, della sua sintassi e dei suoi
costrutti principali. Tra questi è sufficiente citare alcuni dei maggiori linguaggi mainstream
come Java, C# e, naturalmente, C++ che, per certi versi è un C “potenziato” con l’astrazione
della programmazione orientata agli oggetti e della programmazione generica attraverso il
meccanismo dei template.
NOTA
Anche se C++ è spesso definito come un superset del linguaggio C, è comunque un
linguaggio “diverso” che ha sia delle caratteristiche aggiuntive (per esempio, fornisce dei
costrutti propri della programmazione a oggetti) sia, per alcuni costrutti, delle regole diverse
(per esempio, non consente di assegnare un puntatore a void a un puntatore a un altro tipo
senza un opportuno cast). Tuttavia, imparare C permetterà di padroneggiare C++ almeno
nei suoi aspetti essenziali e nei suoi costrutti basici; infatti, con la dovuta attenzione e senza
l’utilizzo delle astrazioni proprie di C++, un programma scritto in C potrà compilarsi senza
problemi con un compilatore C++.

Allo stesso tempo, però, tali linguaggi hanno anche “eliminato” alcuni aspetti di C che
sono complessi, a basso livello e richiedono una certa attenzione e conoscenza come per
esempio i puntatori che sono, in breve, un potente meccanismo attraverso il quale è
possibile accedere in modo diretto alla memoria per leggerla e manipolarla.
In buona sostanza linguaggi come Java, C# e così via, se è vero che da un lato hanno reso
la scrittura del software più “controllata” e “sicura” è anche vero che, dall’altro lato, l’hanno
resa maggiormente vincolata e meno permissiva in termini di libertà operativa ed
espressiva. In definitiva, il programmatore ha sì meno margini di errore, ma ha anche più
lacci e vincoli.
In ogni caso, giova da subito dire che è proprio la totale libertà di azione offerta dal
linguaggio C che ne rappresenta la sua grande forza e, perché no, lo straordinario fascino
che continua a riscuotere sui programmatori sia alle prime armi sia in quelli più esperti. In
sostanza C dà grande responsabilità al programmatore e assume che questi sappia cosa sta
facendo e sia consapevole degli effetti delle sue azioni.
C, come detto, malgrado l’avanzata di altri linguaggi di programmazione, è, in modo
netto e assoluto, uno straordinario e moderno strumento di programmazione con il quale è
possibile scrivere programmi per i più svariati dispositivi hardware, da quelli altamente
performanti e multiprocessore a quelli embedded dove, in taluni casi, le risorse di memoria
e la potenza della CPU sono limitate.
TERMINOLOGIA
Per sistema embedded si intende un sistema elettronico che ha del software e
dell’hardware “incorporati” deputati a eseguire in modo esclusivo un task o una serie di task
per i quali il sistema è stato ideato e progettato. In pratica, è pensabile come un sistema
hardware/software dedicato e non general-purpose come i comuni PC, che può essere
indipendente oppure parte di un sistema più grande. Un sistema embedded ha,
generalmente, i seguenti principali componenti incorporati: dell’hardware simile a quello di
un computer; il software dell’applicazione principale; un sistema operativo real time (RTOS).
In più si caratterizza anche per dei limiti o vincoli che possono influire sulla scelta della
progettazione e del design, quali la quantità di memoria e la potenza del processore
disponibili, le dimensioni richieste, i costi di produzione e così via. I sistemi embedded
trovano applicazione in svariati settori, come quelli delle telecomunicazioni e del networking
(router, firewall...), della medicina, dei satelliti, delle automobili (motor control system, cruise
control...), dell’elettronica digitale di consumo (set top box, fotocamere digitali, lettori MP3...)
e via dicendo.

C è, in definitiva, un importante e potente linguaggio di programmazione e ciò lo


dimostra anche il fatto che la sua conoscenza è ancora uno skill molto richiesto nel mondo
del lavoro e che la sua popolarità è ancora ai primi posti, come dimostrato, per esempio, dal
famoso indicatore di popolarità TIOBE (Figura 1.1) aggiornato a gennaio del 2015.
DETTAGLIO
Il TIOBE Programming Community index è un indicatore che misura la popolarità di un
linguaggio di programmazione Turing completo. Esso è aggiornato mensilmente in base al
numero dei risultati delle ricerche effettuate con 25 search engine di una query avente il
seguente pattern: +"<language> programming".
Figura 1.1 Tabella risultati TIOBE.
Storia della nascita di un “mito”
Il linguaggio C, come oggi lo conosciamo, è frutto di un’affascinante storia che inizia
diversi decenni or sono durante uno startup temporale (siamo all’incirca negli anni Settanta)
di grandi innovazioni tecniche soprattutto nel campo dei computer (per esempio, il 15
novembre del 1971 Intel svela la nascita del primo microprocessore, con ciò sancendo il
passaggio dalla terza generazione di computer, quella dei mainframe e dei minicomputer,
alla quarta generazione di computer, quella dei microcomputer e dei personal computer), dei
sistemi operativi (per esempio, nel 1971 nasce la First Edition di Unix) e del networking
(per esempio, nel 1973 furono attivate la prime due connessioni internazionali di
ARPANET, progenitrice di quella che sarebbe poi diventata Internet, rispettivamente in
Inghilterra e in Norvegia).
Tutto iniziò verso la fine del 1960, quando presso i Bell Telephone Laboratories, famoso
centro di ricerca e sviluppo operante nel settore dell’information technology dove sono state
sviluppate tante straordinarie tecnologie (transistor, laser, il sistema operativo Unix e così
via), decisero di abbandonare il progetto del sistema operativo in time-sharing denominato
Multics (Multiplexed Information and Computing Service), avviato congiuntamente al MIT
(Massachusetts Institute of Technology) e a GE (General Electric), ritenendo che le idee da
implementare non fossero praticabili in tempi relativamente brevi, oltre a essere piuttosto
dispendiose in termini economici.
Al progetto del Multics, tra gli altri, lavorava un giovane scienziato informatico, Kenneth
Thompson, il quale, dopo l’abbandono del progetto medesimo, iniziò a nutrire ambizioni
per la creazione di un nuovo sistema operativo che avrebbe dovuto incorporare gli aspetti
più innovativi di Multics e, nel contempo, eliminare quelli più problematici.
Iniziò, così, nel 1968, su un mainframe GE-635 e su un minicomputer DEC PDP-7, lo
sviluppo, in linguaggio assembly, del sistema operativo che sarebbe divenuto Unix e che
avrebbe visto poi la partecipazione anche di straordinari hacker del codice quali Dennis
MacAlistair Ritchie, Malcolm Douglas McIlroy, Joe Ossanna e Rudd Canaday.
Nel 1969, si ebbe il first run di Unics, ortografia del nome scelto in origine prima di Unix
su suggerimento di Brian Wilson Kernighan e come gioco di parole rispetto al sistema
Multics (Multiplexed Information and Computing Service). Unics era l’acronimo di
UNiplexed Information and Computing Service.
Ken Thompson aveva, tuttavia, anche un’altra ambizione: desiderava infatti creare un
linguaggio di programmazione ad alto livello per quello che poi si sarebbe chiamato Unix,
sollecitato anche dagli sforzi di altri sviluppatori come Douglas McIlroy, che aveva
partecipato alla scrittura di un compilatore per il linguaggio PL/I (Programming Language
One) per il sistema Multics.
Sicché ideò e sviluppò un nuovo linguaggio di programmazione denominato B, basato a
sua volta sul linguaggio di programmazione BCPL (Basic Combined Programming
Language) ideato nel 1966 da Martin Richards, e il cui nome, secondo altre ipotesi,
potrebbe non essere una contrazione di BCPL bensì una derivazione di un altro linguaggio
di programmazione, Bon, creato dallo stesso Thompson durante la sua collaborazione al
progetto Multics.
Dopo lo sviluppo del linguaggio B, sul sistema Unix del PDP-7, furono scritte con esso
solo poche utility e fu scartata l’idea di riscrivere Unix stesso in B a causa della scarsa
potenza ed efficienza computazionale del PDP-7.
Si arriva così al 1970, quando i Bell Labs acquistarono un minicomputer a 16 bit DEC
PDP-11 per il progetto Unix, sicuramente più potente e avanzato del PDP-7, che fu subito
impiegato da Thompson per riscrivere, sempre in assembly, il kernel Unix e altre utility di
base. Sullo stesso PDP-11 fu poi effettuato anche il porting del linguaggio B.
Tuttavia, sul PDP-11, il linguaggio B presentava delle problematiche e inadeguatezze
dovute a scelte progettuali e di design che erano anche strettamente legate all’hardware del
PDP-7. Infatti, prima di tutto B era typeless o, per meglio dire, aveva un solo tipo di dato
definito come word o cell e si adattava bene per il PDP-7, che era una macchina word-
addressable. Il PDP-11, di converso, era una macchina byte-addressable, e ciò causava un
overhead con i puntatori poiché per B un puntatore era una variabile che poteva contenere
un indirizzo di memoria rispetto a un array di indirizzi di memoria disposti secondo un
ordine di tipo word addressing; pertanto a run-time un puntatore doveva essere “convertito”
per contenere il byte address atteso dall’hardware del PDP-11.

Figura 1.2 Differenza tra byte addressing e word addressing.


In secondo luogo, il linguaggio B era lento: un sorgente scritto con esso, infatti, non
produceva direttamente codice eseguibile; da questo punto di vista B era un linguaggio
interpretato che veniva “tradotto” a run-time.
Interviene così, nel 1971, Dennis Ritchie, il quale iniziò a lavorare sul linguaggio B,
estendendolo con l’aggiunta dei tipi int e char, con gli array e i puntatori a quei tipi. In più i
valori dei puntatori contenevano ora indirizzi di memoria misurati in byte, e l’accesso per
indirezione al valore della cella di memoria referenziata non implicava alcun overhead per
scalare il puntatore da un word offset a un byte offset.
Produsse inoltre un compilatore in grado di generare direttamente codice in linguaggio
macchina nativo del PDP-11, rendendo così i programmi prodotti efficienti, compatti e
veloci quasi quanto quelli prodotti con il linguaggio assembly.
Inizialmente decise di denominare questa versione estesa di B come NB, che stava per
New B; successivamente, quando il type system e il compilatore furono pronti, scelse di
cambiarne il nome in C (la motivazione di questa scelta di denominazione è stata lasciata
dallo stesso Ritchie ambigua: egli, infatti, non ha mai specificato se tale singola lettera è una
semplice progressione alfabetica rispetto a B oppure rispetto a BCPL).
Tra il 1972 e il 1973 vi furono altre importanti aggiunte sia strettamente legate al
linguaggio C, come il preprocessore e le strutture, sia dal punto di vista delle librerie, come
la scrittura da parte di Michael Lesk di una serie di funzionalità di I/O portabili.
In più, durante questo periodo, accaddero altre due cose di notevole importanza: la prima,
riferita alla riscrittura in C del sistema Unix sul PDP-11 (nell’estate del 1973); la seconda
riferita al porting di C su altri sistemi come l’Honeywell 635 e l’IBM 360/370.
L’importanza del primo aspetto si commenta da sola: nasceva, infatti, l’edizione di Unix
che avrebbe cambiato la storia dei sistemi operativi e lo avrebbe reso “immortale”, e ciò
soprattutto grazie alla sua stretta integrazione con il linguaggio C. Il secondo aspetto è
altresì importante perché, di fatto, apriva la possibilità di estendere Unix su altri sistemi che
non fossero solo i PDP-11 estendendo la penetrazione sia di tale sistema operativo sia del
linguaggio C. Esempi di quanto detto si hanno, quando, all’incirca tra il 1977 e il 1978,
Ritchie, Thompson e Stephen Curtis Johnson, quest’ultimo autore dell’importante
compilatore portabile pcc (portable C compiler), iniziarono il porting di Unix sul
minicomputer Interdata 8/32 e poi Tom London e John Reiser su un sistema DEC VAX
11/780.
Tra il 1973 e il 1980 il linguaggio C ottenne altre importanti aggiunte, come il tipo long,
le union, le enumerazioni e così via, mentre nel 1978 uscì The C Programming Language, il
primo libro su C (detto il K&R o il “White Book”), a opera di Brian Kernighan e Ritchie
stesso, che divenne una sorta di guida al linguaggio utilizzata come standard de facto per gli
implementatori dei compilatori. Questo testo fondamentale, tuttavia, portò alla luce anche
dei rilevanti problemi legati a incompatibilità tra i compilatori su alcune caratteristiche che
erano state scarsamente dettagliate. A questo si aggiunse il fatto che dopo la pubblicazione
ci furono modifiche al linguaggio che il testo non descriveva; pertanto si iniziò ad avvertire
l’esigenza di formalizzare C con uno standard ufficiale che desse conto della crescente
importanza del linguaggio.
Nel 1983 l’ANSI (American National Standards Institute), un’organizzazione che
produce standard industriali per gli Stati Uniti, istituì il comitato X3J11 con lo scopo di
produrre uno standard per C con questo preciso e ambizioso obiettivo:
to develop a clear, consistent, and unambiguous Standard for the C programming language which
codifies the common, existing definition of C and which promotes the portability of user programs
across C language environments.

I lavori sullo standard terminarono nel dicembre del 1989 e produssero la specifica
X3.159-1989, riferita anche come C89 o ANSI C, che standardizzava, per l’appunto, sia il
core del linguaggio C sia una serie di funzionalità o API (Application Programming
Interface) che andavano a costituire il core di una libreria di base.
Questo standard fu poi, nel 1990, approvato e ratificato dall’ISO (International
Organization for Standardization) un’organizzazione indipendente non governativa,
composta da membri provenienti da oltre 160 paesi, che produce standard internazionali per
svariati settori (agricoltura, informatica, sanità e così via). Il documento di specifica
prodotto dall’ISO fu denominato ISO/IEC 9899:1990 (è riferito anche come C90 o ISO C, e
rispetto alla specifica C89 non ha alcuna differenza se non per la diversa formattazione).
Successivamente, l’ISO diffuse documenti con correzioni ed emendamenti denominati
rispettivamente ISO/IEC 9899/COR1:1994, ISO/IEC 9899/AMD1:1995 (questo riferito
anche come C95) e ISO/IEC 9899/COR2:1996, cui seguì nel 1999 la pubblicazione di un
nuovo documento di specifica del linguaggio C, denominato ISO/IEC 9899:1999 e riferito
come C99, che introdusse molte importanti aggiunte al linguaggio sia nel core (variable-
length arrays, flexible array members, restricted pointers, compound literals, designated
initializers, inline functions, extended identifiers e così via) sia nella libreria standard
(header <complex.h>, <stdbool.h>, <tgmath.h> e così via).
Seguirono, quindi, negli anni successivi i documenti correttivi ISO/IEC 9899:1999/Cor
1:2001, ISO/IEC 9899:1999/Cor 2:2004 e ISO/IEC 9899:1999/Cor 3:2007, che
culminarono nella creazione del nuovo e attuale standard denominato ISO/IEC 9899:2011 e
riferito come C11, che ha apportato ulteriori migliorie al linguaggio (anonymous structures,
anonymous unions, type-generic expressions e così via) e alla libreria standard attraverso,
soprattutto, gli header <stdatomic.h> e <threads.h> per il supporto alla programmazione
concorrente.

C99 E C11
Lo standard C99 ha indubbiamente introdotto innumerevoli modifiche al linguaggio C che
l’hanno migliorato in molti aspetti e, per certi versi, l’hanno “avvicinato” a C++ con l’aggiunta di
caratteristiche come le funzioni inline, i commenti a singola riga tramite i caratteri //, le
dichiarazioni di variabili inseribili in qualsiasi punto di un blocco di codice e così via. Tuttavia, il
supporto da parte dei vendor dei compilatori alle nuove caratteristiche è stato molto lento, e a
oggi vi sono ancora alcuni di essi che le hanno implementate solo parzialmente. Ecco,
dunque, che nel documento di specifica C11, per rendere meno difficoltoso l’adozione di tale
standard, sono state esplicitate alcune caratteristiche del linguaggio che i vendor dei
compilatori possono implementare solo se lo desiderano o se lo ritengono essenziale per il
loro ambiente target (in pratica, il supporto ad alcune caratteristiche come, per esempio,
quelle relativa al multithreading o ai numeri complessi sono opzionali). In ogni caso devono
essere implementate delle macro come __STDC_NO_VLA__, __STDC_NO_COMPLEX__, __STDC_NO_THREADS__
e così via, che consentono di verificare se quella particolare feature è supportata o meno. Per
esempio, se la macro __STDC_NO_THREADS__ ha come valore 1, ciò indicherà che l’attuale
compilatore non supporta l’header <threads.h> e, dunque, la programmazione concorrente.
Caratteristiche
C, come in più parti già evidenziato, è uno straordinario e moderno linguaggio di
programmazione contraddistinto dalle seguenti caratteristiche.
Efficienza. Come in precedenza detto, C è stato ideato, tra le altre cose, anche per
soppiantare l’assembly come linguaggio di programmazione a basso livello per lo
sviluppo del sistema operativo Unix. Esso, infatti, tende a produrre codice compatto e
con elevata velocità di esecuzione, quasi quanto quella di codice prodotto da un
assemblatore, che ben si adatta al sistema hardware per il quale il relativo compilatore
è stato progettato e implementato.
Portabilità. C fa della portabilità uno dei suoi massimi punti di forza. Il linguaggio è
portabile perché un sorgente scritto secondo lo standard C e con la libreria di funzioni
standard può essere compilato senza particolari problemi su altre piattaforme hardware
che hanno il relativo compilatore. C, infatti, si è talmente diffuso che è possibile
trovare compilatori per i più svariati dispositivi hardware, dai sistemi embedded con
poca memoria e poca capacità di calcolo ai sofisticati e potenti supercomputer.
Potenza. C è tra i pochi linguaggi di programmazione che consentono di manifestare
una reale potenza, sia espressiva, grazie a pochi e mirati costrutti sintattici, sia
algoritmica, grazie alla presenza di determinati operatori che permettono l’accesso e la
manipolazione dei singoli bit della memoria.
Flessibilità. A oggi non esiste praticamente alcun dominio applicativo che C non possa
coprire. Esso è, infatti, utilizzato per sviluppare qualsiasi tipologia di software, come
compilatori, sistemi operativi, driver hardware, motori per grafica 3D, editor di testi,
player musicali, videogame e così via. Da questo punto di vista C si è “evoluto” da
mero linguaggio di programmazione di sistema qual era ab origine a vero e proprio
linguaggio di programmazione general-purpose.
Permissività. Rispetto ad altri linguaggi di programmazione, C è stato pensato per
essere un linguaggio per programmatori e pertanto lascia agli stessi una grande libertà
di costruzione dei programmi con gli strumenti offerti (si pensi ai puntatori) senza
fornire, quindi, soprattutto in fase di compilazione, una cattura e un rilevamento di
determinati tipi di errori. In pratica C dà piena fiducia e grande responsabilità al
programmatore; presuppone che lo stesso, se sta facendo una cosa, sappia a cosa va
incontro e sia in grado di valutarne gli effetti.

NOTA
I compilatori moderni sono oggi in grado di effettuare un check del sorgente alla ricerca sia
dei comuni errori in violazione delle regole sintattiche sia di altri tipi di errori, evidenziati
tramite dei messaggi di diagnostica o warning, che nonostante non producano una mancata
compilazione del sorgente potrebbero essere fonte di problemi durante l’esecuzione del
programma. Per esempio, con GCC possiamo usare l’opzione -Warray-bounds per ottenere un
warning se accediamo a un indice di un array fuori dal suoi limiti, -Waddress per ottenere un
warning per utilizzi sospetti degli indirizzi di memoria, -Wreturn-type per ottenere un warning
se, per esempio, in una funzione con un tipo di ritorno non void si scrive un’istruzione di
return senza un valore di ritorno o si omette di scriverla e così via per altri warning.

Tra le altre caratteristiche di C, possiamo altresì evidenziare che esso è considerabile


come un linguaggio di programmazione di medio livello perché mette a disposizione del
programmatore sia facility di alto livello (si pensi al costrutto di funzione o ad altre
astrazioni come il costrutto di struttura), che garantiscono una maggiore efficienza e
flessibilità nella costruzione dei programmi, sia facility di basso livello (si pensi al costrutto
di puntatore) che garantiscono, al pari dei linguaggi machine-oriented come l’assembly,
una maggiore efficienza nello sfruttamento diretto dell’hardware sottostante.
TERMINOLOGIA
Un linguaggio di programmazione è definibile di alto livello se offre un alto livello di
astrazione e indipendenza rispetto ai dettagli hardware di un elaboratore. Ciò implica che un
programmatore utilizzerà keyword e costrutti sintattici di facile comprensione che gli
permetteranno di scrivere il programma in modo relativamente semplificato con la possibilità
di concentrarsi solo sulla logica dell’algoritmo da implementare. Per questo sono definibili
come linguaggi closer to humans. Di converso, un linguaggio di programmazione è
definibile di basso livello quando non offre alcun layer di intermediazione/astrazione rispetto
all’hardware da programmare, e il programmatore deve non solo avere una profonda
conoscenza di tale hardware ma deve anche lavorare direttamente con esso (registri,
memoria e così via). Per questo sono definibili come linguaggi closer to computers.

Infine, C è un linguaggio ricco di funzionalità, ovvero ha un set di API predefinite,


fornite tramite la sua libreria standard, che vanno dalla gestione dell’input/output e delle
stringhe alla gestione della memoria, delle date e degli orari, degli errori e così via.
Cenni sull’architettura di un elaboratore
Prima di addentrarci nello studio del linguaggio C appare opportuno delineare alcuni
concetti teorici che riguardano la struttura di un generico calcolatore elettronico,
soffermandoci in modo più approfondito su due componenti in particolare, ossia la CPU e la
memoria centrale. Ciò si rende necessario perché C consente, tra le altre cose, di sfruttare a
basso livello l’hardware di un sistema target. Pertanto, comprendere, seppure a grandi linee,
come un computer è progettato e costituito può sicuramente aiutare a scrivere programmi
che dialogano con l’hardware sottostante con maggiore consapevolezza dei loro effetti
(capire, per esempio, come è strutturata la memoria centrale può aiutare a gestirla in modo
più sicuro ed efficiente).

Il modello di von Neumann


I linguaggi imperativi, di cui C è un esponente, come vedremo meglio poi, condividono
un modello computazionale che rappresenta un’astrazione del sottostante calcolatore
elettronico dove, in breve, la computazione procede modificando valori memorizzati in
locazioni di memoria. Questo modello è definito come von Neumann architecture dal nome
dello scienziato ungherese John von Neumann che, nel 1945, lo ideò, e fu, ed è ancora, alla
base del design e della costruzione dei computer e quindi dei linguaggi imperativi che vi si
rifanno e che sono delle astrazioni della macchina di von Neumann. Nella sostanza, nel
modello di von Neumann (Figura 1.3) un computer è costituito da una CPU (Central
Processing Unit) per il controllo e l’esecuzione dell’elaborazione, al cui interno si trovano
l’ALU (Arithmetic Logic Unit) e una Control Unit; da celle di memoria identificate da un
indirizzo numerico atte a ospitare i dati coinvolti nell’elaborazione; da dispositivi per l’input
e l’output dei dati da e verso l’elaboratore; da un bus di comunicazione tra le varie parti per
il transito di dati, indirizzi e segnali di controllo. In più, sia i dati sia le istruzioni di
programmazione sono memorizzati nella memoria. In pratica, nel modello di von Neumann
abbiamo due elementi caratterizzanti: la memoria, che memorizza le informazioni anzidette,
e il processore, che fornisce operazioni per modificarne il contenuto, ossia lo stato.
In definitiva, un computer digitale modellato secondo l’architettura di von Neumann non
è altro che un sistema di componenti quali processori, memorie, device di input/output e
così via tra di loro interconnessi (bus oriented) che cooperano congiuntamente al fine di
svolgere i processi computazionali per i quali sono stati progettati.
Figura 1.3 Architettura semplificata di un computer basata sul modello di von Neumann.

La CPU
La CPU (Central Processing Unit) è il “cervello” di ogni computer ed è deputata
principalmente a interpretare ed eseguire le istruzioni elementari che rappresentano i
programmi e a effettuare operazioni di coordinamento tra le varie parti di un computer.
Questa unità di processing, dal punto di vista fisico, è un circuito elettronico formato da
un elevato numero di transistor (varia da diverse centinaia di milioni fino ai miliardi delle
più potenti attuali CPU) che sono ubicati in un circuito integrato (chip) di ridotte dimensioni
(pochi centimetri quadrati).
Dal punto di vista logico, invece, una CPU è formata dalle seguenti unità (Figura 1.4).
ALU (Arithmetic Logic Unit), ossia l’unità aritmetico-logica. Quest’unità è costituita
da un insieme di circuiti deputati a effettuare calcoli aritmetici (addizioni, sottrazioni e
così via) e operazioni logiche (boolean AND, boolean OR e così via) sui dati. Il suo
funzionamento si può così sintetizzare ponendo, per esempio, un’operazione di somma
tra due numeri: preleva da appositi registri di memoria di input gli operandi trasmessi
(diciamo X e Y); effettua l’operazione di addizione (X + Y); scrive il risultato della
somma in un registro di memoria di output. In pratica possiamo vedere una ALU come
una sorta di calcolatrice “primitiva” che riceve dati sui quali effettuare calcoli al fine di
produrre un risultato.
Control Unit, ossia l’unità di controllo. Questa unità coordina e dirige le varie parti di
un calcolatore in modo da consentire la corretta esecuzione dei programmi. In pratica
essa, in una sorta di ciclo infinito, preleva (fetch), decodifica (decode) ed esegue
(execute) le istruzioni dei programmi. Attraverso la fase di prelievo acquisisce
un’istruzione dalla memoria, la carica nel registro istruzione e rileva la successiva
istruzione da prelevare. Attraverso la fase di decodifica interpreta l’istruzione corrente
da eseguire. Attraverso la fase di esecuzione esegue ciò che l’istruzione indica: è
un’azione di input? Allora comanda l’unità di input relativa al trasferimento dei dati
nella memoria centrale. È un’azione di output? Allora comanda l’unità di output
relativa al trasferimento dei dati dalla memoria centrale verso l’esterno. È un’azione di
processing dei dati? Allora comanda il trasferimento dei dati nell’ALU, comanda
l’ALU alla loro elaborazione e al trasferimento del risultato nella memoria centrale. È
un’azione di salto? Allora aggiorna il registro contatore di programma con l’indirizzo
cui saltare. Infine, le operazioni svolte dall’unità di controllo sono regolate da un
orologio interno di sistema (system clock) che genera segnali o impulsi regolari a una
certa frequenza, espressa in Hertz, che consentono alle varie parti di operare in modo
coordinato e sincronizzato. Maggiori sono questi segnali per secondo, detti anche cicli
di clock, maggiore è la quantità di istruzioni per secondo che una CPU può processare
e quindi la sua velocità.
Registers, ossia i registri. I registri sono unità di memoria, estremamente veloci
(possono essere letti e scritti ad alta velocità perché sono interni alla CPU) e di una
certa dimensione, utilizzati per specifiche funzionalità. Vi sono numerosi registri, tra
cui quelli più importanti sono: l’Instruction Register (registro istruzione), che contiene
la corrente istruzione che si sta eseguendo; il Program Counter (contatore di
programma), che contiene l’indirizzo della successiva istruzione da eseguire; gli
Accumulator (accumulatori), che contengono, temporaneamente, gli operandi di
un’istruzione e alla fine della computazione il risultato dell’operazione eseguita
dall’ALU.
Figura 1.4 Vista logica dell’interno di una CPU.

La memoria centrale
La memoria centrale, detta anche primaria o principale, è quella parte del computer dove
sono memorizzate le istruzioni e i dati dei programmi.
Essa ha diverse caratteristiche: è volatile perché il contenuto è perso nel momento in cui
il relativo calcolatore è spento; è veloce, ossia il suo accesso in lettura/scrittura può avvenire
in tempi estremamente ridotti e minimi; è ad accesso casuale perché il tempo di accesso alle
relative celle è indipendente dalla loro posizione e dunque costante per tutte le celle (da
questo punto di vista è definita come RAM, che sta per Random Access Memory).
L’unità basica di memorizzazione è la cifra binaria, o bit, che è il composto aplologico
delle parole binary digit. Un bit può contenere solo due valori, la cifra 0 oppure la cifra 1, e
il correlativo sistema di numerazione binario richiede pertanto solo quei due valori per la
codifica dell’informazione digitale.
NOTA
Consultare l’Appendice C per un approfondimento sul sistema di numerazione binario.

La memoria primaria, dal punto di visto logico, è rappresentabile come una sequenza di
celle o locazioni di memoria ciascuna delle quali può memorizzare una certa quantità di
informazione (Figura 1.5). Ogni cella, detta parola (memory word) ha una dimensione fissa
e tale dimensione, espressa in multipli di 8, ne indica la lunghezza, ossia il suo numero di
bit (possiamo avere, per esempio, word di 8 bit, di 16 bit, di 32 bit e così via). Così se una
cella ha k bit allora la stessa può contenere una delle 2k combinazioni differenti di bit.
Inoltre la lunghezza della word indica anche che quella è la quantità di informazione che
un computer durante un’operazione può elaborare allo stesso tempo e in parallelo.
Altra caratteristica essenziale di una cella di memoria è che ha un indirizzo, ossia un
numero binario che ne consente la localizzazione da parte della CPU al fine del reperimento
o della scrittura di contenuto informativo anch’esso binario. La quantità di indirizzi
referenziabili dipende dal numero di bit di un indirizzo: generalizzando, se un indirizzo ha n
bit allora il massimo numero di celle indirizzabili sarà 2n.

Figura 1.5 Memoria primaria come sequenza di celle.

Per esempio, la Figura 1.6 mostra tre differenti layout per una memoria di 160 bit
rispettivamente con celle di 8 bit, 16 bit e 32 bit. Nel primo caso sono necessari almeno 5
bit per esprimere 20 indirizzi da 0 a 19 (infatti 25 ne permette fino a 32); nel secondo caso
sono necessari almeno 4 bit per esprimere 10 indirizzi da 0 a 9 (infatti 24 ne permette fino a
16); nel terzo caso sono necessari almeno 3 bit per esprimere 5 indirizzi da 0 a 4 (infatti 23
ne permette fino a 8).
Figura 1.6 Tre differenti layout per una memoria di 160 bit.

In più è importante dire che il numero di bit di un indirizzo è indipendente dal numero di
bit per cella: una memoria con 210 celle di 8 bit ciascuna e una memoria con 210 celle di 16
bit ciascuna necessiteranno entrambe di indirizzi di 10 bit.
La dimensione di una memoria è dunque data dal numero di celle per la loro lunghezza in
bit. Nei nostri tre casi è sempre di 160 bit (correntemente, un computer moderno avrà una
dimensione di 4 Gigabyte di memoria se avrà almeno 232 celle indirizzabili di 1 byte di
lunghezza ciascuna).

Ordinamento dei byte


Quando una word è composta da più di 8 bit (1 byte), tipo 16 bit (2 byte), 32 bit (4 byte)
e così via vi è la necessità di decidere come ordinare o disporre in memoria i relativi byte.
Oggi esistono due modalità di ordinamento largamente utilizzate dai moderni elaboratori
elettronici che sono denominate come segue.
Big endian (architetture Motorola 68k, SPARC e così via), dove, data una word, il byte
più significativo (most significant byte) è memorizzato all’indirizzo più basso (smallest
address) e a partire da quello, da sinistra a destra (left-to-right), sono memorizzati i
byte successivi.
Little endian (architetture Intel x86, x86-64 e così via),dove, data una word, il byte
meno significativo (least significant byte) è memorizzato all’indirizzo più basso
(smallest address) e a partire da quello, da destra a sinistra (right-to-left), sono
memorizzati i byte successivi.
CURIOSITÀ
Questi termini si devono allo scrittore e poeta irlandese Jonathan Swift, che nel suo famoso
libro I viaggi di Gulliver prendeva in giro i politici che si facevano guerra per il giusto modo di
rompere le uova sode: dalla punta più piccola (little end) oppure dalla punta più grande (big
end)? I due termini vennero poi ripresi da Danny Cohen, uno scienziato informatico, in un
significativo articolo del 1980, dal titolo ON HOLY WARS AND A PLEA FOR PEACE,
rintracciabile ancora all’URL http://www.ietf.org/rfc/ien/ien137.txt.

Per comprendere quanto detto, consideriamo come viene memorizzato un numero come
2854123 (binario, 00000000001010111000110011101011) in una word di 32 bit secondo
un’architettura big endian e secondo un’architettura little endian (Figura 1.7): nel primo
caso il byte più a sinistra (più significativo) è memorizzato nell’indirizzo 0 e poi a seguire,
da sinistra a destra, gli altri byte negli indirizzi 1, 2 e 3; nel secondo caso il byte più a destra
(meno significativo) è memorizzato nell’indirizzo 0 e poi a seguire, da destra a sinistra, gli
atri byte negli indirizzi 1, 2 e 3.
TERMINOLOGIA
Se vediamo una word come una sequenza di bit (Figura 1.8) piuttosto che come una
sequenza di byte, possiamo altresì dire che essa avrà un bit più significativo (most
significant bit) che sarà ubicato al limite sinistro di tale sequenza (high-order end) e un bit
meno significativo (least significant bit) che sarà ubicato al limite destro sempre della stessa
sequenza (low-order end).

In definitiva, se due sistemi adottano i due differenti metodi di ordinamento in memoria


dei byte e si devono scambiare dei dati, allora vi possono essere dei problemi di congruità
tra i dati inviati da un sistema low endian verso un sistema big endian se non sono previsti
appositi accorgimenti oppure se non sono forniti idonei meccanismi di conversione.

Figura 1.7 Ordinamento della memoria: differenza tra big endian e little endian.
Figura 1.8 Word come sequenza di bit.

Per esempio, se un’applicazione scritta su un sistema SPARC memorizza dei dati binari
in un file e poi lo stesso file è aperto il lettura su un sistema x86, si avranno dei problemi di
congruità perché il file è stato scritto in modo endian-dependent.
Per evitare tale problema si possono adottare vari accorgimenti come, per esempio, quello
di scrivere i dati in un formato neutrale che prevede file testuali e stringhe, oppure adottare
idonee routine di conversione (byte swapping) che, a seconda del sistema in uso, forniscano
la corretta rappresentazione dell’informazione binaria.
Quando si deve scrivere software per diverse piattaforme hardware che hanno sistemi di
endianness incompatibili lo stesso deve essere sempre pensato in modo portabile, non
assumendo mai, dunque, un particolare ordinamento in memoria dei byte.
Paradigmi di programmazione
Un paradigma o stile di programmazione indica un determinato modello concettuale e
metodologico, offerto in termini concreti da un linguaggio di programmazione, al quale fa
riferimento un programmatore per la progettazione e scrittura di un programma informatico
e dunque per la risoluzione del suo particolare problema algoritmico. Si conoscono
numerosi paradigmi di programmazione, ma quelli che seguono rappresentano i più comuni.
Nel paradigma procedurale l’unità principale di programmazione è, per l’appunto, la
procedura o la funzione che ha lo scopo di manipolare i dati del programma. Questo
paradigma è talune volte indicato anche come imperativo, perché consente di costruire un
programma indicando dei comandi (assegna, chiama una procedura, esegui un loop e così
via) che esplicitano quali azioni si devono eseguire, e in quale ordine, per risolvere un
determinato compito. Questo paradigma si basa, dunque, su due aspetti di rilievo: il primo è
riferito al cambiamento di stato del programma che è causa delle istruzioni eseguite (si
pensi al cambiamento del valore di una variabile in un determinato tempo durante
l’esecuzione del programma); il secondo è inerente allo stile di programmazione adottato
che è orientato al “come fare o come risolvere” piuttosto che al “cosa si desidera ottenere o
cosa risolvere”. Esempi di linguaggi che supportano il paradigma procedurale sono
FORTRAN, COBOL, Pascal e C.
Nel paradigma a oggetti l’unità principale di programmazione è l’oggetto (nei sistemi
basati sui prototipi) oppure la classe (nei sistemi basati sulle classi). Questi oggetti,
definibili come virtuali, rappresentano in estrema sintesi astrazioni concettuali degli oggetti
reali del mondo fisico che si vogliono modellare. Questi ultimi possono essere oggetti più
generali (per esempio un computer) oppure oggetti più specifici, ovvero maggiormente
specializzati (per esempio una scheda madre, una scheda video e così via). Noi utilizziamo
tali oggetti senza sapere nulla della complessità con cui sono costruiti e comunichiamo con
essi attraverso l’invio di messaggi (sposta il puntatore, digita dei caratteri) e mediante delle
interfacce (il mouse, la tastiera). Inoltre, essi sono dotati di attributi (velocità del processore,
colore del case e così via) che possono essere letti e, in alcuni casi, modificati. Questi
oggetti reali vengono presi come modello per la costruzione di sistemi software a oggetti,
dove l’oggetto (o la classe) avrà metodi per l’invio di messaggi e proprietà che
rappresenteranno gli attributi da manipolare. Principi fondamentali di tale paradigma sono i
seguenti.
L’incapsulamento, che è un meccanismo attraverso il quale i dati e il codice di un
oggetto sono protetti da accessi arbitrari (information hiding). Per dati e codice
intendiamo tutti i membri di una classe, ovvero sia i dati membro (come le variabili),
sia le funzioni membro (definite anche metodi in molti linguaggi di programmazione
orientati agli oggetti). La protezione dell’accesso viene effettuata applicando ai membri
della classe degli specificatori di accesso, definibili come: pubblico, con cui si
consente l’accesso a un membro di una classe da parte di altri metodi di altre classi;
protetto, con cui si consente l’accesso a un membro di una classe solo da parte di
metodi appartenenti alle sue classi derivate; privato, con cui un membro di una classe
non è accessibile né da metodi di altre classi né da quelli delle sue classi derivate ma
soltanto dai metodi della sua stessa classe.
L’ereditarietà, che è un meccanismo attraverso il quale una classe può avere relazioni
di ereditarietà nei confronti di altre classi. Per relazione di ereditarietà intendiamo una
relazione gerarchica di parentela padre-figlio, dove una classe figlio (definita classe
derivata o sottoclasse) deriva da una classe padre (definita classe base o superclasse) i
metodi e le proprietà pubbliche e protette, e dove essa stessa ne definisce di proprie.
Con l’ereditarietà si può costruire, di fatto, un modello orientato agli oggetti che in
principio è generico e minimale (ha solo classi base) e poi, man mano che se ne
presenta l’esigenza, può essere esteso attraverso la creazione di sottomodelli sempre
più specializzati (ha anche classi derivate).
Il polimorfismo, che è un meccanismo attraverso il quale si può scrivere codice in
modo generico ed estendibile grazie al potente concetto che una classe base può
riferirsi a tutte le sue classi derivate cambiando, di fatto, la sua forma. Ciò si traduce, in
pratica, nella possibilità di assegnare a una variabile A (istanza di una classe base) il
riferimento di una variabile B (istanza di una classe derivata da A) e, successivamente,
riassegnare alla stessa variabile A il riferimento di una variabile C (istanza di un’altra
classe derivata da A). La caratteristica appena indicata ci consentirà, attraverso il
riferimento A, di invocare i metodi di A che B o C hanno ridefinito in modo specialistico,
con la garanzia che il sistema run-time del linguaggio di programmazione a oggetti
saprà sempre a quale esatta classe derivata appartengono. La discriminazione
automatica, effettuata dal sistema run-time di un tale linguaggio, di quale oggetto
(istanza di una classe derivata) è contenuto in una variabile (istanza di una classe base)
avviene con un meccanismo definito dynamic binding (binding dinamico).
Esempi di linguaggi che supportano il paradigma a oggetti sono Java, C#, C++,
JavaScript, Smalltalk e Python.
Nel paradigma funzionale l’unità principale di programmazione è la funzione vista in
puro senso matematico. Infatti, il flusso esecutivo del codice è guidato da una serie di
valutazioni di funzioni che, trasformando i dati che elaborano, conducono alla
soluzione di un problema. Gli aspetti rilevanti di questo paradigma sono: nessuna
mutabilità di stato (le funzioni sono side-effect free, ossia non modificano alcuna
variabile); il programmatore non si deve preoccupare dei dettagli implementativi del
“come” risolvere un problema ma piuttosto di “cosa” si vuole ottenere dalla
computazione. Esempi di linguaggi che supportano il paradigma funzionale sono: Lisp,
Haskell, F#, Erlang e Clojure.
Nel paradigma logico l’unità principale di programmazione è il predicato logico. In
pratica con questo paradigma il programmatore dichiara solo i “fatti” e le “proprietà”
che descrivono il problema da risolvere lasciando al sistema il compito di “inferirne” la
soluzione e dunque raggiungerne il “goal” (l’obiettivo). Esempi di linguaggi che
supportano il paradigma logico sono: Datalog, Mercury, Prolog e ROOP.
Dunque, il linguaggio C supporta pienamente il paradigma procedurale dove l’unità
principale di astrazione è rappresentata dalla funzione attraverso la quale si manipolano i
dati di un programma. Da questo punto di vista, si differenzia dai linguaggi di
programmazione che sposano il paradigma a oggetti come, per esempio, C++ o Java, perché
in quest’ultimo paradigma ci si concentra prima sulla creazione di nuovi tipi di dato (le
classi) e poi sui metodi e le variabili a essi relativi.
In altre parole, mentre in un linguaggio procedurale come C la modularità di un
programma viene fondamentalmente descritta dalle procedure o funzioni che manipolano i
dati, nella programmazione a oggetti la modularità viene descritta dalle classi che
incapsulano al loro interno metodi e variabili. Per questa ragione si suole dire che nel
mondo a oggetti la dinamica (metodi) è subordinata alla struttura (classi).
TERMINOLOGIA
Nei linguaggi di programmazione si usano termini come funzione (function), metodo
(method), procedura (procedure), sotto-programma (subprogram), sotto-routine (subroutine)
e così via per indicare un blocco di codice posto a un determinato indirizzo di memoria che
è invocabile (chiamabile), per eseguire le istruzioni ivi collocate. Dal punto di vista pratico,
pertanto, significano tutte la stessa cosa anche se, in letteratura, talune volte sono
evidenziate delle differenze soprattutto in base al linguaggio di programmazione che si
prende in esame. Per esempio, in Pascal una funzione ritorna un valore, mentre una
procedura non ritorna nulla; in C una funzione può agire anche come una procedura,
mentre il termine metodo non è esistente; in C++ un metodo è una funzionalità “associata”
all’oggetto o alla classe dove è stato definito ed è anche denominato funzione membro.
Concetti introduttivi allo sviluppo
Prima di affrontare lo studio sistematico della programmazione in C, e preliminarmente
all’illustrazione di un semplice programma che ne darà una panoramica generale, appare
opportuno soffermarci su alcuni concetti propedeutici allo sviluppo di C che sono trasversali
al linguaggio e che servono quindi per inquadrarlo meglio nel suo complesso.

Fasi di sviluppo di un programma


Un programma scritto in C prima di poter essere eseguito sul sistema di interesse passa
attraverso le seguenti fasi, che ne definiscono un generico ciclo operativo.
Analisi. È la fase che sottende all’individuazione delle informazioni preliminari allo
sviluppo di un software quali la sua fattibilità in senso tecnico ed economico (analisi
costi/benefici), il suo dominio applicativo, i suoi requisiti funzionali (cosa il software
deve offrire) e così via.
Progettazione. È la fase di design dove si inizia a ideare in modo più concreto come si
può sviluppare il software che è stato oggetto di una precedente analisi. Il software
viene scomposto in moduli e componenti, si definisce la loro interazione e anche il loro
contenuto (dettaglio interno). La progettazione indica, pertanto, come il software deve
essere implementato piuttosto di cosa deve fare (appannaggio della fase di analisi).
Codifica. È la fase dove si implementa concretamente il software oggetto della
progettazione. In pratica, attraverso l’utilizzo di un editor di testo, si scrivono gli
algoritmi, le funzioni e le istruzioni del programma codificate secondo la sintassi
propria del linguaggio C. Il codice scritto nell’editor si chiama codice sorgente (source
code), mentre il file prodotto si chiama file di codice sorgente (source code file).
Compilazione. È la fase dove un apposito programma, il compilatore (compiler),
traduce e converte il codice sorgente presente nel relativo file in codice eseguibile
(executable code), ovvero in codice nativo del sistema hardware riferito. Questo codice
nativo è scritto in un apposito file (executable code file). Ricordiamo che il codice
nativo è il codice espresso nel linguaggio macchina (machine language) del sistema
hardware scelto e che tale linguaggio è anche l’unico che lo stesso può comprendere
direttamente, senza alcuna intermediazione. Inoltre, in questa fase, il compilatore si
occupa di verificare che il codice sorgente sia scevro da errori sintattici e, in caso
contrario, avvisa l’utente e non procede alla compilazione e alla generazione del codice
eseguibile.
Esecuzione. È la fase dove il file contenente il codice eseguibile è caricato nella
memoria ed è eseguito, ovvero produce gli effetti computazionali per i quali è stato
progettato e sviluppato. Solitamente, in una shell testuale, per caricare in memoria ed
eseguire un programma scritto in C, è sufficiente digitare nel relativo ambiente di
esecuzione il nome del corrispondente file eseguibile. Lo stesso file eseguibile, invece,
in un ambiente dotato di GUI, può essere caricato ed eseguito facendo clic due volte di
seguito sulla corrispondente icona.
Test e debug. È la fase dove si verifica la correttezza funzionale del programma in
esecuzione e se lo stesso presenta errori o comportamenti non pertinenti con la logica
che avrebbe, invece, dovuto seguire. A tal fine esistono appositi programmi chiamati
debugger che consentono di analizzare il flusso di esecuzione di un programma step by
step, fermare la sua esecuzione al raggiungimento di un determinato breakpoint per
esaminarne il corrente stato, rilevare il valore delle variabili in un certo momento e così
via.

NOTA
L’Appendice A contiene un breve tutorial introduttivo sull’utilizzo del debugger GDB.

Mantenimento. È la fase dove un programma che è in produzione, a seguito di richieste


od osservazioni degli utenti, può subire modifiche migliorative che impattano, per
esempio, sulla performance, oppure modifiche integrative che riguardano l’aggiunta di
moduli che coprono caratteristiche funzionali supplementari rispetto a quelle previste
in origine.

Codifica, compilazione ed esecuzione: dettaglio


Quando scriviamo un programma in linguaggio C, il relativo codice sorgente è scritto in
un file la cui basename, ovvero la parte del nome che precede il carattere punto (.), può
avere un numero di caratteri il cui massimo valore è uguale a quello stabilito dal sistema
dove il programma deve girare (per esempio, in un sistema MS-DOS la basename può
essere costituita da massimo 8 caratteri), mentre l’extension, ovvero la parte del nome che
segue il carattere punto . (detta anche suffix, type o format), deve essere indicata
obbligatoriamente per molti compilatori con il carattere c (per esempio, client.c, widgets.c,
time.c e così via sono tutti nomi che esprimono file di codice sorgente C). In più è
importante rammentare che nei sistemi operativi Unix-like vi è distinzione tra lettere
maiuscole e lettere minuscole che compongono il nome di un file C. Per esempio, mentre in
Windows non c’è alcuna differenza tra, diciamo, client.c e Client.c, in GNU/Linux c’è;
entrambi, infatti, rappresentano file differenti.
ATTENZIONE
Se denominate un file di codice sorgente C con estensione .C e non .c, un compilatore,
come per esempio quello proprio della suite GCC, può interpretarlo come file di codice
sorgente C++. Infatti, per C++ le comuni estensioni valide sono .cpp, .cc, .cxx e, per
l’appunto, .C.

Dopo la scrittura di un file di codice sorgente denominato secondo le regole indicate, lo


stesso deve essere dato come input a un compilatore C al fine di fargli produrre il relativo
file di codice macchina per il sistema hardware target. Il compilatore, tuttavia, esegue tale
lavoro producendo un file di codice intermedio definito file di codice oggetto (object code
file), che nonostante contenga codice macchina non è ancora eseguibile sul sistema target.
Infatti, interviene poi, durante il processo di compilazione, un altro programma denominato
linker che “combina”, “collega”, il codice dell’object file con il codice di startup del
sistema target corrispondente e con il codice dei file oggetto delle eventuali librerie
utilizzate. Al termine del suo lavoro il linker produce il relativo file di codice eseguibile.
Questa caratteristica di generazione del file eseguibile in una sorta di processo a “due
fasi” ha una spiegazione che è legata al concetto di modularizzazione del codice. Dati più
file oggetto compilati, è possibile grazie al linker combinarli tutti insieme per produrre un
unico file eseguibile. Successivamente, se uno dei file oggetto necessita di modifiche o
aggiustamenti, è necessario compilare solo tale file oggetto e poi ricombinarlo insieme agli
altri file oggetto senza che questi ultimi abbiamo subìto un’altra compilazione.
NOTA
Generalmente il programma linker è invocato automaticamente dal compilatore durante la
fase di compilazione. Il compilatore invoca anche un altro programma denominato
preprocessor (preprocessore) che elabora dei particolari “comandi” scritti all’interno di un file
di codice sorgente, definiti come preprocessor directives (direttive del preprocessore), che
hanno lo scopo di compiere determinate manipolazioni prima dell’effettuazione della
compilazione (per esempio includere all’interno di un file sorgente contenuto presente in un
altro file detto header file).

L’intero processo di generazione di un file di codice eseguibile dato un file di codice


sorgente è visualizzabile nella Figura 1.9.
Figura 1.9 Flusso di generazione di un file eseguibile dato un file sorgente.

Sistemi di compilazione e sistemi target


Per scrivere codice in linguaggio C è necessario utilizzare tool di sviluppo che mettono a
disposizione per un determinato sistema target, sia hardware (architetture ARM, x86,
PowerPC, MIPS e così via) sia software (sistemi Unix, Windows e così via), il relativo
compilatore, le funzionalità della libreria standard e via discorrendo.
In linea generale nei sistemi Unix è presente il comando cc, che invoca il relativo
compilatore e produce, in assenza di opzioni, un file eseguibile denominato a.out (produce
altresì un file oggetto con estensione .o e con la stessa basename del file di codice sorgente,
che però viene eliminato dal linker quando il file eseguibile è stato creato).
CURIOSITÀ
Il nome a.out è un’abbreviazione di assembler output e la sua etimologia si fa risalire ai
tempi di Ken Thompson e del PDP-7, quando il file sorgente di un programma si faceva
processare dal relativo assemblatore che produceva un file di output, con nome fisso, che
era direttamente eseguibile. Dopo di allora è rimasta tradizione dei compilatori Unix
generare tale nome di output per un file eseguibile in mancanza di diversa indicazione.

Il comando cc, è solitamente un alias verso il reale comando di compilazione che può
essere, per esempio, gcc, presente in genere come compilatore di default nei sistemi
GNU/Linux con installata la suite GCC (GNU Compiler Collection) oppure clang, presente
come compilatore di default nei sistemi FreeBSD con installata la suite LLVM (Low Level
Virtual Machine).
Questi comuni compilatori, tuttavia, non sono gli unici utilizzabili in ambienti Unix-like;
è possibile sceglierne altri, quali, solo per citarne alcuni, SAS/C, Amsterdam Compiler Kit,
LCC (Little C Compiler) e PCC (Portable C Compiler).
Per quanto attiene ai sistemi Windows, di default, non sono mai installati né un
compilatore né la relativa suite di strumenti e librerie. È possibile, comunque, usare la suite
GCC tramite i progetti Cygwin e MinGW (Minimalist GNU for Windows), che forniscono
rispettivamente un ambiente runtime Unix/POSIX-like sotto Windows e un ambiente di
compilazione per applicazioni native sotto Windows.
In ogni caso è possibile utilizzare, così come visto per i sistemi Unix, anche altri
compilatori C, quali quello fornito da Microsoft tramite l’installazione dell’IDE Visual
Studio e denominato cl (in questo caso il file oggetto prodotto avrà la stessa basename del
file sorgente ma l’estensione .obj, mentre il file eseguibile avrà la stessa basename del file
sorgente ma l’estensione .exe), Digital Mars e Pelles C.
NOTA
Ricordiamo, come anticipato nella Prefazione, che il compilatore di elezione scelto per la
didattica del corrente testo è quello fornito dalla suite GCC utilizzabile nativamente con il
sistema operativo GNU/Linux. Per il sistema operativo Windows, invece, GCC è utilizzabile,
nel nostro caso, mediante l’installazione dell’environment MinGW. L’Appendice A spiega
come installare e utilizzare GCC sia sotto sistemi GNU/Linux sia sotto sistemi Windows. In
ogni caso verrà anche utilizzato il compilatore cl di Microsoft per evidenziare eventuali
differenze tra differenti implementazioni del linguaggio. Per quanto concerne invece il
sistema target hardware, esso è una piattaforma x86-64, ovvero una versione a 64 bit
dell’instruction set della piattaforma x86 e ciò sia per il sistema GNU/Linux sia per il sistema
Windows (in quest’ultimo caso, comunque, la suite MinGW sarà a 32 bit e ciò per
evidenziare le differenze tra compilatori a 64 bit, come quello del sistema GNU/Linux,
rispetto a quelli a 32 bit).
Il primo programma
Vediamo, attraverso la disamina del Listato 1.1, quali sono gli elementi basilari per
strutturare e scrivere un programma in C, con l’avvertenza che tutte le informazioni
didattiche successivamente enucleate su alcuni costrutti del linguaggio saranno introduttive
ai concetti che tratteranno e giocoforza non dettagliate. Le stesse saranno trattate con
dovizia di particolari nei relativi capitoli di pertinenza.
Abbiamo, in questa sede, infatti, inteso perseguire solamente i seguenti obiettivi:
illustrare una struttura di massima degli elementi costitutivi di un programma in C; fornire
una terminologia basica applicabile ai principali costrutti del linguaggio.

Listato 1.1 PrimoProgramma.c (PrimoProgramma).


/* PrimoProgramma.c :: Struttura di un generico programma :: */
#include <stdio.h>
#include <stdlib.h>

#define MULTIPLICAND 10
#define MULTIPLIER 20

// prototipo di una funzione


int mult(int a, int b);

// entry point del programma


int main(void)
{
// dichiarazione e inizializzazione contestuale
// di più variabili di diverso tipo
char text_1[] = "Primo programma in C:",
text_2[] = " Buon divertimento!";
int a = 10, b = 20;

float f; // dichiarazione
f = 44.5f; // inizializzazione

// stampa qualcosa...
printf("%s%s\n", text_1, text_2);
printf("Stampero' un test condizionale tra a=%d e b=%d:\n", a, b);

if (a < b) // se a < b stampa quello che segue...


{
printf("a < b VERO!");
}
else /* altrimenti stampa quest'altra stringa */
{
printf("a > b VERO!");
}

printf("\nStampero' un ciclo iterativo, dove leggero' ");


printf("per 10 volte il valore di a\n");

/*
* ciclo for
*/
for (int i = 0; i < 10; i++)
{
printf("Passo %d ", i);
printf("--> a=%d\n", a);
}

printf("Ora eseguiro' una moltiplicazione tra %d e %d\n", MULTIPLICAND, MULTIPLIER);


int res = mult(MULTIPLICAND, MULTIPLIER); // invocazione di una funzione
printf("Il risultato di %d x %d e': %d\n", MULTIPLICAND, MULTIPLIER, res);

/*
// esce dalla funzione main
*/
return (EXIT_SUCCESS);
}

/****************************************
* Funzione: mult *
* Scopo: moltiplicazione di due valori *
* Parametri: a, b -> int *
* Ritorno: int *
****************************************/
int mult(int a, int b)
{
return a * b;
}

Il programma del Listato 1.1 inizia con la definizione di un’istruzione di commento,


ovvero con la scrittura, tra il carattere di inizio commento /* e il carattere di fine commento
*/, di testo che viene ignorato dal compilatore e che serve a documentare o chiarire parti del
codice sorgente. Il testo di questo tipo di commento può anche essere suddiviso su più righe
e, infatti, per tale ragione è spesso anche definito commento multiriga.
In ogni caso non è possibile innestare commenti multiriga (Snippet 1.1), e ciò perché il
marker di commento finale */ del commento innestato “pareggia” con il marker di
commento iniziale /* del commento che innesta. In questo modo, quindi, il marker di
commento finale */ del commento che innesta si trova senza un marker di commento
iniziale /* con cui corrispondere, inducendo il compilatore a generare un errore di
compilazione del tipo error: unknown type name 'CCCC'.

Snippet 1.1 Commenti multiriga innestati.


/* // commento che innesta
AAAA
/* // commento innestato
BBB
*/
CCCC
*/

Infine, i commenti multiriga possono essere anche scritti di fianco, a lato di una porzione
di codice (winged comment), come mostra quello definito dopo l’istruzione else, così come
essere scritti in forma di riquadro di contenimento (boxed comment), come mostra quello
scritto prima della definizione della funzione mult, oppure scrivendo degli asterischi * per
ogni riga di separazione, come mostra quello indicato prima del ciclo for.
In ogni caso, a parte le forme indicate di scrittura dei commenti multiriga che sono quelle
più comuni, il programmatore è libero di scegliere la formattazione che più gli aggrada a
condizione, però, che via sia sempre una corrispondenza tra un marcatore di commento
iniziale /* e un marcatore di commento finale */.
C99 ha introdotto, inoltre, un secondo tipo di commento, che è indicato tramite l’utilizzo
di due caratteri slash // cui si fa seguire il testo da commentare. Questo commento, poiché
termina in automatico alla fine della corrispettiva riga, è definito commento a singola riga e
ha l’importante caratteristica che può essere innestato all’interno di un commento multiriga,
come mostra in modo evidente il commento posto prima dell’istruzione return all’interno
della funzione main. In più, anche con questo tipo di commento, è possibile scrivere
commenti multiriga semplicemente scrivendo su ogni riga i caratteri // e la porzione di testo
da commentare. Un esempio di quanto detto è visibile nei primi due commenti posti subito
dopo la parentesi graffa di apertura ({) della funzione main.
DETTAGLIO
Il compilatore quando trova dei commenti, prima della compilazione, li rimuove tutti
sostituendo ciascuno di essi con un carattere di spazio.

Seguono il primo commento del sorgente e le istruzioni #include e #define, che


rappresentano delle direttive per il preprocessore, ovvero dei comandi che saranno da
quest’ultimo eseguiti prima che la compilazione vera e propria venga avviata.
La direttiva #include consente di includere, a partire dal punto dove è stata definita, il
contenuto di un file indicato. Per esempio, #include <stdio.h> e #include <stdlib.h>

includeranno rispettivamente il contenuto del file stdio.h, che fornisce le funzionalità per
effettuare delle operazioni di input/output, e il contenuto del file stdlib.h, che fornisce
funzionalità di carattere generale.
Il contenuto di questi file, detti anche header file, è essenziale per la corretta
compilazione del codice sorgente quando si utilizzano, per l’appunto, le loro funzionalità.
Nel nostro caso è impiegata intensivamente la funzione printf, che visualizza nello
standard output la stringa di caratteri indicata come argomento, la quale è dichiarata nel file
stdio.h che deve, per l’appunto, essere incluso per permetterne l’utilizzo. Lo stesso

meccanismo di inclusione avviene per l’identificatore EXIT_SUCCESS dichiarato nel file


stdlib.h, che viene anch’esso incluso.
NOTA
I file header contengono informazioni per il compilatore che sono essenziali per consentire
la corretta compilazione di un programma (per esempio il nome delle costanti, i prototipi
delle funzioni e così via). Tuttavia, il codice implementativo delle funzioni è posto in altri file
di libreria precompilati (file oggetto) che sono collegati dal linker al file oggetto del
programma principale per produrre il codice eseguibile finale.

La direttiva #define consente di definire, invece, delle macro, cioè degli identificatori che
possono essere utilizzati all’interno di un programma come nomi costanti che il
preprocessore sostituisce con gli effettivi valori collegati. Il programma in esame, dunque,
definisce le macro MULTIPLICAND e MULTIPLIER con i valori relativi di 10 e 20 e, pertanto, quando
il preprocessore le individuerà nel sorgente le sostituirà con i predetti valori.
Abbiamo poi la scrittura o dichiarazione dell’identificatore mult, che rappresenta il
cosiddetto prototipo di funzione con il quale si indicano le sue proprietà quali il tipo di
ritorno e gli eventuali parametri che può processare quando invocata. Nel complesso il
nome di una funzione, il tipo di ritorno e i tipi dei suoi parametri rappresentano il suo
header.
Il prototipo di una funzione è un aspetto fondamentale perché è utilizzato dal compilatore
per verificare la congruità delle relativa definizione (per esempio se i tipi dei parametri
corrispondono) e il suo impiego corretto durante una sua chiamata (per esempio se il tipo di
un argomento fornito è compatibile con il tipo del parametro dichiarato).
Il prototipo di mult specifica che ritorna un tipo di dato intero e accetta due argomenti
sempre di tipo intero. Nell’ambito della funzione main notiamo come mult sia invocata con
due argomenti di tipo intero (i valori 10 e 20) e ritorni un valore di tipo intero assegnato alla
variabile res. La stessa funzione mult ha infatti una sua definizione, ossia una sua
implementazione algoritmica scritta dopo la definizione della funzione main che evidenzia
come ritorni un valore di tipo intero che è il risultato della moltiplicazione tra i parametri a e
b sempre di tipo intero.
La funzione main, invece, è la funzione principale di un qualsiasi programma in C e deve
essere sempre presente perché ne rappresenta l’entry point, ossia il “punto di ingresso”
attraverso il quale viene eseguito. In pratica la funzione main è invocata automaticamente
quando il programma è avviato e poi attraverso di essa vengono eseguite le relative
istruzioni e invocate le altre funzioni eventualmente indicate. Da questo punto di vista un
programma in C è composto sempre almeno da una funzione main e poi da una o più
funzioni ausiliarie; non è altro, quindi, che una collezione di una o più funzioni.
Tale funzione può essere definita con un tipo di ritorno int e con nessun parametro
(keyword void) oppure con un tipo di ritorno int e con due parametri l’uno di tipo int e
l’altro di tipo array di puntatori a carattere (Snippet 1.2 e 1.3).

Snippet 1.2 Modalità di definizione di main. Prima definizione.


int main(void) { /* ... */ }

Snippet 1.3 Modalità di definizione di main. Seconda definizione.


int main(int argc, char *argv[]) { /* ... */ }

In pratica quando si utilizza la prima definizione si indica che main non accetta argomenti
dalla riga di comando, mentre quando si utilizza la seconda definizione si esprime la
volontà di processare gli argomenti forniti dalla relativa shell.
NOTA
Scorrendo codice in C è possibile vedere altre due modalità di definizione della funzione
main: main() { /* ... */ } e void main() { /* ... */ }. Tuttavia, anche se il compilatore in uso

può tollerare lo standard C11, esplicita solo quelle indicate negli Snippet 1.2 e 1.3.

Il tipo di ritorno int di main indica un valore numerico che deve essere ritornato al sistema
operativo e che assume per esso un certo significato: per esempio, il valore 0 (espresso dal
nostro programma attraverso la macro EXIT_SUCCESS definita nel file header <stdlib.h>), indica
una terminazione corretta del corrente programma. Nel caso della funzione main, inoltre,
l’istruzione return termina anche l’esecuzione di un programma (spesso, in alcuni
programmi, si trova, al posto dell’istruzione return 0 l’istruzione exit(0), che termina allo
stesso modo l’esecuzione di un programma).
È altresì possibile omettere l’istruzione return dal main, poiché quando il programma
raggiunge la parentesi graffa di chiusura } della funzione viene in automatico ritornato il
valore 0 (in C89 il valore è non definito, in C11 il valore è comunque non definito se il main
è definito con un tipo di ritorno diverso da int).
Per quanto attiene al contenuto della funzione main notiamo subito una serie di istruzioni
che definiscono delle variabili, ossia delle locazioni di memoria modificabili deputate a
contenere un valore di un determinato tipo di dato. Così gli identificatori text_1 e text_2
indicano variabili che possono contenere caratteri, gli identificatori a, b e res indicano
variabili che possono contenere numeri interi e l’identificatore f indica una variabile che
può contenere numeri decimali, cioè numeri formati da una parte intera e una parte
frazionaria separati da un determinato carattere (in C tale carattere è il punto .).
Una variabile, solitamente, può essere prima dichiarata e poi inizializzata (è il caso della
variabile f) oppure può essere dichiarata e inizializzata contestualmente in un’unica
istruzione (è il caso delle altre variabili).
A parte le varie istruzioni di stampa su console dei valori espressi dalle corrispettive
stringhe delle funzioni printf, notiamo l’impiego: di un’istruzione di selezione doppia
if/else che valuta se una data espressione è vera o falsa eseguendone, a seconda del risultato
della valutazione, il codice corrispondente (quello del ramo valutato vero oppure quello del
ramo valutato falso); di un’istruzione di iterazione for che consente di eseguire ciclicamente
una serie di istruzioni finché una data espressione è vera.
Pertanto l’istruzione if/else valuta se il valore della variabile a è minore del valore della
variabile b e nel caso stampa la relativa stringa, altrimenti, in caso di valutazione falsa,
stampa l’altra stringa a esso relativa. L’istruzione for, invece, stampa su console per 10
volte informazioni sul valore delle variabili i e a.
Un “assaggio” di printf
La funzione della libreria standard printf è dichiarata nel file header <stdio.h> e consente
di visualizzare sullo standard output (generalmente a video) il letterale stringa fornitogli
come argomento. All’interno del letterale stringa è possibile inserire degli appositi caratteri
prefissi dal simbolo percento (%), definiti nel complesso come specifiche di conversione o
specificatori di formato, che rappresentano dei segnaposto di un determinato tipo di dato
che saranno sostituiti, in quella precisa locazione, con i valori delle relative variabili fornite
come ulteriori argomenti alla funzione.
Così il segnaposto %d permette di stampare il valore di una variabile come intero in base
10, %f permette di stampare il valore di una variabile con un determinato numero di cifre
dopo il punto decimale di separazione, %s permette di stampare il valore di una variabile
come stringa di caratteri e così via per altri specificatori.
Quando si utilizza la funzione printf è essenziale sapere che essa non stampa altresì i
caratteri di delimitazione della stringa (i doppi apici ") e non avanza in automatico sulla
successiva riga di output al termine della visualizzazione dei suoi caratteri.
Per istruire printf in modo che avanzi alla successiva riga di output, a partire dalla quale
riprendere (oppure iniziare) la visualizzazione di altri caratteri, è possibile utilizzare una
sequenza di escape formata dalla combinazione dei caratteri \n (newline character).

Elementi strutturali di un programma


Un sorgente in C è modellato idealmente in più parti strutturali e semantiche che sono
combinate con lo scopo di formare un determinato programma (Figura 1.10). Abbiamo: gli
elementi lessicali (keyword, identificatori, letterali stringa, costanti, segni di punteggiatura,
commenti e così via); le istruzioni (statement) e i blocchi (compound statement); le
espressioni (operandi più operatori); le dichiarazioni (di variabili, di funzioni e così via); le
definizioni esterne; le direttive per il preprocessore.
A partire dagli elementi lessicali si costruiscono le istruzioni, che possono formare delle
espressioni, delle dichiarazioni, delle definizioni esterne e delle direttive per il
preprocessore.
Figura 1.10 Elementi strutturali e semantici di un generico programma in C.

Degli elementi lessicali indicati, tutti, tranne i commenti, sono definiti come token e
rappresentano dunque delle entità minimali significative per il linguaggio C.
La Tabella 1.1 mostra tutte le keyword del linguaggio aggiornate allo standard C11; la
Tabella 1.2 mostra i segni di punteggiatura utilizzabili; lo Snippet 1.4 evidenzia alcuni
identificatori, letterali stringa e costanti.
Tabella 1.1 Keyword del linguaggio C (sono case sensitive e riservate nell’uso).
auto else long switch _Atomic
break enum register typedef _Bool
case extern restrict union _Complex
char float return unsigned _Generic
const for short void _Imaginary
continue goto signed volatile _Noreturn
default if sizeof while _Static_assert
do inline static _Alignas _Thread_local
double int struct _Alignof

Tabella 1.2 Segni di punteggiatura.


[ ] ( ) { } . -> ++ --
& * + - ~ ! / % << >>
< > <= >= == != ^ | && ||
? : ; ... = *= /= %= += -=
<<= >>= &= ^= |= , # ## <: :>
<% %> %: %:%:

Snippet 1.4 Esempi di identificatori, letterali stringa e costanti.


...
// identificatori
int number, temp, status;
void foo(void) { /*...*/ }

int main(void)
{
// identificatori
int number, temp, status;
void foo(void) { /*...*/ }

int a = 100; // 100 è una costante intera


float f = 120.78f; // 120.78 è una costante in virgola mobile
char c = 'A'; // 'A' è una costante carattere

char name[] = "Pellegrino"; // "Pellegrino" è un letterale stringa


...
}

L’ammontare di caratteri di spaziatura (spazio, tabulazione, invio e così via) atti a fungere
da separazione tra i token non è obbligatorio: ogni programmatore può scegliere quello che
più gli aggrada secondo il suo personale stile di scrittura. Tuttavia un token non può essere
“diviso” senza causare un probabile errore e un cambiamento della sua semantica. Lo stesso
vale per un letterale stringa con il seguente distinguo: al suo interno è sempre possibile
inserire dei caratteri di spazio, ma è un errore separarlo all’interno dell’editor su più righe
mediante la pressione del tasto Invio.
Per quanto riguarda invece le istruzioni, esse rappresentano azioni o comandi che devono
essere eseguiti durante l’esecuzione del programma. In C, ogni statement deve terminare
con il carattere punto e virgola (;) e due o più statement possono essere raggruppate insieme
a formare un’unica entità sintattica, definita blocco, se incluse tra la parentesi graffa aperta
({) e la parentesi graffa chiusa (}).
Compilazione ed esecuzione del codice
Dopo aver scritto il programma del Listato 1.1, con un qualunque editor di testo o con un
IDE di preferenza (per noi l’IDE sarà NetBeans), vediamo come eseguirne la compilazione
che, lo ricordiamo, è quel procedimento mediante il quale un compilatore C legge un file
sorgente (nel nostro caso PrimoProgramma.c) e lo trasforma in un file (per esempio
PrimoProgramma.exe) che conterrà istruzioni scritte nel linguaggio macchina del sistema
hardware di riferimento.
NOTA
Per i dettagli su come eseguire la compilazione di un programma in C, sia in ambiente
GNU/Linux sia in ambiente Windows e senza l’ausilio di alcun IDE, consultare l’Appendice
A.

Shell 1.1 Invocazione del comando di compilazione (GNU/Linux).


[thp@localhost MY_C_SOURCES]$ gcc -std=c11 PrimoProgramma.c -o ~/MY_C_BINARIES/PrimoProgramma

Shell 1.2 Invocazione del comando di compilazione (Windows).


C:\MY_C_SOURCES>gcc -std=c11 PrimoProgramma.c -o \MY_C_BINARIES\PrimoProgramma

Alla fase di compilazione segue la fase di esecuzione, nella quale un file eseguibile (nel
nostro caso PrimoProgramma.exe o PrimoProgramma), memorizzato per esempio su una memoria
secondaria come un hard disk, viene caricato nella memoria principale (la RAM)
da un apposito loader dove la CPU corrente preleva, decodifica ed esegue le relative
istruzioni che compongono il programma medesimo (Figura 1.11).

Shell 1.3 Avvio del programma (GNU/Linux).


[thp@localhost MY_C_BINARIES]$ ./PrimoProgramma

Shell 1.4 Avvio del programma (Windows).


C:\MY_C_BINARIES>PrimoProgramma.exe

Output 1.1 Esecuzione di Shell 1.3 o di Shell 1.4.


Primo programma in C: Buon divertimento!
Stampero' un test condizionale tra a=10 e b=20:
a < b VERO!
Stampero' un ciclo iterativo, dove leggero' per 10 volte il valore di a
Passo 0 --> a=10
Passo 1 --> a=10
Passo 2 --> a=10
Passo 3 --> a=10
Passo 4 --> a=10
Passo 5 --> a=10
Passo 6 --> a=10
Passo 7 --> a=10
Passo 8 --> a=10
Passo 9 --> a=10
Ora eseguiro' una moltiplicazione tra 10 e 20
Il risultato di 10 x 20 e': 200
Figura 1.11 Flusso completo di generazione di un file eseguibile e sua esecuzione.
Problemi di compilazione ed esecuzione?
Elenchiamo alcuni problemi che si potrebbero incontrare durante la fase di compilazione
oppure di esecuzione del programma appena esaminato.
Il comando di compilazione gcc è inesistente? Verificare che sotto Windows
l’environment MinGW sia stato correttamente installato oppure che la corrente
distribuzione di GNU/Linux scelta presenti il package GCC. Si rimanda
all’Appendice A per i dettagli su come installare e configurare sia MinGW sia GCC.
Il compilatore gcc non trova il file PrimoProgramma.c? Verificare che la directory corrente
sia c:\MY_C_SOURCES (per Windows) oppure ~/MY_C_SOURCES (per GNU/Linux, laddove in
quest’ultimo caso il carattere tilde ~ è sostituito dalla directory home dell’utente
corrente, che per noi sarà /home/thp).
Il file PrimoProgramma.exe o il file PrimoProgramma non viene trovato? Verificare che la
directory corrente sia c:\MY_C_BINARIES (per Windows) oppure ~/MY_C_BINARIES (per
GNU/Linux, laddove in quest’ultimo caso il carattere tilde ~ è sostituito dalla directory
home dell’utente corrente, che per noi sarà /home/thp).
Capitolo 2
Variabili, costanti, letterali e tipi

Un programma, di qualsiasi tipologia esso sia, è nella sostanza sempre composto da


istruzioni e dati che, combinati insieme, consentono di implementare determinate procedure
algoritmiche per la risoluzione di specifici problemi computazionali.
I dati rappresentano, ad alto livello, delle informazioni o dei valori di tipo numerico o
testuale che subiscono nell’ambito di un programma un processo di elaborazione e di
eventuale trasformazione.
Per un computer, comunque, a basso livello, i dati, indipendentemente se di tipo
numerico o testuale, sono sempre visti e memorizzati in modo binario ovvero come insiemi
di bit.
Nell’ambito di un programma, dunque, sono manipolati e gestiti mediante delle
astrazioni logiche, denominate variabili o costanti, cui sono associati per l’appunto dei
valori che possono essere di diverso tipo, per esempio un carattere come j, un numero intero
come 1000, un numero in virgola mobile come 456.99 e così via.
Variabili
Una variabile rappresenta uno spazio di memoria alterabile dove vengono memorizzati
dei valori. Prima di poter utilizzare tale variabile, ovvero prima di poterle assegnare o
leggere un valore, è necessario dichiararne il tipo, cioè determinare che specie di dato potrà
contenere. La decisione sul tipo di dato di una variabile è un aspetto cruciale nella scrittura
di un programma perché incide su come la stessa viene memorizzata e su quali operazioni
sono effettuabili. Da questo punto di vista C è categorizzabile come un linguaggio di
programmazione strongly typed, ovvero fortemente tipizzato.
TERMINOLOGIA
Oltre ai linguaggi fortemente tipizzati (strongly typed), esistono anche linguaggi debolmente
tipizzati (weakly typed o loosely typed) in cui le variabili non sono dichiarate con un tipo
predefinito e nelle stesse, in tempi successivi, possono essere contenuti valori di tipo
diverso: oggetti, stringhe, numeri e così via. Tra questi linguaggi vi sono, solo per citare i più
comuni, JavaScript, PHP e Python.

Dichiarazione
Una dichiarazione (Sintassi 2.1) è una statement (declaration statement) attraverso la
quale si indica al compilatore di predisporre e riservare spazio in memoria atto a contenere
un dato del tipo espressamente specificato. In più, si indica un identificatore, o nome, con
cui riferirsi nel programma a quella locazione di memoria.

Sintassi 2.1 Dichiarazione di un variabile.


data_type identifier;

Lo Snippet 2.1 evidenzia la dichiarazione di una variabile di tipo int di nome number.

Snippet 2.1 Dichiarazione di una variabile di tipo int.


int number;

Questa dichiarazione collega l’identificatore number con una locazione di memoria


specificatamente predisposta dal compilatore (Figura 2.1) e indica pure il tipo di
informazione ivi memorizzabile, cioè un dato numerico di tipo intero, senza quindi la parte
frazionaria (per esempio 100, 0, -33 e così via). La keyword int è una parola riservata del
linguaggio che esprime un tipo di dato (nella fattispecie è un’abbreviazione di integer) e
non può essere impiegata per altri fini come, per esempio, quello di identificare un nome di
una variabile.
Figura 2.1 Rappresentazione della variabile number dopo la sua dichiarazione.

NOTA
In accordo con quanto indicato dallo standard C11, quando una variabile viene dichiarata
senza fornirle un esplicito valore di inizializzazione la stessa, a seconda della sua classe di
memorizzazione, potrà assumere o meno un valore significativo. Nel nostro caso
assumiamo che la variabile number abbia una classe di memorizzazione automatica, pertanto
il suo valore sarà indeterminato, ossia la variabile potrà contenerne uno qualsiasi, come nel
nostro caso è -858993460, che è stato assegnato dal compilatore cl di Microsoft. Nel caso del
compilatore gcc il valore sarà, per esempio, 0.

Lo Snippet 2.2 evidenzia, invece, come attraverso il carattere virgola (,) è possibile
esprimere più dichiarazioni di variabili dello stesso tipo come un’unica istruzione.

Snippet 2.2 Dichiarazione di più variabili di tipo int.


int number, temp, index, max, min;

Una variabile, prima del suo utilizzo, deve essere sempre dichiarata. Secondo lo standard
C89, le statement di dichiarazione devono essere sempre espresse all’inizio di un blocco di
codice e, tra di esse, non possono esservi altri tipi di istruzioni (Snippet 2.3).

Snippet 2.3 Dichiarazione di variabili secondo lo standard C89.


{
int a = 10;
int b = 100;
int c = 1000;
printf("%d - %d - %d\n", a, b, c); // 10 - 100 - 1000
/* altre istruzioni... */
}

A partire invece dallo standard C99, è possibile dichiarare le variabili ovunque si desidera
nell’ambito di un blocco di codice così come frapporre tra di esse altre istruzioni (Snippet
2.4). Quest’ultima modalità di dichiarazione è molto comune nei linguaggi di
programmazione come C++ e Java.

Snippet 2.4 Dichiarazione di variabili a partire dallo standard C99.


{
int a = 10;
int b = 100;

/* le due istruzioni printf stampano i valori


su un'unica linea perché la prima istruzione
printf non va a capo */
printf("%d - %d - ", a, b); /* 10 - 100 - */
int c = 1000;
printf("%d\n", c); /* 1000 */
/* altre istruzioni... */
}

Se proviamo a compilare lo Snippet 2.4 con il comando di compilazione gcc e i flag -


std=c89 e -pedantic-errors che forzano all’utilizzo delle regole proprie dello standard C89,
avremo il seguente errore: error: ISO C90 forbids mixed declarations and code [-Wpedantic].

Come già detto, dopo aver dichiarato il tipo, si deve scrivere un identificatore, ovvero un
nome simbolico con cui referenziare la variabile per il suo utilizzo. Tale identificatore può
essere scritto utilizzando qualsiasi combinazione di lettere minuscole, maiuscole, numeri e
caratteri di sottolineatura (_). ma non può essere composto da più parole separate da spazi e
non può iniziare con un numero e in genere con caratteri che hanno a che fare con la sintassi
del linguaggio (si pensi alle parentesi ( ), ai simboli di relazione > < e così via) o che ne
rappresentino una keyword riservata (per esempio int, struct, auto e così via).
Inoltre gli identificatori sono case-sensitive, nel senso che si fa distinzione tra lettere
minuscole e lettere maiuscole; inoltre, secondo lo standard C11, un compilatore dovrebbe
almeno considerare i primi 63 caratteri come significativi per un identificatore interno o un
nome di una macro e i primi 31 caratteri per gli identificatori esterni. Per lo standard C89,
invece, i limiti considerabili potevano essere, rispettivamente, fino a 31 caratteri e fino a 6
caratteri. Quanto detto significa che se si hanno due identificatori interni lunghi 64 caratteri
allora una qualche implementazione di C potrebbe considerali sia come nomi diversi sia
come nomi uguali perché ha tenuto conto solo dei primi 63 caratteri (per il compilatore gcc
tutti i caratteri di un identificatore interno sono significativi mentre per i nomi esterni il
numero di caratteri significativi è definito dal linker).

NAMING CONVENTION
Per naming convention si intende la regola di scrittura utilizzata per la denominazione degli
elementi di un programma. Nel linguaggio C esistono molte convenzioni di denominazione
degli elementi e, tuttavia, nessuna si può dire migliore o peggiore di un’altra; ciascuna
esprime, alla fine, un gusto personale del programmatore. L’importante, ai fini della leggibilità
del codice, è seguire la stessa convenzione di denominazione per tutto il programma e non
frapporla ad altre. Per esempio, una delle convenzioni comunemente usate prevede che la
scrittura degli identificatori di variabili e funzioni sia effettuata con tutte le lettere minuscole
(number, push, sort e così via). In caso gli identificatori siano formati da più parole, possono
essere separati dal carattere underscore (tipo next_line, get_score, make_average e così via). La
scrittura, invece, delle macro è effettuata utilizzando tutte le lettere maiuscole (tipo
STDIN_FILENO, EOF, BUFFER e così via). Un’altra convenzione, molto utilizzata in ambito Java o C#,

ma che sta prendendo piede anche in C e in C++, è quella che prevede che le strutture si
scrivano usando la notazione UpperCamelCase (Pascal Case), in cui l’identificatore, se
formato da più parole, viene scritto tutto unito e ogni parola inizia con la lettera maiuscola (per
esempio BookInformations), mentre le variabili e le funzioni si scrivono utilizzando la notazione
definita lowerCamelCase in cui, se l’identificatore è formato da più parole, lo stesso viene
scritto tutto unito, e la prima parola inizia con la minuscola mentre le altre con la maiuscola
(per esempio nextLine, getScore, makeAverage e così via).

NOTA
È buona prassi in C non dichiarare variabili con identificatori che inizino con uno _ o due __

caratteri di underscore perché essi sono impiegati estensivamente per i nomi dei tipi
dichiarati all’interno dei file della libreria standard.

DETTAGLIO
A partire dallo standard C99 è possibile scrivere identificatori utilizzando caratteri (UCN,
Universal Character Names) presi da un set di caratteri esteso (per dettagli si rimanda al
Capitolo 11). Così un identificatore come _Bool conterò dove è presente il carattere ò
normalmente non riconoscibile potrà essere scritto come: _Bool conter\u00F2, dove \u00F2 è il
code point, per l’appunto, del carattere ò (LATIN SMALL LETTER O WITH GRAVE).

Snippet 2.5 Alcuni identificatori.


int number_1; // CORRETTO
int number 1; // ERRORE - l'identificatore è separato da un carattere di spazio
int 1number; // ERRORE - l'identificatore inizia con un carattere numerico

// a e A sono variabili DIVERSE!!!


int a;
int A;

Inizializzazione
Un’inizializzazione (Sintassi 2.2) è una statement (assignment statement) attraverso la
quale si indica al compilatore di assegnare, scrivere, un determinato valore nella locazione
di memoria precedentemente predisposta da una corrispettiva istruzione di dichiarazione. A
tal fine si utilizza l’operatore di assegnamento contraddistinto dal carattere uguale (=) e la
stessa istruzione è terminata, come di consueto, dal carattere punto e virgola (;).

Sintassi 2.2 Inizializzazione di un variabile.


data_type identifier = value;

Lo Snippet 2.6 evidenzia la dichiarazione di una variabile di tipo int di nome current_line
e la sua inizializzazione con il valore numerico 10 sempre di tipo intero (Figura 2.2).

Snippet 2.6 Dichiarazione e inizializzazione di una variabile di tipo int.


int current_line = 10;

Figura 2.2 Rappresentazione della variabile current_line dopo la sua inizializzazione.


Ogni dichiarazione/inizializzazione può essere effettuata su più variabili in una sola riga
utilizzando il simbolo di virgola (,) oppure scrivendo prima la dichiarazione e poi
l’inizializzazione (Snippet 2.7). Le inizializzazioni dello Snippet 2.7 sono tutte effettuate
con valori definiti letterali.

Snippet 2.7 Alcune dichiarazioni e inizializzazioni.


// dichiarazione e inizializzazione di alcune variabili
int nr1 = 44, nr2 = 55;
char my_str[] = "C is a great programming language!!!";

// dichiarazione
float fl1, fl2;
// inizializzazione
fl1 = 33.33f;
fl2 = 44.44f;

Una variabile può ottenere un valore anche in modo dinamico, ovvero mediante la
valutazione di un’espressione (Snippet 2.8).

Snippet 2.8 Valore dinamico.


#include <math.h>
...
double db = sqrt(44.44);

Nello Snippet 2.8 la variabile db di tipo double otterrà un valore che è il risultato del
calcolo della radice quadrata di 44.44, dopo l’invocazione della funzione sqrt dichiarata nel
file header <math.h> della libreria standard.
Costanti
Una costante rappresenta uno spazio di memoria a sola lettura (read-only), ovverosia una
locazione di storage dove è memorizzato un valore che non può essere più alterato dopo che
vi è stato assegnato.
Per far sì che un oggetto sia costante è necessario anteporre al tipo di dato relativo un
qualificatore di tipo espresso mediante la keyword const (Sintassi 2.3).

Sintassi 2.3 Qualificatore di tipo const.


const data_type identifier = value;

TERMINOLOGIA
Una qualificatore di tipo (type qualifier) è un attributo applicato a uno specificatore di tipo
che modifica le proprietà di un oggetto come una variabile o una costante.

Per utilizzare correttamente un oggetto costante è importante comprendere che questo


deve essere inizializzato contestualmente alla sua dichiarazione quando impiegato con i tipi
basici del linguaggio (Snippet 2.9).

Snippet 2.9 Utilizzo di const.


// CORRETTO!!!
const float PI = 3.14159f;

// sintassi alternativa accettata


short const count = 10;

// sintassi accettata: i qualificatori sono idempotenti


const const const _Bool ok = 1; // lo stesso di const _Bool

// ERRORE!!!
const int FLAG;
FLAG = 1000; // assignment of read-only variable 'FLAG'

Il frammento di codice evidenzia come la costante di tipo float PI sia correttamente


inizializzata con un valore, mentre la costante di tipo int FLAG produce un errore di
compilazione se proviamo a inizializzarla successivamente alla sua dichiarazione.
Notiamo altresì come sia possibile scrivere il qualificatore const dopo lo specificatore di
tipo (costante count), così come sia permesso ripeterlo più volte (costante ok) perché il
compilatore ignorerà quelli superflui (i qualificatori hanno la proprietà di idempotenza).
La possibilità di definire oggetti costanti piuttosto che mere variabili porta numerosi
benefici, tra i quali: il compilatore può, eventualmente, compiere certi tipi di ottimizzazioni
in quanto il valore della costante non può cambiare durante l’esecuzione del programma;
permette di “documentare” il programma avvisando chi legge o utilizza il sorgente che un
oggetto non è modificabile; consente di rendere più “scalabile” un programma quando si
deve cambiare il valore di una costante nelle parti di programma che l’hanno impiegata (un
cosa è cambiare “letteralmente” un valore costante in tutte le parti di programma, per
esempio sostituire tutte le occorrenze di 3.14159f con 3.1415f, un’altra cosa è cambiare quel
valore solo nella definizione della costante, per esempio const float PI = 3.14159f con const
float PI = 3.1415f; infatti, in quest’ultimo modo, tutte le occorrenze dell’identificatore PI,
quando valutate, ritorneranno il nuovo valore, ossia 3.1415).
CURIOSITÀ
I letterali numerici, come nel caso del nostro 3.14159f, scritti direttamente nell’ambito di un
sorgente (hardcoded) e ripetuti più volte, sono spesso indicati con il termine di numeri
magici (magic numbers); l’etimologia si fa risalire ai sistemi Unix, dove gli stessi sono nati
per identificare il formato dei file binari. Infatti, i file binari hanno dei numeri magici posti,
tipicamente, nei primi byte iniziali che ne descrivono la tipologia: per esempio, se apriamo
un file PDF con un programma che ne fa il dump in esadecimale, vedremo che i primi 4 byte
hanno il valore 25 50 44 46 che rappresentano il codice ASCII di %PDF.

La direttiva #define: un cenno contestuale


In C, è possibile definire delle costanti simboliche utilizzando un altro meccanismo che
passa attraverso la scrittura di un’apposita direttiva per il preprocessore espressa tramite il
comando #define (Sintassi 2.4).

Sintassi 2.4 Direttiva #define per esprimere delle costanti.


#define macro_identifier replacement_value

La sintassi della direttiva #define è più articolata di quanto illustrato ma, per il nostro
attuale obiettivo didattico, quella presentata è più che sufficiente per mostrare una delle sue
caratteristiche essenziali: quella di fungere da mezzo per scrivere delle semplici macro
(object-like macro) che agiscono come simboli di costanti che sono sostituiti dal
preprocessore nel codice sorgente con i relativi valori assegnati.
Nella terminologia adottata da Kernighan e Ritchie queste semplici macro sono definite
anche costanti manifeste (manifest constants) e associano: in linea generale, nomi sostituiti
da sequenze di caratteri; in modo più specifico, nomi sostituiti a valori di tipo numerico, di
tipo carattere o di tipo stringa (Listato 2.1).

Listato 2.1 ManifestConstants.c (ManifestConstants).


/* ManifestConstants.c :: Mostra come creare simboli di costanti con #define :: */
#include <stdio.h>
#include <stdlib.h>

#define PI 3.14159f /* valore: costante numerica */


#define ADDRESS 0x0040f840 /* valore: costante numerica in esadecimale */
#define NL '\012' /* valore: costante carattere in ottale */
#define BEEP '\a' /* valore: costante carattere */
#define HELLO "Un saluto a tutti!" /* valore: costante stringa */
#define BEGIN { /* un carattere qualsiasi */
#define END } /* un carattere qualsiasi */

int main(void)
BEGIN // {
float raggio = 5.0f;

float area = PI * raggio * raggio;


//float area = 3.14159f * raggio * raggio;

const int *ptr = (const int *) ADDRESS;


// const int *ptr = (const int *)0x0040f840;

putchar(BEEP);
// putchar('\a');

printf(HELLO);
// printf("Un saluto a tutti!");

putchar(NL);
// putchar('\012');

return (EXIT_SUCCESS);
END // }

Output 2.1 Dal Listato 2.1 ManifestConstants.c.


Un saluto a tutti!

Il Listato 2.1 mostra come creare attraverso una serie di direttive #define delle macro i cui
nomi, per prassi consolidata scritti con le lettere maiuscole, saranno sostituiti nel sorgente
dai corrispettivi valori. Nella funzione main sono indicate delle istruzioni che fanno uso di
specifiche direttive #define; al di sotto di ciascuna di esse, attraverso dei commenti, sono
indicate le sostituzioni con gli effettivi valori che saranno compiute dal preprocessore prima
dell’avvio della compilazione.
NOTA
La direttiva #define e le altre direttive del preprocessore saranno illustrate in dettaglio nel
Capitolo 10.
Tipi di dato fondamentali
Le variabili e le costanti contengono un valore che è dipendente dal tipo di dato scelto in
fase di dichiarazione. In C i tipi di dato utilizzabili sono quelli esprimibili attraverso le
keyword indicate nella Tabella 2.1 dove, in breve, un tipo char consente di memorizzare
singoli caratteri, un tipo int consente di memorizzare numeri interi, i tipi float e double
consentono di memorizzare numeri decimali, un tipo _Bool consente di memorizzare i valori
booleani false e true e i tipi _Complex e _Imaginary consentono di rappresentare i numeri
complessi e immaginari. I tipi short, long, signed e unsigned consentono, invece, laddove
previsto, di apportate variazioni di semantica ai tipi basici.
Tabella 2.1 Keyword del linguaggio C per esprimere i tipi di dato.
char (K&R) int (K&R) long (K&R) _Bool (C99)

double (K&R) short (K&R) unsigned (K&R) _Complex (C99)


float (K&R) signed (C90) void (C90) _Imaginary (C99)

Tipi interi
Un tipo intero (integer type) rappresenta un valore numerico intero, ossia un numero
senza la parte frazionaria o decimale oppure, detto in altri termini, senza il simbolo punto e
le cifre numeriche poste dopo di esso.
Il tipo intero fondamentale è espresso attraverso la keyword int e ha, generalmente, una
dimensione di 32 bit sui computer moderni e una di 16 bit sui computer meno recenti. Di
default un tipo int è altresì signed, ovvero con segno, e accetta quindi anche valori negativi.

VALORI SIGNED E VALORI UNSIGNED


Un intero si dice con segno (signed integer) se il bit di sinistra più significativo (sign bit) è
utilizzato per rappresentare o meno il suo valore come negativo (bit impostato a 1) o positivo
(bit impostato a 0). Per esempio, il più grande valore di un intero a 16 bit avrà una
rappresentazione binaria di 0111111111111111, che corrisponderà al valore di 32767, ossia 215 - 1.
Il più grande valore di un intero a 32 bit avrà una rappresentazione binaria di
01111111111111111111111111111111, che corrisponderà al valore di 2147483647, ossia 231 - 1. Di

converso, un intero si dice senza segno (unsigned integer) se il bit di sinistra più significativo
è considerato parte della magnitudine del numero. Così il più grande valore di un intero a 16
bit avrà una rappresentazione binaria di 1111111111111111 che corrisponderà al valore di 65535,
ossia 216 - 1, mentre il più grande valore di un intero a 32 bit avrà una rappresentazione
binaria di 11111111111111111111111111111111, che corrisponderà al valore di 4294967295, ossia 232 -
1.
Al tipo int è possibile anteporre altre keyword che consentono di variare il range di valori
utilizzabili e di decidere se i numeri negativi devono essere impiegati.
Esse sono: unsigned, con cui si stabilisce che il tipo intero non può accettare valori
negativi; short, con cui si stabilisce che il tipo intero può occupare meno spazio di
memorizzazione del tipo int; long, con cui si stabilisce che il tipo intero può occupare più
spazio di memorizzazione del tipo int.
Le keyword citate possono essere combinare nei seguenti modi, in ordine crescente di
dimensione, che producono differenti tipi laddove altre combinazioni non sono altro che
sinonimi di questi tipi:
short int (abbreviato usualmente in short che ne è un sinonimo);
unsigned short int (abbreviato usualmente in unsigned short che ne è un sinonimo);
int (scritto alcune volte come signed che ne è un sinonimo);
unsigned int (abbreviato usualmente in unsigned che ne è un sinonimo);
long int (abbreviato usualmente in long che ne è un sinonimo);
unsigned long int (abbreviato usualmente in unsigned long che ne è un sinonimo);
long long int (abbreviato usualmente in long long che ne è un sinonimo);
unsigned long long int (abbreviato usualmente in unsigned long long che ne è un
sinonimo).

NOTA
La keyword signed può essere utilizzata con tutti i tipi con segno per rendere esplicita la
volontà di utilizzare anche valori negativi. Così, signed short int oppure short int oppure
short sono tutti nomi di tipo per la stessa specie di dato.

SUGGERIMENTO
C consente di abbreviare il nome dei tipi interi utilizzabili “eliminando” la keyword int quando
combinata con altre keyword. Così, se dovete utilizzare uno short int potete scrivere
direttamente short; se dovete utilizzare un long int potete scrivere direttamente long.

L’intervallo di valori rappresentabile dai tipi di dato descritti varia da sistema a sistema.
Lo standard C11, tuttavia, stabilisce delle regole cui gli implementatori dei compilatori
devono sottostare.
La prima: un tipo int non deve mai essere più piccolo di un tipo short e un tipo long
non deve mai essere più piccolo di un tipo int (è possibile, comunque, che uno short
rappresenti lo stesso range di valori di un int oppure che un int rappresenti lo stesso
range di valori di un long). Questa regola è importante perché permette agli
implementatori di “adattare” la dimensione dei tipi al sistema target di destinazione.
La seconda: i tipi short, int e long e rispettive varianti devono garantire un certo range
di valori, ovvero devono essere almeno uguali o più grandi rispetto a quelli mostrati
dalla Tabella 2.2.

NOTA
Lo standard C11 asserisce che i valori dei tipi dovranno essere uguali o più grandi in
magnitudine rispetto a quelli da esso previsti così come elencati nella Tabella 2.2.
Tipicamente, come evidenziato dalle prossime tabelle, i valori minimi dei tipi differiscono di
un’unità e ciò è perfettamente lecito. Per esempio, per lo standard, il valore minimo di uno
short int è -32767 (espressione: -(2n-1 -1)) ma, in molte delle più comuni implementazioni,

tale valore minimo è -32768 (espressione: -(2n-1)) e ciò perché è utilizzato il complemento a
due per la rappresentazione dei numeri con segno. In definitiva ciò implica che si può
essere sicuri che nei compilatori aderenti allo standard un short int avrà almeno un valore
minimo di -32767, che però potrà essere anche maggiore come, per esempio, -32678.

Tabella 2.2 Range di valori da garantire secondo lo standard C11.


Tipo Valore minimo Valore massimo
short int -32767 // -(215 - 1) 32767 // 215 - 1
unsigned short int 0 65535 // 216 - 1
int -32767 // -(215 - 1) 32767 // 216 - 1
unsigned int 0 65535 // 216 - 1
long int -2147483647 // -(231 - 1) 2147483647 // 231 - 1
unsigned long int 0 4294967295 // 232 - 1
long long int -9223372036854775807 // -(263 - 1) 9223372036854775807 // 263 - 1
unsigned long long int 0 18446744073709551615 // 264 - 1

Per completezza illustriamo anche i range di valori usualmente rappresentati su sistemi a


16 bit, 32 bit e 64 bit (Tabelle 2.3, 2.4 e 2.5), ribadendo ancora una volta che tali ampiezze
non sono obbligatorie e pertanto potrebbero anche non corrispondere su alcuni sistemi.
Tabella 2.3 Range di valori su un sistema a 16 bit (uno short int e un int hanno la stessa
ampiezza).
Tipo Valore minimo Valore massimo
short int -32768 32767
unsigned short int 0 65535
int -32768 32767
unsigned int 0 65535
long int -2147483648 2147483647
unsigned long int 0 4294967295

Tabella 2.4 Range di valori su un sistema a 32 bit (un int e un long int hanno la stessa
ampiezza).
Tipo Valore minimo Valore massimo
short int -32768 32767
unsigned short int 0 65535
int -2147483648 2147483647
unsigned int 0 4294967295
long int -2147483648 2147483647
unsigned long int 0 4294967295

Tabella 2.5 Range di valori su un sistema a 64 bit (un long int e un long long int hanno la stessa
ampiezza).
Tipo Valore minimo Valore massimo
short int -32768 32767
unsigned short int 0 65535
int -2147483648 2147483647
unsigned int 0 4294967295
long int -9223372036854775808 9223372036854775807
unsigned long int 0 18446744073709551615
long long int -9223372036854775808 9223372036854775807
unsigned long long int 0 18446744073709551615

DETTAGLIO
Per verificare sul sistema in uso il range effettivo di valori utilizzabili è possibile dare uno
sguardo all’interno del file header <limits.h> che è parte della libreria standard di C. Infatti, in
esso sono presenti delle macro, tipo #define INT_MAX 2147483647 e #define INT_MIN (-INT_MAX-1),

che indicano i valori massimi e i valori minimi rappresentabili per un dato di tipo int.

Letterali interi
Un letterale intero è un qualsiasi numero tipo 100, 22, -30 e così via, che rappresenta una
sorta di costante intera, cioè un valore non alterabile senza la parte decimale che è scritto
direttamente nel flusso testuale del codice sorgente.
Una costante intera può essere espressa: in base decimale, quando contiene solo cifre
numeriche da 0 a 9; in base ottale, quando contiene solo cifre numeriche da 0 a 7 e le si deve
anteporre il prefisso 0; in base esadecimale, quando contiene cifre numeriche da 0 a 9 e
lettere da a o A a f o F e le si deve anteporre il prefisso 0x o 0X (zero-ex).
Inoltre, è importante comprendere che nel linguaggio C non ha alcuna importanza con
quale base numerica si decide di rappresentare una costante intera nell’ambito del sorgente
perché tale decisione non influisce su come il corrispettivo valore sarà memorizzato nel
computer. Infatti, scrivere 100, 0144 oppure 0x64 memorizzerà sempre allo stesso modo tale
valore, ossia come numero binario e dunque come 0000000001100100 considerando solo i
primi 16 bit.
Il programma seguente (Listato 2.2) mostra come, dato un numero, sia possibile
stamparne tramite la funzione printf l’equivalente rappresentazione decimale, ottale ed
esadecimale.

Listato 2.2 IntegerConstants.c (IntegerConstants).


/* IntegerConstants.c :: Stampa un numero in base 10, 8 e 16 :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int number;

// istruzioni di input/output
printf("Digita un numero intero: ");
scanf("%d", &number);

printf("Il numero %d espresso in base 10 ha le seguenti "


"rappresentazioni:\n", number);
printf("in base 8: %#o\n", number);
printf("in base 16: %#x\n", number); // %#X stampa 0X con la X in maiuscolo

return (EXIT_SUCCESS);
}

Output 2.2 Dal Listato 2.2 IntegerConstants.c.


Digita un numero intero: 250
Il numero 250 espresso in base 10 ha le seguenti rappresentazioni:
in base 8: 0372
in base 16: 0xfa

In pratica la funzione printf, attraverso gli specificatori di formato %#o e %#x, attua le
rispettive conversioni in base ottale e in base esadecimale del numero in base decimale
fornito in input dalla funzione scanf (se dagli specificatori di formato si omette il carattere
cancelletto #, i prefissi ottale 0 ed esadecimale 0x non saranno visualizzati nell’output della
printf).

DETTAGLIO
La funzione scanf della libreria standard, dichiarata nel file header <stdio.h>, consente di
ottenere dallo standard input (generalmente la tastiera) la sequenza di caratteri digitati. Tali
caratteri, se possibile, sono poi convertiti in un valore corrispondente al tipo di dato indicato
dal relativo specificatore di formato passato come primo argomento, e tale valore è posto
nella variabile indicata dal secondo argomento. Alle funzioni scanf e printf sarà dedicato un
apposito paragrafo nel Capitolo 11.

La funzione scanf del sorgente in esame attende che l’utente digiti da tastiera un numero
intero in base 10 (specificatore %d uguale a quello della funzione printf) e poi ne memorizza
il risultato nella variable number (tralasciando per ora i dettagli motivazionali
dell’apposizione del carattere & come prefisso a number, diciamo per ora che esso fornisce
l’indirizzo di memoria di number dove memorizzare il valore letto dalla tastiera ed
eventualmente convertito).
I letterali interi, infine, hanno un tipo associato che è dipendente dal valore che
esprimono e che è generalmente di tipo int.
In ogni caso a seconda della dimensione del valore del letterale intero e alla base
numerica in cui è espresso lo stesso potrà essere tipizzato dal compilatore con l’esatto tipo
atto a contenerlo secondo le seguenti regole, che indicano i tipi che in successione verranno
provati come idonei a rappresentare il relativo valore:
letterale intero in base decimale: int, long int, long long int;

letterale intero in base ottale o esadecimale: int, unsigned int, long int, unsigned long

int, long long int, unsigned long long int.

Infine è importante dire che è possibile “forzare” un letterale intero a essere trattato
esplicitamente come:
un unsigned int aggiungendogli il suffisso u o U;
un long int aggiungendogli il suffisso l o L;
un unsigned long int aggiungendogli il suffisso ul o UL;
un long long int aggiungendogli il suffisso ll o LL;
un unsigned long long int aggiungendogli il suffisso ull oppure ULL.

NOTA
L’ordine di scrittura dei suffissi U e L non ha importanza così come se sono scritti in
maiuscolo oppure in minuscolo.

In questi ultimi casi, però, l’ordine di ricerca dell’esatto tipo attribuibile al letterale intero
sarà diverso rispetto a quanto indicato in precedenza, perché il compilatore terrà anche in
considerazione il significato dei suffissi. Per esempio, se un letterale intero in base 10 ha il
suffisso L, allora i tipi ricercati saranno, nell’ordine: long int e long long int e così via per gli
altri suffissi (in pratica il suffisso indica il tipo da cui partire fino al massimo tipo
rappresentabile sul sistema target considerando che per i letterali interi espressi in base
diversa da 10 l’ordine tiene conto anche dei tipi unsigned).
Il Listato 2.3 mostra come utilizzare con la funzione printf gli specificatori di formato
che consentono di stampare correttamente l’esatto tipo di un valore di tipo intero.

Listato 2.3 PrintIntegerTypes.c (PrintIntegerTypes).


/* PrintIntegerTypes.c :: Stampa tutti i tipi interi :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
// tipi di dato intero considerando i valori garantiti dallo standard C11
short s = -10000;
unsigned short us = 60000;

int i = -32000;
unsigned int ui = 40000U;

long l = -1000000000L;
unsigned long ul = 4000000000UL; // equivalente LU

long long ll = -45000000000000000LL;


unsigned long long ull = 17000000000000000000ULL; // equivalente LLU

// stampa dei valori usando i corretti specificatori di formato


printf("short: %hd\n", s); // %ho o %hx stampano uno short in ottale o esadecimale
printf("unsigned short: %hu\n", us);

printf("int: %d\n", i); // %o o %x stampano un int in ottale o esadecimale


printf("unsigned int: %u\n", ui);

printf("long: %ld\n", l); // %lo o %lx stampano un long in ottale o esadecimale


printf("unsigned long: %lu\n", ul);

// %llo o %llx stampano un long long in ottale o esadecimale


printf("long long: %lld\n", ll);
printf("unsigned long long: %llu\n", ull);

return (EXIT_SUCCESS);
}

Output 2.3 Dal Listato 2.3 PrintIntegerTypes.c.


short: -10000
unsigned short: 60000
int: -32000
unsigned int: 40000
long: -1000000000
unsigned long: 4000000000
long long: -45000000000000000
unsigned long long: 17000000000000000000

In pratica per stampare uno short si antepone il carattere h ai caratteri d, o oppure x; per
stampare un long si antepone il carattere l ai caratteri d, o oppure x; per stampare un long long

si antepongono i caratteri ll ai caratteri d, o oppure x; per stampare degli unsigned si usa il


carattere u anteponendogli, a seconda del tipo, i caratteri h, l oppure ll (per il tipo int non si
antepone il carattere d).

Tipi in virgola mobile


Un tipo in virgola mobile (floating-point type) rappresenta un valore numerico non intero,
ossia un numero decimale che è formato da una parte intera (posta prima del carattere
punto) e una parte frazionaria (posta dopo il carattere punto).
Il termine “in virgola mobile” indica una modalità di rappresentazione dei numeri reali
laddove, fissata una base B, un numero N è rappresentato dalla coppia M (mantissa) ed E
(esponente) nel seguente modo: N = M * BE. Per esempio, il numero 50.6 può essere
indicato come 0.506×102, 0.0506×103, 5.06×10, 506×10-1 e così via, dove si vede come il punto
decimale di separazione flotta, ovvero si sposta in diverse posizioni (in pratica il valore
dell’esponente determina dove il punto decimale di separazione deve essere posizionato
relativamente all’inizio della mantissa, detta anche significando).
APPROFONDIMENTO
Un numero reale può essere rappresentato anche con la notazione in “virgola fissa”, dove vi
è un numero fisso di cifre prima e dopo il punto decimale di separazione tra la parte intera e
la parte frazionaria.

Il tipo in virgola mobile può essere espresso attraverso una delle seguenti keyword: float,
double e long double che, rispettivamente, consentono di indicare numeri in floating point in
precisione singola, in precisione doppia e con una precisione estesa o multipla.
In pratica maggiore è la precisione, da una più bassa offerta da un tipo float a una più
alta offerta dal tipo long double, maggiori saranno sia il range di valori rappresentabile sia
l’accuratezza di precisione per i calcoli. Tutto ciò è evidenziato dalla Tabella 2.6, che
riporta questi valori in accordo con lo standard IEEE 754 (conosciuto anche come IEC
60559), che è quello indicato come sistema di rappresentazione dei tipi in virgola mobile
dallo standard C11 e che le implementazioni dovrebbero quindi seguire.
Tabella 2.6 Range di valori dei tipi ini virgola mobile come da standard IEEE 754.
Tipo Valore minimo Valore massimo Precisione Bit
float -3.4×10-38 3.4×1038 6 cifre 32
double -1.7×10-308 1.7×10308 15 cifre 64
long double -1.19×10-4932 1.19×104932 18 cifre >=79

APPROFONDIMENTO
Lo standard internazionale IEEE 754 (IEEE Standard for Binary Floating-Point Arithmetic)
definisce delle regole per i sistemi di computazione in virgola mobile, ovvero formalizza
come essi devono essere rappresentati, quali operazioni possono essere compiute, le
conversioni operabili e come devono essere gestite le condizioni di eccezione come, per
esempio, la divisione per 0. I formati esistenti sono: a precisione singola (32 bit), a
precisione singola estesa (>= 43 bit), a precisione doppia (64 bit) e a precisione doppia
estesa (>= 79 bit).

Letterali in virgola mobile


Un letterale in virgola mobile è un qualsiasi numero tipo 44.66, -232.678, 325685.99 e così
via, che rappresenta una sorta di costante decimale ossia un valore non alterabile (formato
da una parte intera, il carattere separatore punto . e una parte frazionaria) che è scritto
direttamente nel flusso testuale del codice sorgente.
Una costante decimale può essere espressa secondo la notazione decimale convenzionale
degli esempi suindicati, ma anche secondo una notazione definita come notazione
esponenziale (e-notation) che è una notazione scientifica che prevede la scrittura di un
numero decimale espresso nel seguente modo: una sola cifra diversa da 0, il carattere
separatore punto ., il suffisso E (o e) e un numero positivo (preceduto opzionalmente dal
carattere più +) o negativo (preceduto dal carattere meno -) che rappresenta una potenza di
10 per cui il numero deve essere moltiplicato.
Ritornando ai nostri numeri avremo allora che 44.66 si potrà scrivere come 4.466e1,
-232.678 si potrà scrivere come -2.32678e2, 325685.99 si potrà scrivere come 3.2568599e5.
SUGGERIMENTO
Per comprendere come “tradurre” un numero espresso in notazione standard in un numero
espresso in notazione esponenziale si può procedere adottando le seguenti regole: se il
punto separatore deve essere spostato verso sinistra allora la potenza di 10 sarà positiva e
ogni spostamento del punto farà aggiungere 1 all’esponente; se il punto separatore deve
essere spostato verso destra allora la potenza di 10 sarà negativa e ogni spostamento del
punto farà togliere 1 all’esponente. Così il numero 123.12 potrà essere indicato come
1.2312e2 perché il punto è stato spostato verso sinistra fino alla cifra 1 iniziale di due
posizioni. Viceversa il numero 0.454 potrà essere indicato come 4.54e-1 perché il punto è
stato spostato verso destra dalla cifra 0 iniziale di una posizione.

Di default un letterale decimale è di tipo double. Tuttavia è possibile utilizzare il suffisso F


o f per far sì che una costante decimale sia di tipo float (per esempio 45.99f) e il suffisso L o
l per far sì che una costante decimale sia di tipo long double (per esempio 10.4456e3l).

Listato 2.4 FloatingPointType.c (FloatingPointType).


/* FloatingPointType.c :: Mostra l'uso dei tipi in virgola mobile :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
float a_float = 1234.444f; // suffisso f per letterale float
double a_double = 4.58e-2; // letterale double in notazione esponenziale
long double a_ldouble = 1.660538921e-27L; // unità di massa atomica...

// letterali in virgola mobile espressi in notazione esponenziale esadecimale!


float f_in_hex = 0xa.cd47bp5;

// visualizza i valori in virgola mobile


printf("Valore di a_float: %.3f\n", a_float);
printf("Valore di a_double: %e\n", a_double); // mostralo in e-notation
printf("Valore di a_ldouble: %Le\n", a_ldouble);
printf("Valore di f_in_hex in esadecimale %a e in decimale %.2f\n",
f_in_hex, f_in_hex);

return (EXIT_SUCCESS);
}

Output 2.4 Dal Listato 2.4 FloatingPointType.c.


Valore di a_float: 1234.444
Valore di a_double: 4.580000e-002
Valore di a_ldouble: 1.660539e-027
Valore di f_in_hex in esadecimale 0xa.cd47bp+5 e in decimale 345.66
Il Listato 2.4 definisce una serie di variabili floating point ciascuna associata a un
differente letterale in virgola mobile che ne rappresenta l’esatto tipo di attribuzione.
Tra di esse è interessante rilevare la dichiarazione della variabile f_in_hex, il cui letterale è
espresso in una notazione esponenziale utilizzabile a partire dallo standard C99 che
permette di indicare le cifre in esadecimale precedute dal prefisso 0x o 0X, il prefisso p (va
bene anche il prefisso P) e un esponente che esprime una potenza di 2.
Le funzioni printf successive mostrano, invece, come a seconda del tipo in virgola
mobile sia possibile utilizzare i corretti specificatori di formato ossia: %f per i tipi float e
double in notazione convenzionale decimale; %e (o %E) per i tipi float e double in notazione
esponenziale; %Le (o %LE) per i tipi long double in notazione esponenziale (%Lf stampa un long
double in notazione convenzionale decimale); %a (o %A) per i tipi float e double in notazione
esponenziale con cifre in esadecimale ed esponente che esprime una potenza di 2 (%La o %LA
stampa un long double in quest’ultima modalità). Per quanto attiene al valore di f_in_hex,
espresso in notazione decimale come 345.66, esso deriva dall’elaborazione del valore
2
esadecimale 0xa.cd47bp5 ricavato dalla valutazione dell’espressione (10 + 12/16 + 13/16 +
3 4 5 5
4/16 + 7/16 + 11/16 ) * 2 , considerando le conversioni delle equivalenti cifre da
esadecimale a decimale (per esempio a = 10, c = 12, d = 13 e così via).

TIPI COMPLESSI E IMMAGINARI


A partire dallo standard C99 è stato introdotto il supporto per i tipi complessi, che consentono
di utilizzare i numeri complessi, cioè dei numeri formati da una parte reale e una parte
immaginaria. In matematica un numero complesso è espresso, per esempio, nella forma 6.7 +
5.9i, cioè dove i è rappresentato dalla radice quadrata di -1. I tipi complessi impiegabili in C
sono float _Complex, double _Complex e long double _Complex, che memorizzano, rispettivamente, i
relativi valori complessi come un array di due elementi di tipo float, double e long double.

Nell’array, il primo elemento rappresenta la parte reale mentre il secondo elemento


rappresenta la parte immaginaria. Lo standard C99 consente anche di rappresentare solo i
tipi immaginari attraverso i tipi float _Imaginary, double _Imaginary e long double _Imaginary. In
questo caso il valore del tipo è dato da un numero reale che rappresenta la parte
immaginaria. In più, oltre al supporto built-in per i numeri complessi, sempre a partire dallo
standard C99, è previsto un header della libreria standard denominato <complex.h> che fornisce
molteplici prototipi di funzione e diverse macro che consentono di operare in modo completo e
facilitato con l’aritmetica dei numeri complessi. Per lo standard C99 il supporto ai tipi
complessi è obbligatorio mentre quello per i tipi immaginari è opzionale, mentre per lo
standard C11 il supporto per entrambi i tipi è opzionale.

Tipi carattere
Un tipo carattere (character type) rappresenta un’unità di informazione atta a contenere
sia un valore che è un simbolo (per esempio una lettera dell’alfabeto, un segno di
punteggiatura e così via) che corrisponde a un grafema visualizzabile di un determinato
linguaggio naturale, sia un qualsiasi altro valore che è un codice di controllo che non
corrisponde ad alcun grafema e che è, quindi, non stampabile (per esempio, il carriage
return, il bell, il backspace e così via).
Il valore attribuito a uno specifico carattere è dipendente dal sistema di codifica di
caratteri (character set) adottato dal sistema in uso.
Tra questi, quello più comune è l’ASCII (American Standard Code for Information
Interchange), un sistema di codifica dei caratteri a 7 bit basato sull’alfabeto inglese che è in
grado di codificare 128 caratteri (codici da 0 a 127) tra quelli stampabili e quelli non
stampabili (esso è spesso riferito come US-ASCII).
Su molti sistemi, talune volte, è tuttavia adottata una versione estesa dell’originario
sistema ASCII a 7 bit denominata Latin-1 (ISO 8859-1), che prevede la possibilità di
rappresentare un più alto numero di caratteri (per esempio quelli con le lettere accentate)
grazie a una codifica che utilizza 8 bit (codici da 0 a 255).
In pratica la codifica Latin-1 consente di rappresentare i caratteri in uso nelle lingue dei
paesi dell’Europa Occidentale e in molti paesi dell’Africa.
NOTA
ISO 8859-1 è una delle 16 parti dello standard conosciuto come ISO/IEC 8859 che definisce
sistemi per la codifica di caratteri a 8 bit per il supporto delle lingue in uso in molti paesi del
mondo, a esclusione però di quella cinese, coreana, giapponese e vietnamita. Per esempio,
la parte denominata ISO-8859-2 o Latin-2 prevede la codifica dei caratteri utilizzati nei paesi
dell’Europa dell’Est e del Centro, la parte denominata Latin/Greek prevede la codifica dei
caratteri del greco moderno e così via per le altre. Lo schema di codifica dei caratteri in tutte
le parti è isomorfa, ovvero avrà sempre questa forma di rappresentazione: i codici da 0 a
127 conterranno i caratteri ASCII standard; i codici da 128 a 159 conterranno i caratteri di
controllo non stampabili; i codici da 160 a 255 conterranno i caratteri variabili codificati in
accordo con il relativo linguaggio.

Il tipo carattere è espresso attraverso la keyword char e deve avere una dimensione che gli
consenta di memorizzare qualsiasi membro del character set del sistema in uso.
Tipicamente, tale dimensione è di 1 byte, che permette a un tipo char di contenere l’insieme
di caratteri dei sistemi di codifica a 8 bit.
Tabella 2.7 Range di valori del tipo char secondo lo standard C11.
Tipo Valore minimo Valore massimo
char -127 // -(27 - 1) 127 // 27 - 1
unsigned char 0 255 // 28 - 1

Tabella 2.8 Range di valori del tipo char secondo una tipica implementazione.
Tipo Valore minimo Valore massimo
char -128 127
unsigned char 0 255

Letterali carattere
Un letterale carattere è un qualsiasi carattere scritto tra singoli apici tipo 'A', '3', 'z' e
così via, che rappresenta una sorta di costante carattere, ossia un valore non alterabile che è
scritto direttamente nel flusso testuale del codice sorgente.
Quando il compilatore incontra un letterale carattere lo converte automaticamente nel
corrispettivo codice numerico così come rappresentato dal corrente set di caratteri.
Così, in un sistema che usa l’ASCII, i letterali 'A', '3', 'z' saranno convertiti nei valori di
tipo int 65, 51 e 122 e ciò non causerà alcun problema di memorizzazione nei correlativi tipi
char perché tali valori “rientreranno” perfettamente nel range di valori di un char.
Quanto detto, ossia che C tratta di fatto i caratteri come numeri interi, consente anche di
assegnare direttamente un valore numerico in un tipo char e di compiere le stesse operazioni
che si potrebbero compiere con in tipi numerici (Snippet 2.10).

Snippet 2.10 Assegnamento di un valore numerico a un tipo char.


char c = 122; // c vale 'z'
c -= 1; // c ora vale 'y' che ha il codice numerico 121
char c_2 = ' '; // c_2 vale 32
char c_3 = c - c_2; // c_3 vale 'Y' ossia 89 che è dato da 121 - 32

Anche se è possibile usare il tipo char come tipo per memorizzare “piccoli” valori interi
che richiedono solo un byte di spazio, bisogna considerare che inserire direttamente dei
valori interi con lo scopo di rappresentare i correlativi caratteri non è consigliabile per
motivi di portabilità perché differenti sistemi target potrebbero usare una codifica di
caratteri diversa, per esempio, dall’ASCII.
Le regole da seguire per utilizzare correttamente un tipo char dovrebbero essere, dunque,
le seguenti: se il tipo char è usato per memorizzare esplicitamente caratteri, allora si deve
scrivere lo stesso tra i singoli apici; se il tipo char è usato come tipo per piccoli interi che
non rappresentano, però, alcun carattere, allora si può scrivere un valore numerico che
rientra nel range di valori accettato da un tipo char.
NOTA
Lo standard del linguaggio C non specifica se un tipo char di default deve essere signed o
unsigned. Ogni implementazione lo può pertanto definire nell’uno o nell’altro caso. Per
“scoprire” per il compilatore in uso se un char è signed o unsigned si può leggere nel file
header <limits.h> la macro CHAR_MIN e verificare il suo valore: se vale 0, allora char sarà
unsigned se vale SCHAR_MIN, allora sarà signed.
All’interno dei letterali carattere possiamo altresì utilizzare lo speciale carattere
backslash, con simbolo \, definito carattere di escape, che consente di inserire dopo di esso:

1. caratteri speciali non inseribili dalla tastiera (per esempio quello che genera un beep
sonoro) oppure non visualizzabili in output (per esempio quello che genera un
backspace);
2. caratteri propri della definizione del letterale, come il carattere singolo apice (') e il
carattere backslash (\);
3. caratteri numerici (numeric escapes) che indicano, in ottale o in esadecimale il codice
del carattere che si desidera utilizzare.
Utilizzando il carattere backslash (\) insieme a uno dei caratteri indicati dai precedenti
punti si forma la cosiddetta sequenza di escape (escape sequence).
Per quanto attiene al punto 1 e al punto 2, la Tabella 2.9 mostra le sequenze di escape
costruibili (character escapes) e la relativa semantica considerando che per posizione attiva
(p.a.), termine utilizzato dallo standard, si intende una generalizzazione del concetto di
locazione, rispetto a un display device che può essere un monitor, una stampante e così via,
dove si posizionerebbe un carattere dopo una sua elaborazione.
Se il display device è un monitor, per esempio, la posizione attiva è comunemente
caratterizzata dal simbolo del cursore visibile in un command prompt o in una shell.
TERMINOLOGIA
Una sequenza trigraph (trigramma) è una successione di tre caratteri, i cui primi due sono
dati dai caratteri punti interrogativi (??), che consentono di rappresentare dei simboli che
potrebbero non essere forniti su alcune keyboard. Per esempio, il trigraph ??< viene
rimpiazzato nell’ambito di un file di codice sorgente con il simbolo parentesi graffa aperta
({). Nel Capitolo 11, nel paragrafo “Grafie alternative <iso646.h>”, sono mostrate tutte le
sequenze trigraph.

Il punto 3 consente, invece, di costruire delle sequenze di escape esprimendo i valori


numerici dei corrispondenti caratteri, in ottale oppure in esadecimale, secondo le regole di
scrittura della Tabella 2.10.
Tabella 2.9 Sequenze di escape (character escapes).
Sequenza Nome Semantica
\a Alert Produce un alert udibile o visibile senza cambiare la p.a.
\b Backspace Muove la p.a. alla precedente posizione sulla corrente riga.
\f Form feed Muove la p.a. all’inizio della prossima pagina.
\n Newline Muove la p.a. all’inizio della prossima riga.
Carriage
\r Muove la p.a. all’inizio della corrente riga.
return

\t
Horizontal Muove la p.a. alla successiva posizione orizzontale di tabulazione
tab sulla corrente riga.
\v Vertical tab Muove la p.a. alla successiva posizione verticale di tabulazione.
\\ Backslash Visualizza il carattere backslash (\).
Single
\' Visualizza il carattere apice singolo (').
quote
Double
\" Visualizza il carattere doppio apice (").
quote

\?
Question Visualizza il carattere punto interrogativo (?) evitando la sua
mark interpretazione come parte di una sequenza trigraph.

Tabella 2.10 Sequenze di escape (numeric escapes).


Sequenza Nome Semantica
\octal-digit[octal-digit][ Esprime un codice carattere in
Octal escape sequence
octal-digit] ottale.

\xhexadecimal-digits
Hexadecimal escape Esprime un codice carattere in
sequence esadecimale.

In pratica la sequenza di escape in ottale consente di scrivere un codice carattere usando il


carattere \ seguito da almeno una cifra ottale e massimo 3 (il suo valore massimo dovrebbe
essere 377 e rappresentabile come un unsigned char), mentre la sequenza di escape in
esadecimale consente di scrivere un codice carattere usando il carattere \ seguito da un
numero qualsiasi di cifre esadecimali che lo possa esprimere (il suo valore massimo,
comunque, dovrebbe essere FF se un carattere è rappresentabile come un unsigned char di 8
bit).

Snippet 2.11 Letterali caratteri espressi in decimale, ottale e in esadecimale.


// tutte rappresentazioni del carattere j
char ch_d = 106; // decimale
char ch_x = '\x6A'; // esadecimale
char ch_o = '\152'; // ottale

// qui 7CC vale 1996 in base 10 ed è fuori dal range massimo rappresentabile
// di un unsigned char che è FF, ossia 255 in base 10
char ch_illegal = '\x7CC'; // warning: hex escape sequence out of range

Listato 2.5 CharType.c (CharType).


/* CharType.c :: Mostra l'uso dei tipi char :: */
#include <stdio.h>
#include <stdlib.h>

// prototipo di funzione di toUpper


int toUpper(int);

int main(void)
{
char ch;

printf("Digita un carattere in \"minuscolo\" [ ]\b\b");

// ottiene un carattere dallo stdin...


scanf("%c", &ch);

// ...ne stampa una rappresentazione in maiuscolo


char ch_U = toUpper(ch);
printf("\007Maiuscolo del carattere %c digitato: %c [%d]\n", ch, ch_U, ch_U);

return (EXIT_SUCCESS);
}

// converte un carattere in maiuscolo


int toUpper(int ch)
{
if ('a' <= ch && ch <= 'z')
return ch - 'a' + 'A';

return ch; // se già maiuscolo o altro carattere ritornalo direttamente


}

Output 2.5 Dal Listato 2.5 CharType.c.


Digita un carattere in "minuscolo" [p]
Maiuscolo del carattere p digitato: P [80]

Il Listato 2.5 mostra come utilizzare lo specificatore di formato %c per ottenere in input
dalla funzione scanf un carattere così come per visualizzare in output con la seconda
funzione printf il medesimo carattere. Nel contempo, tale funzione printf usa lo
specificatore di formato %d per mostrare, dato lo stesso carattere, il suo codice numerico nel
corrente sistema di codifica caratteri che nel nostro caso è l’ASCII, e la sequenza di escape
\007 che è il codice ottale che consente la riproduzione o la visualizzazione di un alert

rispetto alla tabella ASCII (si poteva usare anche la sequenza di escape \a, che anzi sarebbe
stata preferibile al codice ottale per ragioni di portabilità).
La prima funzione printf mostra, invece, come impiegare all’interno di un letterale
stringa delle sequenze di escape: la prima, ovvero la double quote (\"), consente di
racchiudere la parola minuscolo tra doppi apici ed è necessaria perché se avessimo omesso il
carattere \ i primi caratteri " sarebbero stati visti dal compilatore come terminatori del
letterale stringa e la parola minuscolo, in quel contesto, non avrebbe avuto senso e gli avrebbe
fatto generare un errore tipo error: expected ')' before 'minuscolo'.

Le successive sequenze di escape, ovvero i backspace \b\b, consentono di spostare la


posizione attiva di due spazi all’indietro rispetto alla sua locazione corrente permettendo, di
fatto, di posizionare il cursore all’interno dei caratteri [ ].
La funzione toUpper, deputata a convertire un carattere da minuscolo a maiuscolo,
evidenzia in modo esplicito come un letterale carattere è nella pratica trattato dal
compilatore come un tipo numerico corrispondente al suo codice nel corrente sistema di
codifica dei caratteri. Infatti, nella struttura di selezione singola if, data una codifica ASCII
e un valore per ch come 112 (carattere p), la valutazione di 'a' <= ch && ch <= 'z' sarà
“tradotta” dal compilatore nel seguente modo 97 <= 112 && 112 <= 122 ossia sostituirà il
letterale 'a' con il relativo codice 97 e il carattere 'z' con il relativo codice 122. Lo stesso
avverrà con l’espressione ch - 'a' + 'A' ritornata dall’istruzione return, che sarà sostituita
con 112 - 97 + 65, ossia con una vera e propria espressione aritmetica.

Tipi booleani
Un tipo booleano (boolean type) rappresenta un valore che può essere true oppure false
che deriva, normalmente, da valutazioni logiche di verità o falsità.
Il tipo booleano è espresso attraverso la keyword _Bool, disponibile a partire da C99, e ha
una dimensione che deve consentirgli di memorizzare i valori 0 (false) e 1 (true).
Solitamente la dimensione di un tipo _Bool è di 1 byte o comunque pari almeno al valore
della macro CHAR_BIT definita nel file header <limits.h>.
Si può dunque dire che un tipo _Bool è di fatto, per effetto dei valori che può contenere,
ossia 0 e 1, un tipo intero (fa parte della famiglia degli unsigned integer types).

Letterali booleani
Un letterale booleano è, tipicamente, un letterale intero come 0 (indica una condizione di
falsità), oppure 1 (indica una condizione di verità) e rappresenta una sorta di costante
booleana, cioè un valore non alterabile che è scritto direttamente nel flusso testuale del
codice sorgente.
In ogni caso, qualsiasi valore che si prova a inserire in un tipo _Bool viene valutato nel
seguente modo e così convertito: se è valutato come uguale a 0 allora rappresenta il valore
false e viene inserito 0; se è valutato come diverso da 0 (maggiore di 0 o minore di 0 e
dunque negativo) allora rappresenta il valore true e viene inserito 1.
Tabella 2.11 Range di valori di un _Bool.
Tipo Valore minimo Valore massimo
_Bool 0 1

Listato 2.6 BoolType.c (BoolType).


/* BoolType.c :: Mostra l'uso dei tipi _Bool :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 82, b = 90;
_Bool b1 = 10,
b2 = -22,
b3 = '\000', // ASCII code NUL
b4 = 'A',
b5 = a < b; // valutazione di un'espressione

printf("b1 e' %d\n", b1);


printf("b2 e' %d\n", b2);
printf("b3 e' %d\n", b3);
printf("b4 e' %d\n", b4);
printf("b5 e' %d\n", b5);

return (EXIT_SUCCESS);
}

Output 2.6 Dal Listato 2.6 BoolType.c.


b1 e' 1
b2 e' 1
b3 e' 0
b4 e' 1
b5 e' 1

La funzione main del Listato 2.6 definisce 5 variabili di tipo _Bool e ne stampa a video i
valori ivi contenuti tramite la consueta funzione di output printf e lo specificatore di
formato %d (ricordiamo che un _Bool è di fatto un tipo intero).
In dettaglio: la variabile b1 conterrà il valore 1 (true) perché il valore 10 assegnatole è
maggiore di 0; la variabile b2 conterrà il valore 1 (true) perché il valore -22 assegnatole è
minore di 0; la variabile b3 conterrà il valore 0 (false) perché la costante carattere '\000'
assegnatale rappresenta un carattere di controllo ASCII con il valore 0 (null character); la
variabile b4 conterrà il valore 1 (true) perché la costante carattere 'A' assegnatale rappresenta
il codice ASCI 65 che è un numero maggiore di 0; la variabile b5 conterrà il valore 1 (true)
perché l’espressione a < b è vera (82 è minore di 90) e ritorna, pertanto, di già il valore 1 che
è dunque assegnato a b5.
Tipo vuoto
Un tipo vuoto (void type) rappresenta un insieme vuoto di valori (non esistente), senza
operazioni associabili con esso, ed è espresso attraverso la keyword void che si può
applicare in vari contesti: per designare che una funzione non ritorna “nulla” oppure che
non ha parametri; per specificare che un puntatore punta a un valore che non ha un tipo; per
scrivere espressioni che non hanno un valore (Snippet 2.12).

Snippet 2.12 Tipo void.


...
// funzione che non ritorna nulla e che non accetta argomenti
void foo(void) {}

int main(void)
{
// invocazione di foo: il fatto che non ritorna alcun valore
// la designa come un'espressione di tipo void
foo();

// esplicitamente scarto il valore di ritorno che per printf è di tipo int


// sintassi: (void)expression
(void) printf("Stampo qualcosa...\n"); // stampo qualcosa...

// la funzione malloc alloca un blocco di memoria non inizializzato


// della dimensione indicata dall'argomento passato e ritorna
// un puntatore void* all'inizio del blocco;
// il puntatore ritornato non ha un tipo!
// void *ptr rappresenta l'indirizzo in memoria di un oggetto che non ha tipo
void *ptr = malloc(sizeof (int));
...
}
Riepilogo dei tipi
La Figura 2.3 fornisce un panoramica di come lo standard del linguaggio C ha inteso
categorizzare i tipi di dato sin qui esaminati dove, in breve, il tipo _Bool unitamente ai tipi
unsigned sono denominati standard unsigned integer types mentre i tipi interi signed sono
denominati standard signed integer types.

Figura 2.3 Categorizzazione dei tipi di dato in accordo con lo standard C11.

I tipi standard unsigned integer types e i tipi standard signed integer types sono
collettivamente denominati standard integer types.
I tipi in virgola mobile sono denominati real floating types, i tipi complessi sono
denominati complex types e i tipi immaginari sono denominati imaginary types.
I tipi real floating types unitamente ai tipi complex types e ai tipi imaginary types sono
collettivamente denominati floating types.
Infine:
I tipi standard integer types e i tipi floating types sono collettivamente denominati
basic types.
Da questa categorizzazione è escluso il tipo void, che è un tipo a se stante.
Dimensione dei tipi di dato
In base al sistema che si utilizza per lo sviluppo dei programmi in C si ha a disposizione
un range di valori ben determinato che è possibile impiegare nei tipi di dato scelti.
Ciò significa che la memoria richiesta per memorizzare dei valori, per esempio di tipo
intero o in virgola mobile, è dipendente dal sistema in uso così come evidenziato, per
l’appunto per i tipi interi, dalle precedenti Tabelle 2.3, 2.4 e 2.5.
Dato comunque un corrente sistema target, è possibile “scoprire” l’esatta dimensione in
byte usata dai tipi mediante un operatore del linguaggio espresso tramite la keyword sizeof,
di cui diamo un primo esempio di utilizzo (Listato 2.7), impiegando una sintassi che
prevede la scrittura, tra la coppia di parentesi tonde, del nome di un determinato tipo di dato
come operando.

Sintassi 2.5 Operatore sizeof.


sizeof (data_type)

Listato 2.7 TypeSizes.c (TypeSizes).


/* TypeSizes.c :: Visualizza la dimensione dei tipi nel sistema in uso :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
printf("******************************************\n");
printf("* TYPE SIZES DEL CORRENTE SISTEMA IN USO *\n");
printf("******************************************\n\n");
printf(" _Bool\t\t%zu byte\n", sizeof (_Bool));
printf(" char\t\t%zu byte\n", sizeof (char));
printf(" int\t\t%zu byte\n", sizeof (int));
printf(" long\t\t%zu byte\n", sizeof (long));
printf(" long long\t%zu byte\n", sizeof (long long));
printf(" float\t\t%zu byte\n", sizeof (float));
printf(" double\t\t%zu byte\n", sizeof (double));
printf(" long double\t%zu byte\n\n", sizeof (long double));
printf("******************************************\n");
return (EXIT_SUCCESS);
}

Nel main del programma notiamo l’utilizzo della funzione printf con lo specificatore di
formato %zu, fornito a partire dallo standard C99, che consente di stampare correttamente il
valore ritornato dall’operatore sizeof che deve essere di tipo size_t, che è un sinonimo per
un tipo intero unsigned definito all’interno del file header <stddef.h> (come vedremo tra
breve, l’utilizzo dei sinonimi per i tipi consente, tra le altre cose, di fornire un nome di tipo
“standard” e portabile che però “nasconde” l’esatto tipo scelto dalla corrente
implementazione per il corrente sistema target).
NOTA
Nei compilatori non aderenti allo standard C99 o C11 è possibile utilizzare lo specificatore di
formato %lu per visualizzare il valore ritornato dall’operatore sizeof.
Output 2.7 Dal Listato 2.7 TypeSizes.c (sistema a 64 bit).
******************************************
* TYPE SIZES DEL CORRENTE SISTEMA IN USO *
******************************************

_Bool 1 byte
char 1 byte
short 2 byte
int 4 byte
long 8 byte
long long 8 byte
float 4 byte
double 8 byte
long double 16 byte

******************************************

Output 2.8 Dal Listato 2.7 TypeSizes.c (sistema a 32 bit).


******************************************
* TYPE SIZES DEL CORRENTE SISTEMA IN USO *
******************************************

_Bool 1 byte
char 1 byte
short 2 byte
int 4 byte
long 4 byte
long long 8 byte
float 4 byte
double 8 byte
long double 12 byte

******************************************

Gli output del programma mostrano con chiarezza la differenza di dimensionamento dei
tipi tra un sistema a 64 bit rispetto a un sistema a 32 bit: nel primo caso un long e un long
long hanno la stessa dimensione (8 byte) in accordo con quanto indicato in Tabella 2.5,
mentre nel secondo caso un int e un long (4 byte) hanno la stessa dimensione così come
indicato nella Tabella 2.4. In più nel sistema a 64 bit il long double è di 16 byte, mentre nel
sistema a 32 bit è di 12 byte.
ATTENZIONE
Il vostro sistema in uso potrebbe generare valori differenti.
Conversioni di tipo
Quando si scrivono programmi in C è possibile utilizzare diversi tipi di dato combinati tra
loro, ovvero che appaiono nell’ambito di una stessa istruzione o espressione.
Si pensi, per esempio, a una comune operazione aritmetica che somma due valori di tipo
diverso, oppure alla fondamentale istruzione di assegnamento dove si può avere che il
valore posto a destra dell’operatore di assegnamento è di tipo diverso rispetto al valore
atteso dal tipo posto alla sinistra del medesimo operatore.
In queste situazioni il compilatore effettua delle conversioni automatiche o implicite
(implicit conversion) che possono portare a una “promozione” (data type promotion) di un
tipo più piccolo (per esempio uno short) verso un altro tipo più grande (per esempio un int)
oppure a una “retrocessione” (data type demotion) di un tipo più grande (per esempio un
long) verso un tipo più piccolo (per esempio un char).

Nel primo caso non si hanno particolari problemi, perché se un valore di tipo short viene
assegnato a un tipo int, lo stesso rientra sicuramente nel range di valori di quest’ultimo e
dunque non subisce alcuna modifica adattiva.
Nel secondo caso, invece, si può avere una grave conseguenza legata a una possibile
perdita di informazione, soprattutto se il valore contenuto nel tipo long è più grande del
massimo valore rappresentabile nel tipo char.
ATTENZIONE
In C non è considerato illegale compiere un’operazione di data type demotion. Ciò implica
che un compilatore può o meno avvisare, in fase di compilazione, della possibile alterazione
di valore dal tipo più grande rispetto al tipo più piccolo. In GCC si può attivare un apposito
warning passando al comando di compilazione il flag -Wconversion.

NOTA
Le conversioni implicite sono compiute dal compilatore anche in altre due situazioni: quando
i tipi degli argomenti di una funzione non corrispondono ai tipi dei suoi parametri; quando il
tipo ritornato da una funzione non corrisponde al tipo dichiarato per la stessa. Affronteremo
in dettaglio queste conversioni nel Capitolo 6.

Analizziamo subito un primo esempio (Listato 2.8) che mostra cosa avviene in due
comuni casi di data type demotion in virtù delle seguenti regole.
Se si assegna una variabile contenente un valore decimale a una variabile di tipo intero,
si avrà un troncamento della sua parte frazionaria. Se, tuttavia, il valore della parte
intera non può essere rappresentata dal tipo intero di destinazione, allora il
comportamento intrapreso dal compilatore sarà non definito.
Se si assegna un valore di una variabile intera che è più grande del valore massimo
contenibile nell’altra variabile intera senza segno, sarà assegnato un valore (detto
modulo) che rappresenta il resto della divisione tra i due valori (se, tuttavia, la variabile
di destinazione è con segno, il risultato sarà dipendente dall’implementazione
corrente).

Listato 2.8 TypeDemotion.c (TypeDemotion).


/* TypeDemotion.c :: Demotion dei tipi :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 260;
double d = 323.123;
unsigned char b;

// il risultato sarà 260 % 256 che darà come resto 4


b = a;
printf("b = a ---> %d\n", b);

// il risultato sarà 67; infatti prima 323.123 sarà troncato in 323


// e poi si farà 323 % 256 che darà come resto appunto 67
b = d;
printf("b = d ---> %d\n", b);

return (EXIT_SUCCESS);
}

Output 2.9 Dal Listato 2.8 TypeDemotion.c.


b = a ---> 4
b = d ---> 67

Dall’Output 2.9 si evidenzia come l’espressione b = a dà come risultato 4, il resto della


divisione tra 260, che rappresenta il valore contenuto nella variabile a di tipo int, e 256, che
rappresenta il massimo valore assegnabile alla variabile b (poiché ricordiamo che in un tipo
unsigned char di 8 bit il range di valori assegnabile va da 0 a 255).
L’espressione b = d, invece, dà come risultato 67, poiché prima il valore 323.123 della
variabile d di tipo double viene troncato della sua parte decimale, diventando 323, e poi viene
generato il modulo tra 323 e 256, per le stesse ragioni viste per la prima espressione.
In sostanza, durante un’operazione di assegnamento C adotta la seguente regola.
Il tipo del valore dell’espressione che si trova a destra dell’operatore di assegnamento =
è convertito nel tipo della variabile che si trova alla sinistra dello stesso operatore.
Questo processo di conversione può portare a una promozione con ampliamento di
valore del tipo da convertire oppure a una sua restrizione con perdita di informazione.

Conversioni aritmetiche abituali


Le conversioni implicite avvengono anche quando si hanno espressioni binarie, tipo
quelle aritmetiche o relazionali, con due operandi di diverso tipo. La regole basiche che C
segue sono, in linea generale, abbastanza semplici.
Dati due operandi, il tipo inferiore è promosso al tipo superiore prima che la relativa
operazione sia eseguita, e il successivo valore del risultato sarà uguale al tipo di dato
del tipo superiore.
Se vi sono operandi di tipo _Bool, char e short, gli stessi sono sempre prima promossi
(integer promotion) nel tipo int o nel tipo unsigned int (quest’ultimo caso si può
verificare se il tipo short ha la stessa dimensione del tipo int e allora il tipo unsigned
short, essendo più grande del tipo int, deve essere convertito lo stesso nel tipo unsigned
int). In pratica, data un’espressione binaria non vi potranno mai essere tipi inferiori al
tipo int oppure al tipo unsigned int.

Snippet 2.13 Operandi promossi in int.


char b = 2;
int i;
short s = 111;

// ok valido b e s sono stati convertiti in int e possono essere assegnati


// a i che è di tipo int
i = b * s;

// ATTENZIONE: anche se b è di tipo char e il valore è nel suo range,


// gli operandi b e s sono stati convertiti direttamente in int
b = b + s; // warning: conversion to 'char' from 'int' may alter its value

Le regole complete che C segue per adottare promozioni e conversioni tra operandi di
tipo diverso nell’ambito di espressioni binarie sono molto più complesse di quelle indicate,
che sono comunque sufficienti per avere consapevolezza di cosa avviene durante le delicate
fasi di promozione dei tipi.
Ciononostante, per ragioni di completezza di trattazione, specifichiamo meglio tali regole
partendo dal dire che per C11 ogni tipo intero ha un ranking (integer conversion rank) che
determina chi, come tipo, ha peso maggiore (è superiore) rispetto a un altro tipo (è inferiore)
durante la fase di promozione.
Dal più alto al più basso abbiamo:
1. long long int, unsigned long long int;

2. long int, unsigned long int;

3. int, unsigned int;

4. short int, unsigned short int;

5. char, signed char, unsigned char;

6. _Bool.

Scorrendo l’elenco di ranking possiamo notare come i tipi unsigned hanno la stessa
posizione dei rispettivi tipi signed e possiamo esprimere la regola generale per cui:
dati tutti i tipi interi, se abbiamo i tipi A, B e C: se A ha un rank più alto di B e B ha un
rank più alto di C, allora A avrà un rank più alto di C.

Avendo questo ranking possiamo illustrare lo schema completo di regole che C esegue
durante un’abituale conversione aritmetica, in ordine e finché non ne trova una che si
applica:
1. se il tipo di uno degli operandi è in virgola mobile, allora quello con peso minore sarà
convertito in tale operando con peso maggiore secondo quest’ordine, dal più alto al più
basso: long double, double, float (ciò significa, per esempio, che se un operando è di tipo
double e l’altro di tipo float, allora quest’ultimo sarà promosso in double; se un
operando è di tipo float e l’altro di tipo int, allora quest’ultimo sarà promosso in float;
così via per le alte combinazioni); altrimenti,
2. se nessuno degli operandi è in virgola mobile, allora prima prova ad attuare una
promozione verso il tipo int e se i due operandi non sono ancora dello stesso tipo,
allora segue una delle seguenti regole finché una di esse non è applicabile:
a. se entrambi gli operandi sono tipi interi con segno oppure se entrambi gli operandi
sono tipi interi senza segno, allora quello con ranking minore è convertito in quello con
ranking maggiore in accordo con le posizioni dell’integer conversion rank prima elencato;
altrimenti,
b. se un operando è un tipo intero senza segno che ha un ranking maggiore o uguale al
tipo intero con segno dell’altro operando, quest’ultimo è convertito nel tipo intero
dell’operando senza segno; altrimenti,
c. se il tipo intero con segno di un operando può rappresentare tutti i valori del tipo intero
dell’altro operando senza segno, quest’ultimo è convertito nel tipo intero dell’operando con
segno; altrimenti,
d. entrambi gli operandi sono convertiti nel tipo intero senza segno dell’operando che
corrisponde al tipo intero dell’operando con segno.
Lo Snippet 2.14 mostra in modo concreto cosa avviene durante una serie di conversioni
in ragione dell’applicazione delle regole esposte.

Snippet 2.14 Regole implicite per le conversioni aritmetiche abituali (sistema a 64 bit).
// regola punto 1
int a = 11;
double d = 33.455;
double d_res = a + d; // a promosso in double

// regola punto 2
char g = 'F';
short int s = 345;
int i_res = g + s; // g e s promossi in int

// regola punto 2.a


unsigned long int ul = 1234567UL;
unsigned int ui = 213213U;
unsigned long int ul_res = ul + ui; // ui è promosso in unsigned long

// regola punto 2.b


unsigned long int ul_2 = 3234567UL;
long int li = 2400000L;
unsigned long int ul_res_2 = ul_2 + li; // li è promosso in unsigned long

// regola punto 2.c


long int li_2 = 4678989L;
unsigned int ui_2 = 5678999U;
long int li_res = li_2 + ui_2; // ui_2 è promosso in long int

// regola punto 2.d


long long int ll_i = 1234567000LL;
unsigned long int ul_3 = 3456789890UL;
// ul_3 e ll_i sono promossi in unsigned long long int
unsigned long long int ulli_res = ll_i + ul_3;

Mostriamo ancora un altro esempio (Snippet 2.15) che evidenzia come avverrà la
valutazione di un’espressione con operandi di diverso tipo considerando l’utilizzo della
funzione printf con lo specificatore di formato %f che stampa, di default, un valore in double
con 6 cifre decimali di precisione.

Snippet 2.15 Espressione con operandi di differente tipo.


char b = 111;
char c = 'd';
short s = 444;
int i = 2131;
long l = 2112;
float f = 5.6f;
double d = 3322.11;
double ris = (c * i) + (f * b) - (d / s) / l;

// risultato con GCC con sistema a 64 bit


printf("%f\n", ris); // 213721.590207

Nello Snippet 2.15 l’intera espressione sarà valutata ed eseguita come segue.
1. (c*i) sarà convertito in un int con il valore 213100.
2. (f*b) sarà convertito in un float con il valore 621.599976.
3. (d/s) sarà convertito in un double con il valore 7.4822297297297293.
4. Il punto 3 / l è sempre un double con il valore 0.0035427224099099097.
5. Il punto 1 + il punto 2 dà un float con il valore 213721.594.
6. Il punto 5 - il punto 4 dà un double con il valore 213721.59020727756.
7. ris sarà uguale a un valore double ovvero quello di cui il punto 6.

In definitiva, per valutare se un’espressione genera un valore dello stesso tipo della
variabile di destinazione bisogna sempre guardare al tipo degli operandi che ha il massimo
range di valori rappresentabili. Ciò significa che se un’espressione ha un operando di tipo
double e altri operandi di tipo differente, l’intera espressione darà sempre il valore convertito

in double, perché un double sicuramente potrà contenere valori interi (long long, long, int,

short, char e _Bool) e valori float.


Conversioni esplicite
A volte si può desiderare di eludere i meccanismi di conversione implicita effettuati in
automatico dal compilatore al fine di decidere con maggior precisione il tipo verso cui
convertire un altro tipo, oppure per documentare in modo più efficace ed evidente una
determinata conversione di tipo (si pensi a un float convertito in un int e alla perdita della
sua parte frazionaria).
A tal scopo C mette a disposizione un apposito operatore, definito cast operator,
mediante cui è possibile richiedere una esplicita conversione tra il tipo risultante
dall’espressione di cast verso il tipo esplicitato tra le parentesi tonde (Sintassi 2.6).

Sintassi 2.6 Operatore di cast.


(type_name) cast_expression

Listato 2.9 Casting.c (Casting).


/* Casting.c :: Cast operator :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
float res;
int number_1 = 100, number_2 = 33;

// la divisione tra interi ritorna solo la parte intera che per una conversione
// implicita è "promossa" in float
res = number_1 / number_2;
printf("Divisione: %d / %d = %f\n", number_1, number_2, res);

// number_1 è convertito in float sicché tutta l'espressione per


// le usuali regole già viste è di tipo float e res conterrà
// l'esatto risultato della divisione tra float
res = (float) number_1 / number_2;
printf("Divisione: %f / %f = %f\n", (float) number_1, (float) number_2, res);

return (EXIT_SUCCESS);
}

Output 2.10 Dal Listato 2.9 Casting.c.


Divisione tra 100 / 33 = 3.000000
Divisione tra 100.000000 / 33.000000 = 3.030303

Il Listato 2.9 definisce le variabili res di tipo float, number_1 e number_2 di tipo int e poi
compie due divisioni: la prima, tra number_1 e number_2, dà come risultato il valore intero 3 (la
parte frazionaria è esclusa) che, essendo assegnato alla variabile res, viene convertito
implicitamente nel suo tipo ossia float e la funzione printf equivalente mostra come
risultato il valore 3.000000; la seconda, sempre tra number_1 e number_2, dà come risultato il
valore in virgola mobile 3.030303 perché, prima di effettuare la divisione, la variabile
number_1 è stata esplicitamente convertita in un tipo float e poi, per le regole di conversione
implicita prima esaminate, anche la variabile number_2 è stata convertita in un tipo float e
dunque tutta l’operazione di divisione è proceduta tra tipi float.
La variabile res ha quindi ricevuto un valore float che è del suo stesso tipo: infatti, la
funzione printf equivalente ne mostra in modo inequivoco il valore ossia 3.030303.
Sinonimi per i tipi
Una caratteristica di rilievo offerta dal linguaggio consiste nel permettere di attribuire
“nuovi nomi” per i tipi di dati esistenti senza produrre, nel contempo, dei nuovi tipi.
Per avvantaggiarsi di ciò è sufficiente utilizzare la keyword typedef (Sintassi 2.7), cui far
seguire tramite type_identifier il nome di un tipo di dato e tramite new_name il nuovo nome o
alias con il quale tale tipo di dato sarà utilizzato nell’ambito delle comuni operazioni di
dichiarazione di variabili, di cast e così via.

Sintassi 2.7 typedef.


typedef type_identifier new_name

Snippet 2.16 Utilizzo di typedef.


// ora byte è un alias per unsigned char
typedef unsigned char byte;

// ora boolean è un alias per _Bool


typedef _Bool boolean;

// ora String è un alias per char *


typedef char * String;

// è equivalente sintatticamente a utilizzare unsigned char


byte data = 0xFF;

// è equivalente sintatticamente a utilizzare _Bool


boolean test = 6 > 10;

// è equivalente sintatticamente a utilizzare char *


String message = "Attenzione riavvio del server tra 10 secondi!";

Lo Snippet 2.16 mostra la creazione di alias per i tipi unsigned char, _Bool e char * con cui
si evidenzia uno dei vantaggi offerti da typedef, ossia la possibilità di rendere un programma
più “comprensibile” quando si utilizzano determinati tipi di dato.
Si pensi alla dichiarazione della variabile data che è di tipo byte e che rende subito chiaro
che la stessa può memorizzare l’intervallo di valori di un byte che va da 0 a 255, oppure alla
dichiarazione della variabile message che è di tipo String e che consente di comprendere in
modo netto che la stessa può memorizzare una stringa di caratteri.
Un altro vantaggio offerto da typedef è quello inerente la possibilità di scrivere
programmi portabili su sistemi differenti dove i tipi di dato hanno diversi range di valori.
L’esempio più comune è quello che implica l’utilizzo di tipi interi che sono adattati alla
piattaforma di destinazione di un programma.
Così, se avremo l’esigenza di impiegare interi con un range di valori da -2147483648 a
2147483647, allora potremo utilizzare una definizione come typedef int Integer che sarà valida
per un sistema a 32 bit, oppure come typedef long int Integer che sarà valida, invece, su un
sistema a 16 bit. Ciò fatto potremo essere sicuri che nel nostro programma ogni utilizzo di
Integer sarà “interpretato” con il corretto tipo di dato, ossia int oppure long int.

La libreria standard del linguaggio C utilizza typedef in modo sistematico per creare nomi
(o sinonimi) per tipi che diventano “portabili” ancorché dipendenti e variabili rispetto alla
particolare implementazione per un determinato sistema target.
Per convenzione, questi nomi terminano con i caratteri _t, come size_t, wchar_t, time_t,
int32_t e così via, e per utilizzarli è sufficiente includere nel relativo programma i file
header dove il corrispondente typedef è stato impiegato.

TYPEDEF VS #DEFINE
Anche se typedef e #define possono entrambe definire nuovi nomi per tipi preesistenti, esistono
tra di esse le seguenti importanti differenze:
typedef è uno specificatore di classe di memorizzazione interpretato dal compilatore,
mentre #define è una direttiva interpretata dal preprocessore;
typedef può attribuire nomi simbolici solo a tipi di dato, mentre #define può attribuire nomi
anche a valori;
typedef permette di creare degli “effettivi sostituti” di nomi per i tipi (definisce tipi), mentre

#define esegue solo una sostituzione letterale di quanto rappresentato dal relativo nome
(definisce macro; Snippet 2.17 e 2.18);
typedef segue le stesse regole di scope delle dichiarazioni delle variabili (una typedef

può avere, per esempio, una visibilità legata solo al blocco di funzione dove è stata
dichiarata), mentre #define non obbedisce a tali regole. Con #define la sua definizione ha
visibilità dal punto in cui è stata scritta fino alla fine del corrente file.

TERMINOLOGIA
Uno specificatore di classe di memorizzazione determina la durata in memoria di un oggetto
ed è indicabile con apposite keyword, come static, extern e così via, che saranno trattate in
dettaglio nel Capitolo 9. Tuttavia, da un punto di vista semantico, typedef non è un vero e
proprio specificatore di classe di memorizzazione ma è indicato come tale solo per una
“convenienza” sintattica.

TERMINOLOGIA
Per scope si intende quella regione del codice sorgente dove un identificatore è visibile e
dunque utilizzabile. Approfondiremo tale importante concetto nel Capitolo 9.

Snippet 2.17 char * e typedef.


typedef char * String;

// equivalente a char *p1, *p2; oppure detto in altro modo a:


// char *p1;
// char *p2;
String p1, p2;

Snippet 2.18 char * e #define.


#define String char *

// equivalente a char *p1, p2; oppure detto in altro modo a:


// char *p1;
// char p2;
String p1, p2;
Capitolo 3
Array

Sinora abbiamo incontrato un solo tipo di entità o oggetto utilizzabile per memorizzare
dei valori, ossia la variabile, o la sua forma read-only, ovvero la costante.
Questo tipo di oggetto, denominato anche variabile scalare (o tipo scalare se ci riferiamo
in modo esplicito al suo tipo di dato come char, int, float e così via) ha la caratteristica di
poter rappresentare e gestire un solo valore alla volta.
Tuttavia, vi possono essere delle circostanze per cui si ha la necessità di memorizzare un
insieme di valori, come unica e indivisibile entità, e riferirsi a essi attraverso un unico
oggetto che li rappresenti.
Si pensi, per esempio, a un programma che deve rappresentare i mesi di un anno e per
ciascun mese deve indicare la quantità precisa di giorni cui è composto.
In questa situazione è possibile impiegare dodici diverse variabili, denominate january,
february, march e così via e assegnare loro dei valori comi 31, 28 (se l’anno non è bisestile), 30
e così via, oppure utilizzare un “particolare” oggetto, l’array, denominato come variabile
aggregato (o tipo aggregato se ci riferiamo a esso come a un tipo di dato array) che
consente per il suo tramite di rappresentare direttamente tutti i mesi dell’anno come
un’unica e omogenea collezione di dati.
Avendo dunque una sola variabile di tipo array, denominata per esempio months, potremo
accedere tramite essa, secondo una particolare sintassi che tra breve esamineremo,
direttamente al mese di interesse evitando così di avere dodici diverse variabili che, di fatto,
non rappresentano “cose” differenti ma sono tra di loro collegate da un significato: tutte
rappresentano i mesi di un anno.
TERMINOLOGIA
Un tipo array fa parte dei cosiddetti tipi derivati, che sono tipi che si costruiscono a partire
dai tipi fondamentali (char, int, long, float e così via). Fanno altresì parte dei tipi derivati: il
tipo funzione, che descrive una funzione con uno specifico tipo di ritorno; il tipo puntatore,
che fa parte anch’esso dei tipi scalari, che descrive un oggetto che si riferisce a un altro
oggetto di un determinato tipo; il tipo struttura, che descrive un insieme sequenziale di
oggetti di diverso tipo; il tipo unione, che descrive un insieme “sovrapponibile” di oggetti di
diverso tipo; il tipo atomico, che descrive un tipo qualificato attraverso la keyword _Atomic.
Array monodimensionali
Un array monodimensionale, denominato anche vettore, è una struttura dati rappresentata
da un insieme di variabili, identificate da un nome univoco e definite elementi, che sono
tutte di uno stesso tipo e sono disposte in memoria in modo contiguo, ovvero sono
concettualmente visualizzabili come una serie di oggetti posizionati l’uno dopo un altro in
una singola riga o in una singola colonna che rappresentano, nella sostanza, la sua unica e
sola dimensione (Figura 3.1).

Figura 3.1 Visualizzazione di un array monodimensionale denominato data.

Dichiarazione
Un array di dichiara utilizzando la Sintassi 3.1.

Sintassi 3.1 Dichiarazione di un array.


data_type identifier[NR_OF_ELEMENTS];

Qui data_type indica il tipo di dato che avranno gli elementi costituenti l’array (int, char e
così via); identifier indica il nome o identificatore dell’array e deve essere scritto
utilizzando le stesse regole viste per gli identificatori delle variabili (per esempio, non può
cominciare con un carattere numerico); le parentesi quadre [ ] (dette operatore di subscript)
indicano che la variabile relativa è un array del tipo stabilito e devono essere sempre
presenti; NR_OF_ELEMENTS, poste all’interno dell’operatore [ ], indicano la quantità di elementi
facenti parte dell’array e tale valore deve essere espresso attraverso un’espressione costante
intera.
TERMINOLOGIA
Un’espressione costante intera (integer constant expression) è un’espressione che deve
essere di tipo intero e deve contenere come operandi solo costanti intere, costanti di
enumerazione, costanti carattere, espressioni sizeof che ritornano costanti interi e così via.
Un oggetto dichiarato con il qualificatore const non è tuttavia considerato come
un’espressione costante se utilizzato per fornire la dimensione di un array.

IMPORTANTE
Gli elementi di un array sono collocati contiguamente a partire da una posizione che ha
valore 0 e non valore 1. Pertanto, il primo elemento di un array sarà l’elemento 0 e avrà la
posizione 0 (zeroth element).

Un tipo array è dunque caratterizzato dal tipo dei suoi elementi e dalla loro quantità, ossia
dal loro numero che ne determina anche la lunghezza.
Se, quindi, il tipo di un elemento è T, allora un tipo array da esso derivabile è altresì
indicabile come array di T avente un numero di elementi di tipo T pari alla dimensione
specificata.

Snippet 3.1 Dichiarazione di un array di 10 elementi.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
// dichiarazione dell'array a di 10 elementi
int a[SIZE];
...
}

Lo Snippet 3.1 dichiara l’array a di tipo int che avrà 10 elementi (array di int di
dimensione 10) che è, per l’appunto, il valore ritornato dalla valutazione dell’identificatore
SIZE che rappresenta un’espressione costante intera (SIZE è infatti una costante simbolica

creata tramite la direttiva #define).


Quando il compilatore incontra questa istruzione di dichiarazione, riserva una quantità di
memoria adatta a contenere il numero di elementi indicati, e ciascun elemento è
inizializzato con un valore che è arbitrario ed è dipendente da quello che si trovava nella
corrispondente locazione di memoria utilizzata.
NOTA
Le variabili, e dunque anche gli array, possono essere create con differenti classi di
memorizzazione che determinano anche se queste in assenza di esplicite inizializzazioni
debbano contenere un valore non arbitrario. Nel nostro caso l’array a, essendo dichiarato
nell’ambito di una funzione, ha la classe di memorizzazione automatica che non impone
alcun valore di default se gli elementi non ne hanno alcuno specificato.

Una possibile rappresentazione in memoria del nostro Snippet 3.1 è data dalla Figura 3.2,
laddove i valori di ciascun elemento possono variare da sistema a sistema proprio perché
per lo standard C11 le variabili automatiche non inizializzate in modo esplicito conterranno
valori indefiniti.

Figura 3.2 Una possibile rappresentazione in memoria dell’array a.

Inizializzazione
Un array può essere inizializzato (Sintassi 3.2) contestualmente alla sua dichiarazione al
fine di dare valori significativi ai corrispondenti elementi.

Sintassi 3.2 Inizializzazione di un array.


data_type identifier[NR_OF_ELEMENTS] = {value_0, value_1, ..., value_N};

Come la Sintassi 3.2 evidenzia, un array si inizializza utilizzando l’operatore di


assegnamento = e una coppia di parentesi graffe aperte/chiuse { } al cui interno porre dei
valori, detti inizializzatori e separati dalla virgola, che saranno associati ai corrispondenti
elementi degli array.
Quando si utilizza una lista di inizializzatori (initializer list) espressi nella forma indicata
bisogna tenere presente le seguenti regole (Snippet 3.2).
Se il numero di inizializzatori è inferiore alla dimensione indicata da NR_OF_ELEMENTS, i
restanti elementi dell’array saranno inizializzati con il valore 0.
Se il numero di inizializzatori è superiore alla dimensione indicata da NR_OF_ELEMENTS, un
compilatore dovrebbe riportarlo come un errore. GCC, comunque, riporta il seguente
messaggio di diagnostica warning: excess elements in array initializer consentendo
altresì la compilazione del programma.
Non è possibile fornire una lista di inizializzatori vuota. Anche in questo caso GCC si
limita a fornire il seguente messaggio di diagnostica: warning: ISO C forbids empty
initializer braces, permettendo comunque la compilazione.
È possibile omettere NR_OF_ELEMENTS se è presente una lista di inizializzatori perché
quest’ultima è utilizzata dal compilatore per determinare la quantità di elementi del
correlativo array.

NOTA
Non deve apparire strano che GCC riporti solo dei messaggi nei casi di violazione delle
regole sopra citate. Lo standard C11, infatti, prescrive che un’implementazione conforme
deve riprodurre almeno un messaggio di diagnostica qualora vi siano delle violazioni alle
specifiche.

Snippet 3.2 Inizializzazione di array.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
// i rimanenti 7 elementi dell'array avranno il valore 0
int a[SIZE] = {1, 2, 3};

// 11 elementi inizializzati: errore o warning a seconda del compilatore


int a_2[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10, 11};

// lista di inizializzatori vuota: errore o warning a seconda del compilatore


int a_3[SIZE] = {};

// l'array avrà 2 elementi quanti sono per l'appunto gli inizializzatori


int a_4[] = {45, 78};
...
}

A partire da C99 è possibile utilizzare un’altra forma di inizializzazione degli elementi di


un array (Sintassi 3.3) che consente di indicare esplicitamente gli elementi da valorizzare
attraverso l’uso dei cosiddetti inizializzatori designati (designated initializers).

Sintassi 3.3 Inizializzatori designati.


data_type identifier[NR_OF_ELEMENTS] = {[index] = value, ..., [index_N] = value_N};

In pratica, tra la coppia di parentesi graffe { }, viene posta una serie di parentesi quadre
aperte/chiuse [ ] al cui interno si scrive la posizione (index) dell’elemento dell’array cui
attribuire il corrispettivo valore value.
Questa posizione, che rappresenta un designatore, deve essere un’espressione costante
intera e il suo valore può andare da 0 a NR_OF_ELEMENTS - 1.
È altresì interessante rilevare come gli inizializzatori designati possono essere utilizzati
congiuntamente ai normali inizializzatori non designati e che l’ordine degli elementi
inizializzati dai designatori non ha importanza (vi può essere un designatore con index pari a
10, poi un altro designatore con index pari 1 e così via).

Snippet 3.3 Inizializzazione di array con inizializzatori designati e non.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
// elemento 0 = 1000, elemento 1 = 0, elemento 2 = 11
// elemento 3 = 3; elemento 4 = 5; elemento 5 = 0, elemento 6 = 0
// elemento 7 = 0, elemento 8 = 0; elemento 9 = 0
int a[SIZE] = {5000, [3] = 100, [2] = 11, 3, 5, [0] = 1000};

// elemento 0 = 1; elemento 1 = 2; elemento 2 = 0


// elemento 3 = 0; elemento 4 = 10; elemento 5 = 0; elemento 6 = 6
int a_2[] = {1, 2, [6] = 6, [4] = 10};
...
}

Lo Snippet 3.3 definisce l’array a di dimensione 10 che inizializzerà i relativi elementi


considerando la regola che il successivo elemento da inizializzare sarà quello che seguirà
l’ultimo elemento inizializzato che, nel caso dei normali inizializzatori, sarà sempre il
successivo nell’ordine indicato (elemento 0, elemento 1 e così via) mentre, nel caso degli
inizializzatori designati, sarà quello successivo al designatore indicato:
1. elemento posizione 0 valore 5000, elemento successivo posizione 1;
2. elemento posizione 1 valore 0, elemento successivo posizione 2;
3. elemento posizione 2 valore 0, elemento successivo posizione 3;
4. elemento posizione 3 valore 100, elemento successivo sarebbe posizione 4 ma il
successivo designatore ne imposta un’altra;
5. elemento posizione 2 valore 11, elemento successivo posizione 3;
6. elemento posizione 3 valore 3, elemento successivo posizione 4;
7. elemento posizione 4 valore 5, elemento successivo sarebbe posizione 5 ma il
successivo designatore ne imposta un’altra;
8. elemento posizione 0 valore 1000;
9. gli altri elementi non esplicitamente inizializzati avranno il valore 0.

Dagli step elencati si può notare (punti 5. e 8.) come un designatore può interrompere
l’inizializzazione di un successivo elemento facendo ripartire il conteggio da quanto esso
indicato e che un elemento può anche avere più valori assegnati poiché solo l’ultimo sarà
quello effettivamente contenuto.
L’array a_2, invece, mostra che quando un array non ha l’indicazione del numero di
elementi, e sono presenti degli inizializzatori designati, quello con indice maggiore
determina la lunghezza dell’array medesimo.
Nel nostro caso l’array a_2 avrà 7 elementi inizializzati nel seguente modo:

1. elemento posizione 0 valore 1, elemento successivo posizione 1; elemento posizione 1


valore 2, elemento successivo sarebbe posizione 2 ma il successivo designatore ne
imposta un’altra; elemento posizione 6 valore 6, elemento successivo sarebbe
posizione 7 ma il successivo designatore ne imposta un’altra;
2. elemento posizione 4 valore 10;
3. gli altri elementi non esplicitamente inizializzati avranno il valore 0.
Per rendere più chiaro ed esplicito quanto detto, la Figura 3.3 fornisce un
rappresentazione in memoria degli array a e a_2.

Figura 3.3 Rappresentazione in memoria degli array a e a_2.

Subscripting
Dopo la fase di dichiarazione e/o di inizializzazione di un array, gli elementi relativi
possono essere utilizzati in fase di lettura e scrittura mediante la Sintassi 3.4

Sintassi 3.4 Accesso a un elemento di un array.


array_identifier[element_index] = value; // in scrittura
other_variable = array_identifier[element_index]; // in lettura

Si scrive il nome dell’array e l’operatore di subscript [ ], al cui interno si indica l’indice o


la posizione dell’elemento da manipolare, in lettura o in scrittura.
L’indice può essere fornito mediante una qualsiasi espressione intera e il valore risultante
deve essere compreso tra 0 (posizione primo elemento) e N - 1 (posizione ultimo elemento)
dove N esprime la dimensione o lunghezza del corrispettivo array.
TERMINOLOGIA
Per subscripting o indexing si intende quell’operazione attraverso la quale si accede a un
particolare elemento di un array, fornendo a un particolare operatore, detto di subscript ed
espresso con le parentesi quadre aperte/chiuse [ ], un valore che ne rappresenta l’indice o
la posizione all’interno dell’array medesimo.

Snippet 3.4 Accesso agli elementi di un array.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
// array di int di 10 elementi
int c[SIZE] = {[6] = 2450};
int u = 2, z = 4;
c[1] = 333; // scrivo alla posizione con indice 1

int x = c[u + z]; // leggo alla posizione con indice 6

// per lo standard l'accesso a un indice di un array


// fuori dai limiti dà risultati non definiti
c[10] = 1000; // Out of bounds!!!
...
}

Lo Snippet 3.4 dichiara l’array c deputato a contenere 10 elementi di tipo intero, così
come stabilito dalla costante simbolica SIZE, e poi le variabili intere u e z.
Mostriamo quindi come, utilizzando il nome dell’array c e l’operatore di subscript [ ],
accediamo con l’istruzione c[1] al suo secondo elemento e gli assegniamo il valore 333,
mentre con l’istruzione c[u + z] accediamo al suo settimo elemento, perché la valutazione
dell’espressione u + z dà come risultato l’intero 6, e ne assegniamo il corrispettivo valore
alla variabile intera x.
Le operazioni discusse evidenziano la seguente cosa: espressioni indicate nella forma di
c[1] oppure nella forma di c[u + z] sono considerabili alla stessa stregua di espressioni che

indicano esplicitamente nomi di variabili tipo u, z, x, e via discorrendo.


In effetti, se consideriamo un array come un insieme di elementi contigui tra di essi
logicamente collegati, gli stessi elementi non sono altro che variabili referenziabili come
c[0], c[1], c[2] e così via piuttosto che come c0, c1 e c2.

Infine, l’ultima istruzione che assegna il valore 1000 all’elemento con indice 10 dell’array
c solleva un interessante quesito: cosa accade se si prova a utilizzare un indice che è fuori
dai limiti massimi (o minimi) stabiliti per un determinato array?
La risposta è che per lo standard del linguaggio C il comportamento è non definito, ossia
il programma può continuare a funzionare, può comportarsi in modo anomalo oppure può
interrompersi. Ogni implementazione può decidere come gestire quest’anomalia e se
avvisare o meno il programmatore in fase di compilazione di un programma.
Questa libertà concessa dallo standard non deve essere considerata negativa oppure
stupire, anzi, è in linea con due dei principi cardine del linguaggio: il primo, più filosofico, è
legato al fatto di “credere” e avere fiducia nelle capacità di un programmatore che non può
non sapere come indicizzare correttamente un array; il secondo, più pratico, è legato a
ragioni di efficienza e velocità di esecuzione di un programma perché il compilatore non è
costretto ad aggiungere codice supplementare che, a run-time, si deve occupare di verificare
se l’accesso a un elemento di un array è fuori dai limiti consentiti.
Ritornando al nostro caso, GCC non rileva e non avvisa del tentativo di scrivere il valore
1000 fuori dal limite massimo dell’array c che ricordiamo è 9 e non 10 (è comunque

consentito indicizzare 10 elementi perché, ribadiamo, l’indicizzazione degli elementi parte


da 0 incluso e termina con 9 incluso).
TERMINOLOGIA
Quando dobbiamo indicizzare un elemento di un array, bisogna prestare attenzione a come
si decide di referenziarlo. Infatti, dire “accedo al quinto elemento di un array” è diverso da
dire “accedo all’elemento cinque di un array”. Poiché l’indicizzazione di un array è zero-
based, il quinto elemento di un array avrà indice pari a 4, mentre l’elemento cinque avrà un
indice pari a 5 ma indicherà il sesto elemento dell’array. La distinzione sopra indicata, se
non la si ha ben chiara, può causare il cosiddetto errore off-by one (OBOE), ossia il “fuori di
uno”, cioè l’accesso all’elemento successivo all’ultimo.

La Figura 3.4 mostra invece una rappresentazione in memoria dell’array c dopo


l’esecuzione delle suddette inizializzazioni evidenziando altresì:
le locazioni di memoria dei singoli elementi inclusi nell’array (da c[0] a c[9]), che sono
a incrementi di 4 byte su un sistema dove i tipi int sono di 32 bit. In quelle locazioni di
memoria si trovano i valori correnti degli elementi dell’array c;
le locazioni di memoria dell’array quando si prova a indicizzare lo stesso con un valore
che è fuori dal limite massimo (10) oppure che è fuori dal limite minimo (-1). In
quest’ultimo caso l’indicizzazione con un numero negativo è possibile perché il
compilatore accede alla locazione di memoria corrispondente partendo dall’indirizzo
corrente dell’array a cui gli sottrae il valore 4 che, in byte, è la dimensione di un int sul
sistema corrente. In quelle locazioni di memoria si trovano dei valori garbage, ossia
che non hanno alcun senso per il nostro programma.

Figura 3.4 Rappresentazione in memoria dell’array c.

NOTA
Quando studieremo la relazione tra array e puntatori sarà più chiaro il perché
dell’indicizzazione negativa. Per ora si può ragionare nel seguente modo: dato un array di T,
ogni indice fornito sposta la corrente locazione di memoria (in avanti se positivo oppure
indietro se negativo) di tante posizioni in relazione al valore dell’indice e all’ampiezza del
tipo di dato T.

In conclusione vediamo un semplice esempio (Listato 3.1) che crea un array di interi
contenente 10 numeri scelti a caso e poi stampa a video solo quelli dispari scorrendo in una
struttura iterativa (for) i singoli elementi dell’array ove sono contenuti.

Listato 3.1 ArrayMono.c (ArrayMono).


/* ArrayMono.c :: Dato un array ne stampa solo i valori dispari :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

int main(void)
{
// array di interi di 10 numeri
int numbers[SIZE] = {122, 4, 66, 7, 33, 1, 2, 30, 45, 10};

for(int ix = 0; ix < SIZE; ix++)


{
if(numbers[ix] % 2 != 0)
printf("Il numero %d e' dispari\n", numbers[ix]);
}

return (EXIT_SUCCESS);
}

Output 3.1 Dal Listato 3.1 ArrayMono.c.


Il numero 7 e' dispari
Il numero 33 e' dispari
Il numero 1 e' dispari
Il numero 45 e' dispari

DETTAGLIO
In questo esempio abbiamo utilizzato una struttura iterativa, creata con l’istruzione for, che
consente di effettuare ciclicamente le operazioni in essa contenute finché una determinata
condizione è vera. Nel nostro caso la struttura iterativa visita gli elementi contenuti nell’array
numbers finché il contatore ix è minore della lunghezza dell’array stesso. In pratica il ciclo è

eseguito 10 volte da 0 incluso a 9 incluso.


Array multidimensionali
Il linguaggio C consente di dichiarare e inizializzare array con più di un indice, ovvero
con una dimensione maggiore di uno, al fine di astrarre in una struttura dati ad hoc oggetti o
“concetti” del mondo reale, e non, più complessi. Si pensi, per esempio a una scacchiera (ha
otto righe e otto colonne e dunque due dimensioni); alla statistica del numero di sviluppatori
dei più comuni linguaggi di programmazione censiti per annualità (ha righe per indicare un
determinato anno e colonne per indicare il nome dei linguaggi e dunque due dimensioni);
alla mappatura dei clienti di un albergo (l’albergo ha dei piani, ogni piano dei corridoi, ogni
corridoio delle stanze e dunque tre dimensioni); alla rilevazione della velocità di un corpo
dati una posizione e un tempo (ha le coordinate spaziali x, y e z e il tempo t, e dunque
quattro dimensioni). Nella sostanza, comunque, gli array n-dimensionali più utilizzati sono
quelli a due dimensioni, identificati con due coppie di parentesi quadre aperte/chiuse [ ][ ],
e quelli a tre dimensioni, identificati con tre coppie di parantesi quadre aperte/chiuse [ ][ ][

].

ATTENZIONE
L’utilizzo di array multidimensionali, soprattutto quelli con più di due dimensioni, deve
essere ponderato con estrema cautela quando il numero di elementi per indice è notevole,
e ciò per evitare un impiego inutile ed eccessivo di memoria necessaria per la loro completa
creazione (si potrebbe, infatti, non avere alcuna necessità di utilizzare subito tutti gli
elementi allocati). In questo caso può essere più opportuno definire un nuovo tipo di dato
(per esempio una struttura Velocity) che ha come proprietà le dimensioni di interesse (per
esempio le variabili x, y, z e t) e poi creare un array dove gli elementi riferiscono solo alle
strutture di tipo Velocity effettivamente occorrenti.

Array bidimensionali
Un array bidimensionale, definito anche matrice, è una struttura dati che al pari dell’array
monodimensionale è composta di un insieme di variabili, che ne rappresentano sempre gli
elementi, dove però, a differenza del vettore, sono concettualmente visualizzabili (Figura
3.5) come una serie di oggetti posizionati l’uno dopo l’altro in una sorta di tabella ovvero in
righe (prima dimensione) e colonne (seconda dimensione).
Figura 3.5 Visualizzazione di un array bidimensionale denominato data.

Per C, comunque, un array bidimensionale è in realtà un array a una dimensione dove


ogni elemento è esso stesso un array a una dimensione (array di array).
TERMINOLOGIA
Gli array di array in gergo informatico sono chiamati jagged o ragged array (array irregolari)
e si contrappongono agli array bidimensionali puri, chiamati rectangular array (array
rettangolari). In questo contesto, comunque, gli array che creeremo, quantunque array di
array, saranno array rettangolari perché ogni riga avrà un elemento che sarà un sempre un
array della stessa dimensione. Quanto studieremo la relazione tra array e puntatori
vedremo come creare array irregolari, cioè dove per ogni riga potremo avere un elemento
che sarà un array di differenti dimensioni.

NOTA
Nel Capitolo 7 vedremo un’altra rappresentazione concettuale di un array bidimensionale
tramite una tabella di righe e colonne che terrà conto della relazione tra array e puntatori e
del fatto che un array a due dimensioni è di fatto un array di array.

Nella pratica gli elementi di una matrice sono memorizzati in modo sequenziale, riga per
riga (row-major order), ovvero, nell’ordine: prima tutti gli elementi della riga 0, poi tutti gli
elementi della riga 1 e così via per gli elementi delle rimanenti righe (Figura 3.6).

Figura 3.6 Visualizzazione in memoria della matrice data.


La rappresentazione tabellare (Figura 3.7) è dunque solo di ausilio per meglio
visualizzare la disposizione degli elementi di una matrice, ma non ha alcun riscontro di
come C pone in memoria gli elementi della stessa.

Figura 3.7 Visualizzazione concettuale della matrice data.

Dichiarazione
Un array bidimensionale si dichiara utilizzando la Sintassi 3.5.

Sintassi 3.5 Dichiarazione di un array bidimensionale.


data_type identifier[NUMBER_OF_ROWS][NUMBER_OF_COLS];

Qui data_type e identifier hanno lo stesso significato e le stesse regole di quelle viste per
la dichiarazione del tipo vettore, mentre le doppie parentesi quadre [ ], sempre obbligatorie,
indicano che la variabile relativa è una matrice del tipo stabilito costituita dal numero di
righe indicate da NUMBER_OF_ROWS (prima dimensione) e ogni riga dal numero di colonne
indicate da NUMBER_OF_COLS (seconda dimensione).
Anche in questo caso NUMBER_OF_ROWS e NUMBER_OF_COLS devono essere espressioni costanti
intere.

Snippet 3.5 Dichiarazione di una matrice di 2×3 elementi.


...
#define NR_OF_ROWS 2 /* numero di righe */
#define NR_OF_COLS 3 /* numero di colonne */

int main(void)
{
// dichiarazione della matrice data di 2x3 elementi
int data[NR_OF_ROWS][NR_OF_COLS];
...
}

Lo Snippet 3.5 dichiara l’array bidimensionale data di tipo int che avrà 2 righe e ogni riga
avrà 3 colonne, come da risultato della valutazione delle costanti simboliche NR_OF_ROWS e
NR_OF_COLS utilizzate con l’operatore di subscript [ ].
Gli elementi totali a disposizione della matrice saranno pertanto 6, valore che è dato dalla
moltiplicazione del numero di righe (2) per il numero relativo di colonne (3).
Come per il vettore, anche in questo caso il compilatore riserva la giusta quantità di
memoria adatta a contenere gli elementi della matrice, e i valori di inizializzazione sono
quelli che si trovavano nella corrispondente locazione di memoria utilizzata.
Per comprendere la dichiarazione di una matrice può essere opportuno separarla in due
fasi, dove una si riferisce alla prima dimensione e l’altra si riferisce alla seconda
dimensione; così, la matrice int data[2][3] può essere logicamente divisa in:

data[2], che indica che data è un array di 2 elementi;


int[3], che indica che ognuno dei 2 elementi è di tipo int[3], ovvero un array di 3
elementi di tipo intero.
Una possibile rappresentazione in memoria del nostro Snippet 3.5, con visualizzazione
logica tabellare, è data nella Figura 3.8. Ribadiamo che i valori degli elementi sono variabili
da sistema a sistema perché per lo standard del linguaggio C le variabili automatiche non
inizializzate esplicitamente conterranno valori non definiti.

Figura 3.8 Una possibile rappresentazione concettuale della matrice data visualizzata come
tabella.

Inizializzazione
Un array bidimensionale può essere inizializzato (Sintassi 3.6) contestualmente alla sua
dichiarazione in modo da fornire valori significativi ai corrispondenti elementi.

Sintassi 3.6 Inizializzazione di un array bidimensionale.


data_type identifier[NR_OF_ROWS][NR_OF_COLS] =
{
{value_0, value_1, ..., value_N},
{value_0, value_1, ..., value_N},
...
};

In pratica una matrice si inizializza utilizzando l’operatore di assegnamento = e una


coppia di parentesi graffe { } esterne entro cui porre tante coppie di parentesi graffe { }

interne ciascuna contenente degli inizializzatori separati dalla virgola che forniranno dei
valori ai corrispondenti elementi della corrente riga. Ogni lista di inizializzatori interna
inclusiva delle parentesi graffe { } è altresì separata dal carattere virgola (,).
Le regole di scrittura delle liste di inizializzatori ricalcano quelle già viste per
l’inizializzazione di un vettore; considerando però una matrice abbiamo che (Snippet 3.6):
se una lista di inizializzatori per una riga non contiene tutti gli inizializzatori, allora i
rimanenti elementi conterranno il valore 0;
se non sono presenti tutte le liste di inizializzatori, ciascuna per ogni riga, le ultime
righe conterranno elementi con valore 0;
se in una lista di inizializzatori sono presenti più valori di quelli che una riga può
contenere, allora gli stessi saranno scartati e il compilatore potrà generare un errore
oppure un warning, come per esempio questo emesso da GCC: excess elements in array
initializer;

è possibile omettere le coppie di parentesi graffe { } interne e usare solo quelle esterne
e dunque scrivere gli inizializzatori direttamente. In questo caso il compilatore conta
tanti inizializzatori quanti sono sufficienti per valorizzare gli elementi di una riga e poi
passa ai successivi per la riga successiva e così via fino al riempimento di tutte le righe
della matrice. Alcuni compilatori potrebbero generare un messaggio di avviso del tipo
missing braces around initializer. Per GCC, al fine di far emettere tale messaggio, è

possibile usare il flag -Wmissing-braces.

Infine è possibile omettere la dimensione delle righe e delle colonne solo se non si
omettono le liste di inizializzatori interne inclusive delle relative parentesi { }, così come è
consentito utilizzare gli inizializzatori designati.

Snippet 3.6 Inizializzazione di un array bidimensionale.


...
#define NR_OF_ROWS 3
#define NR_OF_COLS 3

int main(void)
{
// matrice di 3×3 elementi
int data[NR_OF_ROWS][NR_OF_COLS] =
{
{10, 100}, // prima riga - meno inizializzatori ultimo elemento valore 0
{-10, -100, -1000, 99999} // seconda riga - più inizializzatori warning o errore?
/* terza riga omessa: tutti gli elementi conterranno il valore 0 */
};

// matrice di 3×3 elementi: utilizziamo solo le { } esterne


int other_data[NR_OF_ROWS][NR_OF_COLS] =
{
1, 2, 3, 4, 5, 6, 7, 8, 9
};

// crea una matrice di 2×2 elementi utilizzando gli inizializzatori designati


int table[2][2] =
{
[0][1] = 1000, /* riga 0 colonna 1 valore 1000 */
[1][0] = 500 /* riga 1 colonna 0 valore 500 */
};
...
}
Subscripting
Dopo la fase di dichiarazione e/o di inizializzazione di una matrice gli elementi relativi
possono essere utilizzati in fase di lettura e scrittura mediante la Sintassi 3.7.

Sintassi 3.7 Accesso a un elemento di un array bidimensionale.


array_identifier[row_index][column_index] = value; // in scrittura
other_variable = array_identifier[row_index][column_index]; // in lettura

Si scrive il nome dell’array e due volte l’operatore di subscript [ ][ ] al cui interno si


indicano, rispettivamente, l’indice o la posizione della riga e della colonna dove si trova
l’elemento da manipolare in lettura o in scrittura.
In buona sostanza per accedere a un elemento nella riga r e nella colonna c di una matrice
m dobbiamo scrivere un’espressione nella forma m[r][c], dove m[r] indica la riga r di m e m[r]
[c] seleziona l’elemento in tale riga che si trova nella colonna c.
ATTENZIONE
Non scrivere mai qualcosa del tipo m[r, c] perché C, in questo contesto, tratta la virgola
come un operatore che combina le due espressioni r e c e ritorna come valore c. Pertanto
indicare m[r, c] è equivalente a scrivere m[c].

Snippet 3.7 Accesso agli elementi di un array bidimensionale.


...
#define NR_OF_ROWS 3
#define NR_OF_COLS 3

int main(void)
{
// matrice di 3×3 elementi
int data[NR_OF_ROWS][NR_OF_COLS] =
{
{10, 100, 1000},
{-10, -100, -1000},
{1, 11, 1111}
};

// accesso alla seconda colonna della seconda riga: valore -100


int element = data[1][1];

// scrittura nella terza colonna della terza riga: sostituisce il valore


// 1111 con il valore 0
data[2][2] = 0;
...
}

Lo Snippet 3.7 dichiara e inizializza la matrice data composta da 3 righe per 3 colonne e
assegna alla variabile di tipo int element il contenuto della sua seconda riga e seconda
colonna così come vi pone il valore 0 nella terza riga e terza colonna.
La Figura 3.9 mostra una rappresentazione della matrice data al termine delle suddette
operazioni visualizzata nel consueto modello tabellare e come è effettivamente disposta in
memoria, evidenziando in quest’ultimo caso il meccanismo del row-major order e che ogni
indirizzo successivo è, dato un int di 32 bit, esattamente 4 byte dopo il precedente.
Figura 3.9 Rappresentazione tabellare e in memoria della matrice data.

Listato 3.2 ArrayBidi.c (ArrayBidi).


/* ArrayBidi.c :: Utilizzo di una matrice :: */
#include <stdio.h>
#include <stdlib.h>

#define YEARS 4
#define QUARTERS 4
#define START_YEAR 2010
#define END_YEAR 2013

int main(void)
{
double total = 0; // totale introiti in tutti gli anni e trimestri
double subtotal; // totale parziale introiti di un anno

// introiti percepiti negli anni dal 2010 al 2013 nei relativi 4 trimestri
double earnings[YEARS][QUARTERS] =
{
/* I II III IV trimestre */
{890.00, 899.00, 1000.11, 998.55}, /* anno 2010 */
{789.59, 800.00, 1234.99, 699.00}, /* anno 2011 */
{1490.00, 497.33, 100.00, 2045.60}, /* anno 2012 */
{678.00, 1999.00, 632.50, 1090.00} /* anno 2013 */
};

printf("Anno\tIntroiti raggruppati per anni dati i trimestri\n\n");


for (int i = 0; i < YEARS; i++) // loop esterno per ogni anno
{
for (int j = 0; j < QUARTERS; j++) // loop interno per ogni trimestre
{
subtotal += earnings[i][j];
}
printf("%d\t%10.2f\n", START_YEAR + i, subtotal);
total += subtotal; // aggiorna il totale
subtotal = 0;
}
printf("\nTotale\t%10.2f\n\n", total);

printf("Trim.\tIntroiti raggruppati per trimestri dati gli anni\n\n");


total = 0;
for (int j = 0; j < QUARTERS; j++) // loop esterno per ogni trimestre
{
for (int i = 0; i < YEARS; i++) // loop interno per ogni anno
{
subtotal += earnings[i][j];
}
printf("%d\t%10.2f\n", 1 + j, subtotal);
total += subtotal; // aggiorna il totale
subtotal = 0;
}
printf("\nTotale\t%10.2f\n\n", total);

return (EXIT_SUCCESS);
}

Output 3.2 Dal Listato 3.2 ArrayBidi.c.


Anno Introiti raggruppati per anni dati i trimestri

2010 3787.66
2011 3523.58
2012 4132.93
2013 4399.50

Totale 15843.67

Trim. Introiti raggruppati per trimestri dati gli anni

1 3847.59
2 4195.33
3 2967.60
4 4833.15

Totale 15843.67

Il Listato 3.2 fornisce una dimostrazione pratica di dichiarazione, inizializzazione e


utilizzo di una matrice che intende modellare i guadagni percepiti da un soggetto nei quattro
trimestri delle annualità 2010-2013.
La matrice earnings inizializza 4 righe, ciascuna rappresentante un anno, laddove ogni
lista di inizializzatori assegna un importo al rispettivo elemento della relativa colonna che
rappresenta il trimestre di riferimento.
Per la visita della matrice utilizziamo delle strutture di iterazione for che sono tra di loro
innestate, dove quella più esterna consente di scorrere l’indice della prima dimensione,
primo operatore [ ], mentre quella più interna permette di scorrere l’indice della seconda
dimensione, secondo operatore [ ].
Questo pattern di accesso agli elementi di un array bidimensionale è molto comune;
nell’utilizzarlo bisogna rammentare che il loop interno è sempre quello che fa scorrere più
velocemente l’indice per la scansione dei relativi elementi (generalmente questo indice è
riferito alla dimensione delle colonne ossia al secondo subscript [ ]).
Sicché nel nostro caso avremo che:
per i primi cicli for per ogni anno (riga) visiteremo tutti i trimestri (colonne) e questo
indice scorrerà più velocemente. Per esempio, la visita della matrice avverrà così:
earnings[0][0], earnings[0][1], earnings[0][2], ..., earnings[1][0], earnings[1][1],

earnings[1][2] e così via fino a earnings[3][3];


per i secondi cicli for per ogni trimestre (colonna) visiteremo tutti gli anni e questo
indice scorrerà più velocemente. Per esempio, la visita della matrice avverrà così:
earnings[0][0], earnings[1][0], earnings[2][0], ..., earnings[0][1], earnings[1][1]

earnings[2][1] e così via fino a earnings[3][3].

Array tridimensionali
Un array tridimensionale è una struttura dati che, al pari del vettore e della matrice, è
anch’essa composta da un insieme di variabili che ne rappresentano sempre gli elementi e
che sono disposte in memoria in modo lineare, ma vi differisce perché ha tre indici e perché
può essere visualizzata come una sorta di pila di tabelle posizionate le une sopra le altre
(Figura 3.10).
In questo caso possiamo assumere che la prima dimensione indica la posizione di
paginazione della corrispettiva tabella nell’ambito di un astratto spazio tridimensionale,
mentre la seconda e la terza dimensione indicano, come di consueto, la quantità di righe e
colonne della tabella.
Tuttavia, per C, anche in questo caso, un array a 3 dimensioni è in realtà un array a una
dimensione dove ogni elemento è esso stesso un array a una dimensione e dove ogni
elemento di quest’ultimo array è esso stesso un array a una dimensione (array di array di
array).

Figura 3.10 Rappresentazione come una pila di tabelle di un array tridimensionale.

Dichiarazione
Un array tridimensionale si dichiara utilizzando la seguente sintassi, dove si noti
l’utilizzo di tre coppie di parentesi quadre [ ] ciascuna a indicare una dimensione (Sintassi
3.8).
Sintassi 3.8 Dichiarazione di un array tridimensionale.
data_type identifier[NUMBER_OF_PAGES][NUMBER_OF_ROWS][NUMBER_OF_COLS];

Snippet 3.8 Dichiarazione di una pila di tabelle di 2×3 elementi.


...
#define NR_OF_PAGES 3 /* numero di pagine */
#define NR_OF_ROWS 2 /* numero di righe */
#define NR_OF_COLS 3 /* numero di colonne */

int main(void)
{
// dichiarazione dell'array 3D data con 3 tabelle di 2×3 elementi
int data[NR_OF_PAGES][NR_OF_ROWS][NR_OF_COLS];
...
}

Lo Snippet 3.8 dichiara l’array tridimensionale data di tipo int che rappresenterà una pila
di 3 (NR_OF_PAGES) tabelle, ciascuna con 2 (NR_OF_ROWS) righe e 3 (NR_OF_COLS) colonne. Gli
elementi totali dell’array 3D saranno 18, valore che è dato dalla moltiplicazione del numero
di pagine (3), per il numero di righe (2) e colonne (3) della relativa tabella.

Figura 3.11 Una possibile rappresentazione concettuale dell’array data come pila di tabelle.

Inizializzazione
Un array tridimensionale può essere inizializzato (Sintassi 3.9) contestualmente alla sua
dichiarazione in modo da fornire valori significativi ai corrispondenti elementi.

Sintassi 3.9 Inizializzazione di un array tridimensionale.


data_type identifier[NR_OF_PAGES][NR_OF_ROWS][NR_OF_COLS] =
{
{
{value_0, value_1, ..., value_N},
{value_0, value_1, ..., value_N}
},
{
{value_0, value_1, ..., value_N},
{value_0, value_1, ..., value_N}
}
...
};

In questo caso si deve fornire, dopo l’operatore di assegnamento =, una coppia di


parentesi graffe { } esterne seguite da una serie di parentesi graffe { } interne ciascuna
contenente un’altra serie di parentesi graffe { } interne con gli inizializzatori che forniranno
dei valori ai corrispondenti elementi della corrente riga della corrente tabella alla corrente
posizione di pagina. Sia le prime parentesi graffe { } interne, che indicano le tabelle, sia le
parentesi graffe interne { } a quest’ultime innestate, che ne marcano le righe, devono essere
separate dal carattere ,.
Le regole di scrittura delle liste di inizializzatori ricalcano quelle già viste per
l’inizializzazione di una matrice, considerando che se si omette una lista di inizializzatori
con annesse parentesi graffe { } per una tabella i relativi elementi saranno tutti inizializzati
con il valore 0.

Snippet 3.9 Inizializzazione di un array tridimensionale.


...
#define NR_OF_PAGES 3
#define NR_OF_ROWS 2
#define NR_OF_COLS 3

int main(void)
{
// array 3D con 3 tabelle di 2×3 elementi
int data[NR_OF_PAGES][NR_OF_ROWS][NR_OF_COLS] =
{
{ // prima tabella
{10, 100}, // prima riga
{-10, -1000} // seconda riga
},
{ // seconda tabella
{44}, // prima riga
{55} // seconda riga
},
// terza tabella: utilizzo di un inizializzatore designato
// e vi pongo 999 alla riga 0 e colonna 0
[2][0][0] = 999
};
...
}

Subscripting
Dopo la fase di dichiarazione e/o di inizializzazione di un array tridimensionale gli
elementi relativi possono essere utilizzati in fase di lettura e scrittura mediante la Sintassi
3.10.

Sintassi 3.10 Accesso a un elemento di un array tridimensionale.


array_identifier[page_index][row_index][column_index] = value; // in scrittura
other_variable = array_identifier[page_index][row_index][column_index]; // in lettura

Si scrive il nome dell’array e tre volte l’operatore di subscript [ ][ ][ ] al cui interno si


indicano, da sinistra a destra, l’indice numerico di pagina, di riga e di colonna dove si trova
l’elemento da manipolare in lettura o in scrittura.
Quindi in un’espressione come m[p][r][c], m[p] indica una determinata tabella p dove
tramite r e c accediamo al correlativo elemento.

Snippet 3.10 Accesso agli elementi di un array tridimensionale.


...
#define NR_OF_PAGES 3
#define NR_OF_ROWS 2
#define NR_OF_COLS 3

int main(void)
{
// array 3D con 3 tabelle di 2×3 elementi
int data[NR_OF_PAGES][NR_OF_ROWS][NR_OF_COLS] =
{
{ // prima tabella
{10, 100}, // prima riga
{-10, -1000} // seconda riga
},
{ // seconda tabella
{44},
{55}
},
// terza tabella; utilizzo un inizializzatore designato
// e vi pongo 999 alla riga 0 e colonna 0
[2][0][0] = 999
};

// accesso alla prima colonna della seconda riga della seconda tabella
// element conterrà il valore 55
int element = data[1][1][0];

// scrittura nella seconda colonna della prima riga della terza tabella
data[2][0][1] = -630;
...
}

Lo Snippet 3.10 dichiara e inizializza l’array tridimensionale data composto da 3 tabelle


ciascuna composta da 2 righe per 3 colonne e assegna alla variabile di tipo int element il
contenuto della prima colonna, della seconda riga della seconda tabella.
Scrive altresì nell’array data il valore -630 nella seconda colonna della prima riga della
terza tabella. Il risultato di queste operazioni è visibile nella Figura 3.12.
Figura 3.12 Rappresentazione come pila di tabelle e in memoria dell’array tridimensionale data.

Listato 3.3 ArrayTridi.c (ArrayTridi).


/* ArrayTridi.c :: Utilizzo di un array tridimensionale :: */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NR_OF_PAGES 3
#define NR_OF_ROWS 20
#define NR_OF_COLS 50

int main(void)
{
// inizializzazione del generatore pseudo-casuale dei numeri
srand((unsigned int) time(NULL));

// vettore per le somme degli elementi di ciascuna tabella


int sum[NR_OF_PAGES] = {0};

// array 3D
int data[NR_OF_PAGES][NR_OF_ROWS][NR_OF_COLS];

// scrivo dei dati casuali nelle tre tabelle


for (int j = 0; j < NR_OF_PAGES; j++) // indice pagina
{
for (int k = 0; k < NR_OF_ROWS; k++) // indice riga
{
for (int l = 0; l < NR_OF_COLS; l++) // indice colonna
{
data[j][k][l] = rand() % 1000; // numeri pseudo-casuali tra 0 e 999

// operazione di somma: operatore += permette di scrivere in modo


// abbreviato: sum[j] = sum[j] + data[j][k][l];
sum[j] += data[j][k][l];
}
}
}

// mostro la somma dei valori per ciascuna tabella


for (int j = 0; j < NR_OF_PAGES; j++)
{
printf("La tabella %d contiene valori per una somma totale di %d\n", j, sum[j]);
}

return (EXIT_SUCCESS);
}

Output 3.3 Dal Listato 3.3 ArrayTridi.c.


La tabella 0 contiene valori per una somma totale di 504794
La tabella 1 contiene valori per una somma totale di 500795
La tabella 2 contiene valori per una somma totale di 510163

Il Listato 3.3 mostra come creare un array tridimensionale che vuole modellare un
ipotetico taccuino composto da 3 pagine dove ciascuna pagina è composta da 20 righe e 50
colonne che conterranno come elementi dei codici numerici scelti a caso tra 0 e 999.
Inoltre, per ogni pagina, è memorizzato in un apposito vettore sum la somma di tutti gli
elementi, che è infine mostrata a video con la consueta istruzione printf.
Per la visita dell’array tridimensionale utilizziamo tre cicli for, dove quello più esterno fa
scorrere l’indice delle pagine, quello a esso innestato fa scorrere l’indice delle righe e quello
a sua volta a quest’ultimo innestato fa scorrere l’indice delle colonne.
Anche in questo caso, dunque, come nell’esempio della visita di un array bidimensionale,
i cicli for nidificati rivestono un ruolo fondamentale per il corretto processing degli elementi
degli array multidimensionali.

LE FUNZIONI SRAND E RAND: UN CENNO


Nel Listato 3.3 ogni elemento dell’array tridimensionale data ha un valore pseudo-casuale che
viene generato grazie alla funzione rand dichiarata nel file header <stdlib.h>. Questa funzione
ritorna, infatti, un valore di tipo int nell’intervallo di valori da 0 a RAND_MAX, costante simbolica
che deve essere almeno pari a 32767. La funzione srand, sempre dichiarata nel file header
<stdlib.h>, consente invece, dato un seme, di generare una nuova sequenza di numeri
pseudo-casuali. Nel nostro caso rand % 1000 produce un valore pseudo-casuale tra 0 e 999 per
effetto del numero 1000, che utilizzato unitamente all’operatore modulo % permette di scalare il
numero ritornato da rand. Per esempio, se il numero ritornato è 10 allora 10 % 1000 produce 10,

se è 15999 allora 15999 % 1000 produce 999 e così via. La funzione srand, invece, è stata
inizializzata con un valore ritornato dalla funzione time, dichiarata nel file header <time.h>, che,
essendo uguale ai secondi passati dalla mezzanotte del 1° gennaio 1970 fino al momento
dell’invocazione, consente di ottenere un seme sempre differente e di conseguenza una
sequenza diversa di numeri pseudo-casuali a ogni esecuzione del programma e dunque una
differente somma degli elementi delle tre tabelle.

Vediamo un altro esempio (Listato 3.4) di impiego di un array tridimensionale, molto


comune, atto a descrivere delle coordinate in un sistema spaziale a tre dimensioni, dove
possiamo avere un punto che è posto in una determinata posizione sull’asse delle x, delle y
e delle z.

Listato 3.4 ThreeDimensionalSpace.c (ThreeDimensionalSpace).


/* ThreeDimensionalSpace.c :: Utilizzo di un array tridimensionale :: */
#include <stdio.h>
#include <stdlib.h>

#define X 100
#define Y 100
#define Z 20

#define false 0
#define true 1

int main(void)
{
// uno spazio tridimensionale: coordinate x, y e z
_Bool space[X][Y][Z] =
{
{
{false} // assenza di punti in tutte le coordinate
}
};

// mettiamo qualche punto nello spazio: true significa che il punto


// è presente in quella coordinata; false significa assenza del punto
space[0][0][0] = true;
space[0][0][2] = true;
space[0][1][0] = true;
space[0][1][1] = true;
space[0][1][2] = true;
space[0][2][1] = true;

for (int x = 0; x < X; x++)


{
for (int y = 0; y < Y; y++)
{
for (int z = 0; z < Z; z++)
{
// comunica le coordinate spaziali solo se è presente un punto
if (space[x][y][z])
{
printf("[X = %d, Y = %d, Z = %d]\n", x, y, z);
}
}
}
}

return (EXIT_SUCCESS);
}

Output 3.4 Dal Listato 3.4 ThreeDimensionalSpace.c.


[X = 0, Y = 0, Z = 0]
[X = 0, Y = 0, Z = 2]
[X = 0, Y = 1, Z = 0]
[X = 0, Y = 1, Z = 1]
[X = 0, Y = 1, Z = 2]
[X = 0, Y = 2, Z = 1]

Il Listato 3.4 dichiara e inizializza l’array a tre dimensioni space atto a contenere
informazioni se un punto è presente (valore true) o meno (valore false) in una determinata
locazione spaziale. A tal fine assegniamo il valore true a sei posizioni dell’array space con le
successive istruzioni di assegnamento e mediante l’utilizzo della consueta square bracket
notation.
In conclusione, utilizziamo il solito ciclo for per scorrere i valori dell’array e per
stampare a video solo le coordinate dove è effettivamente presente un punto.
Array di lunghezza variabile
Gli array sin qui esaminati, sia monodimensionali sia multidimensionali, nella loro fase di
dichiarazione, hanno espresso la quantità di elementi in essi contenuti tramite delle
espressioni che fornivano un valore costante intero.
A partire da C99, invece, è possibile dichiarare un array fornendo agli operatori di
subscript relativi dei valori che possono derivare anche da valutazioni di variabili, e che
sono perciò computati a run-time dal compilatore.
Questa caratteristica, che da C11 è diventata opzionale, permette quindi la creazione dei
cosiddetti array di lunghezza variabile (variable-length array, abbreviati in VLA), ossia di
array la cui dimensione, per l’appunto, è variabile, cioè ricavabile anche da espressioni che
non producono necessariamente valori costanti di tipo intero.
Grazie a questa feature, è possibile creare degli array con un congruo e non arbitrario
numero di elementi; ossia che non hanno più elementi del necessario e dunque non si spreca
inutilmente la memoria; che non hanno meno elementi del necessario e dunque si evita che
il programma possa fallire a causa di inserimenti di valori in posizioni di memoria non
relative all’array, cioè non allocate correttamente.

Listato 3.5 VariableLengthArray.c (VariableLengthArray).


/* VariableLengthArray.c :: Utilizzo di un array VLA :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int dim;
printf("Digita la dimensione del vettore: ");
scanf("%d", &dim);

int vector[dim]; // VLA di dimensione variabile

printf("Digita %d numeri da inserire negli elementi del vettore: ", dim);


for (int n = 0; n < dim; n++)
{
scanf("%d", &vector[n]);
}

printf("Copio i valori degli elementi del tuo vettore in un nuovo vettore...\n\n");


int other_vector[dim];

for (int n = 0; n < dim; n++)


{
other_vector[n] = vector[n];
}

printf("Stampo i valori degli elementi dei due vettori...\n");

printf("Vettore: vector ->\t\t[");


for (int n = 0; n < dim; n++)
{
printf("%d,", vector[n]);
}
printf("\b]\n");
printf("Vettore: other_vector ->\t[");
for (int n = 0; n < dim; n++)
{
printf("%d,", other_vector[n]);
}
printf("\b]\n");

return (EXIT_SUCCESS);
}

Output 3.5 Dal Listato 3.5 VariableLengthArray.c.


Digita la dimensione del vettore: 5
Digita 5 numeri da inserire negli elementi del vettore: 12 22 33 44 55
Copio i valori degli elementi del tuo vettore in un nuovo vettore...

Stampo i valori degli elementi dei due vettori...


Vettore: vector -> [12,22,33,44,55]
Vettore: other_vector -> [12,22,33,44,55]

Il Listato 3.5 consente di inserire da tastiera un numero arbitrario di elementi con cui sarà
creato il vettore vector. Tale vettore, infatti, è dichiarato in modo che a run-time il
compilatore allochi una quantità di memoria che è pari alla dimensione di un int per il
numero di elementi assegnato alla variabile dim, che è poi usata nell’ambito delle parentesi
quadre [ ] per dichiarare il predetto vettore vector (per esempio, se dim sarà pari a 5 avremo
che il compilatore allocherà, dato un int di 4 byte, 20 byte di memoria atti a contenere i
cinque elementi dell’array vector).
Dopo la creazione del vettore il programma permette anche di ottenere da tastiera dei
valori che saranno inseriti negli elementi specificati da dim e che serviranno per
inizializzarlo con valori significativi.
I valori degli elementi di questo vettore sono poi copiati negli elementi di un altro vettore,
denominato other_vector, e infine i valori degli elementi di entrambi i vettori sono stampati a
video al fine di permettere di verificare che entrambi i vettori contengano elementi con gli
stessi valori.
IMPORTANTE
In C non è possibile assegnare direttamente un array a un altro array per copiare i relativi
elementi. Un’istruzione come other_vector = vector non è dunque sintatticamente corretta.

Infine, nell’utilizzo degli array di tipo VLA bisogna tenere presenti le seguenti limitazioni
(Snippet 3.11):
non possono essere inizializzatati contestualmente alla loro dichiarazione con le
consuete liste di inizializzatori;
non possono avere lo specificatore di classe di memorizzazione static o extern;
possono essere dichiarati solo nelle funzioni (anche come parametri), in qualsiasi
blocco di codice e anche come parametri nei prototipi di funzioni.

Snippet 3.11 Restrizioni dei VLA.


int num = 10;

// error: variable-sized object may not be initialized


int data[num] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// error: storage size of 'other_data' isn't constant


static int other_data[num];
Array costanti
Il qualificatore di tipo const può essere utilizzato anche con gli array al fine di dichiarare
che i relativi elementi, dopo essere stati inizializzati, non possono subire modifiche.
Ciò implica, pertanto, che un programma può solo accedere in lettura agli elementi di un
array costante senza potergli assegnare, in scrittura, nuovi valori.

Snippet 3.12 Array costanti.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
// vettore read-only
const int data[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// error: assignment of read-only location 'data[1]'


data[1] = 100;
...
}

Lo Snippet 3.12 mostra in modo inequivocabile come l’array data, essendo qualificato
come costante, non permetta dopo la sua inizializzazione di cambiare i valori dei suoi
elementi. Infatti, l’istruzione data[1] causa un errore in fase di compilazione perché tenta di
assegnare il valore 100 al secondo elemento dell’array data, che però è una locazione di
memoria a sola lettura.
La dimensione degli array e l’operatore sizeof
L’operatore sizeof, già visto per l’utilizzo con i tipi di dati, è impiegato anche per
ritornare, sempre in byte, la dimensione del tipo di un suo operando che può essere una
variabile, un array o una qualsiasi espressione unaria, ossia un’espressione che contiene un
operando e un operatore unario (Sintassi 3.11).

Sintassi 3.11 Operatore sizeof con espressioni.


sizeof expression

NOTA
Nei casi di utilizzo citati, le parentesi tonde non sono obbligatorie. Pertanto, se j è una
variabile di un determinato tipo, è del tutto equivalente scrivere sizeof(j) oppure sizeof j.

Nel caso di un array, applicare a esso l’operatore sizeof consente di ottenere il numero di
totale di byte di tutti i suoi elementi.

Listato 3.6 SizeOfArray.c (SizeOfArray).


/* SizeOfArray.c :: Uso di sizeof con gli array :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 10

int main(void)
{
// un array monodimensionale
int data[SIZE] = {1, 2, 3, 4, 5, 6, 7, 8, 9, 10};

// ritorna la lunghezza dell'array data


printf("Il vettore data ha %zu elementi in totale!\n", sizeof data / sizeof data[0]);

return (EXIT_SUCCESS);
}

Output 3.6 Dal Listato 3.6 SizeOfArray.c.


Il vettore data ha 10 elementi in totale!

Il Listato 3.6 mostra un pattern molto comune impiegato per “scoprire” la quantità di
elementi di un array che prevede, in primo luogo, la valutazione delle due seguenti
espressioni contenenti l’operatore sizeof considerando un sistema target dove il tipo int è di
4 byte:
sizeof data ritorna la quantità in byte di tutti gli elementi dell’array, ovvero 40;
sizeof data[0] ritorna la quantità in byte del primo elemento dell’array, ovvero 4.

In secondo luogo, le due quantità ritornate sono oggetto di un’operazione di divisione.


Infatti, 40 / 4 dà come risultato 10, che è l’esatta quantità di elementi dell’array in accordo
con la costante simbolica SIZE utilizzata per dichiararlo.
Capitolo 4
Operatori

Un operatore è definibile come una sorta di istruzione che agisce su dei dati, detti
operandi, e permette di ottenere un risultato eseguendo un’operazione. Ogni operatore è
rappresentato da simboli che determinano su quali operandi agisce.
Quando in un’espressione si incontrano diversi operatori, l’ordine di esecuzione viene
scelto in base alla precedenza (che indica se un operatore ha priorità maggiore di un altro) e
all’associatività (se più operatori hanno la stessa precedenza allora viene eseguito, nel caso
dell’associatività da sinistra a destra, prima quello che si trova più a sinistra e poi a seguire
gli altri sempre da sinistra; nel caso dell’associatività da destra a sinistra, viene eseguito
prima quello che si trova più a destra e poi sempre da destra a seguire gli altri).
In ogni caso, l’ordine di precedenza prestabilito può essere variato con l’utilizzo
dell’operatore parentesi tonde ( ), che ha la precedenza più alta in assoluto; se vi sono varie
coppie di parentesi annidate, la priorità sarà dalla più interna verso quella più esterna,
mentre se ve ne sono varie sullo stesso livello, l’associatività sarà da sinistra a destra.
In ogni caso è bene tenere presente che non vi è alcuna regola di precedenza tra gli
operandi, data un’espressione complessa formata da più operatori e operandi, la cui
decisione su quale valutare per primo è lasciata all’arbitrio dell’implementazione, e ciò per
le consuete ragioni di efficienza: su una determinata architettura hardware potrebbe essere
per l’appunto più conveniente, valutare prima un operando piuttosto che un altro.
NOTA
In espressioni dove vi sono gli operatori logical AND con simbolo &&, logical OR con simbolo
||, conditional con simboli ?: e comma con simbolo , l’ordine di precedenza degli operandi
nelle relative sotto-espressioni è invece garantito: verranno valutati prima quelli scritti più a
sinistra e poi a seguire gli altri (left-to-right).

CONSIGLIO
Non scrivere mai codice che dipende dall’ordine di valutazione degli operandi in espressioni
complesse perché questo non è portabile tra sistemi diversi che possono scegliere, come
già detto, una priorità di valutazione piuttosto che un’altra. Questo non riguarda però l’ordine
di valutazione degli operatori, le cui regole sono ben definite e non arbitrarie. Consultare a
tal fine la Tabella 4.7 di precedenza degli operatori posta alla fine del corrente capitolo.

Snippet 4.1 Ordine di valutazione di operatori e operandi.


int a = 10;
int b = 12;
int c = 100;
int d = 20;
int res = a * b + c / d; // 120 + 5 = 125

Lo Snippet 4.1 assegna alla variabile res il risultato della valutazione dell’espressione
posta alla destra dell’operatore di assegnamento =.
In questo caso, per le regole di precedenza degli operatori, l’operatore di moltiplicazione
* e l’operatore di divisione / hanno una precedenza più alta rispetto all’operatore di

addizione +. Pertanto si avrà la certezza che saranno eseguiti, prima, a * b ec / d, dopodiché


i relativi risultati saranno sommati.
Tuttavia, non essendo garantita la precedenza degli operandi, non sapremo se il corrente
compilatore avrà valutato prima la sotto-espressione a * b oppure prima la sotto-espressione
c / d che sono, nella sostanza, gli operandi dell’operatore di addizione.
Comunque, ai fini del risultato finale della nostra espressione, questo arbitrio non causerà
alcun problema perché avremo sempre, prima dell’addizione, l’espressione valutata come
120 + 5 e dunque il risultato 125.

La libertà di decisione di quale sotto-espressione valutare per prima ha però in alcuni


casi, delle importanti conseguenze di ambiguità su quale possa essere il risultato finale di
una valutazione complessiva, soprattutto quando in una sotto-espressione si accede al valore
di una variabile e in un’altra se ne modifica il valore (Snippet 4.2):

Snippet 4.2 Ordine di valutazione di operatori e operandi: ambiguità del risultato.


int a = 10;
int b;

int res = (b = a - 5) + (a = 11);

Nello Snippet 4.2, eseguire prima la sotto-espressione (b = a - 5) oppure prima la sotto-


espressione (a = 11) ha conseguenze diverse sul risultato finale assegnato alla variabile res;
infatti nel primo caso il risultato sarà 16, mentre nel secondo sarà 17:

1. a - 5 dà come risultato 5 che viene assegnato alla variabile b. Poi, 11 viene assegnato
alla variabile a. Quindi la valutazione di b + a dà come valore 16;
2. a = 11 assegna tale valore ad a. Poi, a - 5 dà come risultato 6 che viene assegnato alla
variabile b. Quindi la valutazione di b + a dà come valore 17.

NOTA
Alcuni compilatori, quando si compila codice contenente istruzioni come quelle dello Snippet
4.2, possono riportare un messaggio di avviso di comportamento non definito. Con GCC, se
usiamo il flag -Wall oppure il flag esplicito -Wsequence-point, avremo il seguente messaggio:
warning: operation on 'a' may be undefined.

Per evitare, dunque, ambiguità sul risultato valutato non bisogna mai usare quel tipo di
sotto-espressioni ma piuttosto “scomporle” in varie espressioni separate che diano chiarezza
sul quale possa essere il risultato atteso (Snippet 4.3).

Snippet 4.3 Ordine di valutazione di operatori e operandi: eliminazione di ambiguità del risultato.
int a = 10;
int b;
// ordine di valutazione esplicita delle espressioni che useranno la variabile a
b = a - 5; // prima questa espressione...
a = 11; // poi questa espressione...

int res = b + a; // 16
Terminologia essenziale: un dettaglio
Prima di iniziare la disamina degli operatori è opportuno soffermarci sulla corretta
semantica dei termini che lo standard utilizza per descrivere gli elementi fondamentali del
linguaggio considerando che, per alcuni di essi, già citati nel Capitolo 1, amplieremo il
dettaglio esplicativo, mentre di altri forniremo una descrizione ex-novo.
L’importanza di precisare il significato di questi termini, se da un lato può apparire
pedante, dall’altro lato può divenire essenziale per comprendere in modo chiaro e
approfondito i successivi elementi didattici trattati, dove utilizzeremo una terminologia la
cui semantica dovrà essere stata già stata correttamente appresa.
object indica una regione o area della memoria impiegata per memorizzare dei valori.
Un nome di una variabile, un elemento di un array e così via sono modi per identificare
un object.
lvalue indica un’espressione che identifica, localizza, l’area di memoria propria di un
object. Il termine lvalue proviene da una contrazione di left value (valore di sinistra) e
sta indicare una qualsiasi espressione che può comparire come operando a sinistra
dell’operatore di assegnamento (per esempio, se data è una variabile di tipo int la
stessa può comparire in un’istruzione come data = 10). In ogni caso, per effetto del
qualificatore const un lvalue può rappresentare un object ma può non essere un left
value valido. Pertanto, lo standard attuale, predilige designare un lvalue come un
locator value piuttosto che come un left value esplicitando che, data un’istruzione
generale di assegnamento come E1 = E2, E1 deve essere un modifiable lvalue, cioè un
left value modificabile.

ATTENZIONE
Un’istruzione come const int data = 10 è lecita perché in questo contesto l’operatore =

designa un’inizializzazione e non un assegnamento; pertanto non si sta violando la regola


secondo cui a sinistra dell’operatore = possono essere posti solo lvalue modificabili.

rvalue indica un valore risultato dalla valutazione di un’espressione che può essere
assegnato a un lvalue modificabile. Il termine rvalue è una contrazione di right value e
sta a indicare, per l’appunto, una costante, una variabile o una qualsiasi espressione che
produce un valore e che può comparire come operando a destra dell’operatore di
assegnamento (per esempio, considerando sempre la nostra variabile data, in
un’istruzione come data = 1999, 1999 è un rvalue). L’attuale standard usa il termine
value of an expression per riferirsi a un rvalue.
expression indica, solitamente, un’espressione, che è definibile come una sequenza di
operatori e operandi atti a computare un valore. Un operando è esso stesso
un’espressione (si pensi al nome di una variabile che valutato ne ritorna il valore
contenuto) e per subexpression si intende una sotto-espressione, ossia un’espressione
che è parte di un’espressione più complessa (per esempio, nell’espressione a * b - c /
d, a * b ec / d sono considerabili come sue sotto-espressioni). Per full expression,
invece, si intende un’espressione che non è parte di un’altra espressione (non è una
subexpression). Esempi di full expression sono quelle utilizzate nelle istruzioni if,
switch, do, while, for e return.
statement indica un’istruzione completa atta a compiere una qualche operazione
nell’ambito di un programma. In C, si formalizza mediante l’apposizione, alla fine, del
carattere punto e virgola (;). Così, qualcosa come data = 4 è un’espressione che può
comparire nell’ambito di un’espressione più complessa, ma data = 4; è un’istruzione
completa denominata expression statement. Un’espressione di un’expression statement
è considerata una full expression. In realtà qualsiasi espressione può essere
“convertita” in una statement apponendole il punto e virgola finale. Così scrivere 8;
oppure 10 - 4; non produrrebbe alcun errore in fase di compilazione; al massimo
potremmo avere un avviso da parte del compilatore che tali statement non hanno
effetto (con GCC, se usiamo il flag -Wall oppure il flag specifico -Wunused-value, viene
emesso il messaggio warning: statement with no effect). Per compound statement si
intende, infine, un gruppo di due o più statement, racchiuse tra le partentesi graffe
aperte/chiuse { }, che formano un’unica unità sintattica (questa istruzione composta è
anche chiamata block, poiché forma un blocco di codice).
side effect indica un’istruzione che modifica lo stato di un object o di un file. Per
esempio, una statement come data = 100; ha come effetto collaterale quello di cambiare
il contenuto della locazione di memoria riferito dalla variabile data.
sequence point indica un “punto” nell’ambito dell’esecuzione di un programma dove
vi è la garanzia che tutte le operazioni che producono dei side effect siano state
eseguite prima di procedere con il prossimo flusso esecutivo. Questo significa che se si
hanno due espressioni A e B, la presenza di un sequence point tra la valutazione di A e B
garantisce che la computazione di A o qualsiasi side effect causato di A avvenga
(sequenziati) prima della computazione o di side effect causati da B. Seguono alcuni
esempi di sequence point così come sono espressamente indicati dallo standard C11:
tra la valutazione del primo e del secondo operando degli operatori &&, ||, , (comma);
tra la valutazione di una full expression e la successiva full expression da valutare; tra
la valutazione del primo operando dell’operatore ?: e il secondo oppure il suo terzo
operando (a seconda di quello che viene valutato); tra la valutazione di un’invocazione
di funzione e relativi argomenti e la effettiva invocazione; immediatamente prima che
una funzione ritorni al chiamante.
Operatore di assegnamento semplice
L’operatore di assegnamento, con simbolo uguale (=), consente di assegnare un valore in
una variabile ovvero, detto in termini più rigorosi, permette di porre un valore computato da
un’espressione posta alla sua destra, nell’area di memoria rappresentata da un lvalue
modificabile posto alla sua sinistra.
Da ciò ne discende che non è possibile porre alla sinistra dell’operatore = un rvalue come
è il caso, per esempio, di un letterale costante o di una costante.

Snippet 4.4 Assegnamenti leciti e non leciti.


int a;
const int b = 100;

a = 100; // OK lvalue modificabile


b = 1111; // error: assignment of read-only variable 'b'
245 = a; // error: lvalue required as left operand of assignment
a + b = 22; // lvalue required as left operand of assignment

TERMINOLOGIA
Il termine assegnamento è utilizzato quanto assegnazione ed entrambi hanno praticamente
lo stesso significato. Per i programmatori è più consueto parlare di assegnamento, forse
perché c’è maggiore assonanza con il termine inglese assignment.

Quando si utilizza l’operatore di assegnamento bisogna considerare due importanti


aspetti: il primo riguarda il fatto che esso produce side effect poiché, l’aggiornamento
dell’area di memoria dell’operando posto alla sua sinistra rappresenta un’operazione di
modifica del suo stato; il secondo inerisce al processo di conversione che viene
automaticamente effettuato dal compilatore se, data una generica espressione di
assegnamento come E1 = E2, E1 ed E2 non hanno lo stesso tipo.
Ricordiamo, infatti, che nel secondo caso citato, il valore di E2 viene convertito nel tipo
atteso da E1.

Snippet 4.5 Conversioni durante degli assegnamenti.


float a = 11.3f;
int b;

// il valore di a è convertito in int e posto in b con perdita di informazione;


// la parte frazionaria è infatti scartata
b = a; // 11

// il valore 120 è convertito in float e poi posto in a sempre di tipo float


a = 120; // 120.000000

Infine, questo operatore associa da destra a sinistra, permettendo così di effettuare una
catena di assegnamenti.

Snippet 4.6 Assegnamenti multipli.


int a, b, c;
// equivalente a scrivere:
// a = (b = (c = 400));
a = b = c = 400; // a, b e c contengono il valore 400
Operatori aritmetici
Gli operatori aritmetici permettono di eseguire delle comuni operazioni aritmetiche
avvalendosi, in via principale, dei comuni operatori di addizione, sottrazione,
moltiplicazione e divisione.
La Tabella 4.1 ne dà una panoramica completa evidenziando anche come, secondo la
corrente terminologia adottata dallo standard C11, gli operatori + e - sono definiti anche
come operatori additivi mentre gli operatori *, / e % (modulo) sono definiti come operatori
moltiplicativi.
Tabella 4.1 Classificazione degli operatori aritmetici.
Unari Binari - Additivi Binari - Moltiplicativi
+ (unario più) + (addizione) * (moltiplicazione)
/ (divisione)
- (unario meno) - (sottrazione)
% (modulo)

TERMINOLOGIA
Un operatore si dice binario o diadico quando richiede due operandi e unario quando
richiede un operando. In C vi è anche l’operatore condizionale ?:, che è ternario perché
richiede tre operandi.

Operatore di addizione
L’operatore di addizione con simbolo più + consente di sommare il valore dell’operando
posto alla sua sinistra con il valore dell’operando posto alla sua destra.
Entrambi gli operandi sono denominati addendi e il risultato dell’operazione è detto
somma.
Gli operandi possono essere sia lvalue sia rvalue e tale operatore associa da sinistra a
destra.

Snippet 4.7 Operatore di addizione.


// somma tra interi
int a = 10, b = 12, c = 14;
int sum = a + b + c; // 36

// somma tra un float e in int: ul risultato è float


// j prima dell'addizione è convertito in float
float f = 33.44f;
int j = 100;
float other_sum = f + j; // 133.44

Operatore di sottrazione
L’operatore di sottrazione con simbolo meno - consente di sottrarre il valore
dell’operando posto alla sua destra, che rappresenta il sottraendo, dal valore dell’operando
posto alla sua sinistra, che rappresenta il minuendo. Il risultato dell’operazione è
denominato differenza.
Gli operandi possono essere sia lvalue sia rvalue e tale operatore associa da sinistra a
destra.

Snippet 4.8 Operatore di sottrazione.


// sottrazione tra due char: consentita perché i char sono standard integer types
// ATTENZIONE: C e K prima della sottrazione sono convertiti in int e
// poi il valore della sottrazione è convertito in char che è il tipo destinazione
// dell'assegnamento
char C = 'A'; // ASCII code 65
char K = 'z'; // ASCII code 122
char char_diff = C - K; // -57

// sottrazione tra un rvalue e un lvalue


int j = 100;
_Bool b_diff = 25 - j; // 1

Operatore di moltiplicazione
L’operatore di moltiplicazione con simbolo asterisco * consente di moltiplicare il valore
dell’operando posto alla sua sinistra, che rappresenta il moltiplicando, con il valore
dell’operando posto alla sua destra, che rappresenta il moltiplicatore. Il risultato
dell’operazione è denominato prodotto.
Gli operandi possono essere sia lvalue sia rvalue e tale operatore associa da sinistra a
destra.
CURIOSITÀ
In aritmetica il simbolo comunemente utilizzato per denotare la moltiplicazione è una sorta
di croce ruotata × (codice Unicode U+00D7), introdotto nel 1631 dal matematico inglese
William Oughtred, che non bisogna però confondere con la x minuscola (codice Unicode
U+0078). In algebra, invece, il simbolo utilizzato per il prodotto è un punto con simbolo ·

(codice Unicode U+22C5) detto dot operator. In informatica, infine, il simbolo * usato per la
moltiplicazione si fa risalire al FORTRAN, che è stato uno dei primi linguaggi di
programmazione ad alto livello, nato negli anni Cinquanta, orientato al calcolo scientifico e
numerico.

Snippet 4.9 Operatore di moltiplicazione.


const double ONE_METER_EQUALS_TO_FEET = 3.2808399;

// ottengo quanti piedi sono pari a 120 metri


// meter è convertito in double prima della moltiplicazione con la costante double
int meter = 120;
double feet = meter * ONE_METER_EQUALS_TO_FEET; // 393.700788

Operatore di divisione
L’operatore di divisione con simbolo barra (/) consente di dividere il valore
dell’operando posto alla sua sinistra, che rappresenta il dividendo, con il valore
dell’operando posto alla sua destra, che rappresenta il divisore. Il risultato dell’operazione è
denominato quoto.
Gli operandi possono essere sia lvalue sia rvalue e tale operatore associa da sinistra a
destra.
Quando si utilizza l’operatore di divisione bisogna considerare se gli operandi sono di
tipo intero oppure di tipo in virgola mobile. Nel primo caso, infatti, se il risultato della
divisione ha anche una parte frazionaria quest’ultima è scartata (troncata), mentre nel
secondo caso il risultato, essendo di tipo in virgola mobile, conserva anche la parte
frazionaria.
Inoltre, se l’operando divisore è 0 si causerà un comportamento non definito (undefined
behavior), ossia il relativo programma potrà comportarsi in modo diverso su diversi
compilatori oppure non compilarsi, bloccarsi e così via.

Snippet 4.10 Operatore di divisione.


int a = 100, b = 3;
// risultato intero: la parte frazionaria è stata troncata
int res = a / b; // 33

// b è esplicitamente convertito in float cosicché tutta l'espressione


// viene valutata in virgola mobile per le consuete regole di promozione
// dei tipi; infatti a viene convertita in float e dunque la divisione
// avviene tra 100.000000 / 3.000000
float f_res = a / (float)b; // 33.333332

// ATTENZIONE divisione per 0


// l'esecuzione con GCC causa un blocco del programma e un warning: division by zero
int div_0 = 100 / 0;

E se gli operandi della divisione tra interi hanno valori negativi come viene “trattato” un
eventuale risultato con parte frazionaria? In questo caso, secondo C89 un’implementazione
è libera se arrotondare il numero in eccesso o in difetto sempre eliminando la parte
frazionaria, mentre a partire da C99 il numero è sempre troncato eliminando le cifre
decimali (truncating toward zero).

Snippet 4.11 Truncating toward zero.


int a = -7, b = 5;

// il risultato sarebbe -1.4 ma lo stesso è stato troncato ed è -1


// un'implementazione in accordo con C89 potrebbe produrre come
// risultato sia -1 sia -2
int res = a / b;

Operatore modulo
L’operatore modulo con simbolo percentuale (%) consente di ottenere un valore che
rappresenta il resto di una divisione tra due operandi interi.
Non è possibile quindi utilizzare con l’operatore modulo tipi che non siano interi come,
per esempio, quelli in virgola mobile, altrimenti il compilatore genererà un errore specifico.
Gli operandi possono essere sia lvalue sia rvalue e tale operatore associa da sinistra a
destra.
Per quanto attiene alla possibilità di avere operandi negativi, le regole su come debba
essere prodotto il risultato del resto sono: per C89, il segno del resto dipende
dall’implementazione; a partire da C99, il segno del resto è negativo se il primo operando è
negativo altrimenti è positivo.
Infine, come per l’operatore di divisione, se l’operando di destra vale 0 il comportamento
sarà non definito.

Snippet 4.12 Operatore modulo.


double a = 7.0, b = 5.0;
// ATTENZIONE: errore di compilazione:
// error: invalid operands to binary % (have 'double' and 'double')
double res = a % b;

int j = 10, k = -3;


int mod = j % k; // 1 perché j è positivo

int n = -7, m = 5;
int other_mod = n % m; // -2 perché n negativo

DETTAGLIO
Se abbiamo due valori interi, diciamo j e k, allora possiamo ottenere il valore del loro resto
(j % k) con la seguente formula: j - (j / k) * k. Per esempio, 5 / 4 ha come resto il valore 1
risultato del calcolo dell’espressione 5 - (5 / 4) * 4.

Operatori unari + e –
Gli operatori unari con simboli più + e meno - consentono, rispettivamente, di ritornare il
valore dell’operando senza alterarne il segno e di ritornare il valore dell’operando
alterandone il segno (in pratica il valore è il negativo dell’operando).
L’operando può essere sia un lvalue sia un rvalue e tali operatori associano da destra a
sinistra.
In effetti, il + unario è stato introdotto con C90 per simmetria con il - unario ed è usabile
per evidenziare che un valore è positivo (nel K&R C tale operatore non era presente).

Snippet 4.13 Operatori unari + e –.


int a = -55;
int b = -a; // 55

// +100 usabile senza problemi


int k = 100;
int j = +100 - -k; // 200
Operatori unari di incremento e decremento
Gli operatori unari di incremento, con simbolo doppio più (++), e di decremento, con
simbolo doppio meno (––), permettono di sommare o sottrarre il valore 1 alla variabile
operando in una forma abbreviata rispetto alle sintassi n = n + 1 en = n - 1. Sono operatori
unari poiché agiscono su un solo operando; se sono prefissi all’operando si dicono di
preincremento o predecremento, mentre se sono postfissi all’operando si dicono di
postincremento o postdecremento. In un’espressione, se si usa un preincremento o un
predecremento, prima la variabile subisce l’incremento o il decremento e poi il nuovo
valore viene utilizzato; se invece si usa un postincremento o un postdecremento, prima il
valore della variabile viene utilizzato e poi la variabile subisce l’incremento o il
decremento.
CURIOSITÀ
Gli operatori di incremento e decremento ++ e -- sono apparsi per la prima volta nel
linguaggio B di Thompson, il quale li definì sintatticamente con la probabile motivazione che
la traduzione di un’espressione come ++x genera codice più compatto rispetto a
un’espressione come x = x + 1.

NOTA
In matematica un’espressione come n = n + 1 non ha alcun senso. Infatti, se aggiungiamo 1

a un qualsiasi valore di n il risultato non potrà mai essere uguale al numero n stesso. In C, di
converso, quell’espressione ha un preciso significato, ossia: leggi il valore della variabile n,
sommagli 1 e poi assegna tale risultato nella variabile n medesima.

Operatore postfisso di incremento e decremento


L’operatore postfisso di incremento ++ e decremento -- associa da sinistra a destra e il suo
operando può essere solo un lvalue modificabile.
Il valore dell’espressione è il valore dell’operando (Snippet 4.14).

Snippet 4.14 Operatore postfisso di incremento e decremento.


int res, a = 10, b = 9;

// a e b sono, rispettivamente, incrementati e decrementati, dopo che i loro valori


// sono stati computati
res = a++ - b--; // 1

// qui a vale 11 e b vale 8


int after = a + b; // 19

Lo Snippet 4.14 assegna alla variabile intera res il valore 1, risultato della computazione
del valore della variabile a meno il valore della variabile b. Infatti, dato che sia a sia b hanno
un operatore di incremento e decremento postfisso, prima viene ottenuto il rispettivo valore
(per a è 10 e per b è 9) e poi ne vengono effettuati l’incremento e il decremento.
Suffraga quando detto il valore della variabile intera after che ha come valore 19, risultato
dall’addizione del valore di a, che dopo l’incremento vale 11, e il valore di b, che dopo il
decremento vale 8.

Operatore prefisso di incremento e decremento


L’operatore prefisso di incremento ++ e decremento -- associa da destra a sinistra e il suo
operando può essere solo un lvalue modificabile.
Il valore dell’espressione è il valore dell’operando dopo che ha subìto l’incremento
oppure il decremento (Snippet 4.15).

Snippet 4.15 Operatore prefisso di incremento e decremento.


int res, a = 10, b = 9;

// a e b sono, rispettivamente, incrementati e decrementati prima che i loro valori


// vengano computati
res = ++a - --b; // 3

// qui a vale 11 e b vale 8


int after = a + b; // 19

Lo Snippet 4.15 assegna alla variabile intera res il valore 3, risultato della computazione
del valore della variabile a meno il valore della variabile b. In questo contesto, dato che sia a
sia b hanno un operatore di incremento e decremento prefisso, prima ne viene effettuato
l’incremento e il decremento (a varrà 11 e b varrà 8) e poi ne viene ottenuto il rispettivo
valore che è utilizzato per l’operazione di sottrazione.
In più, come nello Snippet 4.14, la variabile intera after ha come valore 19, risultato
dall’addizione del valore di a, che vale 11, e il valore di b, che vale 8.

Sequence point e gli operatori ++ e --


In linea generale l’ordine di valutazione delle espressioni nell’ambito del codice C
procede in modo abbastanza sequenziale e ordinato: dall’alto al basso e da sinistra a destra.
Un compilatore, tuttavia, ha la possibilità di stabilire un ordine di valutazione differente
se vi è un margine di ottimizzazione che consente di migliorare l’efficienza computazionale
del programma.
L’ordine di valutazione delle espressioni o delle sotto-espressioni, che a volte non è
determinabile, è comunque legato ai sequence point laddove un compilatore deve sempre
garantire che tutte le valutazioni delle espressioni di un sequence point siano state effettuate
prima di passare a un sequence point successivo. Tra questi sequence point, tuttavia, non vi
è alcuna garanzia dell’ordine di valutazione delle relative espressioni che un compilatore
può scegliere di adottare.
Così in un’expression statement come a = 10; che è una full expression e marca dunque
un sequence point, possiamo essere certi che prima di passare a un sequence point
successivo il compilatore avrà valutato tale espressione ponendo il valore 10 nella variabile
a.

Ma cosa accade se, invece, abbiamo la seguente espressione?

Snippet 4.16 Sequence point e l’operatore ++.


...
#define SIZE 10 /* dimensione dell'array */

int main(void)
{
int vector[SIZE] = {0};
int ix = 5;

vector[ix] = 2 + ix++; // ordine di valutazione non definito


...
}

Nello Snippet 4.16 definiamo il vettore vector di 10 elementi e poi usiamo un’espressione
che nelle nostre intenzioni “dovrebbe” assegnare all’elemento nella posizione 5 (valore di
ix) la somma tra il letterale intero 2 e il valore della variabile ix.

Tuttavia, per come abbiamo scritto l’espressione di assegnamento, il compilatore può


generare un avviso come warning: operation on 'ix' may be undefined e ciò perché non vi è
alcuna garanzia se sarà valutata prima l’espressione vector[ix] oppure l’espressione 2 + ix++.

Infatti potremmo avere lecitamente uno dei due seguenti risultati:


1. l’indice ix vale 5 e al corrispettivo elemento viene assegnato il valore 7;
2. l’indice ix vale 6 e al corrispettivo elemento viene assegnato il valore 7.

Il primo caso si potrebbe verificare se il compilatore decidesse di valutare prima


l’espressione posta a sinistra dell’operatore = dove ix vale ancora 5.
Il secondo caso, invece, potrebbe verificarsi se il compilatore decidesse di valutare prima
l’espressione posta alla destra dell’operatore = dove ix sarebbe incrementato di 1 e dunque in
vector[ix], ix varrebbe 6. In questo caso è altresì opportuno precisare che l’incremento di ix,
in quanto usato l’operatore postfisso ++, avverrebbe comunque dopo la computazione del
valore tra 2 e ix stesso che, prima di quell’incremento varrebbe ancora 5 e dunque in
vector[ix] sarebbe posto il valore 7.
ATTENZIONE
Una risposta nella quale ix vale 5 e al corrispettivo elemento viene assegnato il valore 8 non
è mai possibile perché il valore dell’espressione complessa posta a destra dell’operatore =
sarà sempre 7, poiché i++ attua un incremento postfisso: prima si legge il valore di i e si
somma a 2 e poi si incrementa i medesimo di 1.

Perché, dunque, in un’espressione come quella evidenziata si può avere un risultato


ambiguo? La risposta risiede nel fatto, come già anticipato, che l’ordine di valutazione delle
espressioni poste tra i sequence point è non definito.
Infatti, nel nostro caso, a parte le precedenti istruzioni di dichiarazione del vettore vector e
della variabile ix, l’altro sequence point è la full expression completa vector[ix] = 2 + ix++;

marcata dal punto e virgola di terminazione dell’istruzione relativa.


L’operatore di assegnamento = non marca, cioè non introduce, alcun sequence point, e
quindi non sarà mai valutata necessariamente, per prima l’espressione posta alla sua sinistra
rispetto all’altra espressione posta alla sua destra.
Comunque, quanto detto, non deve spaventare: è una diretta conseguenza della continua
ricerca di efficienza e ottimizzazione di C, ed è altresì facilmente evitabile. È sufficiente
non scrivere un’espressione complessa nella quale, contemporaneamente, si accede e si
modifica il valore di una stessa variabile (Snippet 4.17).

Snippet 4.17 Eliminazione di ambiguità di valutazione dell’operatore ++.


...
// qui ix sarà 5 senza alcuna ambiguità poiché il suo incremento con l'operatore ++
// lo effettuiamo in un sequence point successivo
vector[ix] = 2 + ix;
ix++;
...
Operatori relazionali
Gli operatori relazionali consentono di determinare delle relazioni d’ordine tra due
operandi, ovvero di comparare i rispettivi valori al fine di stabilire se uno di essi è, per
esempio, maggiore di un altro oppure minore.
Questi operatori, con simboli parentesi angolare chiusa > (maggiore di), parentesi
angolare chiusa e uguale >= (maggiore di o uguale a), parentesi angolare aperta < (minore
di) e parentesi angolare aperta e uguale <= (minore di o uguale a), associano da sinistra a
destra e gli operandi possono essere sia lvalue sia rvalue.
Inoltre, la valutazione di un’espressione relazionale può produrre solo due valori di tipo
int: 0, per indicare che una relazione è falsa e 1 per indicare che una relazione è vera.

NOTA
Ricordiamo che in C qualsiasi valore diverso da 0 è considerato vero. Pertanto valori come
10 oppure -3 sono entrambi valutati come veri. Quanto detto riguarda solo il fatto che
un’espressione relazionale, quando valutata, ritorna 0 per indicare un risultato falso e 1 per
indicare un risultato vero.

ATTENZIONE
Un’espressione relazionale, come per esempio a < b < c, non verifica se b è tra a e c ma per
effetto dell’associatività da sinistra a destra è valutata come segue: (a < b) < c, ossia: il
valore ritornato dalla comparazione di a < b, 0 o 1, è poi comparato con il valore di c e viene
ritornato 0 o 1. Come vedremo, per avere quell’esito, dovremo far uso dell’operatore AND
logico e scrivere qualcosa come a < b && b < c.

Operatore maggiore di
L’operatore maggiore di con simbolo > esegue un confronto tra le espressioni suoi
operandi e determina se il valore dell’espressione posta alla sua sinistra è maggiore del
valore dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.18 Operatore maggiore di.


int expr1 = 10;
int expr2 = 20;

int res = expr1 > expr2; // 0 ossia falso

Operatore maggiore di o uguale a


L’operatore maggiore di o uguale a con simbolo >= esegue un confronto tra le espressioni
suoi operandi e determina se il valore dell’espressione posta alla sua sinistra è maggiore
oppure uguale al valore dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.19 Operatore maggiore di o uguale a.


int expr1 = 10;
int expr2 = 10;

int res = expr1 >= expr2; // 1 ossia vero

Operatore minore di
L’operatore minore di con simbolo < esegue un confronto tra le espressioni suoi operandi
e determina se il valore dell’espressione posta alla sua sinistra è minore del valore
dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.20 Operatore minore di.


int expr1 = 'C';
int expr2 = 'D';

int res = expr1 < expr2; // 1 ossia vero

Operatore minore di o uguale a


L’operatore minore di o uguale a con simbolo <= esegue un confronto tra le espressioni
suoi operandi e determina se il valore dell’espressione posta alla sua sinistra è minore
oppure uguale al valore dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.21 Operatore minore di o uguale a.


int expr1 = 0xFD;
int expr2 = 0xFD;

int res = expr1 <= expr2; // 1 ossia vero

Vediamo, infine, un esempio che fa uso degli operatori relazionali (Listato 4.1) in cui,
data una matrice di valori, si cerca di determinare se vi sono dei valori che sono minori di
altri valori, passati come criteri di ricerca, e per ogni valore che soddisfa tale condizione si
incrementa di un’unità una variabile che tiene traccia della quantità trovata.

Listato 4.1 RelationalOperators.c (RelationalOperators).


/* RelationalOperators.c :: Uso degli operatori relazionali :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 3
#define NR_ROWS 3
#define NR_COLS 3

int main(void)
{
// matrice per la ricerca
int values[NR_ROWS][NR_COLS] =
{
{10, 20, 30},
{-22, -11, -18},
{105, 205, -963}
};

int filter_values[SIZE] = {33, 13, 56}; // valori da confrontare

int how_many = 0; // tiene traccia delle occorrenze trovate

// ciclo per la ricerca


for (int k = 0; k < SIZE; k++)
{
for (int i = 0; i < NR_ROWS; i++)
{
for (int j = 0; j < NR_COLS; j++)
{
int value1 = values[i][j];
int value2 = filter_values[k];

printf("Il valore %4d e' minore del valore %3d ?", value1, value2);

if (value1 < value2)


{
how_many++; // incrementiamo di 1 la variabile
printf(" VERO\n");
}
else
printf(" FALSO\n");
}
}
}
printf("Numero valori trovati: %d\n", how_many);

return (EXIT_SUCCESS);
}

Output 4.1 Dal Listato 4.1 RelationalOperators.c.


Il valore 10 e' minore del valore 33 ? VERO
Il valore 20 e' minore del valore 33 ? VERO
Il valore 30 e' minore del valore 33 ? VERO
Il valore -22 e' minore del valore 33 ? VERO
Il valore -11 e' minore del valore 33 ? VERO
Il valore -18 e' minore del valore 33 ? VERO
Il valore 105 e' minore del valore 33 ? FALSO
Il valore 205 e' minore del valore 33 ? FALSO
Il valore -963 e' minore del valore 33 ? VERO
Il valore 10 e' minore del valore 13 ? VERO
Il valore 20 e' minore del valore 13 ? FALSO
Il valore 30 e' minore del valore 13 ? FALSO
Il valore -22 e' minore del valore 13 ? VERO
Il valore -11 e' minore del valore 13 ? VERO
Il valore -18 e' minore del valore 13 ? VERO
Il valore 105 e' minore del valore 13 ? FALSO
Il valore 205 e' minore del valore 13 ? FALSO
Il valore -963 e' minore del valore 13 ? VERO
Il valore 10 e' minore del valore 56 ? VERO
Il valore 20 e' minore del valore 56 ? VERO
Il valore 30 e' minore del valore 56 ? VERO
Il valore -22 e' minore del valore 56 ? VERO
Il valore -11 e' minore del valore 56 ? VERO
Il valore -18 e' minore del valore 56 ? VERO
Il valore 105 e' minore del valore 56 ? FALSO
Il valore 205 e' minore del valore 56 ? FALSO
Il valore -963 e' minore del valore 56 ? VERO
Numero valori trovati: 19
Operatori di uguaglianza
Gli operatori di uguaglianza consentono di determinare delle eguaglianze oppure delle
diseguaglianze tra due operandi, cioè di comparare i rispettivi valori al fine di stabilire se
uno di essi è, per esempio, uguale oppure diverso rispetto all’altro.
Tali operatori, con simboli doppio uguale == (uguale a), punto esclamativo e uguale !=
(non uguale a), associano da sinistra a destra e gli operandi possono essere sia lvalue sia
rvalue.
Infine, come gli operatori relazionali, anche gli operatori di uguaglianza ritornano il
valore intero 0 se l’eguaglianza o la diseguaglianza è falsa oppure il valore intero 1 se
l’eguaglianza o la diseguaglianza è vera.

OPERATORE DI ASSEGNAMENTO E OPERATORE DI UGUAGLIANZA


Spesso si tende a confondere l’operatore di assegnamento (simbolo =) con l’operatore di
uguaglianza (simbolo ==). Occorre prestare sempre attenzione alla loro differente semantica:
assegnare significa inserire un valore di una variabile, letterale o costante (rvalue) situata a
destra dell’operatore in una variabile (lvalue), mentre uguagliare significa confrontare se due
variabili contengono lo stesso valore. Per discriminarli ricordiamo semplicemente che quando
vogliamo assegnare un valore dobbiamo utilizzare il simbolo = una sola volta, mentre quando
vogliamo confrontare l’uguaglianza tra due valori dobbiamo utilizzare il simbolo = due volte di
seguito.

Operatore uguale a
L’operatore uguale a con simbolo == esegue un confronto tra le espressioni suoi operandi
e determina se il valore dell’espressione posta alla sua sinistra è uguale al valore
dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.22 Operatore uguale a.


int a = 120, b = 111, c = 111, d = 112;
int e = a < b == c > d; // 1

Nello Snippet 4.22 l’espressione sarà valutata come vera (1) perché verranno eseguiti: a <

b che darà 0 (è falso che 120 è minore di 111); c > d che darà 0 (è falso che 111 è maggiore di
112); 0 == 0 (è vero che 0 è uguale a 0).
L’espressione a < b == c > d è stata valutata nell’ordine degli operatori sopra descritti
perché gli operatori relazionali hanno una precedenza maggiore rispetto agli operatori di
eguaglianza. A volte, comunque, per ragioni di chiarezza, l’espressione descritta è
codificabile nel seguente modo del tutto equivalente (a < b) == (c > d).
Operatore non uguale a
L’operatore non uguale a con simbolo != esegue un confronto tra le espressioni suoi
operandi e determina se il valore dell’espressione posta alla sua sinistra è non uguale o
diversa rispetto al valore dell’espressione posta alla sua destra.
In caso affermativo ritorna il valore 1, in caso contrario ritorna il valore 0.

Snippet 4.23 Operatore non uguale a.


int a = 120, b = 111, c = 111, d = 112;
int e = a < b != c > d; // 0
Operatori logici
Gli operatori logici permettono di costruire espressioni complesse a partire da quelle più
semplici e di valutarle nella loro interezza applicando delle regole di logica booleana.
Questi operatori, con simboli doppia e commerciale && (AND logico), doppia barra
verticale || (OR logico) e punto esclamativo ! (NOT logico) associano da sinistra a destra
(tranne il NOT logico che associa da destra a sinistra), e gli operandi possono essere sia
lvalue sia rvalue. Il NOT logico è un operatore unario, mentre gli altri sono operatori binari.
NOTA
Gli operatori logici sono stati introdotti nel 1854 dal matematico e logico britannico George
Boole nell’ambito della formalizzazione di un nuovo tipo di algebra chiamata algebra
booleana, in cui i calcoli possono essere effettuati con l’utilizzo di due soli valori: vero e
falso.

La valutazione di un’espressione nel suo complesso avverrà considerando le tabelle di


verità che seguono, considerando che gli operatori logici producono il valore intero 0 per
indicare il valore falso e il valore intero 1 per indicare il valore vero.
In più i valori dei suoi operandi possono essere diversi da 0, come 10, 2, -3 e così via, e
rappresentano il valore vero, oppure 0 e rappresentano il valore falso.
Per l’operatore AND logico (&&) l’espressione complessa sarà valutata vera (valore 1) solo
se entrambe le espressioni semplici che la costituiscono saranno valutate vere.
Se la prima espressione semplice risulta subito falsa, la valutazione dell’altra espressione
non verrà effettuata e l’intera espressione complessa sarà falsa (valore 0).
Tabella 4.2 Operatore AND logico (&&).
Espressione 1 Espressione 2 Espressione1 && Espressione 2
falsa (0) falsa (0) falsa (0)
falsa (0) vera (non 0) falsa (0)
vera (non 0) falsa (0) falsa (0)
vera (non 0) vera (non 0) vera (1)

Per l’operatore OR logico (||) l’espressione complessa sarà valutata vera (valore 1) se
una delle espressioni semplici che la costituiscono sarà vera o se entrambe le espressioni
semplici saranno vere. Se la prima espressione semplice risulterà falsa, la valutazione
dell’altra espressione verrà effettuata per verificare se sarà vera, e allora l’espressione
complessa sarà vera (valore 1) altrimenti sarà falsa (valore 0).
Tabella 4.3 Operatore OR logico (||).
Espressione 1 Espressione 2 Espressione 1 || Espressione 2
falsa (0) falsa (0) falsa (0)
falsa (0) vera (non 0) vera (1)
vera (non 0) falsa (0) vera (1)
vera (non 0) vera (non 0) vera (1)

Per l’operatore NOT logico (!) la valutazione dell’espressione avverrà rovesciando il


valore dell’operando su cui l’operatore medesimo agisce; infatti, l’espressione sarà valutata
come vera se sarà scritta come NON falsa e sarà valutata come falsa se sarà scritta come
NON vera.
Tabella 4.4 Operatore NOT logico (!).
Espressione 1 !Espressione 1
falsa (0) vera (1)
vera (non 0) falsa (0)

Una proprietà importante degli operatori AND logico e OR logico riguarda il fatto che,
nel loro caso, l’ordine di valutazione degli operandi è garantito, ossia viene sempre valutato
prima l’operando sinistro e poi l’operando destro.
Ciò implica che questi operatori marcano dei sequence point e pertanto tutte le operazioni
definite dall’operando di sinistra, anche quelle che producono side effect, sono valutate e
completate prima che il compilatore passi a valutare e a completare le operazioni
dell’operando di destra.

Snippet 4.24 Operatori logici e sequence point.


int x = 10;

// l'espressione dell'operando di sinistra viene valutata completamente


// prima della valutazione dell'espressione dell'operando di destra
int res = x++ < 11 && x + 1 == 12; // 1

Lo Snippet 4.24 è lecito perché, per effetto della garanzia che l’operando di sinistra è
sempre valutato prima dell’operando di destra, il side effect sulla variabile x a opera
dell’istruzione x++ ha luogo prima che la stessa variabile x subisca l’addizione del valore 1 a
opera dell’istruzione x + 1.

Ciò avviene, ribadiamo, perché l’operatore AND logico && marca un sequence point, e ciò
garantisce che la variabile x sia incrementata a opera dell’operatore di incremento postfisso
++ prima che l’espressione dell’operatore destro sia valutata e computata.
Infine, la circostanza che la valutazione degli operandi degli operatori && e || avvenga da
sinistra a destra produce un’ulteriore conseguenza che in letteratura è definita come
valutazione di cortocircuito.
Il modo di operare degli operatori && e || è una valutazione di cortocircuito proprio
perché essi interrompono le altre eventuali valutazioni se sono subito soddisfatte le seguenti
condizioni che consentono immediatamente di rilevare il risultato di tutta l’espressione
complessa: per l’operatore && se l’espressione alla sua sinistra è falsa, allora tutta
l’espressione complessa sarà subito falsa (l’espressione alla sua destra non verrà valutata);
per l’operatore || se l’espressione alla sua sinistra è vera, allora tutta l’espressione
complessa sarà subito vera (l’espressione alla sua destra non verrà valutata).

Operatore AND logico


L’operatore AND logico con simbolo && esegue una comparazione tra i valori dei suoi
operandi e ritorna il valore 1 se entrambi sono non uguali a 0, altrimenti ritorna il valore 0.

Snippet 4.25 Operatore AND logico.


int a = 10, b = 14;
int c = a > 10 && b < 15; // AND logico espressione falsa

Operatore OR logico
L’operatore OR logico con simbolo || esegue una comparazione tra i valori dei suoi
operandi e ritorna il valore 1 se uno di essi è non uguale a 0, altrimenti ritorna il valore 0.

Snippet 4.26 Operatore OR logico.


int a = 10, b = 14;
int c = a > 10 || b < 15; // OR logico espressione vera

Operatore NOT logico o di negazione logica


L’operatore NOT logico con simbolo ! ritorna il valore 0 se il valore del suo operando è
non uguale a 0, altrimenti ritorna il valore 1 se il valore del suo operando è uguale a 0.
In pratica, data un’espressione come !E, possiamo asserire che il suo valore restituito è
equivalente al risultato della valutazione dell’espressione 0 == E.

Snippet 4.27 Operatore NOT logico.


int a = 10, b = 14;
int c = (!(a > 10)); // NOT logico espressione vera
Operatore condizionale
L’operatore condizionale permette di eseguire in modo abbreviato un’istruzione del tipo
if-then-else e agisce su tre operandi (è infatti altresì conosciuto come operatore ternario).
Il suo simbolo è costituito dal punto interrogativo (?) e dai due punti (:) che vengono
posti in un particolare ordine per dare senso all’espressione complessiva. Associa da destra
a sinistra e i suoi operandi possono essere lvalue e rvalue.

Sintassi 4.1 Operatore condizionale.


expression_1 ? expression_2 : expression_3

La Sintassi 4.1 si legge nel seguente modo: “Se expression_1 è vera (non uguale a 0), allora
(rappresentato dal simbolo ?) valuta expression_2, altrimenti (rappresentato dal simbolo :)
valuta expression_3”.
In definitiva il secondo operando è valutato solo se il primo operando è diverso da 0
mentre il terzo operando è valutato solo se il primo operando è uguale a 0.
In risultato dell’espressione condizionale sarà dunque il valore del secondo o del terzo
operando a seconda di quello che verrà valutato.
NOTA
Ricordiamo che l’operatore condizionale introduce un sequence point e pertanto il suo
operando di sinistra (il primo operando) sarà completamente valutato prima della
valutazione del secondo oppure del terzo operando.

L’esempio seguente (Listato 4.2) mostra un utilizzo dell’operatore condizionale con cui
verifichiamo se, dato un valore estratto dalla matrice values che sia pari, lo stesso sia
maggiore del valore 33 (filter_value); se tale verifica è positiva, lo memorizziamo nell’array
found_values nella stessa posizione dove si trovava nell’ambito della sua matrice originaria;
altrimenti, in quella posizione, memorizziamo il valore 0.
Infine, per la verifica del risultato mostriamo a video il contenuto del predetto array.

Listato 4.2 ConditionalOperator.c (ConditionalOperator).


/* ConditionalOperator.c :: Uso dell'operatore condizionale :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 9
#define NR_ROWS 3
#define NR_COLS 3

int main(void)
{
// matrice per la ricerca
int values[NR_ROWS][NR_COLS] =
{
{10, 100, 30},
{-22, -11, 66},
{105, 204, 333}
};

int filter_value = 33; // valori da confrontare


int found_values[SIZE] = {0}; // numero massimo di elementi da inserire

// ciclo per la ricerca


for (int i = 0; i < NR_ROWS; i++)
{
for (int j = 0; j < NR_COLS; j++)
{
int value = values[i][j];
// posiziono il valore trovato nell'array spostandomi
// nella posizione corretta
if (value % 2 == 0)
found_values[i * NR_COLS + j] = value > filter_value ? value : 0;
}
}
// valori trovati
for (int i = 0; i < SIZE; i++)
printf("Indice %d ---> [ %3d ]\n", i, found_values[i]);

return (EXIT_SUCCESS);
}

Output 4.2 Dal Listato 4.2 ConditionalOperator.c.


Indice 0 ---> [ 0 ]
Indice 1 ---> [ 100 ]
Indice 2 ---> [ 0 ]
Indice 3 ---> [ 0 ]
Indice 4 ---> [ 0 ]
Indice 5 ---> [ 66 ]
Indice 6 ---> [ 0 ]
Indice 7 ---> [ 204 ]
Indice 8 ---> [ 0 ]
Operatori bit per bit
Gli operatori bit per bit (detti anche bit a bit, a livello di bit o bitwise) consentono di
effettuare delle manipolazioni a basso livello sui singoli bit dei propri operandi che devono
essere di tipo intero con o senza segno (char, short, int, unsigned long e così via).
La Tabella 4.5 ne dà una panoramica generale unitamente ai relativi simboli e alla
corretta nomenclatura adottata dallo standard del linguaggio C.
Tabella 4.5 Classificazione degli operatori bit per bit.
Simbolo Denominazione
~ Operatore di complemento a uno (ones’ complement)
& Operatore AND bit per bit (bitwise AND)
| Operatore OR bit per bit inclusivo (bitwise inclusive OR)
^ Operatore OR bit per bit esclusivo (bitwise exclusive OR)
<< Scorrimento a sinistra bit per bit (bitwise left shift)
>> Scorrimento a destra bit per bit (bitwise right shift)

CONSIGLIO
Per comprendere in modo più compiuto il funzionamento degli operatori bit per bit si
raccomanda di leggere preliminarmente l’Appendice C.

La Tabella 4.6, invece, indica come tali operatori, eccetto quelli di scorrimento, agiscono
sui bit: ogni riga evidenzia infatti la valutazione di un’espressione tra A e B, contenenti la
cifra 0 o 1, rispetto all’operatore utilizzato.
Tabella 4.6 Valutazione di espressioni con gli operatori bit per bit (eccetto gli operatori di
scorrimento).
A B ~A A&B A|B A^B
0 0 1 0 0 0
1 0 0 0 1 1
0 1 1 0 1 1
1 1 0 1 1 0

Nella tabella presentata notiamo come:


l’operatore di complemento a uno (~) inverte tutti i bit dell’operando sui cui agisce: se
c’è un bit con 1 allora lo stesso diventerà 0 e viceversa;
l’operatore AND (&) confronta i bit dei due operandi: se entrambi sono 1 allora il
risultato sarà 1. Tale operatore è utile per creare maschere di bit che cancellano i bit di
un altro operando ponendoli a 0;
l’operatore OR inclusivo (|) confronta i bit dei due operandi: il risultato sarà sempre 1
tranne se entrambi sono 0. Tale operatore è utile per creare maschere di bit che
impostano i bit di un altro operando a 1;
l’operatore OR esclusivo (^) confronta i bit dei due operandi: il risultato sarà 1 solo se
uno sarà 1 e l’altro sarà 0.

È inoltre importante precisare che:


per l’operatore di complemento a uno si avrà sempre una promozione integrale del suo
operando e il risultato sarà del tipo promosso (così, se abbiamo un operando di tipo
char, lo stesso verrà convertito nel tipo int prima dell’applicazione del complemento a

uno);
per gli operatori AND, OR inclusivo e OR esclusivo, sui relativi operandi, saranno
applicate le regole viste per le conversioni aritmetiche abituali (per esempio, se
abbiamo un operando di tipo int e un operando di tipo long, allora tutta l’espressione
produrrà un risultato di tipo long);
per gli operatori di scorrimento si avrà sempre una promozione integrale dei propri
operandi e il tipo del risultato sarà quello dell’operando di sinistra promosso. Se
l’operando di destra è negativo oppure è più grande o uguale dell’ampiezza
dell’operando di sinistra, il risultato sarà non definito.

Operatore di complemento a uno


L’operatore di complemento a uno con simbolo tilde (~) consente di produrre l’inverso o
la negazione del suo operando: cambia ogni 1 con lo 0 e ogni 0 con 1.
È un operatore unario, associa da destra a sinistra, il suo operando può essere un lvalue o
un rvalue e la sua esecuzione non altera direttamente il valore del suo operando.

Snippet 4.28 Operatore di complemento a uno.


// 0000 1010
unsigned char number = 10;

// 1111 0101
unsigned char result = (unsigned char) ~number; // 245

// 0000 1010 ---> 10


// --------- ~
// 1111 0101 ---> 245

Operatore AND bit per bit


L’operatore AND bit per bit con simbolo “e” commerciale (&) consente di produrre un
risultato che è l’applicazione di una funzione AND a livello di bit ai suoi operandi: ogni bit
del risultato è 1 solo se i corrispondenti bit degli operandi sono 1.
È un operatore binario, associa da sinistra a destra, i suoi operandi possono essere lvalue
o rvalue e la sua esecuzione non altera direttamente i valori dei suoi operandi.

Snippet 4.29 Operatore AND bit per bit.


// 0001 1110
unsigned char number_1 = 30;

// 0001 0100
unsigned char number_2 = 20;

// 0001 0100
unsigned char result = (unsigned char) number_1 & number_2; // 20

// 0001 1110 ---> 30


// 0001 0100 ---> 20
// --------- &
// 0001 0100 ---> 20

Operatore OR bit per bit inclusivo


L’operatore OR bit per bit inclusivo con simbolo barra verticale (|) consente di produrre
un risultato che è l’applicazione di una funzione OR a livello di bit inclusivo ai suoi
operandi: ogni bit del risultato è 1 se almeno uno dei corrispondenti bit degli operandi è 1.
È un operatore binario, associa da sinistra a destra, i suoi operandi possono essere lvalue
o rvalue e la sua esecuzione non altera direttamente i valori dei suoi operandi.

Snippet 4.30 Operatore OR bit per bit inclusivo.


// 0001 1110
unsigned char number_1 = 30;

// 0001 0100
unsigned char number_2 = 20;

// 0001 1110
unsigned char result = (unsigned char) number_1 | number_2; // 30

// 0001 1110 ---> 30


// 0001 0100 ---> 20
// --------- |
// 0001 1110 ---> 30

Operatore OR bit per bit esclusivo


L’operatore OR bit per bit esclusivo, denominato anche XOR, con simbolo accento
circonflesso (^), consente di produrre un risultato che è l’applicazione di una funzione OR a
livello di bit esclusivo ai suoi operandi: ogni bit del risultato è 1 se e solo se uno dei
corrispondenti bit degli operandi è 1.
È un operatore binario, associa da sinistra a destra, i suoi operandi possono essere lvalue
o rvalue e la sua esecuzione non altera direttamente i valori dei suoi operandi.
Snippet 4.31 Operatore OR bit per bit esclusivo.
// 0001 1110
unsigned char number_1 = 30;

// 0001 0100
unsigned char number_2 = 20;

// 0001 1110
unsigned char result = (unsigned char) number_1 ^ number_2; // 10

// 0001 1110 ---> 30


// 0001 0100 ---> 20
// --------- ^
// 0000 1010 ---> 10

Operatore di scorrimento a sinistra bit per bit


L’operatore di scorrimento a sinistra bit per bit con simbolo doppia parentesi angolare
aperta (<<) consente di traslare o spostare a sinistra dell’operando di sinistra tanti bit quanti
sono indicati dall’operando di destra.
Ciò significa, che data l’espressione E1 << E2, i bit di E1 saranno spostati a sinistra di E2
posizioni di bit, i bit entranti a destra saranno riempiti di 0 e quelli di sinistra spostati che
supereranno i bit massimi del tipo saranno perduti. Se, tuttavia, E1 è con segno e negativo il
risultato sarà non definito, così come se E1 è con segno e positivo se lo spostamento dei bit
pone un bit con valore 1 sul bit più significativo il risultato sarà non definito (E1 non diverrà
necessariamente negativo come in altri linguaggi di programmazione tipo Java).
Di fatto l’operatore di spostamento a sinistra produce un risultato che è dato dalla
moltiplicazione del valore dell’operando di sinistra E1 per 2E2 bit.
È altresì un operatore binario, associa da sinistra a destra, i suoi operandi possono essere
lvalue o rvalue e la sua esecuzione non altera direttamente i valori dei suoi operandi.

Snippet 4.32 Operatore di scorrimento a sinistra bit per bit.


// 0100 0000
unsigned char number = 64;

// 0000 0001
unsigned char positions = 1;

// 1000 0000
unsigned char result = (unsigned char) (number << positions); // 128 o 64 * 21

// 0100 0000 ---> 64


// 0000 0001 ---> 1
// --------- <<
// 1000 0000 ---> 128

Operatore di scorrimento a destra bit per bit


L’operatore di scorrimento a destra bit per bit con simbolo doppia parentesi angolare
chiusa (>>) consente di traslare o spostare a destra dell’operando di sinistra tanti bit quanti
sono indicati dall’operando di destra.
Ciò significa, che data l’espressione E1 >> E2, i bit di E1 saranno spostati a destra, di E2
posizioni di bit, i bit entranti a sinistra, se E1 è senza segno oppure con segno ma non
negativo, saranno riempiti di 0 altrimenti se E1 è con segno e negativo il comportamento sarà
dipendente dall’implementazione (alcuni compilatori potranno riempirli di 1 come è il
valore del bit di segno, mentre altri potranno riempirli di 0). I bit di destra uscenti saranno
invece perduti.
Di fatto l’operatore di spostamento a destra produce un risultato che è la parte intera della
divisione del valore dell’operando di sinistra E1 per 2E2 bit.
È altresì un operatore binario, associa da sinistra a destra, i suoi operandi possono essere
lvalue o rvalue e la sua esecuzione non altera direttamente i valori dei suoi operandi.
TERMINOLOGIA
Quando E1 è negativo, si parla a volte di shift aritmetico se i compilatori riempiono i bit
entranti a sinistra con 1, altrimenti si parla di shift logico se riempiono i bit entranti a sinistra
con 0.

Snippet 4.33 Operatore di scorrimento a destra bit per bit.


// 0100 0000
unsigned char number = 64;

// 0000 0010
unsigned char positions = 2;

// 0001 0000
unsigned char result = (unsigned char) (number >> positions); // 16 o 64 / 22

// 0100 0000 ---> 64


// 0000 0010 ---> 2
// --------- >>
// 0001 0000 ---> 16

Comuni casi di utilizzo


I programmatori neofiti spesso si fanno la seguente domanda: “Bene, ho a disposizione
questi potenti operatori che consentono un accesso a basso livello nei bit di una locazione di
memoria, ma ora in che modo pratico posso utilizzarli?”
Diamo una risposta a questo quesito interessante mostrando alcuni esempi che trattano
pattern di impiego degli operatori bit per bit molto comuni considerando valori di tipo
unsigned short a 16 bit, di tipo unsigned int a 32 bit e di tipo unsigned char a 8 bit.

Per tutti, il bit meno significativo si trova alla posizione 0 e il bit più significativo si trova
alla posizione 15 (per lo short), 31 (per l’int) o 7 (per il char).
Impostazione di singoli bit.
Problema: dato un valore vogliamo scegliere uno o più bit di esso da impostare con il
valore 1.
Soluzione: utilizzare l’operatore OR inclusivo con il suo operando sinistro che
rappresenta il valore da manipolare e il suo operando destro che rappresenta una maschera
con i bit da “accendere” nel valore impostati a 1.
Spiegazione: i bit di una maschera impostati a 1 combinati in OR inclusivo con i
corrispondenti bit di un valore, impostati a 0, li impostano a 1. I bit di una maschera
impostati a 0 lasciano, invece, invariati i corrispondenti bit di un valore.

Snippet 4.34 Impostazione di singoli bit.


// valore decimale: 100
// valore binario: 0000 0000 0110 0100
// valore esadecimale: 64
unsigned short value = 0x0064;

// valore decimale: 3840


// valore binario: 0000 1111 0000 0000
// valore esadecimale: F00
const unsigned short MASK = 0x0F00;

// valore decimale: 3940


// valore binario: 0000 1111 0110 0100
// valore esadecimale: F64
value = value | MASK; // imposto i bit 8, 9, 10 e 11 a 1

Cancellazione di singoli bit.


Problema: dato un valore vogliamo scegliere uno o più bit di esso da cancellare ossia da
impostare con il valore 0.
Soluzione: utilizzare l’operatore AND con il suo operando sinistro che rappresenta il
valore da manipolare e il suo operando destro con anche l’operatore di complemento a uno
che rappresenta una maschera con i bit da “spegnere” nel valore impostati a 1.
Spiegazione: i bit di una maschera impostati a 1 sono prima invertiti in 0 con l’operatore
di complemento a uno che inverte anche i suoi bit da 0 a 1. Dopo, questa maschera
trasformata è combinata con l’operatore AND in modo che solo i bit impostati a 1 di
entrambi siano mantenuti. I bit di una maschera impostati a 0 spengono, invece, i bit
corrispondenti di un valore.

Snippet 4.35 Cancellazione di singoli bit.


// valore decimale: 100
// valore binario: 0000 0000 0110 0100
// valore esadecimale: 64
unsigned short value = 0x0064;

// valore decimale: 96
// valore binario: 0000 0000 0110 0000
// valore esadecimale: 60
const unsigned short MASK = 0x0060;

// valore decimale: 4
// valore binario: 0000 0000 0000 0100
// valore esadecimale: 4
value = value & ~MASK; // cancello i bit 5 e 6

Verifica di un bit.
Problema: dato un valore vogliamo sapere se uno o più bit di esso è impostato con il
valore 1.
Soluzione: utilizzare l’operatore AND con il suo operando sinistro che rappresenta il
valore da manipolare e il suo operando destro che rappresenta una maschera con i bit da
verificare nel valore impostati a 1.
Spiegazione: i bit di una maschera impostati a 1 combinati in AND con i corrispondenti
bit di un valore, impostati a 1, li preservano. I bit di una maschera impostati a 0 spengono,
invece, i corrispondenti bit di un valore.

Snippet 4.36 Verifica di un bit.


// valore decimale: 100
// valore binario: 0000 0000 0110 0100
// valore esadecimale: 64
unsigned short value = 0x0064;

// valore decimale: 4
// valore binario: 0000 0000 0000 0100
// valore esadecimale: 4
const unsigned short MASK = 0x0004;

// operatore di uguaglianza non necessario; scritto solo per


// dare maggiore risalto al test
_Bool is_bit_2_setted = (value & MASK) == MASK; // 1 - VERO
_Bool is_bit_3_setted = (value & MASK << 1) == MASK << 1; // 0 - FALSO

Commutazione di singoli bit.


Problema: dato un valore vogliamo commutare uno o più bit di esso, ossia quelli con
valore 1 devono diventare 0 e quelli con valore 0 devono diventare 1.
Soluzione: utilizzare l’operatore OR esclusivo con il suo operando sinistro che
rappresenta il valore da manipolare e il suo operando destro che rappresenta una maschera
con i bit da commutare impostati con 1.
Spiegazione: i bit di una maschera impostati a 1 combinati in OR esclusivo con i
corrispondenti bit di un valore, impostati a 1, li commutano a 0. I bit di una maschera
impostati a 1 commutano, invece, i corrispondenti bit di un valore impostati a 0 in 1.

Snippet 4.37 Commutazione di singoli bit.


// valore decimale: 100
// valore binario: 0000 0000 0110 0100
// valore esadecimale: 64
unsigned short value = 0x0064;

// valore decimale: 240


// valore binario: 0000 0000 1111 0000
// valore esadecimale: F0
const unsigned short MASK = 0x00F0;

// valore decimale: 148


// valore binario: 0000 0000 1001 0100
// valore esadecimale: 94
value = value ^ MASK; // commuto i bit 4, 5, 6 e 7

Estrazione di singoli bit.


Problema: dato un valore vogliamo estrarre una determinata quantità di bit.
Soluzione: utilizzare l’operatore di scorrimento a destra con il suo operando sinistro che
rappresenta il valore da manipolare e il suo operando destro che rappresenta la quantità di
bit da traslare per poi confrontarli, mediante l’operatore AND, con una maschera con i bit
impostati a 1.
Spiegazione: la quantità di bit spostati mediante l’operatore di scorrimento a destra è
confrontata tramite un AND con una maschera con la stessa quantità di bit i cui bit sono
impostati a 1. I bit del valore con 1 rimangono a 1, mentre gli altri rimangono a 0.

Snippet 4.38 Estrazione di singoli bit.


// valore decimale: 11393254
// valore binario: 0000 0000 1010 1101 1101 1000 1110 0110
// valore esadecimale: ADD8E6
unsigned int color = 0xADD8E6; // Light blue RGB

// valore decimale: 255


// valore binario: 1111 1111
// valore esadecimale: FF
const unsigned char MASK = 0xFF;

// valore decimale: 230


// valore binario: 1110 0110
// valore esadecimale: E6
unsigned char BLUE = (unsigned char) color & MASK; // E6

// valore decimale: 216


// valore binario: 1101 1000
// valore esadecimale: D8
unsigned char GREEN = (unsigned char) (color >> 8) & MASK; // D8

// valore decimale: 173


// valore binario: 1010 1101
// valore esadecimale: AD
unsigned char RED = (unsigned char) (color >> 16) & MASK; // AD
Operatore virgola
L’operatore virgola (,) permette di combinare due espressioni valutandole da sinistra a
destra (marca dei sequence point, e dunque garantisce che tutti i side effect abbiamo luogo),
dove l’operando di sinistra è valutato e il correlativo valore scartato, poi l’operando di
destra è valutato e il correlativo valore diviene il valore di risultato di tutta l’espressione.
Associa da sinistra a destra e i suoi operandi possono essere lvalue e rvalue. Solitamente,
come vedremo tra breve, è utilizzato nell’ambito di un ciclo for per inizializzare e
aggiornare due o più variabili.
Tuttavia, quando il simbolo virgola è impiegato nella fase di dichiarazione di più variabili
oppure quando si passano più argomenti a una funzione, allora non è considerato come un
operatore ma piuttosto come un semplice separatore, e non vi alcuna garanzia di valutazione
da sinistra a destra.

Snippet 4.39 Operatore virgola.


int j = 0;
int a = 10, b = 11, c = 12; // separatore

// operatore
j = (a++, b = a - 5, c = c + b); // 18

// separatore
printf("%d %d %d %d\n", j, a, b, c); // 18 11 6 18

Lo Snippet 4.39 evidenzia l’utilizzo del simbolo virgola sia come semplice separatore (è
il caso della dichiarazione delle variabili a, b e c e dell’invocazione della funzione printf con
una serie di argomenti) sia come operatore (è il caso dell’inizializzazione della variabile j).
In quest’ultimo caso j conterrà il valore 18 perché la valutazione delle sotto-espressioni
delle espressioni con l’operatore virgola a destra dell’operatore di assegnamento procederà
da sinistra a destra e terrà anche conto dei relativi side effect.
A causa anche dell’associatività da sinistra a destra dell’operatore virgola la valutazione
delle espressioni contenenti l’operatore virgola avverrà nel seguente modo (è come se
l’espressione complessa fosse scritta come: ((a++, b = a - 5), c = c + b):

1. la prima espressione eseguita sarà a++, b = a - 5, dove a++ darà il valore 11 e b = a - 5

darà il valore 6, che sarà il valore di tutta l’espressione mentre il valore 11 sarà scartato;
2. il valore 6 diverrà l’operando sinistro dell’altra espressione, laddove la sotto-
espressione c = c + b sarà valutata e darà come risultato 18. Di questi il valore 6 sarà
scartato mentre il valore 18 diverrà il valore di tutta l’espressione.
Operatori di assegnamento composti
Gli operatori di assegnamento composti consentono di assegnare un valore a una
variabile che è uguale al valore della variabile medesima aggiornato con il valore di
un’altra espressione appositamente fornita.
La Sintassi 4.2 ne mostra una forma generale in cui op deve essere sostituito con uno
degli operatori della Tabella 4.8, E1 è l’operando di sinistra che deve essere un lvalue
modificabile ed E2 è l’operando di destra che può essere una qualsiasi espressione che
produce un lvalue oppure in rvalue.
Gli operatori di assegnamento composti associano da destra a sinistra.

Sintassi 4.2 Operatore di assegnamento composto.


E1 op= E2

Dalla Tabella 4.7 si evince come tutti gli operatori di assegnamento composti sono
formati da un simbolo che è uguale a un operatore come +, -, % e altri, cui segue, senza spazi,
il simbolo dell’operatore di assegnamento semplice =.
ATTENZIONE
Quando si utilizza un operatore di assegnamento composto, ricordarsi di indicare come
primo simbolo sempre l’operatore che indica il tipo di operazione da compiere (+, -, >> e altri)
e poi come ulteriore simbolo l’operatore di assegnamento =. Invertire gli operatori, tipo =- al
posto di -=, per quanto non costituisca un errore di sintassi, cambia la semantica
dell’espressione. Infatti, scrivere a =- 10; assegnerà ad a il valore -10 e non diminuirà il
precedente valore di a del valore 10.

Tabella 4.7 Classificazione degli operatori di assegnamento composti.


Simbolo Significato
+= Addizione e assegnamento
-= Sottrazione e assegnamento
*= Moltiplicazione e assegnamento
/= Divisione e assegnamento
%= Modulo e assegnamento
&= AND bit per bit e assegnamento
|= OR bit per bit inclusivo e assegnamento
^= OR bit per bit esclusivo e assegnamento
<<= Scorrimento a sinistra bit per bit e assegnamento
>>= Scorrimento a destra bit per bit e assegnamento
Un operatore di assegnamento composto è, in effetti, una forma contratta per esprimere in
modo conciso un’espressione come E1 = E1 op (E2) e non, contrariamente a quanto si possa
pensare, un’espressione come E1 = E1 op E2.

L’equivalenza di E1 op= E2 con E1 = E1 op (E2) è però “parziale”, perché nel primo caso E1
è valutata solo una volta mentre nel secondo caso E1 è valutata due volte, e ciò ha
implicazioni importanti se la sua valutazione causa anche dei side effect.

Snippet 4.40 Operatore composto di moltiplicazione.


int a = 10;
int b = 11;
int c = 20;

// qui equivalente con a = a * (b + c)


a *= b + c; // 310

a = 10;
// qui si vede, nel risultato, la non equivalenza con a = a * b + c
a = a * b + c; // 130

Lo Snippet 4.40 dichiara le variabili a, b e c e poi usa l’operatore di assegnamento


composto *= per assegnare alla variabile a il risultato dell’espressione posta alla sua destra,
ossia b + c, moltiplicato per il valore di a medesima.
In pratica poiché in questo caso la precedenza dell’operatore + è maggiore rispetto
all’operatore *=, ecco che viene eseguita prima l’espressione b + c e poi il risultato viene
moltiplicato per a; ciò spiega l’equivalenza con a * (b + c) dove, ricordiamo, le parentesi
tonde permettono di cambiare l’ordine di priorità degli operatori facendo eseguire prima b +

c rispetto ad a * b.

In seguito assegniamo ad a nuovamente il valore 10 e poi lo cambiamo con il valore di


un’altra espressione che chiarisce il perché non vi è un’equivalenza tra E1 op= E2 e E1 = E1 op

E2.

Infatti, per effetto dell’ordine di precedenza degli operatori * e +, avremo che prima sarà
eseguita la moltiplicazione tra a e b e poi tale valore sarà addizionato a c e questo risultato
sarà ad a medesima assegnato.

Snippet 4.41 Operatore composto di moltiplicazione con side effect.


int a[] = {1,2,3};
int ix = 0;

// ok ix è incrementato solo una volta


a[ix++] *= 10 + 20; // 30

ix = 0;

// qui ix è incrementato e utilizzato nell'ambito della stessa espressione di


// assegnamento; tuttavia non sappiamo quale ordine di valutazione degli operandi
// il compilatore attuerà per primo; l'operazione su ix è quindi non definita
a[ix++] = a[ix++] * (10 + 20); // ???
Lo Snippet 4.41 evidenzia anche come l’equivalenza tra E1 op= E2 e E1 = E1 op (E2) non
sia “perfetta” quando E1 nell’ambito della sua valutazione causa dei side effect.
Infatti, l’obiettivo del nostro snippet è di assegnare all’elemento 0 dell’array la somma tra
10 e 20 moltiplicata per il precedente contenuto di tale elemento, ossia 1.
Nella prima espressione, notiamo come non via sia alcun problema perché ix è valutato
solo una volta (anche se l’operatore ++ utilizzato su ix è postfisso, al termine del sequence
point introdotto dal punto e virgola che termina l’espressione, ix sarà comunque
incrementato di 1 e una solta volta).
Nella seconda espressione, invece, non solo non sappiamo se verrà valutato prima
l’operando di sinistra oppure prima l’operando di destra e dunque quale elemento dell’array
sarà preso in considerazione, ma in più, la variabile ix, al termine del sequence point
relativo, sarà incrementata sicuramente due volte assumendo dunque il valore di 2.
Tabella di precedenza degli operatori
Riportiamo una tabella riepilogativa di tutti gli operatori disposti a partire da quelli con la
priorità più alta e con la relativa associatività (gli operatori con l’asterisco * saranno
esaminati più avanti).
Tabella 4.8 Tabella riepilogativa di precedenza degli operatori secondo lo standard C11.
Priorità Operatore Denominazione o significato Associatività
1 [] Indice array Da sinistra a destra
() Invocazione di funzione o raggruppamento da sinistra a destra
. Accesso membro struttura* Da sinistra a destra
-> Accesso membro struttura tramite puntatore* Da sinistra a destra
++ Incremento postfisso Da sinistra a destra
-- Decremento postfisso Da sinistra a destra
(type){list} Letterale composto* Da sinistra a destra
2 ++ Incremento prefisso Da destra a sinistra
-- Decremento prefisso Da destra a sinistra
+ Più unario Da destra a sinistra
- Meno unario Da destra a sinistra
& Indirizzo di* Da destra a sinistra
* Deriferimento o indirezione* Da destra a sinistra
~ Complemento a uno Da destra a sinistra
! NOT logico Da destra a sinistra
(type) Cast Da destra a sinistra
sizeof Dimensione di un operando Da destra a sinistra
_Alignof Allineamento di un operando* Da destra a sinistra
3 * Moltiplicazione Da sinistra a destra
/ Divisione Da sinistra a destra
% Modulo Da sinistra a destra
4 + Addizione Da sinistra a destra
- Sottrazione Da sinistra a destra
5 << Scorrimento a sinistra bit per bit Da sinistra a destra
>> Scorrimento a destra bit per bit Da sinistra a destra
6 < Minore di Da sinistra a destra
<= Minore di o uguale a Da sinistra a destra
> Maggiore di Da sinistra a destra
>= Maggiore di o uguale a Da sinistra a destra
7 == Uguale a Da sinistra a destra
!= Non uguale a Da sinistra a destra
8 & AND bit per bit Da sinistra a destra
9 ^ OR bit per bit esclusivo Da sinistra a destra
10 | OR bit per bit inclusivo Da sinistra a destra
11 && AND logico Da sinistra a destra
12 || OR logico Da sinistra a destra
13 ? : Condizionale Da destra a sinistra
14 = Assegnamento semplice Da destra a sinistra
+= Addizione e assegnamento Da destra a sinistra
-= Sottrazione e assegnamento Da destra a sinistra
*= Moltiplicazione e assegnamento Da destra a sinistra
/= Divisione e assegnamento Da destra a sinistra
%= Modulo e assegnamento Da destra a sinistra
<<= Scorrimento a sinistra e assegnamento Da destra a sinistra
>>= Scorrimento a destra e assegnamento Da destra a sinistra
&= AND bit per bit e assegnamento Da destra a sinistra
^= OR bit per bit esclusivo e assegnamento Da destra a sinistra
|= OR bit per bit inclusivo e assegnamento Da destra a sinistra
15 , Virgola Da sinistra a destra
Capitolo 5
Strutture di controllo

Un programma è composto da una serie di istruzioni che possono essere eseguite


nell’ordine in cui vengono scritte e in modo sequenziale, oppure in modo non lineare,
mediante l’utilizzo di appositi costrutti sintattici di programmazione, definiti nel loro
insieme strutture di controllo, che sono espressi mediante delle apposite keyword del
linguaggio che rappresentano essi stessi delle istruzioni (Tabella 5.1).
Tabella 5.1 Classificazione delle strutture di controllo.
Istruzioni di selezione Istruzioni di iterazione Istruzioni di salto
if while break
switch do continue
for goto
return

NOTA
In C esistono anche altre tipologie di istruzioni, come le istruzioni composte (compound
statement), rappresentate dai blocchi di codice delimitati dalle parentesi graffe { }; le
istruzioni nulle (null statement), rappresentate dal singolo carattere punto e virgola; le
istruzioni espressione (expression statement), rappresentate da un’espressione terminata
dal carattere punto e virgola.

ESPRESSIONI, ISTRUZIONI E BLOCCHI DI CODICE: UN BREVE RIPASSO


Un’espressione è un qualsiasi costrutto sintattico composto da un insieme di operatori e
operandi in cui gli operandi rappresentano dei valori che sono valutati al fine di ritornare,
generalmente, un altro valore come risultato. Gli operandi possono essere variabili, costanti,
invocazioni di metodi, altre espressioni e così via, mentre gli operatori possono essere tutti
quelli visti finora. Un’istruzione è, invece, definibile come un costrutto sintattico che consente
di compiere determinate operazioni nell’ambito di un programma informatico. Possiamo infatti
avere istruzioni di dichiarazione e di inizializzazione delle variabili, di selezione e di controllo
del flusso esecutivo del programma e anche istruzioni che rappresentano esse stesse delle
espressioni, come quando, per esempio, assegniamo una valore a una variabile. Un blocco di
codice è, infine, un insieme di più istruzioni racchiuse tra le parentesi graffe { }.
Istruzioni di selezione
Le istruzioni di selezione (selection statement) consentono di dirigere e distribuire il
flusso di esecuzione del codice verso determinate istruzioni piuttosto che verso altre in base
al valore di un’espressione denominata espressione di controllo (controlling expression).
L’espressione di controllo è altresì considerata una full expression, e pertanto qualsiasi
eventuale effetto collaterale che possa lì prodursi sarà valutato prima di una successiva full
expression.

Istruzione di selezione singola if


La prima e più semplice struttura di controllo è quella definita di selezione singola if
(Sintassi 5.1 e 5.2), che permette di eseguire una o più istruzioni se, e solo se, una
determinata espressione è vera.

Sintassi 5.1 Istruzione if che esegue una singola istruzione.


if (expression) statement;

La Sintassi 5.1 evidenzia come l’istruzione di selezione singola if si scriva utilizzando la


medesima keyword e una coppia di parentesi tonde ( ) al cui interno si indica, tramite
expression, un’espressione di controllo da valutare.
Si scrive, poi, attraverso statement, l’istruzione che dovrà essere eseguita solo se
expression sarà diversa da 0 e dunque vera.
Se, viceversa, expression sarà uguale a 0 e dunque falsa, allora il flusso esecutivo del
codice si sposterà alla prima istruzione posta subito dopo l’istruzione if medesima.

Sintassi 5.2 Istruzione if che esegue un blocco di istruzioni.


if (expression)
{
statement_1;
statement_2;
...
statement_N;
}
Figura 5.1 Diagramma dell’istruzione di selezione singola if.

La Sintassi 5.2 mostra, invece, come scrivere un’istruzione if che esegue un blocco di
istruzioni se expression è diversa da 0. In pratica è sufficiente scrivere le istruzioni di
interesse all’interno della consueta coppia di parentesi graffe { }, le quali non
necessiteranno del punto e virgola finale che marca, solitamente, un’istruzione.

Listato 5.1 If.c (If).


/* If.c :: Uso dell'istruzione if :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = -1;

// a è minore di 10?
if (a < 10)
printf("a < 10\n");

return (EXIT_SUCCESS);
}

Output 5.1 Dal Listato 5.1 If.c.


a < 10

Nel Listato 5.1 l’istruzione if valuta l’espressione a < 10 la quale ritorna come valore 1,
ossia una valore diverso da 0 che è dunque vero, e pertanto viene eseguita la correlativa
istruzione printf che manda in output la stringa "a < 10\n".

Snippet 5.1 Espressione di controllo dell’istruzione if come full expression.


int a = 10;
int b = 0;
if (a++ <= 10) // l'espressione è vera: a è minore o uguale a 10
b = a; // qui b varrà 11...

Lo Snippet 5.1 mostra con chiarezza che l’espressione di controllo di un if è considerata


una full expression. Infatti, dato che vi è un sequence point tra la valutazione di una full
expression e la successiva da valutare, il compilatore valuterà se a è minore o uguale a 10,
poi porterà a termine l’effetto collaterale su a, che è quello di incrementarlo di 1 (a++), e
infine, dato che l’espressione di controllo sarà risultata vera, assegnerà a b il valore di a che
sarà diventato 11.
In pratica, anche se sulla variabile a agisce l’operatore di incremento ++ postfisso, tale
incremento non avverrà dopo l’esecuzione dell’assegnamento ma prima, ossia al termine
della valutazione dell’espressione di controllo dell’if.

Istruzione di selezione doppia if/else


La struttura di controllo definita di selezione doppia if/else (Sintassi 5.3) permette di
eseguire una o più istruzioni se, e solo se, una determinata espressione è vera; altrimenti, se,
e solo se, tale espressione è falsa, esegue un’altra o altre istruzioni. I rami di esecuzione
delle istruzioni sono mutualmente esclusivi.
Dal punto di vista operativo, l’istruzione if permette di eseguire una sola azione, mentre
l’istruzione if/else consente di scegliere tra l’esecuzione di due azioni.

Sintassi 5.3 Istruzione if/else.


if (expression)
statement;
else
statement;

La Sintassi 5.3 ha la prima parte del tutto simile alla Sintassi 5.1 ma in più ha l’aggiunta
della keyword else (che rappresenta una clausola) e di un ulteriore statement che
rappresenta l’istruzione che verrà eseguita se expression risulterà pari a 0.
Se però expression risulterà diversa da 0, allora l’istruzione del ramo else non sarà eseguita
perché sarà eseguita l’istruzione del ramo if.
Anche in questo caso è possibile definire un blocco di istruzioni da eseguire, nel ramo if
oppure nel ramo else, ponendole tra le parentesi graffe { }.

Figura 5.2 Diagramma dell’istruzione di selezione doppia if/else.


Listato 5.2 IfElse.c (IfElse).
/* IfElse.c :: Uso dell'istruzione if/else :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 5;

if (a >= 10)
printf("a >= 10\n"); // eseguita se a è maggiore o uguale a 10
else
printf("a < 10\n"); // eseguita in caso contrario

return (EXIT_SUCCESS);
}

Output 5.2 Dal Listato 5.2 IfElse.c.


a < 10

La struttura di controllo if/else può essere costruita con più livelli di annidamento
considerando che lo standard stabilisce che un’implementazione ne deve garantire al
minimo 127.

Listato 5.3 IfElseNested.c (IfElseNested).


/* IfElseNested.c :: Dimostra l'uso di if/else nidificati :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 3;

if(a >= 10)


printf("a >= 10\n");
else
if(a >= 5)
printf("a >= 5 e a < 10\n");
else
if(a >= 0)
printf("a >= 0 e a < 5\n");

return (EXIT_SUCCESS);
}

Lo stesso codice, è scritto, quasi sempre, nel modo seguente (sicuramente più leggibile),
dove ogni else if è scritto nell’ambito della stessa riga e sono indentati esattamente a partire
dal primo if.

Listato 5.4 IfElseNestedAndWithIndentation.c (IfElseNestedAndWithIndentation).


/* IfElseNestedAndWithIndentation.c :: if/else nidificati e indentati :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 3;

if (a >= 10)
printf("a >= 10\n");
else if (a >= 5)
printf("a >= 5 e a < 10\n");
else if (a >= 0)
printf("a >= 0 e a < 5\n");

return (EXIT_SUCCESS);
}

È importante rilevare che questa forma di scrittura delle istruzioni if/else non introduce
alcuna nuova forma di statement di controllo, ma è solo una maniera per rendere più chiaro
l’obiettivo computazionale della struttura di controllo, che è quello di compiere una serie di
valutazioni di espressioni laddove solo una o nessuna potrà essere vera e dunque eseguire o
meno le istruzioni correlate.
In definitiva una struttura di controllo con una serie di if/else nidificati altro non è che
una struttura di selezione doppia if/else dove ogni else ha come istruzione un’altra
istruzione if e così via per altre clausole else.

Output 5.3 Dal Listato 5.3 IfElseNested.c e dal Listato 5.4 IfElseNestedAndWithIndentation.c.
a >= 0 e a < 5

L’Output 5.3 rileva che, comunque si scriva la struttura di controllo, la logica è sempre la
stessa: se è vera una delle espressioni, allora vengono eseguite le istruzioni corrispondenti e
il programma salta al di fuori di tutti gli altri if/else; se nessuna condizione è vera, allora il
programma le salta tutte.
Nel nostro caso, dunque, dato che la variabile a vale 3:

l’espressione del primo if sarà valutata falsa: a non è maggiore o uguale a 10;
l’espressione del secondo if della prima clausola else sarà valutata falsa: a non è
maggiore o uguale a 5;
l’espressione del terzo if della seconda clausola else sarà valutata vera: a è maggiore o
uguale a 0.

Quando si scrivono più strutture if/else, si può incorrere nell’errore denominato dell’else
pendente (dangling else), in cui l’else non è attribuito all corretto e correlativo if.

Listato 5.5 DanglingElse.c (DanglingElse).


/* DanglingElse.c :: Problema dell'else pendente :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 9, b = 3;

if (a > 10)
if (b > 10)
printf("a e b > 10\n"); // eseguita se a e b sono maggiori di 10
else
printf("a < 10\n");
return (EXIT_SUCCESS);
}

L’intento del programma del Listato 5.5 sarebbe quello di far stampare a e b > 10 se le
variabili a e b fossero maggiori di 10 e a < 10 nel caso in cui a fosse minore di 10.
Tuttavia, per come è stato scritto il codice, eseguendo il programma non viene stampata
l’istruzione dell’else, anche se la variabile a è minore di 10 (è infatti uguale a 9) e quindi
soddisfa la condizione corrispondente.
Ciò si verifica perché, come regola, il compilatore associa la clausola else al primo if
precedente che trova (a quello cioè lessicalmente più vicino permesso dalla sintassi).
Nel nostro caso, infatti, il compilatore associa la clausola else all’if più vicino che risulta
essere quello con l’espressione b > 10.

Per questa ragione il compilatore interpreta le istruzioni nel seguente modo: la variabile a
è maggiore di 10? Se lo è, allora valuta se la variabile b è maggiore di 10, e in caso
affermativo lo stampa a e b > 10, altrimenti stampa a < 10.

NOTA
Se lanciamo il compilatore GCC con il flag -Wall oppure -Wparentheses otterremo il messaggio
warning: suggest explicit braces to avoid ambiguous 'else' [-Wparentheses].

Al fine di ottenere il risultato corretto dovremo scrivere il codice come illustrato di


seguito, dove, grazie all’ausilio delle parentesi graffe { } delimitiamo l’if più esterno e
rendiamo evidente che è a esso che fa riferimento il correlativo else.

Listato 5.6 CorrectionOfTheDanglingElse.c (CorrectionOfTheDanglingElse).


/* CorrectionOfTheDanglingElse.c :: Problema dell'else pendente corretto :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 9, b = 3;

if (a > 10)
{
if (b > 10)
printf("a e b > 10\n"); // eseguita se a e b sono maggiori di 10
}
else
printf("a < 10\n");

return (EXIT_SUCCESS);
}

Output 5.4 Dal Listato 5.6 CorrectionOfTheDanglingElse.c.


a < 10

Istruzione di selezione multipla switch


La struttura di selezione multipla switch (Sintassi 5.4) consente di eseguire le istruzioni di
un blocco di codice, marcato da una particolare etichetta espressa da una clausola case o
default, se il valore intero costante che questa rappresenta è uguale (e mai maggiore o
minore) al valore dell’espressione di controllo da valutare, che deve essere di tipo intero
(per esempio, un tipo char, int, long e così via è legale mentre un tipo float, double e altri no).

Sintassi 5.4 Istruzione switch.


switch (integer_expression)
{
case integer_constant_expression_1:
statements_1;
[break];
case integer_constant_expression_2:
statements_2;
[break];
case integer_constant_expression...:
statements...;
[break];
case integer_constant_expression_N:
statements_N;
[break];
[default:
statements_N;
[break;]
]
}

Per costruire un’istruzione di selezione multipla si deve procedere come segue.


1. Si scrive la keyword switch seguita da una coppia di parentesi tonde ( ) al cui interno si
indica un’espressione intera da valutare. Si definisce, quindi, tra le consuete parentesi
graffe { } il body dello switch stesso.
2. Si scrivono delle etichette case che indicano delle espressioni costanti intere i cui valori
saranno confrontati con il valore ritornato dalla valutazione dell’espressione intera
dello switch. Ogni etichetta case potrà avere una o più istruzioni che saranno eseguite
se, e solo se, il valore della sua espressione sarà uguale al valore dell’espressione
dell’istruzione switch. Eventualmente, come statement finale, si può scrivere
un’istruzione break che trasferisce l’esecuzione del codice all’istruzione successiva
all’istruzione switch (di fatto break provoca un’uscita immediata dal blocco switch).
3. Si scrive, nel caso, un’etichetta espressa dalla clausola default che indica una o più
istruzioni che saranno eseguite se nessun caso soddisferà l’espressione indicata dallo
switch.

Prima di vedere degli esempi pratici di utilizzo del costrutto switch è importante dare
anche altre indicazioni: se le etichette case hanno due o più istruzioni, le stesse possono
essere scritte senza racchiuderle tra le parentesi graffe { }; non possono esservi etichette
case duplicate nell’ambito dello stesso switch (in pratica più espressioni dei casi non possono
rintonare lo stesso valore) e l’ordine di scrittura delle stesse non ha importanza; ci può
essere solo un’etichetta default che in genere è posta come ultima label (è possibile
collocarla ovunque all’interno dello switch).

Figura 5.3 Diagramma dell’istruzione di selezione multipla switch (secondo un comune ordine di
scrittura).

Listato 5.7 SwitchCase.c (SwitchCase).


/* SwitchCase.c :: Uso dell'istruzione switch :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int number = 4;

// valuto number
switch (number)
{
case 1: // vale 1?
printf("number = 1\n");
break;
case 2: // vale 2?
printf("number = 2\n");
break;
case 3: // vale 3?
printf("number = 3\n");
break;
case 4: // vale 4?
printf("number = 4\n");
break;
default: // nessuna corrispondenza?
printf("number = [no matching]\n");
}

return (EXIT_SUCCESS);
}

Output 5.5 Dal Listato 5.7 SwitchCase.c.


number = 4

Nel Listato 5.7 la keyword switch valuta il valore della variabile number (in questo caso 4) e
cerca una corrispondenza tra i valori delle etichette case. Se trova un’etichetta case che
soddisfa tale valutazione (e nel nostro caso la trova: case 4:), allora ne esegue le istruzioni
ivi indicate: l’una si limita a stampare i caratteri number = 4 tramite printf; l’altra, break, esce
dallo switch e fa riprendere l’esecuzione del programma dalla prossima istruzione che è
identificata dalla keyword return.
Più etichette case possono essere “raggruppate” in modo da esplicitare delle istruzioni che
saranno eseguite se uno qualsiasi dei valori delle relative espressioni corrisponderà al valore
dell’espressione dello switch.

Listato 5.8 GroupingOfCaseLabels.c (GroupingOfCaseLabels).


/* GroupingOfCaseLabels.c :: Etichette case raggruppate :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
char letter = 'e';

// FALL THROUGH esplicito: è buona norma inserire un commento come questo...


switch (letter)
{
// lettere a, b, c ?
case 'a':
case 'b':
case 'c':
printf("Tra le lettere a, b, c\n");
break;
// lettere d, e, f ?
case 'd':
case 'e':
case 'f':
printf("Tra le lettere d, e, f\n");
break;
// nessuna corrispondenza
default:
printf("Nessuna corrispondenza di lettera");
break; // non necessario ma utile in caso di ulteriori case posti dopo...
}

return (EXIT_SUCCESS);
}

Output 5.6 Dal Listato 5.8 GroupingOfCaseLabels.c.


Tra le lettere d, e, f
Il Listato 5.8 evidenzia che per raggruppare più etichette case è sufficiente indicarle le
une dopo le altre e poi, dopo l’ultima, scrivere le istruzioni che saranno eseguite con questo
scopo.
Nel nostro caso, switch valuta la variabile letter, che ritorna il valore intero ASCII del
carattere e (101 in base decimale), e poi verifica se esiste un’etichetta case con quel valore.
La verifica ha esito favorevole (case 'e':) e, quindi, a partire dal quel punto, inizia a
scorrere il codice finché non trova una qualche istruzione eseguibile. Indipendentemente se
vi siano o meno altre etichette case, il codice eseguito sarà quello indicato dall’etichetta case
'f':, ma ciò non causerà alcun errore logico nel programma perché le etichette case sono
state scritte proprio con l’obiettivo di stampare i caratteri Tra le lettere d, e, f se il valore
dell’espressione letter ricade in quel range di caratteri.
Infine, notiamo come l’etichetta default abbia come ultima istruzione break. Questa,
ancorché non necessaria (se non vi fosse, dopo l’esecuzione della relativa istruzione printf,
il flusso esecutivo del codice si sposterebbe alla parentesi graffa di chiusura dello switch e
quindi all’istruzione return posta dopo di esso) può diventare importante se,
successivamente, decidiamo di inserire un’etichetta case dopo l’etichetta default.
Infatti, in assenza del break, se non vi fossero espressioni delle etichette case in grado di
soddisfare l’espressione di controllo dello switch, il controllo passerebbe all’etichetta default
e poi anche all’altra etichetta case eseguendone in modo non opportuno il relativo codice.
TERMINOLOGIA
Il comportamento eseguito dall’istruzione switch per cui quando l’esecuzione di un caso è
terminata passa in automatico al caso successivo (in cascata), e se non esplicitamente
negato tramite un’istruzione break, è definita falls through.

Snippet 5.2 Espressione di controllo dell’istruzione switch come full expression.


int a = 9;
int b = 100;

switch (a++) // effetto collaterale: a cambia valore da 9 a 10


{
case 9: b = b * a + 250; // questo è il ramo eseguito... ma a varrà 10 e non 9
break;
case 10: b = b * a + 500;
break;
default: break;
}

Lo Snippet 5.2 mostra un ulteriore esempio del fatto che un’espressione di controllo è
una full expression: infatti, la valutazione dell’espressione a dello switch darà come risultato
9 che corrisponderà all’etichetta case 9: laddove, quando il compilatore eseguirà la relativa
istruzione, a sarà stata prima incrementata a 10 dall’operatore postfisso ++.
Istruzioni di iterazione
Le istruzioni di iterazione (iteration statement) consentono di eseguire una o più
istruzioni (loop body) in modo ripetuto (ciclico), finché un’espressione di controllo non
diventa falsa ossia uguale a 0. L’espressione di controllo delle istruzioni while e do e le
espressioni facoltative dell’istruzione for sono considerate delle full expression, e dunque
qualsiasi eventuale effetto collaterale che possa lì prodursi sarà valutato prima della
valutazione di una successiva full expression.
TERMINOLOGIA
A volte le istruzioni di iterazione sono denominate loop condizionali (conditional loop)
perché l’esecuzione ciclica delle istruzioni relative dipenderà da una “condizione”
evidenziata dall’espressione di controllo. Se, per esempio, scrivessimo un’espressione di
controllo per un loop come a < 10, è come se ponessimo come condizione per l’esecuzione
ciclica delle sue istruzioni quella che a deve essere minore del valore 10.

Istruzione di iterazione while


La struttura di iterazione while (Sintassi 5.5) permette di eseguire lo stesso blocco di
istruzioni ripetutamente finché una determinata espressione è vera ossia diversa da 0.

Sintassi 5.5 Istruzione while.


while (expression)
statement;

In pratica per utilizzare tale istruzione di iterazione utilizziamo la keyword while, una
coppia di parentesi tonde ( ) al cui interno vi sarà l’espressione di controllo da valutare e
l’istruzione da eseguire finché expression sarà diversa da 0. È altresì possibile definire due o
più istruzioni da eseguire ciclicamente ponendole tra le parentesi graffe { }.

Figura 5.4 Diagramma dell’istruzione di iterazione while.


Listato 5.9 While.c (While).
/* While.c :: Uso dell'istruzione while :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 8;

printf("a = [ ");
while (a >= 0) // finché a >= 0 esegue il ciclo
printf("%d ", a--);

printf("]\n");

return (EXIT_SUCCESS);
}

Output 5.7 Dal Listato 5.9 While.c.


a = [ 8 7 6 5 4 3 2 1 0 ]

Nel programma del Listato 5.9 il ciclo while si può interpretare in questo modo: finché la
variabile a è maggiore o uguale al valore 0, stampane il valore ripetutamente. Nel blocco di
codice del while l’istruzione a-- è fondamentale poiché permette di decrementarne il valore.
Se non ci fosse questa istruzione, il ciclo while sarebbe infinito, perché la condizione
sarebbe sempre vera (diversa da 0), dato che la variabile a sarebbe sempre maggiore o
uguale a 0.
Ripetiamo, per chiarire meglio come si comporta il flusso esecutivo della struttura
iterativa while:

1. Controlla se a è >= 0 e se lo è va al punto 2, altrimenti va al punto 3.


2. Esegue l’istruzione di stampa, decrementa a e ritorna al punto 1.
3. Esce dal ciclo.
Si nota che, se l’espressione è subito falsa (uguale a 0), le istruzioni nel corpo della
struttura while non verranno mai eseguite.
In pratica, in un’istruzione di iterazione while, l’espressione di controllo è sempre valutata
“prima” che le istruzioni che ne compongono il loop body siano eseguite anche una sola
volta (potrebbero, quindi, non essere mai eseguite se l’espressione di controllo è subito
falsa).

Snippet 5.3 Espressione di controllo dell’istruzione while come full expression.


int j = 1;
int a = -1;

while (j--)
a = j; // qua a varrà 0
Lo Snippet 5.3 evidenzia che quando il compilatore elaborerà l’istruzione while, prima
verificherà se j è diversa da 0 e poi completerà il side effect dell’operatore postfisso ++ che
la decrementerà a 0. Poi assegnerà ad a quel valore.
In pratica anche se l’operatore ++ è postfisso, non assegnerà prima ad a il valore originario
di j, ossia 1, e poi decrementerà j.

Istruzione di iterazione do/while


La struttura di iterazione do/while (Sintassi 5.6) consente, analogamente alla struttura
while, di ripetere un blocco di istruzioni ripetutamente finché un’espressione è vera. In
questo caso, a differenza di while, le istruzioni del corpo della struttura do vengono eseguite
almeno una volta, poiché la valutazione dell’espressione di controllo viene effettuata dopo
che il flusso esecutivo del codice ha raggiunto l’istruzione while posta in coda.

Sintassi 5.6 Istruzione do.


do
statement;
while (expression);

Tale istruzione si utilizza scrivendo la keyword do, l’istruzione da eseguire finché


l’espressione di controllo sarà diversa da 0, la keyword while, una coppia di parentesi tonde (
) al cui interno vi sarà l’espressione di controllo da valutare e il carattere punto e virgola
finale.
È inoltre possibile definire due o più istruzioni da eseguire ciclicamente ponendole tra le
parentesi graffe { }.

Figura 5.5 Diagramma dell’istruzione di iterazione do/while.


Listato 5.10 DoWhile.c (DoWhile).
/* DoWhile.c :: Uso dell'istruzione do/while :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 8;

printf("a = [ ");
do // parentesi non necessarie... scritte solo per maggiore chiarezza
{
printf("%d ", a--);
}
while (a >= 0); // finché a >= 0 esegue il ciclo

printf("]\n");

return (EXIT_SUCCESS);
}

Output 5.8 Dal Listato 5.10 DoWhile.c.


a = [ 8 7 6 5 4 3 2 1 0 ]

Il ciclo del Listato 5.10 si comporta allo stesso modo di quello del Listato 5.9 ma, a
differenza di esso, stampa il valore di a e poi lo decrementa almeno una volta, anche se
potrebbe essere subito minore di 0, e poi verifica se a è maggiore o uguale a 0.
Il flusso esecutivo del blocco do/while è il seguente.

1. Esegue l’istruzione di stampa e decrementa a.


2. Controlla se a è >= 0 e se lo è va al punto 1, altrimenti va al punto 3.
3. Esce dal ciclo.
Si nota che, se l’espressione è subito falsa (uguale a 0), le istruzioni nel corpo della
struttura do/while verranno eseguite almeno una volta.
In pratica, in un’istruzione di iterazione do/while, l’espressione di controllo è sempre
valutata “dopo” che le istruzioni che ne compongono il loop body sono state eseguite anche
una sola volta (potrebbero, quindi, essere state eseguite anche se l’espressione di controllo è
subito falsa).

Snippet 5.4 Espressione di controllo dell’istruzione do/while come full expression.


int a = 3;
int b = 2;
int j = 1;

do
{
// prima della verifica della condizione a varrà 1
// dopo le valutazioni delle espressioni nel while a varrà 6...
a = j;
if (a == 6)
break;
}
while (j = (a + 1, b += a, b));
Lo Snippet 5.4 mostra come anche nell’ambito di un ciclo do/while tutti i side effect
saranno valutati prima di eseguire il loop body.
Infatti sia la variabile j sia la variabile b subiranno una modifica del loro valore in
memoria, e tali modifiche saranno attuate prima di entrare nel corpo del ciclo e ciò perché,
ribadiamo, vi sono due sequence point: il primo è marcato dall’operatore virgola (,) e il
secondo è marcato dall’espressione di controllo del while che è una full expression.

Istruzione di iterazione for


La struttura di iterazione for (Sintassi 5.7) consente, come le strutture while e do/while, di
ripetere un blocco di istruzioni finché un’espressione è vera (diversa da 0); a differenza di
while e do/while, for consente di gestire nell’ambito del suo costrutto delle espressioni
aggiuntive con cui, generalmente, si inizializzano e modificano delle variabili di controllo.

Sintassi 5.7 Istruzione for.


for ([decl_OR_expr_1]; [expression_2]; [expression_3])
statement;

Di fatto, per usare tale struttura, si scrive la keyword for seguita dalle parentesi tonde ( )

che racchiudono tre espressioni opzionali di cui la prima può agire anche come istruzione di
dichiarazione. Abbiamo:
decl_OR_expr_1, che esprime una dichiarazione di una o più variabili che possono essere
impiegate dalle altre espressioni del for o nell’ambito del suo loop body oppure
un’espressione che viene valutata inizialmente prima dell’espressione di controllo.
Segue un punto e virgola;
expression_2, che indica l’espressione di controllo che viene valutata prima

dell’esecuzione del loop body del for e che controlla, quindi, la condizione di
terminazione del ciclo medesimo. Segue un punto e virgola;
expression_3, che designa l’espressione da valutare dopo l’esecuzione del loop body del

for.

Termina la definizione della struttura for l’istruzione (statement) da eseguire ciclicamente


finché l’espressione di controllo sarà vera.
Se si vogliono eseguire in loop due o più istruzioni, anche il ciclo for può racchiuderle tra
la coppia di parentesi graffe { } proprie di una compound statement.
NOTA
È solo a partire dallo standard C99 che è possibile utilizzare un’istruzione di dichiarazione di
una o più variabili direttamente come prima espressione. Prima di esso, infatti, la prima
espressione doveva essere una mera espressione (Snippet 5.5 e 5.6).
Snippet 5.5 Prima espressione e modalità di utilizzo C11.
// dichiarazione della variabile i direttamente come prima espressione:
// C11 consentito
for (int i = 0; i < 10; i++)
; // fai qualcosa...

Snippet 5.6 Prima espressione e modalità di utilizzo C90.


/*
la dichiarazione della variabile i è posta fuori dal costrutto for;
nel costrutto for la prima espressione è una mera espressione
*/
int i;
for (i = 0; i < 10; i++)
; /* fai qualcosa... */

Analizzando la definizione di un’istruzione for possiamo trovare una certa equivalenza


con l’istruzione while: in pratica qualsiasi loop creato con un costrutto for è creabile con un
costrutto while, come mostra la generalizzazione di entrambi di cui la Sintassi 5.8.

Sintassi 5.8 Equivalenza tra il costrutto for e il costrutto while.


// un ciclo for...
for (expression_1; expression_2; expression_3)
statement;

// un ciclo while...
expression_1;
while (expression_2)
{
statement;
expression_3;
}

Figura 5.6 Diagramma dell’istruzione di iterazione for.

Listato 5.11 For.c (For).


/* For.c :: Uso dell'istruzione for :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
printf("a = [ ");
for (int a = 8; a >= 0; a--) // finché a >= 0 esegue il ciclo
printf("%d ", a);

printf("]\n");

return (EXIT_SUCCESS);
}

Output 5.9 Dal Listato 5.11 For.c.


a = [ 8 7 6 5 4 3 2 1 0 ]

Esaminando il Listato 5.11 vediamo che la struttura iterativa for impiegata è formata dai
seguenti blocchi costitutivi: un’istruzione di dichiarazione, eseguita solo una volta, in cui è
inizializzata una variabile di controllo (visibile solamente nel ciclo for), rappresentata
dall’istruzione int a = 8; un’espressione di controllo della condizione di continuazione del
ciclo, rappresentata dall’istruzione a >= 0; un’espressione di modifica della variabile di
controllo, rappresentata dall’istruzione a--.
Il flusso esecutivo del ciclo è invece il seguente.
1. Dichiara e inizializza la variabile a.
2. Controlla se a >= 0 e se lo è va al punto 3, altrimenti va al punto 6.
3. Stampa il valore di a.
4. Decrementa la variabile a.
5. Ritorna al punto 2.
6. Esce dal ciclo.

TERMINOLOGIA
Per ambito di visibilità si intende una regione del codice sorgente dove un identificatore è
accessibile e dunque utilizzabile. Ritorneremo su questo importante concetto nel Capitolo 9,
“Dichiarazioni”.

Vediamo un altro esempio di utilizzo (Listato 5.12) di un ciclo for dove la prima
espressione dichiara e inizializza più variabili, mentre la terza espressione ne modifica il
valore.

Listato 5.12 ComplexExpressionsWithFor.c (ComplexExpressionsWithFor).


/* ComplexExpressionsWithFor.c :: Uso dell'istruzione for con espressioni complesse :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int var1 = 3, var2 = 2;
printf("a\tz\n");
for (int a = var1 * 2 + var2, z = 0; a >= 0; a--, z++)
printf("%d\t%d\n", a, z);

return (EXIT_SUCCESS);
}

Output 5.10 Dal Listato 5.12 ComplexExpressionsWithFor.c.


a z
8 0
7 1
6 2
5 3
4 4
3 5
2 6
1 7
0 8

In definitiva la prima espressione di un ciclo for può contenere una dichiarazione di più
variabili utilizzando il carattere virgola (,) che agisce come separatore, oppure più
espressioni usando sempre il carattere virgola ma che agisce come operatore.
La terza espressione può altresì contenere più espressioni usando ancora l’operatore
virgola.
I blocchi costitutivi della struttura iterativa for (decl_OR_expr_1, expression_2 e expression_3)
si possono anche omettere, a condizione però che i punti e virgola ; di separazione vengano
scritti. Quando, tuttavia, viene omesso expression_2, il compilatore lo sostituisce con una
costante diversa da 0 e pertanto ne rende infinito il ciclo.

Listato 5.13 ForWithoutExpressions.c (ForWithoutExpressions).


/* ForWithoutExpressions.c :: Uso dell'istruzione for senza le espressioni :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 8;

printf("a = [ ");
for (;;) // ciclo infinito che è interrotto dal break
{
if (a < 0)
break; // senza questa istruzione il ciclo diventa infinito
printf("%d ", a--);
}
printf("]\n");

return (EXIT_SUCCESS);
}

Output 5.11 Dal Listato 5.13 ForWithoutExpressions.c.


a = [ 8 7 6 5 4 3 2 1 0 ]

Nel Listato 5.13 notiamo come le espressioni costituenti la struttura iterativa siano state
poste prima dell’istruzione for per la definizione della variabile di controllo, e all’interno
del blocco di istruzioni del ciclo per il controllo di terminazione e per la modifica della
variabile di controllo. Vediamo, infine, che i cicli for si possono costruire anche come cicli
senza corpo. Essi devono però contenere sempre almeno un’istruzione definita come vuota
o nulla (carattere punto e virgola).

Listato 5.14 ForWithoutBody.c (ForWithoutBody).


/* ForWithoutBody.c :: Uso dell'istruzione for senza un loop body :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int val_max = 100, i = 0;

for (i = 0; i < val_max; i++) // ciclo senza corpo


; // istruzione nulla
printf("i = %d\n", i); // i vale 100!!!

return (EXIT_SUCCESS);
}

Output 5.12 Dal Listato 5.14 ForWithoutBody.c.


i = 100

ATTENZIONE
Quando si utilizza un’istruzione nulla bisogna fare attenzione a non incorrere in un errore di
tipo logico come quello mostrato dallo Snippet 5.7, dove, quando a == -5, il compilatore
esegue l’istruzione nulla e anche quella di stampa del valore 5, che probabilmente non si
aveva intenzione di eseguire. Lo Snippet 5.8 mostra invece un errore in tipo sintattico: in
questo caso il compilatore non troverà un if da associare all’ultimo else perché il primo if
non ha racchiuso le sue istruzioni in un blocco delimitato dalle parentesi graffe { }.

Snippet 5.7 Errore logico con un’istruzione nulla.


int a = -5;
if (a == -5); // ERRORE LOGICO
printf("5\n");

Snippet 5.8 Errore sintattico con un’istruzione nulla.


int a = -5;
if (a == -5);
printf("-5\n");
else // error: 'else' without a previous 'if'
printf("non -5\n");

FOR E WHILE
Come detto, se esiste un’equivalenza tra un costrutto for e un costrutto while, quando usare
l’uno e quando usare l’altro? La risposta, quantunque possa essere legata a un gusto
personale, può anche essere data facendo le seguenti considerazioni di ordine pratico: usare
un’istruzione for quando si ha la necessità di creare dei loop che effettuano delle operazioni di
inizializzazione e aggiornamento di variabili che fungono da contatori; usare un’istruzione
while quando si ha l’esigenza di definire dei loop che devono eseguire le relative operazioni

solo al verificarsi una determinata condizione.


Snippet 5.9 Espressioni del ciclo for come full expression.
int a = 3;
int b = 2;
int j = 1;

for (a++; b < a++ +10; j = ++b)


{
printf("%d - % d - %d\n", a, b, j);
if (a == 7) // interrompi il ciclo altrimenti sarebbe infinito...
break;
}

Analizziamo come il compilatore processa ogni espressione del ciclo for in accordo con
le regole di priorità delle valutazioni delle espressioni e del loop body e avendo la
consapevolezza che ciascuna marca un sequence point (sono infatti delle full expression):
1. a++: al termine della valutazione a avrà il valore 4;
2. b < a++ +10: al termine della valutazione b avrà ancora il valore 2 e a avrà il valore 5;
3. printf("%d - % d - %d\n", a, b, j): saranno stampati i valori: per a 5, per b 2 e per j 1;
4. j = ++b: al termine della valutazione b conterrà il valore 3 così come j;
5. b < a++ +10: al termine della valutazione b avrà ancora il valore 3 e a avrà il valore 6;
6. printf("%d - % d - %d\n", a, b, j): saranno stampati i valori: per a 6, per b 3 e per j 3;
7. j = ++b: al termine della valutazione b conterrà il valore 4 così come j;
8. b < a++ +10: al termine della valutazione b avrà ancora il valore 4 e a avrà il valore 7;
9. printf("%d - % d - %d\n", a, b, j): saranno stampati i valori: per a 7, per b 4 e per j 4. In
più il ciclo si interromperà a causa dell’istruzione break perché a == 7 sarà vera.
Istruzioni di salto
Le istruzioni di salto consentono di trasferire, spostare il controllo dell’esecuzione del
codice in un altro punto e in modo incondizionato, ossia senza la dipendenza da alcuna
espressione di controllo.

Istruzione break
Un’istruzione break consente di interrompere l’esecuzione del codice posto all’interno di
un ciclo espresso mediante le keyword while, do/while e for e contestualmente di trasferirne il
controllo alla successiva istruzione.
Questa istruzione può comparire oltre che all’interno di un loop body anche all’interno di
un costrutto switch, in relazione a una determinata clausola case, dove adempie allo stesso
scopo.

Listato 5.15 Break.c (Break).


/* Break.c :: Uso dell'istruzione break :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
printf("a = ");
for (int a = 1; a <= 10; a++) // finché a <= 10
{
if (a == 5)
{
break;
}
printf("%d ", a);
}
printf("\n");

int a = 1;

printf("a = ");
while (a <= 10) // finché a <= 10
{
if (a == 5)
break;
printf("%d ", a++);
}
printf("\n");

return (EXIT_SUCCESS);
}

Output 5.13 Dal Listato 5.15 Break.c.


a = 1 2 3 4
a = 1 2 3 4

L’istruzione break nel Listato 5.15 interrompe l’iterazione sia del ciclo for sia del ciclo
while; infatti quando la variabile a è uguale a 5 il programma esce dall’iterazione, pertanto
saranno stampati solo i valori fino a 4.
Di fatto, un’istruzione break è utile per interrompere un ciclo “nel mezzo” della sua
esecuzione rispetto a una comune interruzione che potrebbe avvenire all’inizio (nel caso di
un while o di un for dove l’exit point è definito dall’espressione di controllo posta prima o
all’inizio del loop body) oppure alla fine (nel caso di un do/while dove l’exit point è definito
dall’espressione di controllo posta dopo o alla fine del loop body).
Quando si utilizza un’istruzione break è importante tenere presente che essa interrompe
l’esecuzione del codice della “più piccola” istruzione while, for, do/while o switch innestata
(Snippet 5.10).

Snippet 5.10 Istruzione break e cicli innestati.


int a = 0, b = 0, c = 0;

// al termine dell'iterazione dei cicli le variabili a, b e c


// conterranno rispettivamente i valori 10, 10 e 5 e ciò dimostrerà
// che il I e il II ciclo non sono stati interrotti dal break del III ciclo
while (a < 10) // I ciclo
{
a++;
while (b < 10) // II ciclo innestato nel I
{
b++;
while (c < 10) // III ciclo innestato nel II
{
if (c == 5)
break; // interrompe solo il III ciclo!
c++;
}
}
}

Istruzione continue
Un’istruzione continue consente di saltare alla fine di un loop body, evitando il processing
di eventuali istruzioni poste dopo di esse, e di riprendere il prossimo step di esecuzione.
Non esce mai, dunque, dal relativo loop body.
Questa istruzione può comparire solo all’interno di un loop body espresso dalle keyword
while, do/while e for ma mai all’interno di un costrutto switch che non sia innestato in un

costrutto di iterazione dove, in quest’ultimo caso, causa un salto alla fine del loop body dal
costrutto.
Nel caso di un ciclo while e do/while il prossimo step di esecuzione è la valutazione della
relativa espressione di controllo, mentre nel caso di un ciclo for il prossimo step di
esecuzione è la valutazione della terza espressione (in genere quella che aggiorna il valore
di una variabile di controllo) e poi della seconda (in genere quella che verifica una
condizione di terminazione del ciclo).

Listato 5.16 Continue.c (Continue).


/* Continue.c :: Uso dell'istruzione continue :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
printf("a = ");
for (int a = 1; a <= 10; a++) // finché a <= 10
{
if (a == 5) // salta l'istruzione successiva se a == 5
continue;
printf("%d%c ", a, a != 10 ? ',' : ' ');
// continue fa spostare il flusso di esecuzione qui;
// poi il ciclo riprende con a++ quindi a <= 10
}
printf("\n");

int a = 1;

printf("a = ");
while (a <= 10) // finché a <= 10
{
if (a == 5) // salta le istruzioni successive se a == 5
{
a++;
continue;
}
printf("%d%c ", a, a != 10 ? ',' : ' ');
a++;
// continue fa spostare il flusso di esecuzione qui;
// poi il ciclo riprende con a <= 10
}
printf("\n");

return (EXIT_SUCCESS);
}

Output 5.14 Dal Listato 5.16 Continue.c.


a = 1, 2, 3, 4, 6, 7, 8, 9, 10
a = 1, 2, 3, 4, 6, 7, 8, 9, 10

Il Listato 5.16 mostra come nella pratica l’istruzione continue salti le rimanenti istruzioni
del corpo di una struttura iterativa, procedendo quindi con la successiva iterazione.
Nel nostro esempio, nonostante i valori stampati dal for e dal while saranno, ugualmente,
da 1 a 10 eccetto il 5, i due cicli avranno un differente flusso esecutivo in ragione anche di
quanto prima detto sul “dove” il codice riprende l’esecuzione dopo l’esecuzione di
un’istruzione continue.
Infatti, per la sequenza del for avremo quanto segue.

1. Inizializza la variabile a = 1.

2. Controlla se a <= 10 e se lo è va al punto 3, altrimenti va al punto 6.


3. Controlla se a == 5 e se lo è non esegue il punto 4 ma va al punto 5. Se, viceversa, a ==

5 è falsa, allora va al punto 4.


4. Esegue l’istruzione printf di stampa del valore di a.
5. Incrementa a e va al punto 2.
6. Esce dal ciclo.
Per la sequenza del while avremo invece quanto segue.

1. Controlla se a <= 10 e se lo è va al punto 2, altrimenti va al punto 5.


2. Controlla se a == 5 e se lo è non va al punto 3, incrementa a di 1 altrimenti il ciclo
diventa infinito e poi va al punto 1. Se, viceversa, a == 5 è falsa, allora va al punto 3.
3. Esegue l’istruzione printf di stampa del valore di a.
4. Incrementa a e va al punto 1.
5. Esce dal ciclo.
Infine, così come abbiamo visto nel caso di un’istruzione break, anche un’istruzione
continue produrrà i suoi effetti solamente nell’ambito della “più piccola” istruzione while, for
o do/while innestata.
Così, se per esempio abbiamo due cicli for innestati, un’istruzione continue elaborata
all’interno del ciclo for più interno farà spostare il flusso esecutivo del codice alla fine di
tale for e non di quello a esso esterno.

Istruzione goto
Un’istruzione goto permette di saltare verso un punto del codice arbitrario marcato da un
identificatore scritto con una particolare sintassi che lo individua come un’etichetta (label).
L’etichetta e l’istruzione goto devono trovarsi nell’ambito della stessa funzione.
NOTA
A partire dallo standard C99 esiste anche la seguente restrizione all’uso del goto (Snippet
5.11): non può essere usato prima della dichiarazione di un array di lunghezza variabile per
“aggirarla” e trasferire il flusso di esecuzione del codice dopo di essa.

Snippet 5.11 Istruzione goto e VLA: restrizione all’uso.


int val;
int dim = 10;
goto label; // error: jump into scope of identifier with variably modified type
int a[dim];

a[2] = 100;

label: // etichetta
val = 10;

Per quanto attiene quest’istruzione, è bene subito dire che il suo uso è, in linea generale,
sconsigliato e ciò per le seguenti ragioni: può produrre il cosiddetto spaghetti code ossia
codice disordinato e non manutenibile che salta da un punto all’altro del programma, avanti
e indietro e in modo intrecciato; può essere sostituito da forme specializzate e ristrette di
goto quali sono le istruzioni break, continue e return e, alcune volte, anche dalla funzione exit

dichiarata nel file header <stdlib.h>.


Comunque, in ragione della filosofia pragmatica e libertaria di C, i progettisti del
linguaggio hanno ritenuto opportuno lasciare tale istruzione e demandare agli sviluppatori la
scelta se usarla o meno in base alle loro esigenze.
In effetti, a ben pensare, ci sarebbero i seguenti casi (Listato 5.17) dove potrebbe essere
utile e conveniente utilizzare il goto:

per uscire direttamente da una serie di cicli profondamente annidati (ricordiamo,


infatti, che un’istruzione break interrompe solo il ciclo corrente e non un altro in cui
eventualmente lo stesso sia annidato);
per uscire da un ciclo da un’istruzione switch che è in esso annidata.

Listato 5.17 Goto.c (Goto).


/* Goto.c :: Uso dell'istruzione goto :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int a = 0, b = 0, c = 0;

while (a < 10) // I ciclo


{
a++;
while (b < 10) // II ciclo innestato nel I
{
b++;
while (c < 10) // III ciclo innestato nel II
{
if (c == 5)
goto print; // salta direttamente all'etichetta print
c++;
}
}
}

// istruzione etichettata raggiunta dal goto...


print:{ printf("a = %d, b = %d, c = %d\n", a, b, c); c = 1; }
for (;;) // ciclo infinito
{
switch (c)
{
case 1:
case 2:
case 3:
case 4:
case 5: printf("c contiene un valore ancora inferiore a 6...\n"); break;
case 6: goto end; // salta direttamente all'etichetta end
}
c++;
}

end: // etichetta
printf("c contiene finalmente il valore 6!!!\n");

return (EXIT_SUCCESS);
}

Output 5.15 Dal Listato 5.17 Goto.c.


a = 1, b = 1, c = 5
c contiene un valore ancora inferiore a 6...
c contiene un valore ancora inferiore a 6...
c contiene un valore ancora inferiore a 6...
c contiene un valore ancora inferiore a 6...
c contiene un valore ancora inferiore a 6...
c contiene finalmente il valore 6!!!

Il Listato 5.17 definisce tre cicli while, dove il primo dovrebbe iterare finché a è minore di
10, il secondo finché b è minore di 10 e il terzo finché c è minore di 10.
Tuttavia, nel terzo ciclo, quando c è uguale a 5 decidiamo di “interrompere” bruscamente
tutte e tre le iterazioni e trasferire, mediante un’istruzione goto, il flusso di esecuzione del
codice all’etichetta print, la quale stampa a video il valore delle variabili a, b e c.
Poi, definiamo un ciclo for dove, finché il valore della variabile c è inferiore a 6
stampiamo una determinata stringa di caratteri che lo rammenta, mentre quando c è uguale a
6, giacché il ciclo for è infinito, decidiamo tramite goto di uscire direttamente da esso e
passare il controllo del flusso del codice alla label end, la cui istruzione etichettata manderà a
video un’altra stringa informativa.
A parte le eccezioni citate, che in ogni caso è sempre possibile evitare anche se al prezzo
di rendere il codice più complesso (Snippet 5.12), ribadiamo che l’utilizzo del goto non
dovrebbe mai essere preso in considerazione.

Snippet 5.12 Eliminazione dell’istruzione goto.


int c = 1;
for (;;) // ciclo infinito
{
switch (c)
{
case 1:
case 2:
case 3:
case 4:
case 5: printf("c contiene un valore ancora inferiore a 6...\n");
break;
case 6: break;
}
if (c == 6)
break;
else
c++;
}
printf("c contiene finalmente il valore 6!!!\n");

Istruzione return
Un’istruzione return permette di terminare l’esecuzione della corrente funzione e di
ritornare, trasferire il controllo del flusso di esecuzione del codice a un’altra funzione
chiamante.
Eventualmente, tale istruzione può anche ritornare al chiamante un valore che, prima di
essere restituito, è convertito nel tipo di ritorno esplicitato all’atto di dichiarazione della
funzione medesima.
NOTA
Un dettaglio significativo su tale istruzione sarà fornito nel Capitolo 6 e ciò perché una sua
piena comprensione potrà essere raggiunta solo dopo aver affrontato le funzioni. È apparso
comunque opportuno citarla in questa sede perché un’istruzione return è categorizzata
come un’istruzione di salto, e dunque è qui che il lettore ne deve avere una prima
indicazione sia termologica sia semantica, per quanto in modo breve e sommario.
Istruzioni etichettate
Le istruzioni etichettate rappresentano delle statement che sono precedute da apposite
etichette (label) definibili attraverso le seguenti sintassi.

Sintassi 5.9 Etichetta identificatore.


identifier: statement

La Sintassi 5.9 definisce un’etichetta attraverso l’indicazione di un identificatore seguito


dal carattere due punti (:) e il suo unico caso di utilizzo è con l’istruzione goto, per la quale
rappresenta un target, ossia una destinazione.

Sintassi 5.10 Etichetta case.


case integer_constant_expression: statement

Sintassi 5.11 Etichetta default.


default: statement

La Sintassi 5.10 e la Sintassi 5.11 definiscono le comuni etichette case e default già viste e
trattate nell’ambito dell’istruzione di selezione multipla switch che, ricordiamo, è l’unico
costrutto all’interno del quale è possibile utilizzarle.
Capitolo 6
Funzioni

Una funzione è un blocco di codice contenente dichiarazioni e istruzioni, deputata a


eseguire solitamente una determinata azione, per esempio stampare a video il valore degli
elementi di un array, oppure uno specifico algoritmo computazionale al fine di ritornare un
risultato, come può essere quello che prevede, dato un numero, il calcolo del suo fattoriale.
Una funzione è un costrutto sintattico di notevole importanza in un linguaggio di
programmazione perché consente di ottenere i seguenti benefici.
Modularità: un programma può essere scomposto in piccole unità di elaborazione, che
da questo punto di vista agiscono, per l’appunto, come moduli, ciascuna avente un
preciso e isolato “scopo” algoritmico. La possibilità di costruire un programma come
un insieme di tante unità di computazione indipendenti porta con sé anche un altro
importante vantaggio che è legato a una migliore e più facile manutenibilità del codice
nel suo complesso. Per esempio, se il programmatore di una funzione che calcola il
fattoriale di un numero trova un algoritmo più efficiente per produrre quel risultato,
dovrà semplicemente modificare il codice all’interno della relativa funzione piuttosto
che andare a trovare quelle parti di programma che, in modo sparso, ne fanno uso.
Riuso: ogni funzione, una volta scritta e testata adeguatamente, può essere riutilizzata
nei programmi che necessitano delle sue funzionalità. Questo beneficio è fondamentale
perché, di fatto, consente di evitare di scrivere ex novo del codice che magari è già
stato prodotto da altri sviluppatori e dunque impiegabile senza particolari problemi o
sforzi nei nostri programmi.
Mancanza di codice duplicato: se non esistesse il costrutto di funzione, un
programmatore sarebbe costretto a scrivere lo stesso pezzo di codice che la rappresenta
in più parti del programma che ne richiedono i servizi. Per esempio, ritornando al caso
della funzione che calcola il fattoriale di un numero, se non esistesse la possibilità di
definire un’apposita funzione, dovremmo, ogni volta che avessimo bisogno di quel
calcolo, scrivere sempre le stesse istruzioni algoritmiche, con ciò creando inutili
duplicati di quel codice. Con il costrutto di funzione, invece, sarebbe sufficiente, nelle
parti di programma che desiderano ottenere il fattoriale di un numero, invocare quella
funzione, ossia “chiamarla per nome” e attendere che produca il risultato atteso.
Occultamento algoritmico: una funzione può essere vista come una sorta di “scatola
chiusa” deputata a fare qualcosa che, nel contempo, “nasconde” come la fa. Al
programmatore, infatti, non deve interessare come l’algoritmo di una certa funzione sia
stato prodotto ma deve interessare solo cosa fa la stessa. Ciò consente di pensare alla
strutturazione di un programma più in termini di design che in termini di dettagli
implementativi, facilitandone la progettazione complessiva.
Possiamo dunque sintetizzare affermando che una funzione è un costrutto che permette di
scrivere i programmi in modo strutturato, con codice più facilmente modificabile, leggibile
e riutilizzabile e che nasconde i dettagli implementativi non necessari per il suo corretto
utilizzo.
TERMINOLOGIA
Ogni linguaggio di programmazione può adottare una certa terminologia e semantica per
designare un “insieme di istruzioni che definiscono una determinata computazione”, che in
modo generale sono indicate, in letteratura, come subprograms (sotto-programmi). I termini
più comuni sono le funzioni, le procedure, le subroutine e i metodi, e le differenze tra di essi
sono legate ai linguaggi di programmazione e a come hanno inteso definire tali sotto-
programmi. Per esempio, un metodo è un termine utilizzato nei linguaggi orientati agli
oggetti e designa un sotto-programma che è associato a un particolare oggetto o classe;
una procedura è un termine utilizzato nei linguaggi procedurali; per esempio, in Pascal
designa un sotto-programma che non ritorna un valore, mentre in C non ha alcun
significato, ed è infatti presente il solo costrutto di funzione (che può o non può ritornare un
valore). Ancora, in FORTRAN, una subroutine, rispetto a una funzione, è un sotto-
programma che può ritornare due o più valori (o nessuno).
Definizione di una funzione
Una funzione è un costrutto di programmazione del linguaggio C e come tale segue delle
precise regole sintattiche per la sua definizione (Sintassi 6.1).

Sintassi 6.1 Definizione di una funzione.


return_type function_identifier(void | parameters_list)
{
declarations;
statements;
[return expression];
}

Abbiamo: return_type, che indica il tipo di dato ritornato dalla funzione al suo chiamante,
che non può essere però un tipo funzione o un tipo array (in C89 è possibile omettere il tipo
di ritorno, e allora si presume che esso sia di tipo int, mentre a partire da C99 è obbligatorio
porre sempre un tipo di ritorno); function_identifier, che indica il nome della funzione; una
coppia di parentesi tonde ( ) al cui interno porre una lista di variabili precedute dal tipo e
separate dal carattere virgola (,) che indicano nell’insieme i parametri della funzione,
oppure la keyword void, che indica che la funzione non ha parametri; una coppia di
parentesi graffe { } al cui interno porre le consuete dichiarazioni di variabili, costanti e altro,
le istruzioni che rappresentano l’algoritmo computazionale della funzione e un’opzionale
istruzione di salto return se la funzione deve ritornare un valore.
Per quanto riguarda gli elementi strutturali della definizione di una funzione è importante
fare le seguenti ulteriori precisazioni.
Un tipo funzione (function type) è un tipo caratterizzato dal suo tipo di ritorno e dal
numero e il tipo dei suoi parametri. Così potremmo dire che in una funzione definita
come int foo(double a, double b) {/*... */} la valutazione dell’espressione foo sarebbe
di tipo (double, double) -> int, ovvero di una funzione che ritorna un int e ha due
parametri double.

CURIOSITÀ
L’identificatore foo è utilizzato nella letteratura informatica per indicare un nome generico e
non significativo da attribuire a variabili, funzioni e così via in porzioni di codice sorgente che
hanno l’unico scopo di illustrare dei concetti didattici. Oltre all’identificatore foo sono utilizzati
gli identificatori bar, baz e foobar.

Il fatto che a partire da C99 sia illegale tralasciare il tipo di ritorno non implica che un
compilatore non debba permettere di compilare correttamente il relativo codice
sorgente. In ogni caso, in accordo con lo standard, quando un compilatore che è
un’implementazione conforme incontra una violazione di sintassi oppure di restrizione
sintattica o semantica (constraint), deve produrre, all’atto della compilazione, almeno
un messaggio di diagnostica che può essere espresso in modo arbitrario. Nel caso di
GCC se compiliamo con il flag -std=c99 o -std=c11 un sorgente che contiene la
definizione di una funzione senza il tipo di ritorno, lo stesso ritornerà solo un
messaggio di diagnostica come warning: return type defaults to 'int' permettendo
comunque la compilazione del predetto sorgente. Tuttavia, anche se il programma è
stato compilato, bisognerà prestare attenzione a quel warning perché il comportamento
dell’eseguibile sarà non definito. La morale di quanto detto è che i messaggi di
diagnostica di un compilatore non vanno mai trascurati, anche se sono dei “semplici” e
apparentemente “innocui” warning.
Un parametro, detto parametro formale (formal parameter), è un oggetto dichiarato
nell’ambito della dichiarazione o della definizione di una funzione che è deputato a
ricevere un valore nel momento dell’invocazione della funzione medesima. Questo
parametro ha l’importante caratteristica di essere una variabile locale alla funzione
dove è stato dichiarato, ossia è visibile e utilizzabile solamente nel suo ambito. Al di
fuori della funzione nessun’altra funzione potrà accedere a tale variabile e ciascuna
funzione potrà avere una variabile dichiarata con lo stesso nome.

TERMINOLOGIA
Se una variabile è dichiarata al di fuori di ogni funzione, la stessa sarà utilizzabile
direttamente da ciascuna di esse. Questa variabile è detta variabile esterna (o globale).
Ritorneremo su questo punto, in modo più preciso e con una terminologia più corretta e
appropriata quando tratteremo l’importante concetto dello scope delle variabili nel Capitolo
9.

Data la funzione int foo(double a, double b) {/*... */} per intestazione della funzione
(function header) si intende la parte composta dal tipo di ritorno, il nome della
funzione e la lista di parametri (tipo più identificatore), mentre per dichiaratore di
funzione (function declarator) si intende la parte composta dal nome della funzione e
la lista dei parametri (tipo più identificatore).
Lo Snippet 6.1 mostra come definire una semplice funzione che dato un numero ne
calcola il cubo, ossia la sua terza potenza.

Snippet 6.1 Definizione della funzione cube.


...
// definizione della funzione cube
long cube(long number)
{
long res; // variabile locale e privata alla funzione cube
res = number * number * number; // algoritmo
return res; // ritorna al chiamante il risultato della computazione
}

int main(void)
{
...
}

NOTA
In alcuni sorgenti è ancora possibile trovare definizioni di funzioni espresse con una sintassi
definita pre-ANSI C (Sintassi 6.2), dove per la parte dei parametri si indicano solo i relativi
identificatori mentre subito dopo e prima dell’inizio del body della funzione, per ogni
parametro, si indica anche il relativo tipo di dato (Snippet 6.2). È importante segnalare che
questa forma di definizione di una funzione è ancora presente nell’attuale standard C11, ma
la stessa è marcata come obsolescent feature ossia, quantunque la si possa ancora
utilizzare, ne è comunque deprecato l’impiego.

Sintassi 6.2 Definizione di una funzione secondo una sintassi pre-ANSI C.


[return_type] function_identifier([identifier_list])
data_type identifier_1;
data_type identifier_N;
{
declarations;
statements;
[return expression];
}

Snippet 6.2 Definizione della funzione cube secondo una sintassi pre-ANSI C.
...
// definizione della funzione cube secondo una sintassi pre-ANSI C
long cube(number) // number è l'identificatore
long number; // dichiarazione del tipo
{
long res; // variabile locale e privata alla funzione cube
res = number * number * number; // algoritmo
return res; // ritorna al chiamante il risultato
}

int main(void)
{
...
}
Invocazione di una funzione
Dopo la definizione di una funzione è necessario conoscere la corretta sintassi da
adoperare al fine di utilizzarne i servizi offerti (Sintassi 6.3).

Sintassi 6.3 Utilizzo di una funzione.


function_identifier([argument_list]);

In pratica si deve scrivere, tramite function_identifier, il nome della funzione da utilizzare


cui far seguire l’operatore di invocazione di funzione espresso da una coppia di parentesi
tonde ( ) al cui interno bisogna inserire una lista di argomenti, ossia una serie di espressioni
(variabili, costanti, espressioni più complesse e altro) separate dal carattere virgola atte a
fornire un valore al corrispondente parametro formale.
Questi argomenti, denominati formalmente dallo standard argomenti attuali (actual
argument), sono opzionali, ovvero devono essere presenti solo se sono presenti i
corrispondenti parametri altrimenti, se una funzione è stata definita senza parametri
l’invocatore di funzione può essere vuoto.
Un’invocazione di funzione consente quindi di “chiamare” una funzione al fine di farne
eseguire i compiti e, se previsto, le passa contestualmente dei valori che saranno copiati nei
corrispettivi parametri e che saranno impiegati dalla funzione medesima per portare a
termine l’elaborazione prevista.
Al termine della predetta elaborazione, che può avvenire per effetto di un’apposita
istruzione return oppure per il raggiungimento della parentesi graffa } di chiusura del body
della funzione stessa, il flusso di processing del codice riprende dall’istruzione successiva a
quella dell’invocazione eseguita nella funzione chiamante.

Listato 6.1 FunctionInvocation.c (FunctionInvocation).


/* FunctionInvocation.c :: Mostra come invocare una funzione :: */
#include <stdio.h>
#include <stdlib.h>

// definizione della funzione cube


long cube(long number)
{
long res; // variabile locale e privata alla funzione cube
res = number * number * number; // algoritmo
return res; // ritorna al chiamante il risultato
}

int main(void)
{
long number;

printf("Digita un numero cui far calcolare il suo cubo tra -1000 e 1000: ");
scanf("%ld", &number);
while (number < -1000 || number > 1000)
{
printf("\007Il numero deve essere compreso tra -1000 e 1000!\n");
printf("Digita un numero cui far calcolare il suo cubo tra -1000 e 1000: ");
scanf("%ld", &number);
}

printf("Il cubo di %ld e' %ld\n", number, cube(number));

return (EXIT_SUCCESS);
}

Output 6.1 Dal Listato 6.1 FunctionInvocation.c.


Digita un numero cui far calcolare il sui cubo tra -1000 e 1000: -2000
Il numero deve essere compreso tra -1000 e 1000!
Digita un numero cui far calcolare il suo cubo tra -1000 e 1000: 50
Il cubo di 50 e' 125000

Il Listato 6.1 definisce la funzione cube che ritorna il cubo di un numero e poi, nell’ambito
della funzione main (funzione chiamante), la invoca solo se un numero immesso da tastiera è
compreso tra -1000 e 1000.
Nel nostro caso, quando cube è invocata, la variabile number, posta tra l’operatore di
invocazione, rappresenta l’argomento il cui valore è copiato nel corrispondente parametro
anch’esso denominato number (ricordiamo che non v’è conflitto di nomi tra variabili che si
chiamano allo stesso modo tra funzioni differenti).
Dopo l’invocazione, il controllo del flusso del codice passa all’interno della funzione cube
la quale utilizza il valore di number per calcolarne il cubo e poi tramite l’istruzione return, ne
termina il compito elaborativo passando al chiamante tale risultato (nel punto del main dove
cube è stata invocata), questo è utilizzato direttamente (non è posto in alcuna variabile
“d’appoggio”) come valore del terzo argomento della funzione printf che lo impiegherà
visualizzandolo a video.
Essendo anche printf una funzione, al termine della sua elaborazione il flusso di
processing del codice riprenderà, nel main chiamante, dall’istruzione successiva alla sua
invocazione, ossia verrà elaborata l’istruzione return (EXIT_SUCCESS).
Dichiarazione di una funzione
Nel programma appena mostrato possiamo notare come la definizione della funzione cube
sia posta prima della definizione della funzione main; questo perché il compilatore prima di
utilizzarla ha la necessità di sapere della sua esistenza ossia di come è strutturata in merito
al tipo di ritorno, al suo nome e agli eventuali parametri (numero e tipo).
In ogni caso nessuno ci vieta di porre la definizione della funzione cube in un altro punto
del programma, per esempio dopo la funzione main (Listato 6.2).

Listato 6.2 FunctionDefinitionAfterMain.c (FunctionDefinitionAfterMain).


/* FunctionDefinitionAfterMain.c :: Definizione di cube dopo main :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
long res = cube(5); /* dichiarazione implicita... */
printf("%ld\n", res);
return 0;

return (EXIT_SUCCESS);
}

/* definizione della funzione cube */


long cube(long number)
{
long res; /* variabile locale e privata alla funzione cube */
res = number * number * number; /* algoritmo */
return res; /* ritorna al chiamante il risultato */
}

Se proviamo a compilare il relativo sorgente il compilatore gcc visualizzerà un errore:


FunctionDefinitionAfterMain.c: In function 'main':
FunctionDefinitionAfterMain.c:7:5: warning: implicit declaration of function 'cube' [-
Wimplicit-function-declaration]
long res = cube(5);
^
FunctionDefinitionAfterMain.c: At top level:
FunctionDefinitionAfterMain.c:15:6: error: conflicting types for 'cube'
long cube(long number) // identificatore
^
FunctionDefinitionAfterMain.c:7:16: note: previous implicit declaration of 'cube' was here
long res = cube(5);

In pratica quando il compilatore incontra nella funzione main la funzione cube, non
sapendo nulla in merito alla sua struttura, prende una decisione in autonomia “effettuando”
una dichiarazione implicita della stessa attribuendole come tipo di ritorno un tipo int e non
assumendo nulla in merito al tipo e al numero dei parametri (la dichiarazione implicita è
qualcosa come int cube();).
In seguito, inoltre, quando incontra la definizione di cube rileva che la stessa ritorna un
tipo long che è diverso dal tipo int attribuito nella dichiarazione implicita, e pertanto genera
un messaggio di errore che ci avvisa di un conflitto di tipi.
NOTA
A partire da C99 un compilatore dovrebbe generare un messaggio di diagnostica di una
dichiarazione di funzione implicita (implicit function declaration) perché tale possibilità,
legale nei compilatori che aderivano agli standard precedenti, è diventata scorretta. Se,
infatti, rimuoviamo dal sorgente la definizione della funzione cube e lo compiliamo con gcc
(flag -std=c11), lo stesso genererà il messaggio warning: implicit declaration of function

'cube', che invece non sarà generato se gcc è compilato il flag -std=c89.

Ciò detto, il linguaggio C fornisce un potente meccanismo che consente di indicare solo il
function header di una funzione prima del suo utilizzo in modo che il compilatore abbia in
anticipo tutte le informazioni necessarie per controllare la correttezza dell’invocazione
anche se la relativa definizione è posta altrove, ossia dopo la sua effettiva chiamata.
Questo meccanismo consente di creare il cosiddetto prototipo di funzione (function
prototype) che è rappresentato, in sostanza, da una dichiarazione esplicita di una funzione
(Sintassi 6.4) dove sono indicati il tipo di ritorno, il suo nome, il tipo dei parametri e,
opzionalmente, anche il loro nome.
CONSIGLIO
Anche se è possibile scrivere un prototipo di funzione omettendo il nome dei parametri, è
buona norma non farlo perché tali nomi garantiscono sicuramente una migliore
documentazione del codice sorgente.

NOTA
Se si decide di scrivere il nome dei parametri in un prototipo di funzione, gli stessi possono
essere diversi da quelli indicati nella corrispettiva definizione.

TERMINOLOGIA
Dato un prototipo di funzione, diremo che per firma o segnatura di una funzione si intendono
le informazioni riguardanti il suo tipo di ritorno e il tipo dei suoi parametri. Per esempio, un
prototipo come double foo(int); indicherà una funzione con una segnatura con un tipo di
ritorno double e un parametro di tipo int.

Sintassi 6.4 Prototipo di funzione.


return_type function_identifier(void | parameters_list);

Avendo questa possibilità possiamo, dunque, riscrivere il nostro sorgente come nel
Listato 6.3 ed essere certi che non vi sarà più alcun errore di compilazione, perché non vi
sarà più alcun conflitto tra i tipi.

Listato 6.3 FunctionPrototype.c (FunctionPrototype).


/* FunctionPrototype.c :: Definizione di un prototipo di funzione :: */
#include <stdio.h>
#include <stdlib.h>

/* prototipo di funzione di cube: dichiarazione esplicita */


long cube(long number);

int main(void)
{
long res = cube(5);
printf("Il cubo di 5 e': %ld\n", res);
return (EXIT_SUCCESS);
}

/* definizione della funzione cube */


long cube(long number)
{
long res; /* variabile locale e privata alla funzione cube */
res = number * number * number; /* algoritmo */
return res; /* ritorna al chiamante il risultato */
}

Output 6.2 Dal Listato 6.3 FunctionPrototype.c.


Il cubo di 5 e': 125

NOTA
È ancora possibile, per ragioni di compatibilità, scrivere la dichiarazione di una funzione non
indicando i parametri come era in uso nel pre-ANSI C, dove non era consentita l’indicazione
di una lista di parametri [per esempio long cube();]. Tuttavia, anche se tale forma di
dichiarazione è ancora ammessa, se ne scoraggia vivamente l’impiego perché il
compilatore non rileva alcun errore in merito al numero e al tipo degli argomenti passati
all’atto dell’invocazione della relativa funzione.

IMPORTANTE
Per dichiarare il prototipo di una funzione che non accetta argomenti (non ha parametri) non
utilizzare mai qualcosa come void foo();, ma impiegare esplicitamente la keyword void come
in void foo(void). Infatti, solo in quest’ultimo caso un compilatore verificherà se
effettivamente durante l’utilizzo dell’operatore di invocazione non si saranno forniti
argomenti e in caso contrario lo segnalerà come errore (per esempio error: too many
arguments to function 'foo').
Parametri di una funzione: dettaglio
Come detto, una funzione può avere dei parametri che, di fatto, sono delle variabili a essa
locali atte a contenere dei valori forniti durante la sua invocazione dai corrispondenti
argomenti.
I valori forniti dagli argomenti sono però delle loro copie, ossia i relativi parametri
potranno agire su di essi senza la preoccupazione che qualsiasi manipolazione
eventualmente prodotta abbia effetto sui valori originari; detto in altri termini, ogni modifica
effettuata sul parametro non si ripercuoterà sul corrispondente argomento.
Ciò accade perché i valori degli argomenti sono assegnati (copiati) in variabili
“temporanee”, ossia i parametri, ed è tramite questi e non tramite gli argomenti che gli stessi
valori possono subire cambiamenti.
Questa modalità di passaggio degli argomenti a una funzione è detta per valore (by value)
e permette proprio di evitare che una funzione chiamata alteri direttamente una variabile di
una funzione chiamante (può solo modificarne una “copia” privata e temporanea).
IMPORTANTE
In C gli argomenti sono sempre passati by value. Il fatto che sia possibile modificare, come
vedremo poi, un argomento tramite un parametro grazie ai puntatori non comporta che
automaticamente esista un’altra modalità di passaggio (per esempio by pointer o by
reference).

Listato 6.4 ByValue.c (ByValue).


/* ByValue.c :: Passaggio degli argomenti per valore :: */
#include <stdio.h>
#include <stdlib.h>

/* prototipo di swap */
void swap(int a, int b);

int main(void)
{
int a = 10, b = 20;
printf("a e b prima dello swap: a=%d - b=%d\n", a, b);

swap(a, b); /* swap di a e b */

printf("a e b dopo lo swap: a=%d - b=%d\n", a, b);

return (EXIT_SUCCESS);
}

/* definizione di swap */
void swap(int w, int z) /* ATTENZIONE gli argomenti non sono modificati!!! */
{
int tmp = w;
w = z;
z = tmp;
}

Output 6.3 Dal Listato 6.4 ByValue.c.


a e b prima dello swap: a=10 - b=20
a e b dopo lo swap: a=10 - b=20

Il Listato 6.4 definisce una funzione swap il cui scopo è quello di scambiare il valore delle
due variabili passate come argomento ossia, nel nostro caso, di assegnare alla variabile a il
valore della variabile b (ossia 20) e alla variabile b il valore della variabile a (ossia 10).
Tuttavia, poiché in C gli argomenti sono sempre passati by value, la funzione swap riesce
a scambiare solo i valori delle copie di a e b che sono rappresentate dalle variabili w e z, le
quali sicuramente avranno i valori scambiati (w avrà il valore 20 e z il valore 10).
La Figura 6.1 aiuterà a capire meglio cosa significa passare gli argomenti per valore con
una rappresentazione grafica delle variabili a e b prima dell’invocazione di swap e poi delle
stesse dopo l’invocazione di swap. Mostra altresì come le variabili w e z, copie di a e b,
eseguano correttamente lo scambio tra di esse.

Figura 6.1 Le variabili a, b, w e z e il passaggio degli argomenti by value.

ARGOMENTI E SEQUENCE POINT


In accordo con lo standard C, quando si invoca una funzione, vi è un sequence point tra la
valutazione di un designatore di funzione e gli argomenti attuali e l’effettiva invocazione
(Snippet 6.3). Ciò significa che tale sequence point è definito prima dell’ingresso effettivo nella
funzione chiamata, e vi è dunque la garanzia che le valutazioni degli argomenti ed eventuali
side-effect siano portati tutti a termine. Allo stesso tempo è anche importante dire che,
indipendentemente dai sequence point, l’ordine di valutazione degli argomenti di una funzione
non è specificato perché nell’ambito della loro indicazione il carattere virgola (,) è trattato
come un mero separatore e non come un operatore laddove, rammentiamo, solo in
quest’ultimo caso è definito un sequence point ed è dunque garantito un ordine di valutazione
da sinistra a destra.

Snippet 6.3 Argomenti e sequence point.


...
// alcuni prototipi...
int F(void);
int G(void);
int H(void);

void foo(int, int);

int main(void)
{
// in questo caso l'ordine di precedenza degli operatori farà si che
// ciò che sarà ritornato da G() sarà moltiplicato per ciò
// che sarà ritornato da H() e tale risultato sarà poi addizionato
// a ciò che sarà ritornato da H();
// tuttavia il compilatore potrà scegliere qualsiasi ordine di valutazione
// degli operandi ossia non è detto che invocherà nell'ordine, come abbiamo scritto,
// prima F() poi G() e poi H();
// questo può essere un problema se una delle funzioni, per esempio F,
// manipola un oggetto globale che poi è riferito da un'altra funzione,
// per esempio G, soprattutto se quest'altra funzione faceva affidamento
// su quella manipolazione;
// infatti, se viene prima valutata G di F che valore avrà quell'oggetto per G?
// ritornando al risultato di questa printf un compilatore potrà ritornare:
// F
// G
// H
// 7
// mentre un altro potrà ritornare:
// G
// H
// F
// 7
printf("%d\n", F() + G() * H()); // ???

int j = 10;
int k = 11;
int l = 100;

// sequence point prima dell'ingresso nella funzione;


// questo garantirà che j sarà incrementato, e a k sarà assegnato il
// valore di l e dunque i parametri formali nel body della funzione
// conterranno, rispettivamente, i valori 11 e 100
foo(++j, k = l);

return (EXIT_SUCCESS);
}

// definizione di F
int F(void)
{
printf("F\n");
return 1;
}
// definizione di G
int G(void)
{
printf("G\n");
return 2;

}
// definizione di H
int H(void)
{
printf("H\n");
return 3;
}
// definizione di foo
void foo(int a, int b)
{
printf("[a = %d] [b = %d]\n", a, b); // [a = 11] [b = 100]
}
Conversione e promozione degli argomenti
Quando si invoca una funzione può certamente accadere che la stessa sia chiamata con
degli argomenti il cui tipo non concorda con il tipo dei corrispondenti parametri.
In questo caso il compilatore può eseguire due distinte azioni dipendenti dal fatto se è
stato dichiarato o meno un prototipo della funzione che si sta invocando oppure se è stata
scritta una dichiarazione nel vecchio stile pre-ANSI C.
Nel primo caso, ossia se è presente un prototipo di funzione, il tipo degli argomenti è
implicitamente convertito, come per assegnamento, nel tipo dei parametri, mentre nel
secondo caso, ovvero se non è presente un prototipo di funzione oppure è presente una
dichiarazione pre-ANSI C, è effettuata una promozione di default degli argomenti (default
argument promotion) dove gli argomenti di tipo float sono convertiti nel tipo double e gli
argomenti di tipo _Bool, char e short sono convertiti nel tipo int (integer promotion).
In quest’ultimo caso, se il numero di argomenti non è uguale al numero dei parametri
oppure se il tipo dell’argomento promosso non concorda con il tipo del parametro, il
comportamento dell’invocazione della funzione sarà non definito.

Listato 6.5 DefaultArgumentPromotion.c (DefaultArgumentPromotion).


/* DefaultArgumentPromotion.c :: Promozione di default degli argomenti :: */
#include <stdio.h>
#include <stdlib.h>

/* nessun prototipo della funzione sum */


/* il compilatore assume di default che sia int sum(); */

int main(void)
{
/* invocazione di sum con un solo argomento */
/* numero degli argomenti non uguale al numero dei parametri */
int res_1 = sum(6);
printf("Somma tra 6 e ? = %d\n", res_1);

/* invocazione di sum con i tipi degli argomenti diversi dai tipi dei parametri */
/* il tipo degli argomenti promossi non concorda con il tipo dei parametri attesi */
int res_2 = sum(6.7f, 7.8f);

printf("Somma tra 6.7 e 7.8 = %d\n", res_2);

return (EXIT_SUCCESS);
}

/* definizione di sum */
int sum(int x, int y)
{
return x + y;
}

Output 6.4 Dal Listato 6.5 DefaultArgumentPromotion.c.


Somma tra 6 e ? = 4201558
Somma tra 6.7 e 7.8 = 216504729

Il Listato 6.5 evidenzia come in assenza di un prototipo di funzione per la funzione sum,
quando nel main si invoca sum con un solo argomento così come quando lo si invoca con
degli argomenti che non corrispondono al tipo atteso dai parametri, il risultato sia non
definito ossia, nel nostro caso, privo di senso.
Infatti, nella prima invocazione avremo come risultato la somma tra 6, che sarà il valore
assegnato alla variabile x e 4201552, che sarà un valore arbitrario che in quel momento si
troverà alla locazione di memoria della variabile y.
Nella seconda invocazione, invece, gli argomenti 6.7f e 7.8f di tipo float saranno sì
convertiti nel tipo double, ma poiché la funzione si attendeva degli int la somma avverrà con
i valori arbitrari -858993459 e 1075498188 che si trovavano alle rispettive locazioni di memoria
“di tipo intero” dei parametri x e y.
In definitiva, per entrambe le invocazioni l’esecuzione della funzione sum darà un esito
non definito e un risultato non congruo con quanto atteso.

Listato 6.6 ArgumentConversion.c (ArgumentConversion).


/* ArgumentConversion.c :: Conversione degli argomenti :: */
#include <stdio.h>
#include <stdlib.h>

/* prototipo della funzione sum */


int sum(int x, int y);

int main(void)
{
/* errore di compilazione: meno argomenti di quelli attesi */
int res_1 = sum(6);
printf("Somma tra 6 e ? = %d\n", res_1);

/* gli argomenti sono convertiti nel tipo int */


/* potremmo avere solo un warning di possibile perdita di dati */
int res_2 = sum(6.7f, 7.8f);

printf("Somma tra 6.7 e 7.8 = %d\n", res_2);

return (EXIT_SUCCESS);
}

/* definizione di sum */
int sum(int x, int y)
{
return x + y;
}

Il Listato 6.6 mostra come la dichiarazione di un prototipo di funzione consenta di


scrivere programmi più robusti perché il compilatore effettuerà una verifica di congruità tra
il numero e il tipo dei parametri del prototipo con il numero e il tipo degli argomenti forniti
all’atto dell’invocazione della funzione.
Se infatti invochiamo sum con un solo argomento il compilatore genererà il messaggio di
errore error: too few arguments to function 'sum', mentre quando invochiamo sum con i due
valori di tipo float automaticamente gli stessi saranno convertiti nel tipo int dei parametri e
al massimo, se previsto, genererà un messaggio di una possibile perdita di informazioni (per
esempio con gcc e il flag -Wconversion verrà generato il messaggio warning: conversion to

'int' alters 'float' constant value).

Verifichiamo quanto detto commentando le istruzioni che generano un errore e


mandando in esecuzione il sorgente.

Output 6.5 Dal Listato 6.6 ArgumentConversion.c.


Somma tra 6.7 e 7.8 = 13

Dall’Output 6.6 si evince con chiarezza che la somma tra 6.7 e 7.8 ha dato come risultato
il valore 13, che è un valore intero perché, ribadiamo, la conversione automatica tra float a
int ne ha fatto perdere informazioni, ossia la parte frazionaria.

Parametri di tipo array


Una funzione può dichiarare come suoi parametri formali anche degli oggetti che sono di
tipo array sia nella forma monodimensionale che in quella multidimensionale, così come
degli oggetti che sono dei VLA (variable length array), ossia array di lunghezza variabile.

Parametri come array monodimensionali


Per gli array a una dimensione presentiamo le Sintassi 6.5 e 6.6, che mostrano come
dichiararli correttamente nell’ambito di un prototipo e di una definizione di funzione, e la
Sintassi 6.7 che mostra come utilizzarli, ossia come passare un argomento di tipo array a un
parametro corrispondente.

Sintassi 6.5 Dichiarazione di un parametro di tipo array: prototipo di funzione.


return_type function_identifier (data_type []);
return_type function_identifier (data_type identifier []); // equivalente

Sintassi 6.6 Dichiarazione di un parametro di tipo array: definizione di una funzione.


return_type function_identifier (data_type identifier[])
{
...
}

In pratica, per dichiarare un parametro di tipo array a una dimensione è sufficiente


indicare il tipo di dato degli elementi ivi contenuti e la consueta coppia di parentesi quadre [
], mentre non è obbligatorio indicare la dimensione dell’array in quanto anche se presente
sarà ignorata dal compilatore (non sarà utilizzata, per esempio, per verificare se l’argomento
di tipo array passato ha effettivamente solo quella esatta quantità di elementi).

Snippet 6.4 Dichiarazione del prototipo della funzione subtraction con un parametro di tipo array.
/* prototipo della funzione subtraction */
int subtraction(int data[]);

Snippet 6.5 Definizione della funzione subtraction con un parametro di tipo array.
/* definizione della funzione subtraction */
int subtraction(int data[])
{
...
}

Sintassi 6.7 Invocazione di una funzione passando un argomento di tipo array.


function_identifier (identifier);

Per invocare una funzione che richiede un argomento di tipo array lo stesso deve essere
fornito indicando solamente il nome dell’array, senza l’apposizione delle parentesi [ ].

Snippet 6.6 Invocazione della funzione subtraction con un argomento di tipo array.
...
int main(void)
{
int some_data[] = {1, 2, 3, 5};
int res = subtraction(some_data);
...
}

Il Listato 6.7 elabora un programma che utilizza la funzione subtraction che, dato un array
come argomento, fornisce un risultato che è dato dalla sottrazione dei valori di tutti i suoi
elementi.

Listato 6.7 OneDimArrayAsParameter.c (OneDimArrayAsParameter).


/* OneDimArrayAsParameter.c :: Array a una dimensione come parametro :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 6

/* prototipo della funzione subtraction */


int subtraction(int data[], int length);

int main(void)
{
int some_data[] = {369, 10, 15, 65, 88, 66};

int res = subtraction(some_data, SIZE);

printf("Il risultato della sottrazione di tutti gli elementi di some_data "


"e': %d\n", res);

return (EXIT_SUCCESS);
}

/* definizione della funzione subtraction */


int subtraction(int data[], int length)
{
int result = data[0];

for (int i = 1; i < length; i++)


result -= data[i];

return result;
}

Output 6.6 Dal Listato OneDimArrayAsParameter.c.


Il risultato della sottrazione di tutti gli elementi di some_data e': 125
Del programma presentato è importare rilevare soprattutto un aspetto: sia nella
dichiarazione del prototipo di subtraction sia nella sua definizione è stato utilizzato un
parametro in più atto a contenere il numero di elementi contenuti nell’array relativo che
rappresenta un dato fondamentale per processarlo correttamente.
Ciò si rende necessario perché, in C, dato un array come parametro di una funzione, non
è possibile in quell’ambito conoscerne la sua lunghezza “interrogandolo”, per esempio, con
l’operatore sizeof (Snippet 6.7).

Snippet 6.7 Operatore sizeof: array come variabile e array come parametro di funzione.
...
int main(void)
{
int some_data[] = {369, 10, 15, 65, 88, 66};

// dim = 6. OK dimensione corretta di some_data


int dim = sizeof some_data / sizeof some_data[0];
...
}

/* definizione della funzione subtraction */


int subtraction(int data[], int length)
{
// dim = 1. ERRORE dimensione non corretta di data
int dim = sizeof data / sizeof data[0];
...
}

Il motivo dell’apparente discrasia di comportamento dell’operatore sizeof quando opera


su un array che è una variabile rispetto a un array che è un parametro di una funzione, è da
ricercarsi nella profonda relazione che vi è in C tra gli array e i puntatori e che sarà
adeguatamente illustrata nel Capitolo 7.
Per ora, basti prendere così com’è la spiegazione che un parametro di tipo array di una
funzione “non è” un mero array ma è piuttosto un “puntatore” al primo elemento dell’array
passato come argomento, e dunque l’operatore sizeof determina la dimensione di un oggetto
di tipo puntatore (sul corrente sistema il puntatore data è di 4 byte così come un tipo int, ed
ecco perché l’espressione che coinvolge sizeof ritorna il valore 1).
IMPORTANTE
Un altro aspetto che coinvolge la relazione tra array e puntatori, e che vedremo in dettaglio
nel Capitolo 7, è che qualsiasi manipolazione degli elementi di un array, parametro di una
funzione, si rifletterà nel corrispondente array passato come argomento.

Parametri come array multidimensionali


Per quanto concerne la possibilità di utilizzare gli array multidimensionali come
parametri di una funzione la sintassi da adottare è la seguente (Sintassi 6.8 e 6.9)
considerando, per esempio, un array a due dimensioni.

Sintassi 6.8 Dichiarazione di un parametro di tipo array a 2 dimensioni: prototipo di funzione.


return_type function_identifier (data_type [][NUMBER_OF_COLS]);
return_type function_identifier (data_type identifier[][NUMBER_OF_COLS]); // equivalente

Sintassi 6.9 Dichiarazione di un parametro di tipo array a 2 dimensioni: definizione di una


funzione.
return_type function_identifier (data_type identifier[][NUMBER_OF_COLS])
{
...
}

La sintassi da adottare è simile a quella della dichiarazione di un array a una dimensione


con la differenza, però, che è necessario scrivere due coppie di parentesi quadre [ ][ ] e
anche la dimensione delle colonne (si può generalizzare la regola dicendo che, data la
dichiarazione di un array multidimensionale come parametro di un array, la prima
dimensione si può sempre omettere mentre le n dimensioni successive devono sempre essere
indicate).

Snippet 6.8 Dichiarazione del prototipo della funzione search con un parametro di tipo 2d-array.
/* prototipo della funzione search */
int search(int data[][COLS]);

Snippet 6.9 Definizione della funzione search con un parametro di tipo 2d-array.
/* definizione della funzione search */
int search(int data[][COLS])
{
...
}

Per l’invocazione di una funzione che accetta un argomento di tipo array a 2 dimensioni
anche qui è sufficiente indicare solo il nome della corrispondente variabile (rivedere la
Sintassi 6.7), senza quindi scrivere anche le doppie parentesi quadre [ ] [ ].

Listato 6.8 TwoDimArrayAsParameter.c (TwoDimArrayAsParameter).


/* TwoDimArrayAsParameter.c :: Array a 2 dimensioni come parametro :: */
#include <stdio.h>
#include <stdlib.h>

#define ROWS 3
#define COLS 5

/* prototipo della funzione search */


int search(int data[][COLS], int rows);

int main(void)
{
int data[][COLS] =
{
{1, 2, 3, 4, 5},
{-4, -6, 10, 2, 9},
{100, -100, 33, 34, 24}
};

// invocazione di search
int res = search(data, ROWS);

printf("La matrice data contiene %d numeri negativi!\n", res);

return (EXIT_SUCCESS);
}

/* definizione della funzione search */


int search(int data[][COLS], int rows)
{
int nr = 0;

for (int r = 0; r < rows; r++)


{
for (int c = 0; c < COLS; c++)
{
int val = data[r][c];
if (val < 0)
nr++;
}
}
return nr;
}

Output 6.7 Dal Listato TwoDimArrayAsParameter.c.


La matrice data contiene 3 numeri negativi!

Parametri come array a lunghezza variable (VLA)


Chiudiamo la modalità di dichiarazione di parametri di tipo array mostrando come
specificare quelli di lunghezza variabile (Sintassi 6.10) e nello specifico quelli
monodimensionali.

Sintassi 6.10 Dichiarazione di un parametro di tipo array a lunghezza variabile: prototipo.


return_type function_identifier (data_type, data_type [*]);

// equivalente
return_type function_identifier (data_type identifier_1, data_type identifier[identifier_1]);

// equivalente
return_type function_identifier (data_type identifier, data_type identifier[*]);

Sintassi 6.11 Dichiarazione di un parametro di tipo array a lunghezza variabile: definizione.


return_type function_identifier (data_type identifier_1, data_type identifier[identifier_1])
{
...
}

Si utilizza un parametro che rappresenta una variabile che indica la dimensione dell’array
e poi un altro parametro che indica l’array stesso, laddove tra le parentesi quadre [ ] è
possibile porre il simbolo asterisco * (solo nei prototipi di funzione, a indicare una sorta di
dimensione non specificata) oppure l’identificatore di quel primo parametro.
In quest’ultimo caso il parametro che indica la dimensione dell’array deve sempre essere
scritto come primo parametro perché utilizzato, poi, dalla dichiarazione del secondo
parametro di tipo array.
NOTA
Come visto, dato che in C è possibile omettere la lunghezza della dimensione nella
dichiarazione di un array monodimensionale come parametro di una funzione, è anche
possibile utilizzare le seguenti dichiarazioni di VLA che sono simili a quelle dei “normali”
vettori: return_type function_identifier (data_type, data_type []); e return_type

function_identifier (data_type identifier, data_type identifier[]);. In ogni caso, quantunque


corrette e consentite, se ne sconsiglia l’impiego perché così non si rende evidente che il
programma intende utilizzare dei VLA.

Snippet 6.10 Dichiarazione del prototipo della funzione sum con un parametro di tipo array a
lunghezza variabile.
/* prototipo della funzione sum */
int sum(int l, int data[l]);

Snippet 6.11 Definizione della funzione sum con un parametro di tipo array a lunghezza variabile.
/* definizione della funzione sum */
int sum(int l, int data[l])
{
...
}

Anche in questo caso non vi è alcuna sintassi particolare per invocare una funzione con
un parametro che è un array a lunghezza variabile: è sufficiente scrivere come relativo
argomento solamente il nome dell’array.

Listato 6.9 VLAAsParameter.c (VLAAsParameter).


/* VLAAsParameter.c :: VLA come parametro :: */
#include <stdio.h>
#include <stdlib.h>

#define ROWS 3
#define COLS 5

// prototipo della funzione sum


// equivalente a:
// 1) int sum(int r, int c, int data[][c]);
// 2) int sum(int r, int c, int data[r][c]);
// 3) int sum(int r, int c, int data[*][*]);
int sum(int r, int c, int data[][*]);

int main(void)
{
int some_data[ROWS][COLS] =
{
{10, 100, 1000, 10000, 99},
{1, 10, 100, 1000, 9},
{210, 2100, 21000, 210000, 299}
};

// invocazione di sum
int res = sum(ROWS, COLS, some_data);
// stampo la somma del primo array di 3*5
printf("La somma degli elementi dell'array some_data di 3*5 elementi e': %d\n", res);

int r = 2, c = 3;
int other_data[r][c];

// inizializzazione del VLA I riga


other_data[0][0] = -33;
other_data[0][1] = -66;
other_data[0][2] = -99;

// inizializzazione del VLA II riga


other_data[1][0] = -55;
other_data[1][1] = -77;
other_data[1][2] = -133;
res = sum(r, c, other_data);
// stampo la somma del secondo array di 2*3
printf("La somma degli elementi dell'array other_data di 2*3 elementi "
"e': %d\n", res);

return (EXIT_SUCCESS);
}

// definizione della funzione sum


// equivalente a:
// 1) int sum(int r, int c, int data[r][c])
int sum(int r, int c, int data[][c])
{
int res = 0;

for (int i = 0; i < r; i++)


{
for (int j = 0; j < c; j++)
{
res += data[i][j];
}
}
return res;
}

Output 6.9 Dal Listato VLAAsParameter.c.


La somma degli elementi dell'array some_data di 3*5 elementi e': 245938
La somma degli elementi dell'array other_data di 2*3 elementi e': -463

Il Listato 6.9 mostra tutta l’utilità degli array a lunghezza variabile quando gli stessi
rappresentano degli array multidimensionali che sono impiegati come parametri di una
funzione. Grazie a essi, è possibile costruire delle funzioni che sono in grado di computare
dinamicamente array multidimensionali le cui dimensioni successive alla prima sono, di
volta in volta, differenti.
Nel nostro caso abbiamo potuto costruire una funzione sum che è in grado di ritornare la
somma degli elementi di una qualsiasi matrice il cui numero di colonne può variare rispetto
a un valore fornito come argomento (per noi la variabile c) che viene valutato dal
compilatore, a run-time, all’ingresso della funzione.
Resta inteso che se non avessimo avuto a disposizione tale caratteristica non avremmo
potuto scrivere la funzione sum in quel modo, ma avremmo dovuto indicare come
dimensione delle colonne un valore computato da un’espressione costante intera come, per
esempio, quella derivante da un direttiva #define, e sum non avrebbe potuto computare array
bidimensionali di diversa lunghezza.

Argomenti come array letterali


A partire da C99 è possibile indicare dei letterali anche per gli array, ossia è lecito
scrivere delle definizioni di array “senza nome” che forniscono per l’appunto, on demand,
tali strutture di dati. Questi letterali sono denominati letterali composti (compound literals).
NOTA
Ricordiamo che un letterale è una sorta di costante che può apparire in qualsiasi punto del
codice: per esempio, 100 è una costante di tipo int; 'c' è una costante di tipo char e così via.

Sintassi 6.12 Dichiarazione di un array monodimensionale letterale.


(data_type []) {value_0, value_1, ..., value_N};

In sostanza un array letterale si dichiara allo stesso modo di un normale array ma con le
seguenti differenze: è omesso l’identificatore; sono presenti delle parentesi tonde ( ) al cui
interno si indica il tipo di dato e le parentesi quadre [ ] che ne esplicitano la dimensione.

Snippet 6.12 Utilizzo di un array letterale.


...
#define SIZE 6

/* prototipo della funzione subtraction */


int subtraction(int data[], int length);

int main(void)
{
// invoco la funzione subtraction passando "al volo" un array
int res = subtraction((int []){1, 2, 3, 4, 5, 6}, SIZE);
...
}

Lo Snippet 6.12 invoca la funzione subtraction passando come primo parametro un array
letterale inizializzato contestualmente con sei valori di tipo intero.
In definitiva l’utilizzo di un array letterale, o di un letterale composto in generale, è utile
perché evita di dichiarare una variabile utilizzata, nel caso, in una sola occasione.
TERMINOLOGIA
Un letterale composto è indicato dallo standard come un’espressione postfissa formata da
una coppia di parentesi tonde ( ) al cui interno si indica un nome di un tipo di dato e cui fa
seguito una coppia di parentesi graffe { } in cui si pongono degli inizializzatori. È quindi un
concetto più generale riferibile non solo agli array ma anche ad altri oggetti come le struct,

le union e le stesse variabili. Per esempio, anche se di scarsa utilità, è possibile creare on
the fly una variabile di tipo int e passarla come argomento a una funzione, come nel caso
dell’invocazione foo((int){10});.

La keyword static e i parametri di tipo array


Da C99 è possibile utilizzare la keyword static anche per un altro scopo che non sia
quello di agire come un mero specificatore di classe di memorizzazione.
Questa keyword è infatti impiegabile all’interno delle parentesi quadre [ ] di indicazione
della dimensione di un array utilizzato come parametro di una funzione per dichiarare che lo
stesso array ha almeno la quantità di elementi da essa esplicitati.
Per esempio, lo Snippet 6.13 definisce la funzione printMe con un parametro che è un
array di tipo int, laddove la keyword static dichiara che tale array avrà almeno 5 elementi
(è consentito riportare la keyword static anche nel relativo prototipo di funzione).

Snippet 6.13 Utilizzo della keyword static con un parametro di tipo array.
void printMe(int value[static 5])
{
...
}

È comunque importante rilevare che l’indicazione della quantità di elementi minimi di un


array non comporta alcun obbligo di controllo di conformità da parte di un compilatore (un
client utilizzatore potrebbe anche passare come argomento un array con meno elementi), ma
è solo una “segnalazione” che lo stesso può decidere se utilizzare o meno per generare del
codice maggiormente ottimizzato quando la relativa funzione è invocata.

Argomenti di lunghezza variabile


Una funzione può essere definita con la possibilità di ricevere una lista di argomenti il cui
numero e tipo non è noto a priori (è a lunghezza variabile) poiché questi possono, per
l’appunto, differire a ogni invocazione.
In C, tuttavia, non è immediato definire una funzione con una lista di argomenti di
lunghezza variabile, ma si devono compiere una serie di passi, come descritto di seguito.
1. Includere il file header <stdarg.h> che fornisce il tipo va_list e le macro va_start, va_arg,
va_end e va_copy necessarie per gestire e scandire gli argomenti a lunghezza variabile di
una funzione.

DETTAGLIO
va_start, va_arg, va_end e va_copy sono delle function-like macro, ossia della macro che
accettano argomenti e che si comportano, dunque, come se fossero delle funzioni.
Ritorneremo su questo punto nel Capitolo 10.

1. Definire una funzione che ha almeno un parametro ordinario con nome e, di seguito,
scrivere dei punti di sospensione ... (ellissi) che indicano che il tipo e il numero di
argomenti forniti alla funzione è variabile. Il parametro ordinario, denominato dallo
standard parmN, deve sempre essere quello più a destra rispetto agli altri eventuali
parametri, mentre le ellissi devono apparire solamente al termine della lista di
parametri.
2. Dichiarare nel corpo della funzione una variabile di tipo va_list deputata a contenere
informazioni necessarie alle macro va_start, va_arg, va_end e va_copy che la utilizzeranno
per la manipolazione degli argomenti variabili. Per convenzione, l’identificatore
utilizzato per questo tipo di dato è ap, ossia argument pointer (puntatore ad
argomento), e sta proprio a indicare che tale variabile potrà contenere, di volta in volta,
un riferimento a un argomento, tra quelli variabili, da processare.
3. Utilizzare la macro va_start fornendo come primo parametro la variabile di tipo va_list
e come secondo parametro il parametro con nome che precede l’ellissi nella
definizione della funzione. Di fatto questa macro inizializza la variabile di tipo va_list
facendola puntare al primo argomento anonimo che si trova dopo il parametro con
nome fornito allo scopo. Questo parametro con nome è dunque essenziale perché
indica a va_start la “posizione” dopo la quale è possibile individuare l’inizio della lista
di argomenti variabili. Lo standard fornisce la seguente indicazione di come dovrebbe
essere dichiarata: va_start: void va_start(va_list ap, parmN).
4. Utilizzare la macro va_arg fornendo come primo parametro la variabile di tipo va_list
precedentemente inizializzata da va_start e come secondo parametro il tipo di dato
dell’argomento da processare. Questa macro permette di ottenere il valore di un
argomento anonimo e modifica la variable di tipo va_list in modo che possa puntare al
successivo argomento anonimo (se non vi è un successivo argomento, oppure se il tipo
di dato fornito non è compatibile con il tipo di dato del successivo argomento, il
comportamento è non definito). Lo standard fornisce la seguente indicazione di come
dovrebbe essere dichiarata: va_arg: type va_arg(va_list ap, type).

NOTA
Quando una funzione con un numero variabile di argomenti è invocata, il compilatore
effettua una promozione di default degli argomenti variabili (per esempio, un char è
convertito in un int e un float in un double).

1. Utilizzare la macro va_end fornendo come primo parametro la variabile di tipo va_list.
Questa macro è essenziale perché effettua delle operazioni necessarie alla “pulizia”
della variabile di tipo va_list per un successivo riutilizzo (per esempio, potrebbe
deallocare la memoria impiegata per contenere gli argomenti variabili e renderla
inutilizzabile finché non avviene un’altra inizializzazione con va_start) oppure per un
corretto ritorno dalla funzione dove è stata impiegata. Lo standard fornisce la seguente
indicazione di come dovrebbe essere dichiarata: va_end: void va_end(va_list ap).

Listato 6.10 VariableArgumentsList.c (VariableArgumentsList).


/* VariableArgumentsList.c :: Funzioni con argomenti di lunghezza variabile :: */
#include <stdio.h>
#include <stdlib.h>
#include <stdarg.h>

/* prototipo della funzione subtraction */


int subtraction(int length, ...);

int main(void)
{
// sottrazione con 6 elementi
int res = subtraction(6, 369, 10, 15, 65, 88, 66);
printf("Il risultato della sottrazione con 6 elementi e': %d\n", res);
// sottrazione con 3 elementi
res = subtraction(3, 100, 50, 20);
printf("Il risultato della sottrazione con 3 elementi e': %d\n", res);

return (EXIT_SUCCESS);
}

/* definizione della funzione subtraction */


int subtraction(int length, ...)
{
va_list ap; // dichiaro una variabile di tipo va_list

// inizializzo ap e indico l'argomento dopo il quale trovare gli altri


// argomenti variabili; deve essere sempre chiamata prima di usare ap!
va_start(ap, length);

// valore primo argomento


int result = va_arg(ap, int);

for (int i = 1; i < length; i++)


result -= va_arg(ap, int); // successivo argomento...

va_end(ap); // clean up di ap

return result;
}

Output 6.8 Dal Listato VariableArgumentsList.c.


Il risultato della sottrazione con 6 elementi e': 125
Il risultato della sottrazione con 3 elementi e': 30

Il Listato 6.10 definisce la funzione subtraction con il primo argomento che indica quanti
argomenti variabili sono utilizzati e poi l’ellissi che indica che la funzione accetta una
quantità variabile di argomenti.
Infatti, nel main, notiamo come subtraction sia chiamata la prima volta, con 6 elementi,
mentre la seconda volta con 3 elementi e in ambedue le situazioni ritorni il corretto risultato
“adattato” alla giusta quantità di argomenti passati.
La definizione della funzione subtraction è invece scritta in modo che va_list, va_start,
va_arg e va_end, nella giusta sequenza, gestiscano e scansionino gli argomenti variabili.
In particolare, ap rappresenta il tipo va_list che è utilizzato da:

va_start, che lo inizializza per farlo puntare al primo argomento anonimo;


va_arg, che lo utilizza per ricavare, in successione, i valori degli argomenti anonimi;
va_end, che lo utilizza per effettuare le opportune operazioni di clean up.

Infine, a partire da C99 è possibile utilizzare anche la macro va_copy, che consente di
inizializzare il suo primo parametro (la destinazione), che è una variabile di tipo va_list, con
il corrente valore di un’altra variabile di tipo va_list passata come secondo parametro (la
sorgente). Lo standard ne fornisce, come indicazione, la seguente dichiarazione: void
va_copy(va_list dest, va_list src).
Questa macro è dunque utile perché consente di memorizzare la corrente “posizione” di
attraversamento di una lista di argomenti variabili e di utilizzare i successivi argomenti
anche se quelli originari sono stati completamente processati.

Snippet 6.14 Utilizzo della macro va_copy nel primo caso di sottrazione con 6 elementi.
/* definizione della funzione subtraction */
int subtraction(int length, ...)
{
va_list ap; // dichiaro una variabile di tipo va_list

// inizializzo ap e indico l'argomento dopo il quale trovare gli altri


// argomenti variabili; deve essere sempre chiamata prima di usare ap!
va_start(ap, length);

// I invocazione con ap
int result = va_arg(ap, int); // 369

// II invocazione con ap
result = va_arg(ap, int); // 10

// copia di ap
va_list ap_copy;
va_copy(ap_copy, ap);

// III invocazione con ap


result = va_arg(ap, int); // 15

// IV invocazione con ap
result = va_arg(ap, int); // 65

// I invocazione con ap_copy


int res = va_arg(ap_copy, int); // 15

va_end(ap); // clean up di ap
va_end(ap_copy); // clean up di ap_copy

return result; // ATTENZIONE: valore non più valido. NON USARE.


}

Lo Snippet 6.14 mostra l’uso della macro va_copy adattata nella funzione subtraction, la
quale però non ritorna più alcun valore valido e congruo.
Vediamo nell’ordine cosa accade.
1. La prima invocazione di va_arg con ap ritorna il risultato del primo argomento variabile,
ossia 369, e modifica ap in modo che possa puntare al successivo argomento.
2. La seconda invocazione di va_arg con ap ritorna il risultato del secondo argomento
variabile, ossia 10, e modifica ap in modo che possa puntare al successivo argomento.
3. L’invocazione di va_copy inizializza la variabile ap_copy di tipo va_list con il corrente
valore di ap che sta puntando al terzo argomento.
4. La terza invocazione di va_arg con ap ritorna il risultato del terzo argomento variabile,
ossia 15, e modifica ap in modo che possa puntare al successivo argomento.
5. La quarta invocazione di va_arg con ap ritorna il risultato del quarto argomento
variabile, ossia 65, e modifica ap in modo che possa puntare al successivo argomento.
6. La prima invocazione di va_arg con ap_copy ritorna il risultato del terzo argomento
variabile, ossia 15, in accordo con quanto accaduto al punto 3, e modifica ap_copy in
modo che possa puntare al successivo argomento.
7. L’invocazione di va_end con ap e poi con ap_copy compie le necessarie operazioni di
pulizia.
L’istruzione return: dettaglio
L’istruzione return (Sintassi 6.13) termina l’elaborazione delle istruzioni nella corrente
funzione e ritorna il controllo dell’esecuzione del codice alla funzione chiamante che
riprende l’elaborazione dall’istruzione successiva alla chiamata della funzione.
In definitiva possiamo dire che mentre gli argomenti passano delle informazioni in
ingresso da una funzione chiamante a una funzione chiamata, l’istruzione return passa delle
informazioni in uscita dalla funzione chiamata alla funzione chiamante.

Sintassi 6.13 Istruzione return.


return [expression];

Come evidenziato dalla Sintassi 6.13, un’istruzione return può ritornare un valore alla
funzione chiamante per il tramite di una qualsiasi espressione.
NOTA
A volte, anche se non è necessario, expression è posta tra una coppia di parentesi tonde ( )

per semplici ragioni di stile di scrittura del codice. Esse non sono, comunque, obbligatorie.

Se una funzione ha un tipo di ritorno, allora expression deve indicare il valore ritornato e il
suo tipo deve concordare con tale tipo di ritorno, altrimenti il compilatore ne effettua una
conversione (per esempio, se una funzione ha come tipo di ritorno un int ma expression ha
come tipo un double, allora il valore di expression è convertito in int prima dell’esecuzione di
return).

Snippet 6.15 Conversione valore di expression.


...
int main(void)
{
// qui res avrà come valore 16 e non 16.6...
int res = sum(5.8, 10.8);
...
}

int sum(double a, double b)


{
double res = a + b;

// qui il valore di res è convertito in int


// e pertanto c'è perdita di informazione poiché
// a e b sono di tipo double
return res;
}

Se, viceversa, una funzione ha come tipo di ritorno void, che statuisce che la stessa non
deve ritornare alcun valore, allora può essere utilizzata return senza expression in qualsiasi
punto della funzione oppure si può attendere la naturale terminazione della funzione che
avviene quando il flusso di esecuzione del codice raggiunge la parentesi graffa } di chiusura
del suo blocco costitutivo.

Snippet 6.16 Funzione con il tipo di ritorno void.


void makeDivision(double dividend, double divisor)
{
// ritorno per evitare una divisione per 0 e l'istruzione printf
// successiva non sarà mai eseguita
if (divisor == 0)
return; // return è posto in questo punto del codice
// e non necessariamente alla fine

printf("Il risultato della divisione tra %.2f e %.2f e': %.2f\n",


dividend, divisor, dividend / divisor);
}

Nell’utilizzare l’istruzione return bisogna fare attenzione ai seguenti casi.

Se una funzione ha un tipo di ritorno diverso da void e si omette di utilizzare


l’istruzione return, un compilatore potrebbe generare un messaggio come warning:
control reaches end of non-void function. Il comportamento del programma sarà non
definito.
Se una funzione ha un tipo di ritorno diverso da void e si utilizza l’istruzione return
senza un’espressione, un compilatore potrebbe generare un messaggio come warning:
'return' with no value, in function returning non-void. Il comportamento del
programma sarà non definito.
Se una funzione ha un tipo di ritorno void e si utilizza l’istruzione return con
un’espressione, un compilatore potrebbe generare un messaggio come warning: 'return'

with a value, in function returning void. Il comportamento del programma sarà non
definito. Inoltre se la funzione chiamante ne vuole utilizzare il valore, per esempio
assegnandolo a una variabile, un compilatore potrebbe generare un errore come error:
void value not ignored as it ought to be.

Snippet 6.17 Utilizzi scorretti dell’istruzione return e/o del tipo di ritorno.
...
int case_1()
{
int v = 100; // warning: control reaches end of non-void function
}

int case_2()
{
int v = 100;
return; // warning: 'return' with no value, in function returning non-void
}

void case_3()
{
int v = 100;
return v; // warning: 'return' with a value, in function returning void
}
int main(void)
{
int v = case_1();
v = case_2();
v = case_3(); // error: void value not ignored as it ought to be
...
}

RITORNO DA UNA FUNZIONE E SEQUENCE POINT


Lo standard del linguaggio C individua come ulteriore sequence point quello che si trova
immediatamente prima dell’uscita da una funzione (Snippet 6.18). In pratica, prima che la
funzione corrente ritorni il controllo alla funzione chiamante, sono eseguite tutte le statement
del body ed è valutata l’espressione dell’eventuale istruzione return, che è infatti categorizzata
come una full expression.

Snippet 6.18 Ritorno da una funzione e sequence point.


...
// prototipo di foo
int foo(void);

int b = 100;

int main(void)
{
int val = foo();
...
}

// definizione di foo
int foo(void)
{
// sequence point prima dell'uscita dalla funzione;
// questo garantirà che il side-effect su b sia completamente portato a termine
// e infatti la variabile esterna b sarà comunque decrementata e varrà 99;
// al contempo, val conterrà il valore 100 perché su b è stato impiegato l'operatore
// di decremento postfisso
return b--;
}
Lo specificatore di funzione _Noreturn
A partire da C11 è possibile utilizzare lo specificatore di funzione _Noreturn per indicare
che una funzione, al termine del suo processo elaborativo, non ritornerà al chiamante.
Questa modalità di comportamento della funzione è differente rispetto allo specificare
come tipo di ritorno void, perché in questo caso la funzione chiamante, al termine delle sue
operazioni, ritornerà il controllo alla funzione chiamata.

Snippet 6.19 _Noreturn.


_Noreturn void makeATask()
{
printf("Inizio esecuzione task...\n");

// fa qualcosa...

exit(0); // esce dal programma non dalla funzione!


}

Nel caso dello Snippet 6.19 la funzione makeATask esplicita, tramite _Noreturn, che al
termine della sua esecuzione non ritornerà al chiamante (per esempio la funzione main) ma
tramite la funzione exit terminerà l’applicazione.
NOTA
Nello standard C11 la stessa funzione exit è prototipata come _Noreturn void exit(int

status); a indicare che non ritornerà mai al chiamante.

Lo scopo dello specificatore _Noreturn è dunque quello di “informare” l’utilizzatore di una


funzione e il compilatore stesso che tale funzione non ritornerà mai il controllo al
chiamante, in modo che il primo possa invocarla correttamente mentre il secondo possa
compiere eventuali ottimizzazioni.
ATTENZIONE
Se in qualche modo una funzione con lo specificatore _Noreturn è in grado di tornare al
chiamante un compilatore, può emettere i seguenti messaggi di diagnostica: warning:

function declared 'noreturn' has a 'return' statement e warning: 'noreturn' function does return.
Le funzioni inline
A partire da C99 è possibile utilizzare lo specificatore di funzione inline per “suggerire”
al compilatore di chiamare una funzione nel modo più efficiente e veloce possibile al fine di
ridurre o eliminare il consueto overhead legato, per l’appunto, alla sua invocazione (per
esempio, nella chiamata di una funzione non inline, un compilatore deve compiere una serie
di operazioni dispendiose come preparare la chiamata eseguendo determinate istruzioni, fare
una copia degli argomenti, saltare all’indirizzo di memoria dove iniziano le istruzioni della
funzione e così via).
TERMINOLOGIA
In questo contesto, per overhead si intende quel complesso di operazioni supplementari o
extra, e dunque “costose”, che un compilatore deve compiere per invocare una funzione e
che sono quindi ulteriori rispetto a quelle che comportano la sola esecuzione del codice ivi
contenuto.

Tuttavia, contrariamente a quanto il nome possa far intendere, non è detto che un
compilatore per ridurre o eliminare l’overhead di chiamata “sostituisca”, inline, l’istruzione
di invocazione di una funzione con il contenuto nel suo body ovvero con le sue istruzioni
costitutive (un compilatore può decidere di adottare altri meccanismi di ottimizzazione o di
non fare nulla; infatti, il come e il se utilizzare questa feature è implementation-defined).

Snippet 6.20 Funzione inline.


...
// definizione di una funzione inline
static inline int max(int a, int b) { return a > b ? a : b; }

int main(void)
{
int x = 10, y = 12;

// qui possibile, ma non certa, espansione inline del body di max


// ↓
int res = max(x, y); // per esempio: x > y ? x : y;
...
}

NOTA
La keyword static applicata alla definizione della funzione inline ha un significato che è
legato al concetto di linkage. Ritorneremo in modo dettagliato su questo punto nel Capitolo
9. Per il momento è sufficiente dire che esso impone che la funzione max sia utilizzabile solo
dalle funzioni definite nel corrente file.

NOTA
GCC può compiere un inline di una funzione se è attiva l’ottimizzazione, per esempio
compilando il sorgente che contiene la funzione inline con il flag -O3.

In conclusione, la definizione di una funzione inline è consigliabile solo se il codice ivi


contenuto è breve e se la stessa è utilizzata molte volte nell’ambito di un programma e non
vi è quindi alcuna computazione complessa, altrimenti è meglio utilizzare la definizione di
una funzione non inline che sarà invocata come di consueto, e il suo naturale overhead sarà
comunque meno “pesante” rispetto all’operazione di inlining di copiose quantità di codice
in ogni punto del programma dove sarà presente la sua invocazione.
Ricorsione
Una funzione è rappresentata, come abbiamo visto, da una serie di istruzioni racchiuse in
un blocco di codice che eseguono un compito specifico, ed è invocata in modo gerarchico.
Questo significa che, in un punto di un programma, una funzione (per esempio il main)
può invocare un’altra funzione (per esempio foo), la quale può invocare ancora un’altra
funzione (per esempio bar) e così via finché tutte le eventuali chiamate di funzione
esauriscono l’obiettivo computazionale.
Tuttavia, per la risoluzione di alcuni problemi algoritmici si possono progettare delle
funzioni che “invocano se stesse” tante volte quante sono necessarie per risolvere i
problemi. Tale modalità di invocazione è detta ricorsione e le funzioni che chiamano se
stesse sono indicate con il termine di funzioni ricorsive.
È poi usata la seguente terminologia: per caso base si intende un punto di uscita
terminale dalla funzione ricorsiva che deve essere sempre presente per evitare una
ricorsione infinita; per passo ricorsivo si intende la corrente invocazione di se stessa da
parte della funzione ricorsiva necessaria per la prosecuzione della ricorsione.
NOTA
La ricorsione è un metodo computazionale equivalente all’iterazione. Infatti, ogni algoritmo
ricorsivo si può scrivere in modo iterativo e ogni algoritmo iterativo si può scrivere in modo
ricorsivo. Generalmente si sceglie un approccio ricorsivo: quando lo stesso permette una
più naturale ed elegante risoluzione di un problema algoritmico anche se, come vedremo,
tale scelta può essere fonte della minore efficienza prestazionale di un programma; quando
la soluzione ricorsiva appare più ovvia e meno problematica da scrivere rispetto
all’equivalente soluzione iterativa.

Vediamo subito un esempio che chiarirà meglio quanto detto, illustrando il concetto
matematico di fattoriale e vedendo come possiamo scrivere un metodo che ne effettua il
calcolo.

IL FATTORIALE DI UN NUMERO
In matematica il calcolo del fattoriale è un procedimento mediante il quale, dato un numero
intero positivo, si deve trovare quel valore che è il prodotto di tutti i numeri interi positivi minori
o uguali del numero stesso. Lo si può trovare usando la formula iterativa: N! = N * (N - 1) * (N
- 2) * ... * 1 oppure quella ricorsiva, N! = N * (N - 1)! considerando che, in entrambi i casi, 0!
= 1 e 1! = 1. Per esempio, con la formula iterativa il fattoriale di 5 è 5 * 4 * 3 * 2 * 1, mentre
con quella ricorsiva il fattoriale di 5 è 5 * (5 - 1)!. In entrambi i casi, ovviamente, il risultato
sarà uguale a 120.

Listato 6.11 Recursion.c (Recursion).


/* Recursion.c :: Esempio di ricorsione: fattoriale di un numero :: */
#include <stdio.h>
#include <stdlib.h>

// prototipo della funzione factorial


unsigned long factorial(unsigned long number);

int main(void)
{
long number, result;
printf("*** Calcolo del fattoriale di un numero ***\n\n");
printf("Digita un numero [q per uscire]: ");
while (scanf("%ld", &number) == 1)
{
if (number < 0)
printf("Digita solo numeri maggiori o uguali a 0.\n");
else if (number > 18)
printf("Digita solo numeri minori o uguali a 18.\n");
else
{
result = factorial(number);
printf("Il fattoriale di %lu e' %lu.\n", number, result);
}
printf("Digita un numero [q per uscire]: ");
}
printf("\n*** Computazione terminata ***\n");

return (EXIT_SUCCESS);
}

// definizione della funzione factorial


unsigned long factorial(unsigned long number)
{
if (number <= 1) // caso base
return 1;
else // passo ricorsivo
return number * factorial(number - 1);
}

Output 6.9 Dal Listato 6.11 Recursion.c.


*** Calcolo del fattoriale di un numero ***

Digita un numero [q per uscire]: 4


Il fattoriale di 4 e' 24.
Digita un numero [q per uscire]: q

*** Computazione terminata ***

Il Listato 6.11 definisce un programma che consente di calcolare il fattoriale di un


numero digitato da tastiera che sia compreso tra 0 (i numeri negativi non sono contemplati
dalla definizione del calcolo di un fattoriale) e 18 (dei numeri più grandi eccederebbero il
massimo range di valori consentiti dal tipo unsigned long scelto, che sul nostro sistema a 32
bit è di 4294967295).
La funzione che consente il calcolo del fattoriale del numero immesso da tastiera è
denominata factorial ed è costruita: con un caso base, che permette di fatto di uscire dalla
funzione (se lo omettessimo avremmo una ricorsione infinita). Nel nostro esempio per il
calcolo di un fattoriale il caso base si verifica quando number è minore o uguale a 1; con un
passo ricorsivo che, invece, permette di invocare nuovamente la funzione factorial (ovvero
se stessa) quando number è maggiore di 1, passando come argomento number - 1. In effetti il
passo ricorsivo permette di elaborare una parte più “piccola” del problema del calcolo del
fattoriale (semplificandolo e riducendolo) fino a far convergere la funzione verso il suo caso
base (che saprà sicuramente risolvere direttamente) e far così ritornare, in successione, da
tutte le funzioni invocate, un valore che è un risultato intermedio della computazione
algoritmica fino all’elaborazione del risultato finale (Figura 6.2).
La Figura 6.2 mostra la sequenza di azioni che avvengono quando si invoca la funzione
factorial con un argomento che ha come valore, per esempio, il numero 4.

Per i passi ricorsivi avremo, infatti:


1. number vale 4 e non è minore o uguale al numero 1, cosicché viene invocato nuovamente
factorial con argomento 4 – 1;

2. number vale 3 e non è minore o uguale al numero 1, cosicché viene invocato nuovamente
factorial con argomento 3 – 1;

3. number vale 2 e non è minore o uguale al numero 1, cosicché viene invocato nuovamente
factorial con argomento 2 – 1;

4. number vale 1 ed è minore o uguale al numero 1, cosicché viene ritornato lo stesso


numero 1 e i passi ricorsivi terminano.

Figura 6.2 Calcolo del fattoriale di 4: passi ricorsivi e ritorno dalla funzione factorial.

Per i ritorni dalle funzioni, invece, avremo:


1. il risultato 1;
2. il risultato 2 * 1;

3. il risultato 3 * 2;
4. il risultato 4 * 6;

5. il risultato 24 che sarà ritornato al chiamante, che nel nostro caso è la funzione main.

Per comprendere ancora meglio quest’importante concetto appare utile fare una breve
divagazione teorica su come in C sono effettivamente eseguite le chiamate alle funzioni.
Prima di tutto è impiegata una struttura di dato denominata function call stack che serve a
memorizzare, secondo una modalità definita LIFO (Last In First Out), un cosiddetto stack
frame (riferito anche come activation record); questo è visualizzabile come una sorta di
“scatola” contenente informazioni e dati essenziali per una funzione come l’indirizzo di
ritorno alla funzione chiamante (ossia il punto del codice dove tornare al termine della sua
esecuzione dove si troverà la successiva istruzione da eseguire), le sue variabili locali, i suoi
eventuali parametri e così via.
La modalità di memorizzazione LIFO utilizzata dal function call stack comporta che ogni
stack frame creato (causa di un’invocazione di funzione da una precedente funzione) venga
“impilato” sopra un altro, proprio come accade quando nella realtà si collocano dei piatti di
portata gli uni sopra gli altri, e l’ultimo stack frame inserito (pushed) è anche il primo a
essere rimosso (popped) quando la funzione cessa il suo compito elaborativo (ritorna alla
funzione chiamante).
Ritornando all’esempio dei piatti di portata, l’ultimo piatto collocato in cima sarà anche
quello che, per primo, bisognerà togliere, pena la caduta di tutta la pila di piatti.
Ciò detto abbiamo che funzioni che chiamano altre funzioni danno origine a una
sequenza di stack frame che sono allocati secondo l’ordine delle chiamate (prima la
funzione A, poi la funzione B, poi la funzione C e così via) e sono deallocati in ordine inverso
(prima la funzione C, poi la funzione B, poi la funzione A).
Ritornando al nostro esempio del Listato 6.11, e in accordo con quanto detto, proviamo a
“disegnare” cosa accade per il calcolo del fattoriale quando viene invocata ricorsivamente la
funzione factorial. Nella Figura 6.3 vediamo il function call stack completamente impilato
dopo che l’ultima invocazione di factorial è stata effettuata (dalla I alla IV in quest’ordine);
nella Figura 6.4 vediamo gli step di deallocazione della funzione factorial dopo che la IV è
ritornata con il valore 1 (dalla III invocazione alla I in quest’ordine).
Figura 6.3 Function call stack dopo l’ultima invocazione di factorial.

Figura 6.4 Step di “srotolamento” del function call stack.

Vediamo allora quali sono i vantaggi e gli svantaggi relativi all’utilizzo della ricorsione:
tra i vantaggi abbiamo che “pensare” alla soluzione di un problema algoritmico in termini
ricorsivi può produrre a volte un codice più elegante e di chiara lettura.
In più, molti problemi computazionali sono risolvibili in modo più intuitivo e agevole
tramite l’utilizzo della ricorsione, che si presta quindi meglio a “codificare” in modo
naturale la relativa soluzione (si pensi ancora una volta al calcolo del fattoriale e a come la
sua formula risolutiva, ossia N * (N - 1)!, è stata codificata tale e quale nel codice).
Tra gli svantaggi abbiamo sicuramente quello della scarsa efficienza prestazionale,
perché richiamare molte volte una funzione richiede tempo per la gestione del function call
stack e fa consumare molta memoria per allocare, a ogni chiamata, un nuovo stack frame
con tutte le sue informazioni e dati (per esempio per allocare una propria copia delle
variabili locali).
La funzione main: nozioni conclusive
Ora che abbiamo appreso tutti i concetti relativi alla definizione e l’utilizzo delle
funzioni, possiamo terminare l’analisi dell’importante funzione main, che ricordiamo essere
la funzione che è invocata in automatico quando il programma che la contiene viene
avviato.
Lo standard C11 stabilisce che la funzione main non è dotata di alcun prototipo e deve
essere definita in uno dei seguenti modi (Sintassi 6.14 e 6.15).

Sintassi 6.14 I definizione di main.


int main(void) { /* ... */ }

Abbiamo un tipo di ritorno int e con il tipo void per la lista dei parametri.

Sintassi 6.15 II definizione di main.


int main(int argc, char *argv[]) { /* ... */ }

Abbiamo un tipo di ritorno int e con due parametri: il primo, argc (argument count), di
tipo intero, conterrà il numero di argomenti forniti dalla riga di comando quando si
invocherà il relativo programma, il cui nome verrà incluso in questo conteggio; il secondo,
argv (argument vector), di tipo vettore o array di puntatori a char, conterrà, come stringhe, i

predetti argomenti forniti (un argomento sarà anche il nome del programma).
NOTA
Il concetto di array di puntatori a un tipo sarà chiarito in dettaglio nel Capitolo 7. Per ora
proviamo a spiegarlo così: dato che un puntatore può contenere un indirizzo in memoria, un
puntatore a carattere può contenere un indirizzo in memoria a partire dal quale si può
trovare una stringa (che è un array di caratteri). Ciò detto, quando si invoca un programma
con una serie di argomenti questi sono trattati come stringhe, dove l’indirizzo in memoria di
ciascuna è memorizzato come valore nel corrispettivo elemento dell’array argv, che quindi è
un vettore a una serie di puntatori a caratteri.

TERMINOLOGIA
Per command-line interface (interfaccia a riga di comando) si intende un ambiente di
interazione utente/computer, messo a disposizione da un sistema operativo, con il quale un
utente invia comandi (esegue programmi di sistema e non) tramite delle righe di testo
(command lines). Così per command-line arguments (argomenti dalla riga di comando) si
intendono eventuali argomenti forniti a un programma sulla stessa riga. Esempi di
command-line interface sono il prompt dei comandi in Windows oppure la shell in ambienti
Unix.

Quando l’array argv sarà popolato, il primo elemento, argv[0], conterrà il nome del
programma (il program name come definito dallo standard), mentre, se presenti degli
argomenti dalla riga di comando (program parameters come definiti dallo standard), gli
stessi si troveranno localizzati come stringhe negli elementi che dell’array che andranno da
argv[1] ad argv[argc - 1]. Così se impartiamo da una shell un comando come chmod 644

data.html, avremo che il parametro argc conterrà il valore 3, argv[0] conterrà la stringa chmod,
argv[1], conterrà la stringa 644 e argv[2] conterrà la stringa data.html.
NOTA
Gli identificatori argc a argv sono del tutto arbitrari. Nulla vieta di utilizzarne altri.

Listato 6.12 echo.c (echo).


/* echo.c :: Mostra l'utilizzo degli argomenti dalla riga di comando :: */
#include <stdio.h>
#include <stdlib.h>

int main(int argc, char *argv[])


{
for (int i = 1; i < argc; i++)
printf("%s ", argv[i]);
printf("\n");

return (EXIT_SUCCESS);
}

Il Listato 6.12 mostra un semplice programma che si limita a ripetere gli argomenti forniti
in una sorta di eco.
Questo programma non ha alcuna utilità pratica: serve solo a illustrare come sia possibile
ottenere dall’array argv i relativi argomenti forniti tramite un semplice ciclo for che inizia
dall’indice 1 perché l’indice 0 contiene il nome del programma.
Per verificare il suo funzionamento è sufficiente digitare quanto riportato di seguito.

Shell 6.1 Esecuzione programma echo.


echo Ciao a tutti dal fantastico mondo della programmazione C!

Output 6.10 Dal Listato 6.12 echo.c.


Ciao
a
tutti
dal
fantastico
mondo
della
programmazione
C!
Capitolo 7
Puntatori

I puntatori sono senza dubbio una delle caratteristiche più importanti ed essenziali del
linguaggio C. La corretta e adeguata comprensione del loro utilizzo è imprescindibile per
padroneggiare proficuamente il linguaggio.
Essi forniscono sia un potente meccanismo per scrivere programmi veloci, efficienti e
compatti sia un utile strumento attraverso il quale fornire un accesso diretto alla memoria,
allocare e deallocare dinamicamente la memoria, costruire complesse strutture di dati (liste
collegate, pile, code, alberi, grafi e così via), passare alle funzioni dei riferimenti a tipi di
dato derivati e complessi (per esempio i tipi struttura) evitando l’overhead del copia dei
rispettivi membri, modificare direttamente all’interno di una funzione chiamata una
variabile passata come argomento dalla rispettiva funzione chiamante, fornire una “sintassi”
alternativa di accesso e manipolazione degli elementi di un array e così via per altre
soluzioni a problemi algoritmici o computazionali di vario tipo.
Dal punto di vista semantico, per dirla in modo semplice, un puntatore altro non è che
una variabile specializzata a contenere un indirizzo di memoria di un’altra variabile.
Ricordiamo, infatti, che ogni variabile di un programma occupa una determinata quantità
di memoria a seconda del suo tipo (per esempio, in un sistema a 32 bit una variabile di tipo
int può occupare 4 byte) ed è localizzabile precisamente attraverso un indirizzo che è esso

stesso un valore numerico (per esempio, la predetta variabile di tipo int potrebbe occupare
gli indirizzi di memoria, byte per byte, 0x0109f9f4, 0x0109f9f5, 0x0109f9f6 e 0x0109f9f7 e
l’indirizzo 0x0109f9f4 sarebbe il suo indirizzo a partire dal quale localizzarla).
NOTA
Il Capitolo 1, al paragrafo “La memoria centrale”, contiene un’analisi dettagliata su come è
organizzata e rappresentata la memoria di un elaboratore.
Figura 7.1 Rappresentazione in memoria di una variabile di tipo int contenente il valore 10.

In modo più rigoroso e formale, un puntatore è definibile come un tipo derivabile da un


tipo funzione oppure da un qualsiasi tipo oggetto (per esempio una variabile) denominato
tipo referenziato. In pratica rappresenta un oggetto contenente un valore che è un
riferimento, un puntamento, verso un altro oggetto che è, per l’appunto, il tipo referenziato.
Possiamo dunque asserire che un puntatore derivato da un tipo referenziato T è definibile
come un puntatore a T.
Ritornando alla nostra variabile di tipo int di 32 bit contenente il valore 10, potremmo
definire un puntatore verso di essa, ossia potremmo costruire un tipo derivato (un puntatore
a un int) che è in grado di contenere l’indirizzo di memoria dove è localizzabile la predetta
variabile e, per il tramite di esso, compiere su quest’ultima delle operazioni di lettura e/o di
scrittura.
La Figura 7.2 mostra il consueto modo, usato in letteratura, per indicare che un oggetto
puntatore punta a un determinato oggetto: in pratica, la variabile puntatore ha come
contenuto una freccia che indica il puntamento verso l’oggetto referenziato (evidentemente
la medesima freccia può essere sostituita con l’indirizzo in memoria della variabile puntata).

Figura 7.2 Rappresentazione grafica di un puntatore.


NOTA
Un puntatore, essendo esso stesso un oggetto, ha un proprio indirizzo in memoria che è
differente dall’indirizzo in memoria in esso contenuto che appartiene all’oggetto referenziato.
Sintassi di base dei puntatori
Un puntatore, come detto, è una variabile che contiene come valore un indirizzo di
memoria appartenente a un oggetto di un determinato tipo. Per dichiararla come tale
bisogna usare una sintassi particolare che prevede il consueto specificatore di tipo e
identificatore, ma con in più il carattere asterisco (*) posto tra lo specificatore e
l’identificatore (Sintassi 7.1).

Sintassi 7.1 Dichiarazione di un puntatore.


data_type *ptr_identifier;

Così lo Snippet 7.1 dichiara la variabile data come un puntatore a un tipo int, ossia
stabilisce che data potrà contenere un riferimento verso qualsiasi oggetto di tipo intero
(potrà contenerne l’indirizzo di memoria).

Snippet 7.1 Dichiarazione di un puntatore a un int.


int *data;

Se si esegue la dichiarazione di cui lo Snippet 7.1, il compilatore predisporrà dello spazio


in memoria dove sarà allocato un puntatore a un int che, però, inizialmente potrà essere non
inizializzato con un valore congruo o corretto, cioè potrà contenere un “qualsiasi” indirizzo
di memoria, in generale, non validamente referenziabile (Figura 7.3).

Figura 7.3 Esecuzione della statement di dichiarazione del puntatore data.

In linea generale la quantità di spazio di allocazione per un puntatore è dipendente dal


sistema in uso; potrà essere, per esempio, di 4 byte su un sistema a 32 bit oppure di 8 byte
su un sistema di 64 bit.
SUGGERIMENTO
Se desideriamo scoprire quanto spazio di memoria il compilatore allocherà per un puntatore
sul sistema in uso, possiamo utilizzare l’operatore sizeof. Per esempio, sull’attuale sistema
di compilazione, l’istruzione sizeof data ritornerà come valore 4, ossia il compilatore
impegnerà 4 byte di memoria (32 bit) per memorizzare un determinato indirizzo di memoria.

Dopo la dichiarazione di un puntatore il passo successivo è quello di assegnargli come


valore un indirizzo di memoria di un altro oggetto compatibile (Sintassi 7.2).

Sintassi 7.2 Assegnamento di un indirizzo di un oggetto a un puntatore.


ptr_identifier = &object;
In pratica è sufficiente adoperare l’operatore di indirizzamento (o indirizzo di) espresso
mediante il simbolo e commerciale (&) sull’oggetto desiderato.
Lo Snippet 7.2 dichiara una variabile di tipo int denominata value e poi assegna
l’indirizzo di memoria dove è localizzata alla variabile di tipo puntatore a int denominata
data.

Snippet 7.2 Assegnamento di un indirizzo di una variabile int a un puntatore a un int.


int value = 10;
int *data = &value; // data conterrà l'indirizzo di value

Figura 7.4 Rappresentazione del puntatore data dopo l’assegnamento dell’indirizzo di value.

Dalla Figura 7.4 si evince come dopo l’assegnamento dell’indirizzo di memoria di value il
puntatore data punti alla variabile value medesima perché, ripetiamo, tale puntatore contiene
come valore quell’indirizzo.
Infine, il seguente operatore, detto di indirezione o deriferimento, espresso mediante il
simbolo asterisco * e prefisso all’identificatore di un puntatore, consente di accedere al
contenuto di un oggetto riferito da un puntatore (Sintassi 7.3).

Sintassi 7.3 Accesso al contenuto di un oggetto riferito da un puntatore.


*ptr_identifier;

Ritornando al precedente esempio, lo Snippet 7.3 assegna alla variabile tmp il contenuto
della variabile value riferita dal puntatore data.

Snippet 7.3 Utilizzo dell’operatore di deriferimento con un puntatore.


int value = 10;
int *data = &value; // data conterrà l'indirizzo di value
int tmp = *data; // tmp = 10

In buona sostanza l’istruzione *data può essere espressa letteralmente nel seguente modo:
“accedi al contenuto dell’oggetto puntato da data e non al contenuto di data stesso”.
Da questo punto di vista, quindi, *data può essere considerato un alias di value, ossia
qualsiasi manipolazione effettuata per il tramite di esso si ripercuoterà su value stessa
(Snippet 7.4 e Figura 7.5).

Snippet 7.4 Manipolazione tramite un puntatore dell’oggetto puntato.


int value = 10;
int *data = &value;

*data = 100; // value = 100


Figura 7.5 Contenuto di value prima e dopo l’operazione di deriferimento del puntatore data.

ATTENZIONE
Non applicare mai l’operatore di deriferimento a un puntatore non inizializzato con un
corretto indirizzo di memoria, altrimenti si potrà avere un comportamento non definito: crash
del programma (l’indirizzo di memoria memorizzato nel puntatore è al di fuori dell’address
space valido del programma), stampa di valori garbage o insensati (l’indirizzo di memoria
memorizzato nel puntatore mostra quello che in quel momento è presente a quell’indirizzo
quantunque valido) e così via.

Riepilogando, per utilizzare correttamente un puntatore, bisogna compiere le seguenti


fondamentali operazioni.
1. Utilizzo dell’operatore di indirezione * durante la fase di dichiarazione di un puntatore;
per esempio int *ptr_data.

2. Assegnamento di un indirizzo di memoria valido, tramite l’operatore di indirizzamento


&, di un oggetto dello stesso tipo di quello indicato durante la fase di dichiarazione di

un puntatore; per esempio ptr_data = &value (con value di tipo int).


3. Utilizzo dell’operatore di indirezione * durante la fase di manipolazione dell’oggetto
riferito da un puntatore; per esempio *ptr_data = 100.

DETTAGLIO
Perché al punto 2 si è precisato che l’indirizzo di memoria assegnato a un puntatore deve
essere di un oggetto dello stesso tipo da esso espresso in fase di dichiarazione? Perché un
puntatore, indipendentemente dall’oggetto puntato, conterrà sempre e solo un indirizzo di
memoria; pertanto, indicando durante la sua dichiarazione qual è il tipo di oggetto
localizzato a quell’indirizzo di memoria, si otterrà che, in fase di accesso, il contenuto di tale
locazione sarà interpretato correttamente. Per esempio, compilando e mandando in
esecuzione il Listato 7.1 avremo sia il messaggio: warning: assignment from incompatible
pointer type, sia un output dove il valore della variabile di tipo float non sarà stato
interpretato correttamente.

Listato 7.1 IncompatibleTypes.c (IncompatibleTypes).


/* IncompatibleTypes.c :: Tipi incompatibili con i puntatori :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int i_number = 100;
float f_number = 100.44f;

int *ptr_i_number = &i_number; // OK tipi compatibili


printf("Valore di i_number: %d\n", *ptr_i_number);

ptr_i_number = &f_number; // ATTENZIONE tipi non compatibili


printf("Valore di f_number: %f\n", *ptr_i_number);

return (EXIT_SUCCESS);
}

Output 7.1 Dal Listato 7.1 IncompatibleTypes.c.


Valore di i_number: 100
Valore di f_number: 0.000000
Puntatori come parametri di funzioni
Quando si definisce una funzione è possibile dichiarare dei parametri che sono di tipo
puntatore, cioè delle variabili che sono in grado di contenere degli indirizzi di memoria dei
corrispettivi argomenti (Sintassi 7.4 e 7.5).

Sintassi 7.4 Prototipo di funzione con un parametro di tipo puntatore.


return_type function_identifier(data_type *ptr_identifier); // con identificatore
return_type function_identifier(data_type *); // senza identificatore

Sintassi 7.5 Definizione di una funzione con un parametro di tipo puntatore.


return_type function_identifier(data_type *ptr_identifier) { ... }

Questa modalità di progettazione delle funzioni consente di “aggirare” il problema del


passaggio per valore degli argomenti poiché i parametri sono in grado, indirettamente, di
modificarne i valori.
Per il passaggio dei puntatori come argomenti di una funzione è possibile usare la
seguente modalità (Sintassi 7.6) dove, cioè, si passa direttamente un oggetto che è già di per
sé un tipo puntatore oppure si antepone l’operatore di indirizzamento & a un determinato
oggetto.

Sintassi 7.6 Invocazione di una funzione passando come argomento un puntatore.


function_identifier(ptr_identifier); // ptr_identifier è un puntatore
function_identifier(&identifier);// identifier è un qualsiasi oggetto

NOTA
In C gli argomenti sono passati, sempre, per valore anche se sono dei puntatori. Il fatto che
un parametro sia un puntatore e che consenta di modificare il relativo argomento non
implica che esista, nativamente, la modalità di passaggio di un argomento “per riferimento”
(come in C++) o “per indirizzo”. Infatti, quando si passa a una funzione l’indirizzo del suo
argomento è più corretto dire che si sta passando “un suo riferimento” e non che
l’argomento è passato “per riferimento”.

Listato 7.2 PointersAndPassByValue.c (PointersAndPassByValue).


/* PointersAndPassByValue.c :: Pass by value e puntatori :: */
#include <stdio.h>
#include <stdlib.h>

void foo(int *p);

int main(void)
{
int a = 10;
int *j = &a;

// per stampare 0x e l'indirizzo si sarebbe potuto usare anche %#p


printf("Indirizzo riferito da j [ 0x%p ] PRIMA del passaggio dell'argomento.\n", j);

foo(j); // passo un puntatore...

// per stampare 0x e l'indirizzo si sarebbe potuto usare anche %#p


printf("Indirizzo riferito da j [ 0x%p ] DOPO il passaggio dell'argomento "
"e p = &k;.\n", j);

return (EXIT_SUCCESS);
}

void foo(int *p)


{
int k = 100;
p = &k; // ok j non è interessato... pass by value
}

Output 7.2 Dal Listato 7.2 PointersAndPassByValue.c.


Indirizzo riferito da j [ 0x0028fee8 ] PRIMA del passaggio dell'argomento.
Indirizzo riferito da j [ 0x0028fee8 ] DOPO il passaggio dell'argomento e p = &k;.

Il Listato 7.2 mostra come anche un puntatore sia passato by value: infatti, quando nel
main viene invocata la funzione foo viene fatta una copia dell’indirizzo di memoria contenuto

nel puntatore j, e tale copia viene posta nel parametro p, anch’esso un puntatore.
A questo punto entrambi i puntatori referenziano lo stesso oggetto, ovvero la variabile a.
Poi, nell’ambito della funzione foo, al puntatore p viene assegnato un altro indirizzo di
memoria, quello della variabile k, senza però che tale assegnamento incida sull’indirizzo di
memoria dell’argomento j che rimane, infatti, inalterato.
A questo punto il puntatore j continua a puntare alla variabile a mentre il puntatore p
“rompe” il puntamento verso la variabile a e imposta un nuovo puntamento verso la
variabile k (Figura 7.6).

Figura 7.6 Immodificabilità di un puntatore passato come argomento a una funzione.

Scorrendo il sorgente è interessante notare l’utilizzo, nell’ambito della funzione printf,


dello specificatore di formato %p che consente di stampare, in modo dipendente
dall’implementazione, un numero esadecimale che rappresenta il valore della locazione di
memoria contenuta nel puntatore.
Mostriamo, infine, la scrittura di una funzione swap (Listato 7.3) che correttamente, grazie
all’impiego dei puntatori, consente di scambiare i valori di due variabili passate come
argomenti per il tramite dei corrispettivi parametri.

Listato 7.3 SwapWithPointers.c (SwapWithPointers).


/* SwapWithPointers.c :: Scambio di valori con l'uso dei puntatori :: */
#include <stdio.h>
#include <stdlib.h>

/* prototipo di swap */
void swap(int *w, int *z);

int main(void)
{
int a = 10, b = 20;
printf("a e b prima dello swap: a=%d - b=%d\n", a, b);

// passo i puntatori ad a e b
swap(&a, &b);

printf("a e b dopo lo swap: a=%d - b=%d\n", a, b);

return (EXIT_SUCCESS);
}

/* definizione di swap */
void swap(int *w, int *z)
{
int tmp = *w;
*w = *z;
*z = tmp;
}

Output 7.3 Dal Listato 7.3 PointersAndPassByValue.c.


a e b prima dello swap: a=10 - b=20
a e b dopo lo swap: a=20 - b=10

TERMINOLOGIA
Quando si passa a una funzione un argomento tipo &a si può anche direttamente dire che si
sta passando un puntatore ad a piuttosto che l’indirizzo di a perché, dato che l’operatore &

genera l’indirizzo di una variabile, &a ne rappresenta un puntatore.


Puntatori come valori di ritorno dalle funzioni
Una funzione può essere anche definita con la possibilità di avere come valore di ritorno
un tipo puntatore; per compiere quest’operazione è sufficiente dichiarare il puntatore nel
consueto modo, ossia tipo di dato, simbolo * e identificatore, e porlo come tipo di ritorno
prima dell’identificatore della relativa funzione (Snippet 7.5).

Snippet 7.5 Funzione che ritorna un tipo puntatore.


int *foo(void) { … }

La funzione foo dello Snippet 7.5 è definita con un valore di ritorno che è un puntatore a
un int, cioè dovrà ritornare un indirizzo di memoria di un oggetto dello stesso tipo di dato.
Quando si definisce una funzione in questo modo bisogna prestare attenzione a non
ritornare mai un indirizzo di memoria di una variabile locale automatica alla funzione
stessa: questo perché, quando la funzione ritorna, tale variabile cesserà di esistere e pertanto
il relativo puntatore sarà considerato invalido (Listato 7.4).

Listato 7.4 ReturningAPointer.c (ReturningAPointer).


/* ReturningAPointer.c :: Ritorno di un puntatore a una variabile locale :: */
#include <stdio.h>
#include <stdlib.h>

int *foo(void);
void bar(void);

int main(void)
{
int *p = foo();

printf("Valore di j per il tramite di *p: %d\n", *p);

bar(); // invoco un'altra funzione

printf("Valore di j per il tramite di *p: %d\n", *p);

return (EXIT_SUCCESS);
}

int *foo(void)
{
int j = 1000;
printf("Indirizzo di j in foo: %#p\n", &j);
return &j;
}

void bar(void)
{
int b = 2000;
printf("Indirizzo di b in bar: %#p\n", &b);
}

Output 7.4 Dal Listato 7.4 ReturningAPointer.c.


Indirizzo di j in foo: 0x28febc
Valore di j per il tramite di *p: 1000
Indirizzo di b in bar: 0x28febc
Valore di j per il tramite di *p: 2000
Il Listato 7.4 definisce la funzione foo che ritorna un puntatore alla variabile locale j lì
definita, la quale contiene il valore 1000, e la funzione bar, che non ritorna nulla e che
definisce la variabile locale b, che contiene il valore 2000.
La relativa funzione main invoca subito la funzione foo che ritorna nel puntatore p
l’indirizzo della variabile locale j. Poi, per il tramite del puntatore p, ne stampa il valore che
è congruo, ossia vale ancora 1000.
Successivamente invoca la funzione bar e poi, alla sua uscita, stampa di nuovo, sempre
per mezzo del puntatore p, il valore riferito, che però questa volta non è più congruo, ossia
non vale più 1000 ma 2000.
Il motivo di questo comportamento è spiegabile analizzando l’output del programma,
dove si nota che quando viene invocata la funzione foo il compilatore crea uno stack frame
dove pone la variabile locale j all’indirizzo di memoria 0x28febc che è ritornato al puntatore
p. Quando però la funzione foo termina, lo stack frame relativo viene rimosso dal function
call stack e la variabile locale j non esiste più e l’indirizzo di memoria 0x28febc diviene
invalido (potrà a questo punto essere usato per allocare altri dati), pur continuando a
contenere il valore 1000 come mostrato dalla successiva istruzione printf invocata nel main.
A questo punto viene invocata la funzione bar e viene creato un altro stack frame dove la
variabile locale b viene allocata all’indirizzo 0x28febc ancora disponibile e utilizzabile (è lo
stesso della ex variabile j) e lì viene posto il valore 2000.
All’uscita della funzione bar, però, il suo stack frame viene rimosso dal function call
stack e la variabile b viene distrutta; quando nel main è invocata in seguito la funzione printf,
il puntatore p, puntando ancora all’indirizzo di memoria 0x28febc, stampa l’ultimo valore
trovato, ossia 2000.
In pratica l’indirizzo di memoria 0x28febc impiegato per le variabili locali citate può
essere utilizzato da ogni invocazione di un nuova funzione per le proprie necessità; questo
spiega il problema della “sovrascrittura” del valore che in origine era di j.
NOTA
Il compilatore GCC, quando si compila il sorgente del Listato 7.4, emetterà il seguente
avviso: warning: function returns address of local variable. Un buon compilatore dovrebbe
dare sempre un avviso di questo tipo per evitare che un programmatore possa utilizzare un
puntatore a un indirizzo di memoria non valido.
Puntatori e array
Uno degli aspetti tra i più interessanti di C è la stretta correlazione tra puntatori e array.
Dato un array come, per esempio, data, la sua valutazione ritorna un indirizzo di memoria,
ossia un puntatore al suo primo elemento che si può quindi assegnare come valore a un
puntatore dello stesso tipo.

Snippet 7.6 Valutazione del nome di un array.


int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = data;

Figura 7.7 ptr_to_data, dopo l’assegnamento, punterà al primo elemento dell’array data.

Lo Snippet 7.6 definisce l’array data deputato a contenere 7 elementi di tipo intero e poi
ne assegna l’indirizzo del primo elemento (l’elemento 0 con valore 10) al puntatore a int
denominato ptr_to_data.
L’assegnamento di data a ptr_to_data è equivalente al seguente che, comunque, anche se
più esplicito e chiaro è raramente usato: int *ptr_to_data = &data[0].

Quanto sopra fa conseguire che qualsiasi accesso a un elemento di un array ottenibile


mediante la nota sintassi che fa uso dell’operatore di subscript [ ] e di un indice è anche
ottenibile tramite un puntatore e la possibilità di “aggiungere” o “sottrarre” da esso un
valore numerico che indica un valore di “scostamento” (o offset) rispetto all’attuale
indirizzo lì contenuto (Snippet 7.7).

Snippet 7.7 Utilizzo di un puntatore per accedere a un elemento di un array riferito.


int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = data;

// fa puntare ptr_to_data al quarto elemento dell'array


ptr_to_data += 3;
Figura 7.8 ptr_to_data dopo lo scostamento, in aggiunta, di 3 unità di storage.

Lo Snippet 7.7 evidenzia come aggiungendo il valore 3 all’attuale indirizzo in memoria di


ptr_to_data lo faccia puntare all’indirizzo di memoria del quarto elemento dell’array data
referenziato (in pratica all’indirizzo di “base” di 0x0041f7bc sono state aggiunte 3 “unità di
storage di scostamento” che l’hanno fatto incrementare al valore di 0x0041f7c8).
È qui importante precisare che gli scostamenti sono espressi in unità di storage perché
ogni tipo di dato necessità di una determinata quantità di memoria per allocare il
corrispondente valore (per esempio, su un sistema a 32 bit un int richiede 4 byte per
memorizzare un intero); pertanto aggiungere 1 unità a un puntatore a int significa spostare il
suo indirizzo di 4 byte e non di 1 byte.
Ritornando al nostro esempio, l’espressione ptr_to_data += 3 sposterà il puntatore di 12
byte (ossia 3 * 4 byte con un int di 32 bit) facendolo, quindi, puntare all’elemento 3
dell’array data.
Facciamo ora un ulteriore passo in avanti che mostra l’equivalenza tra l’aritmetica degli
indici propria di un array e quella propria dei puntatori. Un’espressione come data[i] si può
scrivere anche come *(data + i); ossia: data, essendo di fatto un puntatore al primo elemento
di un array, farà sì che data + i punterà all’i-esimo elemento di quell’array e l’operatore di
deriferimento * consentirà di ottenere il relativo valore.
In linea generale è dunque possibile asserire che &data[i] e data + i sono sinonimi, poiché
entrambi sono puntatori all’i-esimo elemento di data.
Per contro, è possibile utilizzare la notazione propria degli array anche con un puntatore:
per esempio, per accedere all’elemento 5 dell’array data per il tramite di ptr_to_data,
possiamo scrivere anche ptr_to_data[5].
ATTENZIONE
L’equivalenza tra un array e un puntatore è solo relativa alla possibilità di usare in modo
intercambiabile l’aritmetica dei puntatori con l’indicizzazione degli array. Tra di essi vi è
comunque un’importante differenza: il nome di un array è un lvalue non modificabile (per
esempio è un errore scrivere data++), mentre un puntatore è un lvalue modificabile (per
esempio è lecito scrivere ptr_to_data++). Quindi, un nome di un array non è un puntatore;
sono, infatti, tecnicamente, due oggetti distinti ed è solo in un determinato contesto
valutativo che il compilatore converte un array in un puntatore.

CURIOSITÀ
È lecito scrivere qualcosa come i[data] al posto di data[i]? La risposta è certamente
affermativa perché il compilatore, per effetto delle equivalenze discusse tra array e
puntatori, quando incontra un’espressione come questa la trasforma in *(i + data), che è
dunque corretta. Allo stesso modo data[i] sarebbe trasformata in *(data + i).

Aritmetica dei puntatori


L’aritmetica dei puntatori (pointer arithmetic), conosciuta anche come aritmetica degli
indirizzi (address arithmetic), dato un puntatore o dei puntatori che puntano a elementi di
un array, consente di compiere le seguenti operazioni.
Aggiungere un valore intero a un puntatore: consente di incrementare l’attuale
indirizzo di memoria riferito dal puntatore di tante unità di storage quante indicate dal
valore fornito. Così, se ptr_to_data punta all’elemento x dell’array data (data[x]),
aggiungere y a esso (ptr_to_data + y) lo farà puntare all’elemento di data posto y unità
di storage dopo l’attuale indirizzo di ptr_to_data (data[x + y]).

Snippet 7.8 Aggiungere un valore intero a un puntatore (Figura 7.9).


int x = 2;
int y = 3;
int data[] = {10, 100, 20, 40, 50, 60, 70};

int *ptr_to_data = &data[x]; // punta all'elemento 2 con valore 20


ptr_to_data = ptr_to_data + y; // ora punta all'elemento 5 con valore 60
Figura 7.9 Aggiungere un valore intero a un puntatore.

Sottrarre un valore intero da un puntatore: consente di decrementare l’attuale


indirizzo di memoria riferito dal puntatore di tante unità di storage quante indicate dal
valore fornito. Così, se ptr_to_data punta all’elemento x dell’array data (data[x]),
sottrarre y da esso (ptr_to_data - y) lo farà puntare all’elemento di data posto y unità di
storage prima dell’attuale indirizzo di ptr_to_data (data[x - y]).

Snippet 7.9 Sottrarre un valore intero da un puntatore (Figura 7.10).


int x = 5;
int y = 4;
int data[] = {10, 100, 20, 40, 50, 60, 70};

int *ptr_to_data = &data[x]; // punta all'elemento 5 con valore 60


ptr_to_data = ptr_to_data - y; // ora punta all'elemento 2 con valore 100
Figura 7.10 Sottrarre un valore intero da un puntatore.

Sottrarre un puntatore da un altro puntatore: consente di ottenere la distanza, in


elementi dell’array, tra due puntatori che puntano a indirizzi di memoria dell’array
medesimo. Così, se ptr_to_data punta all’elemento x dell’array data (data[x]) e
ptr_to_data_2 punta all’elemento y dell’array data (data[y]), sottrarre ptr_to_data_2 da
esso (ptr_to_data – ptr_to_data_2) oppure sottrarre ptr_to_data da ptr_to_data_2
(ptr_to_data_2 – ptr_to_data) restituirà il numero di elementi di distanza,
rispettivamente, in “negativo” nel primo caso e in “positivo” nel secondo caso.

Snippet 7.10 Sottrarre un puntatore da un altro puntatore (Figura 7.11).


int x = 1;
int y = 3;
int data[] = {10, 100, 20, 40, 50, 60, 70};

int *ptr_to_data = &data[x]; // punta all'elemento 1 con valore 100


int *ptr_to_data_2 = &data[y]; // punta all'elemento 3 con valore 40

ptrdiff_t distance_1 = ptr_to_data - ptr_to_data_2; // -2


ptrdiff_t distance_2 = ptr_to_data_2 - ptr_to_data; // 2
Figura 7.11 Dove i puntatori ptr_to_data e ptr_to_data_2 stanno correntemente puntando.

Lo Snippet 7.10 mostra come il tipo utilizzato per contenere la differenza tra due
puntatori sia ptrdiff_t, il quale è definito nel file header <stddef.h> (generalmente con un
typedef di un tipo intero con segno) ed è il modo “portabile” per esprimere tale differenza
(con la funzione printf il correlativo specificatore di formato utilizzabile è, per esempio,
%td).

ATTENZIONE
Se si compiono le operazioni di aritmetica dei puntatori qui citate con un puntatore che non
punta a un elemento di un array oppure con dei puntatori che non riferiscono elementi di
uno stesso array (come è il caso di sottrazione di un puntatore da un altro), il
comportamento sarà non definito. In ogni caso l’indirizzo di memoria subito successivo a
quello dell’ultimo elemento di un array è garantito essere valido.

Comparazione tra puntatori


Oltre alle operazioni proprie dell’aritmetica dei puntatori è possibile anche comparare dei
puntatori mediante l’utilizzo degli operatori relazionali (<, <=, >, >=) e di uguaglianza (==, !=);
ciò consente di sapere se, dati due puntatori che puntano a elementi dello stesso array, uno è
minore o maggiore di un altro oppure se uno è uguale o diverso da un altro (in quest’ultimo
caso gli operatori == e != possono essere usati anche con puntatori dello stesso tipo anche se
non riferiscono elementi dello stesso array).
I criteri di confronto sono effettuati sugli indirizzi di memoria lì contenuti. Così, se
ptr_to_data punta all’elemento x dell’array data (data[x]) e ptr_to_data_2 punta all’elemento y

dell’array data (data[y]), utilizzare l’operatore minore di (ptr_to_data < ptr_to_data_2)

ritornerà il valore 1 se l’indirizzo di memoria riferito da ptr_to_data sarà inferiore


all’indirizzo di memoria riferito da ptr_to_data_2 (e il valore 0 in caso contrario). Anche per
gli altri operatori il risultato del confronto sarà sempre 1 oppure 0.

Snippet 7.11 Comparazione di due puntatori (Figura 7.12).


int x = 4;
int y = 6;
int data[] = {10, 100, 20, 40, 50, 60, 70};
int *ptr_to_data = &data[x]; // punta all'elemento 4 con valore 50
int *ptr_to_data_2 = &data[y]; // punta all'elemento 6 con valore 70

// ptr_to_data -> indirizzo: 0x0041f7cc


// ptr_to_data_2 -> indirizzo: 0x0041f7d4
_Bool res = ptr_to_data < ptr_to_data_2; // 1 cioè vero!

Figura 7.12 Dove i puntatori ptr_to_data e ptr_to_data_2 stanno correntemente puntando.

ATTENZIONE
Se si compiono le operazioni di comparazione dei puntatori qui citate con gli operatori
relazionali, con dei puntatori che non riferiscono elementi di uno stesso array, il
comportamento sarà non definito. Tuttavia l’indirizzo di memoria subito successivo a quello
dell’ultimo elemento di un array è garantito essere valido e può essere utilizzato per un
confronto.

Puntatori e parametri di una funzione di tipo array


Quando definiamo una funzione con un parametro formale di tipo array, per il
compilatore esso è automaticamente interpretato come un puntatore al tipo di dato
dell’array; ossia, se un parametro formale è scritto come int data[], per il compilatore lo
stesso sarà trattato come int *data, ossia un puntatore a un int.
Quanto evidenziato è comunque perfettamente lecito perché quando si passa, poi, come
parametro attuale il nome di un array (per esempio numbers), quello che si fornisce è
l’indirizzo di memoria del suo primo elemento, ossia un puntatore all’elemento 0.
IMPORTANTE
È solo nell’ambito della definizione di un parametro formale di una funzione che per C vi è
un’equivalenza tra, per esempio, int data[] e int *data. Il parametro data è cioè sempre
considerato come un puntatore a un int (Listato 7.5).

Listato 7.5 ArrayAsPointerInParameterDefinition.c (ArrayAsPointerInParameterDefinition).


/* ArrayAsPointerInParameterDefinition.c :: Array vs puntatori come parametri di una funzione
:: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 6

// prototipo della funzione subtraction


// equivalenti --> int subtraction(int *data, int length);
// int subtraction(int [], int length);
// int subtraction(int *, int length);
int subtraction(int data[], int length);

int main(void)
{
int some_data[] = {369, 10, 15, 65, 88, 66};
printf("L'array some_data, nella funzione main, ha una dimensione di "
"%zu byte\n", sizeof some_data);

int res = subtraction(some_data, SIZE);

printf("Il risultato della sottrazione di tutti gli elementi di "


"some_data e': %d\n", res);

return (EXIT_SUCCESS);
}

// definizione della funzione subtraction


// equivalente --> int subtraction(int *data, int length) { ... }
int subtraction(int data[], int length)
{
printf("\"L'array data\", nella funzione subtraction, ha una dimensione "
"di %zu byte\n", sizeof data);

int result = *data;

// utilizzo dell'aritmetica dei puntatori per scorrere un array...


for (int *p = data + 1; p < data + length; p++)
result -= *p;

return result;
}

Output 7.5 Dal Listato 7.5 ArrayAsPointerInParameterDefinition.c.


L'array some_data, nella funzione main, ha una dimensione di 24 byte
"L'array data", nella funzione subtraction, ha una dimensione di 4 byte
Il risultato della sottrazione di tutti gli elementi di some_data e': 125

Il Listato 7.5 definisce una funzione subtraction che, dato un array come argomento, ne
restituisce un valore che è la differenza di tutti i valori dei suoi elementi.
L’importanza del programma del listato non risiede di sicuro nella funzione di
sottrazione, che è banale, quanto piuttosto perché evidenzia due aspetti di rilievo: il primo è
legato alla verifica che nell’ambito della definizione di una funzione un parametro di tipo
array è di fatto considerato come un puntatore a un suo elemento; il secondo è relativo a
come è possibile utilizzare l’aritmetica dei puntatori per scorrere gli elementi di un array
riferito da un puntatore in sostituzione della consueta indicizzazione.
Per quanto riguarda il primo aspetto, lo stesso è verificabile guardando all’output del
programma, dove:
l’operatore sizeof applicato all’array some_data nell’ambito della funzione main dà come
risultato il valore 24 in accordo con il fatto che è un tipo array di int che contiene 6
elementi di tipo int di 4 byte ciascuno sul corrente sistema a 32 bit;
l’operatore sizeof applicato nell’ambito della funzione subtraction dà come risultato il
valore 4 in accordo con il fatto che è un tipo puntatore a un int, e dunque sul corrente
sistema a 32 bit 4 byte sono lo spazio utilizzato per allocare un tipo puntatore atto a
contenere un indirizzo di memoria.
Il secondo aspetto è invece dimostrabile nel ciclo for della funzione subtraction: qui si
utilizza espressamente l’aritmetica dei puntatori per manipolare il parametro data fornendo
al puntatore p l’indirizzo dell’elemento 1 (data + 1) e verificando, come condizione di
continuazione del ciclo, che l’indirizzo corrente di p sia nel range di indirizzi dove sia
possibile validamente ottenere un valore numerico da computare (p < data + length)

mediante l’operatore di deriferimento (result -= *p).

Puntatori e array multidimensionali


Dato un array multidimensionale, come per esempio quello a due dimensioni definito
nello Snippet 7.12 e con la rappresentazione in memoria fornita dalla Figura 7.13, possiamo
tracciare, al pari di quanto già fatto per gli array monodimensionali, come si relazioni
rispetto a un puntatore.

Snippet 7.12 Definizione di un array bidimensionale.


int data[2][3] = // 2 righe per 3 colonne
{
{1,2,3}, // I riga
{-1, -2, -3} // II riga
};

Figura 7.13 Rappresentazione tabellare e in memoria della matrice data.

Prima di procedere oltre ricordiamo che per C: un array a due dimensioni è un array di
array, ossia un array a una dimensione dove ogni elemento è esso stesso un altro array; la
disposizione in memoria di un array a due dimensioni è fatta riga per riga (row major order,
prima la riga 0, poi la riga 1 e così via per tutte le altre righe).
NOTA
Per semplicità useremo gli array a due dimensioni come forma di array multidimensionale. I
concetti generali esposti saranno comunque validi anche per array di dimensioni maggiori.
Nel prossimo elenco riportiamo la “valutazione” da parte del compilatore
dell’identificatore data, ossia a che tipo di puntatore “corrisponde” quando gli applichiamo o
meno l’operatore di indirizzamento &.

1. data corrisponde a int (*)[3], ossia a un puntatore a un array di 3 elementi. Esso ritorna
l’indirizzo di memoria dell’elemento 0 (0x00a2fbac) che è in pratica visualizzabile come
la riga 0 della matrice contenente i valori 1, 2 e 3.
2. &data[0] corrisponde a int (*)[3], ossia a un puntatore a un array di 3 elementi. Esso
ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac) che è in pratica
visualizzabile come la riga 0 della matrice contenente i valori 1, 2 e 3. Quindi, data e
&data[0] sono “sinonimi”.
3. data[0] corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di
memoria dell’elemento 0 (0x00a2fbac) che è in pratica visualizzabile come la riga 0
della matrice contenente i valori 1, 2 e 3.
4. &data[0][0] corrisponde a int *, ossia a un puntatore a int. Esso ritorna l’indirizzo di
memoria (0x00a2fbac) dell’elemento che si trova alla riga 0 e alla colonna 0;
5. &data corrisponde a int (*)[2][3], ossia a un puntatore a un array di 2 righe per 3
colonne. Esso ritorna l’indirizzo di memoria (0x00a2fbac) che è in pratica quello a
partire dal quale inizia tutto l’array a due dimensioni.
Le espressioni viste sono importanti perché evidenziano che, pur ritornando tutte lo
stesso indirizzo di memoria, avranno comunque dei tipi di puntatori differenti, e ciò ci sarà
utile per comprendere cosa accadrà quando dovremo applicare l’operatore di deriferimento *
su di esse. Avremo infatti quanto segue
1. *data corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di
memoria dell’elemento 0 (0x00a2fbac) che è in pratica visualizzabile come la riga 0
della matrice contenente i valori 1, 2 e 3.
2. *&data[0] corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di
memoria dell’elemento 0 (0x00a2fbac) che è in pratica visualizzabile come la riga 0
della matrice contenente i valori 1, 2 e 3.
3. *data[0] corrisponde a un int. Esso ritorna il valore dell’elemento, ossia 1, posto alla
colonna 0 e riga 0.
4. *&data[0][0] corrisponde a un int. Esso ritorna il valore dell’elemento, ossia 1, posto

alla colonna 0 e riga 0.


5. *&data corrisponde a int (*)[3], ossia a un puntatore a un array di 3 elementi. Esso
ritorna l’indirizzo di memoria dell’elemento 0 (0x00a2fbac) che è in pratica
visualizzabile come la riga 0 della matrice contenente i valori 1, 2 e 3.

In più se si vuole utilizzare in modo assoluto la notazione puntatore/offset in alternativa a


quella propria degli array allora avremo che:
1. data + r sposta il puntatore alla riga indicata da r e ritorna un tipo int (*)[3]. Per
esempio, data + 1 sposterà il puntatore alla riga 1 ossia all’indirizzo 0x00a2fbb8. Ciò
avviene perché data è un puntatore a un array di 3 colonne e pertanto aggiungere
un’unità di storage a esso ne farà incrementare l’indirizzo di 12 byte (3 elementi della
riga per la dimensione di 4 byte propria di un int sul corrente sistema di 32 bit);
2. *(data + r) sposta il puntatore alla riga indicata da r e poi la dereferenzia ritornando un
tipo int *. Per esempio, *(data + 1) sposterà il puntatore alla riga 1 e tornerà un
riferimento al suo primo elemento posto all’indirizzo 0x00a2fbb8;
3. *(data + r) + c sposta il puntatore alla riga indicata da r, poi la dereferenzia ritornando
l’indirizzo di memoria dell’elemento 0 come tipo int * che poi sposta di c unità di
storage. Per esempio, *(data + 1) + 1 sposterà il puntatore alla riga 1 e colonna 1, ossia
all’indirizzo 0x00a2fbbc. Ciò avviene perché il puntatore da spostare si riferisce a quello
che “muove” le colonne dove ogni unità di storage vale 4 byte (ossia un int di 32 bit
sull’attuale sistema);
4. *(*(data + r) + c) sposta il puntatore alla riga indicata da r, poi la dereferenzia

ritornando l’indirizzo di memoria dell’elemento 0 come tipo int * che poi sposta di c
unità di storage. Infine, dereferenzia tale indirizzo di memoria ritornando un tipo int.
Per esempio *(*(data + 1) + 1), ritornerà il valore -2 che è posto nella colonna 1 della
riga 1.
Quanto mostrato sinora è senza dubbio complesso e richiede una certa attenzione e
pazienza per comprendere in modo ottimale la relazione tra un puntatore e un array
bidimensionale; comunque ciò non deve indurre a eccessive preoccupazioni perché, quando
si scrivono programmi che fanno uso di array bidimensionali, è sufficiente:
per accedere a un determinato elemento, utilizzare la più semplice notazione con
indicizzazione propria degli array (per esempio, data[1][1] accede alla colonna 1 della
riga 1);
per impiegare un parametro formale di tipo array bidimensionale, dichiararlo con la
consueta notazione con indicizzazione degli array già vista (per esempio void foo(int
data[][3]) { … }) oppure con la notazione che fa uso dei puntatori (per esempio void
foo(int (*data)[3]) { … }).

ATTENZIONE
Le parentesi ( ) intorno a *data sono essenziali. Se le omettessimo, la dichiarazione relativa,
ossia int *data[3], per effetto della più alta precedenza dell’operatore [ ] rispetto
all’operatore *, significherebbe che data è un array di 3 puntatori a int. Invece, scrivere int

(*data)[3] fa sì che l’espressione significhi che data è un puntatore a un array di 3 elementi


di tipo int.

Listato 7.6 PointerToArrayAsParameter.c (PointerToArrayAsParameter).


/* PointerToArrayAsParameter.c :: Puntatori come parametri di una funzione per array 2d :: */
#include <stdio.h>
#include <stdlib.h>

#define ROWS 3
#define COLS 5

/* prototipo della funzione search */


// equivalente -> int search(int (*ptr_to_data)[COLS], int rows);
int search(int (*)[COLS], int);

int main(void)
{
int data[][COLS] =
{
{1, 2, 3, 4, 5},
{-4, -6, 10, 2, 9},
{100, -100, 33, 34, 24}
};

// invocazione di search
int res = search(data, ROWS);

printf("La matrice data contiene %d numeri negativi!\n", res);

return (EXIT_SUCCESS);
}

/* definizione della funzione search */


int search(int (*ptr_to_data)[COLS], int rows)
{
int nr = 0;

// qui r++ fa spostare alla riga successiva della matrice perché


// ptr_to_data è di tipo int (*)[5]
for (int r = 0; r < rows; r++)
{
// qui c++ fa spostare alla colonna successiva della riga corrente perché
// *(ptr_to_data + r) è di tipo int *
for (int c = 0; c < COLS; c++)
{
// sintassi alternativa puntatore/offset a quella propria degli array
int val = *(*(ptr_to_data + r) + c);
if (val < 0)
nr++;
}
}
return nr;
}

Output 7.6 Dal Listato 7.6 PointerToArrayAsParameter.c.


La matrice data contiene 3 numeri negativi!

Il Listato 7.6 esplicita in modo pratico, nell’ambito della funzione search, come utilizzare
la notazione puntatore/offset per riferirsi agli elementi di una matrice. Il ciclo for più esterno
si occupa di far spostare il puntatore corrente alla riga successiva; lo fa incrementando
ptr_to_data di una unità di storage alla volta che è pari a 20 byte perché ogni riga contiene 5

elementi di tipo int con un int di 4 byte (ricordiamo che ptr_to_data è di tipo int (*)[5]). Il
ciclo for più interno, invece, fa spostare il corrente puntatore alla colonna successiva, e lo fa
incrementando *(ptr_to_data + r) di una unità di storage alla volta che è pari a 4 byte perché
ogni colonna è un elemento di tipo int con un int di 4 byte (ricordiamo che *(ptr_to_data +

r) è di tipo int *).


Infine, applica di nuovo l’operatore di deriferimento sul puntatore ritornato in modo da
ottenere il valore della corrente colonna.
Per comprendere in modo meno astratto quanto detto, la Figura 7.14 mostra dove si
troverà il puntatore ptr_to_data quando r varrà 1 e c varrà 2, ossia sulla colonna 2 della riga 1
(elemento con valore 10).

Figura 7.14 Visualizzazione del puntatore ptr_to_data dopo uno spostamento per offset.

Per quanto concerne, infine, la scrittura dell’array bidimensionale come parametro di una
funzione, possiamo ora comprendere perché una sintassi come int data[][] non sarebbe mai
accettata dal compilatore; dato che esso “converte” la notazione a indicizzazione degli array
come notazione a puntatore, quando valuterà qualcosa come data + 1 non avrà dati a
sufficienza per sapere di quante unità di storage spostare il puntatore corrente alla prossima
riga.
Ecco, quindi, perché bisogna sempre indicare il numero di colonne dell’array mentre non
è obbligatorio indicare il numero di righe (il parametro int data[][COLS] è trattato dal
compilatore come int (*data)[COLS], e infatti il relativo argomento passato è un puntatore a
un array di COLS colonne).

Array di puntatori
Gli array di puntatori sono vettori dove ciascun elemento è un puntatore a un determinato
tipo. Così, una dichiarazione come int *data[4] stabilisce che il nome data è un array di 4
elementi ciascuno dei quali contiene come valore un indirizzo di memoria di un tipo int
ossia un suo puntatore.
Questo tipo di oggetto si presta in modo ottimale a creare i cosiddetti array triangolari o
irregolari, ossia array dove ogni riga può avere un numero di colonne differente.

Snippet 7.13 Un array irregolare.


// array di 4 puntatori a int
int *data[] =
{
(int[]) {1, 2}, // 2 colonne
(int[]) {3, 4, 5}, // 3 colonne
(int[]) {6, 7, 8, 9}, // 4 colonne
(int[]) {10, 11, 12, 13, 14} // 5 colonne
};

Lo Snippet 7.13 crea l’array data composto da 4 righe dove ciascuna riga punta a un array
di colonne, definito con la sintassi “letterale”, di differente dimensione.
Poiché il nome di un array è valutato come un puntatore al suo primo elemento, di fatto,
ogni elemento di data ne contiene un adeguato riferimento ossia un puntatore a int.
Così, data[0] conterrà l’indirizzo di memoria del primo elemento del primo array letterale
(elementi con valori 1 e 2), data[1] conterrà l’indirizzo di memoria del primo elemento del
secondo array letterale (elementi con valori 3, 4 e 5) e così via per data[2] e data[3].
Per quanto riguarda la valutazione da parte del compilatore del nome data, da solo e con
l’operatore di indirizzo &, abbiamo che:

1. data corrisponde a int **, ossia a un puntatore a un puntatore a int. Esso ritorna
l’indirizzo di memoria dell’elemento 0 (0x0028fe90) che è in pratica visualizzabile come
il primo puntatore all’array contenente i valori 1 e 2;
2. &data[0] corrisponde a int **, ossia a un puntatore a un puntatore a int. Esso ritorna
l’indirizzo di memoria dell’elemento 0 (0x0028fe90) che è in pratica visualizzabile come
il primo puntatore all’array contenente i valori 1 e 2;
3. data[0] corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di
memoria riferito dall’elemento 0 (0x0028fea0) che è in pratica visualizzabile come il
primo elemento dell’array puntato (valore 1);
4. &data[0][0] corrisponde a int *, ossia a un puntatore a int. Esso ritorna l’indirizzo di
memoria (0x0028fea0) dell’elemento 0 del primo array riferito (valore 1);
5. &data corrisponde a int *(*)[4], ossia a un puntatore a un array di 4 elementi a puntatori
a int. Esso ritorna l’indirizzo di memoria (0x0028fe90) che è in pratica quello a partire
dal quale inizia tutto l’array di puntatori a int.

Figura 7.15 Rappresentazione tabellare e in memoria con GCC della matrice irregolare data.

Invece, per la valutazione di data con l’operatore di deriferimento * abbiamo che:

1. *data corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di


memoria riferito dall’elemento 0 (0x0028fea0) che è in pratica visualizzabile come il
primo elemento dell’array puntato (valore 1);
2. *&data[0] corrisponde a int *, ossia a un puntatore a un int. Esso ritorna l’indirizzo di
memoria riferito dall’elemento 0 (0x0028fea0) che è in pratica visualizzabile come il
primo elemento dell’array puntato (valore 1);
3. *data[0] corrisponde a un int. Esso ritorna il valore dell’elemento posto alla colonna 0
e riga 0 del primo array riferito, ossia 1;
4. *&data[0][0] corrisponde a un int. Esso ritorna il valore dell’elemento posto alla
colonna 0 e riga 0 del primo array riferito, ossia 1;
5. *&data corrisponde a int **, ossia a un puntatore a un puntatore di int. Esso ritorna
l’indirizzo di memoria dell’elemento 0 (0x0028fe90) che è in pratica visualizzabile come
il primo puntatore all’array contenente i valori 1 e 2.

Infine, utilizzando in modo assoluto la notazione puntatore/offset in alternativa a quella


propria degli array, avremo che:
data + r sposta il puntatore alla riga indicata da r e ritorna un tipo int **. Per esempio,
data + 1 sposterà il puntatore alla riga 1, ossia all’indirizzo 0x0028fe94. Ciò avviene
perché data è un puntatore a un puntatore di int; pertanto aggiungere un’unità di
storage a esso ne farà incrementare l’indirizzo di 4 byte, perché tale dimensione, sul
sistema in uso, è quella usata per allocare un puntatore a un int;

DETTAGLIO
In sostanza in questo caso, rispetto al puntatore data di int (*data)[4], il nome data è
l’indirizzo di memoria del primo puntatore a int allocato che contiene un puntamento verso il
primo array (quello con i valori 1 e 2). Quindi l’incremento di una unità farà spostare il
puntamento corrente al secondo puntatore a int che contiene un puntamento verso il
secondo array (quello con i valori 3, 4 e 5). Lo spostamento sarà di 4 byte alla volta perché
con il sistema corrente il compilatore userà 4 byte per allocare un tipo puntatore a int. La
Figura 7.15 evidenzia, per esempio, come &data[0] sia il primo puntatore allocato all’indirizzo
0x0028fe90 (l’elemento 0 dell’array data), che contiene come valore l’indirizzo 0x0028fea0, che è
l’area di memoria a partire da cui si troveranno gli elementi dell’array riferito (in pratica la
prima riga dell’array data).

*(data + r) sposta il puntatore alla riga indicata da r e poi la dereferenzia ritornando un


tipo int *. Per esempio, *(data + 1) sposterà il puntatore alla riga 1 e tornerà un
riferimento al suo primo elemento posto all’indirizzo 0x0028fea8;
*(data + r) + c sposta il puntatore alla riga indicata da r e poi la dereferenzia ritornando
l’indirizzo di memoria dell’elemento 0 come tipo int *, che poi sposta di c unità di
storage. Per esempio, *(data + 1) + 1 sposterà il puntatore alla riga 1 e colonna 1, ossia
all’indirizzo 0x0028feac. Ciò avviene perché il puntatore da spostare si riferisce a quello
che “muove” le colonne dove ogni unità di storage vale 4 byte (ossia un int di 32 bit
sull’attuale sistema);
*(*(data + r) + c) sposta il puntatore alla riga indicata da r e poi la dereferenzia

ritornando l’indirizzo di memoria dell’elemento 0 come tipo int *, che poi sposta di c
unità di storage. Infine, dereferenzia tale indirizzo di memoria ritornando un tipo int.
Per esempio *(*(data + 1) + 1), ritornerà il valore 4 che è posto nella colonna 1 della
riga 1.
Quanto mostrato, seppur complesso, si può sintetizzare dicendo che:
per accedere a un determinato elemento della matrice, si può utilizzare la più semplice
notazione con indicizzazione propria degli array (per esempio data[1][1] accede alla
colonna 1 della riga 1);
per impiegare un parametro formale di tipo array bidimensionale irregolare, si può
dichiararlo con la consueta notazione con indicizzazione degli array (per esempio void
foo(int *data[]) { … }) oppure con la notazione che fa uso esclusivo dei puntatori (per

esempio void foo(int **data) { … }).

Listato 7.7 ArrayOfPointersAsParameter.c (ArrayOfPointersAsParameter).


/* ArrayOfPointersAsParameter.c :: Array di puntatori come parametri di una funzione per array
2d :: */
#include <stdio.h>
#include <stdlib.h>

#define ROWS 4
#define COLS_I 2
#define COLS_II 3
#define COLS_III 4
#define COLS_IV 5

// prototipo della funzione search


// equivalente --> int search(int **, int);
// --> int search(int *ptr_to_data[], int rows);
// --> int search(int **ptr_to_data, int rows);
int search(int *[], int);

int main(void)
{
// array di 4 puntatori a int
int *data[] =
{
(int[]) {1, 2}, // 2 colonne
(int[]) {3, -4, 5}, // 3 colonne
(int[]) {6, -7, 8, 9}, // 4 colonne
(int[]) {10, 11, -12, 13, -14} // 5 colonne
};

// invocazione di search
int res = search(data, ROWS);

printf("La matrice data contiene %d numeri negativi!\n", res);

return (EXIT_SUCCESS);
}

// definizione della funzione search


// equivalente --> int search(int **ptr_to_data, int rows) { ... }
int search(int *ptr_to_data[], int rows)
{
int nr = 0;
int cols_nr = 0;

// qui r++ fa spostare alla riga successiva della matrice perché


// ptr_to_data è di tipo int **
for (int r = 0; r < rows; r++)
{
switch(r) // necessario per sapere quante colonne ha la corrente riga
{
case 0: cols_nr = COLS_I; break;
case 1: cols_nr = COLS_II; break;
case 2: cols_nr = COLS_III; break;
case 3: cols_nr = COLS_IV; break;
}

// qui c++ fa spostare alla colonna successiva della riga corrente perché
// *(ptr_to_data + r) è di tipo int *
for (int c = 0; c < cols_nr; c++)
{
// sintassi alternativa puntatore/offset a quella propria degli array
int val = *(*(ptr_to_data + r) + c);
if (val < 0)
nr++;
}
}
return nr;
}

Output 7.7 Dal Listato 7.7 ArrayOfPointersAsParameter.c.


La matrice data contiene 4 numeri negativi!

Il Listato 7.7 è simile come logica al Listato 7.6, ossia data una matrice ne deve cercare
gli elementi che contengono dei numeri negativi.
Tuttavia presenta le seguenti importanti differenze:
la matrice data è dichiarata come array di puntatori a int ed è irregolare, ossia ogni riga
ha un diverso numero di colonne;
il prototipo e la definizione della funzione search hanno un parametro dichiarato,
rispettivamente, come int *[] e int *ptr_to_data[] (come detto, sarebbe preferibile
scrivere i prototipi di funzione indicando anche gli identificatori dei tipi dei relativi
parametri; tuttavia in taluni casi, come quello appena indicato, abbiamo ritenuto
opportuno non farlo per ragioni di chiarezza);
dato che non è possibile sapere dinamicamente quante colonne ha ciascuna riga [per
esempio, un sizeof(ptr_to_data) quando ptr_to_data si riferisce alla riga 0 darebbe come
valore 4 e non 8 perché tanti sono i byte che occorrono per allocare un tipo int *],
siamo costretti a utilizzare un costrutto switch che a seconda della riga corrente
valorizza la variabile cols_nr con la costante simbolica relativa (per esempio, se r vale 0
allora cols_nr conterrà il valore di COLS_I e così via per gli altri valori di r).

Puntatori e array di lunghezza variabile


Un puntatore può riferire anche un array, sia monodimensionale sia multidimensionale,
dichiarato con una lunghezza variabile (VLA, variable-length array).
Snippet 7.14 Puntatori a VLA.
// puntatore che riferisce un array monodimensionale
int nr_of_el = 5;
int data[nr_of_el];
int *ptr_to_data = data;

// puntatore che riferisce un array bidimensionale (un puntatore a un array di 5 colonne)


int nr_of_rows = 5;
int nr_of_cols = 5;
int data_m[nr_of_rows][nr_of_cols];
int (*ptr_to_data_m)[nr_of_cols] = data_m;

// puntatore che riferisce un array bidimensionale (un array di 5 puntatori a int)


int *data_o_m[nr_of_rows];
int **ptr_to_data_o_m = data_o_m;

I puntatori dichiarati nello Snippet 7.14 sono definiti dallo standard C11 come variably
modified types perché il loro tipo dipende da valori non costanti: dal valore della variabile
nr_of_el, nel caso di ptr_to_data, e dal valore della variabile nr_of_rows, nel caso di

ptr_to_data_m e ptr_to_data_o_m.
TERMINOLOGIA
Un variably modified (VM) type è nella sostanza un puntatore a un array a lunghezza
variabile (VLA).

Questi puntatori hanno poi, al pari dei VLA, alcune restrizioni come, per esempio:
possono essere dichiarati solo nelle funzioni (anche come parametri) o in qualsiasi blocco di
codice oppure come parametri formali nei prototipi delle funzioni; i relativi identificatori
devono essere degli identificatori ordinari (non possono essere, per esempio, identificatori
di puntatori a VLA che sono membri di struct o union).
Infine, per essi, l’aritmetica dei puntatori è ben definita, si comporta cioè come se tali
puntatori puntassero ad array non VLA.
Puntatori a puntatori
Un puntatore a puntatore, già incontrato nel corso della trattazione sugli array di
puntatori, è, in linea più generale, una variabile che contiene un indirizzo di memoria di
un’altra variabile la quale contiene, anch’essa, un indirizzo di memoria di un’altra variabile
che contiene un valore di un tipo determinato (Snippet 7.15).

Snippet 7.15 Puntatore a puntatore.


// dichiarazione di un doppio puntatore...
int number = 100;
int *ptr_to_number = &number;
int **ptr_to_ptr_to_number = &ptr_to_number;

// un deriferimento
// ritorna come valore l'indirizzo di memoria contenuto in ptr_to_number
int *first_der = *ptr_to_ptr_to_number;

// doppio deriferimento
// ritorna come valore il numero 100 che è contenuto in number
int value = **ptr_to_ptr_to_number;

In sostanza lo Snippet 7.15 si può leggere come segue, considerando anche, per i primi
tre punti, la Figura 7.16.
1. Dichiariamo la variabile di tipo int number contenente il valore 100 che viene allocata
all’indirizzo di memoria 0x006ffb84.
2. Dichiariamo la variabile di tipo puntatore a int ptr_to_number contenente il valore
0x006ffb84 (che è l’indirizzo di memoria di number) che viene allocata all’indirizzo di
memoria 0x006ffb78.
3. Dichiariamo la variabile di tipo puntatore a puntatore a int ptr_to_ptr_to_number
contenente il valore 0x006ffb78 (che è l’indirizzo di memoria di ptr_to_number) che viene
allocata all’indirizzo di memoria 0x006ffb6c.
4. Dichiariamo la variabile di tipo puntatore a int first_der contenente il valore 0x006ffb84
(che è l’indirizzo di memoria di number). Essa contiene tale indirizzo perché l’operatore
di deriferimento applicato sul nome ptr_to_ptr_to_number fa ritornare il valore contenuto
nell’indirizzo di memoria 0x006ffb78 che è, per l’appunto, 0x006ffb84.
5. Dichiariamo la variabile di tipo int value contenente il valore 100. Essa contiene tale
valore perché il primo operatore di deriferimento applicato sul nome
ptr_to_ptr_to_number fa ritornare il valore 0x006ffb84 (che è l’indirizzo di memoria di

number), e poi l’altro operatore di deriferimento applicato su tale indirizzo fa ritornare,


per l’appunto, il valore 100.
Figura 7.16 Rappresentazione grafica di un doppio puntatore.

Vediamo ora un pratico esempio di utilizzo di un doppio puntatore (Listato 7.7) che
consente di “simulare” un passaggio per riferimento di un argomento a un parametro di un
funzione (ricordiamo che in C gli argomenti sono passati sempre per valore), in modo che
venga modificato l’argomento stesso piuttosto che il valore da esso riferito (per effetto di
ciò il parametro della funzione può essere considerato una sorta di alias dell’argomento,
ossia un nome alternativo cui riferirlo e attraverso cui manipolarlo).

Listato 7.8 SimulatingPassByReference.c (SimulatingPassByReference).


/* SimulatingPassByReference.c :: Pass by reference con i doppi puntatori :: */
#include <stdio.h>
#include <stdlib.h>

void foo(int **p);

int main(void)
{
int a = 10;
int *j = &a;

// per stampare 0x si sarebbe potuto usare anche %#p


printf("Indirizzo riferito da j [ 0x%p ] PRIMA del passaggio dell'argomento\n", j);

foo(&j); // passo l'indirizzo di memoria di j esso stesso puntatore...

// per stampare 0x si sarebbe potuto usare anche %#p


printf("Indirizzo riferito da j [ 0x%p ] DOPO il passaggio dell'argomento e "
"*p = &k;\n", j);

return (EXIT_SUCCESS);
}

void foo(int **p)


{
static int k = 100;
*p = &k; // j è interessato... simulazione del pass by reference
}

Output 7.8 Dal Listato 7.8 SimulatingPassByReference.c.


Indirizzo riferito da j [ 0x0037fed8 ] PRIMA del passaggio dell'argomento
Indirizzo riferito da j [ 0x003a901c ] DOPO il passaggio dell'argomento e *p = &k;

Nel Listato 7.8 la funzione main dichiara la variabile a di tipo int e poi la variabile j come
puntatore a essa. Stampa quindi l’indirizzo di memoria contenuto in j (0x0037fed8) che è
quello dove la variabile a è stata allocata. Invoca poi la funzione foo alla quale passa
l’indirizzo di memoria dove è stato allocato il puntatore j medesimo (0x0037fecc).
A questo punto la funzione foo, che prende il controllo dell’esecuzione del codice,
definisce la variabile locale statica k e poi ne assegna l’indirizzo di memoria (0x003a901c) a
ciò cui punta p, ossia al puntatore j, che d’ora in poi punterà a questa nuova variabile
piuttosto che a quella originaria a.
TERMINOLOGIA
Una variabile locale a una funzione si definisce statica quando conserva il suo valore anche
se il flusso di esecuzione del codice esce dal blocco ove è dichiarata. Per definire una
variabile come statica si deve usare lo specificatore di classe di memorizzazione espresso
tramite la keyword static. Ritorneremo in modo approfondito su questo punto nel Capitolo 9.

NOTA
Nella funzione foo si è reso necessario dichiarare la variabile k come statica perché non
viene distrutta al termine dell’esecuzione della funzione, e dunque il suo indirizzo di
memoria, riferito dal puntatore j dopo l’assegnamento a esso compiuto per mezzo
dell’istruzione *p = &k;, resta ancora valido (non è utilizzato per allocare altre variabili) così
come il valore lì contenuto.

Quando, infine, il flusso di esecuzione del codice ritorna nella funzione main, la
successiva istruzione printf stampa nuovamente il valore dell’indirizzo di memoria riferito
dal puntatore j (0x003a901c) che, questa volta, è quello non più alla variabile a, ma quello
della variabile k.
Dunque, per il tramite del parametro p della funzione foo è stato possibile cambiare
l’argomento stesso riferito grazie a un’implementazione “manuale” del meccanismo del
passaggio degli argomenti by reference non presente, di “serie”, nel linguaggio C.
La Figura 7.17 dà una panoramica visuale di quanto descritto, sia dal punto di vista dei
puntamenti sia della disposizione in memoria delle variabili utilizzate.
Figura 7.17 Modificabilità di un puntatore passato come argomento a un doppio puntatore.

In sostanza, dall’analisi della disposizione in memoria della variabili mostrata nella


Figura 7.17 abbiamo che, prima dell’assegnamento di *p = &k nella funzione foo:

1. la variabile a di tipo int è allocata all’indirizzo di memoria 0x0037fed8;


2. la variabile j di tipo puntatore a int è allocata all’indirizzo di memoria 0x0037fecc e
contiene come valore l’indirizzo di a, ossia 0x0037fed8;
3. la variabile p di tipo puntatore a puntatore a int è allocata all’indirizzo di memoria
0x0037fdf8 e contiene come valore l’indirizzo di memoria di j, ossia 0x0037fecc;
4. la variabile k di tipo puntatore a int è allocata all’indirizzo di memoria 0x003a901c.

Al termine delle operazioni avremo che p conterrà un riferimento a j che conterrà un


riferimento ad a che conterrà il valore 10.
Dopo l’assegnamento di *p = &k nella funzione foo abbiamo, invece, la seguente
disposizione in memoria delle stesse variabili:
1. la variabile a di tipo int continua a essere allocata all’indirizzo di memoria 0x0037fed8;
2. la variabile j di tipo puntatore a int continua a essere allocata all’indirizzo di memoria
0x0037fecc, ma ora contiene come valore l’indirizzo di k ossia 0x003a901c;
3. la variabile p di tipo puntatore a puntatore a int continua a essere allocata all’indirizzo
di memoria 0x0037fdf8 e a contenere come valore l’indirizzo di memoria di j, ossia
0x0037fecc;

4. la variabile k di tipo puntatore a int continua a essere allocata all’indirizzo di memoria


0x003a901c.

Cos’è accaduto, quindi, di diverso dopo l’esecuzione dell’istruzione *p = &k;? In pratica


&k dice al compilatore di fornire l’indirizzo di memoria della variabile k (0x003a901c) e di
assegnarlo come contenuto all’indirizzo di memoria contenuto in p (0x0037fecc)
opportunamente dereferenziato. Tale indirizzo di memoria, appartenendo a j, conterrà un
nuovo indirizzo di puntamento, non più verso la variabile a ma verso la variabile k. Al
termine delle operazioni avremo che p conterrà un riferimento a j che conterrà un
riferimento a k che conterrà il valore 100.
Puntatori a funzione
C, da quello straordinario linguaggio che è, consente di utilizzare i puntatori anche per
memorizzare in essi un indirizzo di memoria di una funzione, ossia di “codice” piuttosto
che di “dati”, come è il caso dei puntatori sin qui analizzati.
Quanto detto non deve sorprendere: se un puntatore è una variabile deputata a contenere
come valore un indirizzo di memoria allora, dal “suo punto di vista”, non fa alcuna
differenza se tale indirizzo è quello utilizzato per allocare una variabile oppure per caricare
del codice di una funzione (l’indirizzo di memoria di una funzione è quel punto della
memoria a partire dal quale inizia il codice eseguibile della funzione stessa).

Sintassi 7.8 Dichiarazione di un puntatore a funzione.


data_type (*fptr_identifier)(void | parameters_list);

La Sintassi 7.8 evidenzia che la dichiarazione di un puntatore a funzione è espressa


scrivendola come un normale prototipo di funzione, ma con la differenza che l’identificatore
della funzione deve essere preceduto dal carattere asterisco * e racchiuso tra una coppia di
parentesi tonde ( ).

Snippet 7.16 Dichiarazione di un puntatore a funzione.


int (*ptr_to_func)(int, int);

Lo Snippet 7.16 dichiara un puntatore a funzione, ptr_to_func, capace di contenere un


indirizzo di memoria di una qualsiasi funzione che ha la sua stessa segnatura, ossia è del
suo stesso tipo: ritorna un int e accetta due parametri di tipo int.
Le parentesi tonde che racchiudono il nome del tipo sono essenziali perché permettono di
identificare ptr_to_func come un puntatore a una funzione di tipo (int, int) -> int. In loro
assenza, infatti, int *ptr_to_func(int, int); significherebbe tutt’altro, e cioè che ptr_to_func è
una funzione che ritorna un puntatore a un int e ha due parametri di tipo int, e quindi la sua
segnatura sarebbe (int, int) -> int *.

Dopo aver dichiarato un puntatore a funzione di un determinato tipo è possibile passare a


esso l’indirizzo di memoria di una funzione dello stesso tipo utilizzando il consueto
operatore di assegnamento, dove l’operando di sinistra sarà l’identificatore del puntatore a
funzione mentre l’operando di destra sarà l’identificatore della funzione scelta (Snippet
7.17).

Snippet 7.17 Assegnamenti validi e non validi a un puntatore a funzione.


...
// vari prototipi di funzione…
int sub_P(int a, int b);
double sqrt_P(double j);
int main(void)
{
// puntatore a funzione di tipo (int, int) -> int
int (*ptr_to_func)(int, int);

ptr_to_func = sub_P; // ok, stesso tipo


ptr_to_func = sqrt_P; // no, non dello stesso tipo
ptr_to_func = sub_P(1,2); // no, nessun indirizzo ritornato
...
}

Nello Snippet 7.17 il puntatore a funzione ptr_to_func è inizializzato con tre valori
differenti, ma solo il primo assegnamento è valido e dunque corretto perché il nome sub_P si
riferisce all’indirizzo di memoria di una funzione del suo stesso tipo.
Il secondo assegnamento non è valido perché il nome sqrt_P si riferisce all’indirizzo di
memoria di una funzione di un tipo diverso (double) -> double, mentre il terzo assegnamento
è ancora non valido perché il valore assegnato è di tipo int, ossia quello ritornato
dall’invocazione della funzione sub con i valori 1 e 2.
Nel secondo e terzo caso un compilatore come GCC ritornerà, nell’ordine, i messaggi di
warning assignment from incompatible pointer type e assignment makes pointer from integer
without a cast.

A parte la validità o meno degli assegnamenti mostrati, è anche importante comprendere


che quando il compilatore incontra un identificatore di una funzione, se è sprovvisto delle
parentesi tonde, allora lo valuta ritornando un puntatore, ovvero l’indirizzo di memoria dove
inizia il suo codice eseguibile (non è necessario usare l’operatore di indirizzo &). Se invece è
seguito dalla parentesi tonde, allora le stesse ne rappresentano l’operatore di invocazione di
funzione ed è generato l’opportuno codice di chiamata della funzione stessa.
Per quanto attiene alla modalità di utilizzo di un puntatore a funzione, ossia a come sia
possibile invocare la funzione riferita possiamo fare quanto segue (Snippet 7.18).

Snippet 7.18 Utilizzo di un puntatore a funzione.


...
// vari prototipi di funzione…
int sub_P(int a, int b);
double sqrt_P(double j);

int main(void)
{
// puntatore a funzione di tipo (int, int) -> int
int (*ptr_to_func)(int, int);

ptr_to_func = sub_P; // ok, stesso tipo...


int res = (*ptr_to_func)(100, 100); // ...risultato corretto

ptr_to_func = sqrt_P; // no, non dello stesso tipo...


double res_2 = (*ptr_to_func)(100, 100); // ...comportamento non definito

ptr_to_func = sub_P(1, 2); // no, nessun indirizzo ritornato...


int res_3 = (*ptr_to_func)(100, 100); // ...comportamento non definito
}
In definitiva è sufficiente usare le parentesi tonde al cui interno dereferenziare il relativo
puntatore al fine di far ritornare l’indirizzo di memoria della funzione da invocare con i
seguenti argomenti. Per esempio, quando ptr_to_func si riferirà a sub_P, scrivere
(*ptr_to_func)(100, 100) ne farà ritornare l’indirizzo, e dunque l’operatore di invocazione di
funzione con gli argomenti 100 e 100 sarà utilizzato su di esso (sarà, in pratica, invocata sub_P
per il tramite di ptr_to_func e infatti da questo punto di vista *ptr_to_func è considerabile un
alias di sub_P).
ATTENZIONE
Quando un puntatore di funzione contiene un riferimento verso un indirizzo di memoria di
una funzione di un tipo differente il comportamento del compilatore sarà non definito.

NOTA
È possibile invocare una funzione riferita tramite un puntatore a funzione utilizzando anche
la forma ptr_to_func(100, 100), ossia senza usare le parentesi tonde ( ) e l’operatore di
deriferimento *; questo perché quando ptr_to_func è valutato ritorna l’indirizzo di memoria di
una funzione direttamente chiamabile (ne è fatto un implicito deriferimento). In ogni caso,
anche se più prolissa, la prima forma, cioè (*ptr_to_func)(100,100), rende più esplicito che
ptr_to_func è un puntatore a funzione ed è per il suo tramite che si sta invocando un’altra
funzione. Se, infatti, si utilizzasse un identificatore non significativo, per esempio func, si
potrebbe pensare che func sia “direttamente” il nome della funzione che si sta invocando.

I puntatori a funzione sono un utile strumento di programmazione impiegato spesso per


scrivere algoritmi o funzionalità “generiche”, dove cioè sia possibile separare il “cosa”
l’algoritmo deve fare da il “come” lo deve fare effettivamente.
Il “come”, in C, è codificato in un’apposita funzione che viene poi fornita come
argomento (il suo puntatore) al parametro (di tipo puntatore a funzione) di un’altra
funzione, che ne rappresenta il “cosa”.
Per comprendere quanto asserito è possibile riferirci a un classico esempio didattico che
fa uso della funzione qsort, dichiarata nel file header <stdlib.h> della libreria standard del
linguaggio C con prototipo void qsort(void *base, size_t nmemb, size_t size, int (*compar)

(const void *, const void *)), il cui obiettivo computazionale è quello di ordinare gli
elementi di un array in base a un determinato criterio.
Analizzando il suo prototipo si nota subito come tale funzione sia in effetti “generica”
perché sarà il client utilizzatore che dovrà fornirgli, come ultimo argomento, un puntatore a
una funzione di comparazione che deciderà come gli elementi dell’array dovranno essere
ordinati, ossia secondo quale modalità un elemento dovrà essere considerato minore,
maggiore oppure uguale rispetto a un altro elemento.
In definitiva, la funzione qsort dice “cosa” sta eseguendo in quel momento, cioè che sta
ordinando gli elementi di un array; il “come” debbano essere ordinati, però, è espresso
tramite un’altra funzione che le viene passata come argomento.
TERMINOLOGIA
Una funzione che può accettare come suo argomento un’altra funzione e/o restituire una
funzione come risultato della sua computazione è sovente indicata con il termine di higher-
order function (funzione di ordine superiore).

Listato 7.9 PointersToFunctions.c (PointersToFunctions).


/* PointersToFunctions.c :: Puntatori a funzioni :: */
#include <stdio.h>
#include <stdlib.h>

// prototipi di funzione
// notare come gli identificatori dei parametri abbiano un nome diverso da quello
// dei corrispettivi identificatori indicati nella definizione di tali funzioni;
// ricordiamo che ciò non rappresenta alcun problema: sono semplicemente ignorati!
int makeOperations(int a, int b, int (*f)(int, int));

int addition(int a, int b);


int subtraction(int a, int b);
int multiplication(int a, int b);
int division(int a, int b);

int main(void)
{
int val1, val2, op = 0;

// array di puntatori a funzioni di tipo (int, int) -> int


int (*array_of_op[])(int, int) = {addition, subtraction, multiplication, division};

// array di puntatori a caratteri


char *op_name[] = {"addizione", "sottrazione", "moltiplicazione", "divisione"};

printf("***************** Operazioni Aritmetiche ***************************\n\n");


printf("[0] addizione\n[1] sottrazione\n[2] moltiplicazione\n[3] divisione\n\n");
scanf("%d", &op);

while (op < 0 || op > 3)


{
printf("Operazione aritmetica\n");
printf("[0] addizione, [1] sottrazione, [2] moltiplicazione, [3], divisione ");
scanf("%d", &op);
}

printf("\nPrimo numero: ");


scanf("%d", &val1);

printf("Secondo numero: ");


scanf("%d", &val2);

// stampa il risultato
printf("\nLa %s tra %d e %d ha prodotto come risultato %d\n",
op_name[op], val1, val2, makeOperations(val1, val2, array_of_op[op]));
printf("********************************************************************\n\n");

return (EXIT_SUCCESS);
}

// definizioni delle funzioni


int makeOperations(int value_1, int value_2, int (*op)(int, int))
{
// esegue la funzione riferita; può essere qualsiasi funzione di tipo
// (int, int) -> int
return (*op)(value_1, value_2);
}

int addition(int v1, int v2)


{
return v1 + v2;
}

int subtraction(int v1, int v2)


{
return v1 - v2;
}

int multiplication(int v1, int v2)


{
return v1 * v2;
}

int division(int v1, int v2)


{
return v1 / v2;
}

Output 7.9 Dal Listato 7.9 PointersToFunctions.c.


***************** Operazioni Aritmetiche ***************************

[0] addizione
[1] sottrazione
[2] moltiplicazione
[3] divisione

Primo numero: 20
Secondo numero: 30

La moltiplicazione tra 20 e 30 ha prodotto come risultato 600


********************************************************************

Il Listato 7.9 mette in pratica quanto sin qui detto sulla possibilità di scrivere funzioni
generiche che si avvalgono dei puntatori a funzioni; illustra anche un altro comune pattern
di utilizzo degli stessi, ovvero quello che prevede la capacità di definire un array di
puntatori a funzioni che consente di scegliere quale funzione invocare direttamente e in
modo arbitrario tramite la comune e compatta notazione a indice propria degli array.
Nel programma, tutto ruota attorno alla funzione makeOperations che esegue una qualsiasi
operazione tra due valori numerici di tipo int espressa tramite un’apposita funzione di tipo
(int, int) -> int che le viene passata come argomento.
NOTA
Nel prototipo di makeOperations il parametro puntatore a funzione può essere scritto anche
senza indicarne il nome, come in int (*)(int, int). Invece, nella definizione di makeOperations,
il parametro puntatore a funzione può essere scritto con la stessa sintassi vista per la
dichiarazione (Sintassi 7.8), come in int (*op)(int, int).

La funzione makeOperations permette, quindi, di generalizzare cosa computa e di separare,


nettamente il suo codice dal codice delle funzioni che eseguono nella sostanza la relativa
computazione (come, cioè, essa è eseguita).
Questo aspetto è di notevole importanza perché consente di evitare che all’interno della
funzione makeOperations si scriva anche il codice delle computazioni, cioè delle operazioni
aritmetiche; se, infatti, una qualsiasi delle funzioni di computazione viene in seguito
cambiata (si pensi alla funzione division che nell’implementazione corrente non prevede il
check della possibile divisione per 0), tale cambiamento non ha alcun impatto su
makeOperations, che continua a funzionare in modo trasparente.
La funzione main, invece, definisce un menu di scelta delle quattro operazioni aritmetiche
fondamentali memorizzandone il risultato nella variabile op, la quale verrà poi utilizzata per
“estrarre” dall’array array_op e op_name, rispettivamente, la funzione dell’operazione da
invocare e una stringa di caratteri che ne dà il nome significativo.
In merito all’identificatore array_op, esso dimostra come dichiarare un array di puntatori a
funzioni di un certo tipo; nel nostro caso è un array di puntatori a funzioni che ritornano un
int e accettano come argomenti due int, e pertanto si presta bene a contenere come elementi

i puntatori alle funzioni addition, subtraction, multiplication e division.


L’identificatore op_name, invece, è un array dove ogni elemento è un puntatore a un
carattere, ossia a un indirizzo di memoria a partire dal quale si trovano, in successione, tutti
gli altri caratteri della stringa corrispondente.
Dopo la scelta dell’operazione da eseguire, memorizziamo i valori da computare nelle
variabili val1 e val2 ed eseguiamo la funzione printf dove forniamo come argomenti,
nell’ordine: un stringa che descrive l’operazione scelta; il primo valore da computare; il
secondo valore da computare; il risultato dell’operazione relativa.
Il risultato dell’operazione è ottenuto tramite l’espressione makeOperations(val1, val2,
array_of_op[op]). Data la sua importanza didattica, appare opportuno scomporre nei seguenti
passi per comprenderla pienamente.
1. Viene invocata la funzione makeOperations alla quale si passano val1, val2 e poi il
puntatore alla funzione che rappresenta l’elemento dell’array array_of_op posizionato
all’indice op. Se per esempio op vale 1, allora array_of_op[1] ritornerà il puntatore alla
funzione subtraction.
2. Viene eseguita la funzione makeOperations, che dereferenzia il suo parametro op in modo
da ottenere la locazione di memoria della funzione da eseguire alla quale passa come
valori value_1 e value_2. Al termine dell’esecuzione della funzione puntata ne ritorna il
risultato al chiamante. Ritornando al nostro esempio del punto 1, *op rappresenterà la
funzione subtraction che sarà eseguita e che ritornerà come risultato la differenza tra i
valori forniti come argomenti, che sarà a sua volta ritornata al main da makeOperations.

Un altro esempio molto comune di utilizzo dei puntatori a funzione si ha quando si


desidera impostare delle funzioni listener o di callback, ossia delle funzioni che devono
essere invocate all’accadimento di un determinato evento.
Per implementare tale meccanismo si può definire una funzione, diciamo foo, che avrà
come parametro un puntatore a funzione (che riferirà la funzione di callback) la quale, allo
scatenarsi dell’evento desiderato, per il “tramite” di foo, verrà invocata.
NOTA
Il meccanismo delle funzioni di callback è sovente usato per assegnare delle funzioni
handler come argomenti ad altre funzioni che rappresentano degli eventi che possono
accadere sui componenti o widget delle interfacce grafiche. Così, potremmo definire una
funzione onclick per un pulsante alla quale poter assegnare un’altra funzione che
rappresenta il “comportamento” che dovrà essere intrapreso quando l’utente farà clic sul
pulsante relativo.

Listato 7.10 Callback.c (Callback).


/* Callback.c :: Puntatori a funzioni come callback:: */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

#define NR_OF_ELEMS 10

// prototipi di funzione
void done(int res); // viene invocata solo se il risultato della somma è positivo
void fail(int code_nr); // viene invocata solo se il risultato della somma è negativo

// primo parametro -> array di elementi da sommare


// secondo parametro -> funzione di callback da eseguire se la computazione è eseguita
// correttamente
// terzo parametro -> funzione di callback da eseguire se la computazione
// NON è eseguita correttamente
void sum(int elems[], void(*done)(int), void(*fail)(int));

int main(void)
{
// inizializzazione del generatore pseudo-casuale dei numeri
srand((unsigned int) time(NULL));

int elems[NR_OF_ELEMS];

// inizializzazione elementi dell'array


for (int i = 0; i < NR_OF_ELEMS; i++)
elems[i] = (rand() % 1001) + (-500); // tra +500 e -500

sum(elems, done, fail);

return (EXIT_SUCCESS);
}

// definizioni delle funzioni


void done(int res)
{
printf("Il risultato e' %d\n", res);
}

void fail(int code_nr)


{
printf("Attenzione errore %d di computazione [ risultato < 0 ]\n", code_nr);
}

void sum(int elems[], void(*done)(int), void(*fail)(int))


{
int total = 0;
for (int ix = 0; ix < NR_OF_ELEMS; ix++)
total += elems[ix];

if (total >= 0)
(*done)(total); // chiamo la callback riferita dal parametro done
else
(*fail)(-1); // chiamo la callback riferita dal parametro fail
}

Output 7.10 Dal Listato 7.10 Callback.c.


Attenzione errore -1 di computazione [ risultato < 0 ]

Il Listato 7.10 mostra un semplice esempio di implementazione di un meccanismo di


callback dove la funzione sum è definita in modo che, all’accadimento dell’evento di
completamento della somma degli elementi di un array passato come primo argomento, se il
risultato della computazione è positivo, allora verrà invocata la funzione done passata come
secondo argomento altrimenti, se il risultato è negativo, come è il caso mostrato dall’Output
7.10, verrà invocata la funzione fail passata come terzo argomento.

TYPEDEF E PUNTATORI A FUNZIONE


La sintassi di dichiarazione di un puntatore a funzione è abbastanza elaborata e a volte la sua
prolissità può portare a scrivere codice poco leggibile. Al fine, quindi, di rendere più chiaro il
codice sorgente ma anche più agevole l’utilizzo di un puntatore a funzione, è possibile
utilizzare la keyword typedef. Per esempio, l’istruzione typedef int (*ptr_to_func)(int, int) crea
l’alias ptr_to_func, che è un puntatore a una funzione che ha due parametri di tipo int e ritorna
un tipo int. Poi, nell’ambito del codice sorgente si potrà scrivere: ptr_to_func myFunc; per
dichiarare myFunc come un identificatore del tipo puntatore a funzione ptr_to_func, ossia del tipo
puntatore a una funzione di tipo (int, int) -> int (Listato 7.11).

Listato 7.11 typedefForFunctionPointer.c (typedefForFunctionPointer).


/* typedefForFunctionPointer.c :: typedef e puntatori a funzioni :: */
#include <stdio.h>
#include <stdlib.h>

// typedef per una funzione di tipo (int, int) -> int


typedef int (*ptr_to_operations)(int, int);

int sum(int a, int b);


int sub(int a, int b);

// senza il typedef la dichiarazione di una funzione che ritorna un puntatore a funzione


// di tipo (int, int) -> int sarebbe stata molto più complessa e poco leggibile:
// int(*choose(char))(int, int);
ptr_to_operations choose(char);

// senza il typedef la dichiarazione di una funzione che accetta come argomento


// un puntatore a funzione di tipo (int, int) -> int sarebbe stata molto più complessa
// e poco leggibile:
// int makeComputation(int (*)(int, int), int, int);
int makeComputation(ptr_to_operations, int, int);

int main(void)
{
int value1 = 2000;
int value2 = 1000;
// eseguo prima l'addizione
printf("Addizione tra %d e %d = %d\n", value1, value2,
makeComputation(choose('+'), value1, value2));

// eseguo poi la sottrazione


printf("Sottrazione tra %d e %d = %d\n", value1, value2,
makeComputation(choose('-'), value1, value2));

return (EXIT_SUCCESS);
}

int sum(int n1, int n2)


{
return n1 + n2;
}

int sub(int n1, int n2)


{
return n1 - n2;
}

// senza il typedef la definizione di una funzione che ritorna un puntatore a funzione


// sarebbe stata molto più complessa e poco leggibile:
// int(*choose(char code))(int, int)
ptr_to_operations choose(char code)
{
switch (code)
{
case '+': return sum;
case '-': return sub;
}

// nessuna scelta valida, quindi ritorniamo un puntatore nullo a indicare nessun


// indirizzo di puntatore a funzione valido
return NULL;
}

// senza il typedef la dichiarazione di una funzione che accetta come argomento


// un puntatore a funzione sarebbe stata molto più complessa e poco leggibile:
// int makeComputation(int (*op)(int, int), int n1, int n2)
int makeComputation(ptr_to_operations op, int n1, int n2)
{
// se ptr_to_operations contiene un indirizzo di puntatore a funzione valido
// esegui la funzione riferita; equivalente a *op != NULL
if (*op)
return (*op)(n1, n2);
else
{
printf("ATTENZIONE ptr_to_operations contiene un indirizzo non usabile!\n");
printf("ESCO subito dal programma!\n");
exit(EXIT_FAILURE);
}
}

Output 7.11 Dal Listato 7.11 typedefForFunctionPointer.c.


Addizione tra 2000 e 1000 = 3000
Sottrazione tra 2000 e 1000 = 1000
Puntatori a void
Dal punto di vista di un puntatore, un indirizzo di memoria non è altro che una locazione
di storage a partire dalla quale viene memorizzato un valore di un determinato tipo.
Sinora abbiamo visto che ogni puntatore deve avere un tipo associato affinché il
compilatore possa essere in grado, quando si applica l’operatore di deriferimento * sul
puntatore medesimo, di interpretarlo in modo adeguato ed estrarne il valore corretto.
In ogni caso, in C è possibile dichiarare anche puntatori al tipo void (void *), ossia
puntatori che non puntano a nessun tipo in particolare, oppure, detto in altro modo,
puntatori che possono puntare a qualsiasi tipo (sono definiti, infatti, puntatori generici).
Tuttavia, quando si utilizzano puntatori a void è importante considerare che su di essi non
è possibile applicare l’operatore di deriferimento *, così come usare l’aritmetica dei
puntatori, perché, contenendo degli indirizzi di memoria di tipi “sconosciuti”, il compilatore
non è in grado di sapere quanti byte deve usare per dereferenziarli correttamente.
Però, a differenza dei puntatori a un qualsiasi tipo T, laddove se si assegna un puntatore a
un tipo (per esempio float *) a un puntatore a un tipo diverso (per esempio int *) un
compilatore emette un apposito messaggio di diagnostica di incompatibilità di
assegnamento, i puntatori a tipi diversi possono sempre essere assegnati a puntatori a void e
viceversa un puntatore a void può essere sempre assegnato a un puntatore a un altro tipo.
ATTENZIONE
È possibile solo dichiarare variabili di tipo void * ossia puntatori a void ma mai variabili di tipo
void ossia variabili di tipo void.

Listato 7.12 VoidPointers.c (VoidPointers).


/* VoidPointers.c :: Puntatori a void :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
int i_data = 100;
double d_data = 223.2232;
int *ptr_to_int = &i_data;
double *ptr_to_double = &d_data;

// ATTENZIONE assegnamento tra puntatori a tipi diversi


ptr_to_int = ptr_to_double;

printf("Deriferimento di ptr_to_int che contiene l'indirizzo contenuto in "


"ptr_to_double\n[ %d ]\n", *ptr_to_int);

ptr_to_int = &i_data;

// puntatore a void
void *ptr_to_void = ptr_to_int; // ora punta a un int

// ptr_to_void è convertito a int * e poi dereferenziato


printf("Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in "
"ptr_to_int\n[ %d ]\n", *(int*) ptr_to_void);

ptr_to_void = ptr_to_double; // ora punta a un double

// ptr_to_void è ora convertito a double * e poi dereferenziato


printf("Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in "
"ptr_to_double\n[ %.4f ]\n", *(double*) ptr_to_void);

return (EXIT_SUCCESS);
}

Output 7.12 Dal Listato 7.12 VoidPointers.c.


Deriferimento di ptr_to_int che contiene l'indirizzo contenuto in ptr_to_double
[ 1951633139 ]
Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in ptr_to_int
[ 100 ]
Deriferimento di ptr_to_void che contiene l'indirizzo contenuto in ptr_to_double
[ 223.2232 ]

Il Listato 7.12 definisce alcune variabili e dei puntatori a esse. Poi prova ad assegnare la
variabile ptr_to_double, che è un puntatore a un double, alla variabile ptr_to_int, che è un
puntatore a int, facendo emettere dal compilatore in uso (GCC) il messaggio warning:
assignment from incompatible pointer type.

Proviamo, quindi, a far stampare mediante la funzione printf il valore puntato da


ptr_to_int che però, come dimostrato dall’Output 7.12, è incongruo; ciò si verifica perché
l’indirizzo di memoria contenuto nella variabile ptr_to_int è di una variabile di tipo double
(d_data), che è a sua volta contenuto nella variabile ptr_to_double. Pertanto il compilatore
quando userà l’operatore di deriferimento * su ptr_to_int interpreterà i byte dell’indirizzo
come memoria contenente un valore intero e non, invece, come memoria contenente un
valore di tipo decimale (leggerà, per esempio, solo i primi 4 byte propri di un int e non tutti
gli 8 byte propri di un double sul corrente sistema target).
Infine, dimostriamo come l’assegnamento di un puntatore a un tipo (per esempio int * o
double *) a un puntatore a void (void *) non faccia generare alcun warning da parte del
compilatore ma, anzi, la relativa conversione con cast esplicito [(int *) e poi (double *)], e
poi l’operazione di deriferimento, consentano di far accedere al valore corretto presente nel
corrente indirizzo di memoria riferito.
Solitamente, comunque, i puntatori a void sono un utile strumento per costruire funzioni
generiche, ossia funzioni che sono in grado di accettare argomenti di differente tipo oppure
di ritornare tipi differenti (Listato 7.13).

Listato 7.13 GenericFunctions.c (GenericFunctions).


/* GenericFunctions.c :: Funzioni generiche :: */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

// prototipo di swap
void g_swap(void *val_1, void *val_2, size_t size);

int main(void)
{
int a = 10;
int b = 20;

float f = 10.20f;
float g = 22.33f;

printf("Valori di a e b prima dello swap\t[ %d ] [ %d] \n", a, b);

// swap di tipi int


g_swap(&a, &b, sizeof (int));
printf("Valori di a e b dopo lo swap\t\t[ %d ] [ %d] \n", a, b);

printf("Valori di f e g prima dello swap\t[ %.2f ] [ %.2f] \n", f, g);

// swap di tipi float


g_swap(&f, &g, sizeof (float));
printf("Valori di f e g dopo lo swap\t\t[ %.2f ] [ %.2f] \n", f, g);

return (EXIT_SUCCESS);
}

// definizione di swap
void g_swap(void *val_1, void *val_2, size_t size)
{
void *tmp = malloc(size);
memcpy(tmp, val_1, size);
memcpy(val_1, val_2, size);
memcpy(val_2, tmp, size);
free(tmp);
}

Output 7.13 Dal Listato 7.13 GenericFunctions.c.


Valori di a e b prima dello swap [ 10 ] [ 20]
Valori di a e b dopo lo swap [ 20 ] [ 10]
Valori di f e g prima dello swap [ 10.20 ] [ 22.33]
Valori di f e g dopo lo swap [ 22.33 ] [ 10.20]

Il Listato 7.13 definisce la funzione generica g_swap che consente di scambiare il valore di
due variabili di qualsiasi tipo. Essa è definita con due parametri formali di tipo puntatore a
void e un terzo parametro formale, di tipo size_t, atto a contenere la dimensione del tipo di

dato da elaborare e che è fondamentale per processare i valori dei tipi passati (un void * è
considerabile come un puntatore a un blocco di memoria grezzo, e non sa nulla in merito a
cosa è un tipo di dato o alla sua dimensione in byte).
Il body della funzione g_swap dichiara, come primo oggetto, la variabile tmp come un tipo
void * e le passa, grazie alla funzione malloc, l’indirizzo di memoria di partenza dello spazio
di storage allocato della dimensione fornita dal parametro size.
Ciò permetterà di allocare e processare la giusta quantità di memoria occorrente per dei
tipi int, float, double e così via, passati come argomenti alla funzione g_swap.
Successivamente, utilizza la funzione memcpy, dichiarata nel file header <string.h>, per
copiare il contenuto della memoria di una variabile sorgente (il secondo argomento)
nell’area di memoria riferita da una variabile destinazione (il primo argomento):
con la prima invocazione, memcpy copierà il contenuto della variabile riferita da val_1
nell’area di memoria riferita da tmp;
con la seconda invocazione, memcpy copierà il contenuto della variabile riferita da val_2
nell’area di memoria riferita da val_1;
con la terza invocazione di memcpy, copierà il contenuto della memoria riferita da tmp
nell’area di memoria riferita da val_2.

Infine utilizza la funzione free, dichiarata nel file header <stdlib.h>, per liberare la
memoria allocata e puntata dalla variabile tmp.
Puntatori nulli
Un puntatore nullo (null pointer) è un puntatore che contiene un valore “speciale” atto a
segnalare che esso non punta a niente, ossia non punta e non riferisce alcun indirizzo di
memoria concretamente utilizzabile di alcun oggetto (dato) o funzione (codice).
Lo standard del linguaggio C stabilisce che un’espressione costante intera con il valore 0
oppure la stessa espressione convertita in un puntatore a void (void *) rappresenta una
costante di tipo puntatore nullo (null pointer constant), la quale genera un puntatore nullo
quando è utilizzata durante un’istruzione di inizializzazione, assegnamento o comparazione
con una variabile di tipo puntatore.
Tale costante di tipo puntatore nullo è espressa attraverso la macro NULL, che è definita, in
linea generale e dalla maggior parte dei compilatori, come #define NULL ((void *)0) in molti
file header come <stdio.h>, <stdlib.h>, <stddef.h> e così via.
ATTENZIONE
Un puntatore nullo è differente da un puntatore non inizializzato. Il primo, infatti, non punta
né a un oggetto né a una funzione; il secondo, invece, punta a qualsiasi cosa.

NOTA
Ogni compilatore è libero di scegliere la propria rappresentazione di un puntatore nullo, e
dunque questo non necessariamente dovrà avere come riferimento un indirizzo di memoria
tipo 0x00000000 ma potrà anche contenere un indirizzo di memoria non esistente. Pertanto,
dal punto di vista di un programmatore, è sufficiente utilizzare NULL o 0 per generare un
puntatore nullo e avere la certezza che esso non punterà a niente di validamente
utilizzabile.

Quando si vuole assegnare a un puntatore un puntatore nullo è possibile, in modo


intercambiabile, utilizzare il valore 0 oppure il valore NULL.
Tuttavia, è solo in quel contesto che 0 e NULL sono equivalenti (entrambi rappresentano
una costante di tipo puntatore nullo). Per esempio, assegnare il valore 0 a una variabile di
tipo int significa semplicemente che essa conterrà quel valore costante intero, mentre
assegnare alla stessa variabile il valore NULL potrà fare generare a un compilatore un
messaggio di diagnostica come initialization makes integer from pointer without a cast, che
indicherà, per l’appunto, che si sta provando ad assegnare direttamente un puntatore a void a
una variabile di tipo intero.
CONSIGLIO
Per motivi si stile e chiarezza usare sempre la costante NULL per assegnare a un puntatore
un puntatore nullo.
I puntatori nulli si rilevano, dunque, essenziali quando si deve verificare se un puntatore
contiene un valido indirizzo di memoria referenziabile (si pensi alla funzione malloc che se
riesce ad allocare lo spazio di storage richiesto ritorna un puntatore valido a esso altrimenti
ritorna un puntatore nullo). Infatti: in caso di verifica affermativa (ha un indirizzo di
memoria diverso da 0) la valutazione del puntatore ritornerà un valore uguale a 1 (true); in
caso di verifica negativa (ha un indirizzo di memoria uguale a 0) la valutazione del
puntatore ritornerà un valore uguale a 0 (false) e permetterà di evitare deriferimenti che
potrebbero causare eventi disastrosi come il crash del programma attualmente in esecuzione
(in ogni caso, per lo standard di C, dereferenziare un puntatore nullo causerà un
comportamento non definito, e un compilatore potrebbe anche non far terminare un
programma ma generare piuttosto un risultato inaspettato o non prevedibile).

Snippet 7.19 Puntatore nullo.


int value = 200;

// p_value è impostata per puntare a un puntatore nullo


int *p_value = NULL;

// p_value conterrà un riferimento a un puntatore nullo


// sarà quindi valutata come uguale a 0
_Bool b_p = p_value; // false - b_p conterrà il valore 0

p_value = &value; // puntatore a value

// p_value conterrà un riferimento a un puntatore valido


// sarà quindi valutata come uguale a 1
b_p = p_value; // true - b_p conterrà il valore 1

// un test per verificare se il puntatore è nullo


int nr = 100;
int *p_nr = &nr;
int storage;

// solo se p_nr contiene un indirizzo valido, dereferenziarlo ponendo


// il valore dell'oggetto puntato nella variabile storage
if (p_nr) // equivalente ma prolisso: if(p_nr != NULL)
storage = *p_nr;
else
storage = 0;
La keyword const e i puntatori
Il qualificatore di tipo espresso tramite la keyword const può essere applicato anche a un
puntatore che, a seconda della sua collocazione, assumerà una determinata semantica
(Sintassi 7.9, 7.10 e 7.11):

Sintassi 7.9 Puntatore a costante – qualificatore const prima del tipo di dato.
const data_type *ptr_identifier;

La Sintassi 7.9 permette di dichiarare un puntatore che potrà contenere un indirizzo di


memoria di un oggetto di un determinato tipo che sarà considerato costante.
Ciò implica che il puntatore potrà puntare a un qualsiasi altro indirizzo di memoria, ma il
valore dell’oggetto riferito non potrà subire modifiche per effetto dell’applicazione
dell’operatore di deriferimento * oppure dell’operatore di subscript con un indice, i quali
potranno quindi essere utilizzati solo per delle operazioni di lettura.
NOTA
Un puntatore a costante potrà contenere validamente sia l’indirizzo di memoria di un tipo
costante sia di un tipo non costante. Ciò implica che la non modificabilità del valore è
significativa solo dal punto di vista del puntatore a costante. Infatti, se contiene un indirizzo
di memoria di un dato non costante, quest’ultimo potrà ancora subire modifiche per il tramite
del suo identificatore ma non per il tramite dell’identificatore del puntatore a costante.

Snippet 7.20 Puntatore a costante.


// array non costante
int data[] = {100, 200, 300};

// array costante
const int ro_data[] = {-1, -2, -3};

// puntatore a costante di tipo int


// assegnamento di un dato non costante
const int *ptr_1 = data;
*ptr_1 = 10; // error: assignment of read-only location '*ptr_1'
data[0] = -100; // OK data non è const

// puntatore a costante di tipo int


// assegnamento di un dato costante
const int *ptr_2 = ro_data;
*ptr_2 = 10; // error: assignment of read-only location '*ptr_2'
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]

// ok i puntatori a costante possono puntare ad altri oggetti


int other = 2;
ptr_1 = ptr_2 = &other;

Sintassi 7.10 Puntatore costante – qualificatore const prima dell’identificatore.


data_type *const ptr_identifier;

La Sintassi 7.10 permette di dichiarare un puntatore che potrà contenere un indirizzo di


memoria di un oggetto di un determinato tipo ma che non potrà puntare e riferire altri
indirizzi di memoria di altri oggetti dello stesso tipo.
Ciò implica che il puntatore non potrà puntare a qualsiasi altro indirizzo di memoria, ma
il valore dell’oggetto riferito potrà subire modifiche per effetto dell’applicazione
dell’operatore di deriferimento * oppure dell’operatore di subscript con un indice i quali
potranno quindi essere utilizzati per operazioni di scrittura e di lettura.

Snippet 7.21 Puntatore costante.


// array non costante
int data[] = {100, 200, 300};

// array costante
const int ro_data[] = {-1, -2, -3};

// puntatore costante a un int


// assegnamento di un dato non costante
int *const ptr_1 = data;
*ptr_1 = 10; // OK il puntatore non è un puntatore a costante
data[0] = -100; // OK data non è const

// puntatore costante a un int


// assegnamento di un dato costante
int *const ptr_2 = ro_data; // warning: initialization discards 'const' qualifier
// from pointer target type
*ptr_2 = 10; // modifica di un dato costante con *ptr_2; comportamento non definito
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]

int other = 2;
ptr_1 = &other; // error: assignment of read-only variable 'ptr_1'
ptr_2 = &other; // error: assignment of read-only variable 'ptr_2'

Lo Snippet 7.21 dichiara le stesse variabili dello Snippet 7.20 ma fa un’importante


modifica: i puntatori ptr_1 e ptr_2 diventano puntatori costanti, e dunque, dopo
l’assegnamento, rispettivamente dell’indirizzo di memoria del primo elemento di data e
dell’indirizzo di memoria del primo elemento di ro_data, non potranno puntare ad altri
oggetti come evidenziato dall’errore di compilazione che il compilatore ha emesso quando
abbiamo tentato di assegnare l’indirizzo della variabile other ai predetti puntatori.
Notiamo anche che il compilatore ha emesso un warning quando l’indirizzo del primo
elemento di ro_data è stato assegnato al puntatore costante ptr_2.
Questo è accaduto perché, come regola generale, lo standard di C asserisce che se si
prova a modificare un oggetto costante per il tramite di un oggetto non costante il
comportamento sarà non definito; il nostro compilatore, dunque, ci mette in guardia che
quell’inizializzazione scarta il qualificatore const dell’oggetto riferito dal puntatore ptr_2
(l’oggetto riferito era infatti di tipo const int) e pertanto su di esso potranno avvenire
modifiche per il tramite dell’oggetto non costante *ptr_2 (è di tipo int). Tuttavia non è
possibile esere certi se queste modifiche avverranno o meno oppure se vi potranno essere
effetti differenti (il comportamento è cioè non definito; potrà accadere qualsiasi cosa).
Quello che bisogna comprendere è che la non modificabilità di un puntatore costante è
riferita solo al fatto che esso non può puntare a un altro indirizzo di memoria ma può
certamente cambiare il valore dell’oggetto riferito.
Infatti, una dichiarazione come int *const ptr_2 può essere letta in modo estensivo,
partendo da destra verso sinistra, come “ptr_2 è un identificatore costante di un oggetto non
costante che è un puntatore a un int”; dunque, in questo caso, quello che è costante, è il
puntatore che non può subire modifiche del valore lì contenuto una volta assegnato (in
questo caso, quindi, &ro_data[0] e ptr_2 non avranno corrispondenza di dichiarazioni const tra
il dato che potrà essere riferito, che sarà di tipo const int, e l’oggetto che potrà essere
dereferenziato, che sarà di tipo int).
Invece, una dichiarazione come const int *ptr_2 presente nello Snippet 7.20 può essere
letta in modo dettagliato, sempre da destra verso sinistra, come: “ptr_2 è un identificatore
non costante di un oggetto costante che è un puntatore a un const int”; dunque, in questo
caso, quello che è costante è il dato dereferenziato (in questo caso, quindi, &ro_data[0] e
ptr_2 avranno corrispondenza di dichiarazioni const tra il dato che potrà essere riferito, che
sarà di tipo const int, e l’oggetto che potrà essere dereferenziato, che sarà di tipo const int).

Sintassi 7.11 Puntatore costante a costante – qualificatore const prima del tipo e prima
dell’identificatore.
const data_type *const ptr_identifier;

La Sintassi 7.11 permette di dichiarare un puntatore che potrà contenere un indirizzo di


memoria di un oggetto di un determinato tipo che sarà considerato costante e non potrà
puntare ad altri indirizzi di memoria di altri oggetti dello stesso tipo.
Ciò implica che il puntatore non potrà puntare a qualsiasi altro indirizzo di memoria, e il
valore dell’oggetto riferito non potrà subire modifiche per effetto dell’applicazione
dell’operatore di deriferimento * oppure dell’operatore di subscript con un indice, i quali
potranno quindi essere utilizzati solo per delle operazioni di lettura.

Snippet 7.22 Puntatore costante a costante.


// array non costante
int data[] = {100, 200, 300};

// array costante
const int ro_data[] = {-1, -2, -3};

// puntatore costante a costante di tipo int


// assegnamento di un dato non costante
const int *const ptr_1 = data;
*ptr_1 = 10; // error: assignment of read-only location '*ptr_1'
data[0] = -100; // OK data non è const

// puntatore costante a costante di tipo int


// assegnamento di un dato costante
const int *const ptr_2 = ro_data;
*ptr_2 = 10; // error: assignment of read-only location '*ptr_2'
ro_data[0] = 1; // error: assignment of read-only location 'ro_data[0]

int other = 2;
ptr_1 = &other; // error: assignment of read-only variable 'ptr_1'
ptr_2 = &other; // error: assignment of read-only variable 'ptr_2'

CONSIGLIO
Per leggere e comprendere correttamente le dichiarazioni di puntatori con o senza l’uso del
qualificatore const si può procedere nel seguente modo, partendo dall’identificatore e poi
procedendo con tutti gli altri elementi posti alla sua sinistra; per esempio, int *p; dichiara p

come un puntatore a un int (p as pointer to int); const int *p; dichiara p come un puntatore a
una costante di tipo int (p as pointer to const int); int *const p; dichiara p come un puntatore
costante a un int (p as const pointer to int); const int *const p; dichiara p come un puntatore
costante a una costante di tipo int (p as const pointer to const int).

La keyword const viene sovente impiegata con i parametri di una funzione di tipo
puntatore per specificare che gli stessi non potranno modificare i relativi argomenti.
È molto comune, per esempio, dichiarare una funzione che deve elaborare gli elementi di
un array passato come argomento con un parametro che è un puntatore a costante del tipo
degli elementi dell’array. Questa tecnica, di fatto, permette il raggiungimento di due scopi
contemporaneamente: il primo è legato all’efficienza, perché nel parametro non sono copiati
tutti gli elementi dell’array ma solo l’indirizzo di memoria del suo primo elemento; il
secondo, invece, è legato alla sicurezza, perché gli elementi dell’array passato come
argomento non potranno subire modifiche inattese per il tramite del parametro puntatore.

Snippet 7.23 Caso d’uso di un puntatore a costante di tipo int come parametro di una funzione.
...
int sumArray(const int *elems, int size)
{
int sum = 0;
for (int i = 0; i < size; i++)
{
// se un elemento è negativo lo voglio rendere positivo...
if (*elems < 0)
*elems = -(*elems); // error: assignment of read-only location '*elems'
sum += elems[i]; // ok elems è usato solo in lettura
}

return sum;
}

int main(void)
{
int data[] = {-1, -2, -3, -4, -5, -6, 1, 2, 3, 4, 5, 6, 7, 8, 9, 10};
sumArray(data, sizeof data / sizeof (int));
...
}

Lo Snippet 7.23 definisce la funzione sumArray, dove esplicita che il parametro elems è un
puntatore a costante di tipo int, ossia per il tramite di esso non sarà possibile modificare un
qualsiasi oggetto riferito che, per il nostro obiettivo didattico, sono tutti gli elementi
dell’array data passato come argomento e processati nel relativo ciclo for.
Questa limitazione ha permesso di evitare che nel body di sumArray potessimo modificare
il valore di un elemento dell’array data, quando negativo, in positivo; in pratica abbiamo sia
protetto da modifiche l’array originario sia ottenuto un’adeguata performance, perché nel
puntatore elems, quando sumArray è stata invocata, sono stati copiati solo 4 byte (nel sistema
in uso a 32 bit), ossia la dimensione dello spazio di storage richiesto per memorizzare un
puntatore che nel nostro caso è rappresentato dall’indirizzo di memoria del primo elemento
dell’array data.
La keyword restrict e i puntatori
A partire dallo standard C99, è stato introdotto un nuovo qualificatore di tipo espresso
tramite la keyword restrict che è applicabile solo a un puntatore il quale diventa, in
conseguenza della sua applicazione, un puntatore ristretto (restricted pointer).

Sintassi 7.12 Puntatore ristretto.


data_type *restrict ptr_identifier;

La Sintassi 7.12 illustra che la keyword restrict deve essere posta prima
dell’identificatore del puntatore; essa, infatti, qualifica il puntatore come ristretto e non
l’oggetto cui punta. Prima di spiegare cos’è un puntatore ristretto, è opportuno illustrare
alcuni concetti preliminari che aiuteranno a comprendere il perché della sua introduzione.
Aliasing: situazione per cui due o più oggetti si riferiscono alla stessa locazione di
memoria che può dunque essere manipolata, in modo equivalente, per il tramite dei
predetti oggetti (Snippet 7.24). In pratica l’aliasing consente di riferire uno stesso
oggetto mediante l’impiego di più nomi.

Snippet 7.24 Aliasing.


int i_value = 100;

// aliasing: i due puntatori si riferiscono allo stesso indirizzo in memoria


int *ptr_to_i_value_1 = &i_value;
int *ptr_to_i_value_2 = &i_value;

// *ptr_to_i_value_1 è anche un alias di i_value


// entrambi riferiscono la stessa area di memoria che è modificata con il valore 300
*ptr_to_i_value_1 = 300;

La possibilità nel linguaggio C di creare alias può però portare sia a sottili errori di
programmazione difficili da scoprire se tali alias non sono stati pianificati correttamente
(per esempio, in una funzione si modifica il valore di un oggetto puntato da un parametro di
tipo puntatore ma tale modifica non doveva però accadere) sia a possibili restrizioni cui
devono sottostare i compilatori che non possono compiere eventuali ottimizzazioni sul
codice eseguibile prodotto (per esempio, se un indirizzo di memoria è riferito da più oggetti,
il compilatore dovrà necessariamente, per ogni oggetto, compiere opportune operazioni di
lettura e/o scrittura del relativo valore perché ciascun oggetto potrà avere compiuto con tale
indirizzo delle manipolazioni; in caso contrario, però, se un solo oggetto riferisce un
indirizzo di memoria un compilatore potrebbe memorizzarne il valore nelle veloci unità di
memoria quali sono i registri). Dal punto di vista di un compilatore, pertanto, non viene
fatta alcuna assunzione che l’aliasing non avvenga e pertanto non compie, nell’eventualità,
alcuna ottimizzazione.
Strict aliasing: regola per cui un compilatore assume che due o più puntatori, di tipo
differente non si riferiscono mai alla stessa locazione di memoria. In questo caso,
quindi, un compilatore è in grado di compiere certe ottimizzazioni per rendere il codice
eseguibile più veloce ed efficiente. Questo, comunque, se da una parte può migliorare
l’efficienza del codice generato, può portare a comportamenti non definiti se il
programmatore vìola la regola dello strict aliasing (GCC, per esempio, genera solo il
messaggio warning: dereferencing type-punned pointer will break strict-aliasing rules,
se si accorge della sua violazione).

TERMINOLOGIA
Per type punning si intende una tecnica mediante la quale è possibile “aggirare” il type
system di un linguaggio di programmazione al fine di far manipolare il valore di un tipo come
valore di un altro tipo. Ciò può avvenire, per esempio, quando si fa il cast in un puntatore a
int (destinazione) da un puntatore a float (sorgente) e se ne manipola il valore tramite il

puntatore di destinazione.

Snippet 7.25 Strict aliasing.


float *data[2];

// violazione di strict aliasing:


// puntatori di tipo diverso puntano alla stessa area di memoria

// warning: dereferencing type-punned pointer will break strict-aliasing rules


int *i = (int *) data;

// warning: dereferencing type-punned pointer will break strict-aliasing rules


short *s = (short *) data;

// manipolazione dell'area di memoria con due puntatori a tipi differenti


// qualsiasi risultato possibile perché il compilatore assume che il
// programmatore rispetti la regola dello strict aliasing e prova a fare
// delle ottimizzazioni
*i = 42;
s[0] = 0;
s[1] = 1;

NOTA
In GCC si possono usare durante la fase di compilazione i seguenti flag: -fstrict-aliasing

per consentire al compilatore di assumere che venga rispettata la regola dello strict aliasing;
-Wstrict-aliasing=2 per far attivare dei warning se il codice di un programma vìola la regola

dello strict aliasing che il compilatore usa per compiere delle ottimizzazioni; -O3 per far
attivare tutte le ottimizzazioni possibili. Ciò detto è possibile mandare in esecuzione del
codice sorgente contenente lo Snippet 7.25 con, per esempio, il seguente comando: gcc -
std=c11 -fstrict-aliasing -Wstrict-aliasing=2 -O3 7.25.c -o 7.25.

Come abbiamo detto, un compilatore assumerà sempre che l’aliasing tra oggetti sia
potenzialmente effettuabile e pertanto non provvederà mai a compiere alcuna
ottimizzazione sul codice al fine di renderlo più efficiente.
Ecco allora che entra in gioco l’utilità della keyword restrict: essa darà l’indicazione al
compilatore che il programmatore “si impegnerà” a non far mai puntare la stessa area di
memoria a due o più puntatori dello stesso tipo e, pertanto, potrà tentare di eseguire
qualsiasi ottimizzazione sul codice che riterrà opportuno.
NOTA
Un compilatore può anche ignorare la keyword restrict e non compiere alcuna
ottimizzazione.

Tuttavia, e questo è un punto di notevole importanza da ricordare, il compilatore “si


fiderà” del programmatore e non verificherà la violazione di questa sorta di contratto
stipulato con esso: ciò significa che se viene fatto un alias di un puntatore ristretto e poi per
il tramite di tale alias viene modificato l’oggetto puntato, avremo un comportamento non
definito.

Snippet 7.26 Puntatori ristretti.


int value = 100;

int *restrict ptr_to_value_1 = &value;

// ATTENZIONE *ptr_to_value_2 è un alias di *ptr_to_value_1


int *restrict ptr_to_value_2 = ptr_to_value_1;

// ciò potrà causare un comportamento non definito


*ptr_to_value_2 = 1000;
Conversioni e puntatori
Anche i tipi puntatore, così come gli altri tipi di oggetti, sono soggetti a delle regole di
conversione quando utilizzati nelle comuni operazioni di assegnamento, inizializzazione o
confronto con altri tipi di puntatori oppure altri tipi di oggetti o costanti.
La Tabella 7.1 ne dà un riepilogo dove: la colonna T.O.S. (tipo operando a sinistra)
indica il tipo posto a sinistra dell’operatore usato; la colonna T. O. D. (tipo operando a
destra) indica il tipo posto a destra dell’operatore usato; la colonna Risultato indica se
l’operazione dà un risultato definito (ossia se è fattibile senza problemi), non definito
oppure dipendente dalla corrente implementazione.
Tabella 7.1 Conversioni tra puntatori, oggetti e valori costanti.
T. O. S. T. O. D. Risultato
puntatore a un tipo T void * Definito
void * puntatore a un tipo T Definito
puntatore a funzione di tipo T void * Comportamento non definito
void * puntatore a funzione di tipo T Comportamento non definito
puntatore a un tipo T 0 Definito
puntatore a un tipo T qualsiasi intero Definito dall’implementazione
puntatore a funzione di tipo T 0 Definito
puntatore a funzione di tipo T qualsiasi intero Definito dall’implementazione
qualsiasi intero puntatore a un tipo T Definito dall’implementazione1
qualsiasi intero puntatore a funzione di tipo T Definito dall’implementazione1
puntatore a un tipo T puntatore a un tipo W Definito2
puntatore a un tipo T puntatore a un tipo T Definito
puntatore a funzione di tipo T puntatore a funzione di tipo W Definito3
puntatore a funzione di tipo T puntatore a funzione di tipo T Definito
1
Se però il risultato della conversione non può essere rappresentato in un tipo intero, allora il
comportamento sarà non definito.
2
Se però il puntatore risultante non è correttamente allineato in memoria per il tipo
referenziato allora, se riconvertito nuovamente, il risultato sarà non definito. Altrimenti, se
l’allineamento è corretto e il puntatore è riconvertito, il puntatore risultante dovrà essere uguale
al puntatore originario (Snippet 7.27).
3
Se però il puntatore della funzione di tipo T è usato per invocare la funzione riferita di tipo W,
che non è quindi del suo stesso tipo, il comportamento sarà non definito. In più, dato un
puntatore a una funzione di tipo W convertito in un puntatore a una funzione di tipo T, se il
puntatore alla funzione di tipo T viene convertito nuovamente nel puntatore alla funzione di
tipo W, vi dovrà essere la garanzia che il puntatore risultante sarà uguale al puntatore a
funzione originario, e dunque il codice invocato sarà quello della funzione corretta.
Snippet 7.27 Eventuale problema di allineamento tra puntatori a tipi differenti.
char c = 'A';

// conversione tra un puntatore a un char e un puntatore a un int


// potrebbe esserci perdita di informazione...
int *ip = (int*) &c;

// riconversione del puntatore a un int nel puntatore a un char


// non è detto che *cp ritorni il carattere 'A'
// in alcune implementazioni cp potrebbe non essere uguale a &c
char *cp = (char*) ip;

// b = true o b = false a seconda dell'implementazione!


_Bool b = cp == &c;

TERMINOLOGIA
Lo standard di C11 fornisce le seguenti indicazioni terminologiche, laddove per behavior
intende quel comportamento che un programma può intraprendere a seguito dell’utilizzo dei
costrutti del linguaggio C e per implementation intende l’ambiente software (compilatore,
linker, sistema di run-time e così via) utilizzato per una particolare piattaforma hardware.
Undefined behavior (comportamento non definito): indica il comportamento di un
programma a seguito di codice non portabile o erroneo per cui lo standard non impone
alcun requisito in particolare. Ciò significa che un programma: può ignorare la situazione
producendo però risultati non prevedibili; può segnalare il problema durante la compilazione
o esecuzione; può terminare bruscamente e così via. In pratica, poiché qualsiasi cosa può
accadere, è buona norma evitare sempre di scrivere codice che può produrre
comportamenti non definiti.
Unspecified behavior (comportamento non specificato): indica che un programma può
“scegliere” di comportarsi sulla base di un set di comportamenti definiti dallo standard. Per
esempio, un comportamento non specificato si ha sull’ordine di valutazione degli argomenti
di una funzione laddove, data una funzione invocata come foo(a, b), potrebbe essere
valutato prima l’argomento b e poi l’argomento a o viceversa. Resta inteso che se gli
argomenti di una funzione producono side-effect, e pertanto l’ordine di valutazione è
importante, allora il codice eseguibile può produrre bug non facilmente individuabili. Anche
in questo caso, quindi, è bene evitare di scrivere codice che può produrre comportamenti
non specificati.
Implementation-defined behavior (comportamento definito dall’implementazione): indica che
un programma può intraprendere uno dei comportamenti non specificati ma
l’implementazione ne documenta la scelta. In pratica, il comportamento di un programma è
dipendente dalla corrente implementazione e può, dunque, variare da implementazione a
implementazione. Se si devono scrivere programmi portabili è consigliabile evitare di
scrivere codice dipendente da una specifica implementazione.

Snippet 7.28 Esempi di conversioni con void *.


...
int sum(int a, int b)
{
return a + b;
}

double foo(void)
{
return 1.0;
}

int main(void)
{
int a = 100;
int b = 200;

// puntatore a un tipo T
int *ptr_to_a = &a;

// puntatore a una funzione di tipo T


int(*ptr_to_f)(int, int) = sum;

// puntatore a un tipo void


void *v_ptr = &b;

// puntatore a un tipo T <---> void *


// DEFINITO
int *a_ptr = v_ptr;

// void * <---> puntatore a un tipo T


// DEFINITO
void *v2_ptr = ptr_to_a;

// puntatore a una funzione di tipo T <---> void *


// COMPORTAMENTO NON DEFINITO
ptr_to_f = v_ptr;

// void * <--- > puntatore a una funzione di tipo T


// COMPORTAMENTO NON DEFINITO
double(*ptr_to_f_2)(void) = foo;
void *v3_ptr = ptr_to_f_2;
...
}

NOTA
Se un puntatore a un tipo T è convertito in un puntatore a void e poi il puntatore a void è
convertito nuovamente nel puntatore al tipo T, dovrà essere garantito che non ci sarà alcuna
perdita di informazione (in pratica il puntatore risultante dalla riconversione del puntatore a
void nel puntatore al tipo T dovrà essere uguale al puntatore originario).

Snippet 7.29 Esempi di conversioni con tipi interi.


...
int sum(int a, int b)
{
return a + b;
}

double foo(void)
{
return 1.0;
}

int main(void)
{
int a = 100;
int b = 200;

// puntatore a un tipo T
int *ptr_to_a = &a;

// puntatore a una funzione di tipo T


int(*ptr_to_f)(int, int) = sum;

// puntatore a un tipo T <---> un int


// DEFINITO DALL'IMPLEMENTAZIONE
int *ptr_to_int = 0x22334455;

// un int <---> puntatore a un tipo T


// DEFINITO DALL'IMPLEMENTAZIONE
int any = ptr_to_a;

// puntatore a una funzione di tipo T <---> un int


// DEFINITO DALL'IMPLEMENTAZIONE
ptr_to_f = 0x66777788;

// un int <---> puntatore a una funzione di tipo T


// DEFINITO DALL'IMPLEMENTAZIONE
double(*ptr_to_f_2)(void) = foo;
int any_2 = ptr_to_f_2;
...
}

Snippet 7.30 Esempi di conversioni tra puntatori a tipi differenti.


int a = 100;
float c = 222.3f;

// puntatore a un tipo T
int *ptr_to_a = &a;

// puntatore a un tipo W
float *ptr_to_c = &c;

// puntatore a un tipo T <---> puntatore a un tipo W


// DEFINITO se allineamento in memoria corretto
ptr_to_a = ptr_to_c;

// ptr_to_a riconvertito nuovamente in ptr_to_c


// se allineamento in memoria corretto *ptr_to_c darà come valore 222.3
// ossia il suo valore in virgola mobile
ptr_to_c = ptr_to_a;

Snippet 7.31 Esempi di conversioni tra puntatori a funzione di tipi differenti.


...
int sum(int a, int b)
{
return a + b;
}

double foo(void)
{
return 1.0;
}

int main(void)
{
// puntatore a una funzione di tipo T
int(*ptr_to_f)(int, int) = sum;

// puntatore a una funzione di tipo W


double(*ptr_to_f_2)(void) = foo;

// puntatore a una funzione di tipo T <---> puntatore a una funzione di tipo W


// DEFINITO
ptr_to_f = ptr_to_f_2;

// ptr_to_f riconvertito nuovamente in ptr_to_f_2


// COMPORTAMENTO 'RIPRISTINATO' ptr_to_f_2 è di tipo (void) -> double
// e referenzia correttamente il codice da invocare
ptr_to_f_2 = ptr_to_f;

// puntatore a funzione di tipo (float, float) -> float che contiene


// un riferimento a una funzione di tipo (int, int) -> int
// DEFINITO
float (*ptr_to_f_3)(float, float) = sum;

// invocazione di sum per il tramite di ptr_to_f_3


// il tipo della funzione riferita è diverso dal tipo della funzione di cui
// l'identificatore ptr_to_f_
// COMPORTAMENTO NON DEFINITO
(*ptr_to_f_3)(5, 6);
...
}
Capitolo 8
Strutture, unioni ed enumerazioni

Tutti i listati e gli snippet di codice che abbiamo sin qui scritto per illustrare nella pratica
il significato dei fondamentali costrutti del linguaggio C hanno fatto uso, per la
rappresentazione e la memorizzazione delle relative informazioni, di due tipi di oggetti
principali: le variabili scalari (tipi di dato elementari o semplici), ideali per gestire e
manipolare un solo valore alla volta (per esempio, una variabile come int number è utile per
rappresentare un qualsiasi numero intero), e gli array (tipi di dato aggregati o complessi),
ideali per raggruppare in modo logico valori correlati e dello stesso tipo (per esempio, un
array come int months[12] è utile per rappresentare i dodici mesi di un anno).
Tuttavia, quando si scrivono dei programmi, si possono avere delle esigenze ulteriori per
la rappresentazione o per la strutturazione delle informazioni che i tipi citati non sono in
grado di coprire; si pensi, per esempio, alla necessità di memorizzare un insieme di dati che
rappresentano un’ipotetica agenda telefonica che deve contenere informazioni quali un
identificativo di un soggetto (nome e cognome), un numero di telefono (prefisso e numero),
un indirizzo civico (via e numero), un indirizzo e-mail e così via per le altre.
In questo caso appare evidente che le informazioni sono sì correlate, ma i tipi di dati
occorrenti sono di differente tipo (il nome e il cognome potrebbero essere di tipo array di
caratteri, mentre il numero civico potrebbe essere di tipo int); pertanto, il tipo array non può
essere utilizzato per la strutturazione di questa informazione complessa.
Ancora, si pensi alla necessità di avere una struttura di dati capace di memorizzare
informazioni di tipo diverso in modo “dinamico”, ossia in un determinato momento un suo
elemento può essere “solo” di un tipo (per esempio, o int o float o double e così via).
TERMINOLOGIA
Il termine struttura di dati indica il modo in cui i dati sono conservati e organizzati nella
memoria e le operazioni che si possono compiere su di essi (algoritmi). È pertanto
generalizzabile nella seguente forma: STRUTTURA DI DATI = INSIEME DI ELEMENTI +
OPERAZIONI.

Così potremmo definire una sorta di array contenente elementi di tipo diverso, dove cioè
ciascun suo elemento è del tipo della struttura succitata che usa, però, un indirizzo di
memoria condiviso che, cioè, di volta in volta, può contenere un valore di un tipo di dato
differente.
Si pensi, infine, all’esigenza di migliorare la leggibilità di un programma quando fa uso
di valori interi che esprimono un determinato significato; per esempio, potremmo avere
l’array months inizializzato con i valori 1 (per gennaio), 2 (per febbraio), 3 (per marzo) e così
via fino a 12 (per dicembre), e utilizzarlo per assegnare a una variabile di tipo int
current_month il mese corrente con un’istruzione come current_month = months[2].

In questo caso, però, è evidente come si debba, in qualche modo, documentare che
months[2] rappresenti il mese di febbraio perché esso non è implicitamente chiaro.

Se, invece, abbiamo la possibilità di creare un nuovo tipo di dato i cui valori sono
espressi trami dei nomi significativi che possono essere “assegnati” direttamente a delle
variabili, ecco che la leggibilità del programma migliora significativamente.
Ritornando, quindi, al problema di rappresentare i mesi dell’anno potremmo creare un
tipo months inizializzato con i valori JANUARY, FEBRUARY, MARCH e così via fino a DECEMBER, e poi
assegnare alla variabile current_month il valore del mese corrente con un’istruzione come
current_month = FEBRUARY.

NOTA
Si poterebbe rilevare come lo stesso risultato di dotare il codice di maggiore leggibilità sia
raggiungibile anche con l’uso delle macro proprie della direttiva #define. Tuttavia ciò
presenta diversi svantaggi quali, solo per citarne due: il preprocessore sostituisce nel codice
sorgente qualsiasi riferimento al nome simbolico con il valore relativo, e dunque tale nome
non è disponibile in caso di debugging del programma; dobbiamo scrivere tante direttive
#define quanti sono i nomi che dobbiamo impiegare, e ciò, oltre che essere scomodo, non

evidenzia neppure che tra di esse vi sia una sorta di collegamento, ossia che siano parte di
uno stesso tipo di dato.

Per soddisfare, dunque, tutte le esigenze evidenziate, il linguaggio C mette a disposizione


degli appositi tipi di dati creabili mediante:
la keyword struct, utile per l’ipotetica agenda telefonica;
la keyword union, utile per l’array con elementi di tipo diverso;
la keyword enum, utile per il tipo con nomi significativi.
Strutture
Una struttura (structure) è un tipo di dato derivato rappresentato da una sequenza o
insieme di elementi, detti membri o campi (fields), che sono allocati in memoria
sequenzialmente e che possono anche essere di differente tipo (Sintassi 8.1).

Sintassi 8.1 Dichiarazione di una struttura.


struct [tag]
{
data_type identifier_1;
data_type identifier_2;
...
data_type identifier_N;
} [identifier_1, identifier_2, ..., identifier_N];

Una struttura si dichiara indicando la keyword struct; un tag o etichetta facoltativa utile
per dare un identificativo alla relativa struttura; una coppia di parentesi graffe { } al cui
interno esplicitare le dichiarazioni di altri tipi di oggetti e che ne rappresentano i membri;
uno o più identificatori opzionalmente indicati dopo la parentesi graffa di chiusura (}) della
struttura che rappresentano i nomi degli oggetti del tipo di struttura definita.

Snippet 8.1 Dichiarazione di alcune strutture.


...
#define MAX_A_LENGTH 50

int main(void)
{
// dichiara una struttura di tipo struct book contenente
// come membri gli oggetti lì dichiarati
struct book
{
char title[MAX_A_LENGTH];
char author_name[MAX_A_LENGTH];
char publisher_name[MAX_A_LENGTH];
int publication_year;
float price;
int stock;
};

// definisce l'oggetto a_book di tipo struct book


struct book a_book;

// dichiara una struttura di tipo struct senza alcun tag contenente


// i membri x e y e contestualmente definisce l'oggetto a_point
// di quel tipo di struttura senza nome
struct
{
int x;
int y;
} a_point;

// dichiara una struttura di tipo struct employee contenente i membri


// lì indicati e contestualmente definisce gli oggetti White, Stone
// e Gattes di quel tipo
struct employee
{
char first_name[MAX_A_LENGTH];
char last_name[MAX_A_LENGTH];
int identifier_code;
char job_title[MAX_A_LENGTH];
} White, Stone, Gattes;
...
}

Lo Snippet 8.1 definisce tre diverse strutture.


La prima è utile per rappresentare un tipo di dato che modella il concetto di libro e ha
dei campi che ne indicano il titolo, l’autore, il prezzo e via discorrendo. Questa
struttura è definita con il tag book che consente di associarle un identificativo, ossia di
decidere un nome di tipo per essa che sarà fondamentale per creare oggetti di quel tipo.
Infatti, l’istruzione successiva alla dichiarazione della struttura book, struct book a_book,
dichiara la variabile a_book come un oggetto di tipo struct book, ossia come un
identificatore che rappresenta una collezione di elementi a loro volta identificati,
raggruppati sotto il nome book. È inoltre fondamentale precisare che la dichiarazione di
una struttura modella solo un nuovo tipo ma non alloca contestualmente spazio di
memoria per essa; si limita, in pratica, a fornire una descrizione di come tale struttura
deve essere rappresentata (quale è la sua forma). Infatti, è solo una contestuale o
successiva dichiarazione di una variabile del suo tipo che farà allocare spazio di
memorizzazione al compilatore in conformità al tipo dei membri lì esplicitati.

TERMINOLOGIA
Quando si dichiara una struttura è prassi dire che si crea un tipo struttura (structure type),
mentre quando si dichiara una variabile del suo tipo è abitudine dire che si crea una
variabile di struttura o variabile di tipo struttura (structure variable).

La seconda è invece utile per descrivere un generico punto in un piano bidimensionale


e ha, infatti, i campi x e y. Questa struttura è definita senza alcun tag ed è utilizzabile
solo per dichiarare contestualmente variabili del suo tipo. Ciò implica che se volessimo
dichiarare altri oggetti di una struttura similare (con dei campi x e y), ancorché non
compatibile, dovremmo necessariamente ripeterne la dichiarazione. In più, questa
forma di dichiarazione di una struttura senza tag, non avendo un tipo, presenta anche lo
svantaggio di non poter essere utilizzata come parametro formale di una funzione. Di
fatto è come se avessimo dichiarato una struttura di un tipo sena nome (unnamed-tag)
non referenziabile, quindi, in altro modo a seguito della sua dichiarazione.
La terza è infine utile per rappresentare un generico impiegato e ha campi che ne
descrivono il nome, il cognome, il titolo lavorativo e così via. Questa struttura è
definita con il tag employee (è quindi una struttura di tipo struct employee) e
contestualmente sono definite tre variabili del suo tipo con gli identificatori White, Stone
e Gattes.
Quando si dichiara una struttura è possibile utilizzare, senza che ciò generi alcun
conflitto: un nome di tag oppure un nome di un membro che sia uguale al nome di un
identificatore di un’altra variabile posta all’interno dello stesso programma (ciò è possibile
perché una struttura ha uno spazio dei nomi riservato e differente da quello usato per gli
identificatori ordinari); nomi di membri uguali a nomi di membri di altre strutture (ciò è
possibile perché ogni struttura definisce un proprio scope separato dallo scope di altre
strutture).

Snippet 8.2 Strutture e scope.


// nessun conflitto tra i nomi dei membri di STRUTTURA 1, STRUTTURA 2
// e STRUTTURA 3 anche se hanno stesso nome; red, green a blue
struct // STRUTTURA 1
{
int red;
int green;
int blue;
} RGB_1;

struct // STRUTTURA 2
{
int red;
int green;
int blue;
} RGB_2;

struct color // STRUTTURA 3


{
int red;
int green;
int blue;
} RGB_3;

// ATTENZIONE struct color già dichiarata come STRUTTURA 3


// messaggio: error: redefinition of 'struct color'
// cioè non si possono avere due o più strutture dichiarate con lo stesso tag
struct color // STRUTTURA 4
{
int R;
int G;
int B;
};

// nessun conflitto con i nomi dei membri delle strutture sopra indicate
// e anche con il tag color dell'ultima struttura
int red, green, blue, color;

// ATTENZIONE RGB_3 è in conflitto con l'identificatore RGB_3 definito


// per la STRUTTURA 3; esso è un nome di una variabile di struttura che
// non sta in uno scope separato, e pertanto se un'altra variabile
// nel programma ha il suo stesso nome allora si genera un conflitto di nomi
struct // STRUTTURA 5
{
int red;
int green;
int blue;
} RGB_3;

// ATTENZIONE conflitto con RGB_3 della STRUTTURA 3 e RGB_3 della STRUTTURA 5


// anche qui questo identificatore non sta in alcun scope separato
int RGB_3;
Inizializzazione
I membri di una struttura possono essere inizializzati allo stesso modo degli elementi di
un array, ossia fornendo un’apposita lista di inizializzatori (Sintassi 8.2 e 8.3 e 8.4):

Sintassi 8.2 Inizializzazione di una variabile di struttura che non ha un tipo espresso con un tag.
struct
{
data_type identifier_1;
data_type identifier_2;
...
data_type identifier_N;
} identifier_1 = {value_1, value_2, ..., value_N};

Sintassi 8.3 Inizializzazione di una variabile di struttura di un determinato tipo.


struct [tag]
{
data_type identifier_1;
data_type identifier_2;
...
data_type identifier_N;
} identifier_1 = {value_1, value_2, ..., valueN_};

Sintassi 8.4 Inizializzazione di una variabile di struttura di un determinato tipo: alternativa.


struct [tag]
{
data_type identifier_1;
data_type identifier_2;
...
data_type identifier_N;
};

struct [tag] identifier_1 = {value_1, value_2, ..., value_N};

In sostanza, le regole per l’inizializzazione dei membri di una struttura sono le seguenti:
ogni inizializzatore assegna un valore al membro corrispondente e nell’ordine dato (per
esempio, value_1 sarà il valore per identifier_1, value_2 sarà il valore per identifier_2 e così
via per gli altri; i valori possono essere forniti anche da espressioni non costanti, magari
ricavati da valutazioni di variabili, ma ciò è consentito solo se la struttura ha una classe di
memorizzazione diversa da quella statica, come quella automatica); è possibile fornire
meno inizializzatori rispetto ai membri di una struttura e, in questo caso, i membri non
esplicitamente inizializzati conterranno il valore 0 (se un membro è un puntatore, sarà
inizializzato con un null pointer).

Snippet 8.3 Inizializzazione di una struttura.


...
#define MAX_A_LENGTH 50

int main(void)
{
struct employee // dichiarazione di una struttura di tipo struct employee
{
char first_name[MAX_A_LENGTH];
char last_name[MAX_A_LENGTH];
int identifier_code;
char job_title[MAX_A_LENGTH];
};

// inizializzazione esplicita di tutti i membri della struttura struct employee


struct employee PP =
{
"Pellegrino",
"Principe",
45456,
"Sviluppatore Software"
};

int code = 12434;

// inizializzazione di alcuni membri della struttura struct employee


struct employee AR =
{
"Alberto",
"Rossi",
// ok valore ricavato da un'espressione non costante: la struttura
// ha implicitamente la classe di memorizzazione automatic
code
// qui ultimo valore mancante... stringa vuota...
};
...
}

Una struttura può essere inizializzata anche mediante l’utilizzo degli inizializzatori
designati che però, a differenza di quelli visti per gli array hanno una diversa sintassi
(Sintassi 8.5).

Sintassi 8.5 Inizializzazione mediante gli inizializzatori designati.


struct [tag] identifier_1 = {.member = value, ..., .member_N = value_N};

Tra la coppia di parentesi graffe { } devono essere impiegati dei designatori costituiti dal
simbolo punto . (dot operator) e il nome del membro da valorizzare. Segue, quindi, per
ogni designatore l’operatore di assegnamento e il relativo valore.

Snippet 8.4 Inizializzazione di una struttura mediante inizializzatori designati.


struct color // dichiarazione della struttura struct color
{
int red;
int green;
int blue;
};

// utilizzo degli inizializzatori designati per valorizzare la struttura struct color


struct color magenta =
{
.red = 255,
.green = 85,
.blue = 163
};

Anche nel caso degli inizializzatori designati si possono applicare le seguenti regole di
utilizzo: l’ordine di collocazione dei designatori può non corrispondere a quello dei membri
da valorizzare perché l’operatore punto consente di accedere direttamente al membro
indicato dopo di esso; è possibile valorizzare i membri di una struttura sia mediante dei
designatori sia mediante dei valori senza designatori (in quest’ultimo caso il valore senza
designatore valorizzerà il membro posto subito dopo il membro indicato dal designatore che
lo precede).

Snippet 8.5 Utilizzo degli inizializzatori designati e di inizializzatori regolari per la struct color.
struct color // dichiarazione della struttura struct color
{
int red;
int green;
int blue;
};

// utilizzo degli inizializzatori designati e di un valore diretto


// per valorizzare la struttura struct color
struct color magenta =
{
.red = 255,
85, // valorizzerà il membro green
.blue = 163
};

Operazioni effettuabili con le strutture


La prima e più importante operazione che C permette di compiere con le strutture è quella
per ottenere o impostare il valore dei suoi membri. Essa si effettua mediante l’utilizzo
dell’operatore punto (dot operator) unitamente al nome di una variabile di struttura e al
nome di un membro da selezionare (Sintassi 8.6).

Sintassi 8.6 Selezione di un membro di una struttura.


data_type identifier = structure_variabile_identifer.member_identifier; // per un get
structure_variabile_identifer.member_identifier = value; // per un set

Listato 8.1 DotOperator.c (DotOperator).


/* DotOperator.c :: Utilizzo dell'operatore punto :: */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define MAX_A_LENGTH 50

int main(void)
{
struct employee // dichiarazione di una struttura di tipo struct employee
{
char first_name[MAX_A_LENGTH];
char last_name[MAX_A_LENGTH];
int identifier_code;
char job_title[MAX_A_LENGTH];
} PP; // PP è una variabile di struttura di tipo struct employee

// set dei membri


strcpy(PP.first_name, "Pellegrino");
strcpy(PP.last_name, "Principe");
PP.identifier_code = 45456;
strcpy(PP.job_title, "Sviluppatore Software");

// get dei membri


printf("%s %s [%d] ha il titolo di %s\n", PP.first_name, PP.last_name,
PP.identifier_code, PP.job_title);
return (EXIT_SUCCESS);
}

Output 8.1 Dal Listato 8.1 DotOperator.c.


Pellegrino Principe [45456] ha il titolo di Sviluppatore Software

Il Listato 8.1 definisce la struttura employee e contestualmente la variabile di struttura PP di


quel tipo. Poi pone in essere delle semplici operazioni di assegnamento di valori per i campi
della struttura utilizzando l’operatore punto sull’identificatore PP.
I valori assegnati sono quindi visualizzati a video grazie alla consueta funzione printf.
Anche in questo caso i valori sono ottenuti impiegando l’operatore punto che è preceduto
dall’identificatore PP ed è seguito, in successione, dal nome deli membri da selezionare.
NOTA
I membri first_name, last_name e job_title sono stati valorizzati mediante l’utilizzo della
funzione strcpy, dichiarata nel file header <string.h>, che copia i caratteri della stringa
puntata dal secondo argomento nell’array puntato dal primo argomento. Ciò si è reso
necessario perché non è possibile assegnare direttamente un letterale stringa a un array di
caratteri come per esempio PP.first_name = "Pellegrino" (ricordiamo che il nome di un array è
un lvalue non modificabile, ossia dopo che è stato inizializzato non è possibile farlo puntare
ad altri oggetti, nel nostro caso a un’altra stringa di caratteri).

Un’altra operazione effettuabile con le strutture è quella che prevede l’utilizzo


dell’operatore di assegnamento =, che consente di copiare i valori di tutti i membri di una
struttura sorgente in tutti gli altri membri di una struttura destinazione.
Tuttavia l’operazione di copia è lecita solo se le strutture sono “compatibili”; due o più
strutture sono compatibili quando le variabili di struttura relative si riferiscono allo stesso
tipo oppure sono definite contestualmente alla dichiarazione di una struttura senza tag.

Snippet 8.6 Compatibilità tra strutture.


struct HSB_color // struttura di tipo struct HSB_color
{
int hue;
int saturation;
int brightness;
};

struct RGB_color // struttura di tipo struct RGB_color


{
int red;
int green;
int blue;
};

struct RGB_color fuchsia = {255, 0, 255};


struct RGB_color silver = {192, 192, 192};
struct HSB_color fuchsia_2 = {300, 100, 100};

// copia permessa: fuchsia e silver sono dello stesso tipo ossia struct color
silver = fuchsia;

// error: incompatible types when assigning to type 'struct RGB_color' from type 'struct
// HSB_color'
silver = fuchsia_2;
// alcune strutture senza tag; anche se hanno gli stessi membri sono strutture
// di "differente" tipo, ossia di tipi senza nome
struct
{
int j[2];
int k;
} a = {
{2, 2}, // primo membro: un array, necessaria un'altra lista di inizializzatori
1000 // secondo membro
},
b = {{4, 4}, 2000}; // scrittura compatta ma equivalente

struct
{
int j[2];
int k;
} c = {{8, 8}, 3000};

// copia permessa: a e b sono definite contestualmente alla struct senza tag


a = b;

// error: incompatible types when assigning to type 'struct <anonymous>'


// from type 'struct <anonymous>'
b = c;

A parte le operazioni di selezione dei membri e di assegnamento tra strutture compatibili


ora evidenziate, non è possibile effettuare altri tipi di operazioni come, per esempio, quelle
per verificare se due strutture sono uguali (operatore ==) o diverse (operatore !=).

Strutture e la keyword typedef


È possibile utilizzare la keyword typedef anche con le strutture in modo da definire un
nuovo nome di tipo per esse che può essere impiegato, come modo alternativo, per definire
delle variabili di quel tipo di struttura.

Snippet 8.7 Strutture e typedef.


// dichiaro una struttura di tipo struct color nel consueto modo:
// utilizzo cioè il tag color
struct color
{
int red;
int green;
int blue;
};

// definisco la variabile RED che è di tipo struct color


struct color RED = {255, 0, 0};

// dichiaro una struttura di un tipo senza nome a cui però attribuisco


// un nome di tipo, ossia color, tramite typedef
typedef struct
{
int red;
int green;
int blue;
} color;

// definisco la variabile GREEN che è di tipo color


color GREEN = {0, 255, 0};

// dichiaro una struttura di tipo struct rect e uso anche un typedef


// con cui definisco un alias di tipo. Il tag rect è comunque opzionale
typedef struct rect
{
int x;
int y;
int width;
int height;
} rect;

// definisco la variabile a_rect di tipo rect


rect a_rect = {0, 0, 200, 300};

// dichiaro una struttura di tipo struct point


struct point
{
int x;
int y;
};

// in questa dichiarazione uso typedef per creare un alias del tipo struct point
typedef struct point point;

// definisco delle variabili di tipo point


point a_point = {10, 10};
point another_point = {20, 20};

// assegnamento consentito: tipi compatibili perché entrambi sono, per l'appunto,


// di tipo point
a_point = another_point;

Lo Snippet 8.7 dichiara: una struttura di tipo struct color nel consueto modo, ossia
mediante il tag color posto subito dopo la keyword struct; una struttura senza un nome di
tipo cui viene associato, però, il nome di tipo color mediante la keyword typedef; una
struttura di tipo struct rect per la quale viene anche definito l’alias di nome di tipo rect
mediante la keyword typedef (il tag è comunque superfluo); una struttura di tipo struct point

di cui, successivamente, in un’istruzione separata, viene fatto un alias del tipo mediante la
keyword typedef.
NOTA
Leggendo le dichiarazioni di alcune delle strutture presentate è stato usato per esse, senza
problemi di conflitto, lo stesso identificatore per il tag (per esempio rect) e per la variabile di
struttura (per esempio rect). Ribadiamo che ciò è perfettamente lecito perché una struttura
ha un proprio spazio dei nomi disgiunto dallo spazio dei nomi delle variabili ordinarie;
pertanto, ritornando alla nostra struttura che definisce un template per un rettangolo, anche
se l’identificatore rect si trova dichiarato due volte nello stesso scope (quello della funzione
main), esso è di fatto “inserito” nei due spazi di nomi differenti detti.

A prescindere comunque da tutte le modalità di utilizzo di typedef, perfettamente


equivalenti perché tutte definiscono un alias di un tipo struct senza nome di tipo o con nome
di tipo, è importante comprendere che il suo utilizzo è assolutamente arbitrario, ossia
possiamo decidere o meno se usarlo in sostituzione del più “prolisso” modo che fa uso della
keyword struct seguita dal nome del rispettivo tag di tipo.
CURIOSITÀ
Nella comunità di sviluppatori C c’è chi ritiene che typedef debba essere usato solo per i casi
documentati nel K&R, ossia per rendere il codice più portabile e per rendere più facile la
comprensione di dichiarazioni particolarmente complesse (si pensi ai puntatori a funzioni).
Usarlo invece per altri scopi, come per esempio per le strutture, non farebbe altro che
“offuscare”, nascondere, il vero tipo di dato della variabile, e dovrebbe pertanto essere
evitata. Per esempio, nelle linee guida sullo stile di scrittura del codice sorgente per il kernel
Linux è scritto al Capitolo 5 quanto segue: “Please don’t use things like “vps_t”. It’s a
_mistake_ to use typedef for structures and pointers. When you see a vps_t a; in the
source, what does it mean? In contrast, if it says struct virtual_container *a; you can actually
tell what “a” is. Lots of people think that typedefs “help readability”. Not so”.

Strutture e l’operatore sizeof


Se abbiamo una struttura come quella dello Snippet 8.8 e alla stessa vi applichiamo
l’operatore sizeof, quanti byte di dimensione saranno ritornati come risultato?

Snippet 8.8 L’operatore sizeof applicato a una struttura.


struct S
{
char c_data;
int data;
} s = {'A', 65};

// quanti byte saranno allocati per la struttura s di tipo struct S?


size_t size_of_s = sizeof (s); // ???

Di istinto, la risposta su un sistema dove gli int sono di 32 bit potrebbe essere 5 byte, cioè
1 byte per allocare il tipo char c_data e 4 byte per allocare il tipo int data.
Purtroppo, invece, la risposta è diversa; sono infatti allocati in totale 8 byte: 1 byte per il
tipo char, 3 byte di memoria “non usati” detti holes (buchi) e 4 byte per il tipo int.
Cosa rappresentano, dunque, quei 3 byte di memoria non usati? Per poterlo comprendere
appieno dobbiamo, in via preliminare, fare una breve digressione sul concetto di
allineamento della memoria.
Iniziamo ricordando che ogni dato è allocato in memoria a partire da un determinato
indirizzo e per una certa quantità di storage che è uguale alla dimensione del tipo di dato
scelto (per esempio, il numero 100 potrebbe essere memorizzato come tipo int, memorizzato
a partire dalla locazione 0x003efa64, e occupare in totale 4 byte, su un sistema a 32 bit, fino,
quindi, alla locazione 0x003efa67).
Chiaramente non tutti i dati richiedono lo stessa quantità di memoria di allocazione; un
dato di tipo char, infatti, richiede 1 byte, un dato di tipo short può richiedere 2 byte, un dato
di tipo double può richiedere 8 byte e così via.
Se, quindi, abbiamo una serie di dati allocati senza alcuna considerazione di un possibile
allineamento e tali dati sono di tipo char, di tipo short e di tipo int, la memoria potrebbe
apparire non allineata (Figura 8.1), data un’architettura dove una word è di 32 bit (4 byte).
Figura 8.1 Memorizzazione di dati non allineati.

Per non allineamento intendiamo la collocazione in memoria di dati secondo una


diposizione che non garantisce che ognuno di essi sia posto a partire da un indirizzo di
memoria che è un esatto multiplo della sua dimensione massima (per esempio, a indirizzi
“pari” per un tipo short di 2 byte, a indirizzi multipli di 4 per un tipo int di 4 byte, a indirizzi
multipli di 8 per un tipo double di 8 byte e così via).
Quando i dati non solo allineati in memoria si creano problemi di efficienza per il loro
reperimento perché, come nel nostro esempio, nel caso si volesse accedere al dato del tipo
int si richiederebbero sia due accessi in memoria (l’uno per prendere un byte posto nella

word che inizia all’indirizzo 0x0000000 e l’altro per prendere i rimanenti byte posti alla
successiva word che inizia all’indirizzo 0x00000004) sia una qualche operazione di bitshifting
per ottenere il dato completo.
Per sopperire a questo importante problema e per garantire un’efficienza di accesso ai
dati in memoria, i compilatori moderni, in caso di dati non allineati, tendono ad aggiungere
delle unità di memoria inutilizzate (buchi) che hanno come scopo quello di riempire
(padding) la memoria al fine di allinearla correttamente.
Così, allineata con dei buchi, la nostra memoria potrebbe apparire come quella presentata
nella Figura 8.2, dove appare ora evidente come, se si volesse accedere al dato di tipo int,
sarebbe richiesto un solo accesso in memoria, cioè a partire dall’indirizzo della word
0x00000004 che contiene tutti i 4 byte del dato in esame.
Figura 8.2 Memorizzazione di dati allineati.

Ciò detto, appare chiaro perché il compilatore abbia posto 3 byte in più di memoria
inutilizzata alla nostra struttura, che potrebbe essere disposta in memoria come mostrato
dalla Figura 8.3, dove risulta evidente il suo corretto allineamento e dove si nota anche
come i membri della stessa siano stati posti, sequenzialmente nello stesso ordine in cui sono
stati scritti all’interno della dichiarazione di S medesima.

Figura 8.3 Disposizione in memoria dei membri della struttura S.

NOTA
Lo standard di C stabilisce che solo gli elementi di un array e i membri di una struttura
debbano essere posti in memoria in locazioni di memoria sequenziali, in modo crescente e
secondo l’ordine di dichiarazione (nel caso delle strutture, però, non necessariamente
devono occupare locazioni di memoria contigue perché potrebbero esserci dei buchi a
causa di problemi di allineamento). Ciò implica che se dichiariamo, in modo disgiunto, tre
diverse variabili, queste possono essere poste in memoria, per esempio, in locazioni di
memoria consecutive, in modo crescente o decrescente, oppure in locazioni di memoria non
consecutive. In pratica, è a discrezione dell’implementazione come e dove allocarle.

Facciamo un ulteriore passo in avanti dicendo, in modo generalizzato, che: data una
struttura un compilatore tenderà sempre ad allinearla rispetto alle necessità di allineamento
del tipo più grande e inserirà, se occorrenti, anche dei buchi supplementari di memoria
garbage per allineare la struttura stessa.
Così se abbiamo la seguente struttura (Snippet 8.9 e Figura 8.4), l’operatore sizeof
ritornerà un valore di dimensionamento di 24 byte e non di 10 byte, che è la somma della
dimensione dei due tipi char e del tipo double, e ciò perché la struttura è stata allineata per
multipli di 8 byte laddove tale valore è la dimensione del tipo double che è, infatti, come
dimensione, il tipo più grande.

Snippet 8.9 Dimensione di una struttura con il tipo maggiore di tipo double.
struct S
{
char c_1;
double d;
char c_2;
} s = {'A', 44.44, 'Z'};

size_t size_of_s = sizeof (s); // 24 byte!!!

Figura 8.4 Allineamento di una struttura di tipo struct S.

Il fatto che la struttura s di tipo struct S sia stata allineata per multipli di 8 byte garantisce
anche che, se si definisce un array di tipi struct S, il membro d di tipo double si verrà sempre
a trovare in locazioni di memoria che partiranno a un indirizzo multiplo di 8 byte.
Listato 8.2 StructAlignment.c (StructAlignment).
/* StructAlignment.c :: Allineamento delle strutture :: */
#include <stdio.h>
#include <stdlib.h>

#define S_LENGTH 2

int main(void)
{
struct S
{
char c_1;
double d;
char c_2;
};

// array di strutture di tipo struct S


struct S s[S_LENGTH] =
{
{'A', 44.44, 'B'}, // s[0]
{'Y', 88.88, 'Z'} // s[1]
};

printf("Indirizzo di c_1 in s[0] HEX=%p -- DEC=%d\n", &s[0].c_1, &s[0].c_1);


printf("Indirizzo di d in s[0] HEX=%p -- DEC=%d\n", &s[0].d, &s[0].d);
printf("Indirizzo di c_2 in s[0] HEX=%p -- DEC=%d\n", &s[0].c_2, &s[0].c_2);

printf("\nIndirizzo di c_1 in s[1] HEX=%p -- DEC=%d\n", &s[1].c_1, &s[1].c_1);


printf("Indirizzo di d in s[1] HEX=%p -- DEC=%d\n", &s[1].d, &s[1].d);
printf("Indirizzo di c_2 in s[1] HEX=%p -- DEC=%d\n", &s[1].c_2, &s[1].c_2);

return (EXIT_SUCCESS);
}

Output 8.2 Dal Listato 8.2 StructAlignment.c.


Indirizzo di c_1 in s[0] HEX=0028fec0 -- DEC=2686656
Indirizzo di d in s[0] HEX=0028fec8 -- DEC=2686664
Indirizzo di c_2 in s[0] HEX=0028fed0 -- DEC=2686672

Indirizzo di c_1 in s[1] HEX=0028fed8 -- DEC=2686680


Indirizzo di d in s[1] HEX=0028fee0 -- DEC=2686688
Indirizzo di c_2 in s[1] HEX=0028fee8 -- DEC=2686696

Da quanto sinora detto appare evidente che una struttura con membri di diverso tipo può
portare a consumare molta quantità di memoria necessaria per allineare correttamente i tipi
e la struttura stessa (si immagini cosa possa significare in termini di spreco di memoria in
programmi complessi con centinaia o migliaia di strutture).
Al fine di minimizzare la quantità di memoria utilizzata in caso di strutture che
necessitano di un allineamento, è possibile porre in atto una tecnica di “riordino” dei relativi
membri (structure packing) collocando gli stessi in ordine decrescente di spazio di storage
richiesto dai corrispettivi tipi.
Ritornando alla struttura dichiarata nello Snippet 8.9 e nel Listato 8.2, che pesava in
memoria 24 byte, potremmo dichiararla come segue (Snippet 8.10 e Figura 8.5), ossia
ponendo come primo membro il tipo double e a seguire i tipi char, e avere un recupero di 8
byte di spazio (la struttura pesa ora 16 byte con un recupero di circa il 33% di spazio).

Snippet 8.10 Dimensione di una struttura con i tipi ordinati.


struct S
{
double d;
char c_1;
char c_2;
} s = {44.44, 'A', 'Z'};

size_t size_of_s = sizeof (s); // 16 byte!!!

Figura 8.5 Allineamento di una struttura di tipo struct S con i membri ordinati.

In pratica, il compilatore ha posto dei buchi solamente per allineare la struttura stessa
(trailing padding) a un multiplo di 8 byte proprio del tipo più grande, ossia il tipo double.
NOTA
Lo standard di C specifica che i buchi possono essere posti solo tra i membri di una
struttura oppure dopo il suo ultimo membro. Ciò implica che non vi sarà mai un leading
padding, e l’indirizzo della struttura coinciderà sempre con l’indirizzo del suo primo membro.

Strutture e letterali composti


Così come per gli array, a partire da C99, è possibile definire dei letterali composti anche
per dei tipi di strutture; è legale, cioè, scrivere delle dichiarazioni di struct “senza nome”
(Sintassi 8.7) atte a fornire on demand tali oggetti.

Sintassi 8.7 Dichiarazione di un letterale composto per un tipo di struttura.


(struct tag | type) {value_1, value_2, ..., value_N};

Un letterale struttura si dichiara indicando tra la coppia di parentesi tonde ( ) un tipo di


struttura espresso tramite la keyword struct e il relativo tag oppure attraverso un suo alias di
tipo definito tramite la keyword typedef. Si indicano, quindi, tra la coppia di parentesi graffe
{ } i valori di inizializzazione dei suoi membri esprimibili anche mediante degli
inizializzatori designati.

Snippet 8.11 Utilizzo di diversi letterali composti applicati a vari tipi di strutture.
struct color // una struttura di tipo struct color
{
int red;
int green;
int blue;
};

typedef struct // una struttura di tipo point


{
int x;
int y;
} point;

// letterali strutture
struct color a_color = (struct color){255, 0, 0}; // via struct tag
point a_point = (point){300, 10}; // via typedef

// letterale struttura con membri inizializzati tramite degli inizializzatori designati


a_color = (struct color){.red = 0, .green = 255, .blue = 0};

Strutture annidate
Una struttura può contenere come suoi membri non solo variabili dei consueti tipi come
int, char, double e altri, ma anche, come già visto, tipi di array e tipi di struttura.

In quest’ultimo caso, quando cioè un tipo di struttura è membro di un’altra struttura, si


dice che essa vi è annidata oppure nidificata.
La possibilità di annidare un tipo di struttura all’interno di un’altra si rileva utile quando
risulta più logico “separare” delle informazioni in tante strutture di diverso tipo.
Per esempio, ritornando alla nostra struttura (Snippet 8.1) che ha modellato un generico
impiegato (struct employee), notiamo come vi siano delle informazioni (il nome, membro
first_name, e il cognome, membro last_name) che sarebbe più logico inserire in una struttura
separata creata ad hoc (diciamo, struct names) e fornire questa struttura come membro a tutte
quelle strutture che potrebbero necessitare di tali informazioni (non solo, dunque, a strutture
di tipo impiegato ma anche, per esempio, a strutture di tipo studente).

Listato 8.3 NestedStructures.c (NestedStructures).


/* NestedStructures.c :: Strutture annidate :: */
#include <stdio.h>
#include <stdlib.h>

#define MAX_A_LENGTH 50

int main(void)
{
struct names // una struttura di tipo struct names
{
char first_name[MAX_A_LENGTH];
char last_name[MAX_A_LENGTH];
};

struct employee // dichiarazione di una struttura di tipo struct employee


{
struct names e_names; // struttura annidata di tipo struct names
int identifier_code;
char job_title[MAX_A_LENGTH];
};

struct employee PP =
{
{"Pellegrino", "Principe"}, // inizializza la struttura annidata struct names
45456,
"Sviluppatore Software"
};

// get dei membri


printf("%s %s [%d] ha il titolo di %s\n", PP.e_names.first_name,
PP.e_names.last_name, PP.identifier_code, PP.job_title);

return (EXIT_SUCCESS);
}

Output 8.3 Dal Listato 8.3 NestedStructures.c.


Pellegrino Principe [45456] ha il titolo di Sviluppatore Software

Il Listato 8.3 dichiara la struttura di tipo struct names deputata a contenere informazioni
sul nome e cognome di una generica persona e la struttura di tipo struct employee atta a
contenere informazioni su un qualsiasi impiegato.
Quest’ultima struttura contiene come membro una struttura annidata, ossia e_names, che è
una variabile di struttura di tipo struct names, la quale conterrà le informazioni sul nome e
sul cognome del corrente oggetto impiegato definito per questo.
Infatti, l’istruzione successiva, definisce la variabile PP di tipo struct employee e la
inizializza fornendo, dapprima, una lista di inizializzatori separata per valorizzare i membri
first_name e last_name della struttura annidata di tipo struct names, e poi altri due valori che

inizializzano, rispettivamente, il membro identifier_code e job_title membri della struttura


di tipo struct employee.

Infine, l’istruzione printf stampa i valori di PP di tipo struct employee evidenziando anche
che per stampare i valori della sua struttura annidata di tipo struct names è necessario
utilizzare, sull’identificatore PP, due volte in successione l’operatore punto di selezione;
difatti, la valutazione di PP.e_names ritorna come valore il riferimento alla relativa struttura di
tipo struct names sulla quale è poi applicato nuovamente l’operatore punto per selezionare i
membri first_name e last_name.

Strutture e array
Una struttura può contenere come suo membro un tipo array di qualsiasi dimensione
desiderata, così come un array può contenere come suo elemento una struttura di un
qualsiasi tipo (è definito come array di strutture).
La dichiarazione di un array capace a contenere elementi di un certo tipo di struttura è
fatta allo stesso modo di quella utilizzata per dichiarare un array di un qualsiasi tipo
primitivo: per esempio, se int data[10]; dichiara un array di 10 elementi di tipo int, struct
employee ems[10]; dichiara un array di 10 elementi di tipo struct employee.

Ciò detto, l’applicazione dell’operatore di subscript [ ] ritornerà come elemento la


struttura posizionata all’interno dell’array all’indice indicato; dopodiché, sulla struttura
ritornata sarà possibile applicare il consueto operatore di selezione punto per scegliere il
membro da manipolare.

Snippet 8.12 Array di strutture.


...
#define MAX_COLORS 5

int main(void)
{
struct color // una struttura di tipo struct color
{
int red;
int green;
int blue;
};

// array di struct color


struct color colors[MAX_COLORS] =
{
{255, 215, 0}, // primo elemento (gold)
{154, 205, 50}, // secondo elemento (yellow green)
[2] = {0, 206,209}, // terzo elemento (dark turquoise)
{.red = 186, .green = 85, .blue = 211 }, // quarto elemento: (medium orchid)
{255, 222, 173} // quinto elemento (navajo white)
};

// ottengo il primo elemento: una struttura di tipo struct color contenente


// indicazioni sul colore gold
struct color GOLD = colors[0];

// ottengo i singoli valori dei componenti del colore gold...


int red = GOLD.red;
int green = GOLD.green;
int blue = GOLD.blue;
...
}

Lo Snippet 8.12 dichiara l’array colors dove ogni elemento sarà di tipo struct color.

Contestualmente inizializza tutti i suoi cinque elementi; di questi, notiamo come il terzo
elemento è inizializzato tramite l’utilizzo di un apposito inizializzatore designato, mentre il
quarto elemento è inizializzato utilizzando anche qui gli inizializzatori designati, che sono
però applicati per valorizzare i membri della relativa struttura.
In ogni caso, in linea generale, un array di strutture si inizializza scrivendo dopo il
simbolo di uguale = una coppia di parentesi graffe { } esterne e poi, per ogni struttura suo
elemento, un’altra coppia di parentesi graffe { } interne al cui interno si indicano i valori di
inizializzazione dei relativi membri.
NOTA
È opportuno precisare che l’utilizzo degli inizializzatori designati, sia per accedere agli
elementi di un array sia per accedere ai membri di una struttura, è facoltativo. Nel nostro
caso il loro impiego ha voluto solo dimostrare come sia possibile inizializzare gli elementi di
un array oppure i membri di una struttura in modo intercambiabile e frammisto ai normali
inizializzatori.

Strutture e puntatori
Una struttura è allocata a un indirizzo di memoria a partire dal quale sono disposti
consecutivamente i suoi membri e tale indirizzo può lecitamente essere assegnato a una
variabile che è un puntatore a quel tipo di struttura (è un puntatore a struttura).

Listato 8.4 PointerToStructures.c (PointerToStructures).


/* PointerToStructures.c :: Impiego di un puntatore a struttura :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
struct color // una struttura di tipo struct color
{
int red;
int green;
int blue;
};

// red è una variabile di tipo struct color


struct color red = {255, 0, 0};

// ptr_to_red è un puntatore a una struttura di tipo struct color


struct color *ptr_to_red = &red;

printf("red e' allocato a partire dall'indirizzo: [0x%p]\n", &red);


printf("ptr_to_red contiene l'indirizzo: [0x%p]\n", ptr_to_red);

// accesso ai membri di red per il tramite del puntatore ptr_to_red


printf("Valori dei membri della struttura puntata da ptr_to_red:\n");
printf("{ red=%d, green=%d, blue=%d }\n",
ptr_to_red->red, ptr_to_red->green, ptr_to_red->blue);

// sintassi alternativa di accesso a un membro di una struttura puntata;


// le parentesi ( ) sono essenziali perché l'operatore . ha una più alta
// precedenza dell'operatore *
int single_value = (*ptr_to_red).red;

printf("Il valore del membro red e' [%d]\n", single_value);

return (EXIT_SUCCESS);
}

Output 8.4 Dal Listato 8.4 PointerToStructures.c.


red e' allocato a partire dall'indirizzo: [0x0028fedc]
ptr_to_red contiene l'indirizzo: [0x0028fedc]
Valori dei membri della struttura puntata da ptr_to_red:
{ red=255, green=0, blue=0 }
Il valore del membro red e' [255]

Il Listato 8.4 dichiara la struttura color, ne crea la variabile di struttura red e poi ne passa
l’indirizzo di memoria, tramite il consueto operatore di indirizzamento &, al puntatore
ptr_to_red dello stesso tipo (rispetto al nome di un array, il nome di una variabile di struttura
non è valutato direttamente come l’indirizzo del suo primo membro, e pertanto bisogna
sempre utilizzare l’operatore & sul suo nome).
In definitiva un puntatore a una struttura si dichiara scrivendo la keyword struct e il suo
tag, il simbolo asterisco * e poi il solito identificatore; nel nostro caso, dunque, ptr_to_red è
un puntatore a una struttura di tipo struct color.
Utilizza, quindi, due istruzioni printf che stampano l’indirizzo di memoria di red e di
ptr_to_red (quello che contiene) evidenziando come la valutazione di un’espressione come
&red == ptr_to_red sia sempre vera; infatti, ptr_to_red conterrà come valore memorizzato lo
stesso indirizzo di memoria dove red sarà stato allocato.
Dopo di ciò impiega un’istruzione printf per stampare i valori dei membri red, green e blue
della struttura puntata dalla variabile puntatore ptr_to_red.
In questo caso viene però utilizzato un nuovo operatore, costituito dalla combinazione in
successione del carattere trattino (-) e maggiore di (>), che agisce anch’esso, al pari
dell’operatore punto (.), come operatore di selezione dei membri.
TERMINOLOGIA
L’operatore -> è spesso indicato con il termine arrow operator o right arrow selection.

In realtà, tuttavia, l’operatore di selezione -> è una notazione abbreviata messa a


disposizione di C per accedere in modo semplificato a un membro di una struttura per il
tramite di un puntatore; infatti, come evidenziato dall’istruzione seguente alla printf
descritta, che assegna il valore del membro red alla variabile di tipo int single_value, è
possibile comunque accedere a un membro di una struttura puntata dereferenziando prima il
puntatore alla struttura e poi applicando il consueto operatore di selezione punto.
NOTA
Se abbiamo deciso di usare un alias di tipo di una struttura tramite typedef, potremo anche
usare dei puntatori a quel tipo (Snippet 8.13).

Snippet 8.13 Puntatori, strutture e typedef.


typedef struct // un tipo color...
{
int red;
int green;
int blue;
} color;

// red è una variabile di tipo color


color red = {255, 0, 0};

// ptr_to_red è un puntatore a un tipo color


color *ptr_to_red = &red;

Lo Snippet 8.14, invece, mostra cosa accade quando si utilizza l’aritmetica dei puntatori
con dei puntatori a strutture (Figura 8.6).

Snippet 8.14 Aritmetica dei puntatori con le strutture.


...
#define MAX_POINTS 5

int main(void)
{
struct point // una struttura di tipo struct point
{
int x;
int y;
};

// un array di struct point


struct point points[MAX_POINTS] =
{
{10, 10},
{10, 20},
{10, 30},
{10, 40},
{10, 50}
};

// ptr_to_point punta ora alla prima struttura dell'array


struct point *ptr_to_point = &points[0];

// ptr_to_point punta ora all'ultima struttura dell'array


ptr_to_point += (MAX_POINTS - 1);
...
}

Nella fattispecie, l’applicazione dell’operatore += al puntatore ptr_to_point, che punterà al


principio all’indirizzo di memoria della prima struttura di tipo struct point che è un
elemento dell’array points, lo farà avanzare di tante unità di memoria quante indicate dalla
valutazione dell’espressione MAX_POINTS - 1.

Ma quale sarà il valore di queste unità di memoria? Dato che la dimensione della struttura
point è di 8 byte (pari alla somma dei suoi membri di tipo int senza aggiunta di alcun valore

di padding in quanto i dati sono già allineati in memoria), ogni incremento effettuato sul
puntatore ptr_to_point lo farà spostare di 8 byte e pertanto le 4 unità di memoria addizionate
(MAX_POINT - 1) saranno, in effetti, pari a 32 byte di scostamento reale dall’indirizzo di
partenza di ptr_to_point, che lo faranno dunque puntare all’indirizzo dell’ultima struttura di
tipo struct point elemento dell’array points.

Figura 8.6 Rappresentazione in memoria del puntatore ptr_to_point.


Strutture e funzioni
Una struttura può essere utilizzata come parametro formale di una funzione e anche come
suo tipo di ritorno. Si può, allo stesso modo, dichiarare un parametro di funzione e un tipo
di ritorno come puntatore a una struttura di un tipo.
Nel primo caso, si avrà come vantaggio quello di una protezione dei dati della struttura
passata come argomento perché i valori dei suoi membri saranno copiati nei membri della
relativa struttura dichiarata come parametro. Lo svantaggio sarà, però, un probabile
overhead su un programma, soprattutto se una struttura conterrà molti membri.
Nel secondo caso, invece, si avrà come vantaggio quello di una maggiore efficienza
computazionale perché sarà passato come dato solamente l’indirizzo della struttura
argomento. Lo svantaggio sarà, tuttavia, la mancanza di protezione dei dati della struttura
passata come argomento, che potranno essere modificati dalla struttura dichiarata come
parametro (a meno che non si deciderà di usare il qualificatore const facendo diventare il
parametro come un puntatore a costante del tipo di struttura riferita).

Listato 8.5 StructuresAndFunctions.c (StructuresAndFunctions).


/* StructuresAndFunctions.c :: Strutture e funzioni :: */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

struct point // una struttura di tipo struct point


{
int x;
int y;
int sum;
};

// prototipi di funzione
struct point buildAPoint(int x, int y);
struct point sumOfPoints(struct point p1, struct point p2);
void randomPoint(struct point *ptr_to_point);

int main(void)
{
// inizializzazione del generatore pseudo-casuale dei numeri
srand((unsigned int) time(NULL));

// genero due strutture di tipo struct point


struct point point_1 = buildAPoint(100, 100);
struct point point_2 = buildAPoint(200, 200);

printf("point_1 [x=%d], [y=%d]\n", point_1.x, point_1.y);


printf("point_2 [x=%d], [y=%d]\n\n", point_2.x, point_2.y);

// genero una struttura che è la somma di point_1 e point_2


struct point sum_of_point_1_and_point_2 = sumOfPoints(point_1, point_2);

printf("sum_of_point_1_and_point_2 [x=%d], [y=%d]\n\n",


sum_of_point_1_and_point_2.x, sum_of_point_1_and_point_2.y);

// modifico rand_point tramite una funzione che ne imposta in modo random


// i membri x e y
struct point rand_point;
randomPoint(&rand_point);

printf("rand_point [x=%d], [y=%d]\n", rand_point.x, rand_point.y);


return (EXIT_SUCCESS);
}

// ritorna una struttura di tipo struct point con i membri inizializzati


// con i valori dei parametri x e y
struct point buildAPoint(int x, int y)
{
return (struct point){.x = x, .y = y};
}

// calcola la somma di due punti e li ritorna in una struttura di tipo struct point
struct point sumOfPoints(struct point p1, struct point p2)
{
return (struct point){.x = p1.x + p2.x, .y = p1.y + p2.y};
}

// assegna ai membri x e y della struttura puntata da ptr_to_point dei valori casuali


void randomPoint(struct point *ptr_to_point)
{
ptr_to_point->x = rand() % 1000; // numeri pseudo-casuali tra 0 e 999
ptr_to_point->y = rand() % 1000; // numeri pseudo-casuali tra 0 e 999
}

Output 8.5 Dal Listato 8.5 StructuresAndFunctions.c.


point_1 [x=100], [y=100]
point_2 [x=200], [y=200]

sum_of_point_1_and_point_2 [x=300], [y=300]

rand_point [x=788], [y=248]

Il Listato 8.5 dichiara le funzioni buildAPoint, sumOfPoints e randomPoint che evidenziano


come nella pratica sia possibile, rispettivamente: ritornare una struttura di tipo struct point;

dichiarare dei parametri formali di tipo struct point; dichiarare un parametro formale di tipo
puntatore a una struttura di tipo struct point.

NOTA
È possibile far ritornare a una funzione un puntatore a una struttura di un certo tipo usando
la stessa sintassi vista per dichiarazione di un parametro di tipo puntatore a struttura. Per
esempio, un prototipo di funzione come struct node *addToListOfPoints(struct node *, struct
point); ritornerà un puntatore a una struttura di tipo struct node.

Membri array flessibili


Una struttura non può avere come proprio membro un array di lunghezza variabile
(VLA). Questa limitazione rappresenta un problema quando desideriamo dichiarare delle
strutture con degli array la cui dimensione è però dipendente da valori variabili; non
potendolo fare, siamo infatti costretti a dichiarare degli array con delle dimensioni fisse
(fixed-length array), che potrebbero però causare inutili sprechi di memoria se gli elementi
effettivamente memorizzati fossero inferiori rispetto a quelli preventivati.
A partire da C99 questa limitazione è stata parzialmente risolta grazie all’introduzione dei
cosiddetti flexible array members (membri array flessibili), degli array la cui dimensione
può essere omessa perché di lunghezza non specificata.
Un membro array flessibile deve, però, sottostare alle seguenti regole.
Deve essere dichiarato come ultimo membro di una struttura.
Deve esserci almeno un altro membro nella struttura dove è stato dichiarato.
Non può essere dichiarato se è già presente un altro membro array flessibile.
Deve essere dichiarato come un array ordinario ma le parentesi quadre [ ] di
indicazione del numero di elementi non devono contenere alcun valore.
In più, prima di utilizzarlo, è importante sapere che la struttura che lo contiene è di
dimensione pari alla dimensione di tutti gli altri membri più eventuale spazio di padding per
l’allineamento della memoria. In pratica, quando viene definita una variabile di una tale
struttura non viene allocata memoria per il membro array flessibile; è solo assegnato per
esso un indirizzo di una locazione di memoria a partire dalla quale verranno posti tutti i suoi
elementi quando avverrà l’effettiva allocazione.
Ciò detto, appare evidente la necessità di dover allocare la giusta quantità di memoria per
il membro array flessibile; a tal fine possiamo utilizzare la funzione malloc, con la quale
allocheremo spazio per la struttura più altro spazio per l’array flessibile (Snippet 8.15).

Snippet 8.15 Allocazione di memoria per una struttura con un membro array flessibile.
int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[]; // array flessibile
};

// an_int è un puntatore a una struttura di tipo struct IntegerData che conterrà


// un indirizzo di memoria che rappresenterà il punto di partenza dell'area di memoria
// allocata per contenere i membri della struttura relativa;
// malloc allocherà 32 byte di memoria posto un int di 32 bit: i primi 16 per le 3
// variabili di tipo int e la variabile di tipo float; i secondi 16 per i 4 elementi
// di tipo int dell'array data
struct IntegerData *an_int = malloc(sizeof(struct IntegerData) + nr * sizeof(int));

Nello Snippet 8.15 la funzione malloc allocherà lo spazio di memoria necessario a


contenere i valori per le variabili length, min, max e average e per i quattro elementi di tipo int
dell’array flessibile data. A partire da questo momento sarà dunque possibile valorizzare tali
elementi in modo lecito e corretto.

STRUCT HACK
Prima che C99 introducesse i membri array flessibili, era comune aggirare la limitazione di
non poter avere un membro di tipo array con dimensione variabile nell’ambito di una struttura
tramite una tecnica conosciuta con il nome di struct hack. In sostanza si dichiarava come
ultimo membro di una struttura un array con un solo elemento (dummy element), e poi si
utilizzava la funzione malloc per allocare la quantità di memoria effettivamente occorrente.
Tuttavia, questo approccio presentava (e presenta) un inconveniente non indifferente; un
compilatore può infatti generare del codice che non ritorna il valore atteso quando si prova ad
accedere al secondo elemento dell’array. Quanto detto può accadere perché, in accordo con
quanto stabilito dallo standard di C, si può avere un comportamento non definito se si prova
ad accedere a un elemento dell’array che è posto oltre il suo limite massimo.

Snippet 8.16 Struct hack.


int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[1]; // struct hack
};

// in questo caso si usa nr - 1 perché l'array ha già un elemento indicato...


struct IntegerData *an_int = malloc(sizeof (struct IntegerData) + (nr - 1) * sizeof (int));

// questo è l'unico elemento che è garantito essere valido


an_int->data[0] = 100;

// attenzione sto scrivendo e leggendo fuori dai limiti massimi dell'array data
// undefined behavior?
an_int->data[1] = 200;
int value = an_int->data[1];

Presentiamo ora un sorgente (Listato 8.6) che mostra un uso effettivo della struttura
IntegerData e come i suoi membri possono essere valorizzati.

Listato 8.6 FlexibleArrayMember.c (FlexibleArrayMember).


/* FlexibleArrayMember.c :: Membro array flessibile :: */
#include <stdio.h>
#include <stdlib.h>
#include <time.h>

int main(void)
{
// inizializzazione del generatore pseudo-casuale dei numeri
srand((unsigned int) time(NULL));

int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[]; // array flessibile
};

// an_int è un puntatore a una struttura di tipo struct IntegerData


struct IntegerData *an_int = malloc(sizeof (struct IntegerData) + nr * sizeof (int));

// valorizzazione membro length


an_int->length = nr;

// valorizzazione membri dell'array data


for (int i = 0; i < an_int->length; i++)
an_int->data[i] = rand() % 1000;

// valorizzazione min e max


int min = an_int->data[0];
int max = an_int->data[0];
for (int i = 1; i < an_int->length; i++)
{
min = an_int->data[i] < min ? an_int->data[i] : min;
max = an_int->data[i] > max ? an_int->data[i] : max;
}
an_int->min = min;
an_int->max = max;

// valorizzazione average
int tot = 0;
for (int i = 0; i < an_int->length; i++)
tot += an_int->data[i];
an_int->average = (float)tot / an_int->length;

// mostra i dati valorizzati nella struttura


printf("length:\t %d\n", an_int->length);
printf("min:\t %d\n", an_int->min);
printf("max:\t %d\n", an_int->max);
printf("average: %.2f\n", an_int->average);
printf("data[]: ");
for (int i = 0; i < an_int->length; i++)
printf("%d ", an_int->data[i]);
printf("\n");

free(an_int); // deallocazione memoria puntata da an_int

return (EXIT_SUCCESS);
}

Output 8.6 Dal Listato 8.6 FlexibleArrayMember.c.


length: 4
min: 318
max: 717
average: 492.75
data[]: 318 360 717 576

In conclusione, quando utilizziamo strutture che hanno un membro array flessibile


dobbiamo anche sapere che:
se copiamo una struttura in un’altra struttura il membro array flessibile non sarà
copiato (Snippet 8.17);
non è considerato corretto valorizzare un membro array flessibile tramite una lista di
inizializzatori (Snippet 8.18);
non è considerato corretto utilizzare una struttura con un membro di array flessibile
che sia elemento di un array oppure membro di un’altra struttura (Snippet 8.19).

Snippet 8.17 Copia di una struttura in un’altra con un membro di array flessibile.
int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[]; // array flessibile
};

// creo un puntatore a una struttura di tipo struct IntegerData e la valorizzo


struct IntegerData *an_int = malloc(sizeof (struct IntegerData) + nr * sizeof (int));
an_int->length = nr;
an_int->data[0] = 100;
an_int->data[1] = 200;
an_int->data[2] = 300;
an_int->data[3] = 400;
an_int->min = an_int->data[0];
an_int->max = an_int->data[3];
an_int->average = (float) (an_int->data[0] + an_int->data[1] + an_int->data[2] + an_int-
>data[3]) / an_int->length;

// copio la struttura puntata da *an_int nella struttura other_int


// in questo caso solo i membri ordinari saranno copiati in other_int
// other_int.length = 4
// other_int.min = 100
// other_int.max = 400
// other_int.average = 250.000000
// other_int.data[] = ?
struct IntegerData other_int = *an_int;

Snippet 8.18 Inizializzazione non consentita di un membro array flessibile.


int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[]; // array flessibile
} an_int =
{
4,
100,
500,
250.00f,
{100, 200, 300, 400, 500} // error: non-static initialization
// of a flexible array member
};

Snippet 8.19 Struttura con membro array flessibile come elemento di un array o membro di
un’altra struttura.
...
#define NRS 5

int main(void)
{
int nr = 4;
struct IntegerData // una struttura di tipo struct IntegerData
{
int length;
int min;
int max;
float average;
int data[]; // array flessibile
};

// struttura con membro array flessibile membro di un array di struct IntegerData


struct IntegerData ids[NRS]; // warning: invalid use of structure with flexible
// array member

// struttura con membro array flessibile membro di un'altra struttura


struct IWrapper
{
struct IntegerData an_int; // warning: invalid use of structure with flexible
// array member
};
...
}
Strutture anonime
Lo standard C11 ha introdotto un’altra interessante novità: è ora possibile dichiarare dei
membri di strutture come strutture senza nome (anonymous structure), ossia come strutture
dotate del solo specificatore struct, senza alcun tag e senza alcuna variabile di struttura.

Snippet 8.20 Strutture anonime.


// questa struttura rappresenta un rettangolo come una coppia di punti
// che individuano gli angoli diagonalmente opposti
struct rect // una struttura di tipo struct rect
{
int width;
int height;
struct // struttura anonima
{
int x1;
int y1;
};
struct // struttura anonima
{
int x2;
int y2;
};
};

// a_rect è di tipo struct rect


struct rect a_rect;

a_rect.width = a_rect.height = 0;

// valorizziamo i membri delle strutture anonime; in questo caso l'accesso a essi


// è semplificato e diretto, ossia avviene utilizzando una sola volta l'operatore punto
a_rect.x1 = 10;
a_rect.y1 = 10;
a_rect.x2 = 50;
a_rect.y2 = 50;

Lo Snippet 8.20 dichiara una struttura di tipo struct rect, che descrive un generico
rettangolo che ha come suoi membri, tra gli altri, due strutture annidate anonime che
rappresentano due coppie di punti su un piano utili a individuare le coordinate grazie alle
quali “generare” il relativo rettangolo.
Definisce, quindi, la variabile di struttura a_rect e poi valorizza i membri delle strutture
anonime annidate accedendo direttamente a essi; ossia tramite l’operatore punto
sull’oggetto a_rect; questo è possibile perché i membri di una struttura anonima sono
membri della struttura contenitrice (in pratica è come se i membri x1, y1 e x2, y2 fossero non
membri delle rispettive strutture anonime bensì della struttura rect).
NOTA
L’inizializzazione dei membri delle strutture anonime può essere compiuta allo stesso modo
di quella già vista per le strutture annidate non anonime, ossia ponendo per ognuna delle
coppie di parentesi graffe { } interne a quelle più esterne della struttura contenitrice
(Snippet 8.21).

Snippet 8.21 Inizializzazione dei membri di strutture anonime.


// questa struttura rappresenta un rettangolo come una coppia di punti
// che individuano gli angoli diagonalmente opposti
struct rect // una struttura di tipo struct rect
{
int width;
int height;
struct // struttura anonima
{
int x1;
int y1;
};
struct// struttura anonima
{
int x2;
int y2;
};
} a_rect =
{
0,0, // membri width e height
{10,10}, // membri x1 e y1 della struttura anonima
{50,50} // membri x2 e y2 della struttura anonima
};

Campi di bit
Nel Capitolo 4 abbiamo studiato gli operatori bit a bit (bitwise operators) che,
ricordiamo, consentono di accedere e manipolare direttamente i bit di un operando.
In quel contesto abbiamo anche analizzato alcune tecniche utili per impostare, cancellare,
verificare, commutare o estrarre dei bit da un operando impiegando tali operatori e
opportune maschere di bit.
In ogni caso le tecniche mostrate, ancorché utili, soffrono di uno svantaggio: sono
complicate da usare e a volte la loro comprensione è poco agevole.
C, da “variegato” linguaggio quale è, offre una valida e più semplice alternativa per
manipolare a basso livello i bit di un dato che si realizza nella possibilità di usare appositi
campi di bit (bit fields) dichiarati come membri all’interno di una struttura.

Sintassi 8.8 Dichiarazione di una struttura con campi di bit.


struct [tag]
{
data_type [identifier_1] : size;
data_type [identifier_2] : size;
...
data_type [identifier_N] : size;
} [identifier_1, identifier_2, ..., identifier_N];

La Sintassi 8.8 evidenzia che una struttura contenente membri che sono campi di bit si
dichiara quasi similmente a una struttura contenente membri ordinari. Si hanno però le
seguenti differenze: data_type può essere solo uno dei seguenti tipi predefiniti del
linguaggio, come _Bool, int, signed int e unsigned int, oppure un tipo definito dalla corrente
implementazione; dopo l’identificatore del membro, che è opzionale, deve apparire il
carattere due punti (:) cui far seguire, tramite size, un’espressione costante intera con un
valore non negativo che indica la dimensione in bit del relativo campo (tale valore deve
anche essere minore o uguale alla dimensione massima dei bit del tipo specificato; per
esempio, massimo 32 se il tipo è unsigned o signed int).

Snippet 8.22 Una struttura per delle date con dei campi di bit.
struct date // una struttura di tipo struct date
{
unsigned int day : 5; // 5 bit -> valori da 0 a 31
unsigned int month : 4; // 4 bit -> valori da 0 a 15
unsigned int year : 11; // 11 bit -> valori da 0 a 2047
_Bool isLeapYear : 1; // 1 bit -> valori 0 o 1
};

Lo Snippet 8.22 dichiara la struttura date utile a rappresentare una generica data formata
dalle indicazioni di un giorno, mese, anno e se l’anno è bisestile o meno.
Essa ha come membri dei campi di bit dove: il campo day, lungo 5 bit, è capace di
contenere un valore che può esprimere un giorno (valori significativi da 1 a 31); il campo
month, lungo 4 bit, è capace di contenere un valore che può esprimere un mese (valori
significativi da 1 a 12); il campo year, lungo 11 bit, è capace di contenere un valore che può
esprimere un anno (valori significativi da 0 a 2047); il campo isLeapYear, lungo 1 bit, è capace
di contenere un valore che può esprimere se il corrente anno è bisestile (valore significativo
1) oppure non lo è (valore significativo 0).

In definitiva, l’utilizzo di una struttura dichiarata come quella vista, ossia con dei campi
di bit, a volte consente di risparmiare memoria; se infatti non avessimo avuto a disposizione
tale feature, avremmo potuto dichiarare una struttura di una data (date_V2) nel seguente
inefficiente modo (Snippet 8.23).

Snippet 8.23 Una struttura per delle date senza dei campi di bit.
struct date_V2 // una struttura di tipo struct date_V2
{
int day; // 32 bit
int month; // 32 bit
int year; // 32 bit
_Bool isLeapYear; // 8 bit
};

Avremo così sprecato memoria non necessaria (infatti, un sizeof di una variabile di una
struttura date può dare, in genere, 4 come valore dei byte utilizzati, mentre un sizeof di una
variabile di una struttura date_V2 può dare 16 come valore dei byte utilizzati).
Perché sono utilizzati solo 4 byte per allocare i campi di bit della struttura date? La
risposta è che un compilatore quando incontra una struttura con dei campi di bit utilizza una
unità di storage, usualmente grande come una memory word della dimensione di un int nel
corrente sistema, nella quale “impacchetta”, in successione, i bit dichiarati per i relativi
campi di bit.
NOTA
Lo standard C11 non dice espressamente quanto debba essere grande una unità di
storage, ma asserisce che un’implementazione può allocare qualsiasi unità di storage
indirizzabile che sia grande abbastanza per contenere un bit field.

Per esempio, nel corrente sistema la memory word scelta sarà grande 4 byte (32 bit), e
rappresenterà l’unità di storage dove verranno collocati, consecutivamente e senza vuoti, da
sinistra a destra oppure da destra a sinistra, i 21 bit della struttura date.
Quindi, una variabile di struttura di tipo struct date peserà in memoria solo 4 byte perché
i 32 bit propri della memory word in uso saranno sufficienti a contenere tutti i bit dei campi
di bit lì definiti.

Listato 8.7 BitFields.c (BitFields).


/* BitFields.c :: Campi di bit :: */
#include <stdio.h>
#include <stdlib.h>

// prototipo di isLeapYear
_Bool isLeapYear(int year);

int main(void)
{
struct date // una struttura di tipo struct date
{
unsigned int day : 5; // 5 bit -> valori da 0 a 31
unsigned int month : 4; // 4 bit -> valori da 0 a 15
unsigned int year : 11; // 11 bit -> valori da 0 a 2047
unsigned int isLeapYear : 1; // 1 bit -> valori 0 o 1
};

// current_date è di tipo struct date


struct date current_date;

// valorizzazione dei campi di bit


current_date.day = 29;
current_date.month = 9;
current_date.year = 2014;
current_date.isLeapYear = isLeapYear(current_date.year);

printf("La data corrente scelta e': %d/%d/%d\nL'anno %d %s bisestile\n",


current_date.day,
current_date.month,
current_date.year,
current_date.year,
current_date.isLeapYear ? "e'" : "non e'"
);

return (EXIT_SUCCESS);
}

// definizione di isLeapYear
_Bool isLeapYear(int year)
{
return (year % 4 == 0) &&
(year % 100 != 0) ||
(year % 400 == 0);

Output 8.7 Dal Listato 8.7 BitFields.c.


La data corrente scelta e': 29/9/2014
L'anno 2014 non e' bisestile
Il Listato 8.7 dichiara e utilizza la struttura date in precedenza analizzata. È qui
importante notare come i campi di bit sono impiegati allo stesso modo dei normali membri
di una qualsiasi altra struttura, ossia vi si accede, in scrittura oppure in lettura, applicando il
consueto operatore punto (.) di selezione dei membri.
ATTENZIONE
Un campo di bit, diversamente da un membro di struttura ordinario, non occupa un indirizzo
di memoria referenziabile e pertanto non è possibile applicargli l’operatore &.

La Figura 8.7 illustra invece una possibile rappresentazione in memoria dell’unità di


storage scelta dal compilatore per allocare i campi di bit della variabile current_date di tipo
struct date (nel nostro caso l’unità di storage sarà di 4 byte e la disposizione dei bit sarà
effettuata da destra a sinistra).
Quando si dichiarano queste tipologie di strutture è anche possibile definire campi di bit
anonimi, cioè senza la specifica di alcun identificatore; in questo caso il valore espresso
come dimensione di bit se è diverso da 0, porrà dei bit di padding, tanti quanti indicati, fino
al successivo bit field, mentre se è uguale a 0, farà cominciare il prossimo bit field alla
successiva unità di storage.

Figura 8.7 Rappresentazione in memoria dei campi di bit della variabile di struttura current_date.

Snippet 8.24 Una struttura per delle date con dei campi di bit anonimi.
struct date // una struttura di tipo struct date
{
unsigned int day : 5; // 5 bit -> valori da 0 a 31
unsigned int month : 4; // 4 bit -> valori da 0 a 15
unsigned int : 7; // 7 bit di padding
unsigned int year : 11; // 11 bit -> valori da 0 a 2047
unsigned int : 0; // isLeapYear inizierà alla successiva unità di storage
unsigned int isLeapYear : 1; // 1 bit -> valori 0 o 1
} current_date = {29, 9, 2014, 0};

size_t s = sizeof current_date; // 8 byte

Lo Snippet 8.24 dichiara la struttura date con il campo day lungo 5 bit, il campo month
lungo 4 bit, un campo anonimo lungo 7 bit, il campo year lungo 11 bit, un campo anonimo
lungo 0 bit e il campo isLeapYear lungo 1 bit.
Contestualmente dichiara la variabile current_date di tipo struct date la quale avrà la
rappresentazione in memoria di cui alla Figura 8.8 e una dimensione di 8 byte perché sono
state utilizzate due unità di storage da 4 byte; la prima atta a contenere i campi di bit day,
month e year; la seconda atta a contenere il campo di bit isLeapYear che è stato posto a partire
dal suo inizio.

Figura 8.8 Rappresentazione in memoria dei campi di bit della variabile di struttura current_date.

Vediamo ora un altro esempio (Listato 8.9) che mostra come in concreto sia più semplice
utilizzare i campi di bit rispetto agli operatori bitwise per manipolare dei bit di un dato
operando.

Listato 8.8 BitFieldsVSBitwiseOperators.c (BitFieldsVSBitwiseOperators).


/* BitFieldsVSBitwiseOperators.c :: Campi di bit a confronto con gli operatori bitwise :: */
#include <stdio.h>
#include <stdlib.h>

#define MASK 0xFF

// prototipi di funzione per l'estrazione delle componenti di un colore


unsigned char getRED(unsigned int color);
unsigned char getGREEN(unsigned int color);
unsigned char getBLUE(unsigned int color);

int main(void)
{
// un colore espresso nella forma RGB
// Light blue RGB -> R = 173, G = 216, B = 230
unsigned int bw_color = 0xADD8E6;

// accesso alle componenti; si noti come per estrarre tali componenti occorra
// chiamare le relative funzioni che usano gli operatori bitwise >>, & e una maschera
// di bit; è inoltre indispensabile sapere la "posizione" delle componenti, ossia che
// le abbiamo disposte nel formato RGB e non, per esempio, come BGR
printf("Componenti bw_color: [%d, %d, %d]\n",
getRED(bw_color),
getGREEN(bw_color),
getBLUE(bw_color)
);

// un colore espresso tramite una struttura con campi di bit


struct RGB_color
{
unsigned int RED : 8;
unsigned int GREEN : 8;
unsigned int BLUE : 8;
} bf_color = {0xAD, 0xD8, 0xE6}; // Light blue

// accesso alle componenti; si noti l'estrema semplicità di utilizzo!!!


printf("Componenti bf_color: [%d, %d, %d]\n",
bf_color.RED,
bf_color.GREEN,
bf_color.BLUE
);

return (EXIT_SUCCESS);
}

// definizione delle funzioni per l'estrazione delle componenti di un colore


unsigned char getRED(unsigned int color)
{
return (color >> 16) & MASK;
}

unsigned char getGREEN(unsigned int color)


{
return (color >> 8) & MASK;
}

unsigned char getBLUE(unsigned int color)


{
return color & MASK;
}

Output 8.8 Dal Listato 8.9 BitFieldsVSBitwiseOperators.c.


Componenti bw_color: [173, 216, 230]
Componenti bf_color: [173, 216, 230]

In conclusione è comunque opportuno dire che le strutture con dei campi di bit,
quantunque consentano di manipolare i bit di un dato in modo più pratico e leggibile
rispetto agli operatori bitwise, presentano lo svantaggio che i programmi che ne fanno uso
non sono portabili, e ciò perché molte “decisioni” sulla loro gestione dipendono dalla
corrente implementazione e dal corrente sistema target (quanto sarà lunga la word? Come
saranno allineati i bit? Se c’è spazio insufficiente nella corrente unità di storage, il
successivo campo di bit sarà posto in una successiva unità di storage oppure potrà
sovrapporsi in unità di storage adiacenti? E così via).
Unioni
Un’unione (union) è un tipo di dato derivato rappresentato da una sequenza o insieme di
elementi, detti membri o campi (fields), che sono allocati in memoria in modo
“sovrapposto” e che possono anche essere di differente tipo (Sintassi 8.9).

Sintassi 8.9 Dichiarazione di una unione.


union [tag]
{
data_type identifier_1;
data_type identifier_2;
...
data_type identifier_N;
} [identifier_1, identifier_2, ..., identifier_N];

Un’unione si dichiara indicando la keyword union; un tag o etichetta opzionale utile per
dare un identificativo alla relativa unione; una coppia di parentesi graffe { } al cui interno
esplicitare le dichiarazioni di altri tipi di oggetti e che ne rappresentano i membri; uno o più
identificatori opzionalmente indicati dopo la parentesi graffa di chiusura } dell’unione che
rappresentano i nomi degli oggetti del tipo di unione definita.
In sostanza un’unione è un tipo “molto” simile al tipo struttura ma ha rispetto a esso
un’importante differenza: il compilatore allocherà per un’unione uno spazio di memoria
adeguato a contenere i valori del membro che ha il tipo di dimensioni maggiori.
Ciò significa che, rispetto a una struttura che potrà contenere nello stesso tempo dei
valori per tutti i tipi dichiarati (per esempio un valore int e un valore float e un valore char e
così via), un’unione potrà contenere solo un valore di uno dei tipi dichiarati (per esempio un
valore int o un valore float o un valore char e così via).
Quanto detto si verifica perché il compilatore utilizza la memoria per allocare un tipo
struttura oppure un tipo unione in modo differente.
Per una struttura avremo l’allocazione di uno spazio di memoria grande abbastanza per
contenere tutti i suoi membri più eventuale spazio di padding per l’allineamento. Il
primo membro è memorizzato a partire dall’indirizzo di memoria dove inizia la
struttura stessa, e gli altri membri sono memorizzati a indirizzi di locazioni di memoria
successive e nell’ordine in cui sono stati dichiarati. In questo caso una struttura potrà
contenere tanti valori quanti sono i membri lì dichiarati.
Per un’unione avremo l’allocazione di uno spazio di memoria grande quanto il più
grande dei tipi dei suoi membri. Tutti i suoi membri sono memorizzati a partire
dall’indirizzo di memoria dove inizia l’unione stessa. In questo caso un’unione potrà
contenere il valore di un solo membro alla volta.
La Figura 8.9 evidenzia in modo esplicito la differenza di allocazione di memoria per la
variabile di struttura s di tipo struct S (i suoi membri sono memorizzati in indirizzi di
memoria diversi) e la variabile di unione u di tipo union U (i suoi membri sono memorizzati
allo stesso indirizzo di memoria) di cui lo Snippet 8.25.

Figura 8.9 Rappresentazione in memoria della variabile s e della variabile u.

Snippet 8.25 Differenza tra strutture e unioni.


struct S // struttura di tipo struct S
{
char c;
int i;
} s = {'a', 33};

// 8 byte; 1 per il tipo char, 3 di padding, 4 per il tipo int


size_t size_s = sizeof s; // 8

union U // unione di tipo union U


{
char c;
int i;
} u = {'a'};

// 4 byte; pari al tipo maggiore, ossia int, che è di 4 byte


size_t size_u = sizeof u; // 4

Per quanto riguarda l’utilizzo di una variabile di tipo unione esso è identico a quello visto
per una variabile di tipo struttura; è cioè sufficiente impiegare l’identificatore del tipo
unione, l’operatore di selezione punto . (o freccia -> se è un puntatore a un unione) e il
nome del membro da manipolare.
Tuttavia, in ragione di quanto sin qui detto, quando si accede a un membro di un’unione
per valorizzarlo il valore del precedente membro eventualmente valorizzato andrà perso.
Ritornando alla variabile u di tipo union U dello Snippet 8.25, se assegniamo il valore 100
al membro i di tipo int il contenuto del membro c di tipo char sarà “distrutto”; ciò è
perfettamente logico perché il valore 100 verrà posto a partire dalla locazione di memoria
0x00eefbf4 che è “condivisa” dal membro i e dal membro c (solo uno di questi membri potrà
contenere un valore valido in un determinato momento).

Snippet 8.26 Accesso a un membro di un’unione.


union U // unione di tipo union U
{
char c;
int i;
} u = {'a'};

// u.c non conterrà più 'a'


u.i = 100;

NOTA
In fase di inizializzazione di un’unione è possibile assegnare un solo valore che sarà per il
primo membro. Nel nostro caso il letterale carattere 'a' è stato assegnato al primo membro,
ossia c. Chiaramente è sempre possibile usare un inizializzatore designato per scegliere
quale membro dell’unione inizializzare (Snippet 8.27).

Snippet 8.27 Inizializzazione esplicita di un membro di un’unione.


union U // unione di tipo union U
{
char c;
int i;
} u = {.i = 500}; // inizializzatore designato

Per il resto, come già anticipato, un’unione, al pari di una struttura (Snippet 8.28):
può essere assegnata a un’altra unione;
se ne può creare un alias di tipo tramite la keyword typedef;
se ne può definire un letterale composto; è legale, cioè, scrivere delle dichiarazioni di
union “senza nome”;
può essere annidata in un’altra unione;
può contenere come suo membro un tipo array, di qualsiasi dimensione desiderata, così
come un array può contenere come suo elemento un’unione di un qualsiasi tipo (è
definito array di unioni);
può essere di tipo puntatore (puntatore a unione);
può essere utilizzata come parametro formale di una funzione e anche come suo tipo di
ritorno. Si può, allo stesso modo, dichiarare un parametro di funzione e un tipo di
ritorno come puntatore a un’unione di un tipo;
può avere come propri membri delle unioni senza nome (anonymous unions), ossia
unioni dotate del solo specificatore union, senza alcun tag e senza alcuna variabile di
unione;
può avere come propri membri dei bit field.

Snippet 8.28 Similitudine di proprietà e operazioni tra unioni e strutture.


...
#define SIZE 10
#define A_SIZE 4

union Z// unione di tipo union Z


{
char c;
short s;
};

// prototipo della funzione setZ


union Z setZ(union Z z);

int main(void)
{
union U // unione di tipo union U
{
char c;
int i;
} u = {'a'}, z;
z = u; // assegnamento di un'unione a un'altra unione

// dichiaro un'unione di un tipo senza nome a cui però attribuisco


// un nome di tipo, ossia FloatingPoint, tramite typedef
typedef union
{
float f;
double d;
long double ld;
} FloatingPoint;
FloatingPoint fp = {112.4f}; // fp è di tipo FloatingPoint

// letterale union
union U u_i = (union U){.i = 1000};

// annidamento di unioni
union parent // unione di tipo union parent
{
int a;

union child // unione annidata di tipo union child


{
int b;
double d;
} c;
} p = {.c.d = 100.333};

// contiene come membri degli array


union data // unione di tipo union data
{
int a[SIZE];
float f[SIZE];
} d;
d.a[0] = 200;
d.a[1] = 300;

// array di union data


union data ds[A_SIZE];
ds[1].f[0] = 11.11f;
// puntatore a un'unione di tipo union data
union data *another_data = &ds[1];
float f_data = another_data->f[0];

// invoco la funzione setZ passando un'unione di tipo union Z


// la funzione ritorna un'unione sempre di tipo union Z dove il membro
// c avrà il valore di 'Z'
union Z a_z = setZ((union Z){'Y'});

// unioni anonime
union A // un'unione di tipo union A
{
union // unione anonima
{
short s;
int i;
};

union // unione anonima


{
float f;
double d;
};
};
union A a;
a.d = 2222.2222;

// unione con dei campi di bit


union B
{
unsigned int a : 10;
unsigned int b : 5;
unsigned int c : 1;

};
union B b;
b.a = 1023; // ok il valore "entra" nei 10 i bit di a
...

// definizione della funzione setZ che ritorna un'unione di tipo union Z e che
// accetta come argomento un'unione sempre di tipo union Z
union Z setZ(union Z z)
{
return (union Z){.c = z.c + 1};
}

NOTA
Gli esempi di unioni presentati hanno solo uno scopo didattico. Molte di esse sono infatti
servite solo per mostrarne la sintassi di utilizzo.

Casi d’uso effettivi


Abbiamo studiato la sintassi e le proprietà delle unioni e visto degli esempi di utilizzo.
A questo punto, può sorgere spontanea la domanda: nella programmazione “reale”, che
utilità hanno? Si può rispondere citando due comuni casi d’uso: il primo è legato alla
possibilità di migliorare l’efficienza di occupazione di memoria di una struttura
ottimizzandone e riducendone lo spazio di utilizzo (Listato 8.9); il secondo è legato alla
possibilità di creare un tipo che può contenere tipi di dato differenti (Listato 8.10).
Ottimizzazione della memoria
Listato 8.9 MemoryOptimization.c (MemoryOptimization).
/* MemoryOptimization.c :: Ottimizzazione della memoria grazie alle union :: */
#include <stdio.h>
#include <stdlib.h>
#include <string.h>

#define SIZE 30
#define A_SIZE 100
#define T_SIZE 20
#define ITEMS_SIZE 50
#define NR_STORE 3
#define NR_ITEMS 10
#define BOOK_STORE 0
#define CLOTHING_STORE 1
#define COMPUTER_STORE 2

int main(void)
{
struct ShoppingCenter // una struttura di tipo struct ShoppingCenter
{
// dati comuni al centro commerciale
char name[SIZE];
char address[A_SIZE];
char shopping_center_tel[T_SIZE];
int number_of_floors;
int number_of_store;

struct // struttura per un negozio


{
// dati comuni per ogni negozio
char name[SIZE];
int floor;
int store_nr;

struct // struttura per un item


{
// dati comuni per ogni item
float price;
int general_item_code;

union // unione anonima per i negozi


{
struct // struttura per un libro
{
char title[ITEMS_SIZE];
char author[ITEMS_SIZE];
int number_of_pages;
} book;

struct // struttura per un capo di abbigliamento


{
char type[ITEMS_SIZE];
int size;
char color[ITEMS_SIZE];
} clothing;

struct // struttura per un computer


{
char model_name[ITEMS_SIZE];
int ram; // in GByte
int cpu_speed; // in GHertz
int hd_capacity; // in GByte
_Bool printer;
_Bool scanner;
_Bool integrated_video_card;
} computer;
};
} item[NR_ITEMS]; // assumiamo per semplicità che vi siano solo 10 item
// per negozio... dimensione totale 1160 byte
} store[NR_STORE]; // assumiamo per semplicità che vi siano solo tre negozi...
};

// sc variabile di struttura di tipo struct ShoppingCenter


struct ShoppingCenter sc;

// valorizzazione membri centro commerciale


strcpy(sc.name, "Centro Commerciale 4TER");
strcpy(sc.address, "Via Po, 44 Torino");
strcpy(sc.shopping_center_tel, "011/88556699");
sc.number_of_floors = 10;
sc.number_of_store = NR_STORE;

// valorizzazione membri di un negozio


strcpy(sc.store[BOOK_STORE].name, "Apogeo Library");
sc.store[BOOK_STORE].floor = 1;
sc.store[BOOK_STORE].store_nr = 100;

// valorizzazione di un item di un negozio tra: book, clothing o computer


sc.store[BOOK_STORE].item[0].general_item_code = 19982;
sc.store[BOOK_STORE].item[0].price = 45.00f; // in EURO

strcpy(sc.store[BOOK_STORE].item[0].book.title, "Java 8 Guida Completa");


strcpy(sc.store[BOOK_STORE].item[0].book.author, "Pellegrino Principe");
sc.store[BOOK_STORE].item[0].book.number_of_pages = 682;

printf("%s\n%s\n%s\n", sc.name, sc.address, sc.shopping_center_tel);


printf("-----------------------\n");
printf(
"Libro: %s di %s\nPrezzo: %.2f Euro acquistabile presso la libreria %s [piano %d]\n",
sc.store[BOOK_STORE].item[0].book.title,
sc.store[BOOK_STORE].item[0].book.author,
sc.store[BOOK_STORE].item[0].price, sc.store[BOOK_STORE].name,
sc.store[BOOK_STORE].floor);

return (EXIT_SUCCESS);
}

Output 8.9 Dal Listato 8.9 MemoryOptimization.c.


Centro Commerciale 4TER
Via Po, 44 Torino
011/88556699
-----------------------
Libro: Java 8 Guida Completa di Pellegrino Principe
Prezzo: 45.00 Euro acquistabile presso la libreria Apogeo Library [piano 1]

Il Listato 8.9 crea una struttura atta a modellare un generico centro commerciale nel quale
sono presenti una moltitudine di negozi tra i quali, nel nostro caso e per semplicità: una
libreria, un negozio di abbigliamento e un negozio di computer.
Questa struttura è identificata dal tag ShoppingCenter ed è costruita nel seguente modo.

1. Ha dei membri che sono delle variabili scalari che rappresentano dei dati comuni a un
centro commerciale (name, address, number_of_floorse così via).
2. Ha un membro che è una struttura annidata che rappresenta un negozio generico. Per
essa viene dichiarata la variabile di struttura store che è un array contenente tre
elementi di questo tipo di struttura (in pratica ci sono tre negozi).
La struttura di un negozio è invece costruita in questo modo.
1. Ha dei membri che sono delle variabili scalari che rappresentano dei dati comuni a un
negozio (name, floor e store_nr).
2. Ha un membro che è una struttura annidata che rappresenta un generico item (o
articolo). Per essa viene dichiarata la variabile di struttura item che è un array
contenente dieci elementi di questo tipo di struttura (in pratica ci sono dieci articoli per
negozio).
La struttura di un item è invece costruita in questo modo.

1. Ha dei membri che sono delle variabili scalari che rappresentano dei dati comuni a un
item (price e general_item_code).
2. Ha un membro che è un’unione anonima annidata atta a descrivere uno specifico item
ossia: un libro (variabile di struttura book) oppure uno specifico capo di abbigliamento
(variabile di struttura clothing) oppure uno specifico computer (variabile di struttura
computer). Ognuna di queste strutture ha dei membri a essa specifici e dunque non
comuni (per esempio, per la struttura di cui la variabile book avremo per un item libro il
dato title, che è assente perché non necessario per la struttura di cui la variabile
clothing, dove è invece presente per un item abbigliamento il dato color).

Di tutte le strutture dichiarate la struttura che descrive un item è quella che mostra come
risparmiare spazio di memorizzazione grazie all’ausilio di un’unione.
Infatti, dato un item, le uniche informazioni comuni a tutti sono quelle che riguardano il
prezzo (variabile price) e il codice (variabile general_item_code). Le altre informazioni sono
invece specifiche per ogni item e pertanto, piuttosto che replicarle tutte nell’ambito della
struttura di un item, qualsiasi esso sia, è parso più opportuno dichiarale nell’ambito di una
specifica struttura, membro di un’unione, che verrà scelta di volta in volta.
Questa modellazione di una struttura di un item che fa uso di un’unione che descrive tre
diverse tipologie di articoli ci ha permesso di risparmiare una notevole quantità di memoria,
che avremmo invece sprecato se avessimo dichiarato la struttura di un item ponendo in essa
tutte le informazioni di tutti gli item (essa avrebbe pesato in memoria 2840 byte invece dei
1160 byte impiegati grazie alla nostra unione).

Snippet 8.29 Dimensione della struttura degli item senza le union.


...
// tutti i membri, di ogni item, sono posti, senza differenziazione nell'ambito
// della stessa struttura
struct // struttura per gli item
{
float price;
int general_item_code;

char title[ITEMS_SIZE];
char author[ITEMS_SIZE];
int number_of_pages;

char type[ITEMS_SIZE];
int size;
char color[ITEMS_SIZE];

char model_name[ITEMS_SIZE];
int ram; // in GByte
int cpu_speed; // in GHertz
int hd_capacity; // in GByte
_Bool printer;
_Bool scanner;
_Bool integrated_video_card;

} item[NR_ITEMS];

size_t size_of_items = sizeof item; // 2840 byte

Creazione di un tipo che può contenere tipi differenti


Listato 8.10 Variant.c (Variant).
/* Variant.c :: Creazione di un tipo che può contenere tipi diversi :: */
#include <stdio.h>
#include <stdlib.h>

#define CHAR 0
#define INT 1
#define FLOAT 2
#define DOUBLE 3
#define SIZE 4

// creiamo un alias per una struttura capace di contenere quattro tipi di dato diversi
typedef struct
{
int type;

union // unione anonima


{
char c;
int i;
float f;
double d;
};
} variant;

// prototipo di printValue
void printValue(variant v);

int main(void)
{
variant v; // dichiaro v di tipo variant

// ora è char
v.type = CHAR;
v.c = 'Z';
printValue(v);

// ora è int
v.type = INT;
v.i = 100;
printValue(v);

// ora è double
v.type = DOUBLE;
v.d = 33.56;
printValue(v);

// creiamo un array dove ogni elemento è un "tipo" diverso


variant array_of_v[SIZE] =
{
{CHAR, .c = 'A'},
{INT, .i = 100},
{FLOAT, .f = 22.22f},
{DOUBLE, .d = 123.44}
};
printf("Stampiamo i valori gli elementi dell'array array_of_v: ");

for (int i = 0; i < SIZE; i++)


{
if (array_of_v[i].type == CHAR)
printf("%c ", array_of_v[i].c);
else if (array_of_v[i].type == INT)
printf("%d ", array_of_v[i].i);
else if (array_of_v[i].type == FLOAT)
printf("%.2f ", array_of_v[i].f);
else if (array_of_v[i].type == DOUBLE)
printf("%.2f ", array_of_v[i].d);
}
printf("\n");

return (EXIT_SUCCESS);
}

// definizione di printValue
void printValue(variant v)
{
switch (v.type)
{
case CHAR:
printf("%c\n", v.c);
break;
case INT:
printf("%d\n", v.i);
break;
case FLOAT:
printf("%.2f\n", v.f);
break;
case DOUBLE:
printf("%.2f\n", v.d);
}
}

Output 8.10 Dal Listato 8.10 Variant.c.


Z
100
33.56
Stampiamo i valori gli elementi dell'array array_of_v: A 100 22.22 123.44

Il Listato 8.10 definisce una struttura con un membro type, che descrive il corrente tipo di
dato gestito tra CHAR, INT, FLOAT e DOUBLE, e un membro union anonima capace di contenere un
char o un int o un float o un double.
Di questa struttura viene fatto un alias di tipo mediante typedef e le viene attribuito il
nome variant che può essere utilizzato in modo semplice per creare variabili di tale tipo.
La funzione main evidenzia come usare il tipo variant; in pratica la variabile v di tipo
variant viene di volta in volta valorizzata con un valore di tipo differente (prima il tipo char,
poi il tipo int e infine il tipo double) e con un valore che rappresenta il tipo immesso.
È inoltre interessante notare la creazione di un array, array_of_v, capace di contenere
elementi di tipo differente (di tipo variant), cosa non possibile quando utilizziamo gli array
con i tipi fondamentali (int, float e così via) poiché tali array possono contenere solo
elementi dello stesso tipo.
NOTA
Avremmo potuto creare un’unione variant senza che questa fosse stata un membro di una
struttura. Tuttavia, così facendo, non avremmo potuto sapere, in modo agevole e in un
determinato momento, qual era il tipo correntemente utilizzato.
Enumerazioni
Un’enumerazione (enumeration) è un tipo di dato intero (enumerated type) rappresentato
da una serie di nomi o identificatori simbolici costanti di tipo int (enumeration constants)
che indicano quell’insieme di valori che dovrebbe “possibilmente” contenere (Sintassi
8.10).

Sintassi 8.10 Dichiarazione di un’enumerazione.


enum [tag]
{
identifier_1 [= value];
identifier_2 [= value];
...
identifier_N [= value];
} [identifier_1, identifier_2, ..., identifier_N];

Un’enumerazione si dichiara indicando la keyword enum; un tag o etichetta opzionale utile


per dare un identificativo alla relativa enumerazione; una coppia di parentesi graffe { } al
cui interno elencare (enumerare) degli identificatori senza tipo che ne rappresentano le
costanti di enumerazione che possono contenere, opzionalmente, un valore di
inizializzazione derivato da un’espressione costante intera; uno o più identificatori
opzionalmente indicati dopo la parentesi graffa di chiusura } dell’enumerazione che
rappresentano i nomi degli oggetti del tipo di enumerazione definita.
Un’enumerazione si dichiara allo stesso modo di una struttura o unione (se ne può
definire anche un alias di tipo tramite typedef) ma vi si differenzia perché:

è di un tipo intero che deve essere compatibile, a scelta della corrente


implementazione, tra char, signed int o unsigned int;
non ha una serie di membri allocati sequenzialmente in memoria (come le strutture)
oppure sovrapposti in memoria (come le unioni);
gli identificatori che rappresentano le costanti di enumerazione fanno parte dello stesso
spazio dei nomi degli identificatori ordinari, e pertanto possono generare un conflitto
di nomi se sono uguali a questi ultimi (ciò non si verifica per un eventuale tag del tipo
enumerato che condivide, invece, lo stesso spazio dei nomi dei tag delle strutture e
delle unioni; ciò significa che un tipo enumerato può avere un tag denominato allo
stesso modo di un identificatore ordinario senza generare alcun conflitto di nomi).
Al di là della doverosa e formale spiegazione di cosa sono le enumerazioni, vediamo di
spiegare il perché della loro esistenza, ossia quale è la loro utilità pratica.
Partiamo dal delineare il seguente problema, spesso ricorrente nella scrittura di
programmi: al posto di meri valori numerici vogliamo definire per essi un insieme di nomi
“significativi”, cioè nomi che sono in grado di esprimere in modo chiaro e leggibile, nel
punto del codice dove sono impiegati, cosa rappresentano.
Se volessimo esprimere dei valori atti a rappresentare i punti cardinali vorremmo poterlo
fare dichiarando un oggetto di un qualche tipo che possa accettare direttamente valori quali
NORD, NORD-EST, EST, SUD-EST, SUD, SUD-OVEST, OVEST e NORD-OVEST piuttosto che 0 per NORD, 1 per

NORD-EST e così via per gli altri.


Per farlo potremmo utilizzare le macro introducibili con la direttiva #define, ma questa
tecnica presenta degli inconvenienti: non evidenzia in modo chiaro se queste macro sono
correlate in un qualche tipo; i loro nomi sono sostituiti dai corrispettivi valori dal
preprocessore e non sono disponibili a un debugger.
Ecco, quindi, che l’impiego di un tipo enumerato mostra tutta la sua utilità: esso delinea
un oggetto deputato in modo esplicito a contenere un insieme di valori ricavabili tramite dei
nomi o simboli significativi e correlati e che sono utilizzabili anche da un debugger.
NOTA
Un compilatore non è tenuto a avvisare se si assegna a un tipo enumerato un valore che
non è tra quelli espressamente ricavabili dalle costanti di enumerazione per questo
dichiarate.

Listato 8.11 Enumerations.c (Enumerations).


/* Enumerations.c :: Enumerazioni :: */
#include <stdio.h>
#include <stdlib.h>

int main(void)
{
enum cardinal_points // enumerazione di tipo enum cardinal_points
{
NORTH,
NORTH_EAST,
EAST,
SOUTH_EAST,
SOUTH,
SOUTH_WEST,
WEST,
NORTH_WEST
};

// cp è una variabile di tipo enum cardinal_points che contiene


// come valore iniziale NORTH
enum cardinal_points cp = NORTH;

// assegnamento lecito: cp è, di fatto, di un tipo intero scelto


// dal compilatore in uso
// value conterrà come valore il valore intero di NORTH che in assenza
// di una specifica attribuzione è 0
int value = cp;

printf("Valore di value: %d\n", value);

// assegnamento lecito: cp è di un tipo intero scelto dal compilatore in uso


// tuttavia il valore 10 non è tra quelli ricavabili dalle corrispettive costanti
// di enumerazione e un compilatore potrebbe segnalarlo...
cp = 10;

printf("sizeof di cp: %zu\n", sizeof cp);


printf("sizeof di EAST: %zu\n", sizeof EAST);

return (EXIT_SUCCESS);
}

Output 8.11 Dal Listato 8.11 Enumerations.c.


Valore di value: 0
sizeof di cp: 4
sizeof di EAST: 4

Il Listato 8.11 dichiara l’enumerazione cardinal_points con delle costanti di enumerazione


atte a descrivere i punti cardinali, e poi definisce la relativa variabile cp inizializzata con il
valore 0 ritornato dal nome simbolico NORTH.
La costante NORTH ha come valore di default 0 perché il compilatore adotta le seguenti
regole per l’attribuzione di valori agli identificatori propri delle costanti di enumerazione
(Snippet 8.30):
1. se una costante di enumerazione ha il simbolo di =, allora gli assegna il corrispettivo
valore che deve derivare da un’espressione costante intera; altrimenti,
2. se tale costante è la prima tra le costanti di enumerazione elencate, il valore assegnato è
di default 0; altrimenti,
3. se tale costante non è la prima tra le costanti di enumerazione elencate, il valore
assegnato è di default 1, se è la seconda 2, se è la terza 3 e così via per le altre. Se però
la costante di enumerazione che precede tale costante ha un valore esplicito, allora il
valore di tale costante sarà pari a quel valore esplicito + 1.

Continuando la disamina del programma notiamo come il valore della variabile cp di tipo
enum cardinal_points sia assegnato senza problemi alla variabile value di tipo int; ciò è
legittimo perché, di fatto, cp è di un tipo intero scelto dal compilatore corrente.
Quanto detto è suffragato dalla possibilità di assegnare a cp il valore intero 10, anche se
non è un valore contemplato dalle sue costanti di enumerazione, e dal risultato
dell’operatore sizeof che ritorna 4 come sua dimensione di tipo in memoria (corrisponde al
tipo int sul corrente sistema).
Infine, l’operatore sizeof impiegato sull’identificatore EAST ritorna altresì 4 a
dimostrazione che le costanti di enumerazione sono di tipo int.

Snippet 8.30 Attribuzione dei valori alle costanti di enumerazione.


// dichiaro un'enumerazione di un tipo senza nome a cui però attribuisco
// un nome di tipo, ossia cardinal_points, tramite typedef
typedef enum
{
NORTH = 100,
EAST, // 101, valore di NORTH + 1
SOUTH = 300,
WEST // 301, valore di SOUTH + 1
} cardinal_points;
// cp è di tipo cardinal_points e conterrà come valore 301
cardinal_points cp = WEST;

CURIOSITÀ
In diversi sorgenti di C è possibile trovare delle dichiarazioni di tipi enumerati dove l’ultima
costante di enumerazione è terminata da un carattere virgola , (trailing comma). A partire
da C99 tale pratica è divenuta legale ed è stata standardizzata. La ragione di questa
curiosa caratteristica risiede in due motivazioni principali; la prima è legata a ragioni di
consistenza perché era già previsto il trailing comma nelle liste di inizializzatori; la seconda
è legata a una ragione di semplicità di inserimento di una nuova costante di enumerazione
che può essere direttamente inserita alla fine della lista di enumeratori senza influire sulle
eventuali “righe” di dichiarazione degli altri enumeratori (Snippet 8.31).

Snippet 8.31 Trailing comma dopo l’ultima costante di enumerazione.


enum colors
{
RED,
GREEN,
BLUE, // ok, trailing comma permesso; l'inserimento, per esempio, di WHITE
// può avvenire direttamente su questa riga senza "scomodare" le righe
// di dichiarazione precedenti!
};
Capitolo 9
Dichiarazioni

Nel linguaggio C la fase di dichiarazione di un qualsivoglia oggetto o funzione è un


aspetto di notevole importanza; infatti, tramite essa possiamo non solo esplicitare, per
esempio, un nome e un tipo per una variabile, ma anche un insieme di altre proprietà come
la sua durata in memoria, se è utilizzabile da altre funzioni in altri file e così via.
Appare dunque opportuno fornire un’analisi dettagliata di come impostare queste
proprietà avanzate così come illustrare in modo rigoroso altri concetti che abbiamo solo
accennato nei precedenti capitoli, come quello riferito allo scope di un oggetto.
Allo stesso tempo, per completezza di trattazione e uniformità di collocazione con
l’argomento corrente, ritorneremo brevemente su quali sono i qualificatori di tipo, gli
specificatori di tipo e gli specificatori di funzione; laddove necessario, tratteremo anche di
qualificatori o specificatori non ancora analizzati.
Concetti propedeutici
Prima di addentrarci nello studio di tutto quello che riguarda la fase di dichiarazione di un
elemento di C trattiamo in dettaglio dell’importante differenza tra una variabile interna
rispetto a una variabile esterna, di cosa sono i blocchi di codice (come definirli o
individuarli) e dello scope o campo di visibilità di un identificatore.

Variabili interne
Una variabile è definita interna (internal variable) o anche locale (come è a volte
informalmente chiamata) quando è dichiarata nell’ambito di un blocco di codice come può
essere, per esempio, uno che delimita il corpo di una funzione (in questo caso di dice che la
variabile è locale a quella funzione), oppure uno più generico creato mediante una coppia di
parentesi graffe { } (in quest’altro caso si dice che la variabile è locale a quel blocco).

Snippet 9.1 Variabili locali.


void foo(void)
{
// a è una variabile locale alla funzione foo
int a = 100;

// j è una variabile locale al blocco di codice sotto indicato


{
int j = a;
}
}

Le variabili locali godono, in modo predefinito, delle seguenti proprietà.


Durata in memoria automatica (automatic storage duration): il compilatore alloca in
automatico spazio di memoria per una variabile locale quando l’esecuzione del codice
“entra” nel relativo blocco e dealloca, sempre in automatico, tale spazio quando
l’esecuzione del codice “esce” dal blocco. Il valore di una variabile locale automatica è
perso quando l’esecuzione del codice esce dal blocco dove è stata dichiarata.
Visibilità nel blocco (block scope): una variabile locale “è vista”, può cioè essere
referenziata, solo nel relativo blocco di codice dove è stata dichiarata (a partire
dall’esatto punto dove è stata dichiarata e fino alla chiusura del suo blocco
contenitore). Ciò implica che ogni blocco di codice rappresenta uno specifico ambito
di visibilità o scope, e più blocchi di codice possono avere variabili con identificatori
uguali.

NOTA
Anche un parametro di una funzione è considerato come un variabile locale e ha le
medesime proprietà appena illustrate.
Tornando al nostro Snippet 9.1, per la variabile a dichiarata nella funzione foo viene
allocata la memoria, quando il programma entra in tale funzione, e viene deallocata la
memoria quando il programma esce.
Per la variabile j, invece, la memoria viene allocata quando il codice entra nel blocco
contenitore e ne viene deallocata quando il codice esce.
Per la visibilità avremo che: la variabile a sarà referenziabile a partire dal suo punto di
dichiarazione e per tutta la funzione foo (anche nel blocco di codice innestato che contiene
la variabile j); la variabile j sarà referenziabile a partire dal suo punto di dichiarazione e per
tutto il suo blocco contenitore (ma non al di fuori di esso e dunque in altri punti della
funzione foo).
NOTA
A partire da C99 è possibile dichiarare una variabile locale in qualsiasi punto di un blocco di
codice e prima del suo utilizzo (mixed declarations and code). Prima di C99, invece, una
variabile locale poteva essere dichiarata solo all’inizio del relativo blocco di codice.

Snippet 9.2 Dichiarazione di variabili locali non all’inizio di un blocco di codice.


void bar(void)
{
// dichiarazione all'inizio del blocco; da C99 non è più necessario
int k = 10;

printf("bar\n"); // bar

// ok lecito a partire da C99; tale dichiarazione si trova dopo


// il codice della funzione printf
int a = 100;
}

Per le variabili locali vi è comunque la possibilità di preservare il valore lì contenuto


anche quando si esce dal blocco di codice che le contiene e poi vi si rientra nuovamente; in
pratica, utilizzando la keyword static durante la fase di dichiarazione delle variabili locali si
istruisce il compilatore ad assegnare loro degli indirizzi di memoria permanenti che saranno
validi per tutta l’esecuzione del programma (si usa dire che queste variabili locali hanno una
durata statica in memoria, static storage duration).

Snippet 9.3 Variabili locali che preservano il loro valore.


void incrementMe(void)
{
// a ogni invocazione di incrementMe la variabile a mantiene il suo valore,
// che viene anzi incrementato di un'unità
// questa variabile locale statica ha, come una variabile locale automatica,
// uno scope a livello di blocco
static int a = 1;

// prima di uscire dalla funzione, alla prima invocazione, a conterrà 2,


// poi a ogni invocazione successiva conterrà 3, 4, 5 e così via
// il valore di a non sarà mai perso; a avrà una durata statica in memoria
a++;
}
Variabili esterne
Una variabile è definita esterna (external variable) o anche globale (come a volte
informalmente chiamata) quando è dichiarata al di fuori di ogni corpo di funzione.

Snippet 9.4 Variabili globali.


...
// variabili esterne; dichiarate al di fuori di ogni funzione
int number;
int data;

void foo(void); // prototipo di foo

int main(void)
{
// ok number e data sono visibili perché globali
number = 10;
data = 15;
foo();

...
}

void foo(void) // definizione di foo


{
// ok number e data sono visibili perché globali
// a conterrà il valore 25...
int a = number + data;
}

Le variabili globali godono, in modo predefinito, delle seguenti proprietà.


Durata statica in memoria (static storage duration): permangono in memoria per tutta
la durata del programma (come le variabili locali dichiarate static). Ciò implica che il
valore memorizzato in queste variabili non viene mai perso.
Visibilità nel file (file scope): una variabile globale “è vista”, e può essere referenziata
in tutto il file dove è stata dichiarata (a partire cioè dall’esatto punto dove è stata
dichiarata e fino alla fine del file). Ciò implica che ogni funzione presente nel file può
manipolarla direttamente (tale variabile rappresenta perciò una sorta di oggetto
“condiviso”).
Così, per lo Snippet 9.4, le variabili number e data sono accessibili, perché visibili in
quanto dichiarate globalmente, sia alla funzione main sia alla funzione foo.
ATTENZIONE
Quantunque allettanti, è sconsigliato abusare delle di variabili globali. Tra le ragioni:
difficoltà di debugging: poiché ogni funzione può modificarne il valore, può essere difficile
capire quali tra di esse abbia potuto provocare un comportamento errato o inatteso del
programma a causa, per l’appunto, di una modifica inopportuna; difficoltà di riutilizzo delle
funzioni, poiché ogni funzione per eseguirsi correttamente deve dipendere da eventuali
variabili globali utilizzate, nel caso volessimo riutilizzare queste funzioni per altri programmi,
dovremmo ricordarci di includere anche tali variabili globali, e ciò è chiaramente indice di
una cattiva progettazione del codice, oltre che fonte di possibili “dimenticanze”.
NOTA
In C una funzione è sempre un oggetto esterno. Non è infatti possibile definire una funzione
all’interno di un’altra funzione e renderla a essa locale.

Blocchi
Un blocco è un’unità sintattica, delimitata da una coppia di parentesi graffe { }, al cui
interno possono essere raggruppate delle dichiarazioni e delle istruzioni proprie del
linguaggio C (per esempio, il corpo di definizione di una funzione è un blocco).
TERMINOLOGIA
Per lo standard di C un blocco espresso nella forma { [block-item-list] }, dove block-item-

list è una lista di dichiarazioni e/o istruzioni, è denominato istruzione composta (compound
statement).

Spazio dei nomi


Gli identificatori possono appartenere a una delle seguenti categorie, ciascuna delle quali
rappresenta un determinato spazio dei nomi (name space), ossia una sorta di “container”
logico:
nomi delle etichette (label names);
tag delle strutture, unioni o enumerazioni;
membri delle strutture o unioni;
identificatori ordinari (ordinary identifiers), ovvero tutti gli altri identificatori che non
ricadono nei primi tre punti e le costanti di enumerazione.
Ciò implica che se un identificatore appartenente a uno spazio dei nomi è denominato
allo stesso modo di un identificatore appartenente a un altro spazio dei nomi, non vi sarà
alcun conflitto, ossia C lo consentirà, e un determinato contesto sintattico di dichiarazione
permetterà di far comprendere al compilatore (disambiguare) il relativo identificatore a
quale corretto spazio dei nomi apparterrà.

Snippet 9.5 Spazio dei nomi.


...
int main(void)
{
data: // data appartiene allo spazio dei nomi delle label
printf("OK\n");

// data appartiene allo spazio dei nomi dei tag delle strutture,
// unioni o enumerazioni
struct data
{
int member;
};

union U
{
// data appartiene allo spazio dei nomi dei membri delle strutture o unioni
int data;
};

// data appartiene allo spazio dei nomi degli identificatori ordinari


int data = 100;
...
}

Lo Snippet 9.5 evidenzia come l’identificatore data, quantunque dichiarato nello stesso
scope, ossia nell’ambito della funzione main, non farà generare al compilatore alcun errore di
conflitto di nomi perché, ribadiamo, ogni nome di data sarà appartenente a un determinato
spazio dei nomi correttamente individuato e attribuito dal compilatore.

Scope
Per scope o campo (ambito) di visibilità si intende quella “regione” di codice di un
programma dove un determinato identificatore è visibile e dunque utilizzabile. In C vi sono
quattro differenti ambiti di visibilità.
Block scope (visibilità nel blocco). La regione di codice di questo scope è rappresentata
da un blocco delimitato dalle parentesi graffe { }, e una variabile lì dichiarata è visibile
dal suo punto di dichiarazione (esattamente dopo la scrittura del relativo identificatore)
e fino al termine della definizione del blocco medesimo (la parentesi graffa di
chiusura). Le variabili locali e i parametri formali di una funzione hanno sempre un
block scope. A partire da C99 un block scope è esteso anche alle istruzioni di selezione
(if), alle istruzioni di iterazione (for, while e do/while) e alle inner statement di tali
istruzioni con o senza la consueta coppia di parentesi graffe di delimitazione di un
blocco. Ciò significa, per esempio, che se in un’istruzione for usiamo come prima
espressione una dichiarazione di una variabile, la stessa sarà visibile solo nell’ambito
del costrutto for. Al termine del for, quindi, tale variabile non esisterà più e non sarà
referenziabile in altre regioni di codice poste dopo il for stesso.

Snippet 9.6 Block scope.


{ // un block scope esplicito; a è visibile solo in quest'ambito
int a = 100;
}

// qui a non è visibile; error: 'a' undeclared (first use in this function)
int b = a;

// il for delimita un block scope; i è visibile solo in quest'ambito


for (int i = 0; i < 10; i++)
printf("[%d]\n", i);

int n = 10;
int *data;

// l'array letterale esisterà solo nell'ambito del block scope


// relativo all'inner statement della if
// data conterrà, quindi, un puntatore di un oggetto non più valido
// e il suo eventuale utilizzo causerà un comportamento non definito
if (n == 10)
data = (int[]){1, 2, 3};
else
{ // un block scope esplicito per il ramo else
// number esisterà solo in questo blocco
int number = 1000;
}

for (int j = 0; j < 10; j++)


{ // blocco esplicito
// nessuna ridefinizione di j perché sta nel loop body che è un blocco
// disgiunto dal blocco proprio dell'istruzione for
int j = 1;
printf("%d\n", j);
}

// qui i non è visibile; error: 'i' undeclared (first use in this function)
b = i;

Function scope (visibilità nella funzione). La regione di codice di questo scope è


rappresentata da tutta la funzione. Solo le etichette (label) utilizzabili da un’istruzione
goto hanno questo scope. Ciò implica che, indipendentemente dal punto dove una label

è posta (per esempio anche in un blocco nidificato all’interno di una funzione), essa è
sempre referenziabile perché, per l’appunto, il suo ambito di visibilità si estende in
tutta la funzione.

Snippet 9.7 Function scope.


...
goto execute;

// una serie di block scope nidificati


{
{
{
// anche se la label execute è dichiarata dopo l'istruzione goto
// essa è comunque raggiungibile perché ha uno scope che è
// a livello della funzione; la sua visibilità va dall'inizio
// alla fine della funzione dove è contenuta
execute: printf("Execute!\n"); // Execute!
}
}
}

Function prototype scope (visibilità nel prototipo di funzione). La regione di codice di


questo scope è rappresentata dalla dichiarazione del prototipo di una funzione. Ciò
implica che se utilizziamo dei nomi per gli identificatori dei parametri formali tali
nomi saranno visibili fino al termine della dichiarazione del relativo prototipo di
funzione.

Snippet 9.8 Function prototype scope.


// x, y e data sono visibili solo in quest'ambito, ossia nell'ambito della
// corrente dichiarazione del prototipo di foo; per esempio, x è visibile dal
// punto della sua dichiarazione (dopo di esso) e fino alla parentesi tonda
// che delimita la chiusura del function declarator
void foo(int x, int y, int data[][y]);
File scope (visibilità nel file). La regione di codice di questo scope è rappresentata da
tutto il file. In pratica le variabili esterne (o globali) hanno questo scope e sono
pertanto visibili a partire dal punto dove sono state dichiarate (dopo il loro
identificatore) e fino al termine del file medesimo.

Snippet 9.9 File scope.


...
// data è visibile da dopo il suo punto di collocazione e fino al termine del file
// dove è stata dichiarata
int data;

int main(void)
{
// data è visibile perché variabile esterna con scope a livello di file
int x = data;
...
}

TERMINOLOGIA
Quando asseriamo che una variabile esterna è visibile in tutto il file dove è stata dichiarata,
dobbiamo precisare che per file si deve intendere quella che C considera “unità di
traduzione” (translation unit). Un’unità di traduzione è un’unità di input, formata da
quell’insieme di codice (file sorgente più file header) risultato del lavoro svolto dal
preprocessore, che il compilatore utilizzerà per produrre il relativo file oggetto che potrà poi
essere impiegato per generare un corrispettivo file eseguibile.

Possiamo quindi dire che, dato un identificatore che può denotare un oggetto (per
esempio una variabile), una funzione, un tag, un membro (per una struttura, un’unione o
un’enumerazione), un nome typedef o una label, è certamente possibile per esso denominare
più entità differenti in divere regioni di un programma (per esempio, potremmo avere
l’identificatore foo che denomina una variabile ma anche una funzione).
In questo caso, però, le differenti entità denotate dallo stesso identificatore devono avere
differenti scope oppure devono risiedere in differenti spazi di nomi, pena la generazione da
parte di un compilatore di un errore come error: 'foo' redeclared as different kind of
symbol, che evidenzia un “conflitto” di nomi di dichiarazioni.
Quando un identificatore che denomina diverse entità risiede nello stesso spazio dei
nomi, lo scope potrà “sovrapporsi” e il compilatore adotterà la seguente fondamentale
regola: l’identificatore dell’entità dichiarata in uno scope più interno (inner scope) sarà
prevalente (ossia sarà quello impiegato) rispetto all’identificatore dell’entità dichiarata in
uno scope più esterno (outer scope) che sarà quindi “nascosto”.

Listato 9.1 Scope.c (Scope).


/* Scope.c :: Mostra i vari scope di C :: */
#include <stdio.h>
#include <stdlib.h>

// I dichiarazione
int number = 100; // file scope
int main(void) // block scope di main
{
// la ricerca dell'entità corretta parte dal punto di utilizzo verso "l'alto",
// ossia dal blocco corrente verso gli eventuali blocchi contenitori
// la prima dichiarazione di number trovata è int number = 100
printf("%d\n", number); // qua stampa 100;

// è lecito dichiarare un identificatore che designa un'entità in uno scope


// che ha lo stesso nome di un altro identificatore che designa un'altra
// entità in un altro scope
// II dichiarazione
int number = 10; // block scope

// la ricerca dell'entità corretta parte dal punto di utilizzo verso "l'alto",


// ossia dal blocco corrente verso gli eventuali blocchi contenitori
// la prima dichiarazione di number trovata è int number = 10
printf("%d\n", number); // qua stampa 10

{ // blocco interno al main


// ok number è dichiarato in un proprio scope e "nasconde" number dichiarato
// nello scope esterno...
int number = 1; // block scope

// la ricerca dell'entità corretta parte dal punto di utilizzo verso "l'alto",


// ossia dal blocco corrente verso gli eventuali blocchi contenitori
// la prima dichiarazione di number trovata è int number = 1
printf("%d\n", number); // qua stampa 1
}

return (EXIT_SUCCESS);
}

Output 9.1 Dal Listato 9.1 Scope.c.


100
10
1

Il Listato 9.1 dichiara dei tipi int in diversi punti del codice sorgente del file Scope.c e con
uno stesso identificatore, ossia number; la prima dichiarazione assocerà number alla relativa
variabile che conterrà il valore 100 e avrà un file scope; la seconda dichiarazione assocerà
number alla relativa variabile che conterrà il valore 10 e avrà un block scope; la terza
dichiarazione assocerà number alla relativa variabile che conterrà il valore 1 e avrà un block
scope.
In tutte e tre le dichiarazioni non vi sarà alcun conflitto di nomi perché ciascun
identificatore avrà un proprio scope; il primo number avrà un file scope; il secondo number
avrà un block scope (outer scope rappresentato dal corpo di definizione del main); il terzo
number avrà un block scope (inner scope rappresentato dalla coppia di parentesi graffe { }).
In più, poiché tutti gli identificatori number sono parte dello stesso name space (quello
degli identificatori ordinari) gli scope citati si sovrapporranno; ciò significherà, per esempio,
che l’istruzione printf eseguita nell’ambito del blocco interno al main stamperà il valore 1
che sarà relativo alla variabile number lì dichiarata, la cui dichiarazione “nasconderà” la
dichiarazione della variabile number nel blocco del main, che non sarà quindi utilizzata.
REFERENCING ENVIRONMENT, LEXICAL SCOPE E DYNAMIC SCOPE
C è un linguaggio di programmazione che ha un tipo di scope definito lessicale o statico
(static o lexical scope). Ma cosa significa? Per rispondere alla domanda partiamo dal definire
il concetto di referencing environment. Il referencing environment è un’astrazione software
che contiene l’insieme di tutti i nomi che associano variabili e che sono visibili in un
determinato punto di un programma, ovvero utilizzabili. In pratica, identifica una collezione di
scopes (ambiti di risoluzione o visibilità) che vengono esaminati al fine di trovare il binding
nome-variabile ricercato. Nei linguaggi con scope statico (static-scoped), come C, tale
insieme include tutti i nomi delle variabili definite localmente e i nomi di quelle definite negli
eventuali blocchi contenitori (i suoi ancestor). Nei linguaggi con scope dinamico (dynamic
scoped), come per esempio Common Lisp, tale insieme contiene tutti i nomi delle variabili
definite localmente e i nomi di quelle delle funzioni, in un’eventuale pila di chiamate, ancora in
esecuzione (cioè correntemente attive e non terminate). Come più volte detto, lo scope di una
variabile è dato da quell’insieme di righe di codice in cui essa è visibile, e quindi
referenziabile, oppure, in altri termini, da quella regione di un programma dove un determinato
binding nome-variabile è attivo. I linguaggi di programmazione possono quindi adottare una
regola di scope definita statica o lessicale, che è basata sulla struttura testuale del
programma (è detta anche spaziale, basata cioè sul più vicino blocco nidificato), o dinamica,
che è basata sull’ordine in cui le funzioni sono invocate (è detta anche temporale, basata cioè
sulla più recente associazione). In pratica, con lo scope lessicale, il binding del nome a una
variabile è determinato prima dell’esecuzione del relativo programma (a compile time)
leggendo il codice sorgente e senza considerare il flusso di esecuzione computazionale che
avviene a run-time. Con lo scope dinamico, invece, il binding del nome a una variabile è
determinato solo durante l’esecuzione di un programma. Infatti, quando si raggiunge una
statement che accede al nome di una variabile, l’ultima dichiarazione raggiunta
dall’esecuzione del programma sarà quella che determinerà il binding tra il nome e la variabile
riferita. Dato, per esempio, il blocco di codice di cui lo Snippet 9.10 avremo che:
con lo scope lessicale, la funzione bar, non trovando una dichiarazione locale della
variabile x, utilizzerà per l’assegnamento del relativo valore alla variabile z quella trovata
nel blocco contenitore ascendente più prossimo, ossia quello della funzione foo dove x =

10;

con lo scope dinamico la funzione bar, non trovando una dichiarazione locale della
variabile x, utilizzerà per l’assegnamento del relativo valore alla variabile z quella trovata
nella sua precedente funzione chiamante (dynamic parent), ossia la funzione foo dove x

= 10;

con lo scope lessicale, la funzione baz, non trovando una dichiarazione locale della
variabile k, utilizzerà per l’assegnamento del relativo valore alla variabile j quella trovata
nel blocco contenitore ascendente più prossimo, ossia quello della funzione foo dove k =

20;

con lo scope dinamico la funzione baz, non trovando una dichiarazione locale della
variabile k, utilizzerà per l’assegnamento del relativo valore alla variabile j quella trovata
nella sua precedente funzione chiamante (dynamic parent), ossia la funzione foobar dove
k = 10.
In definitiva, la ricerca di un binding per una variabile partirà sempre dal blocco contenitore
locale, ma se esso non sarà trovato, allora: per lo scope statico, la ricerca proseguirà nei
successivi blocchi contenitori ascendenti (static parent) così come lessicalmente scritti nel
codice; per lo scope dinamico, la ricerca proseguirà con le precedenti funzioni chiamanti
(dynamic parent) in base al loro ordine di invocazione.

Snippet 9.10 Scope lessicale e scope dinamico (pseudo-codice).


Function foo()
{
var x = 10;
var k = 20;

Function bar()
{
var z = x;
var k = 1000;

// scope dinamico = 10; scope statico = 10


print(z);
foobar();
}

Function baz()
{
var j = k;

// scope dinamico = 10; scope statico = 20


print(j);
}

Function foobar()
{
var k = 10;
baz();
}

bar();
}

NOTA
Lo Snippet 9.10 è stato scritto con una sintassi propria di uno pseudo-linguaggio perché
meglio si presta a esemplificare in modo generico il comportamento di un linguaggio con
scope statico rispetto a un linguaggio con scope dinamico (per esempio, in C non avremmo
potuto scrivere lo stesso codice perché una funzione non può essere definita all’interno di
un’altra funzione).

Linkage
Come abbiamo visto, quando si scrive un programma in C possiamo impiegare degli
oggetti interni, per esempio delle variabili definite localmente a una funzione, oppure degli
oggetti esterni, per esempio delle variabili definite al di fuori da ogni funzione ma anche
delle funzioni che sono sempre e solo entità esterne, visto che in C non è possibile definire
una funzione all’interno di un’altra funzione.
Gli oggetti interni e quelli esterni, siano essi semplici variabili o complesse funzioni,
hanno una proprietà importante definita dallo standard come linkage (collegamento), che
permette di stabilire se tali oggetti sono visibili solo all’interno di un’unità di traduzione
(linkage interno) oppure anche in altre unità di traduzione (linkage esterno), ossia
nell’ambito di tutto un programma complesso formato da più file sorgenti e file header.
Questi oggetti possono anche non avere alcun collegamento, come è il caso delle variabili
locali a una funzione, e sono detti senza linkage (in questo caso sono oggetti “privati” alla
funzione dove sono stati dichiarati, e nessun’altra funzione potrà utilizzarli).
TERMINOLOGIA
Quando parliamo di linkage, ma anche di scope, dobbiamo precisare che questi termini si
riferiscono agli identificatori piuttosto che agli oggetti o alle funzioni. Per esempio, è più
corretto dire “l’identificatore foo ha un linkage esterno e un scope a livello di file” piuttosto
che “la variabile foo ha un linkage esterno e uno scope a livello di file”. In ogni caso, per non
“appesantire” di rigidi formalismi terminologici una spiegazione, sia essa scritta od orale, è
molto comune dire che è la tale variabile ad avere un certo scope oppure un certo linkage.

In breve possiamo dire che:


una variabile con un block scope, function scope o function prototype scope non ha
alcun linkage. Essa è privata al blocco, funzione o prototipo dove è stata definita;
una variabile con file scope può avere un linkage esterno, e può essere usata da tutti i
file sorgente che costituiscono un programma complesso, oppure può avere un linkage
interno e può essere usata solo nell’ambito del file sorgente dove è stata dichiarata. Per
rendere una variabile con file scope a linkage interno dobbiamo anteporle lo
specificatore di classe di memorizzazione static. Senza alcun specificatore una
variabile ha per default linkage esterno. Lo specificatore static può essere usato o
meno anche con le funzioni con lo stesso significato.

Proprietà delle variabili: riepilogo


Alla luce di tutto quanto sin qui detto possiamo asserire che una variabile ha le seguenti
proprietà.
Durata in memoria (storage duration): una variabile può avere una durata automatica
in memoria (automatic storage duration), e allora lo spazio di storage per essa è
allocato quando il flusso del codice entra nel suo blocco contenitore ed è deallocato
quando tale flusso esce dallo stesso blocco. Può anche avere una durata statica in
memoria (static storage duration), e allora lo spazio si storage è allocato quando il
programma si avvia ed è deallocato quando lo stesso termina. In quest’ultimo caso
avremo un indirizzo di memoria per una variabile che è permanente e utilizzabile per
tutta la durata di un programma (il suo valore non viene mai perso e può essere sempre
manipolato in modo congruo).
Campo di visibilità (scope): una variabile ha una visibilità, ossia può essere
lecitamente referenziata dal punto che va dopo la fine della scrittura del relativo
dichiaratore (l’identificatore) e fino al termine di un blocco (block scope), di un file
(file scope) oppure di una dichiarazione di prototipo (function prototype scope).
Collegamento (linkage): una variabile può avere un collegamento esterno (external
linkage), e allora è utilizzabile in modo condiviso tra i diversi file sorgenti facenti parte
di un programma. Può anche avere un collegamento interno (internal linkage), e allora
è utilizzabile solo alle funzioni definite dell’unità di traduzione dove è stata dichiarata.
Infine, può anche non avere alcun collegamento (no linkage), e allora è utilizzabile
solo nell’ambito della funzione dove è stata dichiarata.
In base alla regione di codice dove una variabile è stata dichiarata avremo per essa un
determinato “valore” di default per le proprietà di durata in memoria, scope e linkage
(Tabella 9.1 e Snippet 9.11).
Tabella 9.1 Proprietà di default per le variabili interne e esterne.
Tipo variabile Storage duration Scope Linkage
interna automatica blocco nessuno
esterna statica file esterno

La Tabella 9.1 si potrà leggere dicendo che:


una variabile interna avrà, per default, una durata automatica in memoria, un block
scope, nessun linkage;
una variabile esterna avrà, per default, una durata statica in memoria, un file scope,
linkage esterno.

Snippet 9.11 Proprietà di default per le variabili interne ed esterne.


...
// STORAGE DURATION: static
// SCOPE: file
// LINKAGE: external
int data; // variabile esterna

int main(void)
{
// STORAGE DURATION: automatic
// SCOPE: block
// LINKAGE: none
int number; // variabile interna
...
}
Dichiarazioni
Una dichiarazione consente di specificare la “natura” di un identificatore di un oggetto o
funzione permettendo di indicarne, in modo raffinato, le proprietà.
Al contempo fornisce al compilatore tali informazioni in modo che lo stesso possa
interpretarle correttamente.

Sintassi 9.1 Dichiarazione.


declaration-specifiers declarators;

La Sintassi 9.1 mostra la forma generale con la quale si scrivere una dichiarazione:
abbiamo la parte declaration-specifiers (specificatori di dichiarazione), che fornisce una
serie di specificatori con i quali si indicano le proprietà di durata in memoria, linkage o tipo
dell’entità che il relativo identificatore (dichiaratore) denota. Segue la parte declarators
(dichiaratori), che fornisce uno o più dichiaratori separati dal carattere virgola, ciascuno dei
quali può avere, opzionalmente, informazioni addizionali sul tipo oppure un inizializzatore
(in pratica un dichiaratore rappresenta un identificatore per un entità).
Gli specificatori di dichiarazione sono raggruppati nelle seguenti categorie.
Classi di memorizzazione (storage classes): sono esplicitate tramite le keyword auto,
static, extern, register, _Thread_local e typedef. In una dichiarazione può essere presente
al massimo uno di questi specificatori e, nel caso, deve essere indicato come prim, a
eccezione però di _Thread_local, che può apparire anche con static o extern.
Qualificatori di tipo (type qualifiers): sono specificati tramite le keyword const,
restrict, volatile e _Atomic. In una dichiarazione possono esservi 0 o più di questi
qualificatori.
Specificatori di tipo (type specifiers): sono esplicitati tramite le keyword void, char,
short, int, long, float, double, signed, unsigned, _Bool, _Complex, _Atomic, struct, union, enum e
typedef-name. In una dichiarazione deve esserci almeno uno di questi specificatori,
alcuni dei quali possono anche essere lecitamente combinati come indicato dal
seguente elenco (la virgola su una stessa riga di un elenco separa gli specificatori che
hanno la stessa semantica e sono impiegabili in modo interscambiabile perché
rappresentano lo stesso tipo):
— void;

— char;

— signed char;

— unsigned char;
— short, signed short, short int oppure signed short int;

— unsigned short oppure unsigned short int;

— int, signed oppure signed int;

— unsigned oppure unsigned int;

— long, signed long, long int oppure signed long int;

— unsigned long oppure unsigned long int;

— long long, signed long long, long long int oppure signed long long int;

— unsigned long long oppure unsigned long long int;

— float;

— double;

— long double;

— _Bool;

— float _Complex;

— double _Complex;

— long double _Complex;

— _Atomic;

— struct;

— union;

— enum;

— typedef-name.

Specificatori di funzione (function specifiers); sono esplicitati tramite le keyword


inline e _Noreturn. In una dichiarazione possono essere presenti 0 o entrambi questi

specificatori.
Specificatore di allineamento (alignment specifier). È esplicitato tramite la keyword
_Alignas. In una dichiarazione può essere o meno presente.

I dichiaratori, invece, dichiarano un identificatore che può essere scritto in modo


autonomo (un nome di una variabile), può essere seguito dalle parentesi quadre [ ] (un
nome di un array), può essere preceduto dall’asterisco * (un nome di un puntatore) oppure
può essere seguito dalle parentesi tonde ( ) (un nome di una funzione).

DICHIARAZIONE E DEFINIZIONE
Come detto, una dichiarazione indica le proprietà di uno o più identificatori associati a
un’entità (introduce cioè un nome al compilatore e gli dice qualcosa di quel nome). Una
definizione di un identificatore, invece, è una dichiarazione di quell’identificatore che fa
allocare, generalmente, una determinata quantità di memoria per l’entità da esso denotata.
Per esempio, per una varabile, una definizione di un relativo identificatore farà allocare da
parte del compilatore una quantità di memoria capace di contenere valori del tipo indicato; per
una funzione, invece, la definizione del relativo identificatore ne fornirà il corpo (function
body), ossia la descrizione di cosa essa sarà deputata a compiere (la sua implementazione).

TERMINOLOGIA
Così come nel caso del linkage e dello scope, è più corretto terminologicamente parlare di
dichiarazione o definizione di un identificatore piuttosto che di dichiarazione o definizione di
un’oggetto o una funzione. Anche in questo caso, tuttavia, è prassi non appesantire una
trattazione e dire senza problema che si sta dichiarando o definendo la tal variabile o la tal
funzione.

Snippet 9.12 Alcuni esempi di dichiarazioni.


// void -> type specifier
// foo(void) -> declarator
void foo(void)
{
// static -> storage class
// const -> type qualifier
// int -> type specifier
// number -> declarator
// 100 -> initializer
static const int number = 100;

// signed -> type specifier


// short -> type specifier
// int -> type specifier
// data[] -> declarator
// {1,2,3} -> initializers list
signed short int data[] = {1, 2, 3};

// int -> type specifier


// restrict -> type qualifier
// *ptr_to_value -> declarator
int *restrict ptr_to_value;

// float -> type specifier


// a -> declarator
// b -> declarator
// c -> declarator
float a, b, c;
}

NOTA
Solitamente quando si costruisce una dichiarazione è prassi scriverla indicando come prima
cosa uno specificatore di classe, come seconda cosa un qualificatore di tipo, come terza
cosa uno specificatore di tipo e come quarta cosa un dichiaratore.

Dichiarazioni complesse
Un dichiaratore, come visto, può contenere oltre al nome semplice di un identificatore,
anche i simboli [ ], * e ( ). Questi simboli possono essere combinati insieme, per tipo e per
numero di occorrenze, al fine di scrivere dichiaratori di qualsivoglia natura la cui
“decifrazione” dell’entità denotata, talune volte, può essere di notevole difficoltà. La
dichiarazione di una siffatta entità è definita in gergo dichiarazione complessa, e per
comprenderla possiamo adottare le seguenti regole.
1. Iniziare a leggere il dichiaratore a partire dal nome dell’identificatore e avviare la
decifrazione della dichiarazione complessa da quel punto.
2. Prediligere sempre i simboli [ ] e ( ) rispetto al simbolo *. Per esempio, se il simbolo *
precede un identificatore e il simbolo [ ] lo segue, tale identificatore rappresenterà un
array di e non un puntatore a. Allo stesso modo, se il simbolo * precede un
identificatore e il simbolo ( ) lo segue, tale identificatore rappresenterà una funzione di
tipo e non un puntatore a. La priorità dei simboli [ ] e( ) rispetto al simbolo * può
essere cambiata con il consueto uso del simbolo ( ).

Vediamo un esempio di crescente difficoltà di alcune dichiarazioni e di come possono


essere decifrate.
int *data[4]: questa dichiarazione dice che data (punto 1, cominciamo la lettura
dall’identificatore) è un array di 4 elementi di tipo puntatore a int (punto 2, il simbolo [
] è prioritario rispetto al simbolo *).
int (*data)[4]: questa dichiarazione dice che data (punto 1, cominciamo la
letturadall’identificatore) è un puntatore a un array di 4 elementi di tipo int (punto 2, le
parentesi tonde ( ) intorno a *data hanno forzato la priorità del simbolo * rispetto al
simbolo [ ]).
int *func (int, int): questa dichiarazione dice che func (punto 1, cominciamo la lettura
dall’identificatore) è una funzione che ritorna un puntatore a int e che ha due parametri
di tipo int (punto 2, il simbolo ( ) è prioritario rispetto al simbolo *).
int (*ptr_to_func)(int, int): questa dichiarazione dice che ptr_to_func (punto 1,
cominciamo la lettura dall’identificatore) è un puntatore a una funzione che ritorna un
int e ha due parametri di tipo int (punto 2, le parentesi tonde ( ) intorno a *ptr_to_func

hanno forzato la priorità del simbolo * rispetto al simbolo ( ) ).


float *(*data[4])(float): questa dichiarazione dice che data (punto 1, cominciamo la
lettura dall’identificatore) è un array di 4 puntatori a funzioni che ritornano un
puntatore a float e hanno un parametro di tipo float (punto 2, le parentesi tonde ( )

intorno a *data[4] hanno forzato la priorità dei simboli * e [ ] rispetto al simbolo ( )

ma, all’interno di ( ), il simbolo [ ] ha priorità rispetto al simbolo *).

NOTA
L’interpretazione delle dichiarazioni complesse richiede tempo e pratica. Un buon punto di
partenza per fare esperienza può essere quello di collegarsi al sito http://cdecl.org/ e
inserire nell’apposito campo di testo la dichiarazione che si vuole decifrare per ottenere una
risposta sul suo significato (Figura 9.1).
Figura 9.1 Il risultato di una dichiarazione complessa sul sito cdecl.org.
Specificatori della classe di memorizzazione
Gli specificatori della classe di memorizzazione consentono di indicare per un’entità (per
esempio una variabile o una funzione) la sua durata in memoria ma anche il tipo di linkage.

auto
Lo specificatore di classe auto attribuisce agli oggetti dichiarati una classe di
memorizzazione automatica e il suo impiego è valido solo per le dichiarazioni degli stessi
effettuate all’interno di un blocco (in pratica all’interno del corpo delle funzioni).
Questo specificatore è usato di default per le variabili interne (non è obbligatorio
esplicitarlo) e fa sì che esse nascano (quando il flusso di esecuzione del codice entra nel
relativo blocco) e muoiano (quando il flusso di esecuzione del codice esce dal relativo
blocco), per l’appunto, in automatico.
NOTA
Nel caso di un array di lunghezza variabile c’è un’eccezione alla regola sopra esposta:
esso, infatti, nasce dal punto della sua dichiarazione piuttosto che dall’entrata del flusso di
esecuzione del codice nel blocco dove è stato dichiarato.

In più, una dichiarazione auto si comporta anche come definizione e provoca


l’allocazione di memoria per la relativa variabile.
Una variabile con lo specificatore di classe auto ha dunque: una durata automatica in
memoria; un block scope; nessun linkage.

Snippet 9.13 Lo specificatore di classe auto.


...
// error: file-scope declaration of 'a' specifies 'auto'
auto int a = 100; // auto non usabile per variabili esterne!!!

int main(void)
{
// auto scritto in modo esplicito; non necessario perché le variabili interne
// hanno sempre, di default, lo specificatore di classe auto
auto int b = 200;
...
}

static
Lo specificatore di classe static attribuisce agli oggetti dichiarati una classe di
memorizzazione statica e il suo impiego è valido sia per dichiarazioni degli stessi effettuate
all’esterno di un blocco sia per quelle effettuate all’interno di un blocco.
La sua semantica cambia a seconda di dove esso è usato; per un oggetto esterno, indica
che lo stesso può essere usato solo dalle funzioni definite nello stesso file dove tale oggetto
è stato definito; per un oggetto interno, indica che lo stesso ha una durata statica in
memoria, ossia permanente per tutta la durata del programma.
In più, una dichiarazione static per una variabile interna si comporta anche come
definizione e provoca per essa lo stanziamento di memoria (la sua allocazione).
Una variabile con lo specificatore di classe static dichiarata all’interno di un blocco ha
dunque: una durata in memoria statica; un block scope; nessun linkage.
Una variabile con lo specificatore di classe static dichiarata all’esterno di un blocco ha
dunque: una durata statica in memoria; un file scope; un linkage interno.
NOTA
Lo specificatore static è utilizzabile anche con una dichiarazione di funzione al fine di
renderla usabile solo nel corrente file.

Snippet 9.14 Lo specificatore di classe static.


...
// prototipo di foo
void foo(void);

// number può essere usata solo da main e da altre funzioni poste


// nello stesso file dove number stesso è stato definito
static int number;

int main(void)
{
foo(); // I invocazione
foo(); // II invocazione
...
}

// definizione di foo
void foo(void)
{
// data ha una durata in memoria permanente e conserva il suo valore
// tra la I e la II invocazione della sua funzione avvenuta da main
static int data = 2000;
int a = number; // ok number è visibile...
}

NOTA
Dato che una variabile locale e statica permane in memoria per tutta la durata di un
programma, è lecito assegnare il suo indirizzo di memoria o puntatore come valore di ritorno
dalla funzione dove è stata dichiarata. Lo stesso non è però consigliabile per una variabile
locale automatica perché, ribadiamo, al termine dell’esecuzione della funzione dove è stata
dichiarata cessa di esistere (il suo indirizzo di memoria non è più valido e potrebbe essere
usato per altri oggetti).

extern
Lo specificatore di classe extern attribuisce agli oggetti dichiarati una classe di
memorizzazione esterna e il suo impiego è valido sia per dichiarazioni degli stessi effettuate
all’esterno di un blocco sia per quelle effettuate all’interno di un blocco.
La sua semantica è la seguente: dichiara senza definire, e dunque senza allocare memoria,
la relativa variabile cui è applicato; in pratica, informa il compilatore che si ha intenzione di
usare tale variabile che è stata, però, definita altrove (nello stesso file oppure in qualche
altro file) con un linkage normalmente esterno.
TERMINOLOGIA
Questo tipo di dichiarazione è detto anche “dichiarazione che riferisce” (referencing
declaration) perché il compilatore non alloca memoria per il corrispettivo oggetto come è il
caso di una “dichiarazione che definisce” (defining declaration) propria della scrittura senza
alcuno specificatore di un oggetto esterno.

TERMINOLOGIA
Se dichiariamo una variabile esterna senza lo specificatore di classe extern e senza
un’esplicita inizializzazione, tale dichiarazione è indicata dallo standard di C con il termine di
definizione provvisoria (tentative definition).

È importante dire che in un programma si possono indicare quante dichiarazioni extern si


vogliono ma si può fornire solo una definizione di un corrispettivo oggetto.
Se lo specificatore di classe extern è applicato a un oggetto esterno, lo stesso avrà: una
durata statica in memoria; un file scope e un linkage determinato da quello applicato alla
definizione del corrispettivo oggetto (dunque o sarà interno oppure sarà esterno).
Se lo specificatore di classe extern è applicato a un oggetto interno, lo stesso avrà: una
durata statica in memoria; un block scope e un linkage determinato da quello applicato alla
definizione del corrispettivo oggetto (dunque o sarà interno oppure sarà esterno).
NOTA
Lo specificatore extern è utilizzabile anche con una dichiarazione di funzione ma è
ridondante perché una funzione è, di default, visibile in tutto il programma, ossia anche ad
altre eventuali unità di traduzione.

Snippet 9.15 Lo specificatore di classe extern.


...
// prototipo di foo; simile a extern void foo(void);
// se non compare extern è assunto tale specificatore per default
void foo(void);

// variabile esterna esplicitamente definita; simile a extern int number = 100;


// se avessimo scritto int number; tale dichiarazione sarebbe stata considerata
// dallo standard come una definizione provvisoria (tentative definition) e avrebbe
// avuto il valore di default 0 se nell'unità di traduzione corrente non vi fosse stata
// almeno una sua definizione esplicita
int number = 100;

int main(void)
{
// esplicito la volontà di usare number che è stata definita altrove: in questo
// caso si riferisce alla variabile esterna number che avendo scope a livello di file
// consentirebbe anche di omettere tale dichiarazione
// infatti, è consuetudine ometterla sempre!!!
extern int number;
int a = number; // 100
foo();

// error: 'data' undeclared (first use in this function)


// qui data non è visibile perché è stato dichiarato dopo la fine del main
int value_1 = data;

// comunico al compilatore che data è stata definita altrove... qui non


// c'è alcun errore perché poi il linker provvederà a cercare la relativa
// definizione; in pratica consentiamo al compilatore di utilizzare tale nome
extern int data;
int value_2 = data; // 1000
...
}

// variabile esterna; file scope a partire però da dopo la sua dichiarazione


int data = 1000;

// definizione di foo; simile a extern void foo() {...}


// se non compare extern è assunto tale specificatore per default
void foo(void)
{
// ok data è visibile!
int x = data;
}

NOTA
Nel Capitolo 10 vedremo un esempio pratico di utilizzo dello specificatore di classe extern

per riferirsi a oggetti esterni definiti in altri file, caso in cui il suo impiego è obbligatorio.

register
Lo specificatore di classe register attribuisce agli oggetti dichiarati una classe di
memorizzazione di tipo registro e il suo impiego è valido per le dichiarazioni degli stessi
effettuate all’interno di un blocco (in pratica all’interno del corpo delle funzioni) ma anche
all’interno delle parentesi tonde di scrittura dei parametri formali (una variabile che è un
parametro formale può avere lo specificatore di classe register).
Una variabile dichiarata con tale specificatore di classe ha le stesse proprietà di una
variabile dichiarata con lo specificatore di classe auto (per esempio una durata in memoria
automatica, un block scope e nessun linkage), ma rispetto a essa ha un’importante
differenza: lo spazio di storage può essere, eventualmente, allocato nei registri della CPU
piuttosto che nella memoria principale.
NOTA
Ricordiamo che un registro è un’area di storage della CPU dove i dati lì memorizzati
possono essere manipolati in modo estremamente veloce. Si rimanda al Capitolo 1 per un
maggior dettaglio sulla CPU e sui relativi registri.

Lo scopo dello specificatore di classe register è quello di “suggerire” al compilatore di


porre le rispettive variabili decorate in una memoria ad accesso estremamente veloce perché
si ritiene che così facendo si possa migliorare le prestazioni di un algoritmo o del
programma nel suo complesso.
Come detto, però, register dà solo un’indicazione al compilatore, il quale può
tranquillamente ignorare tale richiesta e trattare una variabile register come una semplice
variabile auto (in termini pratici, cioè, come il compilatore agirà in caso di una dichiarazione
register è deciso dall’implementazione corrente).

Snippet 9.16 Lo specificatore di classe register.


...
#define SIZE 6

int main(void)
{
int data[SIZE] = {1, 2, 3, 4, 5, 6};

int res = 1;

// in questo caso sarebbe auspicabile che il compilatore usasse la veloce memoria


// di un registro CPU perché la variabile ix avrà dei ripetuti accessi (si pensi
// al caso di un array contenente molti elementi)
for (register int ix = 0; ix < SIZE; ix++)
res *= data[ix];

register int number = 1000;


// error: address of register variable 'number' requested
// non è possibile ottenere l'indirizzo di memoria di una variabile register!
int *addr_of_reg = &number;
...
}

_Thread_local
Lo specificatore di classe _Thread_local, introdotto dallo standard C11, attribuisce agli
oggetti dichiarati una classe di memorizzazione di tipo thread per effetto della quale essi
hanno una durata in memoria che è uguale alla durata del correlativo thread di esecuzione
dove sono stati, per l’appunto, dichiarati.
Per impiegarlo correttamente bisogna rammentare le seguenti costrizioni:
se è usato per una dichiarazione di un oggetto con un block scope, allora deve essere
obbligatoriamente presente anche lo specificatore di classe static o extern;
non può apparire come specificatore di classe per una dichiarazione o definizione di
funzione.

DETTAGLIO
Un processo è definibile come un ambiente di esecuzione all’interno del quale gira il
programma che l’ha creato. È un’unità di elaborazione, indipendente dagli altri processi,
costituita dal proprio spazio di memoria assegnato, dal proprio codice eseguibile e da
riferimenti a eventuali risorse di sistema allocate per esso. Un thread è invece definibile
come un’unità di elaborazione in cui può essere diviso un processo. Esso vive, pertanto,
all’interno di un processo dove condivide, con altri thread eventualmente presenti, le risorse,
la memoria e le informazioni di stato del processo medesimo. Un processo ha sempre un
thread di esecuzione rappresentato da se stesso, ma può avere anche altri thread creati al
suo interno per girare in parallelo.

TERMINOLOGIA
Il termine inglese thread si può tradurre in italiano come “filo”; visivamente, se immaginiamo
il processo come una fune, possiamo vedere i thread come i vari fili in esso avviluppati.

Per utilizzare questo specificatore di classe, bisogna usare un compilatore che abbia
implementato tale caratteristica e le API della programmazione concorrente in generale
come da specifica dello standard C11; nel momento in cui vengono scritte queste righe,
però, nessun compilatore, incluso GCC, ha fornito ancora il supporto.

typedef
Lo specificatore di classe typedef non stanza memoria ed è posto tra questi specificatori
solo per convenienza sintattica. Esso consente di creare identificatori che rappresentano dei
nuovi nomi per dei tipi preesistenti, ossia dei sinonimi di tipi (questi identificatori sono
denominati nomi typedef).

Snippet 9.17 Lo specificatore di classe typedef.


...
void foo(void); // prototipo di foo

typedef int MYINT;

int main(void)
{
// number è di tipo int; MYINT ne è un sinonimo
// lo scope di MYINT è a livello di file perché è lì che è stato creato
// tale identificatore, ossia il nome typedef di int
MYINT number = 100;

typedef struct P
{
int x;
int y;
} point;

// p è di tipo puntatore a una struttura di tipo struct P; point ne è un sinonimo


// lo scope di point è a livello di blocco perché è lì che è stato creato
// tale identificatore, ossia il nome typedef della struct P
point *p;
...
}

void foo(void) // definizione di foo


{
// ok MYINT visibile...
MYINT m = 1000;

// error: unknown type name 'point'


// qui point non è conosciuto perché il nome typedef è stato dichiarato nell'ambito
// del main e ha quindi un block scope
point *p_1;
}
Qualificatori di tipo
I qualificatori di tipo hanno lo scopo di qualificare i tipi delle variabili al fine di definire
per esse delle proprietà aggiuntive e possono comparire in qualsiasi numero e ordine.
A partire da C99 è possibile porre lo stesso qualificatore di tipo più di una volta in una
lista di qualificatori di tipo perché le altre occorrenze saranno semplicemente ignorate
(idempotent type qualifiers).

const
Il qualificatore di tipo const consente di rendere un oggetto “a sola lettura”: dopo che lo
stesso è stato inizializzato con un valore, quest’ultimo non potrà essere più cambiato.

Snippet 9.18 Il qualificatore di tipo const.


...
// costante "esterna" di tipo int
const int value = 1000;

int main(void)
{
// costante di tipo int
const int number = 10;

// array con elementi che sono costanti di tipo int


const int data[] = {1, 2, 3, 4, 5};

// puntatore a una costante di tipo int


const int *ptr_to_i;

// puntatore costante a un int


int *const ptr_to_j;

// puntatore costante a una costante di tipo int


const int *const ptr_to_k;

// "costante di struttura": questa qualifica si applica alla variabile


// del tipo struttura indicata e non al tipo di struttura stessa, così come
// non si applica ai membri della struttura che non sono essi stessi costanti
// quindi p1 è una costante di struttura di tipo struct point
// dopo l'inizializzazione non si può scrivere qualcosa come p1.x = 100;
// perché l'oggetto p1 è a sola lettura; ossia costante
// infatti, il compilatore genererebbe il seguente messaggio:
// error: assignment of member 'x' in read-only object
const struct point
{
int x;
int y;
} p1 = {100, 200};
...
}

restrict
Il qualificatore di tipo restrict, introdotto dallo standard C99 e applicabile solo ai
puntatori, “suggerisce” al compilatore di compiere eventuali ottimizzazioni per produrre
codice più efficiente, perché si farà in modo che l’oggetto riferito dal puntatore ristretto
venga manipolato solo per il suo tramite.

Snippet 9.19 Il qualificatore di tipo restrict.


...
// prototipo di copy
void copy(int nr, int *restrict p1, int *restrict p2);

int main(void)
{
// un array...
int data[] = {1, 2, 3};

// puntatore ristretto; asseriamo che solo per il suo tramite gli elementi
// di data saranno manipolati
int *restrict r_data = data;

// VALIDO
// p1 punterà a un'area di memoria Es. 0x28feb8 + 5 ossia 0x0028fecc con valore 600
// p2 punterà a un'altra area di memoria Es. 0x28feb8 con valore 100
// p1 e p2 punteranno ad aree di memoria in modo non "sovrapposto"
// quando il loop sarà eseguito;
// Es.
// p1 = 0x0028fecc - p2 = 0x0028feb8
// p1 = 0x0028fed0 - p2 = 0x0028febc
// p1 = 0x0028fed4 - p2 = 0x0028fec0
// p1 = 0x0028fed8 - p2 = 0x0028fec4
// p1 = 0x0028fedc - p2 = 0x0028fec8
// al termine values avrà questi valori
// {100, 200, 300, 400, 500, 100, 200, 300, 400, 500}
int values[] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000};
copy(5, values + 5, values);

// NON VALIDO - undefined behavior


// p1 punterà a un'area di memoria Es. 0x0028fe90 + 1 ossia 0x0028fe94 con valore 200
// p2 punterà a un'altra area di memoria Es. 0x0028fe90 con valore 100
// p1 e p2 punteranno ad aree di memoria in modo "sovrapposto" quando il loop
// sarà eseguito, ossia non sarà rispettato il "contratto" per cui si accederà
// a una determinata area di memoria solo dal relativo puntatore ristretto;
// Es.
// p1 = 0x0028fe94 - p2 = 0x0028fe90
// p1 = 0x0028fe98 - p2 = 0x0028fe94
// p1 = 0x0028fe9c - p2 = 0x0028fe98
// p1 = 0x0028fea0 - p2 = 0x0028fe9c
// p1 = 0x0028fea4 - p2 = 0x0028fea0
// al termine values_2 avrà questi valori
// {100, 100, 100, 100, 100, 100, 700, 800, 900, 1000}
int values_2[] = {100, 200, 300, 400, 500, 600, 700, 800, 900, 1000};
copy(5, values_2 + 1, values_2);

return (EXIT_SUCCESS);
}

// definizione di copy
// copiamo un certo numero di valori da un indirizzo di memoria a un altro
// in questo caso facciamo una "promessa" al compilatore che non vi sarà alcuna
// "sovrapposizione" di accesso agli indirizzi di memoria riferiti; ossia p2 accederà
// a un'area a cui non si accederà anche per il tramite di p1 e viceversa
// in caso di "non mantenimento della promessa" il comportamento sarà non definito...
// restrict usabile anche per i parametri
void copy(int nr, int *restrict p1, int *restrict p2)
{
while (nr-- > 0)
*p1++ = *p2++;
}
volatile
Il qualificatore di tipo volatile indica al compilatore che l’oggetto cui è applicato potrà
subire dei cambiamenti di valore da parte di “agenti esterni” al corrente programma (si
pensi a una periferica hardware che modifica in autonomia il valore di una variabile per
indicare un suo cambiamento di stato). Pertanto, nel caso, il predetto compilatore non deve
provare a eseguire eventuali ottimizzazioni sul codice quali possono essere, per esempio,
quelle di eliminare una variabile se non è usata in una statement, quelle di impiegare i veloci
registri della CPU per memorizzare valori usati frequentemente e così via.
Infatti, dato che una variabile è volatile (il suo valore può cambiare inaspettatamente),
bisogna garantire che la stessa sia utilizzabile sempre anche ai predetti agenti esterni oppure
che il valore letto sia sempre quello effettivamente da essi aggiornato. Le eventuali
ottimizzazioni eseguite potrebbero quindi impedire quell’importante accesso oppure quella
fondamentale lettura; una dichiarazione volatile, permette proprio di porre in essere questa
garanzia.
In pratica ogni lettura o scrittura di una variabile volatile deve essere sempre effettuata,
nel numero e nell’ordine cui compaiono nel relativo codice sorgente; il compilatore non
deve, dunque, riordinare tali statement per ragioni di ottimizzazione del codice.
DETTAGLIO
Il qualificatore di tipo volatile è usualmente impiegato nella scrittura di driver per i device
hardware laddove gli indirizzi di memoria utilizzati contengono dati che possono essere
modificati, in autonomia, dal sistema operativo oppure dall’hardware che gestisce tali
device.

Snippet 9.20 Il qualificatore di tipo volatile.


// conterrà un indirizzo di memoria dove saranno posti gli stati di un lettore DVD
// per esempio: 0, chiuso; 1, aperto; 2, motore avviato; 3, motore arrestato; ecc.
int *DVD_status = (int *) 0x0000ABCD; // indirizzo di memoria fittizio!
// CONSIGLIATO
// volatile int *DVD_status = (int *) 0x0000ABCD; // indirizzo di memoria fittizio!

// assegnamenti multipli; in questo caso, dato che la variabile DVD_status non viene
// modificata il compilatore potrebbe impiegare dei registri CPU per migliorare
// il suo accesso consecutivo; tuttavia, poiché DVD_status può essere cambiato anche
// dall'hardware che gestisce il DVD, questa ottimizzazione è un problema perché
// il compilatore leggerebbe il valore da un registro invece che dalla memoria principale
// e tale valore sarebbe, pertanto, non aggiornato correttamente
int stat_1 = *DVD_status;
int stat_2 = *DVD_status;
int stat_3 = *DVD_status;
int stat_4 = *DVD_status;

// conterrà un indirizzo di memoria di un timer hardware che sarà aggiornato


// in automatico
// poniamo il caso, per semplicità, che tale aggiornamento avvenga ogni millisecondo
int *timer = (int *) 0x00001234; // indirizzo di memoria fittizio!
// CONSIGLIATO
// volatile int *timer = (int *) 0x00001234; // indirizzo di memoria fittizio!
int current_time = 0;

// un loop che monitora il timer e alla fine di 1 secondo esce;


// un compilatore potrebbe ottimizzare questo loop cambiandolo nel seguente:

// current_time = *timer; // legge solo una volta il valore corrente del timer
// while (current_time < 1000) { ; /* non fa niente */ }

// perché può ritenere che sia inutile leggere ripetutamente il valore di *timer
// e assegnarlo alla variabile current_time non essendo quest'ultimo affetto da alcun
// cambiamento (loop invariant expression);
// in pratica può ritenere inefficiente dover assegnare un valore che, dal suo punto
// di vista, "è sempre lo stesso" per 1000 volte alla variabile current_time;
// a questo scopo, quindi, può ritenere che sia sufficiente leggere il valore di *timer
// solo una volta...;
// chiaramente quest'ottimizzazione è un problema sia perché causa un loop infinito sia
// perché c'è necessità di leggere, sempre, ogni millisecondo il valore cambiato
// dal timer hardware
while (current_time < 1000)
current_time = *timer;

_Atomic
Il qualificatore di tipo _Atomic, introdotto dallo standard C11 e non applicabile al tipo
array e al tipo funzione, consente di dichiarare un oggetto come di tipo atomico, ossia come
un oggetto al quale, nel contesto della programmazione concorrente, si potrà accedere solo
dal corrente thread di esecuzione; nessun altro thread potrà, cioè, accedere a un oggetto di
un tipo atomico durante un’operazione atomica.
TERMINOLOGIA
Un’operazione è definita atomica quando tutte le istruzioni che la rappresentano sono viste
come un’unica entità che può fallire o riuscire soltanto nel suo complesso, senza che
durante la loro esecuzione vi possano essere interferenze esterne da parte di altro codice.

Snippet 9.21 Il qualificatore di tipo _Atomic.


...
// definisce macro, tipi e funzioni utilizzabili per gestire
// operazioni atomiche su dati condivisibili tra più thread
#include <stdatomic.h>

int main(void)
{
// data è un oggetto di tipo atomico
// per lo standard la dimensione, la rappresentazione e l'allineamento di un
// tipo atomico non necessitano essere uguali a quelli del corrispondente tipo
// non qualificato
_Atomic int data;

// lo store del valore 100 nella variabile data è garantito essere un'operazione
// atomica; durante tale operazione nessun altro thread potrà accedere a data
atomic_store(&data, 100); // atomic_store è una macro definita nel file header
// <stdatomic.h>
...
}

NOTA
Per utilizzare questo qualificatore di tipo, bisogna usare un compilatore che abbia
implementato tale caratteristica, come per esempio GCC dalla versione 4.9. Ricordiamo che
questa è una conditional feature, ossia una caratteristica che un’implementazione non deve
necessariamente applicare.
Specificatori di tipo
Gli specificatori di tipo consentono di indicare in modo generale per un’entità il suo tipo
di dato, ossia la natura, il significato dei valori che potrà contenere (si pensi al tipo int che
potrà contenere valori interi oppure al tipo float che potrà contenere valori decimali) oppure
che potrà ritornare (si pensi a un valore di un certo tipo ritornato da una funzione).
In accordo con lo standard C11 i tipi sono suddivisi in object types, ossia tipi che
descrivono oggetti, e in function types, ossia tipi che descrivono funzioni.
Questi object type, in un determinato punto di un’unità di traduzione, possono poi essere:
tipi incompleti se mancano di sufficienti informazioni per determinare la dimensione di
storage del relativo oggetto (si pensi a un tipo array dichiarato con una dimensione
sconosciuta come per esempio extern double data[]; oppure a un tipo struttura o unione
dichiarati con del contenuto sconosciuto, ovvero senza l’indicazione dei propri membri,
come per esempio struct tag;); tipi completi se hanno, invece, sufficienti informazioni per
determinare lo spazio di storage del relativo oggetto.
Dagli object type e function type possono poi essere costruiti qualsiasi numero di derived
types (tipi derivati) che sono categorizzati in: array types (vettori di oggetti di un certo tipo),
structure types (strutture contenenti una sequenza di oggetti di vario tipo), union types
(unioni capaci di contenere uno fra diversi oggetti di vario tipo), function types (funzioni
che ritornano un oggetto di un determinato tipo), pointer types (puntatori a oggetti o
funzioni di un certo tipo) e atomic types (oggetti di tipo atomico)

void
Il tipo void descrive un tipo di oggetto incompleto che rappresenta un insieme vuoto di
valori. Esso è applicabile in vari contesti in cui designa una specifica semantica (per
esempio: se impiegato come tipo di ritorno di una funzione indica che la stessa non ritorna
nulla; se indicato come tipo di un puntatore indica che tale puntatore non punta, in
particolare, ad alcuno specifico tipo di dato).

Snippet 9.22 Lo specificatore di tipo void.


// non consentito!
// cosa mai potrebbe significare una variabile di tipo void?
// è senza alcuna utilità: non si può assegnare alcun valore e non si può convertire
// in alcun tipo
void f; // error: variable or field 'f' declared void

// consentito!
// ptr_to_void è un puntatore a void, ossia un puntatore generico che può puntare
// a qualsiasi tipo di dato; detto in altri termini, è un puntatore che non punta
// a nulla di specifico
void *ptr_to_void;
char
Il tipo char descrive un tipo di oggetto grande abbastanza da poter memorizzare qualsiasi
carattere del set di caratteri in uso. Quando in un oggetto di tipo char è memorizzato un
carattere valido dell’insieme di caratteri del set di caratteri in uso, il suo valore è equivalente
al codice intero del carattere ed è garantito essere mai negativo.
Viceversa, se è memorizzato un carattere non valido il valore equivalente è definito
dall’implementazione.

Snippet 9.23 Lo specificatore di tipo char.


// carattere non valido:
// con GCC il valore memorizzato è -84; dà i seguenti messaggi di diagnostica:
// warning: multicharacter character constant
// warning: overflow in implicit constant conversion
// con cl di Microsoft il valore memorizzato è -128
char c = '€';

// carattere valido:
// valore memorizzato 74 come da codice ASCII
char j = 'J';

NOTA
I tipi char, signed char e unsigned char sono tipi differenti e sono categorizzati come character
types.

short
Il tipo short descrive un tipo di oggetto che deve essere in grado di contenere dei
“piccoli” valori numerici interi senza parte frazionaria. Per C11 il range di valori da
garantire va da -32767 a 32767. Esso è categorizzato come uno standard signed integer type.

Snippet 9.24 Lo specificatore di tipo short.


// un oggetto di tipo short
// dichiarazioni equivalenti:
// signed short s = 32000;
// short int s = 32000;
// signed short int s = 32000;
short s = 32000;

int
Il tipo int descrive un tipo di oggetto che deve essere in grado di contenere dei “normali”
valori numerici interi senza parte frazionaria e avente la grandezza naturale dell’architettura
del corrente ambiente di esecuzione (deve essere grande abbastanza da contenere qualsiasi
valore nel range da INT_MIN a INT_MAX così come definito nel file header <limits.h>). Per C11 il
range di valori da garantire va da -32767 a 32767. Esso è categorizzato come uno standard
signed integer type.
Snippet 9.25 Lo specificatore di tipo int.
// un oggetto di tipo int
// dichiarazioni equivalenti:
// signed i = INT_MAX;
// signed int i = INT_MAX;
int i = INT_MAX;

long
Il tipo long descrive un tipo di oggetto che deve essere in grado di contenere dei “grandi”
valori numerici interi senza parte frazionaria. Per C11 il range di valori da garantire va da
-2147483647 a 2147483647. Esso è categorizzato come uno standard signed integer type.

Snippet 9.26 Lo specificatore di tipo long.


// un oggetto di tipo long
// dichiarazioni equivalenti:
// signed long l = 2147483600;
// long int l = 2147483600;
// signed long int l = 2147483600;
long l = 2147483600;

NOTA
Anteponendo lo specificatore di tipo long allo specificatore di tipo long si definisce un tipo
long long che descrive un tipo di oggetto in grado di contenere dei valori numerici interi
senza parte frazionaria “estremamente” grandi (per C11, il range di valori da garantire va da
-(263 - 1) a 263 - 1).

float
Il tipo float descrive un tipo di oggetto che deve essere in grado di contenere dei valori
numerici in virgola mobile a singola precisione. Per C11, in accordo con lo standard IEEE
754, il range di valori da garantire va da -3.4×10-38 a 3.4×1038 e la precisione deve essere di 6
cifre. Esso è categorizzato come un real floating type.

Snippet 9.27 Lo specificatore di tipo float.


// un oggetto di tipo float
float f = 834.454f;

double
Il tipo double descrive un tipo di oggetto che deve essere in grado di contenere dei valori
numerici in virgola mobile a doppia precisione. Per C11, in accordo con lo standard IEEE
754, il range di valori da garantire va da -1.7×10-308 a 1.7×10308 e la precisione deve essere di
15 cifre. Esso è categorizzato come un real floating type.

Snippet 9.28 Lo specificatore di tipo double.


// un oggetto di tipo double
double d = 1.22e-5;

NOTA
Anteponendo lo specificatore di tipo long allo specificatore di tipo double si definisce un tipo
long double che descrive un tipo di oggetto in grado di contenere dei valori numerici in virgola
mobile a precisione estesa (per C11, in accordo con lo standard IEEE 754, il range di valori
da garantire va da -1.19×10-4932 a 1.19×104932 e con una precisione di 18 cifre).

signed
Il tipo signed descrive un tipo di oggetto che deve essere in grado di contenere dei valori
numerici interi senza la parte frazionaria che possono essere anche valori negativi.
In effetti lo specificatore di tipo signed è ridondante con i tipi short, int, long e long long

perché, di default, tali tipi possono anche contenere dei numeri minori di 0; invece, si può
esplicitare con il tipo char per imporne il segno perché lo standard di C non specifica se un
tipo char di default deve essere con segno oppure senza segno.

Snippet 9.29 Lo specificatore di tipo signed.


// un oggetto di tipo signed char
signed char sc = '2';

unsigned
Il tipo unsigned descrive un tipo di oggetto che deve essere in grado di contenere dei valori
numerici interi senza la parte frazionaria che non possono essere anche valori negativi.
È applicabile ai tipi char, short, int e long che, in questo caso sono categorizzati come
standard unsigned integer types.

Snippet 9.30 Lo specificatore di tipo unsigned.


// un oggetto di tipo unsigned short
// dovrebbe contenere solo numeri maggiori o uguali a 0
// warning: negative integer implicitly converted to unsigned type
unsigned short us = -12; // ATTENZIONE numero negativo!!!

_Bool
Il tipo _Bool descrive un tipo di oggetto che deve essere in grado di contenere i valori 0
(per rappresentare un valore di falsità) e 1 (per rappresentare un valore di verità).
Esso è categorizzato come uno standard unsigned integer type.

Snippet 9.31 Lo specificatore di tipo _Bool.


// un oggetto di tipo _Bool
_Bool b = 4 > 3; // conterrà 1 (true)
_Complex
Il tipo _Complex descrive un tipo di oggetto che deve essere in grado di contenere dei
numeri complessi. Lo standard C11 stabilisce che ci sono tre tipi complessi, esprimibili
come float _Complex, double _Complex e long double _Complex; i tipi complessi sono una feature
condizionale che un’implementazione può non supportare.
I tipi real floating visti (float, double e long double) e i tipi complex citati sono denominati
collettivamente dallo standard come floating types.
Per ogni floating type c’è un corrispondente real type, che, nel caso dei tipi complessi è
dato dal tipo ottenuto cancellando la keyword _Complex (per esempio, in una dichiarazione
come float _Complex, float è il corrispondente real type).

Snippet 9.32 Lo specificatore di tipo _Complex.


// un oggetto di tipo _Complex
// I è una macro definita nell'header <complex.h> e rappresenta l'unità immaginaria i
// laddove l'unità immaginaria è un numero i tale che i2 = -1.
double _Complex dc = 5.0 - 3.0 * I; // parte reale = 5.0; parte immaginaria = -3.0i

NOTA
Nello standard corrente è anche previsto lo specificatore di tipo _Imaginary che è utilizzabile
per descrivere un tipo di oggetto che deve essere in grado di contenere un valore che è la
parte immaginaria di un numero complesso. Tale specificatore di tipo è usabile come
specificatore di tipo _Complex, ossia si aggiunge dopo il tipo float, double o long double;
possiamo avere quindi i tipi: _Imaginary float, _Imaginary double e _Imaginary long double. I tipi
immaginari sono una feature condizionale, e un’implementazione può scegliere di non
supportarli. I tipi immaginari unitamente ai tipi real floating e ai tipi complex sono
categorizzati come floating types.

_Atomic
Il tipo _Atomic, introdotto dallo standard C11, descrive un tipo di oggetto atomico. Esso si
utilizza scrivendo la relativa keyword e una coppia di parentesi tonde ( ) al cui interno si
indica il nome del tipo che dovrà essere “modificato” in modo che il relativo oggetto
dichiarato sia trattato in modo atomico (Sintassi 9.2).
I tipi atomici sono una feature condizionale che un’implementazione può non supportare.

Sintassi 9.2 Lo specificatore di tipo _Atomic.


_Atomic(type_identifier) object;

Snippet 9.33 Lo specificatore di tipo _Atomic.


// un oggetto di tipo atomico
// equivalente ad: atomic_int data;
// atomic_int è definito come un typedef nell'header <stdatomic.h>
_Atomic(int) data;
TERMINOLOGIA
In accordo con lo standard C11, quando la keyword _Atomic è immediatamente seguita da
una parentesi tonda ( deve essere interpretata come uno specificatore di tipo; in caso
contrario deve essere interpretata come un qualificatore di tipo.

NOTA
Per utilizzare questo specificatore di tipo, bisogna usare un compilatore che abbia
implementato tale caratteristica come, per esempio GCC dalla versione 4.9. Ricordiamo che
questa è una conditional feature, ovvero una caratteristica che un’implementazione non
deve necessariamente implementare.

struct
Il tipo struct descrive un tipo di oggetto che è di tipo struttura, ossia un tipo che è
costituito da un insieme di oggetti, detti membri, allocati sequenzialmente e nell’ordine in
cui sono stati dichiarati. Per lo standard un tipo struct è un derived type chiamato anche
aggregate type (tipo aggregato).

Snippet 9.34 Lo specificatore di tipo struct.


// un oggetto di tipo struttura; una struttura di tipo struct data
struct data
{
int code;
float ratio;
};

union
Il tipo union descrive un tipo di oggetto che è di tipo unione, ossia un tipo che è costituito
da un insieme di oggetti (membri), allocati in modo sovrapposto.
Per lo standard un tipo union è un derived type ma non un aggregate type, perché un
oggetto di tipo unione può contenere, validamente, un solo membro alla volta.

Snippet 9.35 Lo specificatore di tipo union.


// un oggetto di tipo unione; un'unione di tipo union type
union type
{
struct
{
int i;
long l;
} int_T;
struct
{
float f;
double d;
} float_T;
};
enum
Il tipo enum descrive un tipo di oggetto che è di tipo enumerazione, ossia un tipo che è
costituito da un insieme di costanti intere con nome dette enumeratori o costanti di
enumerazione.

Snippet 9.36 Lo specificatore di tipo enum.


// un oggetto di tipo enumerazione; un'enumerazione di tipo enum suit
enum suit {DIAMONDS, HEARTS, CLUBS, SPADES};

typedef-name
Il tipo typedef-name descrive un tipo di oggetto che è di tipo typedef-name, dove typedef-
name è un nome di tipo definito mediante lo specificatore della classe di memorizzazione
typedef.

Snippet 9.37 Lo specificatore di tipo typedef-name.


// definizione di un nome di tipo; a typedef-name
// BYTE è un alias per unsigned char
typedef unsigned char BYTE;

// b è un oggetto di tipo BYTE


BYTE b = 128;
Specificatori di funzione
Gli specificatori di funzione consentono di indicare per una funzione delle proprietà o
delle caratteristiche aggiuntive. Questi specificatori devono seguire le seguenti importanti
regole: devono essere usati solo nell’ambito di una dichiarazione di una funzione; non
devono essere usati per decorare la funzione main.

inline
Lo specificatore di funzione inline, introdotto con lo standard C99, suggerisce al
compilatore di porre in essere, eventualmente, delle ottimizzazioni al fine di rendere la
chiamata alla relativa funzione il più veloce ed efficiente possibile.
Una funzione inline può avere linkage interno (si utilizza lo specificatore della classe di
memorizzazione static) oppure linkage esterno, e in quest’ultimo caso vi sono le seguenti
restrizioni.
Deve essere definita nella stessa unità di traduzione e allora tale definizione diventa
una inline definition (definizione inline).
Non può essere comunque invocata da altre funzioni in altre unità di traduzione anche
se il linkage esterno, per semantica, ne permetterebbe una referenziazione. In pratica
una definizione inline non è una definizione esterna per la relativa funzione. Come
corollario si ha che non vi è alcun divieto nello scrivere una definizione esterna della
stessa funzione in un’altra unità di traduzione. In questo caso il compilatore, quando
dovrà invocare la relativa funzione, potrà scegliere se usare le istruzioni della
definizione inline oppure quelle della definizione esterna.
Non deve contenere una definizione di un oggetto con lo specificatore della classe di
memorizzazione static.
Non deve contenere riferimenti a oggetti con linkage interno.

Listato 9.2 Inline.c (Inline).


/* Inline.c :: Uso dello specificatore inline :: */
#include <stdio.h>
#include <stdlib.h>
#include <stdbool.h>

_Bool ext = true;

static int max_value = 100;


inline double inchToCms(double i)
{
// ATTENZIONE: definizione di un oggetto static
// messaggio da parte del compilatore:
// warning: 'ratio' is static but declared in inline function 'inchToCms'
// which is not static
static double ratio = 2.54;
// ATTENZIONE: riferimento a un oggetto con linkage interno
// messsaggio da parte del compilatore:
// warning: 'max_value' is static but used in inline function 'inchToCms'
// which is not static
int _m = max_value;

return i * ratio;
}

// linkage esterno; inline definition


inline double dollarToEuro(double e, double rate)
{
return e * rate;
}

// linkage esterno; inline definition


inline double celsToFahr(double t)
{
ext = false;
return (9.0 * t) / 5.0 + 32.0;
}

// prototipo di makeConversion; definita però in Other.c


void makeConversion(double v1, double v2);

int main(void)
{
double cels = 28;

// 5$ con un rapporto di 1$ = 0.78€


makeConversion(5, 0.78);

// qui il compilatore potrà scegliere di utilizzare la versione inline qui definita


// oppure la versione definita esternamente nel file Other.c
// con GCC se usiamo il flag di compilazione -O3 si noterà come il compilatore userà
// la versione inline di celsToFahr; senza flag userà la versione esterna;
// in accordo con lo standard C11 è comunque non specificato se un compilatore
// sceglierà la versione inline oppure la versione esterna di una funzione
// che è stata definita, per l'appunto, inline oppure esternamente
double res = celsToFahr(cels);
printf("%.2f gradi Celsius sono equivalenti a %.2f gradi Fahrenheit\n", cels, res);
printf("Risultato generato dalla funzione celsToFahr [%s]\n",
ext ? "esterna" : "inline");

return (EXIT_SUCCESS);
}

Listato 9.3 Other.c (Inline).


/* Other.c :: Uso dello specificatore inline :: */
#include <stdbool.h>

// prototipo di dollarToEuro; definito in Inline.c


// in ogni caso la funzione non è utilizzabile perché è definita,
// in Inline.c, come inline
double dollarToEuro(double e, double rate);

// riferimento alla variabile ext definita nel file Inline.c


extern _Bool ext;

// definizione alternativa di celsToFahr definita nel file Inline.c


double celsToFahr(double t)
{
ext = true;
return (9.0 * t) / 5.0 + 32.0;
}

void makeConversion(double v1, double v2)


{
// qui il linker darà il seguente messaggio di errore: undefined reference to
// 'dollarToEuro'
// per verificare decommentare la seguente istruzione
// dollarToEuro(v1, v2);
}

Output 9.2 Dal Listato 9.2 Inline.c.


28.00 gradi Celsius sono equivalenti a 82.40 gradi Fahrenheit
Risultato generato dalla funzione celsToFahr [esterna]

_Noreturn
Lo specificatore di funzione _Noreturn, introdotto dallo standard C11, indica che una
funzione, al termine del suo processo elaborativo, non ritornerà il controllo del flusso di
esecuzione del codice alla funzione che l’ha chiamata.

Snippet 9.38 Lo specificatore di funzione _Noreturn.


_Noreturn void foo(int n)
{
// per come è scritta la funzione si potrebbe avere un comportamento
// non definito perché per valori > 0 la funzione ritorna il controllo
// del flusso alla funzione chiamante
// GCC, infatti, emette il seguente messaggio:
// warning: 'noreturn' function does return
if (n < 0) abort();
}
Specificatori di allineamento
Gli specificatori di allineamento permettono di indicare per un oggetto una quantità di
byte che rappresentano per esso un valore di allineamento, ossia un valore che il
compilatore deve prendere in considerazione per allocarlo a un determinato indirizzo di
memoria esattamente divisibile per questo valore.
Ciò significa che possiamo “forzare”, per esempio, una variabile di tipo char che richiede
di default un allineamento di 1 byte con un allineamento di 4 byte e far sì che essa sia
allocata a un indirizzo di memoria solamente divisibile per 4.

L’OPERATORE _ALIGNOF
L’operatore _Alignof (Sintassi 9.3), disponibile a partire da C11, consente di ottenere il
requisito di allineamento del suo operando, che è il nome di un tipo, ossia ritorna un valore
intero costante (che deve essere una potenza intera non negativa di 2) che indica il numero di
byte tra indirizzi consecutivi dove saranno memorizzati i valori di quel tipo; detto in altri
termini, il tipo di dato sarà memorizzato a indirizzi di memoria che saranno multipli del valore
ritornato da _Alignof.

TERMINOLOGIA
Un numero è un multiplo di un altro se la divisione del primo per il secondo dà come resto 0;
ovvero, detto in termini più formali, un numero intero j è un multiplo di un altro numero
intero k se vi è un altro numero intero l tale che l * k dà come risultato j. Per esempio, 8 (j)
è multiplo di 2 (k) perché vi è un altro numero, 4 (l), per cui 4 (l) * 2 (k) = 8 (j).

Sintassi 9.3 Operatore _Alignof.


_Alignof(type_identifier);

Snippet 9.39 Operatore _Alignof.


// 4 byte; un int sarà memorizzato a indirizzi di memoria multipli di 4; es. 0x28feec
size_t ar_1 = _Alignof(int); // 4

// 8 byte; un double sarà memorizzato a indirizzi di memoria multipli di 8; es. 0x28fee8


size_t ar_2 = _Alignof(double); // 8

// 2 byte; per un tipo array il valore di allineamento ritornato sarà quello del tipo
// del suo elemento; nel nostro caso il tipo è short
// NOTA: _Alignof non valuterà tipi funzione e tipi incompleti
size_t ar_3 = _Alignof(short [3]); // 2

_Alignas
Lo specificatore di allineamento _Alignas (Sintassi 9.4), introdotto con lo standard C11,
consente di indicare per un oggetto un determinato valore di allineamento, definito dallo
standard come stricter alignment, che può derivare da quello relativo a un determinato tipo
oppure dalla valutazione di un’espressione costante intera.
Sintassi 9.4 Lo specificatore di allineamento _Alignas.
_Alignas(type_identifier | integer_constant_expression) object;

Il valore dell’espressione costante intera dovrà ritornare un intero che dovrà essere un
valido valore di allineamento indicato dallo standard con il termine di fundamental
alignment, oppure un intero che dovrà essere un valore di allineamento supportato dalla
corrente implementazione indicato dallo standard con il termine di extended alignment,
oppure il valore 0. In ogni caso il valore di allineamento specificato non dovrà essere
inferiore rispetto all’allineamento normalmente richiesto per il tipo da allineare.
TERMINOLOGIA
Per fundamental alignment lo standard intende un valore di allineamento che è minore o
uguale al più grande valore di allineamento supportato dall’implementazione corrente e che
è uguale a quello ritornato da _Alignof (max_align_t). Per extended alignment lo standard
intende un valore di allineamento che è più grande del più grande valore di allineamento
supportato dall’implementazione corrente e che è uguale a quello ritornato da _Alignof
(max_align_t). In quest’ultimo caso è definito dall’implementazione corrente se supportare o
meno tale allineamento. Precisiamo che max_align_t è un nome di tipo (typedef di una struct)

definito nel file header <stddef.h>.

Snippet 9.40 Lo specificatore di allineamento _Alignas.


// error: requested alignment is not a power of 2
// non si può allineare un oggetto a indirizzi di memoria che non sono potenze di 2
_Alignas(3) int i = 1000;

// ok; i due char saranno allineati a indirizzi di memoria multipli di 2


// e non a qualsiasi indirizzo di memoria perché, per l'appunto, char
_Alignas(2) char c1 = 'A'; // Es. 0x28feee - (2686702)
_Alignas(2) char c2 = 'B'; // Es. 0x28feec - (2686700)

// error: '_Alignas' specifiers cannot reduce alignment of 'd'


// un double ha un normale valore di allineamento che è pari a 8 byte e dunque
// non è possibile richiedere un allineamento inferiore
_Alignas(4) double d = 100.33;

// la corrente implementazione di GCC supporta un extended alignment


// qui f1 e f2 saranno allineati a una distanza di 16 byte
// in questo caso i tipi si dicono "over-aligned"
_Alignas(16) float f1 = 12.3f; // Es. 0x28fee0 - (2686688)
_Alignas(16) float f2 = 22.3f; // Es. 0x28fed0 - (2686672)

// nessun effetto...
_Alignas(0) int i2 = 10;
Inizializzazioni
Un’inizializzazione è un’operazione tramite la quale si fornisce a un oggetto un valore
iniziale. È attuabile utilizzando, nell’ambito di una dichiarazione, il simbolo uguale = posto
subito dopo il dichiaratore, e un apposito inizializzatore o più inizializzatori, se tale oggetto
è un tipo aggregato o un tipo unione, posto dopo il predetto simbolo.
TERMINOLOGIA
In C inizializzazione e assegnamento non sono la stessa cosa; l’inizializzazione è
un’operazione che avviene nel momento della dichiarazione di un oggetto, ossia quando
deve essere creato, e ne fornisce un valore iniziale in modo esplicito oppure in modo
implicito (al termine di quest’operazione l’oggetto sarà stato allocato in memoria e conterrà
quel valore di inizializzazione); l’assegnamento è un’operazione che avviene dopo che un
oggetto è stato già creato (allocato in memoria) e ne sovrascrive il valore lì contenuto.

Un inizializzatore per un oggetto di tipo scalare (per esempio una variabile data di tipo
int) è rappresentato da una singola espressione, opzionalmente racchiusa tra le parentesi
graffe { }, che fornisce un valore inziale per tale oggetto eventualmente convertito nel suo
tipo se di tipo diverso (le regole di conversione adottate sono le stesse già viste per le
operazioni di assegnamento).
Un inizializzatore per un oggetto di tipo aggregato (per esempio una struttura o un array)
o di tipo unione è, invece, rappresentato da un lista di inizializzatori separati dal carattere
virgola , e racchiusi tra le parentesi graffe { }, che forniscono dei valori iniziali per i
membri o gli elementi di tale oggetto eventualmente convertiti nel suo tipo se di tipo
diverso (le regole di conversione adottate sono le stesse già viste per le operazioni di
assegnamento). In quest’ultimo caso, un tipo di una struttura o di un’unione che ha una
durata automatica in memoria può altresì essere inizializzato con un’espressione che ritorna
un oggetto del medesimo tipo di struttura o di unione.
Abbiamo poi le seguenti regole applicabili a seconda che un oggetto abbia una durata
automatica o statica in memoria.
Se un oggetto ha una durata automatica in memoria, e non si fornisce durante la fase di
inizializzazione un esplicito valore, allora sarà indeterminato, ossia potrà contenere
qualsiasi cosa (per esempio, se abbiamo data che è un oggetto automatico di tipo int
potrà contenere valori come 0, -44, 2000 e così via in modo del tutto casuale e dunque
qualsiasi valore). Se invece si fornisce un esplicito valore di inizializzazione, lo stesso
potrà essere fornito da espressioni non necessariamente costanti (per esempio come
valore ricavato dalla valutazione di un variabile).
Se un oggetto ha una durata statica in memoria, e non si fornisce durante la fase di
inizializzazione un esplicito valore, allora sarà: per un tipo puntatore, un puntatore
nullo; per un tipo aritmetico, uno 0; per un tipo aggregato, uno 0 per ciascun membro o
elemento; per un tipo unione, uno 0 per il suo primo membro. Se invece si fornisce un
esplicito valore di inizializzazione, lo stesso dovrà essere fornito da espressioni
necessariamente costanti (per esempio come valore ricavato dalla valutazione di una
direttiva #define).

TERMINOLOGIA
Per C i tipi aritmetici (arithmetic types) sono rappresentati, collettivamente, dai tipi interi
(integer types) e dai tipi in virgola mobile (floating types).

Snippet 9.41 Alcuni esempi di inizializzazioni.


...
#define VALUE 100

// static storage duration; a e b inizializzate con il valore 0


struct S
{
int a;
int b;
} s;

// static storage duration; p inizializzato con il valore ((void *)0)


int *p;

// static storage duration; a inizializzato con il valore 0


union U
{
short a;
int i;
float f;
} u;

int var = 10;


// error: initializer element is not constant
// ext_a, che ha una durata statica in memoria, non può essere inizializzata
// con il valore di var che è un'espressione non costante
int ext_a = var;

// ok inizializzazione consentita
// ext_b, che ha una durata statica in memoria, può essere inizializzata con
// il valore di VALUE perché, essendo una macro, è considerata un'espressione costante
int ext_b = VALUE;

int main(void)
{
// a, che è un oggetto scalare, è inizializzato con il valore 2
int a = 2;

// data, che è un oggetto aggregato, è inizializzato con i valori 1, 2 e 4


int data[] = {1, 2, 4};

// una struttura di tipo struct point


struct point
{
int x;
int y;
};
// p1 e p2 sono tipi aggregati e sono inizializzati, rispettivamente,
// con una lista di inizializzatori e con un'espressione dello stesso tipo
// infatti p1 è di tipo struct point così come p2
struct point p1 = {1, 2}, p2 = p1;

// consentita l'inizializzazione degli elementi di nrs tramite delle espressioni


// non costanti come è il caso dei valori ritornati dalle variabili c e d
int c = 10, d = 11;
int nrs[] = {c, d};

// variabile automatica non inizializzata; valore indeterminato!


int _c;
...
}
Asserzioni statiche
Le asserzioni statiche, espresse tramite la keyword _Static_assert (Sintassi 9.5) e
disponibili da C11, sono un utile strumento di programmazione atte a fornire dei messaggi
diagnostici durante la compilazione di un programma, e a interromperla se una data
espressione risulta essere falsa (compile-time check).
Costituiscono quindi un prezioso strumento di diagnostica perché, permettendo di
verificare a tempo di compilazione se certe “assunzioni” sono non corrette (si pensi al caso
di un tipo di dato che deve essere solo di una certa grandezza perché il programma deve
essere eseguito solo su un’architettura che lo supporta), evitano di far eseguire un
programma che potrebbe avere, per causa loro, dei problemi di malfunzionamento
difficilmente individuabili oppure, nel caso peggiore, un arresto anomalo.

Sintassi 9.5 _Static_assert.


_Static_assert(integer_constant_expression, string_literal);

Un’asserzione statica si costruisce utilizzando la keyword _Static_assert e una coppia di


parentesi tonde ( ) al cui interno porre un’espressione costante intera (se ritornerà un valore
diverso da 0 allora la dichiarazione non produrrà alcun effetto, mentre se ritornerà un valore
uguale a 0 allora la dichiarazione interromperà la compilazione del corrente sorgente
mostrando un messaggio di diagnostica) e un letterale stringa (string_literal), che
rappresenta il messaggio di diagnostica mostrato dal compilatore.
NOTA
In C esiste anche la possibilità, come vedremo nel Capitolo 11, di utilizzare un’asserzione
che è valutata a run-time (run-time check) e abortisce il corrente programma se la relativa
espressione è valutata uguale a 0. Essa si utilizza tramite la macro assert definita nel file
header <assert.h>.

Snippet 9.42 Utilizzo di _Static_assert.


...
// affermi che nel sistema la corrente implementazione supporta
// i tipi long aventi una grandezza di 8 byte?
// se sì, allora non interrompere la compilazione
// se no, allora interrompi la compilazione
_Static_assert(sizeof (long) == 8,
"You must run a 64-bit system with a 64-bit compiler!");

int main(void)
{
// affermi che un char ha un allineamento di 1 byte?
// se sì, allora non interrompere la compilazione
// se no, allora interrompi la compilazione
_Static_assert(_Alignof(char) == 1, "Alignment of a char must be 1!");

struct time_T
{
char UTC;
int h;
int m;
int s;
};

// affermi che la struttura di tipo struct time non ha padding?


// se sì, allora non interrompere la compilazione
// se no, allora interrompi la compilazione
_Static_assert(sizeof (struct time_T) == (sizeof (char) + sizeof (int) * 3),
"The structure must not have any padding.");
...
}

NOTA
Un’asserzione statica è per lo standard una dichiarazione; pertanto, come evidenzia il
codice dello Snippet 9.42, può essere usata sia a livello di file scope sia a livello di block
scope.
Capitolo 10
Il preprocessore

Il linguaggio C, come visto in più occasioni, ha delle caratteristiche che lo rendono unico
e “speciale” rispetto ad altri linguaggi di programmazione mainstream.
Tra queste, vi è sicuramente la presenza di un tool, denominato preprocessore di C
(abbreviato come cpp, che sta per C preprocessor), che compie delle operazioni preliminari
di “modifica” del codice sorgente in base a degli appositi comandi, scritti con una
particolare sintassi e denominati direttive.
Al termine dell’esecuzione di queste operazioni, effettuate prima della fase di
compilazione vera e propria, il sorgente così modificato viene processato dal compilatore
stesso, durante la fase di compilazione, per produrre il relativo codice oggetto.
In pratica, possiamo dire che l’input del preprocessore, strumento che in alcune
implementazioni dei compilatori è parte del compilatore mentre in altre è un programma a
se stante automaticamente invocato, è un file di codice C contenente delle direttive che lo
stesso comprende ed esegue e poi al termine della fase di preprocessing rimuove dal
sorgente stesso.
Nel contempo, l’output del preprocessore è un altro file di codice C, modificato in
accordo con quanto indicato dalla direttive e senza di esse, che viene preso come input dal
compilatore il quale compie la consueta operazione di compilazione.

L’AMBIENTE DI TRADUZIONE DI C: UN DETTAGLIO


Un’implementazione di un compilatore conforme allo standard esegue, fondamentalmente, un
processo di traduzione dei file sorgente, scritti secondo le regole e la sintassi propria di C, e
un’operazione di esecuzione dei relativi programmi. In particolare, la fase di esecuzione di un
programma avviene nel cosiddetto execution environment (ambiente di esecuzione), mentre
la fase di traduzione di un file sorgente avviene in quello che lo standard definisce translation
environment (ambiente di traduzione), laddove si hanno diverse fasi ciascuna deputata a
compiere una ben specifica operazione. Prima, comunque, di vedere queste fasi di
traduzione, appare opportuno ribadire e approfondire anche alcuni termini e step che
riguardano la strutturazione di un generico programma in C; abbiamo, infatti, che:
un testo di un programma è scritto in apposite “unità” chiamate source files (file
sorgente) o preprocessing files (file pre-elaborazione);
un file sorgente, unitamente ad altri file sorgente inclusi tramite la direttiva #include (per
esempio i file header), è riferito con il termine di preprocessing translation unit (unità di
traduzione pre-elaborazione);
dopo la fase di preprocessing l’unità di traduzione pre-elaborazione è chiamata
translation unit (unità di traduzione).
Ciò detto, ogni unità di traduzione può comunicare con altre unità di traduzione che hanno
funzioni oppure oggetti i cui identificatori hanno un linkage esterno, e ciascuna di queste unità
di traduzione può essere tradotta separatamente e poi essere linkata per produrre un
programma eseguibile. Per quanto riguarda, quindi, le diverse fasi di traduzione abbiamo le
seguenti numerate in ordine di accadimento.
1. Mappatura dei caratteri multibyte nel set di caratteri del codice sorgente e sostituzione
delle sequenze trigraph con le corrispondenti rappresentazioni interne dei singoli
caratteri.
2. Localizzazione all’interno del codice sorgente di tutte le occorrenze del carattere
backslash (\) seguito da un carattere new line (inserito premendo fisicamente sulla
tastiera il tasto Invio) e cancellazione degli stessi al fine di creare delle righe logiche al
posto delle relative righe fisiche. Questa operazione è altresì conosciuta con il termine di
line splicing (congiunzione delle righe).
3. Scomposizione del contenuto di un file sorgente in: token pre-elaborazione
(preprocessing tokens) separati da caratteri di spaziatura; sequenze di caratteri di
spaziatura; commenti. Ogni eventuale commento è sostituito da un singolo carattere di
spazio, mentre ogni implementazione può scegliere se le sequenze di caratteri di
spaziatura saranno sostituite da un singolo carattere di spazio oppure saranno lasciate
immutate.
4. Elaborazione delle direttive per il preprocessore presenti nel file sorgente ed espansione
delle macro. Al termine dell’elaborazione tutte le direttive sono cancellate.
5. Conversione di ogni membro del set di caratteri del codice sorgente e delle sequenze di
escape presenti nelle costanti carattere e nei letterali stringa con i corrispondenti membri
del corrente set di caratteri di esecuzione.
6. Concatenazione dei letterali stringa adiacenti.
7. Conversione dei token pre-elaborazione in token regolari che saranno sintatticamente e
semanticamente analizzati e tradotti come unità di traduzione.
8. Risoluzione dei riferimenti agli oggetti e alle funzioni esterne e collegamento con codice
di librerie esterne non presenti nella corrente traduzione. Tutto l’output tradotto è poi
assemblato in “un’immagine” di un programma che è in grado di eseguirsi nel corrente
ambiente di esecuzione.
Delle fasi elencate quelle più importanti sono la 4, dove interviene il preprocessore, la 7, dove
interviene il compilatore stesso e la 8, dove interviene il linker. Solitamente, in taluni
compilatori, l’invocazione del relativo comando di compilazione del codice sorgente invocherà,
in automatico e a seconda della fase di traduzione in corso, il programma preprocessore (per
esempio cpp nella suite di compilazione GCC), il programma compilatore (per esempio gcc
nella suite di compilazione GCC) e il programma linker (per esempio ld nella suite di
compilazione GCC).

TERMINOLOGIA
Un set di caratteri (character set) nell’ambito di C indica quell’insieme di caratteri che può
essere validamente usato nella scrittura di un sorgente, definito dallo standard come source
character set (set di caratteri del codice sorgente o di origine), oppure che può essere
validamente interpretato ed è dunque disponibile durante l’esecuzione di programma ed è
definito dallo standard come execution character set (set di caratteri di esecuzione).
Ciascuno dei due set di caratteri è poi diviso in un basic character set (set di caratteri di
base) i cui membri sono indicati nella Tabella 10.1, e in 0 o più membri specifici del corrente
linguaggio locale definiti come extended characters (caratteri estesi). Inoltre, il set di
caratteri di base unitamente ai caratteri estesi disponibili è definito extended character set
(set di caratteri estesi).

Tabella 10.1 Membri appartenenti al set di carattere di base in accordo con lo standard C11.
Cifre
Lettere maiuscole e minuscole dell’alfabeto latino Caratteri grafici
decimali
ABCDEFGHIJKLMNOPQRSTUVWXYZ 01234 !“#%&‘()*+,-./:;
abcdefghijklmnopqrstuvwxyz 56789 <=>?[\]^_{|}~

NOTA
Ai membri della Tabella 10.1 vanno aggiunti il carattere di spazio (space character) e i
caratteri di controllo rappresentanti: la tabulazione orizzontale (horizontal tab); la
tabulazione verticale (vertical tab); il salto o avanzamento di pagina (form feed); il segnale
d’allerta (alert); la cancellazione dell’ultimo carattere (backspace); il ritorno del carrello
(carriage return) e l’avanzamento all’inizio della riga successiva (new line). Infine va anche
aggiunto il carattere nullo (null character) utilizzato come marcatore di terminazione di una
stringa di caratteri.

Listato 10.1 PreprocessingDirectives.c (PreprocessingDirectives).


/* PreprocessingDirectives.c :: Alcune direttive... :: */
#include <stdio.h>
#include <stdlib.h>

#define SIZE 5

// ??= sequenza di tre caratteri che sta per #


??=define NR 10

int main(void)
{
// due righe "fisiche" separate dal carattere \ e new line
printf("Un programma che mostra come effettuare un ciclo che\
consente di scansionare\nogni elemento di un determinato array!!!\n");

// un array
int data[SIZE] = {1, 2, 3, 4, NR};

// due letterali stringa adiacenti


printf("L'array data contiene i" " seguenti valori: [ ");
for (int i = 0; i < SIZE; i++)
printf("%d ",/* array data */data[i]);

printf("]\n");

return (EXIT_SUCCESS);
}

Output 10.1 Dal Listato 10.1 PreprocessingDirectives.c.


Un programma che mostra come effettuare un ciclo che consente di scansionare
ogni elemento di un determinato array!!!
L'array data contiene i seguenti valori: [ 1 2 3 4 10 ]

Il Listato 10.1 è un semplice programma in C utile solo per dimostrare cosa avviene
durante le fasi di traduzione citate. Ne mostriamo alcune significative; per esempio:
in accordo con la fase 1, la sequenza triplice ??= è sostituita con il carattere #;
in accordo con la fase 2, le due righe fisiche in cui è divisa la prima istruzione printf,
posta subito dopo il main, vengono congiunte per formare una sola riga logica; avremo
qualcosa come printf("Un programma che mostra come effettuare un ciclo che consente di

scansionare\nogni elemento di un determinato array!!!\n");;

in accordo con la fase 3, vengono identificati come token pre-elaborazione, per


esempio, i nomi degli header <stdio.h> e <stdlib.h>. Inoltre i commenti come nel caso
di /* array data */ sono sostituiti con un singolo carattere di spazio;
in accordo con la fase 4, le direttive #include vengono eseguite e le macro SIZE e NR di
cui le direttive #define vengono espanse. Inoltre, tali direttive vengono eliminate dal
sorgente dove al loro posto, come è il caso delle direttive #define, sono lasciate delle
righe vuote (per le direttive #include, invece, dal punto dove sono indicate viene
inserito il testo dei file sorgente che includono);
in accordo con la fase 5, una sequenza di escape \n è interpretata e convertita con un
membro del set di caratteri di esecuzione che esprime lo spostamento del cursore in
una nuova riga;
in accordo con la fase 6, i letterali stringa adiacenti "L'array data contiene i" " seguenti
valori: [ " sono concatenati nel seguente modo: "L'array data contiene i seguenti

valori: [ ";

in accordo con la fase 7, vengono identificati i token regolari int, for e così via;
in accordo con la fase 8, viene prodotto in ambiente GNU/Linux il file eseguibile
PreprocessingDirectives (o PreprocessingDirectives.exe in ambiente Windows).

Infine mostriamo un esempio dell’output del preprocessore cpp così come è espresso
grazie all’invocazione del comando gcc con il flag -E (Shell 10.1), mantenendo nella
funzione main la spaziatura relativa.

Shell 10.1 Utilizzo di gcc per generare il file di output del preprocessore.
gcc -std=c11 -E PreprocessingDirectives.c -o PreprocessingDirectives.i

Output 10.2 Dal file PreprocessingDirectives.i.


...
[inclusione inline del contenuto dei file header <stdio.h> e <stdlib.h>]
...
int main(void)
{

printf("Un programma che mostra come effettuare un ciclo che consente di scansionare\nogni
elemento di un determinato array!!!\n");

int data[5] = {1, 2, 3, 4, 10};


printf("L'array data contiene i" " seguenti valori: [ ");
for (int i = 0; i < 5; i++)
printf("%d ", data[i]);

printf("]\n");

return (0);
}

NOTA
L’Output 10.2 mostra il risultato delle fasi di traduzione dalla 1 alla 4. Non vedremo, quindi,
ciò che accadrà nelle altre fasi, come per esempio nella fase 6, dove i letterali stringa
adiacenti saranno concatenati.
Concetti preliminari
Una direttiva del preprocessore è rappresentata da una sequenza di elementi lessicali
minimi (detti token pre-elaborazione), laddove per la loro corretta scrittura si devono e/o si
possono soddisfare determinati requisiti.
Il primo token della sequenza deve essere il carattere cancelletto # che può essere posto
come primo carattere oppure come carattere preceduto da caratteri di spaziatura.
Il successivo token deve essere la keyword o il nome della direttiva (per esempio
define, include e così via) che può essere anche preceduto da caratteri di spaziatura (per

esempio è valido scrivere # define piuttosto che #define).


Gli altri token possono essere qualsiasi ulteriore elemento lessicale necessario al
completamento della sintassi della relativa direttiva (per esempio un identificatore
come SIZE e un valore numerico come 10 a completamento di una direttiva #define come
#define SIZE 10).

Tra tutti i token che rappresentano nel complesso una direttiva del preprocessore può
esservi qualsiasi numero di caratteri di spaziatura arbitrari (per esempio è valido
scrivere qualcosa come # define SIZE 10).
L’ultimo token della sequenza può essere un carattere di new line, e allora esso
marcherà la terminazione della direttiva stessa. È comunque possibile scrivere i token
di una direttiva su più righe di testo utilizzando il carattere backslash \ e il carattere
new line. Questa tecnica è spesso utilizzata per consentire a una direttiva
particolarmente lunga di continuare su più righe di testo e rendere più leggibile la sua
strutturazione.
Infine, per completezza di trattazione è utile dire che: una direttiva del preprocessore può
essere posta dovunque all’interno del codice sorgente anche se le direttive #include e #define
sono usualmente poste all’inizio del predetto codice; è possibile usare i consueti commenti
per esplicitare la semantica di una direttiva.
Definizione di macro
Una macro, denominata anche macroistruzione, è una sequenza arbitraria di token cui
viene attribuito un nome che, quando utilizzato nell’ambito del codice sorgente, viene
sostituito dal preprocessore da quella esatta sequenza di token.
Detto in termini più pratici, una macro altro non è che un “frammento” di testo con un
identificatore che viene inserito nel codice sorgente nell’esatto punto dove è posto tale
identificatore che lo denomina, che viene a tal fine eliminato e dunque rimpiazzato.

Object-like macro
Una macro definita nella forma della Sintassi 10.1 è denominata dallo standard come
object-like macro (macro simile a oggetto) ed è utilizzata, comunemente, per creare dei
nomi simbolici che rappresentano delle costanti numeriche, carattere o stringa.
Questa tipologia di macro, riferita talune volte anche come macro semplice, è denominata
object-like macro perché ricorda la definizione di un qualsiasi oggetto del linguaggio come
può essere, per esempio, la definizione di una variabile che è caratterizzata, oltre che da un
tipo, da un identificatore e da un valore associato.

Sintassi 10.1 Object-like macro.


#define macro_identifier token_replacement_list

La Sintassi 10.1 evidenzia che una macro semplice si definisce utilizzando, nell’ordine: il
carattere cancelletto #, il nome define, un nome per la macro e una lista di token.
Il simbolo cancelletto # e la keyword define rappresentano in modo congiunto la direttiva
del preprocessore #define, mentre per macro_identifier il nome indicato deve seguire le stesse
regole di scrittura dei comuni identificatori utilizzati per designare le entità del linguaggio,
come gli oggetti, le funzioni, i tag delle strutture e così via; ciò significa, per esempio, che
tale nome non può essere separato da caratteri di spaziatura, deve contenere solo lettere,
numeri, il carattere underscore _ e via discorrendo.
Invece, token_replacement_list, è rappresentato da una sequenza di token pre-elaborazione
quali identificatori, costanti numeriche, costanti carattere, letterali stringa, segni di
punteggiatura e così via.
TERMINOLOGIA
In accordo con lo standard di C, fa parte dei token pre-elaborazione, ed è dunque
utilizzabile con la direttiva #define, anche quello definito preprocessing number, che è un
token rappresentato da un numero, opzionalmente preceduto dal carattere punto (.), che
può essere seguito da un qualsiasi altro numero, carattere e sequenza di caratteri e+, e-, E+,
E-, p+, p-, P+, o P-. Ciò implica che è valido sia un comune numero intero come 100 o in
virgola mobile come 4.5 sia un “numero” come .314E+1 o 1a2b.

Ciò detto avremo che, durante la fase 4 di traduzione del codice, il preprocessore, quando
nel codice sorgente troverà ogni occorrenza di macro_identifier la sostituirà con quanto
espresso da token_replacement_list.
TERMINOLOGIA
L’operazione con cui il preprocessore sostituisce ogni occorrenza di un nome di una macro
con l’equivalente lista di token è anche detta espansione della macro (macro expansion).

Snippet 10.1 La direttiva #define e alcune macro semplici.


// definizioni di macro semplici; tutte valide
// nell'ambito del codice sorgente dove si troveranno i loro nomi il preprocessore
// li espanderà, letteralmente, con la lista di token definita dopo il nome stesso
#define NR 10 /* una macro per una costante numerica intera */
#define EULER 0.5772 /* una macro per una costante numerica in virgola mobile */
#define TpT 2per2 /* una macro per un preprocessing number */
#define MY_INT int /* una macro per una keyword (è un normale identificatore per il
preprocessore) */
#define LT < /* una macro per un segno di punteggiatura */
#define GT > /* una macro per un segno di punteggiatura */
#define NL '\n' /* una macro per una costante carattere */
#define _WARN_ "warning: " /* una macro per una costante stringa */
#define I_LOOP while(1) /* una macro per un frammento di codice */

// per questa macro tutti gli spazi dopo il nome della macro e dopo la cifra 4
// non sono considerati come parte dell'espansione; allo stesso modo tutti i
// caratteri extra tra la cifra 0 il carattere * e tra lo stesso carattere * e la cifra 4
// non sono considerati, ossia è considerato solo un carattere di spazio di separazione;
// ciò significa che in un sorgente dove avremo qualcosa come int a = SIZE; la stessa
// sarà presentata al compilatore come int a = 10 * 4;
#define SIZE 10 * 4 /* una macro semplice ... */

// attenzione un'implementazione può generare un messaggio come:


// warning: "ONE" redefined
#define ONE 1
#define ONE 2

// ridefinizione consentita!
#define TWO 10
#define TWO 10

#define SIX 3 * 2 /* I definizione */


// per il preprocessore 3*2 è un solo token perché tra 3 * e 2 non vi sono spazi
// infatti per il preprocessore ogni token è numerabile se separato dallo spazio
#define SIX 3*2 /* II definizione warning: "SIX" redefined */
#define SIX 2 * 3 /* III definizione warning: "SIX" redefined */

Lo Snippet 10.1 evidenzia la scrittura di una serie di macro semplici tutte effettuate
seguendo sempre lo stesso pattern che è caratterizzato dall’indicazione della direttiva
#define, dal nome della macro e da una lista di token di sostituzione.

Di tutte le macro è interessante notare quelle denominate ONE e quelle denominate TWO. Nel
primo caso un compilatore come GCC ci avvisa che abbiamo ridefinito una macro; in
questo contesto, per ridefinizione si intende quell’operazione per cui si definiscono due o
più macro con lo stesso nome ma con una lista di token differente.
In ogni caso, lo standard di C chiarisce la semantica del processo di ridefinizione di una
macro asserendo che la stessa è valida solo se le liste di sostituzioni di due o più macro con
lo stesso nome sono perfettamente uguali, ossia sono considerate identiche.
Nel secondo caso, infatti, non avremo alcun avviso da parte del compilatore perché le due
macro TWO, quantunque abbiano lo stesso nome, hanno altresì una lista di token uguali
laddove l’uguaglianza è verificata in merito a quali sono questi token, al loro numero e al
loro ordine di scrittura (per esempio, i token della lista di sostituzione delle macro
denominate SIX non sono uguali perché la prima definizione ha una lista di sostituzione con
tre token ossia 3, * e 2, la seconda definizione ha una lista di sostituzione con un token ossia
3*2 e la terza definizione ha una lista di sostituzione con 3 token che, seppur uguali a quelli
della prima, sono scritti in ordine diverso ossia 2, * e 3).
NOTA
Il compilatore GCC, in caso di ridefinizione di più macro, si limiterà a emettere degli appositi
warning e utilizzerà, in caso di espansione delle stesse, la lista di token dell’ultima
definizione. Altri compilatori potrebbero però segnalare tali ridefinizioni come errore. La
morale: seguire ciò che dice lo standard di C ed evitare di ridefinire le macro.

Listato 10.2 ObjectLikeMacro.c (ObjectLikeMacro).


/* ObjectLikeMacro.c :: Un esempio di macro semplici :: */
#include <stdio.h>
#include <stdlib.h>

#define MSG "Questo programma stampera' il totale di due moltiplicazioni \


definite tramite\ndelle direttive #define"

#define SIZE_1 (10 * 2)


#define SIZE_2 (10 * 3)
#define TOT_SIZE SIZE_1 + SIZE_2 /* totale di SIZE_1 con SIZE_2 */
#define PRINT_SIZE printf("Il totale di TOT_SIZE e' %d\n", TOT_SIZE)

int main(void)
{
printf("%s\n", MSG); // espande MSG con l'equivalente letterale stringa
PRINT_SIZE; // espande PRINT_SIZE con gli equivalenti token

#define NR 10 /* questa macro è utilizzabile da qui in poi ... */

return (EXIT_SUCCESS);
}

void foo(void)
{
// qui NR sarà visibile ed espansa con 10
int nr = NR;
}

Output 10.3 Dal Listato 10.2 ObjectLikeMacro.c.


Questo programma stampera' il totale di due moltiplicazioni definite tramite
delle direttive #define
Il totale di TOT_SIZE e' 50
Il Listato 10.2 definisce, in primo luogo, la macro MSG che sarà espansa con tutti i caratteri
costituenti il relativo letterale stringa doppi apici " inclusi.
In questo caso è interessante rilevare l’utilizzo dei caratteri backslash \ e new line che ci
hanno permesso di dividere su due righe fisiche quella stringa particolarmente lunga;
tuttavia, come già detto, questo non rappresenta un problema sintattico perché durante la
fase 2 di traduzione quei caratteri saranno eliminati e sarà creata un’unica riga logica.
A tal fine è importante dire che ogni direttiva del preprocessore terminerà quando lo
stesso troverà il primo carattere di new line posto dopo il carattere #; per MSG tale new line di
terminazione della direttiva sarà quello posto dopo il carattere " di chiusura della relativa
stringa e non quello posto dopo il carattere backslash \ perché, ribadiamo, quest’ultimo ci ha
permesso di dividere, senza errori, il contenuto di una riga lunga su più righe oppure, detto
in altro modo, di “continuare” una direttiva su una successiva riga.
Per la definizione delle macro SIZE_1 e SIZE_2 non c’è molto da dire e, infatti, esse saranno
espanse, letteralmente con (10 * 2) e (10 * 3). Le macro TOT_SIZE e PRINT_SIZE mostrano,
invece, come sia possibile definire, rispettivamente, delle liste di sostituzione con altre
macro oppure con token e altre macro.
In ambedue i casi quando il preprocessore incontrerà nell’ambito del codice sorgente
quelle macro provvederà a effettuare delle espansioni ricorsive: così, per esempio, quando
nell’ambito della funzione main incontrerà PRINT_SIZE la stessa sarà espansa utilizzando i
seguenti step di elaborazione:
1. printf("Il totale di TOT_SIZE e' %d\n", TOT_SIZE).

2. printf("Il totale di TOT_SIZE e' %d\n", SIZE_1 + SIZE_2).

3. printf("Il totale di TOT_SIZE e' %d\n", (10 * 2) + (10 * 3)).

Dal processo di espansione mostrato è importante rilevare come, per esempio, la


sostituzione di TOT_SIZE non è stata effettuata all’interno del letterale stringa passato come
argomento alla funzione printf; infatti, le sostituzioni di macro riguardano solo i token e
non i caratteri eventualmente presenti all’interno di una coppia di doppi apici.
NOTA
Se nel codice sorgente avessimo trovato qualcosa come MY_TOT_SIZE la sostituzione
“parziale” di TOT_SIZE non sarebbe avvenuta. MY_TOT_SIZE, infatti, rappresenta un identificatore
ben diverso da TOT_SIZE.

Infine, la funzione main definisce la macro NR che è visibile, indipendentemente da tale


blocco main, dal suo punto di definizione e fino al termine della corrente unità di traduzione
pre-elaborazione oppure fino a una corrispondente direttiva #undef.
In conclusione, le macro semplici, sono un pratico strumento utilizzato soprattutto per
definire delle costanti simboliche e, in tal caso, consentono di raggiungere diversi obiettivi
tra i quali: rendono un programma “più leggibile” perché permettono di non inserire
direttamente nel codice numeri, caratteri o stringhe (un nome di una macro come EULER è di
sicuro più significativo del numero 0.5772); rendono un programma “più manutenibile”
perché se dobbiamo cambiare un valore che è utilizzato innumerevoli volte nell’ambito di
un sorgente è più facile farlo una sola volta nella definizione della relativa macro piuttosto
che in tutte le sue occorrenze “fisiche”.
CURIOSITÀ
La comunità di programmatori in C usa scrivere il nome di una macro che rappresenta una
costante simbolica con tutte le lettere in maiuscolo. Probabilmente tale convenzione è stata
adottata perché è quella consigliata da Kernighan e Ritchie nel loro famoso libro Linguaggio
C, dove è detto: “i nomi delle costanti simboliche sono scritti per convezione in maiuscolo
perché è più immediato distinguerli dai nomi delle variabili che sono invece scritti per
convenzione in minuscolo”.

Function-like macro
Una macro definita nella forma della Sintassi 10.2 è denominata dallo standard come
function-like macro (macro simile a funzione) ed è utilizzata, comunemente, per creare delle
“piccole funzioni” deputate a svolgere semplici elaborazioni.
Questa tipologia di macro, riferita talune volte anche come macro parametrica o macro
con argomenti, è denominata function-like macro sia perché la sua definizione ricorda
quella di una comune funzione del linguaggio – dove è cioè presente un nome, una coppia
di parentesi tonde ( ), una lista di parametri formali e un corpo di istruzioni – sia anche
perché il suo utilizzo è sintatticamente uguale a quello delle comuni funzioni, ossia si scrive
il suo nome e una coppia di parentesi tonde ( ) al cui interno si inseriscono gli eventuali
argomenti.

Sintassi 10.2 Function-like macro.


#define macro_identifier([identifier_list]) replacement_list

In pratica, come da Sintassi 10.2, una macro parametrica si definisce utilizzando la


direttiva del preprocessore #define, un identificatore espresso tramite macro_identifier, una
coppia di parentesi tonde ( ) – laddove quella di sinistra ( deve essere scritta subito dopo
l’identificatore senza cioè che tra di essi vi siano caratteri di spaziatura – una lista di token
che saranno sostituiti a macro_identifier in rapporto alla “chiamata” della macro.
Per quanto concerne identifier_list, essa rappresenta quell’insieme degli identificatori
dei parametri formali che possono essere replicati in replacement_list in modo che durante
l’invocazione della macro parametrica gli stessi siano sostituiti dai corrispettivi argomenti
per questo forniti.
In più, identifier_list può essere anche opzionale, ossia è possibile scrivere durante la
definizione della relativa macro la coppia di parentesi tonde senza tale lista, cosi che durante
l’invocazione della macro non si renda necessario fornire alcun argomento.
IMPORTANTE
Bisogna rammentare che una macro parametrica “sembra essere” una comune funzione,
soprattutto nella sua fase di utilizzo ma, nella realtà, “non è” come tale. In effetti, il
preprocessore quando troverà una macro parametrica non farà altro che porre in essere le
consuete sostituzioni già viste pe le macro semplici le quali, però, a differenza di quelle
effettuate per quest’ultime, saranno parametriche.

Listato 10.3 FunctionLikeMacro.c (FunctionLikeMacro).


/* FunctionLikeMacro.c :: Un esempio di macro parametriche :: */
#include <stdio.h>
#include <stdlib.h>

// una serie di macro parametriche


#define max(x, y) ((x) > (y) ? (x) : (y)) /* calcola il massimo tra due numeri */
#define cube(x) ((x) * (x) * (x)) /* calcola il cubo di un numero */
#define c_print(c) printf("%c\n", c) /* visualizza un carattere... */
#define i_print(i) printf("%d\n", i) /* visualizza un intero... */
#define nl() printf("\n") /* stampa un carattere di new line */

int main(void)
{
printf("Di seguito il risultato dell'invocazione di alcune macro parametriche:");
nl(); /* nl sarà espansa come printf("\n") */

int j = 10, p = 11;

// m conterrà come valore 11 perché p è maggiore di j!


int m = max(j, p); /* max sarà espansa come ((j) > (p) ? (j) : (p)) */

// c conterrà come valore 1000 che è il cubo di j


int c = cube(j); /* cube sarà espansa come ((j) * (j) * (j)) */

char a_char = 'A';


c_print(a_char); /* c_print sarà espansa come printf("%c\n", a_char) */
i_print(j); /* i_print sarà espansa come printf("%d\n", j) */

return (EXIT_SUCCESS);
}

Output 10.4 Dal Listato 10.3 FunctionLikeMacro.c.


Di seguito il risultato dell'invocazione di alcune macro parametriche:
A
10

Il Listato 10.3 definisce la serie di macro parametriche max, cube, c_print, i_print e nl che
si comportano come se fossero delle funzioni che, rispettivamente, ritornano il massimo tra
due numeri, ritornano il cubo di un numero, visualizzano un carattere e inviano un carattere
di new line, visualizzano un intero e inviano un carattere di new line, inviano un carattere di
new line.
Per comprendere come avviene la sostituzione di una macro parametrica possiamo
prendere come esempio quella denominata max: gli identificatori x e y posti tra le parentesi
tonde ( ) di definizione sono “replicati” nell’ambito del corpo delle istruzioni che ne
rappresenta sia il processo elaborativo sia la lista di sostituzione.
Dunque, quando durante la fase 4 di traduzione il preprocessore troverà nell’ambito della
funzione main la definizione max(j, p), la cancellerà e la sostituirà con la sua replacement list
laddove tutte le occorrenze dell’identificatore del parametro x saranno sostituite con
l’identificatore j e tutte le occorrenze dell’identificatore del parametro y saranno sostituite
con l’identificatore p.
In definitiva la chiave di scrittura di una macro parametrica è la seguente: gli argomenti
forniti all’atto della sua invocazione saranno sempre sostituiti ai corrispondenti parametri e
ne prenderanno, quindi, il loro posto.
Scorrendo ulteriormente il sorgente del Listato 10.3 notiamo altresì come, nell’ambito
delle liste di sostituzione, gli identificatori dei parametri delle macro max e cube siano stati
posti tra una coppia di parentesi tonde ( ) e lo stesso è stato fatto per tutte le liste di
sostituzione ossia sono state esse stesse racchiuse tra tali parentesi.
Per comprenderne la motivazione vediamo cosa accadrebbe se definissimo la macro
parametrica cube come nello Snippet 10.2, ossia senza quelle parentesi tonde, e la usassimo
poi per calcolare il cubo di valore dato da un’espressione come, per esempio, j + 3.

Snippet 10.2 Parentesi mancanti attorno ai parametri di una lista di sostituzione.


...
// cube scritta in modo errato!!!
#define cube(x) x * x * x /* calcola il cubo di un numero */

int main(void)
{
int j = 10;

// m avrà come valore 73!


int m = cube(j + 3); /* cube sarà espansa come j + 3 * j + 3 * j + 3 */
...
}

In pratica senza parentesi il preprocessore sostituirà cube(j + 3) con j + 3 * j + 3 * j + 3.

Dopodiché, tale espressione verrà valutata dal compilatore, il quale, in accordo con le
consuete regole di precedenza e associatività, produrrà come valore 73 (in pratica saranno
prima eseguite le moltiplicazioni tra 3 e j e poi le addizioni tra quei risultati e i rimanenti
operandi). Chiaramente quello non era il risultato atteso perché noi avremmo voluto avere la
computazione del cubo di j + 3 che avrebbe dovuto dare come valore 2197 che è, per
l’appunto, il cubo di 13 laddove 13 è il risultato di j + 3 dove j vale 10.
Ecco quindi l’importanza delle parentesi tonde ( ) poste attorno ai parametri di una lista
di sostituzione; esse consentono di rispettare l’ordine di valutazione desiderato e atteso.
Ancora, vediamo lo Snippet 10.3 che mostra cosa accadrebbe se definissimo la macro
parametrica cube senza le parentesi tonde che racchiudono tutta la relativa lista di
sostituzione e la usassimo per calcolare un valore che è il risultato di un altro valore diviso
per il cubo di un altro valore.

Snippet 10.3 Parentesi mancanti attorno alla lista di sostituzione.


...
// cube scritta in modo errato!!!
#define cube(x) (x) * (x) * (x) /* calcola il cubo di un numero */

int main(void)
{
int j = 10;
int val = 10000;

// m avrà come valore 100000!


int m = val / cube(j); /* cube sarà espansa come (j) * (j) * (j) */
...
}

In questo caso senza parentesi il preprocessore sostituirà cube(j) con (j) * (j) * (j).

Dopodiché, tale espressione verrà valutata dal compilatore unitamente a val e all’operatore
di divisione /, il quale, in accordo con le consuete regole di precedenza e associatività,
produrrà come valore 100000 (in pratica sarà prima eseguita la divisione tra val e la prima
occorrenza di j e poi le moltiplicazioni tra quel risultato e i valori espressi dalle altre due
occorrenze di j). Chiaramente, anche qui, quello non era il risultato atteso perché noi
avremmo voluto avere la computazione del valore di val diviso per il cubo di j che avrebbe
dovuto dare come valore 10 risultante, per l’appunto, dal valore di val, che vale 10000, diviso
per il cubo di j che è 1000 perché j vale 10.
Vediamo, inoltre, un altro esempio di codice (Snippet 10.4) che evidenzia un’ulteriore
problematica legata, questa volta, a un utilizzo di una macro parametrica con un argomento
che produce un side-effect.

Snippet 10.4 Risultati non voluti prodotti da valutazioni di argomenti che producono side-effect.
...
#define max(x, y) ((x) > (y) ? (x) : (y)) /* calcola il massimo tra due numeri */

int main(void)
{
int j = 100, p = 50;

// m conterrà come valore quello di j che è più grande del valore di p


// m conterrà quindi il valore 101 per effetto dell'incremento di j avuto
// durante la valutazione dell'espressione (j++) > (p)
int m = max(j++, p); /* max sarà espansa come ((j++) > (p) ? (j++) : (p)) */

// j_val conterrà 102 perché j è stato valutato 2 volte al termine della valutazione
// di tutta la full expression che ricordiamo marca un sequence point
int j_val = j;
...
}

In pratica quando la macro parametrica max viene invocata come max(j++, p) il compilatore
si trova ad analizzare l’espressione ((j++) > (p) ? (j++) : (p)), conseguenza dell’espansione
effettuata dal preprocessore, dove è evidente come l’argomento j viene incrementato di
un’unità per ben due volte; la prima volta durante la valutazione del primo operando
dell’operatore condizionale, ossia (j++) > (p), e la seconda volta durante la valutazione del
secondo operando dello stesso operatore ossia (j++). Quanto detto, è poi dimostrato dalla
successiva operazione di assegnamento dove il valore della variabile j_val è 102 ossia quanto
contenuto nella predetta variabile j.
Anche in questo caso quello che avremmo voluto era semplicemente che m avesse
contenuto il valore più grande tra j e p, senza però che durante quella valutazione il valore
di j fosse incrementato. Allo stesso tempo, comunque, per effetto dell’operatore di
incremento postfisso ++ il valore di j avrebbe potuto contenere il valore 101 che sarebbe stato
assegnato lecitamente alla variabile j_val.
Quanto indicato è proprio il risultato che sarebbe stato garantito se avessimo definito max
non come una macro parametrica ma come una normale funzione.

Snippet 10.5 Correttezza di risultato in caso max fosse una normale funzione.
...
// prototipo di max
int max(int x, int y);

int main(void)
{
int j = 100, p = 50;

// m conterrà come valore quello di j che è più grande del valore di p


// m conterrà quindi il valore 100 e non 101 perché l'operatore di incremento
// sull'argomento j è postfisso e non prefisso
int m = max(j++, p);

// j_val conterrà 101 a causa dell'operatore di incremento postfisso applicato


// sull'argomento j che ricordiamo è stato comunque effettuato perché vi è un
// sequence point prima dell'ingresso nella funzione max ossia dopo la valutazione
// dei suoi argomenti e prima dell'esecuzione delle espressioni nel suo body
int j_val = j;
...
}

// definizione di max
int max(int x, int y)
{
return x > y ? x : y;
}

CURIOSITÀ
Rispetto alla convenzione di scrittura evidenziata in precedenza per le macro semplici, che
è in modo abbastanza uniforme accettato dalla comunità di programmatori in C, quella
adottata per la scrittura delle macro parametriche è invece divergente. Alcuni, cioè,
preferiscono scrivere gli identificatori delle macro parametriche con le lettere tutte in
maiuscolo, mentre altri preferiscono adottare lo stile adottato nel già citato libro sul C di
Kernighan e Ritchie, dove gli identificatori delle macro parametriche sono scritti con le
lettere tutte in minuscolo.

Macro con argomenti di lunghezza variabile


A partire dallo standard C99, una macro parametrica può essere anche definita con la
possibilità di accettare un numero variabile di argomenti (Sintassi 10.3).

Sintassi 10.3 Function-like macro con un numero variabile di argomenti.


#define macro_identifier([identifier_list], ...) replacement_list

In pratica per definire una macro parametrica e variadica è sufficiente compiere i


seguenti passi: utilizzare il token rappresentato dai punti di sospensione ... (ellissi) scritti al
termine di un’eventuale lista di identificatori per indicare che la relativa macro è in grado di
accettare un numero variabile di argomenti; impiegare lo speciale identificatore __VA_ARGS__
scritto nella lista di sostituzione per indicare quell’insieme di argomenti, il cui numero non è
noto a priori, che prenderanno il suo posto all’atto di invocazione della relativa macro.
TERMINOLOGIA
Anche se non espressamente riferito nello standard, è diventata prassi comune indicare una
macro parametrica che accetta un numero variabile di argomenti con il termine di macro
variadica. Allo stesso modo una funzione che accetta un numero variabile di argomenti è
spesso citata con il termine di funzione variadica.

Listato 10.4 VariadicMacro.c (VariadicMacro).


/* VariadicMacro.c :: Macro parametriche e variadiche :: */
#include <stdio.h>
#include <stdlib.h>

// debug può accettare un numero variabile di argomenti...


// è interessante notare l'uso di un do/while subito "falso", che è un'utile
// tecnica che permette di raggruppare più istruzioni in modo che le stesse
// possano essere utilizzate senza problemi anche in istruzioni if;
// se, infatti, avessimo definito la macro debug come:
// #define debug(...) { \
// printf("Nella funzione %s: ", __func__);\
// printf(__VA_ARGS__); \
// }
// e l'avessimo usata in un'istruzione if come nel seguente modo:
// if (is_debug)
// debug("a=%d\n", a);
// else
// ...
// il preprocessore l'avrebbe sostituita nel seguente modo:
// if(is_debug)
// { printf("Nella funzione %s: ", __func__); printf("a=%d\n", a); };
// else
// ...
// e il compilatore per effetto dell'istruzione nulla rappresentata dal
// punto e virgola messo dopo la parentesi } avrebbe generato il messaggio
// error: 'else' without a previous 'if'
#define debug(...) do \
{ \
printf("Nella funzione %s: ", __func__);\
printf(__VA_ARGS__); \
} while(0)

// prototipo di foo
void foo(void);

int main(void)
{
int a = 10;
int b = 11;

// debug sarà espansa come:


// do { printf("Nella funzione %s: ", __func__); printf("[a = %d] [b = %d]\n", a, b); }
// while(0);
debug("[a = %d] [b = %d]\n", a, b);

foo();

return (EXIT_SUCCESS);
}

// definizione di foo
void foo(void)
{
int x = 100, y = 101, z = 102;

// debug sarà espansa come:


// do { printf("Nella funzione %s: ", __func__); printf("[x = %d] [y = %d]
// [z = %d]\n", x, y, z); } while(0);
debug("[x = %d] [y = %d] [z = %d]\n", x, y, z);
}

Output 10.5 Dal Listato 10.4 VariadicMacro.c.


Nella funzione main: [a = 10] [b = 11]
Nella funzione foo: [x = 100] [y = 101] [z = 102]

Il Listato 10.4 definisce la macro parametrica debug in modo che possa accettare un
numero variabile di argomenti e che abbia come scopo quello di stampare a video il nome
della corrente funzione di esecuzione, tramite l’identificatore predefinito __func__, e il nome
di un numero arbitrario di variabili unitamente al loro valore.
In ambedue i casi, nell’ambito della lista di sostituzione, utilizziamo l’istruzione printf
che è essa stessa definita come una funzione in grado di accettare un numero variabile di
argomenti e, soprattutto nel secondo caso, si presta bene all’impiego dell’identificatore
__VA_ARGS__ che gli può fornire, per l’appunto, un qualsiasi numero di argomenti.

Per esempio, quando nella funzione main verrà invocata la macro parametrica debug, tutti i
suoi argomenti, ossia il letterale stringa, l’identificatore della variabile a e l’identificatore
della variabile b, unitamente al carattere virgola (,) di separazione, sostituiranno nella
seconda printf posta all’interno del do/while l’identificatore __VA_ARGS__.
DETTAGLIO
L’identificare __func__ è stato introdotto con lo standard C99 e ogni implementazione ne
deve perciò fornire una dichiarazione implicita. In pratica, esso deve essere dichiarato come
se ogni funzione avesse come prima istruzione del suo body qualcosa come static const
char __func__[] = "function_name"; dove function_name è il nome della relativa funzione.
ATTENZIONE
L’identificatore __func__ non è in alcun modo correlato al preprocessore. Infatti durante la
fase 4 di traduzione tale identificatore non viene espanso con il nome della corrente
funzione.

Macro con argomenti “vuoti”


Dallo standard C99 è possibile invocare una macro parametrica che accetta argomenti
omettendone uno o più di uno; in ogni caso, per un corretto utilizzo di questa caratteristica
bisogna prestare attenzione a che la macro non sia espansa in modo invalido, dal punto di
vista del compilatore, oppure che contenga, nel caso di una macro con due o più argomenti,
la giusta quantità di virgole di separazione tra gli stessi in modo che il preprocessore
medesimo non la ritenga altresì invalida.
Se, invece, una macro parametrica invocata senza l’indicazione di uno o più argomenti è
valida per il preprocessore, lo stesso effettuerà la consueta espansione, eliminando però
dalla lista di sostituzione ogni riferimento di uno o più corrispettivi parametri.

Snippet 10.6 Invocazione di una macro parametrica omettendo degli argomenti.


...
// una macro parametrica con due argomenti
#define min(x, y) ((x) < (y) ? (x) : (y))

int main(void)
{
int a = 10, b = 11;

// omesso il primo argomento;


// il suo corrispettivo parametro, ossia x, sarà eliminato dalla lista
// di sostituzione;
// il sorgente non sarà compilabile perché la macro espansa non sarà valida
// error: expected expression before ')' token
int min_1 = min(, b); /* min sarà espansa come (() < (b) ? () : (b)); */

// omesso il secondo argomento;


// il suo corrispettivo parametro, ossia y, sarà eliminato dalla lista
// di sostituzione;
// il sorgente non sarà compilabile perché la macro espansa non sarà valida
// error: expected expression before ')' token
int min_2 = min(a,); /* min sarà espansa come ((a) < () ? (a) : ()); */

// attenzione l'omissione di tutti e due gli argomenti senza indicazione


// del carattere , di separazione fa generare un errore anche da parte del
// preprocessore: error: macro "min" requires 2 arguments, but only 1 given
int min_3 = min(); /* min sarà espansa come (() < () ? () : ()); */

// omessi il primo e il secondo argomento;


// i suoi corrispettivi parametri, ossia x e y, saranno eliminati dalla
// lista di sostituzione;
// il sorgente non sarà compilabile perché la macro espansa non sarà valida
// error: expected expression before ')' token
int min_4 = min(,); /* min sarà espansa come (() < () ? () : ()); */
...
}

Lo Snippet 10.6 definisce la macro parametrica min che ritorna il più piccolo tra due
valori. La funzione main dichiara quindi le variabili a e b da comparare e invoca la macro min
più volte e in modo differente in quanto a indicazione dei relativi argomenti.
Tutte quelle invocazioni di min, però, presentano dei problemi di invalidità rilevate sia dal
compilatore (è il caso della prima, seconda e quarta invocazione dove le macro, una volta
espanse, mostrano dei problemi sintattici) sia dal preprocessore (è il caso della terza
invocazione dove l’espansione non viene neppure elaborata, sempre per problemi sintattici,
e la compilazione si ferma durante la fase 4 di traduzione).
In pratica, per il preprocessore non sarà mai considerato un errore l’omissione di uno o
più argomenti durante un’invocazione di una macro parametrica ma solo se si scriverà
l’esatta quantità di virgole di separazione tra di essi; per esempio, per una macro con due
argomenti bisognerà indicare una sola virgola, per una macro con tre argomenti bisognerà
indicare due virgole e così via per gli altri casi.
NOTA
Se la macro accetta un solo argomento è possibile ometterlo scrivendo l’identificatore della
macro e le parentesi tonde ( ) di invocazione vuote. Così, se è definita una macro
parametrica come #define QUAL(q) q sarà possibile scrivere, in modo valido, qualcosa come
QUAL().

Macro parametriche e funzioni


Le macro parametriche hanno dei vantaggi rispetto alle normali funzioni che possiamo
riepilogare come segue.
Essendo espanse inline evitano l’overhead causato dall’invocazione delle normali
funzioni. Quando una macro parametrica è invocata innumerevoli volte (si pensi al suo
utilizzo in un’istruzione di iterazione), il programma può risultare più veloce rispetto a
se la stessa fosse scritta come una funzione e invocata come tale.
Permettono di utilizzare dati indipendenti dal tipo. È possibile, cioè, scrivere macro
parametriche “generiche” che operano su tipi di dato differenti (si pensi alla funzione
min appena presentata e al fatto che può ritornare il minimo tra due tipi int, float, double

e così via).
Allo stesso tempo le macro parametriche hanno anche, sempre rispetto alle normali
funzioni, i seguenti svantaggi.
Possono provocare un aumento di dimensione del codice sorgente e di conseguenza del
codice compilato. Ciò è tanto più evidente se si utilizzano molte macro parametriche
quando le relative liste di sostituzione sono inserite inline in molti punti del codice
sorgente.
Non vi è un type checking degli argomenti e neppure un’eventuale conversione verso i
tipi dei rispettivi parametri. Quest’assenza di controllo sui tipi può chiaramente
provocare dei problemi soprattutto se i tipi utilizzati sono differenti (si pensi alla macro
min e alla possibilità di passare come argomenti un tipo int e un tipo array i cui
elementi sono di tipo char).
Gli argomenti possono essere valutati più di una volta causando ogni tanto dei
comportamenti non attesi soprattutto in presenza di side-effect (vedere, a tal proposito,
lo Snippet 10.4 presentato in precedenza).
In buona sostanza, prima di decidere se usare una macro parametrica rispetto a una
normale funzione bisogna sempre valutare quali sono gli obiettivi che vogliamo raggiungere
e se i relativi svantaggi sono accettabili per il particolare processo computazionale che
intendiamo rappresentare e dunque codificare.
NOTA
Ricordiamo che da C99 è possibile utilizzare le funzioni inline per scrivere “piccole” funzioni
che consentono di evitare il consueto overhead legato all’invocazione di normali funzioni,
rendendole preferibili rispetto alle macro parametriche. In ogni caso, prima di decidere se
privilegiare una funzione inline rispetto a una macro parametrica, rammentiamo che bisogna
tener presente che il comportamento di un compilatore rispetto a una funzione inline è
implementation-defined, ossia può tanto decidere di “espandere” inline il codice che è parte
del suo body quanto decidere di non far nulla.

La direttiva #undef
La direttiva del preprocessore #undef (Sintassi 10.4) permette di rimuovere una
definizione di una macro, sia semplice sia parametrica, ossia provoca l’annullamento del
suo identificatore che non può, quindi, dal quel punto in poi, essere più utilizzato (tranne,
chiaramente, se non se ne fornisce un’altra definizione).

Sintassi 10.4 La direttiva #undef.


#undef macro_identifier

In genere la direttiva #undef si usa sia per rimuovere una definizione di una macro già
esistente per la quale si desidera dare una nuova definizione (ricordiamo che per lo standard
è lecito ridefinire una macro solo se le liste di sostituzione sono uguali) sia per garantire che
un certo nome denoti un identificatore di una funzione piuttosto che quello di una macro
parametrica (Snippet 10.7).

Snippet 10.7 Utilizzo di #undef.


...
#include <ctype.h>

// macro parametrica per determinare se un carattere è una cifra decimale


#define isdigit(y) ((y) >= '0' && (y) <= '9' ? 1 : 0)

int main(void)
{
char a = '8';

// qui userà la macro parametrica isdigit


printf("%d\n", isdigit(a)); // 1

#undef isdigit /* annullamento definizione della macro isdigit */

char b = 'a';

// qui userà la funzione isdigit dichiarata nel file header <ctype.h>


printf("%d\n", isdigit(b)); // 0
...
}

NOTA
Se si utilizza la direttiva #undef con un identificatore non definito oppure che non rappresenta
una macro, non accadrà nulla: la direttiva sarà senza effetto e il preprocessore non
genererà alcun avviso diagnostico.

L’operatore hash #
È possibile utilizzare nell’ambito di una lista di sostituzione di una macro parametrica lo
speciale operatore del preprocessore avente come simbolo il carattere hash #.
Quest’operatore applicato in modo prefisso all’identificatore di un parametro della macro
fa sì che del corrispettivo argomento venga creato un letterale stringa, e tale letterale venga
inserito al posto del predetto parametro.
TERMINOLOGIA
Il processo di creazione di un letterale stringa di un argomento di una macro è definito dallo
standard come stringizing.

Un utilizzo piuttosto ricorrente dell’operatore # si ha quando si ha la necessità di creare


una macro parametrica laddove un’istruzione come printf possa mandare a video delle
informazioni di debug che contengano letteralmente il “nome” dell’espressione che si sta
controllando (Snippet 10.8).

Snippet 10.8 Utilizzo dell’operatore #.


...
// una macro parametrica
#define debug(expr) printf(#expr " = %d\n", expr)

int main(void)
{
int a = 100;

// in questo caso #expr sarà sostituita da "a"


debug(a); /* debug sarà espansa come printf("a" " = %d\n", a); */

int b = 200;

// in questo caso #expr sarà sostituita da "b"


debug(b); /* debug sarà espansa come printf("b" " = %d\n", b); */

// in questo caso #expr sarà sostituita da "b / a"


debug(b / a); /* debug sarà espansa come printf("b / a" " = %d\n", b / a); */
...
}

IMPORTANTE
Se come argomento di una macro parametrica che usa l’operatore # non forniamo nulla
(argomento vuoto), allora il risultato sarà una stringa vuota "". Per esempio, se definiamo la
macro #define P(p) #p e poi la invochiamo come in char eS[] = P();, tale istruzione sarà
presentata al compilatore come char eS[] = "";.

L’operatore doppio hash ##


È possibile utilizzare nell’ambito di una lista di sostituzione di una macro parametrica
oppure di una macro semplice lo speciale operatore del preprocessore avente come simbolo
il doppio carattere hash ##.
Quest’operatore consente di concatenare i due token pre-elaborazione tra i quali è posto
in modo da formare un solo token.
Ciò significa, per esempio, che se un token è l’identificatore di un parametro della macro
e un altro token è un valore numerico qualsiasi, quando la macro sarà invocata il parametro
sarà sostituito dal corrispettivo argoment, che sarà altresì “fuso” con tale valore numerico in
modo da formare un unico token.
TERMINOLOGIA
Il processo di creazione di un solo token risultato della concatenazione di due token è
definito dallo standard come token pasting.

Snippet 10.9 Utilizzo dell’operatore ##.


...
// una macro parametrica che consente di creare una serie di funzioni min ciascuna
// con il proprio identificatore e capace di processare lo specifico tipo di dato
#define makeMin(type) \
type type##Min(type x, type y) \
{ \
return x < y ? x : y; \
}

// makeMin sarà espansa come:


// int intMin(int x, int y) { return x < y ? x : y; }
makeMin(int)

// makeMin sarà espansa come:


// double doubleMin(double x, double y) { return x < y ? x : y; }
makeMin(double)

int main(void)
{
int a = 10, b = 11;
int min_1 = intMin(a, b); // 10

double m = 10.11, n = 22.11;


double min_2 = doubleMin(m, n); // 10.11
...
}

Lo Snippet 10.9 mostra un interessante esempio di una macro parametrica, makeMin, che è
capace di creare delle definizioni di funzioni ciascuna con un proprio identificatore e
ciascuna deputata a ritornare un risultato che è il valore minimo tra due valori laddove ogni
valore è di uno specifico tipo di dato.
Al fine di far generare un identificatore sempre diverso per ogni definizione di funzione
creata, la macro makeMin usa, nell’ambito della lista di sostituzione, l’operatore ## con i token
type e Min, dove type è l’identificatore di un parametro mentre Min è una serie arbitraria di
caratteri.
Ciò implicherà che, quando per esempio sarà invocata la macro makeMin(int), l’argomento
int sarà sostituito al parametro type e poi sarà concatenato con Min per formare il token
intMin. Lo stesso procedimento avverrà con l’invocazione di makeMin(double), dove al termine
dell’operazione di pasting si avrà il token doubleMin.
IMPORTANTE
Se come argomento di una macro parametrica che usa l’operatore ## non forniamo nulla
(argomento vuoto), allora tale argomento sarà sostituito da uno speciale token “invisibile”
chiamato placemarker token che sarà, poi, concatenato con un token ordinario e ritornerà
quel token originario (Snippet 10.10). Se invece a essere concatenati sono due placemarker
token, perché per esempio abbiamo invocato un macro con due argomenti vuoti, allora sarà
ritornato un singolo placemarker token. In ogni caso, al termine dell’espansione della macro,
ogni eventuale placemarker token sarà rimosso.

Snippet 10.10 Placemarker token.


...
#define list(x,y,z) x##y##z

int main(void)
{
// alla fine delle espansioni data sarà visto dal compilatore come:
// int data[] =
// {
// 123,
// 13,
// 23,
// 12,
// 1,
// 2,
// 3,
//
// };
// da notare lo spazio "vuoto" dopo 3, lasciato dal preprocessore
// a seguito dell'espansione di list(,,)
int data[] =
{
list(1, 2, 3), /* list sarà espansa come 123 */
list(1,, 3), /* list sarà espansa come 13 */
list(, 2, 3), /* list sarà espansa come 23 */
list(1, 2,), /* list sarà espansa come 12 */
list(1,,), /* list sarà espansa come 1 */
list(, 2,), /* list sarà espansa come 2 */
list(,, 3), /* list sarà espansa come 3 */
list(,,) /* espansione "vuota"... */
};
...
}
Espressioni di selezioni generiche
A partire dallo standard C11 è stata introdotta la keyword _Generic (Sintassi 10.5) che
permette di costruire un’espressione che è generica rispetto a un determinato tipo di dato
(type-generic expressions), ossia che è in grado di ritornare un determinato valore in base,
per l’appunto, a uno specifico tipo di dato fornito come “valore” di input.
TERMINOLOGIA
Lo standard C11 utilizza anche il termine di generic selection expression (espressione di
selezione generica) per indicare un’espressione di tipo generico che “seleziona” un valore in
base al tipo di un’espressione.

Sintassi 10.5 _Generic.


_Generic(controlling_expression, type_name: assignment_expression, type_name_other:
assignment_expression, default: assignment_expression)

Per definire un’espressione di selezione generica bisogna utilizzare la keyword _Generic e


una coppia di parentesi tonde ( ) al cui interno porre i seguenti elementi separati dal
carattere virgola (,).

Una controlling expression, ossia un’espressione di controllo utilizzata per la


determinazione di uno specifico tipo di dato. Questa espressione non è comunque
valutata, ossia se si fornisce qualcosa come x che è l’identificatore di una variabile di
tipo int che contiene il valore 10 tale valore non sarà computato ma sarà solo “fornito”
come valore il nome di tipo int.
Una serie di type name seguiti dal carattere due punti :, ossia una serie di nomi di tipi
di dato (per esempio int, char, float e così via) che rappresentano delle specie di
etichette che introducono delle espressioni da valutare, dette assignment expressions, e
che saranno valutate se il relativo nome di tipo sarà compatibile con il nome di tipo
determinato dall’espressione di controllo. L’elemento type_name e l’elemento
assignment_expression sono definiti da C11, in modo unitario, con il termine di generic
association (associazione generica).
Un’etichetta di default, ossia una label, espressa tramite la keyword default, la cui
relativa espressione sarà valutata se tutte le altre label indicanti i nomi dei tipi di dato
non sono corrispondenti al tipo di dato dell’espressione di controllo.
In base a quanto esplicitato, possiamo quindi asserire che se un’espressione di selezione
generica ha un’associazione generica con un nome di tipo compatibile con il nome di tipo
dell’espressione di controllo, allora l’espressione risultato di tale selezione avrà un tipo e un
valore che saranno uguali al tipo e al valore dell’assignment expression di
quell’associazione. Se, invece, non vi è alcuna concordanza di tipi tra il tipo
dell’espressione di controllo e uno dei tipi delle associazioni generiche, allora l’espressione
risultato della selezione generica avrà il tipo e il valore dell’assignment expression
dell’etichetta default.
CONSIGLIO
È possibile pensare a un’espressione di selezione generica come a una sorta di istruzione
che ricorda quella di selezione multipla switch, laddove mentre in quest’ultima è il valore
dell’espressione relativa che è confrontato con il valore di una serie di etichette case, nella
prima è il tipo dell’espressione relativa che è comparato con una serie di etichette di nomi di
tipi.

Listato 10.5 GenericSelection.c (GenericSelection).


/* GenericSelection.c :: Un caso d'uso di una selezione generica :: */
#include <stdio.h>
#include <stdlib.h>

// definisce una macro parametrica che si espande in un'espressione generica


// che a seconda del tipo di dato ritorna un determinato specificatore di formato
#define gen_fmt(f) _Generic((f),\
char: "%c", \
short: "%hd", \
int: "%d", \
long:"%ld", \
long long: "%lld", \
float:"%f", \
double: "%f", \
long double: "%Lf") \

// definisce una macro parametrica che consente di stampare con printf


// un valore di qualsiasi tipo di dato
#define g_print(p) printf(gen_fmt(p), p)

#define NL printf("\n")

int main(void)
{
char c = 'A';
g_print(c); NL;

int i = 10;
g_print(i); NL;

float f = 10.3f;
g_print(f); NL;

double d = 10.3344;
g_print(d); NL;

return (EXIT_SUCCESS);
}

Output 10.6 Dal Listato 10.5 GenericSelection.c.


A
10
10.300000
10.334400

NOTA
Il presente listato è compilabile correttamente con una versione di GCC dalla 4.9 in poi; è
solo a partire da quella versione che è stato introdotto il supporto alle espressioni di
selezioni generiche tramite la keyword _Generic.

Il Listato 10.5 definisce la macro parametrica gen_fmt con una lista di sostituzione formata
dalla definizione di un’espressione di una selezione generica che ha come obiettivo quello
di ritornare un letterale stringa che rappresenta un determinato specificatore di formato
utilizzabile con la funzione printf.
Lo specificatore di formato ritornato sarà quello indicato da un’etichetta di un nome di
tipo di dato che sarà compatibile con il nome di tipo di dato di cui l’espressione f (per
esempio, se invochiamo direttamente la macro gen_fmt nel seguente modo gen_fmt(10); sarà
ritornato il letterale stringa "%d" perché il valore 10 è di tipo int).
Successivamente definisce anche la macro parametrica g_print la cui lista di sostituzione
è formata dall’istruzione printf che ha come argomenti la macro gen_fmt stessa e
l’identificatore p del relativo parametro.
Questa macro, nella sostanza, consente di usare l’istruzione printf in modo “generico”,
ossia consente di stampare il valore di un’espressione senza indicare un determinato
specificatore perché lo stesso, come detto, sarà fornito dall’espressione di selezione generica
ricavata dall’espansione della macro gen_fmt.
Così, quando per esempio nella funzione main sarà usata la macro parametrica g_print(i);,
il compilatore si troverà ad analizzare la seguente istruzione frutto della sua espansione:
printf( _Generic(( i ), char: "%c", short: "%hd", int: "%d", long:"%ld", long long: "%lld",

float:"%f", double: "%f", long double: "%Lf") , i );. Qui appare evidente come sia anche
stata espansa la macro gen_fmt il cui parametro p è stato sostituito dall’argomento i, il quale
essendo di tipo int farà ritornare come valore della selezione generica lo specificatore di
formato "%d" che sarà poi usato dalla printf stessa per stampare correttamente il valore della
variabile i.
IMPORTANTE
Un’espressione di selezione generica è per lo standard un’espressione primaria e non è una
direttiva per il preprocessore. Abbiamo comunque preferito inserirla in questo contesto
didattico perché essa è usata, generalmente, con la direttiva #define per creare delle macro
parametriche che si possono comportare come delle funzioni generiche, cioè come delle
funzioni che agiscono in modo indipendente da un tipo di dato.
Inclusione condizionale
Una caratteristica importante del preprocessore è quella che permette di includere in
modo selettivo o condizionale pezzi di codice ovvero, in base al valore di una condizione
esaminata, il preprocessore può scegliere se includere o meno sezioni di testo del corrente
codice sorgente di un determinato programma.
L’inclusione condizionale, detta anche compilazione condizionale, è una tecnica
comunemente utilizzata per: verificare determinati punti di codice facendo stampare certe
informazioni diagnostiche solo quando si decide di attivare tale fase di debugging; scrivere
programmi portabili su hardware o sistemi operativi differenti; scrivere programmi adattati
per certe implementazioni di C piuttosto che per altre; proteggere l’inclusione multipla di
uno stesso file header; fornire una definizione di default di una macro verificando che non
sia stata già definita.

La direttiva #if
La direttiva del preprocessore #if (Sintassi 10.5) verifica se l’espressione costante intera
che deve valutare è diversa da 0 e, nel caso, indica al preprocessore di non rimuovere dal
codice sorgente da compilare il relativo gruppo di righe di codice; in questo caso, le righe di
codice considerate sono quelle scritte fino alla relativa direttiva #endif o #else o #elif.
Se, invece, la predetta espressione vale 0, allora indica al preprocessore di rimuovere dal
codice sorgente da compilare quel relativo gruppo di righe di codice che non saranno
pertanto visibili al compilatore. In ambedue i casi, comunque, la direttiva #if viene
eliminata dal preprocessore.

Sintassi 10.6 La direttiva #if.


#if integer_constant_expression new_line [rows_group]

Come evidenziato dalla Sintassi 10.6, la direttiva del preprocessore #if valuta, attraverso
integer_constant_expression, un’espressione costante intera che è però soggetta, in questo
contesto di utilizzo, alle seguenti ulteriori restrizioni rispetto a quelle già esaminate nel
Capitolo 3: non può contenere espressioni sizeof; non può contenere il costrutto cast; se si
utilizzano come operandi delle costanti enumerative esse saranno trattate come degli
identificatori non appartenenti a macro è saranno sostituite con il valore 0.
Segue, quindi, un carattere effettivo di new line (digitato cioè da tastiera) e poi attraverso
rows_group l’indicazione delle righe di testo di codice da includere o meno nella fase di
compilazione a seconda se l’espressione costante intera sia o meno diversa da 0.
Snippet 10.11 La direttiva #if.
...
// 0 = disattiva debug; 1 = attiva debug
#define DEBUG 1
#define SIZE 10

int main(void)
{
int data[SIZE] = {1, 2, 44, 55, 11, 2, 4, 5, 6, -1};
int sum = 0;

char c = '1';

for (int i = 0; i < SIZE; i++)


{
#if DEBUG /* se DEBUG è diversa da 0 fammi vedere i valori degli elementi di data */
printf("valore di data[%d] = %d\n", i, data[i]);
#endif /* DEBUG */
sum += data[i];
}
...
}

Lo Snippet 10.11 definisce un array data e poi elabora un ciclo for che deve ottenere la
somma di tutti i valori degli elementi del predetto array, da memorizzare, poi, nella variabile
sum definita allo scopo.

Il ciclo for ha nel suo blocco una direttiva #if che valuta la macro DEBUG la quale,
espandendosi in un valore diverso da 0, farà sì che il preprocessore lasci all’interno del
codice sorgente la relativa riga che è rappresentata da un’istruzione printf che manda a
video, per scopi diagnostici, un letterale stringa contenente, per ogni step dell’iterazione,
l’indicazione del corrente elemento analizzato unitamente al suo valore.
Se decidiamo di non volere più quei messaggi di debug, sarà sufficiente porre come lista
di sostituzione della macro DEBUG la costante 0 e ricompilare il codice sorgente; non sarà
quindi necessario eliminare le direttive del preprocessore, #if ed #endif, e la relativa
istruzione printf, che potranno sempre tornarci utili in futuro se vorremo rielaborare quelle
informazioni diagnostiche.
NOTA
Se una direttiva #if valuterà un identificatore non definito, lo tratterà come se fosse una
macro con il valore 0.

La direttiva #elif
La direttiva del preprocessore #elif (Sintassi 10.7) permette di valutare la sua espressione
costante intera solo se l’espressione costante intera della relativa direttiva #if è risultata
uguale a 0 (#elif è paragonabile al costrutto else if incontrato nel Capitolo 5).

Sintassi 10.7 La direttiva #elif.


#elif integer_constant_expression new_line [rows_group]

Snippet 10.12 La direttiva #elif.


...
#define LINUX 1 /* attuale sistema hardware di compilazione... */
#define WINDOWS 0
#define AMIGA_OS 0
#define MAC_OS 0

#define CURRENT_OS LINUX

int main(void)
{
#if CURRENT_OS == WINDOWS
printf("Windows...\n");
#elif CURRENT_OS == MAC_OS
printf("Mac...\n");
#elif CURRENT_OS == AMIGA_OS
printf("Amiga...\n");
#elif CURRENT_OS == LINUX /* questa espressione sarà diversa da 0 */
printf("Linux...\n");
#endif /* CURRENT_OS */
...
}

Lo Snippet 10.12 mostra come utilizzare la direttiva #elif in una serie di valutazioni per
verificare qual è il corrente sistema operativo dove compileremo il programma.
Analizzando il codice appare evidente come solo l’ultima espressione della direttiva #elif
ritornerà un valore diverso da 0 perché il valore della macro CURRENT_OS sarà uguale a LINUX,
che è esso stesso l’identificatore di una macro che sarà uguale a 1.
In più, è importante rilevare come, senza alcun problema, le espressioni costanti intere
siano interessate dall’utilizzo dell’operatore di uguaglianza uguale a ==; infatti, allo stesso
modo, è possibile impiegare l’altro operatore di uguaglianza non uguale a !=, gli operatori
aritmetici, gli operatori relazionali, gli operatori logici, gli operatori bit a bit e l’operatore
condizionale.
In pratica è certamente fattibile costruire delle espressioni complesse utilizzando la
molteplicità di operatori indicati perché la valutazione di tali espressioni ritornerà sempre un
valore intero utilizzabile nell’ambito delle predette direttive condizionali.
NOTA
Vi è un ulteriore modo per scrivere il codice presentato, ma lo vedremo tra breve quando
tratteremo della direttiva #ifdef.

La direttiva #else
La direttiva del preprocessore #else (Sintassi 10.8) include le righe di codice relative solo
se l’espressione costante intera della relativa direttiva #if, o di altre direttive #elif, sono
risultate uguali a 0 (#else è paragonabile al costrutto else incontrato nel Capitolo 5.

Sintassi 10.8 La direttiva #else.


#else new_line [rows_group]

Snippet 10.13 La direttiva #else.


#if __STDC_VERSION__ == 201112
printf("Ok standard C11: possiamo usare le sue caratteristiche...\n");
#else /* questo ramo sarà eseguito solo se l'#if sarà uguale a 0 */
printf("ERRORE no standard C11: non possiamo usare le sue caratteristiche...\n");
#endif

Per quanto riguarda questa direttiva bisogna rammentare che al massimo una di esse può
essere presente nell’ambito di un blocco #if / #endif, mentre questa limitazione non è
presente per le direttive #elif che possono esservi in qualsiasi numero si desidera.

La direttiva #endif
La direttiva del preprocessore #endif (Sintassi 10.9) permette di evidenziare la chiusura di
un blocco definito dalla direttiva #if (oppure dalle direttive #ifdef e #ifndef).

Sintassi 10.9 La direttiva #endif.


#endif new_line

Lo Snippet 10.14 è utile solo per mostrare come sia possibile costruire delle strutture di
selezione innestate anche mediante le direttive #if / #endif.
In ogni caso è prassi codificarle senza porre degli spazi di indentazione ma facendo
terminare le direttive #endif con un commento che evidenzia a quale #if appartiene.

Snippet 10.14 La direttiva #endif.


...
#define A 1
#define B 1
#define C 1

int main(void)
{
// una serie di #if / #endif innestati; saranno incluse tutte e tre le istruzioni
// di printf
#if A
printf("Ramo A...\n");
#if B
printf("Ramo B...\n");
#if C
printf("Ramo C...\n");
#endif /* C */
#endif /* B */
#endif /* A */
...
}

La direttiva #ifdef
La direttiva del preprocessore #ifdef (Sintassi 10.10) consente di verificare se un
determinato identificatore è stato definito come un nome di una macro.
Sintassi 10.10 La direttiva #ifdef.
#ifdef macro_identifier new_line [rows_group]

La direttiva #ifdef è per certi versi simile alla direttiva #if perché indica al preprocessore
se includere o meno nel codice che sarà compilato il gruppo di righe di testo relativo;
tuttavia essa ha un’importante differenza, ovvero non valuta un’espressione costante intera
ma verifica solo se un nome di una macro è definito (infatti, non è necessario che la macro
abbia associato un valore come 0, 1 e così via).

Snippet 10.15 La direttiva #ifdef.


...
#define NL

int main(void)
{
int data[] = {1, 2, 3, 4, 5};

for (size_t i = 0; i < sizeof data / sizeof (int); i++)


{
#ifdef NL /* stampa gli elementi di data ciascuno su una singola riga */
printf("%d\n", data[i]);
#else /* stampa gli elementi di data su una stessa riga */
printf("%d ", data[i]);
#endif /* NL */
}
...
}

Nello Snippet 10.5 il ciclo for stamperà a video i valori di tutti gli elementi dell’array data
in modo che saranno visualizzati ciascuno su una riga di testo separata.
Ciò sarà possibile perché la direttiva #ifdef verificherà che il nome NL è definito come
nome di una macro, e pertanto indicherà al preprocessore di lasciare nel codice sorgente la
relativa istruzione printf che conterrà anche la sequenza di escape di nuova riga.

La direttiva #ifndef
La direttiva del preprocessore #ifndef (Sintassi 10.11) consente di verificare se un
determinato identificatore non è stato definito come un nome di una macro.

Sintassi 10.11 La direttiva #ifndef.


#ifndef macro_identifier new_line [rows_group]

Questa direttiva indica al preprocessore di includere nel codice sorgente da compilare il


relativo gruppo di righe di testo solo se il nome della macro cui macro_identifier non è stato
definito.

Snippet 10.16 La direttiva #ifndef.


// definisci la macro SIZE e attribuisci il valore 10 ma solo se non è già definita
#ifndef SIZE
#define SIZE 10
#endif
L’operatore defined
L’operatore espresso tramite la keyword defined (Sintassi 10.12) è un operatore del
preprocessore che consente di verificare se un identificatore è stato definito come nome di
una macro. Se l’identificatore è stato definito allora l’espressione relativa ritornerà il valore
1, altrimenti ritornerà il valore 0.

Sintassi 10.12 L’operatore defined.


defined macro_identifier

L’identificatore controllato da defined può, opzionalmente, essere posto tra una coppia di
parentesi tonde ( ), come evidenzia lo Snippet 10.17, dove verifichiamo se è stato definito
l’identificatore LINUX come nome di una macro e nel caso lasciamo inserita nel codice la
relativa istruzione printf.

Snippet 10.17 L’operatore defined.


...
#define LINUX

int main(void)
{
#if defined (WINDOWS)
printf("Windows...\n");
#elif defined (MAC_OS)
printf("Mac...\n");
#elif defined (AMIGA_OS)
printf("Amiga...\n");
#elif defined (LINUX) /* questa espressione sarà diversa da 0 */
printf("Linux...\n");
#endif /* WINDOWS */
...
}

NOTA
Non vi è alcuna differenza semantica nell’usare #ifdef macro_identifier e #if defined

macro_identifier e #ifndef macro_identifier oppure #if !defined macro_identifier.


Inclusione di file sorgente
Un programma di una certa complessità è nella pratica costituito da diversi file di codice
sorgente che sono rappresentati, secondo le normali consuetudini di C, in file aventi
estensione .c, che contengono definizioni di funzioni e di variabili, e file aventi estensione
.h., che contengono definizioni di macro, prototipi di funzioni, istruzioni typedef e così via.
I file con estensione .h, detti include file o header file (file di intestazione), sono di
notevole importanza nell’ambito dello sviluppo di un programma non banale perché sono
impiegati per “condividere” delle informazioni lì contenute tra altri file di codice sorgente.
Queste informazioni possono riguardare, per esempio, dei nomi di macro, dei nuovi nomi
di tipo, delle dichiarazioni di funzioni, delle dichiarazioni di variabili esterne, delle
dichiarazioni di tipo struttura e così via.
Ciò detto, uno o più file di codice sorgente che dovesse avere bisogno di queste
informazioni dovrebbe avere un modo per poterle utilizzare, ossia dovrebbe poter
“incorporare” nel suo contesto quei nomi di tipo, qui prototipi di funzione e così via.
A tal fine lo standard di C mette a disposizione del programmatore una speciale direttiva
del preprocessore che consente di “includere” quei file header nell’ambito di uno specifico
file sorgente e dunque utilizzarne le relative informazioni.
Questa direttiva è denominata #include e nasce con lo scopo di poter rintracciare un file
sorgente o un file header che può essere validamente processato dalla corrente
implementazione.
NOTA
La direttiva #include, come detto, nasce per consentire di includere file di codice sorgente e
quindi nulla vieta di includere in un file .c altri file .c. Tuttavia, questa è una pratica non
utilizzata perché, in linea generale, un file .c, può contenere, oltre alla dichiarazione di
variabili esterne, anche definizioni di funzioni. Pertanto se abbiamo due file .c, per esempio
One.c che contiene la funzione main e Two.c, che entrambi includono un altro file .c con una
definizione di funzione, per esempio Three.c e la funzione foo, quando manderemo in
compilazione il programma che contiene la funzione main (One.c), unitamente all’altro file
(Two.c), il linker ci segnalerà l’errore multiple definition of 'foo'. Quanto detto ha senso solo
perché è prassi consolidata nell’ambito dello sviluppo di un programma in C usare i file .h

per scrivere codice che contiene delle dichiarazioni di funzioni e i file .c per scrivere codice
che contiene delle definizioni di funzioni.

TERMINOLOGIA
Più precisamente, possiamo dire che un file di codice sorgente per C è sia un file .c sia un
file .h. In ogni caso, è nella prassi indicare come file di codice sorgente solo i file .c e come
file di intestazione solo i file .h che sono disgiunti dai primi. Per quanto riguarda la
terminologia adottata in questo testo essa si avvarrà a volte di un significato più preciso
(come è il caso di quest’unità didattica che tratta dell’inclusione di file sorgente), mentre
altre volte di un significato più snello e pratico (come è il caso, per esempio, del Capitolo 1).

La direttiva #include
La direttiva del preprocessore #include (Sintassi 10.13, 10.14 e 10.15) permette di
incorporare, dal punto dove è definita e in un file sorgente che ne fa uso, il contenuto di
altro file sorgente da essa riferito.

Sintassi 10.13 La direttiva #include (I forma).


#include <h_char_sequence>

Con la Sintassi 10.13 si include il contenuto del file sorgente specificato tra una coppia di
parentesi angolari < > e tale contenuto sostituisce quella direttiva che viene eliminata.
La sequenza di caratteri h_char_sequence definisce generalmente il nome di un file esterno
e può contenere qualsiasi membro del corrente set di caratteri eccetto i caratteri new line e >
(parentesi angolare chiusa).
Con la direttiva #include scritta in questa forma il preprocessore cerca il file sorgente
indicato in una serie di locazioni che sono dipendenti dall’implementazione corrente la
quale, a sua volta, a seconda del sistema target, ne ha di predefinite; per esempio:
per GCC in ambienti Unix-like i percorsi di ricerca sono usualmente /usr/local/include,
/usr/include e [libdir]/gcc/[target]/[version]/include (nel nostro sistema GNU/Linux
quest’ultimo percorso è espanso come /usr/lib/gcc/x86_64-redhat-linux/4.9.2/include);
per GCC (MinGW) in ambienti Windows i percorsi di ricerca sono usualmente
C:\MinGW\lib\gcc\mingw32\[version]\include, C:\MinGW\lib\gcc\mingw32\[version]\include-

fixed (nel nostro sistema Window 8.1 [version] è espanso come 4.8.1) e
C:\MinGW\include, C:\MinGW\mingw32\include.

I file sorgente inclusi riguardano, tipicamente, i file header della libreria di funzioni dello
standard di C, e la loro corretta denominazione è quella indicata dalla relativa specifica del
linguaggio (per esempio stdio.h, math.h, assert.h, stdlib.h, time.h e così via).

Snippet 10.18 La direttiva #include (I forma).


#include <stdio.h>
#include <stdlib.h>
#include <string.h>
#include <assert.h>
#include <math.h>
#include <stdint.h>
#include <time.h>
#include <stdarg.h>
#include <complex.h>

int main(void)
{
...
}

Lo Snippet 10.18 include una serie di file header propri della libreria standard del
linguaggio C; quando il preprocessore incontrerà ciascuna direttiva #include la eliminerà
sostituendola con il contenuto effettivo che si troverà nel file specificato, in accordo anche
con quanto è lì indicato (per esempio, potrà includere o meno dello specifico codice per
effetto della valutazione delle direttive condizionali #if, #else e via discorrendo).

Sintassi 10.14 La direttiva #include (II forma).


#include "q_char_sequence"

Con la Sintassi 10.14 si include il contenuto di un file sorgente specificato tra una coppia
di doppi apici " " e tale contenuto sostituisce quella direttiva, che viene eliminata.
La sequenza di caratteri q_char_sequence definisce generalmente il nome di un file esterno
e può contenere qualsiasi membro del corrente set di caratteri eccetto i caratteri new line e "
(doppio apice).
Con la direttiva #include scritta in questa forma il preprocessore cerca il file sorgente in
un modo che è definito dall’implementazione corrente laddove, tuttavia, i correnti
compilatori tipicamente seguono questo procedimento: iniziano a cercare a partire dalla
locazione dove è presente il file sorgente che fa uso di tale direttiva, e se lì non trovano il
file da includere allora provano a cercarlo nei path utilizzati nel caso la direttiva #include
fosse stata scritta nella I forma ossia con le parentesi angolari < >.
ATTENZIONE
Con questa II forma di utilizzo della direttiva #include è possibile utilizzare dei percorsi di
ricerca prestabiliti come, per esempio, /opt/cl/my_include oppure d:\cl\my_include. È tuttavia
sconsigliato esplicitare path in modo assoluto per evitare problemi di portabilità del codice
su altri sistemi.

I file sorgente inclusi riguardano, generalmente, i file header di librerie di funzioni scritte
dal programmatore stesso e indispensabili per il corretto funzionamento del proprio
programma.

Snippet 10.19 La direttiva #include (II forma).


...
#include "stack.h"

int main(void)
{
...
}

Lo Snippet 10.19 include il file header stack.h che dovrà trovarsi, in accordo con quanto
detto, almeno nella stessa locazione del file di codice sorgente che lo sta includendo.

Sintassi 10.15 La direttiva #include (III forma).


#include pp_tokens

Con la Sintassi 10.15 si indicano dei token pre-elaborazione che sono usualmente
rappresentati dall’identificatore di una macro semplice, la cui lista di sostituzione deve
contenere dei token in grado di rappresentare una delle due forme della direttiva #include
prima citate; detto in altri termini, la macro deve espandersi con una coppia di parentesi
angolari con il nome di un header <header_name> oppure con una coppia di doppi apici con il
nome del file sorgente "file_source_name".
DETTAGLIO
Leggendo quanto appena detto potremmo chiederci perché nel caso della I forma della
direttiva #include esplicitiamo il “nome” di un header e nel caso della II forma della stessa
direttiva esplicitiamo il “nome di un file” di un sorgente. Il motivo risiede nel fatto che la
specifica di C non richiede espressamente che il nome di un header sia il nome di un file
sorgente da ricercare in uno specifico filesystem (i dettagli su come accedere a
quell’intestazione dipendono infatti dall’implementazione corrente). Resta inteso, tuttavia,
che quanto evidenziato è solo per un rigore formale e terminologico; infatti, nella prassi è
lecito indicare la sintassi della I forma della direttiva #include come #include
<file_source_name>.

Snippet 10.20 La direttiva #include (III forma).


#define VERSION 3

#if VERSION == 1
#define INC_FILE "stack_v1.h"
#elif VERSION == 2
#define INC_FILE "stack_v2.h"
#elif VERSION == 3
#define INC_FILE "stack_v3.h"
#else
#define INC_FILE "stack_beta.h"
#endif
#include INC_FILE /* sarà: #include "stack_v3.h" */

Lo Snippet 10.20 mostra come sia possibile includere un file sorgente in modo
“variabile”, ossia con un valore dipendente da quanto indicato da un lista di sostituzione di
una specifica macro. Nel nostro caso questa possibilità, definita computed include, ci ha
permesso di scrivere una sola istruzione #include piuttosto che tante istruzioni #include
ciascuna con un proprio valore di un nome di un file sorgente lì codificato direttamente.

Strutturazione di programmi complessi


Strutturare un programma complesso, ossia formato da più file di codice sorgente, è di
sicuro un’operazione non banale che richiede sia una completa padronanza e
consapevolezza di quelli che sono i costrutti e le tecniche di programmazione proprie di C
sia un’adeguata conoscenza dei principi di progettazione e di design di un programma.
Costrutti e tecniche di programmazione
Per quanto riguarda i costrutti e le tecniche di programmazione che consentono di
strutturare un programma complesso in C, essi sono strettamente legati a una corretta
comprensione di quali sono le adeguate metodologie che ci consentono di mettere a
disposizione di diversi file di codice sorgente (condividere) le dichiarazioni di variabili, di
prototipi di funzioni, di nomi di macro e così via.
Per esempio, per condividere tra più file sorgente una variabile esterna dobbiamo
compiere i seguenti passi (Listati 10.6, 10.7 e 10.8):
1. definirla come variabile esterna in un file .c assegnandole eventualmente anche un
valore iniziale.
2. dichiararla in un file .h utilizzando lo specificatore di classe extern.

Listato 10.6 A_1.h (SharingVariables).


// dichiarazione della variabile data
extern int data;

Listato 10.7 A_1.c (SharingVariables).


// inclusione dell'header A_1.h
#include "A_1.h"

// definizione della variabile data con valore iniziale 100


int data = 100;

Listato 10.8 SharingVariables.c (SharingVariables).


/* SharingVariables.c :: Utilizzo di una variabile definita altrove :: */
#include <stdio.h>
#include <stdlib.h>

// inclusione dell'header A_1.h


#include "A_1.h"

int main(void)
{
printf("Il valore di data e': %d\n", data);

return (EXIT_SUCCESS);
}

Output 10.7 Dal Listato 10.8 SharingVariables.c.


Il valore di data e': 100

Il progetto SharingVariables è costituito da tre file: A_1.h, che contiene la dichiarazione


della variabile data; A_1.c, che contiene la definizione della variabile data; SharingVariables.c,
che contiene la funzione main e che utilizza la variabile data.
Il programma funziona correttamente perché SharingVariables.c include il file header
A_1.h, che gli fornisce la dichiarazione della variabile data permettendo alla funzione printf
di utilizzare l’identificatore data senza alcun problema (infatti il programma si compilerà
senza errori).
Ricordiamo, a tal fine, che nella fase di compilazione, quella che produce solo un file
oggetto e che non fa ancora uso del linker, è sufficiente per il compilatore che vi sia una
dichiarazione valida di un identificatore di un oggetto prima del suo utilizzo.
È utile evidenziare che anche il file sorgente A_1.c include il file header A_1.h e ciò per
consentire al compilatore, durante la fase di compilazione di A_1.c, di verificare che vi sia
una corrispondenza tra la dichiarazione della variabile data e la sua definizione (se proviamo
infatti a dichiarare data come extern long data un compilatore come GCC, ci segnalerebbe il
seguente messaggio: error: conflicting types for 'data').

Per condividere, invece, tra più file sorgente una funzione dobbiamo compiere i seguenti
passi (Listati 10.9, 10.10 e 10.11):
1. definirla in un file .c, ossia scriverla indicando il relativo corpo di istruzioni;
2. dichiararla in un file .h, ossia scriverla indicando il relativo prototipo.

Listato 10.9 A_2.h (SharingFunctions).


// prototipo di foo
void foo(void);

Listato 10.10 A_2.c (SharingFunctions).


#include <stdio.h>

// inclusione dell'header A_2.h


#include "A_2.h"

// definizione di foo
void foo(void)
{
printf("Elaborazione di foo...\n");
}

Listato 10.11 SharingFunctions.c (SharingFunctions).


/* SharingFunctions.c :: Utilizzo di una funzione definita altrove :: */
#include <stdio.h>
#include <stdlib.h>

// inclusione dell'header A_2.h


#include "A_2.h"

int main(void)
{
// eseguo foo...
foo();

return (EXIT_SUCCESS);
}

Output 10.8 Dal Listato 10.11 SharingFunctions.c.


Elaborazione di foo...
Il progetto SharingFunctions è costituito da tre file: A_2.h, che contiene il prototipo della
funzione foo; A_2.c, che contiene la definizione della funzione foo; SharingFunctions.c, che
contiene la funzione main e che utilizza la funzione foo.
Anche in questo caso la funzione main del file SharingFunctions.c può lecitamente utilizzare
la funzione foo perché l’identificatore foo è lì visibile grazie all’inclusione del file header
A_2.h che ne contiene un appropriato prototipo.
Allo stesso tempo il file A_2.c include il file A_2.h, in modo che quando il compilatore
compilerà A_2.c potrà verificare una corrispondenza tra la dichiarazione e la definizione
della funzione foo.
Infine per condividere tra più file sorgente il nome di una macro, una dichiarazione
typedef, una dichiarazione di una struttura e così via è sufficiente porle in un apposito file

header e includerlo nei file .c che ne necessitano (Listati 10.12, 10.13, 10.14).

Listato 10.12 A_3.h (SharingMacroTypedefEtc).


// alcune macro semplici
#define SIZE 100
#define MSG "ELABORAZIONE IN CORSO...\n"

// una dichiarazione typedef


typedef long int BigInt;

// dichiarazione di una struttura


struct point
{
int x;
int y;
};

// dichiarazione di un enum
enum colors
{
RED = 0xF00, GREEN = 0x0F0, BLUE = 0x00F, WHITE = 0xFFF, BLACK = 0x000
};

// prototipo di setPixelAt
void setPixelAt(struct point p, enum colors c);

Listato 10.13 A_3.c (SharingMacroTypedefEtc).


#include <stdio.h>

// inclusione dell'header A_3.h


#include "A_3.h"

// definizione di setPixelAt
void setPixelAt(struct point p, enum colors c)
{
printf(MSG);
printf("Accendo il pixel alle coordinate [%d, %d] con il colore: [%X]\n",
p.x, p.y, c);
}

Listato 10.14 SharingMacroTypedefEtc.c (SharingMacroTypedefEtc).


/* SharingMacroTypedefEtc.c :: Utilizzo di una funzione definita altrove :: */
#include <stdio.h>
#include <stdlib.h>

// inclusione dell'header A_3.h


#include "A_3.h"

int main(void)
{
// uso un alias di tipo e una macro semplice definiti in A_3.h
BigInt numbers[SIZE];

// uso la struttura definita in A_3.h


struct point p = {100, 100};

// uso l'enumerazione definita in A_3.h


enum colors red = RED;

// uso setPixelAt dichiarata in A_3.h e definita in A_3.c


setPixelAt(p, red);

return (EXIT_SUCCESS);
}

Output 10.9 Dal Listato 10.14 SharingMacroTypedefEtc.c.


ELABORAZIONE IN CORSO...
Accendo il pixel alle coordinate [100, 100] con il colore: [F00]

Il progetto SharingMacroTypedefEtc è costituito da tre file: A_3.h, che contiene la definizione


di alcune macro semplici, una dichiarazione di una struttura di tipo struct point, una
dichiarazione di un’enumerazione di tipo enum colors e il prototipo di una funzione
denominata setPixelAt; A_3.c, che contiene la definizione della funzione setPixelAt;
SharingMacroTypedefEtc.c, che contiene la funzione main che utilizza i tipi, la macro e la
funzione dichiarati nel file header A_3.h per questo incluso.

Principi di progettazione e di design di un programma


Per quanto riguarda i principi di progettazione e di design di un programma, una
trattazione completa ed esaustiva esulerebbe dagli obiettivi didattici del presente libro e
pertanto proveremo solo a darne un’indicazione di massima lasciando al lettore un
eventuale approfondimento con dei testi opportuni.
Partiamo dicendo subito che un programma complesso richiede un’attenta analisi di
quello che è il suo principale obiettivo computazionale, in modo più generico possibile, per
poi elaborarlo scomponendolo in tanti sotto-obiettivi più specializzati fino ad arrivare a un
livello di granularità che ci soddisfa oppure che non ha più senso estendere.
Questi obiettivi possono essere codificati in appositi “pezzi di software” indipendenti,
usualmente indicati con il termine di moduli software, che sono però tra di loro comunicanti
perché devono consentire all’unisono il raggiungimento dell’obiettivo computazionale
principale del programma.
Ciascuno di questi moduli è tipicamente costituito da due parti di cui una si occupa di
descrivere i servizi e le funzionalità disponibili per un generico client utilizzatore, mentre
l’altra si occupa di fornire per tali servizi e funzionalità un apposito dettaglio realizzativo.
Dal punto di vista terminologico, la prima parte è denominata interfaccia mentre la
seconda parte è denominata implementazione.
Ogni linguaggio di programmazione può offrire opportuni strumenti e differenti
metodologie che consentono di definire e utilizzare tali moduli; per quanto riguarda C è
possibile realizzare un modulo software avvalendosi dei file header .h per la definizione
della parte interfaccia (conterrà, per esempio, i prototipi delle funzioni), mentre dei file di
codice sorgente .c per la definizione della parte implementazione (conterrà, per esempio, la
definizione delle funzioni prototipate).
Il client utilizzatore, invece, è semplicemente un file di codice sorgente .c che conterrà la
funzione main di ingresso del programma principale attraverso la quale si invocheranno le
funzionalità messe a disposizione dai moduli software che si desidera utilizzare.
Da questo punto di vista possiamo dire che la libreria standard del linguaggio C non è
altro che un insieme di moduli software ciascuno deputato a offrire uno specifico servizio;
per esempio, se volessimo utilizzare delle funzioni per la gestione o la manipolazione ad
alto livello delle stringhe potremmo utilizzare il relativo modulo mediante l’inclusione nel
nostro programma del file header <string.h> che ne rappresenta l’interfaccia.
Allo stesso modo, se il nostro programma avesse bisogno di funzioni specializzate per la
gestione di date o orari, potremmo utilizzare i servizi offerti da uno specifico modulo la cui
interfaccia è fornita dal file header <time.h>.
E così via il procedimento da seguire sarebbe sempre lo stesso, ossia si dovrebbe in primo
luogo individuare il modulo software necessario e poi si dovrebbe importare nel programma
la relativa interfaccia espressa da un appropriato file header.
NOTA
Per quanto riguarda la parte implementativa di ogni modulo software della libreria standard
del linguaggio C, essa è solitamente definita in una serie di appositi file oggetto che
vengono utilizzati in automatico dal linker durante la produzione del file eseguibile finale.
Per esempio, per quanto riguarda GCC, la libreria standard utilizzata è quella denominata
glibc (The GNU C Library), la quale fornisce, sotto forma di una libreria statica oppure di una
libreria dinamica, tutti i file oggetto che il linker può utilizzare. Per un dettaglio ulteriore si
consulti l’Appendice A.

La possibilità di strutturare un programma in tanti moduli software permette di ottenere i


seguenti benefici.
Riuso: ogni modulo software, se progettato in modo