Questo documento contiene approfondimenti sul linguaggio C, tralasciando completamente le funzioni di libreria (che comunque non fanno parte del linguaggio), pensando ad un pubblico che sa già programmare in qualche altro linguaggio. È una integrazione ad A-C-X.html.
Ricordiamo che il C è pensato per essere molto vicino al processore e alle sue strutture; non dovrebbe perciò stupire la scelta degli autori del linguaggi di rappresentare un vettore con l'indirizzo del suo primo elemento. Puntatori e vettori sono perciò concetti intercambiabili e ad un puntatore si può applicare un indice tramite l'operatore parentesi-quadre senza che questo provochi errori o messaggi di attenzione. Il nome di un vettore è diverso da un puntatore in quanto non gli può essere assegnato un nuovo valore: si tratta di un indirizzo costante istanziato all'atto della compilazione del programma.
Ai puntatori possono essere sommati e sottratti numeri interi: il
risultato della somma di un puntatore e di un numero intero n è
il puntatore all'elemento numero n del vettore. Il numero
intero non rappresenta cioè il numero di byte da aggiungere
nell'indirizzo, ma il numero di elementi, il fattore di scala
appropriato viene applicato dal compilatore in base al tipo di
puntatore oggetto di operazione aritmetica. È possibile quindi
incrementare/decrementare un puntatore, come pure fare la differenza
(ma non la somma) tra puntatori dello stesso tipo, e il risultato è un
numero intero). L'aritmetica su puntatori generici (puntatori a
void
) con gcc
usa 1 byte come dimensione
dell'elemento puntato, mentre non è definita secondo lo standard del
linguaggio.
Il "puntatore nullo" vale zero e non è un puntatore valido; nelle
funzioni che ritornano un puntatore è spesso ritornato come
segnalazione di errore. La macro NULL
vale 0, e 0
è confrontabile con qualunque puntatore.
Gli operatori più importanti per usare i puntatori sono
"*
" (si legge "il puntato da") e "&
"
("l'indirizzo di").
Esempi:
int i, v[10], *p; /* un numero intero, un vettore e un puntatore: "è intero l'elemento del vettore v, di dimensione 10" "è intero quello che è puntato da p" */ p = v; /* assegno a p il valore di v, l'indirizzo del primo elemento */ p = &v[0]; /* come sopra */ p = &v[4]; /* assegno a p l'indirizzo del quinto elemento di v */ p = v+4; /* come sopra */ p++; /* incremento p */ i = p-v; /* assegno 5 a i (in conseguenza delle due righe precedenti) */
Nel seguente ciclo, assegno all'intero i
la somma di
tutti gli elementi del vettore v
che non sono zero. Si noti
che deve esserci un elemento di v
che vale zero,
altrimenti il puntatore assumerà valori non validi. Il linguaggio non
effettua alcun controllo implicito su puntatori e indici di vettori.
i=0; for (p = v; *p; p++) i += *p;
È un errore fare assegnamenti tra puntatori di tipo diverso, così come è un errore fare operazioni aritmetiche tra puntatori di tipo diverso. È comunque possibile convertire un puntatore da un tipo ad un altro (ma anche un puntatore in numero intero e viceversa); queste conversioni non provocano la generazione di codice macchina, perché comunque nel processore sono tutti numeri interi, ma sono necessarie per la pulizia semantica del codice sorgente.
È sempre consentito l'assegnamento di un puntatore-a-void a
qualunque altro tipo di puntatore, come pure l'assegnamento di
qualunque puntatore ad un puntatore-a-void. Questo perché il tipo
"void *
" è quello che normalmente si usa per gestire
indirizzi generici di memoria, operazione comunissima nei sistemi
operativi e nelle librerie di sistema.
L'operatore sizeof()
, applicato ad un tipo,
ad un nome di variabile o ad un'espressione, ritorna la dimensione in
byte dell'oggetto indicato. Tale calcolo viene effettuato in compilazione
in base ai tipi di dato che vengono passato a sizeof
.
Se incremento un puntatore p
, il suo valore numerico
(indirizzo in memoria in byte) viene incrementato di sizeof(*p)
.
Esempio:
int i, v[10], *p; /* le stesse variabili di prima */ i = sizeof(int); /* normalmente 4, ma può essere 8, oppure 2 */ i = sizeof(i); /* come sopra */ i = sizeof(v[0]); /* come sopra */ i = sizeof(*p); /* come sopra */ i = sizeof(p); /* 4 (dimensione del puntatore), oppure 8 */ i = sizeof(v); /* 40, oppure 80, oppure 20 */ i = sizeof(v)/sizeof(*v); /* 10: il numero di elementi nel vettore v */ #define ARRAY_SIZE(x) (sizeof(x)/sizeof(*x)) /* una comoda macro */
Una stringa in C è un vettore di caratteri terminato da un byte a zero. La rappresentazione tra virgolette è solo una notazione semplificata per rappresentare un vettore. Ogni volta che nel testo del programma appare una stringa tra virgolette, il compilatore memorizza la stringa nel segmento dati del programma e la rappresenta con l'indirizzo del suo primo elemento. Un carattere incluso in apice singolo è un numero intero, cioè il codice ASCII del carattere indicato.
Esempi di dichiarazioni di stringhe e puntatori:
char s[] = "prova"; /* un vettore di 6 caratteri, compreso il terminatore */ char s[] = {'p', 'r', 'o', 'v', 'a', 0}; /* lo stesso, in notazione barocca */ char c, *t; /* un carattere, un puntatore a carattere */ c = *s; /* c prende il valore 'p' */ t = s+2; /* t rappresenta la stringa "ova" */ s[0] = 't'; /* ora la stringa s è "trova" */ char *name = "arturo"; /* un puntatore ad un'area preiinizializzata di 7 byte */ char surname[] = "rossi"; /* un'area di 6 byte, con indirizzo "surname" */ name++; /* ora name indica "rturo" */ surname++; /* errore: surname è un indirizzo costante */
strlen
, funzione che ritorna
la lunghezza di una stringa, può quindi essere la seguente:
int strlen(char *s) { char *t = s; for (; *t; t++) ; return t - s; }
Come nel caso dei vettori, una funzione viene rappresentata
dall'indirizzo del codice associato; tutte le volte che si usa un nome
di funzione in un programma si sta in pratica usando il puntatore a
tale funzione. L'uso più consueto di un puntatore a funzione è
l'applicazione dell'operatore parentesi-tonde, situazione che
normalmente non viene pensata in termini di puntatori ed operatori da
parte del programmatore. Un puntatore a funzione può anche essere
assegnato ad altri puntatori, per esempio all'interno di strutture
dati che definiscono i metodi con cui operare sugli oggetti, oppure
passato come argomento a altre funzioni, per esempio la funzione di
libreria qsort
, funzione che implementa l'algoritmo
"quick sort" su un vettore. Il compilatore verifica in compilazione
che i tipi dei puntatori a funzione siano compatibili, cioè le funzioni
come dichiarate ricevano gli stessi argomenti.
Esempio:
#include <string.h> /* per la dichiarazione di strcmp */ #include <stdlib.h> /* per la dichiarazione di qsort */ char *strings[100]; /* definisco un vettore di 100 puntatori */ strcmp(strings[0], strings[1]); /* confronto due stringhe */ /* chiamo qsort dicendo che strcmp() è la funzione di confronto da usare */ qsort(strings, 100, sizeof(char *), strcmp); strncmp(strings[0], strings[1], 5); /* confronto solo i primi 5 caratteri */ /* questo invece è un errore, perché strncmp riceve tre argomenti */ qsort(strings, 100, sizeof(char *), strncmp);
Il linguaggio non offre primitive di gestione di memoria (cose tipo
new
, creatori e distruttori) e nemmeno la raccolta della
spazzatura («garbage collection» sembra più raffinato, ma di quello
si tratta).
La memoria usata dai programmi può essere di tre tipi: statica,
dinamica, automatica. Una variabile o struttura dati statica è quella
dichiarata in compilazione, cui il linker assegna un indirizzo
immutabile. Una struttura dati dinamica è allocata durante il
funzionamento del programma, per esempio chiamando malloc
e accedendo allo spazio così ottenuto tramite un puntatore. Una
variabile cosiddetta "automatica" viene allocata sullo stack e
scompare al termine del blocco di codice che la dichiara.
Una variabile statica è inizializzata a zero, a meno che il programma non dichiari un valore costante da precaricare nella variabile. Le variabili inizializzate sono salvate su disco e risiedono nel "segmento dati" del programma e del file eseguibile ELF; le variabili non inizializzate stanno nel "segmento bss" del programma, una zona di memoria che viene allocata e azzerata prima dell'esecuzione del programma, il file su disco non contiene una copia del bss ma solo la sua dichiarazione.
Una variabile dinamica risiede in memoria che viene richiesta al
sistema durante il funzionamento del programma. Al momento
dell'allocazione non si possono fare assunzioni sul contenuto di tale
memoria: potrebbe essere azzerata ma potrebbe contenere informazioni
residue di precedenti allocazioni poi liberate. Ad ogni
malloc
deve corrispondere una free
, in
mancanza della quale abbiamo una situazione di perdita di memoria
(«memory leakage») e la dimensione del programma in esecuzione
aumenterà in continuazione. Mentre la memoria di un programma in
spazio utente viene liberata tutta al termine del programma, una
allocazione non liberata in spazio kernel causa una perdita di memoria
che può essere recuperata solo riavviando la macchina.
Una variabile automatica è una variabile locale di una funzione o di un blocco di codice, risiede sullo stack e non viene inizializzata a meno che il programmatore non imponga un valore a tale variabile; in tal caso il codice macchina generato dal compilatore contiene le istruzioni necessarie a riempire la variabile come richiesto. La memoria delle variabili automatiche, essendo parte dello stack del programma, non è più utilizzabile al termine della procedura che definisce la variabile stessa.
Esempi:
int i; /* dato inizializzato a zero, segmento bss */ int v[4] = {2,1,}; /* dato inizializzato a {2,1,0,0}, segmento dati */ int j = f(3, i); /* errore: valore non noto in compilazione */ int *f(int x, int y) { int z; /* var. automatica, non so quanto vale */ int a=0, b=1, c=2; /* variabili inizializzate a run-time */ int *p = malloc(4 * sizeof(int)); /* inizializzazione valida a run-time */ int *q, *r = &z; /* due puntatori, uno vale l'.ind. di z */ *q = y; /* errore: il puntatore non è stato assegnato */ *r = y; /* corretto: r è l'indirizzo di z, quindi assegno z */ if (x) return p; /* corretto: la memoria allocata rimane disponibile */ else return &z; /* errore: all'esterno di f non posso usare z */ }
Si veda il file operator.tbl per una tabella di tutti gli operatori con la loro precedenza e associatività. Tale file può essere stampato in una pagina a4 o a5. Gli operandi di ogni operatore sono altre espressioni tranne in due casi particolari. Questa sezione mostra solo l'uso di ogni operatore, nello stesso ordine di operator.tbl:
()
chiamata a funzione. L'operatore si
applica ad un solo operando: un nome di funzione o un puntatore,
specificando gli argomenti da passare dentro le parentesi, ognuno dei
quali è una espressione.
extern int (*rd_data)(void *buffer, int count); void *p = malloc(1024); int result = rd_data(p,1024);
[]
elemento di vettore. L'operatore
prende due operandi, uno prima delle parentesi e l'altro tra
parentesi; normalmente il primo è un puntatore e il secondo un numero
intero, anche se in realtà l'operazione è commutativa. Dato il
puntatore ad intero (o vettore) v
, le seguenti istruzioni
sono tutte uguali:
v[3] = 0; *(v+3) = 0; 3[v] = 0;
.
elemento di struttura. I due operandi
sono una struttura (un'espressione il cui valore è una struttura)
e il nome di campo di tale struttura.
#include <sys/stat.h> /* st_mode è un campo intero di struct stat */ struct stat st, *stptr, stvec[10]; int i; i = st.st_mode; i = (*stptr).st_mode; i = stvec[5].st_mode;
->
elemento di struttura da puntatore.
I due operandi sono un puntatore a struttura e il nome di un campo di tale
struttura. È il modo più comune per utilizzare le strutture dati.
#include <sys/stat.h> /* st_mode è un campo intero di struct stat */ struct stat st, *stptr, stvec[10]; int i; extern struct stat *getstatptr(int i); /* funzione ipotetica */ i = stptr->st_mode; i = (stvec+5)->st_mode; i = (&st)->st_mode; /* & tra parentesi, -> ha priorità maggiore */ getstatptr(5)->st_mode;
!
negazione logica. Nega l'operando
alla sua destra: se l'operando è 0 il risultato è 1, se l'operando è
non-zero il risultato è zero.
p = malloc(128); if (!p) { /* gestione errore */ } return !!i; /* ritorna 0 se i vale 0, 1 se i è diverso da zero */
~
complemento a 1. Nega i bit dell'operando
alla sua destra.
i = ~0; /* 0xffffffff se 32 bit, 0xff se 8 bit, eccetera */ int page_mask = ~(PAGE_SIZE-1) /* 0xfffff000 se la pagina è 4k cioè 0x1000*/
-
negazione unaria. Nega l'espressione
alla sua destra
i = -1; return -EINVAL; /* EINVAL è un codice di errore intero positivo */
++ --
incremento e decremento.
Operatori con un solo operando; se l'operatore sta dopo l'operando
(esempio: i++
) l'incremento o decremento viene fatto dopo
aver usato il valore dell'operando, se l'operatore sta prima
dell'operando, l'incremento o decremento viene fatto prima di usare il
valore. L'operando deve essere assegnabile (si veda la discussione di
"lvalue" più avanti, nell'operatore di assegnamento =
).
int stack[10], sp=0; /* stack pointer che punta al primo elemento vuoto */ stack[sp++] = datum; /* inserimento ("push") */ datum = stack[--sp]; /* estrazione ("pop") */ i=10; while (--i) { /* ciclo per i che varia da 9 a 1 */ } i=10; while (i--) { /* ciclo per i che varia da 9 a 0 */ }
&
estrazione di indirizzo. L'operatore
ritorna l'indirizzo dell'operando alla sua destra.
#include <sys/stat.h> struct stat stbuf; stat("/bin/sh", &stbuf); /* la funzione chiamata scrive in stbuf */ char s[32]; sscanf(s, "%i", &i); /* sscanf converte da stringa e scrive in i */
*
uso di puntatore. L'operatore dereferenzia
il puntatore che sta alla sua destra.
int v[32], *p; for (p = v + 32; p >= v; p--) sum = sum + *p;
(type)
cambio di tipo (cast).
La sintassi "(tipo)espressione
" converte l'espressione
nel tipo indicato. Spesso non comporta generazione di codice,
per esempio per convertire tra unsigned
e signed
,
o tra puntatori di tipo diverso.
/* mmap ritorna un indirizzo e l'indirizzo -1 indica errore */ addr = mmap( /* argomenti */ ); if (addr == (void *)-1) { /* gestione errore */ }
sizeof
dimensione in byte. L'operatore
sizeof viene valutato all'atto della compilazione del programma e
diventa un intero costante nel codice generato. Restituisce la
dimensione del tipo o del dato alla sua destra, come in sizeof
int
. Per chiarezza, è consuetudine mettere l'operando di
sizeof
tra parentesi e pensare a sizeof
come
a una funzione -- anche se sintatticamente tali parentesi sono le
parentesi aritmetiche per alterare la priorità degli operatori, non
una vera chiamata a funzione.
struct buf *buffers = malloc(10 * sizeof(struct buf)); if (sizeof(int) == 2) { /* codice per processore a 16 bit */ } else if (sizeof(int) == 8) { /* codice per 64 bit */ } else { /* codice per processore a 32 bit */ }
* /
moltiplicazione e divisione.
Normale molti plicazione e divisione aritmetica. L'asterisco in
questo uso non provoca ambiguità con la dereferenziazione di
puntatore, perché la moltiplicazione ha due operandi e comunque
non può essere effettuata sui puntatori. La moltiplicazione
intera non gestisce l'overflow dei valori; la divisione intera
scarta il resto.
fahr = cels * 9 / 5; /* conversione di temperatura */ cels = fahr / 9 * 5; /* errato per perdita del resto: usare "* 5 / 9" */ int nsec = sec * 1000000000 /* overflow se sec < -2 o sec > 2 */
%
resto di divisione intera. Applicato
a due operandi interi, restituisce il resto della divisione.
void stampatempo(int s) { int h, m; m = s/60; s = s%60; h = m/60; m = m%60; printf("%i:%02i:%02i\n", h, m, s); }
+ -
somma e sottrazione. Uno dei
due operandi può essere un puntatore, in tal caso il risultato è
un puntatore. Come per la moltiplicazione, non c'è nessun controllo
sull'overflow.
int a=200, b=300; unsigned int c; c = a-b; /* un numero positivo: 2 alla 32 meno 100 */
<< >>
spostamento di bit.
Sull'operando a sinistra si opera un bit-shift del valore corrispondente
all'operando di destra.
/* conversione da rgb888 a rgb565 -- */ unsigned char rgb[3]; unsigned short pixel; pixel = ((rgb[0]>>3) << 11) + ((rgb[1]>>2) <<5) + (rgb[2]>>3); /* attenzione a come si salva pixel (16 bit) su big-endian/little-endian */
< <= > >=
confronto. Il risultato
dell'operazione è un intero: 1 se il confronto è vero e 0 se è falso.
int fuorimisura = i>100 || i<50; i = i * 1000 / 254; /* converto da cm in decimi di pollice */ if (fuorimisura) { /* gestione errore */ }
== !=
confronto. Come sopra, il risultato
è zero oppure 1 in caso di uguaglianza o diversità.
int zero = i==0;
&
AND di bit tra interi. Ogni bit
del risultato è 1 solo se entrambi i bit corrispondenti degli
operandi sono 1.
int lowbyte = val & 0xff; int highbyte = val & 0xff00;
^
XOR di bit tra interi. Ogni bit
del risultato è 1 se i bit corrispondenti dei due operandi
sono diversi.
while ( /* condizione */ ) { /* calcolo */ led = led ^ 1; /* faccio lampeggiare il bit più basso */ }
|
OR di bit tra interi. Ogni bit
del risultato è1 se almeno uno dei bit corrispondenti dei due operandi è 1.
flags = flags | FLAG_BUSY; /* ... */ flags = flags & ~FLAG_BUSY;
&&
AND logico. Il risultato
vale 1 solo se entrambi gli operandi sono veri (diversi da zero),
altrimenti il risultato è zero. Il secondo operando viene valutato
solo se il primo è vero; se il primo è falso il risultato è già
noto, quindi il secondo operando non viene valutato.
/* chiamo il metodo print solo se nessun puntatore in gioco è nullo */ if (strptr && strptr->methods && strptr->methods->print) strptr->methods->print(strptr);
||
OR logico. Il risultato è 1 solo
se almeno uno dei due operandi è diverso da zero. Se il primo operando
è zero, il secondo operando non viene valutato. La priorità di OR
è minore di quella di AND, perché OR è assimilabile ad una somma,
mentre AND è assimilabile ad una moltiplicazione.
if (v[i] || fill_item(&v[i]) || set_default(&v[i])) /* lavoro sulla struttura puntata da v[i] */ ;
?:
espressione condizionale. Operatore
con tre operandi: se l'espressione intera prima del punto
interrogativo è vera, la seconda espressione viene valutata come
risultato, altrimenti la terza espressione, dopo i due punti, viene
valutata come risultato. Il tipo della seconda e della terza
espressione deve essere compatibile.
printf("%i byte%s in %i file%s", bytes, bytes==1 ? "" : "s", files, files==1 ? "" : "s");
=
assegnamento. L'assegnamento è una
espressione il cui risultato è uguale all'operando di
destra. L'operando di sinistra deve essere una variabile o una
struttura dati o una espressione equivalente. Tale operando si chiama
lvalue
, abbreviazione di "left value". I messaggi di
errore del compilatore relativi ad lvalue
si riferiscono
ad assegnamenti erronei.
a = b = c = 0; /* a = (b = (c = 0)) */ if (i = 0) /* sintatticamente valido, equivalente a if(0) */ ; stat_array[12]->st_mode = 0; /* bene */ "nome" = s; /* lvalue non valida: un vettore non è assegnabile */ 3 = i; /* lvalue non valida: come sopra ma più evidente */
*= /= %= += -= <<= >>= &= ^= |=
assegnamento. Tutti gli assegnamenti "<expr1>
op= <expr2>" sono forme concise di "<expr1> = <expr1> op <expr2>".
m = s / 60; s %= 60; /* da secondi a minuti e secondi */ flags |= FLAG_BUSY; /* alzo un bit */ flags &= ~FLAG_BUSY; /* abbasso un bit */
,
virgola. L'operatore virgola
esegue l'espressione alla sua sinistra ignorandone il
risultato, poi esegue l'espressione alla sua destra che vale
come risultato dell'operazione "virgola". Usato principalmente
nei cicli while
e per fare cicli for
con
due o più indici.
while(next_number(&i), i) { /* il nuovo i è diverso da zero */ } for (p = v, i = 0; i<32; p++, i++) { /* p scorre il vettore lungo 32 */ }
Il costrutto di controllo switch
serve a scegliere tra
diversi comportamenti in base al valore di una espressione intera,
ricordando che un carattere tra apici è un numero intero. La
sintassi è diversa da quella degli altri costrutti di controllo,
perché le parentesi graffe sono obbligatorie; inoltre fa uso delle
parole chiave case
e default
che non
hanno altri usi nel linguaggio.
La sintassi completa è la seguente:
switch ( espressione-intera ) { case espressione-costante : [ istruzione ... ] [ break ; ] case espressione-costante : [ istruzione ... ] [ break ; ] [ default: ] [ istruzione ... ] [ break ; ] }
Le espressioni di ogni case
devono essere espressioni
intere e costanti, cioè valutabili all'atto della compilazione. La
presenza di istruzioni dopo ogni case
è facoltativa, per
permettere di raggruppare lo stesso codice in relazione a diversi casi.
La presenza di break
alla fine di un caso è facoltativa,
per permettere che le istruzioni associate ad un caso continuino con
il codice del caso successivo; è sempre meglio commentare la mancanza
di break
, perché non sembri una dimenticanza a chi legge
il codice.
Il costrutto default
è facoltativo; se presente viene
selezionato quando l'espressione del costrutto switch
non
trova corrispondenza tra i casi elencati. Non è obbligatorio che
default
sia l'ultimo caso del costrutto.
Esempio: conversione estremamente inefficiente da esadecimale a
decimale, un carattere alla volta. Si noti come c
viene
modificato dopo essere stato usato per la selezione del caso
corretto; non deve stupire in quanto l'espressione di switch
viene valutata una volta sola.
int value; int nextchar(int c) { switch(c) { case 'a': case 'b': case 'c': case 'd: case 'e': case 'f': c = c - 'a' + 10 + '0'; /* fall through */ case '0': case '1': case '2': case '3': case '4': case '5': case '6': case '7': case '8': case '9': value = value * 10 + c - '0'; break; case 'p': printf("%i\n", value); value = 0; break; default: return -1; /* error */ } return 0; }
Normalmente switch
viene usato per selezionare tra
diversi comandi, per esempio nell'implementazione della chiamata di
sistema ioctl()
, oppure nella valutazione dei parametri
sulla riga di comando. L'uso di due o più case
per lo
stesso blocco di codice è raro, come è rara la necessità di
scavalcare un case
nell'esecuzione di quello precedente.
Una struttura dati (struct
) può includerne altre o
includere puntatori ad altre strutture. Mentre i puntatori possono
riferire una struttura da un'altra ciclicamente, l'inclusione di
strutture non può essere ricorsiva, perché la struttura inclusa è
interamente contenuta nella struttura includente.
Poiché il compilatore effettua una sola passata sul codice, se una struttura contiene il puntatore ad un'altra struttura, occorre dichiarare tale struttura preventivamente, anche senza definirne l'elenco dei campi. Tale struttura non può essere istanziata, perché il compilatore non sa la sua dimensione in byte, ma si possono definire puntatori ad essa, perché i puntatori hanno tutti la stessa dimensione.
Esempio:
struct father; struct child { struct father *father; /* ... */ }; struct father { struct child *child; /* ... */ };
Dichiarare una struttura senza specificarne l'elenco dei campi permette anche di creare strutture «opache» in una libreria, normalmente usate per dati privati della libreria stessa. Se una struttura contiene un puntatore alla struttura stessa non serve la dichiarazione preventiva, perché mentre il compilatore legge la lista dei campi ha già visto il nome della struttura stessa.
struct dpriv; struct datum { struct dpriv *priv; /* lista dei campi ignota all'utente di datum */ /* ... */ struct datum *next; /* per l'inserimento in una lista */ };
In C, lo spazio dei nomi di variabili e funzioni è piatto, non esiste cioè il supporto per «namespace» separati; anzi variabili e funzioni non possono avere lo stesso nome, perché ad un nome può essere associato un solo indirizzo, sia esso codice o dati.
A differenza delle variabili globali, le variabili locali, o
«automatiche», sono visibili solo all'interno del blocco in cui
sono dichiarate; tale blocco può essere una funzione o anche
un'istruzione composta racchiusa tra graffe, sia essa il corpo di un
costrutto di controllo (if
, for
, eccetera) o
un'istruzione composta a se stante. Le variabili locali a un blocco
sono allocate sullo stack, mentre non è possibile definire funzioni
"locali" all'interno di un blocco. Se una variabile definita all'interno
di un blocco ha lo stesso nome di un'altra variabile, globale o locale,
all'interno del blocco il nome si riferisce alla sua
definizione più interna. Come già detto, gli argomenti di una funzione sono
variabili locali della funzione stessa.
La parola chiave static
, un qualificatore per codice e
dati, serve per cambiare le regole di visibilità. Un simbolo globale
(funzione o variabile) se dichiarato static
non è
visibile all'esterno del file ove è definito, perché il suo nome non
viene reso disponibile al linker. Una variabile locale, se
static
, viene allocata nello spazio dati globale, ma senza
esportarne il nome; permette quindi di avere uno stato persistente
tra le varie invocazioni del blocco in cui è definita.
Esempio:
int i; /* globale */ static int j; /* globale, ma visibile solo in questo file */ static int invert(int i) /* invert può essere chiamata solo in questo file */ { int j; /* allocata sullo stack */ j = -i; /* variabili locali, "i" è l'argomento della funzione */ return j; } int count(void) /* count è definita globalmente nel programma */ { static int i; /* locale ma persistente, inizializzata a zero */ return ++i; /* incrementa il contatore e ritorna il valore */ }
Il compilatore Esempio: Siate coerenti nello stile di programmazione, fate rientrare
i blocchi sempre allo stesso modo, qualunque esso sia. Lo stile
più diffuso è quello di Kernighan e Ritchie (graffa aperta a fine
riga e graffa chiusa da sola in una riga a se stante); non importa
molto la vostra preferenza stilistica quanto la coerenza in tutto
il codice sorgente.
Qualunque sia il vostro livello di rientro dei blocchi (in
spaghetti-inglisc: «indentazione»): 2, 4 o 8 caratteri, il carattere
Mantenete le funzioni brevi e comprensibili, se una funzione
diventa troppo complessa è meglio dividerne il codice in blocchi
concettualmente separati (funzioni distinte).
Usate le strutture dati per maggior chiarezza e manutenibilità;
definite creatori e distruttori per gli oggetti più che usare
variabili globali.
Controllate sempre tutti gli errori: ogni funzione che chiamate
può fallire, il codice chiamante deve verificare il valore di ritorno
e comportarsi in maniera appropriata, che spesso vuol dire propagare
l'errore alla funzione chiamante.
Non chiamate Commentate bene il vostro codice; evitate costrutti particolarmente
«furbi», ma se lo fate spiegate il perché della vostra scelta.
Specificate sempre i termini di licenza nel file sorgente; in
assenza di permessi specifici vale la clausola «tutti i diritti
riservati», ma anche se questa è la vostra intenzione è sempre
meglio specificarlo per chiarire ogni dubbio.
Non fate interazione utente se non strettamente necessario. Se
necessario, leggete stdin con I costrutti che non sono stati trattati in questi due documenti,
in quanto relativamente poco usati, sono:
gcc
, come ogni implementazione di
cc
, riceve opzioni sulla riga di comando. I file
vengono elaborati in base al proprio nome: se terminano in .c
vengono compilati, se terminano in .S
vengono solo
passati all'assemblatore, se terminano in .o
vengono
solo passati al linker.
Opzioni più importanti (file
indica un nome
di file, ogni volta diverso):
gcc opzioni -o file
: l'output viene
scritto nel file specificato, prevaricando il comportamento
predefinito.gcc -c file
: «compile only», il risultato
è un file oggetto il cui nome è derivato dal nome del sorgente,
anche se di solito si usa -o
.gcc -E file
: solo preprocessore, il risultato
viene scritto su stdout, se non viene specificato -o
.gcc -Dsimbolo
: definisce la macro di preprocessore,
assegnando la stringa vuota.gcc -Dsimbolo=valore
: definisce la macro di
preprocessore al valore indicato.gcc -Idirectory
: specifica di usare
la directory indicata nella ricerca dei file di intestazione.gcc -Ldirectory
: specifica di usare
la directory indicata nella ricerca dei file di libreria.gcc -lnome
: specifica di usare la
libreria indicata nella fase di link.
gcc -DDEBUG jpegdemo.c -I/usr/local/include -L/usr/local/lib
-ljpeg -o jpegdemo
Approfondimento: stile di programmazione
TAB
vale 8 spazi; attenzione alla configurazione
predefinita del vostro editor che potrebbe essere scorretta.
exit
dall'interno di una funzione in caso
di errore, lasciate decidere al programma principale.
fgets
e poi
sscanf
, mai con scanf
direttamente; scrivete
stdout per righe complete, terminate da
'\n'
. Evitate l'output inutile («il silenzio è d'oro»)
e le righe vuote supreflue.
Cosa manca
enum
: definizione di nomi simbolici senza il
preprocessore.typedef
: definizione di nuovi tipi a partire
da tipi esistenti.union
: un tipo speciale di struttura, utilissimo
in situazioni molto particolari ma molto insidioso.volatile
, const
, inline
:
qualificatori per aiutare l'ottimizzazione del codice.goto
ed etichette associate: un famigerato costrutto,
che ha comunque la sua ragione di esistere in situazioni
particolari.register
: una obsoleta direttiva per
l'ottimizzazione. Da evitare, diffidate di chi ne parla bene.gcc
, come l'uso di assembler
nel codice C e mille altre cose comode ma esotiche, ben documentate
nel manuale del compilatore stesso.
Alessandro Rubini
Last modified: Marzo 2010