[successivo] [precedente] [inizio] [fine] [indice generale] [violazione GPL] [translators] [docinfo] [indice analitico] [volume] [parte]


Capitolo 283.   Linguaggio C: introduzione

Il linguaggio C è il fondamento dei sistemi Unix. 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 dovrebbe disporre di tutto quello che è necessario per provare gli esempi di questi capitoli.

283.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 */.

283.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 programma C tipico richiede l'inclusione di codice esterno composto da file che terminano con l'estensione .h. La libreria che viene inclusa più frequentemente è quella necessaria alla gestione dei flussi di standard input, standard output e standard error; si dichiara il suo utilizzo nel modo seguente:

#include <stdio.h>

283.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;

{istruzioneistruzioneistruzione; }

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.

283.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.

283.1.4   Funzione principale

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

283.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. */
int 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.

283.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!

283.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

283.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.

283.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 283.1.

Tabella 283.1. Elenco dei tipi di dati primitivi elementari in C.

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

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 283.2 riassume i vari tipi primitivi con le combinazioni dei qualificatori.

Tabella 283.2. Elenco dei tipi di dati primitivi in C assieme ai 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.

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>

int 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.

283.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; inoltre non c'è più un limite di dimensione, ma solo 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.

283.3.2   Costanti letterali

Quasi tutti i tipi di dati primitivi, hanno la possibilità di essere rappresentati 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 * (1015), cioè un sette con 15 zeri. Nello stesso modo, 7e-5, rappresenta l'equivalente di 7 * (10-5), cioè 0,000 07.

È possibile rappresentare anche le stringhe in forma di costante 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, ma la differenza è notevole. Le stringhe verranno descritte meglio in seguito.

283.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 224.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 283.3 riporta i vari tipi di rappresentazione delle costanti carattere attraverso codici di escape.

Tabella 283.3. Elenco dei modi 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>).

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.

283.3.3   Campo di azione delle variabili

Il campo di 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.

283.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 1 000.

int numero = 1000;

283.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.

283.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 283.4 con tutti questi prefissi proposti.

Tabella 283.4. Convenzione proposta per i nomi delle variabili.

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

283.4   Operatori ed espressioni

L'operatore è qualcosa che esegue un qualche tipo di funzione, su uno o due operandi, restituendo 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.

283.4.1   Operatori aritmetici

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

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

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

283.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 zero (Falso). È importante sottolineare che qualunque valore diverso da zero, equivale a Vero in un contesto logico. Gli operatori di confronto sono elencati nella tabella 283.6.

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

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.

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 283.7.

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

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.

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.

283.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 283.8.

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

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

In particolare, lo spostamento può avere effetti differenti a seconda che venga utilizzato su una variabile senza segno o con segno, dove in questo ultimo caso si possono ottenere risultati diversi su piattaforme differenti. Per questo, verrà mostrato solo l'esempio 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)

283.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.

(tipoespressione

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.

283.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.

y = x = 10;

In questo esempio ulteriore, si vede l'assegnamento alla variabile y dello stesso valore che viene assegnato alla variabile x. In pratica, sia x che y contengono alla fine il numero 10.

283.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.

283.5.1   if

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

if (condizioneistruzione

if (condizioneistruzione else istruzione

Se la condizione si verifica, viene eseguita l'istruzione o il gruppo di istruzioni che segue; 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");
  }

283.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, allo scopo di 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;
  }

283.5.3   while

while (condizioneistruzione

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");

283.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, poi se ne ripete l'esecuzione finché la condizione restituisce il valore Vero.

283.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 (espressione1espressione2espressione3istruzione

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 = ncondizionevar++) 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 10 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.

283.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, con tipi di dati 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.

283.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.

283.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.

283.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; inoltre, ciò 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.

283.7   Struttura e campo di 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: 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.

283.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 (...)
{
...
}

283.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 di 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).

Dichiarando una variabile come appartenente alla classe register, si richiede di utilizzare per questa un registro del microprocessore (ammesso che ciò sia possibile). L'utilizzo di un registro per una variabile serve a velocizzare l'esecuzione di un programma che deve accedere frequentemente a una certa variabile, ma generalmente 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 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.

283.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.

283.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: 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 1 000 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, infine, al posto della terza metavariabile %d è stato inserito il valore 1 050.

La scelta della metavariabile corretta determina il tipo di trasformazione che il parametro corrispondente deve ricevere. La tabella 283.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: %%.

Tabella 283.9. Alcune metavariabili utilizzabili per la formattazione di stringhe con printf().

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.

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 283.10. In presenza di valori numerici, si può indicare il numero di cifre decimali intere (ampiezza), aggiungendo 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).

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

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.

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

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

283.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, ma 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).

283.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().

283.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.

283.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.

283.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.

283.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 *);

int 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.

283.10.3   Compilatore standard cc

Il compilatore C di GNU è 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 eseguibile 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).

-llibreria

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.

283.10.4   Problemi con l'ottimizzazione di GCC

Il compilatore GCC consente di utilizzare diverse opzioni per ottenere un risultato più o meno ottimizzato. L'ottimizzazione richiede una potenza elaborativa maggiore, al crescere del livello di ottimizzazione richiesto. In situazioni particolari, può succedere che la compilazione non vada a buon fine a causa di questo problema, interrompendosi con segnalazioni più o meno oscure, riferite alla scarsità di risorse. In particolare potrebbe essere rilevato un uso eccessivo della memoria virtuale, per arrivare fino allo scarico della memoria (core dump).

È evidente che in queste situazioni diventa necessario diminuire il livello di ottimizzazione richiesto, modificando opportunamente le opzioni relative. L'opzione in questione è -On, come descritto nella tabella 283.11. In generale, l'assenza di tale opzione implica la compilazione normale senza ottimizzazione, mentre l'uso dell'opzione -O0 può essere utile alla fine della serie di opzioni, per garantire l'azzeramento delle richieste di ottimizzazione precedenti.

Tabella 283.11. Opzioni di ottimizzazione per GCC.

Opzione Descrizione
-O, -O1 Ottimizzazione minima.
-O2 Ottimizzazione media.
-O3 Ottimizzazione massima.
-O0 Annullamento delle richieste precedenti di ottimizzazione.

Alle volte, compilando un programma, può succedere che a causa del livello eccessivo di ottimizzazione prestabilito, non si riesca a produrre alcun risultato. In questi casi, può essere utile ritoccare i file-make, dopo l'uso del comando configure; per la precisione si deve ricercare un'opzione che inizia per -O. Purtroppo, il problema sta nel fatto che spesso si tratta di più di un file-make, in base all'articolazione dei file che compongono il sorgente.

Ammesso che si tratti dei file Makefile, si potrebbe usare il comando seguente per attuare la ricerca:

find . -name Makefile <-'
`->-exec echo \{\} \; <-'
`->-exec grep \\-O \{\} \;

Il risultato potrebbe essere simile a quello che si vede qui di seguito:

./doc/Makefile
./backend/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./frontend/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./include/Makefile
./japi/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./lib/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./sanei/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./tools/Makefile
CFLAGS = -g -O2 -W -Wall -DSCSIBUFFERSIZE=131072
./Makefile

In questo caso, si può osservare che i file ./doc/Makefile, ./include/Makefile e Makefile, non contengono tale stringa.

Questo problema può riguardare anche la compilazione di un kernel Linux. In tal caso, dovrebbe essere sufficiente modificare il solo file /usr/src/linux/Makefile, anche se non è l'unico in cui appaia tale opzione. Le righe su cui intervenire potrebbero avere l'aspetto seguente:

HOSTCFLAGS      = -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer
CFLAGS := $(CPPFLAGS) -Wall -Wstrict-prototypes -O2 -fomit-frame-pointer

283.11   Riferimenti

Appunti di informatica libera 2003.01.01 --- Copyright © 2000-2003 Daniele Giacomini -- daniele @ swlibero.org

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.


Dovrebbe essere possibile fare riferimento a questa pagina anche con il nome linguaggio_c_introduzione.html

[successivo] [precedente] [inizio] [fine] [indice generale] [violazione GPL] [translators] [docinfo] [indice analitico]