Guida Linux_ Il Linguaggio C
-
Upload
tommy84mail5328 -
Category
Documents
-
view
6 -
download
1
description
Transcript of Guida Linux_ Il Linguaggio C
Il linguaggio C
21.1 Linux il linguaggio C e la programmazione
Nel sistema operativo Linux sono presenti vari linguaggi di programmazione, come il
C, il Perl, il PHP etc, ma qui ci occuperemo del linguaggio C, in quanto Linux e' stato
scritto usando il linguaggio C. Una differenza peculiare tra i sistemi Linux e Windows
e' la disponibilita' dei sorgenti dei vari progammi del sistema operativo stesso. Unsorgente e' la traduzione del linguaggio comprensibile dalla macchina in linguaggiocomprensibile dagli umani. Per capire cosa e' un sorgente, occorre partire dall'inizio,
cioe' dal microprocessore. Ogni PC contiene al suo interno una CPU (Central
Processing Unit) o microprocessore. Ogni processore puo' eseguire un numero di
istruzioni prestabilito. Si dice cioe' che ogni microprocessore possiede un
determinato set di istruzioni. Ad esempio, per spostare un valore da una parte
all'altra della memoria RAM, esiste una istruzione MOV (da MOVe, cioe' sposta), persaltare da una istruzione all'altra viene usata una istruzione JMP (da JuMP, cioe' salto)
e via dicendo. Il microprocessore Intel Pentium riconosce un insieme di istruzioni che
sono diverse da quelle riconosciute da un microprocessore AMD Athlon le quali a lorovolta sono diverse da un microprocessore Motorola 68000 e cosi' via. Ogni tipomarca e modello di microprocessore cioe', riconosce esclusivamente le istruzioni perle quali e' stato costruito. Ad ogni modo queste istruzioni alla fine sono semprecodificate in numeri binari. Ad esempio un ipotetica istruzione 'Mov reg2,reg1'potrebbe corrispondere alla serie 0100010001000100. Un programma e' un insiemedi istruzioni (o comandi) impartiti al microprocessore e puo' essere considerato indefinitiva come un file contenente una lunghissima serie di 0 e di 1. Questa serieinterminabile di zeri e di 1 viene letta e capita perfettamente dalla macchina ma nonsi puo' dire altrettando degli umani... ;o) Il microprocessore legge le istruzioni e leesegue. I primissimi programmi erano di dimensioni modeste, percio' con non pocadifficolta' si potevano scrivere direttamente in binario. Successivamente conl'aumentare delle dimensioni dei programmi questo tipo di gestione divenneimpraticabile e si passo' dal sistema binario al sistema esadecimale perche' piu'compatto. Ma in fondo anche il sistema esadecimale non e' un linguaggio da
'umani'...leggere una serie di lettere e cifre come AF01D2C43AC012FC43D non e'proprio il massimo. Ecco che allora venne creato un linguaggio mnemonico piu' vicinoagli umani: ogni istruzione che originariamente era una riga di 0 e 1 venne tradotta in
istruzioni piu' leggibili come 'mov reg2,reg1'. Il rapporto era 1 a 1, cioe' ad ogni
istruzione composta da una serie di 0 e 1 corrispondeva ora una istruzione inlinguaggio mnemonico. Questo linguaggio mnemonico si chiama assembler.
Mediante tale linguaggio la creazione dei programmi divenne un'attivita' alla portatadegli umani, ma si trattava pur sempre di una attivita' abbastanza complessa. Le
cose rimasero cosi' fino a quando divenne chiaro che alcuni insiemi di istruzioni
potevano essere raggruppati per formare delle macroistruzioni. Per eseguire unadeterminata azione infatti, si scriveva sempre lo stesso insieme di istruzioni. Ad
esempio per leggere un carattere da un file, il microprocessore deve eseguire sempredeterminate operazioni (scritte in assembler dai programmatori). Ma allora perche'non creare delle macroistruzioni composte da piu' istruzioni elementari assembler?Ad esempio una macroistruzione 'leggi-file', una 'scrivi-file', una 'verifica-condizione'
e via dicendo. Vennero cosi' alla luce i linguaggi di programmazione di seconda
generazione, cioe' quei linguaggi composti da macroistruzioni decisamente piu'leggibili dagli umani. Ora questi linguaggi evoluti, erano dotati di macroistruzioni
come read, write, move, if, tutte istruzioni intelligibili per gli umani. Read significava
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
1 di 12 03/03/2015 23:23
leggi un file, write scrivilo etc. In questi tipi di linguaggi il rapporto non e' piu' di 1 a 1
come con l'assembler ma di 1 a N. Cioe', ad ogni macroistruzione corrispondono piu'istruzioni elementari assembler. Tali linguaggi vengono definiti linguaggi di alto livello.
Successivamente vennero creati dei linguaggi di terza generazione: in sostanza piu'
passa il tempo e piu' i nuovi linguaggi si discostano dalla lingua della macchina e si
avvicinano alla lingua degli uomini. Il C e' un linguaggio di alto livello, cioe' ad ogni
istruzione scritta in C corrispondono N istruzioni elementari. Un programma scritto
mediante un linguaggio di programmazione viene definito sorgente. Un sorgentequindi e' un insieme di istruzioni impartite al microprocessore e scritte in un file di
testo. Un file contenente istruzioni in cifre binarie interpretabili dalla macchina vienedetto eseguibile o file binario. Ma come fare per tradurre questo file sorgente
leggibile dagli umani in istruzioni in linguaggio binario? Utilizzando i compilatori. Un
compilatore non e' altro che un programma che legge un sorgente e lo traduce nella
corrispondente serie interminabile di cifre binarie leggibili dal microprocessore equindi eseguibili. In sostanza quindi, un compilatore traduce un file sorgente in un file
eseguibile. Questa affermazione evidenzia lo scopo di un compilatore ma nella realta',
come al solito, le cose sono un po' piu' complesse. Infatti il compilatore non crea un
eseguibile direttamente, ma crea un file oggetto. Il file oggetto e' un file
parzialmente eseguibile. Per poter arrivare all'eseguibile vero e proprio occorre
ancora l'intervento di un altro programma: il linker (o linkage editor). Iprogrammatori nel tempo si resero conto che alcune funzioni generali venivanoripetute spesso all'interno dei loro programmi. Programmi che fanno cose diversecontengono in realta' al loro interno alcune istruzioni uguali. Ad esempio unprogramma che visualizza a video il risultato di una somma di due numeri ed unprogramma che visualizza a video il risultato di una radice quadrata sono dueprogrammi diversi. Producono due risultati diversi, hanno istruzioni diverse al lorointerno ma nonostante cio' hanno un punto in comune: visualizzano a video unrisultato. Perche' scrivere ogni volta le istruzioni necessarie per visualizzare a videoun risultato? Perche' non creare un piccolo programmino generico che effettuaquesto lavoro? Un programma che prende dei dati in input e li visualizza a video. Sipotrebbe creare un programma che legge un carattere da un file, un programma chescrive un carattere in un file e via dicendo. Si potrebbero cioe' creare dei piccoliprogrammini che effettuano una operazione generica ed utilizzarli tutte le volte cheoccorre effettuare quell'operazione all'interno di un qualsiasi programma. Infatti e'stato fatto cosi': sono stati creati parecchi programmini generici utilizzabili da
qualsiasi programmatore. Questi programmini chiamati moduli (o anche membri)
sono stati inseriti all'interno di contenitori chiamati librerie. Sono anche loro deiprogrammini parzialmente eseguibili, cioe' dei file oggetto. I programmi scritti daiprogrammatori hanno bisogno di questi programmini per poter eseguire delle
operazioni generiche (come la stampa a video) e questi programmini a loro volta
hanno bisogno di un programma che gli fornisca dei dati per svolgere il loro lavoro.Come collegare i programmi ai moduli di libreria? Questa operazione di collegamento
(link) viene effettuata dal linker. Il linker unisce, cuce, collega dei moduli oggetto tra
loro. Ogni file oggetto (il programma appena scritto ed i vari moduli di libreria)rappresenta un tassello del puzzle. Il linker unisce i vari tasselli e produce il puzzle,
cioe' il file eseguibile finale. Studiando il linguaggio C si scoprira' che esistono unamoltitudine di programmini gia' scritti e perfettamente funzionanti posti all'interno di
librerie ed utilizzabili da chiunque. Esistono svariate librerie ed ogni libreria si occupa
di un argomento particolare. Esistono librerie generiche, librerie dedicate allestringhe, librerie dedicate alle operazioni matematiche, librerie dedicate all'accesso aifile, librerie dedicate alla grafica e via dicendo. Quando utilizziamo uno di questiprogrammini gia' scritti, diciamo che utilizziamo una funzione di libreria. Ad
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
2 di 12 03/03/2015 23:23
esempio la funzione 'printf' e' un programmino che permette di visualizzare a video i
dati che gli forniamo in input. E' una funzione usata all'interno di parecchi programmi.Ma torniamo ai sorgenti: dovrebbe ormai essere chiaro che per ogni file esguibile
deve esistere da qualche parte il relativo file sorgente. Qui entra in ballo Linux. Infatti
Linux insieme ai programmi eseguibili fornisce anche i relativi sorgenti. Qualsiasi
programmatore che conosca il linguaggio C puo' esaminare Linux in profondita' per
capire quali sono le operazioni che eseguono determinati programmi. Ma un
programmatore che conosce il linguaggio C, puo' anche modificare i programmi oaddirittura riscriverli! Certo, occorre essere un programmatore esperto, ma e'
possibile. Di quali strumenti occorre disporre per scrivere un programma? Un editordi testi per scrivere il sorgente nel linguaggio di programmazione che si conosce ed il
relativo compilatore per tradurlo in formato eseguibile. Per ogni linguaggio di
programmazione esiste il relativo compilatore. In ogni distribuzione Linux viene
fornito anche un compilatore per il linguaggio C. Il compilatore C fornito nelle variedistribuzioni Linux e' il compilatore GCC (GNU Compiler Collection). Quando parliamo
genericamente di compilatore normalmente intendiamo sia il compilatore che il linker.
21.2 Un approccio insolito al C
Sin dai tempi della prima versione del libro 'Linguaggio C' di Brian W. Kerninghan eDennis M. Ritchie(creatore del C), la tradizione vuole che per trattare un linguaggio diprogrammazione si debba partire dal famoso programmino Hello World, cioe' il piu'semplice programma che visualizza a video la frase 'Hello World'. Qui pero' vorreitentare un approccio diverso: comincero' ad affrontare i tipi di dati previsti nel Cevidenziando tutti quegli aspetti che spesso sono fonte di confusione se non chiaritiadeguatamente. Il testo guida adottato qui e' il famoso libro: 'linguaggio C' di BrianW. Kerninghan e Dennis M. Ritchie (la cosidetta bibbia del C, versione in linguaoriginale: "The C Programing Language 2nd Edition (ANSI C)", B.W. Kernighan, D.M.Ritchie (1988); Prentice Hall; ISBN 0-13-110362-8. Versione tradotta in italiano:Linguaggio C, Gruppo Editoriale Jackson, ISBN 88-7056-443-6). Cerchero' dirimanere fedele alle raccomandazioni del C ANSI/ISO. ANSI e ISO sono dueorganizzazioni che si occupano di standard, ANSI sta per American National StandardInstitute mentre ISO sta per International Standard Organization. Lo standard ANSIC venne definito nel 1989 dall' American National Standard Institute e vennesuccessivamente adottato dall' International Standard Organization come standardinternazionale. Lo standard ANSI originale e' definito nel documento ANSI
X3.159-1989 (chiamato anche C89). Tale standard venne adottato dall'ISO nel
documento: ISO/IEC 9899:1990 (chiamato anche C90). Per avere maggioriinformazioni sullo stato dello standard si possono consultare i siti www.iso.ch ehttp://std.dkuug.dk/JTC1/SC22/WG14/ oppure cercare su google la stringa 'ISO/IEC
9899: 1999'. Ma perche' e' importante uno standard? Quando sviluppiamo deiprogrammi in C possiamo scrivere del codice:
conforme allo standard1.
con comportamento definito dall'implementazione (implementation defined)2. con comportamento non definito (undefined)3.
Un codice conforme allo standard verra' compilato da qualsiasi compilatore aderenteallo standard ANSI e, cosa piu' importante, sara' portabile, nel senso che, una volta
ricompilato, girera' ('girare' e' un termine gergale che significa 'essere eseguibile'.
Quindi quando si dice che un programma 'gira su Linux' significa che tale programmae' eseguibile sui sistemi Linux) su qualsiasi sistema operativo senza apporre
modifiche alcune o con poche modifiche. Viceversa, un programma non aderente allo
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
3 di 12 03/03/2015 23:23
standard ANSI non sara' portabile, nel senso che potra' girare solo sul sistema
operativo per il quale e' stato pensato. In particolare, del codice non aderente allostandard, potra' avere un comportamento definito dall'implementazione oppure
indefinito. Scrivere del codice con comportamento definito dall'implementazione
significa scrivere programmi che possono comportarsi in un modo piuttosto che in un
altro a seconda del compilatore usato. Poiche' i compilatori sono ottimizzati per il
sistema operativo per i quali sono stati scritti, ogni compilatore e' diverso da un altro.
Un compilatore ottimizzato per Linux e' scritto (implementato) in modo da produrrecodice piu' efficente per i sistemi Linux, mentre un compilatore Windows e'
implementato in modo da sfruttare al massimo le caratteristiche del sistemaoperativo Windows. Ne consegue che le stesse istruzioni scritte in linguaggio C
vengono tradotte in modo diverso a seconda del compilatore usato. Se si scrive del
codice che produce determinati risultati con un compilatore Linux e se tale codice
non e' conforme allo standard ma e' implementation defined, i risultati che si avrannoricompilando lo stesso codice con un compilatore Windows potrebbero essere
diversi. Infine, scrivere del codice dal comportamento indefinito, significa scrivere del
codice sintatticamente corretto ma semanticamente errato. Del codice con queste
caratteristiche sara' scritto seguendo le regole della grammatica del linguaggio (ed il
compilatore non produrra' errori) ma conterra' delle istruzioni logicamente insensate.
Di fronte a queste istruzioni il compilatore potra' generare un eseguibile funzionante,un eseguibile che va in crash (si pianta), un eseguibile che spegne il PC e chissa' chealtro! Da tutto questo ne segue che sarebbe meglio scrivere del codice conforme allostandard ANSI. I compilatori aderenti allo standard ANSI sono tenuti a documentarepunto per punto tutte le parti implementation defined del linguaggio. Piu' avantiverrano evidenziate quelle parti di codice che potrebbero avere un comportamentoimplementation defined o undefined (indefinito).
21.3 Variabili e costanti
Un programma e' un file di testo contenente una parte di istruzioni (la parte definitacodice) ed una parte dati. Ad esempio per effettuare una divisione ci occorre unfoglio di carta ed una penna per annotare tutti i passi ed i valori intermedi prima diottenere il risultato finale (oppure usare una calcolatrice! ;o). La stessa cosa e' validaper i programmi. Un programma, eseguendo determinate operazioni, puo' avere lanecessita' di memorizzare dei valori temporanei da qualche parte. Questi valori
vengono memorizzati nella memoria RAM all'interno delle variabili. Una variabile
puo' essere paragonata ad una scatola che puo' contenere dei valori che variano neltempo. Tale scatola puo' contenere delle penne, dei colori, delle puntine da disegno,
dei fiori etc. Quindi una variabile e' una zona della memoria RAM riservata alprogramma che puo' contenere dei valori. Supponendo di voler memorizzare il valore
2 da qualche parte nella memoria, potremmo usare una variabile 'x' ed assegnargli ilvalore 2. A questa variabile potremmo dare un nome a piacere del tipo
'mia-variabile'. Successivamente potremmo assegnare a tale variabile un altro valore
se vogliamo. Ma come viene creata una variabile nella memoria RAM? La RAM e' unamatrice di celle di memoria, dove ogni cella puo' contenere un valore. Puo essereparagonata ad un quaderno a quadretti dove all'interno di ogni quadretto si puo'inserire un valore. Il microprocessore individua ogni cella di memoria (il quadretto) in
base al suo indirizzo all'interno della RAM (il foglio) usando dei sistemi di
indirizzamento particolari. Ad esempio potrebbe individuare un quadretto tramite lecoordinate riga/colonna: la riga 37 e colonna 44 identificano insieme un preciso
quadretto all'interno del foglio. Un qualsiasi sistema di individuazione del quadrettoviene definito indirizzamento. In realta' all'interno della RAM tutto e' molto piu'
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
4 di 12 03/03/2015 23:23
complesso ed ogni cella di memoria ha un indirizzo ben preciso. Poiche' gli indirizzi di
memoria sono difficili da ricordare per gli umani, nei linguaggi di programmazionesono state inventate le variabili. Una variabile nasce quando viene eseguito un
programma e muore quando tale programma termina l'esecuzione. Una variabile
allora non e' altro che un nome simbolico al quale e' associato un indirizzo di
memoria ben preciso. Piu' precisamente il nome di una variabile viene detto
identificatore. E' il compilatore che, nella fase di compilazione (cioe quando traduce
un sorgente in file oggetto) associa al nostro nome a piacere un preciso indirizzo dimemoria. Tutte le variabili all'interno del nostro programma avranno cosi' nomi di
fantasia: pippo, pluto, topolino, mia-variabile, variabileX etc, ma ad ogni nome ilcompilatore associera' un indirizzo di memoria ben preciso (il compilatore compila
delle tabelle di simboli appunto, dove ad ogni nome o simbolo viene associato un
indirizzo di una zona di memoria RAM). Tutte le volte che faremo riferimento alla
variabile 'mia-variabile', il compilatore la cerchera' nelle sue tabelle dei simboli dallaquale leggera' l'indirizzo di memoria associato. Come al solito, nella realta' le cose
non sono cosi' semplici, perche' con un nome di variabile possiamo identificare una o
piu' celle in base al tipo di variabile. Il contrario di una variabile e' una costante,
cioe' un valore che non varia nel tempo ma anzi rimane fisso (costante) per tutta la
durata dell'esecuzione del programma. Percio' una variabile puo' variare mentre una
costante non puo' variare. Occorre aver ben chiaro questo concetto in modo daevitare confusioni quando parleremo dei puntatori e delle loro relazioni con gli array.
21.4 I tipi di dati
Abbiamo detto che un valore o meglio un oggetto occupa una o piu' celle di memoriaa seconda del tipo di oggetto. Una variabile puo' contenere solo oggetti di undeterminato tipo. E' un po' come dire che la scatola delle scarpe puo' contenere soloscarpe, la scatola dei fiammiferi puo' contenere solo fiammiferi, la scatola dellecaramelle solo caramelle e cosi' via. Insomma, ogni scatola puo' contenere solooggetti di un determinato tipo. Non esistono quindi delle variabili generiche chepossono contenere qualsiasi tipo di oggetto (in realta' nel C e' stato introdotto il tipovoid che fa riferimento ad un oggetto inesistente e quindi non associato ad alcun tipoin particolare). Ma vediamo quali erano i tipi di dati ammessi in C inizialmente:
tipi fondamentali:
unsigned char
signed char
unsigned short int
signed short int
unsigned int
signed int
unsigned long int
signed long int
float
doublelong double
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
5 di 12 03/03/2015 23:23
void
enum
tipi derivati:
array
puntatori
struttureunion
Successivamente a seguito di varie revisioni degli standard, vennero aggiunti nuovi
tipi. Consultando lo standard ISO/IEC 9899/1999 si rilevano infatti questi nuovi tipi:
bool
long long int (intero a 64 bit)
float _Complex
double _Complex
long double _Complex
Le dimensioni dei tipi fissate dallo standard ANSI non sono rigide in quanto possonovariare a seconda dell'implementazione, cioe' ogni implementazione deve attenersi ailimiti minimi ma puo' aumentarli se lo ritiene necessario. Ad esempio il tipo int puo'avere una dimensione che varia da 2 byte (16 bit) a 4 byte (32 bit) in base allamacchina. Per conoscere la dimensione di un tipo nella macchina che si sta usando,e' possibile usare l'operatore sizeof messo a disposizione dal C. In seguito vedremol'uso di tale operatore. Ad ogni modo esistono alcune regole fissate dallo standardANSI che devono essere rispettate da tutti i compilatori conformi a tale standard. Inparticolare gli short e gli int devono essere di almeno 2 byte ed i long di 4. Inoltreuno short non puo' essere piu' grande di un int ed un int non puo' essere piu' grandedi un long. I valori minimi e massimi consentiti e legati al compilatore sono definitiall'interno di 2 file: limits.h e float.h. In generale si ha:
char ≤ short ≤ int ≤ long
In ogni caso i tipi numerici char, short, int, long possono essere signed o unsigned,cioe' con o senza segno. I tipi senza segno, in base alla loro ampiezza possono
assumere i seguenti valori:
1 byte (es. un char) = da 0 a 255
2 byte (es. uno short) = da 0 a 32.7674 byte (es. un long) = da 0 a 4.294.967.296
Se tali tipi fossero al contrario segnati si avrebbe:
1 byte (es. un char) = da -128 a 127
2 byte (es. uno short) = da -32768 a 32.7674 byte (es. un long) = da -2.147.483.648 a 2.147.483.647
In generale l'intervallo di valori esprimibile da un tipo dipende dalla sua lunghezza e,limitatamente alla rappresentazione dei dati in complemento a due, e' dato da:
-2n-1...2n-1-1
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
6 di 12 03/03/2015 23:23
Dove n rappresenta il numero di bit utilizzati. Ad esempio un tipo di dato con
ampiezza di 2 byte utilizzera' 16 bit (2*8) pertanto il valore di n sara' 16. In realta' lostandard ISO/IEC 9899: 1999 pone dei limiti inferiori, infatti un char ad esempio
varia da -127 a 127 ed uno short da -32767 a 32767. E' bene far riferimento allo
standard se l'obiettivo e' la portabilita' ed ai file limits.h e float.h per conoscere il
range di valori esatto consentito dal compilatore se non si e' interessati alla
portabilita'. I tipi char, a dispetto del nome sono in realta' dei piccoli interi e possono
contenere un intervallo di numeri da -128 a 127 se segnati o da 0 a 255 se senzasegno. Cio' puo' essere dimostrato dal codice seguente perfettamente legale:
char c;
c = 'A';
c = c * 3;
Inizializzare una variabile di tipo char con la costante carattere 'A' infatti, equivale
inizializzarla con il numero 65 se stiamo lavorando su un sistema che utilizza lacodifica ASCII (ma puo' equivalere ad un altro numero, come ad esempio 193 se
stiamo lavorando su un sistema che utilizza la codifica EBCDIC). Occorre far
attenzione agli apici: una costante carattere e' delimitata dagli apici singoli mentre gli
apici doppi delimitano una stringa costante. Una curiosita': se stiamo lavorando su
un sistema Unix/Linux abbiamo 3 tipi di apici a disposizione: gli apici singoli, gli apicidoppi e gli apici inversi. Ognuno ha un significato specifico. I tipi float double e longdouble, sono tipi di dato in virgola mobile (floating point, cioe' punto fluttuante, datala notazione inglese inversa alla nostra secondo la quale la virgola decimale e'rappresentata dal punto, mentre per rappresentare le decine, le centinaia, le migliaiaetc si usa la virgola) sono un po' diversi e meritano una trattazione separata. Quandotrattiamo numeri con decimali (cioe' non interi), numeri molto grandi o numeri moltopiccoli, possiamo utilizzare la rappresentazione in virgola mobile. Ad esempio setrattiamo dati come i dati finanziari di una azienda, la distanza tra la luna e la terra, lamassa di un elettrone, gli abitanti di una nazione e cosi' via. I calcoli in virgola mobilevengono eseguiti dalla FPU (Floating Point Unit) una sorta di calcolatore specializzatoper questi calcoli ed integrato nella CPU. Inizialmente i calcolatori possedevano laCPU ed un coprocessore matematico utilizzato per questo genere di calcoli.Attualmente il coprocessore e' integrato nella CPU (FPU) (le applicazioni 3D sono unesempio di applicazioni che fanno intenso uso di calcoli in virgola mobile). Secondotale rappresentazione un numero puo' essere rappresentato in questi modi:
0,001 oppure 1*10-3 oppure 1E-3
Queste 3 notazioni si equivalgono. Percio' 1E-38 equivale a 1*10-38 (notazione
scientifica), cioe': 0,00000000000000000000000000000000000001 (spero di avercontato correttamente 38 zeri ;o). I numeri in virgola mobile possono essere aprecisione singola oppure a precisione doppia. I float sono numeri in virgola
mobile a singola precisione, mentre i double sono a doppia precisione. Nellaprecisione singola vengono usati 32 bit (4 byte) mentre nella precisione doppia 64 (8
byte). Esiste poi la precisione doppia estesa che usa 80 bit (10 byte) e laprecisione quadrupla che usa 128 bit. Nella notazione a virgola mobile il numeroviene scomposto in 2 parti: la mantissa (o significante) e l'esponente. La mantissa
contiene le cifre piu' significative del numero, mentre l'esponente indica la posizionedella virgola all'interno del numero stesso (da qui la denominazione di virgola
'mobile'). Nella precisione singola si hanno a disposizione 32 bit di cui 23 dedicati allamantissa 1 al segno ed i rimanenti 8 bit sono dedicati all'esponente. Nella precisione
doppia si hanno a disposizione 64 bit di cui 52 per la mantissa, 1 per il segno ed i
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
7 di 12 03/03/2015 23:23
rimanenti 11 per l'esponente. Questo come stabilito dallo standard IEEE 754
(Institute of Electrical and Electronics Engineers, una associazione senza scopo dilucro formata da piu' di 380.000 membri appartenenti a 150 paesi che si occupano di
elettronica, telecomunicazioni, computer etc). E' importante saper gestire i tipi di
dato a virgola mobile per evitare di commettere errori. Nel passato alcuni errori sui
tipi a virgola mobile sono stati fatali. 4 giugno 1996: viene lanciato Arianne 5 a
Kourou. Dopo 36 secondi il razzo cambio' rotta e si autodistrusse. Cosa era
accaduto? Un sistema di riferimento inerziale cerco' di convertire un numero a 64 bitin virgola mobile in un numero a 16 bit in virgola fissa. Poiche' il numero di partenza
era troppo grande per essere contenuto nel numero di arrivo, venne generato unerrore di overflow che provoco' l'invio di un messaggio diagnostico al computer di
bordo. Questo messaggio di errore venne interpretato erroneamente come un dato di
volo corretto. Tornando alla notazione scientifica, abbiamo visto che un numero puo'
essere espresso nella forma: 1*10n. Se cio' e' vero allora le forme:
1260
1260*100
126,0*101
12,60*102
1,260*103
0,126*104
12600*10-1
126000*10-2
1260000*10-3
...
...
sono tutte equivalenti (ricordiamo che N*B-E equivale a N/B E, ad esempio: 2*10-2
equivale a 2/102, cioe' 2 diviso 100, il quale da' come risultato finale 0,02). Questopuo' creare dei problemi in quanto non esiste un modo unico che identifica unostesso numero. Occorre percio' definire uno standard che non crei dubbi sulla formautilizzata per rappresentare un numero. A tale scopo viene utilizzata la notazionescientifica normalizzata adottata dallo standard IEEE 754. Secondo tale standard
per convenzione la prima cifra intera prima della virgola e' diversa da zero pertantose ragioniamo in base 2 (i calcolatori lo fanno) la prima cifra intera prima della virgolanon puo' che essere 1 (nello standard IEEE 754 la cifra 1 viene omessa in quanto
sottintesa). Sempre secondo tale standard l'esponente e' polarizzato, cioe' se si
hanno 8 bit a disposizione per l'esponente, un bit e' riservato al segno e quindi i
valori possibili con un esponente di 8 bit saranno compresi tra 2-127 e 2127 (poiche'un bit e' riservato al segno si ha 8-1=7, quindi con 7 bit a disposizione si possono
avere 27 combinazioni diverse, cioe' 127). Data una mantissa 'm' ed un esponente 'e'si potranno rappresentare i seguenti intervalli di valori:
-∞oooo-m eoooooooooooooooooooo-m-eoooooZEROooooo
m-eoooooooooooooooooooo m eoooo ∞
L' intervallo dei valori rappresentabili nella modalita' in virgola mobile e' evidenziato in
rosso i rimanenti intervalli non sono rappresentabili. In particolare gli intervallievidenziati in giallo rappresentano l'overflow, mentre gli intervalli evidenziati in
verde rappresentano l'underflow. La differenza tra underflow ed overflow e'
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
8 di 12 03/03/2015 23:23
importante in quanto in caso di errori di underflow il calcolatore puo' porre rimedio
esprimendo con lo zero il valore infinitesimale che e' uscito dal range disponibile (e siottiene un errore di approssimazione) mentre in caso di errori di overflow il
calcolatore non e' in grado di porre rimedio (non e' possibile approssimare
introducendo un valore infinito) percio' si blocca il calcolo ed il programma termina in
errore. E' da notare che la distanza tra due numeri in virgola mobile vicini tra loro
tende ad aumentare con l'aumentare del valore del numero e tende a diminuire verso
lo zero. Cioe' la distribuzione dei numeri non e' costante. In altre parole si ha unararefazione all'aumentare del valore del numero e viceversa un addensamento con il
diminuire del valore. Diminuendo il valore la distanze si accorciano e i numeri siavvicinano tra loro mentre aumentando il valore le distanze si allungano ed i numeri
si allontanano tra loro:
-∞---|-|-|-|-|-
ZERO-|-|-|-|--|---|-----|------|---------|----------|---------------|-------------------->∞
Ad ogni modo la percentuale di errore e' costante nei vari intervalli. La mantissa
rappresenta la precisione del valore rappresentato, cioe' il numero di cifre
significative del valore rappresentato: maggiore e' il numero delle cifre dellamantissa maggiore sara' la precisione ottenuta. Poiche' un numero binario equivale in
decimale al numero moltiplicato per 0,301, avremo che 2127 equivale circa a 1038 inquanto 127 * 0,301 = 38,227. Leggendo i valori contenuti nel file float.h l'intervallo divalori ammissibile dai tipi in virgola mobile della mia implementazione e':
float = da -2 128 a -2-125 e da 2-125 a 2 128
double = da -2 1024 a -2-1021 e da da 2-1021 a 2 1024
long double = da -2 16384 a -2-16381 e da 2-16381 a 2 16384
Oppure in decimale:
float = da -10 38 a -10-37 e da 10-37 a 10 38
double = da -10 308 a -10-307 e da 10-307 a 10 308
long double = da -10 4932 a -10-4931 e da 10-4931 a 10 4932
Il massimo valore della parte intera esprimibile, e' in realta la precisione massimaottenibile, mentre il numero massimo della parte frazionaria (esponente) indica
l'intervallo massimo di numeri rappresentabili. Togliendo un bit alla mantissa lo si
puo' aggiungere all'esponente e viceversa. Per conoscere l'intervallo di valori interiammissibili o, piu' esattamente il numero di cifre della parte intera esprimibile,
occorre far riferimento ancora una volta allo standard IEEE 754. Secondo talestandard i tipi float (precisione singola) dispongono di 32 bit di cui 24 sono dedicati
alla parte intera (in realta' 23 perche' uno e' dedicato al segno). Cio' significa che il
valore massimo esprimibile (limitatamente alla parte intera) e' di circa 223, ossia
8388608, quindi 6 cifre 'piene' e circa 7 cifre 'quasi' piene (esattamente fino a8388608) a disposizione. Cio' significa che per elaborare un numero di 8 cifre una
variabile di tipo float non e' sufficiente. Lo stesso ragionamento e' applicabile ai
double (precisione doppia) ed ai long double (precisione estesa). Per i double,sempre secondo lo standard IEEE 754 i bit disponibili per la parte intera sono 52 (53
- 1 bit dedicato al segno) mentre per i long double sono 63 (64 - 1). Eseguendo lostesso calcolo visto per i float sara' evidente che con i double saranno disponibili 15
cifre per la parte intera (fino a 252 = 4503599627370496) e con i long double 18
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
9 di 12 03/03/2015 23:23
cifre (fino a 252 = 9223372036854775808). Comunque questi valori sono indicativi,
in quanto l'esatto intervallo e la precisione dipende dall'implementazione ed e'
ricavabile esclusivamente dai file limits.h, float.h e values.h. Un compilatore diverso
su una macchina diversa, potrebbe fornire valori diversi. Ecco la tabella dei tipi a
virgola mobile secondo lo standard IEEE 754:
formato bit usati bit della mantissa bit dell'esponente bit del segno
float 32 23 8 1
double 64 52 11 1
long double 80 64 15 1
Per verificare i limiti delle variabili in virgola mobile supportate dal compilatore e'
possibile utilizzare il seguente programma:
#include<float.h>
#include<stdio.h>
int main(void)
{
float f;
double x;
long double ld;
printf("\nValori minimo e massimo rappresentabili: \n");
printf(" float minimo : %e\n", FLT_MIN);
printf(" double minimo : %.15le\n", DBL_MIN);
printf(" long double minimo : %.30Le\n", LDBL_MIN);
printf(" float massimo : %e\n", FLT_MAX);
printf(" double massimo : %.15le\n", DBL_MAX);
printf(" long double massimo: %.30Le\n", LDBL_MAX);
printf("\n");
return 0;
}
Lo standard ISO/IEC 9899:1990 in realta' pone dei limiti minimi (in valore assoluto)che ogni implementazione deve rispettare, ma ogni implementazione e' libera di
aumentare tali valori (sempre in valore assoluto):
float = da 1E-37 a 1E 37double = da 1E-37 a 1E 37
long double = da 1E-37 a 1E 37
Attenzione a non confondere il long double con il nuovo long long. Il long double e'un tipo a virgola mobile a doppia precisione estesa di 80 bit (10 byte di cui 64 per lamantissa, 15 per l'esponente ed 1 per il segno) mentre il long long e' un intero a 64
bit (8 byte). Il long long ammette un intervallo di valori da -263 a 263-1. Anche per
questi tipi vale il discorso dell'implementazione: lo standard definisce i limiti minimi
che devono essere rispettati, ma ogni implementazione e' libera di aumentarli. Infatti
su alcuni sistemi esistono dei char da 64 bit (cioe' lunghi 8 byte) oppure dei longdouble lunghi 128 bit. La verita' in un ipotetico sistema X, con un sistema operativo Y
ed un compilatore Z e' data solo dai file limits.h, float.h e values.h. Lo standard adogni modo definisce i limiti minimi ai quali qualsiasi compilatore deve attenersi:
occorre seguire le direttive dello standard se l'obiettivo e' la portabilita'. Secondo lo
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
10 di 12 03/03/2015 23:23
standard IEEE 754 esistono 4 tipi di dato in virgola mobile: a precisione singola (4
byte), a doppia precisione (8 byte), a doppia precisione estesa (10 byte) e aquadrupla precisione (16 byte). Lo standard definisce la precisione quadrupla un tipo
non specificato dallo IEEE 754 ma lo riconosce come uno standard de facto. Gli altri
due tipi di oggetti fondamentali sono il tipo void ed il tipo enum. Il tipo void (che
significa vuoto) significa nessun tipo. Attenzione a non confondere NULL con il tipo
void: il primo e' un valore nullo il secondo e' un tipo di oggetto. Il tipo void e' utile
per vari scopi che vedremo in seguito. Il tipo enum e' un tipo di costanteenumerativa. Un'enumerazione e' un elenco di costanti intere come ad esempio:
enum giorni { LUN = 1, MAR = 2, MER = 3, GIO = 4, VEN = 5, SAB = 6, DOM = 7 )
oppure:
enum boolean { TRUE, FALSE )
oppure:
enum giorni { LUN = 1, MAR, MER, GIO, VEN, SAB, DOM )
Nel primo caso ogni costante ha un valore specificato esplicitamente, nel secondocaso implicitamente la prima costanta ha valore 0 e la seconda 1 (una terza costanteavrebbe valore 2, una quarta 3, una quinta 4 e cosi' via) e nell'ultimo caso i valorinon specificati continuano la progressione a partire dall'ultimo specificato(nell'esempio sopra MAR vale 2, MER vale 3 e cosi' via). Una alternativa al tipo enume la direttiva #define che vedremo in seguito. I tipi derivati (vettori, strutture,puntatori e union) verranno illustrati in seguito. Alcuni testi annoverano tra i tipianche le funzioni in quanto sono pur sempre oggetti in memoria e possono ancheessere indirizzati attraverso i puntatori ma personalmente preferisco considerarle unostrumento per scrivere dei piccoli blocchi di istruzioni richiamabili in vari punti delprogramma e che possono ritornare dei valori; un po' come le procedure del Pascal,le perform del Cobol o le subroutines del Fortran o del Basic (le gosub).
21.5 Ancora sulle costanti
Abbiamo visto che il contrario di una variabile e' una costante, cioe' un valore che
non varia nel tempo ma anzi rimane fisso (costante) per tutta la durata
dell'esecuzione del programma. Le costanti possono essere intere, carattere ostringa. Una costante intera come 123 e' una costante di tipo int. Una costante interadi tipo long e' seguita dal carattere 'l' o 'L' come ad esempio 123456789L. Le costanti
senza segno sono seguita da una 'u' o 'U' come ad esempio 123456789UL (che
corrisonde ad un tipo unsigned long). Una costante a virgola mobile termina con ilcarattere 'f' o 'F' come ad esempio 123.4 (ricordiamo che il punto corrisponde alla
nostra virgola decimale) e corrisponde al tipo float. Una costante a virgola mobile di
tipo long double termina con una 'l' o 'L'. Una costante intera puo essere espressa indecimale, in ottale oppure in esadecimale. Un prefisso '0x' o '0X' indica la notazione
esadecimale come ad esempio 0X1F (che corrisponde al numero decimale 31). Unozero prefisso ad un intero indica la notazione ottale, ad esempio il numero di prima
puo' essere scritto come 037. Percio' 0X1F, 037 e 31 si equivalgono e corrispondono
al numero decimale 31. Una costante carattere puo' rappresentare dei caratteri dicontrollo particolari come carattere backspace, il carattere di tabulazione od ilcarattere new line attraverso la cosidetta sequenza di escape. Una sequenza diescape e' un sistema per indicare dei caratteri speciali utilizzando come prefisso il
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
11 di 12 03/03/2015 23:23
carattere '\' (backslash o barra inversa). Ecco l'elenco delle sequenze di escape:
\a (campanello)\b (backspace)
\f (form feed o salto pagina)
\n (new line o nuova riga)
\r (return o ritorno carrello)
\t (tab o carattere di tabulazione)
\v (tab verticale)\\ (backslash o barra inversa)\? (punto interrogativo)
\' (apice singolo)
\" (apice doppio)
\ooo (numero ottale)
\xhh (numero esadecimale)
La costante carattere '\0' rappresenta il carattere nullo cioe' con valore zero, da non
confondere con NULL. La stringa costante e' una serie do zero o piu' caratteri
racchiusi tra doppi apici. Internamente ogni stringa e' terminata dal carattere nullo
'\0'. Attenzione ancora una volta che '\0' e "\0" non sono la stessa cosa, in quanto laprima espressione identifica un carattere mentre la seconda una stringa. Un esempiodi stringa puo' essere "pippo" oppure "paperino pippo e pluto". Una stringacostante in memoria viene rappresentata come un vettore di caratteri.Poiche' ogni stringa costante termina con il carattere '\0', lo spazio occupato inmemoria e' maggiore di un carattere. Ad esempio la stringa "ciao" occupa 5 caratteriin quanto in memoria viene rappresentata cosi': "ciao\0". '\0' e' un carattere e nondue in quanto il carattere backslash (barra inversa) in realta' e' un metacarattere,cioe' rappresenta un informazione relativamente al carattere vero e proprio. Adesempio 'b' e '\b' sono due cose diverse in quanto il primo rappresenta il carattere 'b'mentre il secondo rappresenta il carattere di controllo backspace (cioe' il tasto chepermette di tornare indietro nella digitazione del testo cancellando). Infine esistono leespressioni costanti. Un esempio di espressione costante e' la seguente: #defineMASSIMO 1000. Questa e' una direttiva per il precompilatore che all'atto dellacompilazione sostituira' la parola MASSIMO con il valore 1000 in qualsiasi punto delprogramma venga trovata. L'utilita' di queste espressioni constanti e' chiara:
impostato un valore costante poniamo a 1000, se successivamente si rendesse
necessario portare tale valore a 2000, sarebbe sufficiente modificare una sola riga (lariga #define MASSIMO 1000 diventerebbe #define MASSIMO 2000). Viceversa, senzal'uso delle espressioni costanti, occorrerebbe modificare tutte le righe che presentino
il valore costante 1000 per portarlo a 2000.
Inizio della guida Precedente Indice Il primo programma in C
Copyright (c) 2002-2003 Maurizio Silvestri
Guida Linux: il linguaggio C http://www.wowarea.com/italiano/linux/linguaggioc01.htm
12 di 12 03/03/2015 23:23