[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]


153. Linguaggio C: introduzione

Il linguaggio C è il fondamento dei sistemi Unix, e così anche di GNU/Linux che dispone di ANSI C GNU. Un minimo di conoscenza di questo linguaggio è importante per sapersi districare tra i programmi distribuiti in forma sorgente.

Il linguaggio C richiede la presenza di un compilatore per generare un file eseguibile (o interpretabile) dal kernel. Se si dispone dei cosiddetti «strumenti di sviluppo», intendendo con questo ciò che serve a ricompilare il kernel, si ha a disposizione tutto quello che è necessario per provare gli esempi di questi capitoli.

153.1 Struttura fondamentale

Il contenuto di un sorgente in linguaggio C può essere suddiviso in tre parti: commenti, direttive del preprocessore e istruzioni C. I commenti vanno aperti e chiusi attraverso l'uso dei simboli `/*' e `*/'.

153.1.1 Direttive del preprocessore

Le direttive del preprocessore rappresentano un linguaggio che guida alla compilazione del codice vero e proprio. L'uso più comune di queste direttive viene fatto per includere porzioni di codice sorgente esterne al file. È importante fare attenzione a non confondersi, dal momento che tali istruzioni iniziano con il simbolo `#': non si tratta di commenti.

Il tipico programma C richiede l'inclusione di codice esterno composto da file che terminano con l'estensione `.h'.

La tipica libreria che viene inclusa è quella necessaria alla gestione dei flussi di standard input, standard output e standard error, e si dichiara il suo utilizzo nel modo seguente:

#include <stdio.h>

153.1.2 Istruzioni C

Le istruzioni C terminano con un punto e virgola (`;') e i raggruppamenti di queste si fanno utilizzando le parentesi graffe (`{ }').

<istruzione>;

{<istruzione>; <istruzione>; <istruzione>; }

Generalmente, un'istruzione può essere interrotta e ripresa nella riga successiva, dal momento che la sua conclusione è dichiarata chiaramente dal punto e virgola finale. L'istruzione nulla viene rappresentata utilizzando un punto e virgola da solo.

153.1.3 Nomi

I nomi scelti per identificare ciò che si utilizza all'interno del programma devono seguire regole determinate, definite dal compilatore C a disposizione. Per cercare di scrivere codice portabile in altre piattaforme, conviene evitare di sfruttare caratteristiche speciali del proprio ambiente. In particolare:

La lunghezza dei nomi può essere un elemento critico; generalmente la dimensione massima dovrebbe essere di 32 caratteri, ma ci sono versioni di C che ne possono accettare solo una quantità inferiore. In particolare, C GNU ne accetta molti di più di 32. In ogni caso, il compilatore non rifiuta i nomi troppo lunghi, semplicemente non ne distingue più la differenza oltre un certo punto.

153.1.4 Funzione principale

Il codice di un programma C è scomposto in funzioni, e l'esecuzione del programma corrisponde alla chiamata della funzione `main()'. Questa funzione può essere dichiarata senza argomenti oppure con due argomenti precisi: `main(int argc, char *argv[])'.

153.2 Ciao mondo!

Come sempre, il modo migliore per introdurre a un linguaggio di programmazione è di proporre un esempio banale, ma funzionante. Al solito si tratta del programma che emette un messaggio e poi termina la sua esecuzione.

/*
 *	Ciao mondo!
 */

#include <stdio.h>

/* La funzione main() viene eseguita automaticamente all'avvio. */
main() {
    /* Si limita a emettere un messaggio. */
    printf("Ciao mondo!\n");
}

Nel programma sono state inserite alcune righe di commento. In particolare, all'inizio, l'asterisco che si trova nella seconda riga non serve a nulla, se non a guidare la vista verso la conclusione del commento stesso.

Il programma si limita a emettere la stringa «Ciao Mondo!» seguita da un codice di interruzione di riga, rappresentato dal simbolo `\n'.

153.2.1 Compilazione

Per compilare un programma scritto in C si utilizza generalmente il comando `cc', anche se di solito si tratta di un collegamento simbolico al vero compilatore che si ha a disposizione. Supponendo di avere salvato il file dell'esempio con il nome `ciao.c', il comando per la sua compilazione è il seguente:

cc ciao.c[Invio]

Quello che si ottiene è il file `a.out' che dovrebbe già avere i permessi di esecuzione.

./a.out[Invio]

Ciao mondo!

Se si desidera compilare il programma definendo un nome diverso per il codice eseguibile finale, si può utilizzare l'opzione standard `-o'.

cc -o ciao ciao.c[Invio]

Con questo comando, si ottiene l'eseguibile `ciao'.

./ciao[Invio]

Ciao mondo!

153.2.2 Emissione dati attraverso printf()

L'esempio di programma presentato sopra si avvale di `printf()' per emettere il messaggio attraverso lo standard output. Questa funzione è più sofisticata di quanto possa apparire dall'esempio, in quanto permette di formattare il risultato da emettere. Negli esempi più semplici di codice C appare immancabilmente questa funzione, per cui è necessario descrivere subito, almeno in parte, il suo funzionamento.

int printf( <stringa-di-formato> [, <espressione>]... )

`printf()' emette attraverso lo standard output la stringa indicata come primo parametro, dopo averla rielaborata in base alla presenza di metavariabili riferite alle eventuali espressioni che compongono i parametri successivi. Restituisce il numero di caratteri emessi.

L'utilizzo più semplice di `printf()' è quello che è già stato visto, cioè l'emissione di una semplice stringa senza metavariabili (il codice `\n' rappresenta un carattere preciso e non è una metavariabile, piuttosto si tratta di una cosiddetta sequenza di escape).

    printf("Ciao mondo!\n");

La stringa può contenere delle metavariabili del tipo `%d', `%c', `%f',... e queste fanno ordinatamente riferimento ai parametri successivi. Per esempio,

    printf("Totale fatturato: %d\n", 12345 );

fa in modo che la stringa incorpori il valore indicato come secondo parametro, nella posizione in cui appare `%d'. La metavariabile `%d' stabilisce anche che il valore in questione deve essere trasformato secondo una rappresentazione decimale intera. Per cui, il risultato sarà esattamente quello che ci si aspetta.

Totale fatturato: 12345

153.3 Variabili e tipi

I tipi di dati elementari gestiti dal linguaggio C dipendono molto dall'architettura dell'elaboratore sottostante. In questo senso, volendo fare un discorso generale, è difficile definire la dimensione delle variabili numeriche; si può solo dare delle definizioni relative. Solitamente, il riferimento è dato dal tipo numerico intero (`int') la cui dimensione in bit è data dalla dimensione della parola, ovvero dalla capacità dell'unità aritmetico-logica del microprocessore. In pratica, con l'architettura i386 la dimensione di un intero normale è di 32 bit.

153.3.1 Tipi primitivi

I tipi di dati primitivi rappresentano un valore numerico singolo, nel senso che anche il tipo `char' può essere trattato come un numero. Il loro elenco essenziale si trova nella tabella 153.1.

Tipo Descrizione
char Carattere (generalmente di 8 bit).
int Intero normale.
float Virgola mobile a singola precisione.
double Virgola mobile a doppia precisione.

Tabella 153.1: Elenco dei tipi di dati primitivi elementari in C.

Come già accennato, non si può stabilire in modo generale quali siano le dimensioni esatte in bit dei vari tipi di dati, si può solo stabilire una relazione tra loro.

char <= int <= float <= double

Questi tipi primitivi possono essere estesi attraverso l'uso di alcuni qualificatori: `short', `long' e `unsigned'. I primi due si riferiscono alla dimensione, mentre l'ultimo modifica il modo di valutare il contenuto di alcune variabili. La tabella 153.2 riassume i vari tipi primitivi con le combinazioni dei qualificatori.

Tipo Abbreviazione Descrizione
char
unsigned char Tipo `char' usato numericamente senza segno.
short int short Intero più breve di `int'.
unsigned short int unsigned short Tipo `short' senza segno.
int Intero normale.
unsigned int unsigned Tipo `int' senza segno.
long int long Intero più lungo di `int'.
unsigned long int unsigned long Tipo `long' senza segno.
float
double
long double Tipo a virgola mobile più lungo di `double'.

Tabella 153.2: Elenco dei tipi di dati primitivi in C assieme ai qualificatori.

Così, il problema di stabilire le relazioni di dimensione si complica

char <= short <= int <= long

			float <= double <= long double

I tipi `long' e `float' potrebbero avere una dimensione uguale, altrimenti non è detto quale dei due sia più grande.

Il programma seguente, potrebbe essere utile per determinare la dimensione dei vari tipi primitivi nella propria piattaforma. *1*

/* dimensione_variabili */

#include <stdio.h>

main() {
    printf( "char        %d\n", (int)sizeof(char) );
    printf( "short       %d\n", (int)sizeof(short) );
    printf( "int         %d\n", (int)sizeof(int) );
    printf( "long        %d\n", (int)sizeof(long) );
    printf( "float       %d\n", (int)sizeof(float) );
    printf( "double      %d\n", (int)sizeof(double) );
    printf( "long double %d\n", (int)sizeof(long double) );
}

Il risultato potrebbe essere quello seguente:

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

I numeri rappresentano la quantità di caratteri, nel senso di valori `char', per cui il tipo `char' dovrebbe sempre avere una dimensione unitaria.

153.3.1.1 Valori contenibili

I tipi primitivi di variabili mostrati sono tutti utili alla memorizzazione di valori numerici, a vario titolo. A seconda che il valore in questione sia trattato con segno o senza segno, varia lo spettro di valori che possono essere contenuti.

Nel caso di interi (`char', `short', `int' e `long'), la variabile può essere utilizzata per tutta la sua estensione a contenere un numero binario. In pratica, il massimo valore ottenibile è (2**n)-1, dove n rappresenta il numero di bit a disposizione. Quando invece si vuole trattare il dato come un numero con segno, il valore numerico massimo ottenibile è circa la metà.

Nel caso di variabili a virgola mobile, non c'è più la possibilità di rappresentare esclusivamente valori senza segno, e non c'è più un limite di dimensione, ma di approssimazione.

Le variabili `char' sono fatte, in linea di principio, per contenere il codice di rappresentazione di un carattere, secondo la codifica utilizzata nel sistema. Generalmente si tratta di un dato di 8 bit (1 byte), ma non è detto che debba sempre essere così. A ogni modo, il fatto che questa variabile possa essere gestita in modo numerico, permette una facile conversione da lettera a codice numerico corrispondente.

Un tipo di valore che non è stato ancora visto è quello logico: Vero è rappresentato da un qualsiasi valore numerico diverso da zero, mentre Falso corrisponde a zero.

153.3.2 Costanti letterali

Quasi tutti i tipi di dati primitivi, hanno la possibilità di essere rappresentato in forma di costante letterale. In particolare, si distingue tra:

Per esempio, 123 è generalmente una costante `int', mentre 123.0 è una costante `double'.

Per quanto riguarda le costanti che rappresentano numeri con virgola, si può usare anche la notazione scientifica. Per esempio, `7e+15' rappresenta l'equivalente di 7*(10**15), cioè un sette con 15 zeri. Nello stesso modo, `7e-5', rappresenta l'equivalente di 7*(10**-5), cioè 0.00007.

È possibile rappresentare anche le stringhe in forma di costante, e questo attraverso l'uso degli apici doppi, ma la stringa non è un tipo di dati primitivo, trattandosi piuttosto di un array di caratteri. Per il momento è importante fare attenzione a non confondere il tipo `char' con la stringa. Per esempio, `'F'' è un carattere, mentre `"F"' è una stringa, e la differenza è notevole. Le stringhe verranno descritte meglio in seguito.

153.3.2.1 Caratteri speciali

È stato affermato che si possono rappresentare i caratteri singoli in forma di costante, utilizzando gli apici singoli come delimitatore, e che per rappresentare una stringa si usano invece gli apici doppi. Alcuni caratteri non hanno una rappresentazione grafica e non possono essere inseriti attraverso la tastiera.

In questi casi, si possono usare tre tipi di notazione: ottale, esadecimale e simbolica. In tutti i casi si utilizza la barra obliqua inversa (`\') come carattere di escape, cioè come simbolo per annunciare che ciò che segue immediatamente deve essere interpretato in modo particolare.

La notazione ottale usa la forma `\ooo', dove ogni lettera o rappresenta una cifra ottale. A questo proposito, è opportuno notare che se la dimensione di un carattere fosse superiore ai fatidici 8 bit, occorrerebbero probabilmente più cifre (una cifra ottale rappresenta un gruppo di 3 bit).

La notazione esadecimale usa la forma `\xhh', dove h rappresenta una cifra esadecimale. Anche in questo caso vale la considerazione per cui ci vorranno più di due cifre esadecimali per rappresentare un carattere più lungo di 8 bit.

Dovrebbe essere logico, ma è il caso di osservare che la corrispondenza dei caratteri con i rispettivi codici numerici dipende dalla codifica utilizzata. Generalmente si utilizza la codifica ASCII, riportata anche nella sezione 125.1.

La notazione simbolica permette di fare riferimento facilmente a codici di uso comune, quali <CR>, <HT>,... Inoltre, questa notazione permette anche di indicare caratteri che altrimenti verrebbero interpretati in maniera differente dal compilatore. La tabella 153.3 riporta i vari tipi di rappresentazione delle costanti carattere attraverso codici di escape.

Codice di escape Descrizione
\ooo Notazione ottale.
\xhh Notazione esadecimale.
\\ Una singola barra obliqua inversa (`\').
\' Un apice singolo destro.
\" Un apice doppio.
\0 Il codice <NUL>.
\a Il codice <BEL> (bell).
\b Il codice <BS> (backspace).
\f Il codice <FF> (formfeed).
\n Il codice <LF> (linefeed).
\r Il codice <CR> (carriage return).
\t Una tabulazione orizzontale (<HT>).
\v Una tabulazione verticale (<VT>).

Tabella 153.3: Elenco dei modi di rappresentazione delle costanti carattere attraverso codici di escape.

Nell'esempio introduttivo, è già stato visto l'uso della notazione `\n' per rappresentare l'inserzione di un codice di interruzione di riga alla fine del messaggio di saluto.

    printf("Ciao mondo!\n");

Senza di questo, il cursore resterebbe a destra del messaggio alla fine dell'esecuzione di quel programma, ponendo lì l'invito.

153.3.3 Campo d'azione delle variabili

Il campo d'azione delle variabili in C viene determinato dalla posizione in cui queste vengono dichiarate e dall'uso di particolari qualificatori. Per il momento basti tenere presente che quanto dichiarato all'interno di una funzione ha valore locale per la funzione stessa, mentre quanto dichiarato al di fuori, ha valore globale per tutto il file.

153.3.4 Dichiarazione delle variabili

La dichiarazione di una variabile avviene specificando il tipo e il nome della variabile, come nell'esempio seguente dove si dichiara la variabile `numero' di tipo intero.

int numero;

La variabile può anche essere inizializzata contestualmente, assegnandogli un valore, come nell'esempio seguente in cui viene dichiarata la stessa variabile `numero' con il valore iniziale di 1000.

int numero = 1000;

153.3.4.1 Costanti simboliche

Una costante è qualcosa che non varia e generalmente si rappresenta attraverso una notazione che ne definisce il valore. Tuttavia, a volte può essere più comodo definire una costante in modo simbolico, come se fosse una variabile, per facilitarne l'utilizzo e la sua identificazione all'interno del programma. Si ottiene questo con il modificatore `const'. Ovviamente, è obbligatorio inizializzala contestualmente alla sua dichiarazione. L'esempio seguente dichiara la costante simbolica `pi' con il valore del P-greco.

const float pi = 3.14159265;

Le costanti simboliche di questo tipo, sono delle variabili per le quali il compilatore non concede che avvengano delle modifiche.

È il caso di osservare, tuttavia, che l'uso di costanti simboliche di questo tipo è piuttosto limitato. Generalmente è preferibile utilizzare delle macro definite e gestite attraverso il preprocessore. L'utilizzo di queste verrà descritto più avanti.

153.3.4.2 Convenzioni necessarie

Una caratteristica fondamentale del linguaggio C è quella di permettere di fare qualsiasi operazione con qualsiasi tipo di dati. In pratica, per esempio, il compilatore non si oppone di fronte all'assegnamento di un valore numerico a una variabile `char' o all'assegnamento di un carattere a un intero. Però ci possono essere situazioni in cui cose del genere accadono accidentalmente, e il modo migliore per evitarlo è quello di usare una convenzione nella definizione dei nomi delle variabili, in modo da distinguerne il tipo. A puro titolo di esempio viene proposto il metodo seguente, che non fa parte però di uno standard accettato universalmente.

Si possono comporre i nomi delle variabili utilizzando un prefisso composto da una o più lettere minuscole che serve a descriverne il tipo. Nella parte restante si possono usare iniziali maiuscole per staccare visivamente i nomi composti da più parole significative.

Per esempio, `iLivello' potrebbe essere la variabile di tipo `int' che contiene il livello di qualcosa. Nello stesso modo, `ldIndiceConsumo' potrebbe essere una variabile di tipo `long double' che rappresenta l'indice del consumo di qualcosa. *2*

In questa fase non sono ancora stati mostrati tutti i tipi di dati che si possono gestire effettivamente; tuttavia, per completezza, viene mostrata la tabella 153.4 con tutti questi prefissi proposti.

Prefisso Tipo corrispondente
c `char'
uc `unsigned char'
si `short int'
usi `unsigned short int'
i `int'
ui `unsigned int'
li `long int'
uli `unsigned long int'
f `float'
d `double'
ld `long double'
a array
ac array di `char', o stringa
auc array di `unsigned char'
a... array di ...
acz stringa terminata con `\0'
t `struct'
u `union'
p puntatore
pc puntatore a `char'
puc puntatore a `unsigned char'
p... puntatore a ...
e enumerazione

Tabella 153.4: Convenzione proposta per i nomi delle variabili.

153.4 Operatori ed espressioni

L'operatore è qualcosa che esegue un qualche tipo di funzione, su uno o due operandi, e restituisce un valore. Il valore restituito è di tipo diverso a seconda degli operandi utilizzati. Per esempio, la somma di due interi genera un risultato intero. Gli operandi descritti di seguito sono quelli più comuni e importanti.

153.4.1 Operatori aritmetici

Gli operatori che intervengono su valori numerici sono elencati nella tabella 153.5.

Operatore e operandi Descrizione
++<op> Incrementa di un'unità l'operando prima che venga restituito il suo valore.
<op>++ Incrementa di un'unità l'operando dopo averne restituito il suo valore.
--<op> Decrementa di un'unità l'operando prima che venga restituito il suo valore.
<op>-- Decrementa di un'unità l'operando dopo averne restituito il suo valore.
+<op> Non ha alcun effetto.
-<op> Inverte il segno dell'operando.
<op1> + <op2> Somma i due operandi.
<op1> - <op2> Sottrae dal primo il secondo operando.
<op1> * <op2> Moltiplica i due operandi.
<op1> / <op2> Divide il primo operando per il secondo.
<op1> % <op2> Modulo: il resto della divisione tra il primo e il secondo operando.
<var> = <valore> Assegna alla variabile il valore alla destra.
<op1> += <op2> `<op1> = <op1> + <op2>'
<op1> -= <op2> `<op1> = <op1> - <op2>'
<op1> *= <op2> `<op1> = <op1> * <op2>'
<op1> /= <op2> `<op1> = <op1> / <op2>'
<op1> %= <op2> `<op1> = <op1> % <op2>'

Tabella 153.5: Elenco degli operatori aritmetici e di quelli di assegnamento relativi a valori numerici.

153.4.2 Operatori di confronto e operatori logici

Gli operatori di confronto determinano la relazione tra due operandi. Il risultato dell'espressione composta da due operandi posti a confronto è di tipo booleano, rappresentabile in C come !0, o non-zero (Vero), e 0 (Falso). È importante sottolineare che qualunque valore diverso da zero, equivale a Vero in un contesto logico. Gli operatori di confronto sono elencati nella tabella 153.6.

Operatore e operandi Descrizione
<op1> == <op2> Vero se gli operandi si equivalgono.
<op1> != <op2> Vero se gli operandi sono differenti.
<op1> < <op2> Vero se il primo operando è minore del secondo.
<op1> > <op2> Vero se il primo operando è maggiore del secondo.
<op1> <= <op2> Vero se il primo operando è minore o uguale al secondo.
<op1> >= <op2> Vero se il primo operando è maggiore o uguale al secondo.

Tabella 153.6: Elenco degli operatori di confronto. Le metavariabili indicate rappresentano gli operandi e la loro posizione.

Quando si vogliono combinare assieme diverse espressioni logiche, comprendendo in queste anche delle variabili che contengono un valore booleano, si utilizzano gli operatori logici (noti normalmente come: AND, OR, NOT, ecc.). Il risultato di un'espressione logica complessa è quello dell'ultima espressione elementare a essere valutata. Gli operatori logici sono elencati nella tabella 153.7.

Operatore e operandi Descrizione
! <op> Inverte il risultato logico dell'operando.
<op1> && <op2> Se il risultato del primo operando è Falso non valuta il secondo.
<op1> || <op2> Se il risultato del primo operando è Vero non valuta il secondo.

Tabella 153.7: Elenco degli operatori logici. Le metavariabili indicate rappresentano gli operandi e la loro posizione.

Un tipo particolare di operatore logico è l'operatore condizionale, che permette di eseguire espressioni diverse in relazione al risultato di una condizione. La sua sintassi si esprime nel modo seguente:

<condizione> ? <espressione1> : <espressione2>

In pratica, se l'espressione che rappresenta la condizione si avvera, viene eseguita la prima espressione che segue il punto interrogativo, altrimenti viene eseguita quella che segue i due punti.

153.4.3 Operatori binari

In C, così come non esiste il tipo di dati booleano, non esiste nemmeno la possibilità di gestire variabili composte da un bit singolo. A questo problema si fa fronte attraverso l'utilizzo dei tipi di dati esistenti in modo binario. Sono disponibili le operazioni elencate nella tabella 153.8.

Operatore e operandi Descrizione
<op1> & <op2> AND bit per bit.
<op1> | <op2> OR bit per bit.
<op1> ^ <op2> XOR bit per bit (OR esclusivo).
<op1> << <op2> Spostamento a sinistra di <op2> bit.
<op1> >> <op2> Spostamento a destra di <op2> bit.
~<op1> Complemento a uno.
<op1> &= <op2> `<op1> = <op1> & <op2>'
<op1> |= <op2> `<op1> = <op1> | <op2>'
<op1> ^= <op2> `<op1> = <op1> ^ <op2>'
<op1> <<= <op2> `<op1> = <op1> << <op2>'
<op1> >>= <op2> `<op1> = <op1> >> <op2>'
<op1> ~= <op2> `<op1> = ~<op2>'

Tabella 153.8: Elenco degli operatori binari. Le metavariabili indicate rappresentano gli operandi e la loro posizione.

In particolare, lo spostamento può avere effetti differenti a seconda che venga utilizzato su una variabile senza segno o con segno, e quest'ultimo caso può dare risultati diversi su piattaforme differenti. Per questo, verrà mostrato solo il caso dello spostamento su variabili senza segno.

Per aiutare a comprendere il meccanismo vengono mostrati alcuni esempi. In particolare si utilizzano due operandi di tipo `char' (a 8 bit) senza segno:

AND

c = a & b

`c' conterrà il valore 34, come mostrato dallo schema seguente:

00101010 (42) AND
00110011 (51) =
-------------
00100010 (34)
OR

c = a | b

`c' conterrà il valore 59, come mostrato dallo schema seguente:

00101010 (42) OR
00110011 (51) =
-------------
00111011 (59)
XOR

c = a ^ b

`c' conterrà il valore 25, come mostrato dallo schema seguente:

00101010 (42) XOR
00110011 (51) =
-------------
00011001 (25)
Spostamento a sinistra

c = a << 1

`c' conterrà il valore 84, come mostrato dallo schema seguente:

00101010 (42) <<
00000001 (1)  =
-------------
01010100 (84)

In pratica si è ottenuto un raddoppio.

Spostamento a destra

c = a >> 1

`c' conterrà il valore 21, come mostrato dallo schema seguente:

00101010 (42) >>
00000001 (1)  =
-------------
00010101 (21)

In pratica si è ottenuto un dimezzamento.

Complemento

c = ~a

`c' conterrà il valore 213, corrispondente all'inversione dei bit di `a'.

00101010 (42)
11010101 (213)

153.4.4 Conversione di tipo

Quando si assegna un valore a una variabile, nella maggior parte dei casi, il contesto stabilisce il tipo di questo valore in modo corretto. Di fatto, è il tipo della variabile ricevente che stabilisce la conversione necessaria. Tuttavia, il problema si può porre durante la valutazione di un'espressione.

Per esempio, 5/4 viene considerata la divisione di due interi, e di conseguenza l'espressione restituisce un valore intero, cioè 1. Diverso sarebbe se si scrivesse 5.0/4.0, perché in questo caso si tratterebbe della divisione tra due numeri a virgola mobile (per la precisione, di tipo `double') e il risultato è un numero a virgola mobile.

Quando si pone il problema di risolvere l'ambiguità si utilizza esplicitamente la conversione del tipo, attraverso un cast.

(<tipo>) <espressione>

In pratica, si deve indicare tra parentesi il nome del tipo di dati in cui deve essere convertita l'espressione che segue. Il problema sta nella precedenza che ha il cast nell'insieme degli altri operatori, e in generale conviene utilizzare altre parentesi per chiarire la relazione che ci deve essere.

Esempi

int x = 10;
long y;
...
y = (long)x/9;

In questo caso, la variabile intera `x' viene convertita nel tipo `long' (a virgola mobile) prima di eseguire la divisione. Dal momento che il cast ha precedenza sull'operazione di divisione, non si pongono problemi, inoltre, la divisione avviene trasformando implicitamente il 9 intero in un 9 di tipo `long'. In pratica, l'operazione avviene utilizzando valori `long' e restituendo un risultato `long'.

153.4.5 Espressioni multiple

Un'istruzione, cioè qualcosa che termina con un punto e virgola, può contenere diverse espressioni separate da una virgola. Tenendo presente che in C l'assegnamento di una variabile è anche un'espressione, che restituisce il valore assegnato, si veda l'esempio seguente:

int x;
int y;
...
y = 10, x = 20, y = x*2;

L'esempio mostra un'istruzione contenente tre espressioni: la prima assegna a `y' il valore 10, la seconda assegna a `x' il valore 20, e la terza sovrascrive `y' assegnandole il risultato del prodotto `x*2'. In pratica, alla fine la variabile `y' contiene il valore 40 e `x' contiene 20.

153.5 Strutture di controllo di flusso

Il linguaggio C gestisce praticamente tutte le strutture di controllo di flusso degli altri linguaggi di programmazione, compreso go-to che comunque è sempre meglio non utilizzare, e qui, volutamente, non viene presentato.

Le strutture di controllo permettono di sottoporre l'esecuzione di una parte di codice alla verifica di una condizione, oppure permettono di eseguire dei cicli, sempre sotto il controllo di una condizione. La parte di codice che viene sottoposta a questo controllo, può essere una singola istruzione, oppure un gruppo di istruzioni. Nel secondo caso, è necessario delimitare questo gruppo attraverso l'uso delle parentesi graffe.

Dal momento che è comunque consentito di realizzare un gruppo di istruzioni che in realtà ne contiene una sola, probabilmente è meglio utilizzare sempre le parentesi graffe, in modo da evitare equivoci nella lettura del codice. Dato che le parentesi graffe sono usate nel codice C, se queste appaiono nei modelli sintattici indicati, queste fanno parte delle istruzioni e non della sintassi.

153.5.1 if

La struttura condizionale è il sistema di controllo fondamentale dell'andamento del flusso delle istruzioni.

if ( <condizione> ) <istruzione>

if ( <condizione> ) <istruzione> else <istruzione>

Se la condizione si verifica, viene eseguita l'istruzione (o il gruppo di istruzioni) seguente, e quindi il controllo passa alle istruzioni successive alla struttura. Se viene utilizzata la sotto-struttura che si articola a partire dalla parola chiave `else', nel caso non si verifichi la condizione, viene eseguita l'istruzione che ne dipende. Sotto vengono mostrati alcuni esempi.

int iImporto;
...
if ( iImporto > 10000000 ) printf( "L'offerta è vantaggiosa\n" );

---------

int iImporto;
int iMemorizza;
...
if ( iImporto > 10000000 ) {
	iMemorizza = iImporto;
	printf( "L'offerta è vantaggiosa\n" );
} else {
	printf( "Lascia perdere\n" );
}

---------

int iImporto;
int iMemorizza;
...
if ( iImporto > 10000000 ) {
	iMemorizza = iImporto;
	printf( "L'offerta è vantaggiosa\n" );
} else if ( iImporto > 5000000 ) {
	iMemorizza = iImporto;
	printf( "L'offerta è accettabile\n" );
} else {
	printf( "Lascia perdere\n" );
}

153.5.2 switch

La struttura di selezione, che si attua con l'istruzione `switch', è un po' troppo complessa per essere rappresentata facilmente attraverso uno schema sintattico. In generale, questa struttura permette di eseguire una o più istruzioni in base al risultato di un'espressione. L'esempio seguente mostra la visualizzazione del nome del mese, in base al valore di un intero.

int iMese;
...
switch (iMese) {
    case 1: printf( "gennaio\n" ); break;
    case 2: printf( "febbraio\n" ); break;
    case 3: printf( "marzo\n" ); break;
    case 4: printf( "aprile\n" ); break;
    case 5: printf( "maggio\n" ); break;
    case 6: printf( "giugno\n" ); break;
    case 7: printf( "luglio\n" ); break;
    case 8: printf( "agosto\n" ); break;
    case 9: printf( "settembre\n" ); break;
    case 10: printf( "ottobre\n" ); break;
    case 11: printf( "novembre\n" ); break;
    case 12: printf( "dicembre\n" ); break;
}

Come si vede, dopo l'istruzione con cui si emette il nome del mese attraverso lo standard output, viene richiesta l'interruzione esplicita dell'analisi della struttura, attraverso l'istruzione `break', e questo serve a togliere ambiguità al codice, garantendo che sia evitata la verifica degli altri casi.

Un gruppo di casi può essere raggruppato assieme, quando si vuole che ognuno di questi esegua lo stesso insieme di istruzioni.

int iAnno;
int iMese;
int iGiorni;
...
switch (iMese) {
    case 1:
    case 3:
    case 5:
    case 7:
    case 8:
    case 10:
    case 12:
	iGiorni = 31;
	break;
    case 4:
    case 6:
    case 9:
    case 11:
	iGiorni = 30;
	break;
    case 2:
	if ( ((iAnno % 4 == 0) && !(iAnno % 100 = 0)) ||
	        (iAnno % 400 == 0) )
	    iGiorni = 29;
	else
	    iGiorni = 28;
	break;
}

È anche possibile definire un caso predefinito che si verifica quando nessuno degli altri si avvera.

int iMese;
...
switch (iMese) {
    case 1: printf( "gennaio\n" ); break;
    case 2: printf( "febbraio\n" ); break;
    ...
    case 11: printf( "novembre\n" ); break;
    case 12: printf( "dicembre\n" ); break;
    default: printf( "mese non corretto\n" ); break;
}

153.5.3 while

while ( <condizione> ) <istruzione>

L'iterazione si ottiene normalmente in C attraverso l'istruzione `while', che esegue un'istruzione, o un gruppo di queste, finché la condizione continua a restituire il valore Vero. La condizione viene valutata prima di eseguire il gruppo di istruzioni e poi ogni volta che termina un ciclo, prima dell'esecuzione del successivo.

L'esempio seguente fa apparire per 10 volte la lettera «x».

int iContatore = 0;

while (iContatore < 10) {
    iContatore++;
    printf( "x" );
}
printf( "\n" );

Nel blocco di istruzioni di un ciclo `while', ne possono apparire alcune particolari:

L'esempio seguente è una variante del calcolo di visualizzazione mostrato sopra, modificato in modo da vedere il funzionamento dell'istruzione `break'. All'inizio della struttura, `while (1)' equivale a stabilire che il ciclo è senza fine, perché la condizione è sempre vera. In questo modo, solo la richiesta esplicita di interruzione dell'esecuzione della struttura (attraverso l'istruzione `break') permette l'uscita da questa.

int iContatore = 0;

while (1) {
    if (iContatore >= 10) {
	break;
    }
    iContatore++;
    printf( "x" );
}
printf( "\n" );

153.5.4 do-while

Una variante del ciclo `while', in cui l'analisi della condizione di uscita avviene dopo l'esecuzione del blocco di istruzioni che viene iterato, è definito dall'istruzione `do'.

do <blocco-di-istruzioni> while ( <condizione> );

In questo caso, si esegue un gruppo di istruzioni una volta, e poi se ne ripete l'esecuzione finché la condizione restituisce il valore Vero.

153.5.5 for

In presenza di iterazioni in cui si deve incrementare o decrementare una variabile a ogni ciclo, si usa preferibilmente la struttura `for', che in C permetterebbe un utilizzo più ampio di quello comune.

for ( <espressione1>; <espressione2>; <espressione3> ) <istruzione>

Questa è la forma tipica di un'istruzione `for', in cui la prima espressione corrisponde all'assegnamento iniziale di una variabile, la seconda a una condizione che deve verificarsi fino a che si vuole che sia eseguita l'istruzione (o il gruppo di istruzioni), e la terza all'incremento o decremento della variabile inizializzata con la prima espressione. In pratica, potrebbe esprimersi nella sintassi seguente:

for ( <var> = n; <condizione>; <var>++ ) <istruzione>

Il ciclo `for' potrebbe essere definito anche in maniera differente, più generale: la prima espressione viene eseguita una volta sola all'inizio del ciclo; la seconda viene valutata all'inizio di ogni ciclo e il gruppo di istruzioni viene eseguito solo se il risultato è Vero; l'ultima viene eseguita alla fine dell'esecuzione del gruppo di istruzioni, prima che si ricominci con l'analisi della condizione.

L'esempio già visto, in cui veniva visualizzata per dieci volte una «x», potrebbe tradursi nel modo seguente, attraverso l'uso di un ciclo `for'.

int iContatore;

for ( iContatore = 0; iContatore < 10; iContatore++ ) {
    printf( "x" );
}
printf( "\n" );

Anche nelle istruzioni controllate da un ciclo `for' si possono collocare istruzioni `break' e `continue', con lo stesso significato visto per il ciclo `while'

Sfruttando la possibilità di inserire più espressioni in una singola istruzione, si possono realizzare dei cicli `for' molto più complessi, anche se però questo è sconsigliabile per evitare di scrivere codice troppo difficile da interpretare. In questo modo, l'esempio precedente potrebbe essere ridotto a quello che segue:

int iContatore;

for ( iContatore = 0; iContatore < 10; printf( "x" ), iContatore++ ) {
    ;
}
printf( "\n" );

Il punto e virgola solitario rappresenta un'istruzione nulla.

153.6 Funzioni

Il linguaggio C offre le funzioni come mezzo per realizzare la scomposizione del codice in subroutine. Prima di poter essere utilizzate attraverso una chiamata, le funzioni devono essere dichiarate, anche se non necessariamente descritte. In pratica, se si vuole indicare nel codice una chiamata a una funzione che viene descritta più avanti, occorre almeno dichiararne il prototipo.

Le funzioni del linguaggio C prevedono il passaggio di parametri solo per valore, e soltanto di tipi primitivi (compresi i puntatori che verranno descritti nel prossimo capitolo).

Il linguaggio C offre un gran numero di funzioni interne, che vengono importate nel codice attraverso l'istruzione `#include' del preprocessore. In pratica, in questo modo si importa la parte di codice necessaria alla dichiarazione e descrizione di queste funzioni standard. Per esempio, come si è già visto, per poter utilizzare la funzione `printf()' si deve inserire la riga `#include <stdio.h>' nella parte iniziale del file sorgente.

153.6.1 Dichiarazione di un prototipo

<tipo> <nome> ([<tipo-parametro>[,...]]);

Quando la descrizione di una funzione può essere fatta solo dopo l'apparizione di una sua chiamata, occorre dichiararne il prototipo all'inizio, secondo la sintassi appena mostrata.

Il tipo, posto all'inizio, rappresenta il tipo di valore che la funzione restituisce. Se la funzione non deve restituire alcunché, si utilizza il tipo `void'. Se la funzione richiede dei parametri, il tipo di questi deve essere elencato tra le parentesi tonde. L'istruzione con cui si dichiara il prototipo termina regolarmente con un punto e virgola.

Lo standard C ANSI stabilisce che una funzione che non richiede parametri deve utilizzare l'identificatore `void' in modo esplicito, all'interno delle parentesi.

Esempi

int fattoriale( int );

In questo caso, viene dichiarato il prototipo della funzione `fattoriale', che richiede un parametro di tipo `int' e restituisce anche un valore di tipo `int'.

void elenca();

Si tratta della dichiarazione di una funzione che fa qualcosa senza bisogno di ricevere alcun parametro e senza restituire alcun valore (void).

void elenca( void );

Esattamente come nell'esempio precedente, solo che è indicato in modo esplicito il fatto che la funzione non riceve argomenti (il tipo `void' è stato messo all'interno delle parentesi), come richiede lo standard ANSI.

153.6.2 Descrizione di una funzione

La descrizione della funzione, rispetto alla dichiarazione del prototipo, aggiunge l'indicazione dei nomi da usare per identificare i parametri e naturalmente le istruzioni da eseguire. Le parentesi graffe che appaiono nello schema sintattico fanno parte delle istruzioni necessarie.

<tipo> <nome> ([<tipo> <parametro>[,...]]) {<istruzione>;... }

Per esempio, la funzione seguente esegue il prodotto tra i due parametri forniti e ne restituisce il risultato.

int prodotto( int x, int y ) {
    return x*y;
}

I parametri indicati tra parentesi, rappresentano una dichiarazione di variabili locali che conterranno inizialmente i valori usati nella chiamata. Il valore restituito dalla funzione viene definito attraverso l'istruzione `return', come si può osservare dall'esempio. Naturalmente, le funzioni di tipo `void', cioè quelle che non devono restituire alcun valore, non hanno questa istruzione.

153.6.2.1 Variabili locali e globali

Le variabili dichiarate all'interno di una funzione, oltre a quelle dichiarate implicitamente come mezzo di trasporto dei parametri, sono visibili solo al suo interno, mentre quelle dichiarate al di fuori, dette globali, sono accessibili a tutte le funzioni. Se una variabile locale ha un nome coincidente con quello di una variabile globale, allora, all'interno della funzione, quella variabile globale non sarà accessibile.

Le regole da seguire per scrivere programmi chiari, e facilmente modificabili, prevedono che si debba fare in modo di rendere le funzioni indipendenti dalle variabili globali, fornendo loro tutte le informazioni necessarie attraverso i parametri della chiamata. In questo modo diventa del tutto indifferente il fatto che una variabile locale vada a mascherare una variabile globale, e oltre a questo permette di non dover tenere a mente il ruolo di queste variabili globali.

In pratica, ci sono situazioni in cui può avere senso l'utilizzo di variabili globali per fornire informazioni alle funzioni, tuttavia occorre giudizio, come in ogni cosa.

153.7 Struttura e campo d'azione

Un programma scritto in linguaggio C può essere articolato in diversi file sorgenti, all'interno dei quali si può fare riferimento solo a «oggetti» dichiarati preventivamente. Questi oggetti sono variabili e funzioni, e la loro dichiarazione non corrisponde necessariamente con la loro descrizione che può essere collocata altrove, nello stesso file o in un altro file sorgente del programma.

153.7.1 Funzioni

Quando si vuole fare riferimento a una funzione descritta in un file sorgente differente, o in una posizione successiva dello stesso file, occorre dichiararne il prototipo in una posizione precedente. Se si desidera fare in modo che una funzione sia accessibile solo nel file sorgente in cui viene descritta, occorre definirla come `static'.

static void miafunzione(...) {
...
}

153.7.2 Variabili e classi di memorizzazione

Quando si dichiarano delle variabili, senza specificare alcuna classe di memorizzazione (cioè quando lo si fa normalmente come negli esempi visti fino a questo punto), il loro campo d'azione è relativo alla posizione della dichiarazione:

Si distinguono quattro tipi di classi di memorizzazione, a cui corrisponde una parola chiave per la loro dichiarazione:

La prima, `auto', è la classe normale: vale in modo predefinito e non occorre indicarla quando si dichiarano le variabili (variabili automatiche).

Una variabile dichiarata come appartenente alla classe `register', viene posta in un registro del microprocessore. Ciò può essere utile per velocizzare l'esecuzione di un programma che deve accedere frequentemente a una certa variabile, ma generalmente l'utilizzo di questa tecnica è sconsigliabile.

La classe di memorizzazione `static' genera due situazioni distinte, a seconda della posizione in cui viene dichiarata la variabile. Se si tratta di una variabile globale, cioè definita al di fuori delle funzioni, risulterà accessibile solo all'interno del file sorgente in cui viene descritta. Se invece si tratta di una variabile locale, cioè interna a una funzione, si tratta di una variabile che mantiene il suo valore tra una chiamata e l'altra. In questo senso, una variabile locale statica, richiede generalmente un'inizializzazione all'atto della dichiarazione; tale inizializzazione avverrà una sola volta, all'avvio del programma.

Quando da un file sorgente si vuole accedere a variabili globali dichiarate in modo normale in un altro file, oppure, quando nello stesso file si vuole poter accedere a variabili dichiarate in una posizione più avanzata dello stesso, occorre una sorta di prototipo delle variabili: la dichiarazione `extern'. In questo modo si informa esplicitamente il compilatore e il linker della presenza di queste.

Esempi

int accumula( int iAggiunta ) {
    static int iAccumulo = 0;
    iAccumulo += iAggiunta;
    return iAccumulo;
}

La funzione appena mostrata si occupa di accumulare un valore e di restituirne il livello raggiunto a ogni chiamata. Come si può osservare, la variabile statica `iAccumulo' viene inizializzata a zero, altrimenti non ci sarebbe modo di cominciare con un valore di partenza corretto.

---------

static int iMiaVariabile;
...
int miafunzione(...) {
...
}
...

La variabile `iMiaVariabile' è accessibile solo alle funzioni descritte nello stesso file in cui questa si trova, impedendo l'accesso a questa da parte di funzioni di altri file attraverso la dichiarazione `extern'.

---------

extern int iMiaVariabile;
...
int miafunzione(...) {
    iMiaVariabile = ...
}
int iMiaVariabile = 123;
...

In questo esempio, la variabile `iMiaVariabile' è dichiarata formalmente in una posizione centrale del file sorgente; per fare in modo che la funzione `miafunzione' possa accedervi, è stata necessaria la dichiarazione `extern' iniziale.

---------

extern int iTuaVariabile;
...
int miafunzione(...) {
    iTuaVariabile = ...
}
...

Questo caso rappresenta la situazione in cui una variabile dichiarata in un altro file sorgente diventa accessibile alle funzioni del file attuale attraverso la dichiarazione `extern'. Perché ciò possa funzionare, occorre che la variabile `iTuaVariabile' sia stata dichiarata in modo normale, senza la parola chiave `static'.

153.8 I/O elementare

Con il linguaggio C, l'I/O elementare si ottiene attraverso l'uso di due funzioni fondamentali: `printf()' e `scanf()'. La prima si occupa di emettere una stringa dopo averla trasformata in base a determinati codici di formattazione; la seconda si occupa di ricevere input (generalmente da tastiera) e di trasformarlo secondo determinati codici di formattazione. Infatti, il primo problema che si incontra quando si vogliono emettere informazioni attraverso lo standard output per visualizzarle sullo schermo, sta nella necessità di convertire in qualche modo tutti i tipi di dati che non siano già di tipo `char'. Dalla parte opposta, quando si inserisce un dato che non sia un semplice carattere alfanumerico, occorre una conversione adatta nel tipo di dati corretto.

Per utilizzare queste due funzioni, occorre includere il file di intestazione `stdio.h', come è già stato visto più volte.

153.8.1 printf()

int printf( <stringa-di-formato>[, <espressione>]... )

`printf()' emette attraverso lo standard output la stringa indicata come primo parametro, dopo averla rielaborata in base alla presenza di metavariabili riferite alle eventuali espressioni che compongono i parametri successivi. Restituisce il numero di caratteri emessi.

In pratica, se viene fornito a `printf()' un solo parametro di tipo stringa, questa viene emessa così com'è, senza trasformazioni. Se invece vengono forniti anche altri parametri, questi verranno inclusi nella stringa attraverso una serie di metavariabili inserite nella stringa stessa, e in corrispondenza dei punti in cui si trovano tali metavariabili, queste verranno sostituite dal contenuto dei parametri corrispondenti. Per esempio,

printf( "Il capitale di %d al tasso %f ha fruttato %d", 1000, 0.05, 1050 );

emette la frase seguente:

Il capitale di 1000 al tasso 0.05 ha fruttato 1050

In pratica, al posto della prima metavariabile `%d' è stato inserito il valore 1000 dopo averlo convertito in modo da essere rappresentato da quattro caratteri (`'1'', `'0'', `'0'', `'0''), al posto della seconda metavariabile `%f' è stato inserito il valore 0.05 dopo un'opportuna conversione in caratteri, e infine, al posto della terza metavariabile `%d' è stato inserito il valore 1050.

La scelta della metavariabile corretta determina il tipo di trasformazione che il parametro corrispondente deve ricevere. La tabella 153.9 elenca alcune delle metavariabili utilizzabili. È necessario ricordare che per rappresentare il simbolo di percentuale si usa una metavariabile fasulla composta dalla sequenza di due segni percentuali: `%%'.

Simbolo Corrispondenza
%c Un carattere singolo.
%s Una stringa.
%d Un intero con segno a base 10.
%u Un intero senza segno a base 10.
%o Un intero senza segno in ottale.
%x Un intero senza segno in esadecimale.
%e Un numero a virgola mobile, in notazione scientifica.
%f Un numero a virgola mobile, in notazione decimale fissa.
%g Un numero a virgola mobile, secondo la notazione di `%e' o `%f'.

Tabella 153.9: Alcune metavariabili utilizzabili per la formattazione di stringhe con `printf()'.

Le metavariabili possono contenere informazioni aggiuntive tra il simbolo di percentuale e la lettera che definisce il tipo di trasformazione. Si tratta di inserire un simbolo composto da un carattere singolo, seguito eventualmente da informazioni aggiuntive, secondo la sintassi seguente:

%[<simbolo>][<ampiezza>][.<precisione>][{h|l|L}]<tipo>

Questi simboli sono rappresentati dalla tabella 153.10. In presenza di valori numerici, si può indicare il numero di cifre decimali intere (ampiezza), ed eventualmente il numero di decimali (precisione), se si tratta di rappresentare un numero a virgola mobile. Sempre nel caso di trasformazioni di valori numerici, è anche possibile specificare il tipo particolare a cui appartiene il dato immesso, attraverso una lettera: `h', `l' e `L'. Queste indicano rispettivamente che si tratta di un intero `short', `long' e `double'; se manca questa indicazione, si intende che si tratti di un intero normale (`int').

Simbolo Corrispondenza
spazio Il prefisso di un numero positivo è uno spazio.
+ Il prefisso di un numero positivo è il segno `+'.
- Allinea a sinistra rispetto al campo.
0 Utilizza zeri, invece di spazi, per allineare a destra.

Tabella 153.10: Elenco dei simboli utilizzabili tra il segno di percentuale e la lettera di conversione.

Nella stringa di formattazione possono apparire anche sequenze di escape come già mostrato nella tabella 153.3.

Si veda anche la pagina di manuale printf(3).

153.8.2 scanf()

int scanf( <stringa-di-formato>[, <puntatore>]... )

`scanf()' potrebbe essere definito come l'inverso di `printf()', nel senso che riceve input dallo standard input interpretandolo opportunamente, secondo le metavariabili inserite nella stringa di formattazione (la stringa di formattazione deve contenere solo metavariabili).

Per esempio,

printf( "Inserisci l'importo:" );
scanf( "%d", &iImporto );

emette la frase seguente,

Inserisci l'importo:_

e resta in attesa dell'inserimento di un valore numerico intero, seguito da [Invio]. Questo verrà inserito nella variabile `iImporto'. Si deve osservare il fatto che i parametri successivi alla stringa di formattazione sono dei puntatori, per cui, avendo voluto inserire il dato nella variabile `iImporto', questa è stata indicata preceduta dall'operatore `&' in modo da fornire alla funzione l'indirizzo corrispondente.

Con una stessa funzione `scanf()' è possibile inserire dati per diverse variabili, come si può osservare dall'esempio seguente, e in questo caso, per ogni dato viene richiesta la pressione di [Invio].

printf( "Inserisci il capitale e il tasso:" );
scanf( "%d%f", &iCapitale, &iTasso );

Le metavariabili utilizzabili sono simili a quelle già viste per `printf()'; in particolare non si utilizzano simboli aggiuntivi, mentre è sempre possibile inserire la dimensione.

`scanf()' restituisce il numero di elementi che sono stati letti con successo, intendendo con questo non solo il completamento della lettura, ma anche il fatto che i dati inseriti risultano corretti in funzione delle metavariabili indicate.

Si veda anche la pagina di manuale scanf(3).

153.9 Restituzione di un valore

I programmi, di qualunque tipo siano, al termine della loro esecuzione, restituiscono un valore che può essere utilizzato da uno script di shell per determinare se il programma ha fatto ciò che si voleva o se è intervenuto qualche tipo di evento che lo ha impedito.

Convenzionalmente si tratta di un valore numerico, in cui zero rappresenta una conclusione normale, ovvero priva di eventi indesiderati, mentre qualsiasi altro valore rappresenta un'anomalia. A questo proposito si consideri quello «strano» atteggiamento degli script di shell, per cui zero equivale a Vero.

Se nel sorgente C non si fa nulla per definire il valore restituito, questo sarà sempre zero, mentre per agire diversamente, conviene utilizzare la funzione `exit()'.

153.9.1 exit()

exit( <valore-restituito> )

La funzione `exit()' provoca la conclusione del programma, dopo aver provveduto a scaricare i flussi di dati e a chiudere i file. Per questo motivo, non restituisce un valore all'interno del programma, al contrario, fa in modo che il programma restituisca il valore indicato come argomento.

Per poterla utilizzare occorre includere il file di intestazione `stdlib.h' che tra l'altro dichiara già due macro adatte a definire la conclusione corretta o errata del programma: `EXIT_SUCCESS' e `EXIT_FAILURE'. *3*

#include stdlib.h
...
...
if (...) {
    exit( EXIT_SUCCESS );
} else {
    exit( EXIT_FAILURE );
}

L'esempio mostra in modo molto semplice come potrebbe essere utilizzata questa funzione.

153.10 Suddivisione dei sorgenti e compilazione

All'inizio del capitolo era stato descritto in modo semplice come compilare un programma composto da un sorgente unico. Di solito i programmi di dimensioni normali sono articolati in più file sorgenti separati che vengono compilati in modo indipendentemente e infine collegati in un eseguibile unico. Questo permette di ridurre i tempi di compilazione quando si fanno modifiche solo in uno o alcuni file sorgenti, in modo da non dover ricompilare sempre tutto.

153.10.1 Suddivisione in più file sorgenti

La suddivisione del codice in più file sorgenti richiede un po' di attenzione nell'inclusione dei file di intestazione, nel senso che si deve ripetere l'inclusione dei file necessari in tutti i sorgenti. Se si utilizzano delle macro del preprocessore, queste dovranno essere dichiarate in tutti i sorgenti che ne fanno uso; per questo conviene solitamente predisporre dei file di intestazione aggiuntivi, in modo da facilitarne l'inclusione in tutti i sorgenti.

Un altro problema è dato dalle funzioni descritte in un file e utilizzate anche in altri. Ogni file sorgente, all'interno del quale si fa riferimento a una funzione dichiarata altrove, deve contenere una dichiarazione di prototipo opportuna. In modo analogo occorre comportarsi con le variabili globali. Anche queste definizioni possono essere inserite in un file di intestazione personalizzato, da includere in ogni sorgente.

153.10.2 Compilazione e link

Disponendo di diversi file sorgenti separati, la compilazione avviene in due fasi: la generazione dei file oggetto e il link di questi in modo da ottenere un file eseguibile. Fortunatamente, tutto questo può essere gestito tramite lo stesso compilatore `cc'.

Per generare i file oggetto si utilizza `cc' con l'opzione `-c', mentre per unirli assieme, si utilizza l'opzione `-o'. Si osservi l'esempio seguente:

/* prova1.c */
#include <stdio.h>

/* una funzione banalissima */
void messaggio( char *pc ) {
    printf( pc );
}

---------

/* prova0.c */
#include <stdio.h>

/* prototipo della funzione banalissima */
void messaggio( char * );

main() {
    messaggio( "saluti a tutti\n" );
}

Si suppone che il primo file sia stato nominato `prova1.c' e il secondo `prova0.c'. Si inizia dalla compilazione dei singoli file in modo da generare i file oggetto `prova1.o' e `prova0.o'.

cc -c prova1.c[Invio]

cc -c prova0.c[Invio]

Quindi si passa all'unione dei due risolvendo i riferimenti incrociati, generando il file eseguibile `prova'.

cc -o prova prova1.o prova0.o[Invio]

Se si volesse fare una modifica su uno solo dei file sorgenti, basterebbe rigenerare il file oggetto relativo e riunire il tutto con il comando `cc -o' appena mostrato.

153.10.3 Compilatore standard cc

Il compilatore C di GNU/Linux è `gcc' (`cc' GNU), tuttavia, le sue caratteristiche sono tali da renderlo conforme al compilatore standard POSIX. Per mantenere la convenzione, è presente il collegamento `cc' che si riferisce al vero compilatore `gcc'.

cc [ <opzioni> | <file> ]...

La sintassi esprime in maniera piuttosto vaga l'ordine dei vari argomenti della riga di comando, e in effetti non c'è una particolare rigidità.

Alcune opzioni

-c

Genera come risultato i file di oggetto, senza avviare il link dell'eseguibile finale.

-g

Aggiunge delle informazioni diagnostiche utili per il debug attraverso strumenti appositi come `gdb'.

-o <file>

Definisce il nome del file che deve essere generato a seguito della compilazione (indipendentemente dal fatto che si tratti di un file eseguibile o di un file oggetto o altro ancora).

-l<libreria>

Compila utilizzando la libreria indicata, tenendo presente che, per questo, verrà cercato un file che inizia per `lib', continua con il nome indicato, e termina con `.a' oppure `.so'.

Estensioni tipiche

.c

Sorgente C.

.o

File oggetto.

.a

Libreria.

153.11 Riferimenti

---------------------------

Appunti Linux 2000.04.12 --- Copyright © 1997-2000 Daniele Giacomini --  daniele @ pluto.linux.it


1.) Come si può osservare, la dimensione è restituita dalla funzione `sizeof()', che però nell'esempio risulta preceduta dalla notazione `(int)'. Si tratta di un cast, perché il valore restituito dalla funzione è di tipo speciale, precisamente si tratta del tipo `size_t'. Il cast è solo precauzionale perché generalmente tutto funziona in modo regolare senza questa indicazione.

2.) Di fatto, questa è la convenzione usata nel linguaggio Java, però si tratta di un'idea valida e perfettamente applicabile anche in C.

3.) In pratica, `EXIT_SUCCESS' equivale a zero, mentre `EXIT_FAILURE' equivale a uno.


[inizio] [indice generale] [precedente] [successivo] [indice analitico] [contributi]