Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/wikka.php on line 315
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 176
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 463
Deprecated: Function set_magic_quotes_runtime() is deprecated in /home/demetres/public_html/didattica/ae/wikka.php on line 120
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/safehtml.php on line 308
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 159
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 161
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 162
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 163
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 165
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 166
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 167
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 243
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 250
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 259
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 266
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 273
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 280
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 467
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 469
Deprecated: Assigning the return value of new by reference is deprecated in /home/demetres/public_html/didattica/ae/3rdparty/core/safehtml/classes/HTMLSax.php on line 471
Appunti sul linguaggio C
Hello World in C
Il primo programma in C che vediamo, come da tradizione, è "Hello World":
#include <stdio.h>
int main
() {
printf("Hello World\n");
// stampa
return 0;
}
Mettiamolo a confronto con la relativa versione Java:
public class HelloWorld
{
public static void main
(String[] args
) {
System.
out.
println("Hello World");
// stampa
}
}
Osserviamo che:
- In C non esiste un costrutto per definire classi (diversamente da Java e dal C++).
- I metodi statici di Java sono rimpiazzati da funzioni in C.
- In C non esiste un equivalente dei metodi non statici di Java.
- Un programma C è formato da uno o più file con estensione .c o .h:
- I file .c contengono le definizioni delle funzioni.
- I file .h (chiamati header) contengono le dichiarazioni che servono al programma per tipi definiti dall'utente e per usare funzioni e variabili definite in altri file .c.
- Per poter usare funzioni e variabili definite in altri file .c, è necessario includere un file header con la direttiva #include. Per ora pensiamolo come simile all'import di Java. Nel nostro esempio, #include <stdio.h> serve per poter invocare la funzione di stampa printf. In Java non serve perché la class System è nel package java.lang, che viene importato automaticamente.
- Ci deve essere almeno un file .c contenente una funzione main da cui parte il programma, così come in Java ci deve essere almento una classe contenente la definizione del metodo statico main.
- Diversamente dal metodo main di Java, la funzione main del C restituisce un int. Il valore restituito è un codice che definisce lo stato di terminazione del programma. Per convenzione, 0 significa che il programma è terminato in modo normale, senza errori.
- I commenti si scrivono nello stesso modo che in Java: usando // per commenti su una sola linea (non erano ammessi in C prima dell'introduzione dello standard C99) o /* ... */ per commenti su più linee.
Usando i compilatore
gcc in ambienti come Linux, UNIX, MacOS, CygWin, ecc., il programma può esserere
compilato con il comando:
che genera, nella directory corrente, un file eseguibile chiamato
a.out ottenuto traducendo il programma C in codice macchina. Come osservato nella lezione precedente, il programma viene compilato
senza alcuna ottimizzazione. Il programma può essere eseguito come segue:
Per creare un
eseguibile con un nome diverso, usare l'opzione
-o:
Ora verrà creato un file eseguibile chiamato
hello:
Per
attivare le ottimizzazioni di base del compilatore, usare l'opzione
-O1 che genera codice più veloce:
$ gcc hello.c -o hello -O1
Data la semplicità del programma, in questo caso serve a poco.
Oggetti in C
Forse la nozione più importante da capire nel linguaggio C è quello di "oggetto", che denota una porzione di memoria che contiene dei dati in uso al programma.
Oggetto C: un oggetto in C è una regione contigua dello spazio di memoria del processo contenente un valore (o un gruppo di valori) di un certo tipo.
Un oggetto C è pertanto caratterizzato da:
- Un indirizzo: l'indirizzo del primo byte, cioè quello con indirizzo più basso, dell'oggetto.
- Un tipo: il tipo del valore in esso contenuto, che determina la dimensione in byte dell'oggetto stesso.
- Un valore: il valore dell'oggetto, definito dal valori che assumono in memoria i singoli byte in esso contenuti.
Si noti che
un oggetto non ha di per sè un nome associato. Un tipico esempio di espressione che denota un oggetto C è la
variabile, che associa un nome (identificatore) a un oggetto, in modo da potervi accedere agevolmente per poterne leggere e scrivere il contenuto.
Allocazione/deallocazione di oggetti in C
Gli oggetti devono risiedere in zone di memoria
allocate al programma. Ad esempio, l'oggetto denotato da una variabile è allocato grazie alla dichiarazione della variabile stessa, che dice al compilatore di riservare (allocare) spazio in memoria per l'oggetto.
Vi sono
tre modi di allocare memoria da usare per gli oggetti in C:
1) Dichiarare una
variabile globale, cioè dichiarata fuori dal corpo delle funzioni o dichiarata come
static o
extern dentro una funzione: in questo caso, la memoria per l'oggetto verrà allocata in una zona dello spazio logico del processo chiamata
DATA. Le variabili globali vengono
allocate staticamente al momento dell'avvio del programma e vengono
deallocate quando il programma termina.
Esempio:
int x; // variabile globale
void foo() {
x = 10;
}
2) Dichiarare una
variabile locale, cioè dichiarata dentro il corpo di una funzione (senza specificare
static o
extern): in questo caso, la memoria per l'oggetto verrà allocato nel
record di attivazione della funzione in cui appare al momento dell'invocazione della funzione stessa. I record di attivazione risiedono in un una zona dello spazio logico del processo chiamata
STACK. Le variabili locali vengono
allocate al momento dell'attivazione della funzione in cui appaiono e vengono
deallocate quando l'invocazione termina. Questo tipo di allocazione viene chiamata
allocazione automatica.
Esempio:
void foo() {
int x; // variabile locale
x = 10;
}
3) Allocare
dinamicamente un
blocco (o regione) di memoria all'interno di una zona dello spazio logico del processo chiamata
HEAP. L'
allocazione esplicita di un blocco della dimensione voluta avviene tramite la funzione
malloc (varianti:
calloc,
realloc) dichiarata in
stdlib.h e la
deallocazione esplicita avviene tramite la funzione
free.
Esempio:
p = malloc(4); // alloco dinamicamente un oggetto di 4 byte e ne tengo l'indirizzo in p
... // uso l'oggetto allocato
free(p); // ora l'oggetto non serve più: libero (dealloco) la memoria per altri usi futuri
la funzione di libreria
malloc usata nell'esempio riserva spazio per un oggetto di 4 byte nell'HEAP. La funzione restituisce l'indirizzo dell'oggetto allocato che viene messo in
p per poi potervi accedere (vedremo più avanti come fare). La funzione
malloc è concettualmente simile all'operatore
new di Java, che però non richiede al programmatore di pensare in termini di byte.
Attenzione:
- Quando si alloca dinamicamente la memoria con malloc, è cura del programmatore specificare un numero di byte da allocare sufficiente a contenere l'oggetto (o gli oggetti) che si intende usare. Il C non effettua alcun controllo!
- Ogni blocco allocato esplicitamente deve essere poi deallocato esplicitamente quando non serve più, altrimenti si ha un memory leak, cioè si "perde" memoria che divenda inutilizzabile per il programma occupando spazio inutile. Chi è abituato a programmare in Java dimentica facilmente di deallocare esplicitamente i blocchi precedentemente allocati (Java non richiede la deallocazione, ma usa una tecnica chiamata garbage collection per eliminare gli oggetti non più in uso).
Tipi primitivi in C
I tipi primitivi in C sono i seguenti:
- Tipo void: serve per indicare che una funzione non restituisce alcun valore (vedremo un altro uso più in là)
- Tipi numerici interi: char, short, int, long, long long
- Tipi numerici in virgola mobile: float, double, long double.
Il numero di byte richiesti per rappresentare valori dei tipi numerici non è definito esplicitamente dallo standard, ma dipende dalla piattaforma. Sulle moderne macchine a 64 bit [
System V Application Binary Interface (AMD64)∞], si ha:
Tipo |
Dimensione in byte |
char |
1 |
short |
2 |
int |
4 |
long |
8 |
long long |
8 |
float |
4 |
double |
8 |
long double |
16 |
Si noti che non esiste un tipo
boolean come in Java (il C99 introduce tuttavia il tipo
_Bool). In C il valore
false viene denotato da zero, mentre il valore
true viene denotato da un valore intero diverso da zero.
Per una trattazione più approfondita della dimensione dei tipi su varie piattaforme, si veda
qui∞.
Diversamente da Java, per ogni tipo numerico intero è possibile specificare se il valore deve essere considerato
con segno (
signed) o
senza segno (
unsigned). Se si omette
signed e
unsigned il tipo è considerato con segno:
Tipo |
Dimensione in byte |
Ordine di grandezza |
Valore minimo rappresentabile |
Valore massimo rappresentabile |
[signed] char |
1 |
centinaia |
-128 (-27) |
+127 (+27-1) |
unsigned char |
1 |
centinaia |
0 |
+255 (+28-1) |
[signed] short |
2 |
decine di migliaia |
-32,768 (+215) |
+32,767 (+215-1) |
unsigned short |
2 |
decine di migliaia |
0 |
+65,535 (+216) |
[signed] int |
4 |
miliardi |
-2,147,483,648 (-231) |
+2,147,483,647 (+231-1) |
unsigned int |
4 |
miliardi |
0 |
+4,294,967,295 (+232-1) |
[signed] long |
8 |
miliardi di miliardi |
-9,223,372,036,854,775,808 (-263) |
+9,223,372,036,854,775,807 (+263-1) |
unsigned long |
8 |
miliardi di miliardi |
0 |
+18446744073709551615 (+264-1) |
[signed] long long |
8 |
miliardi di miliardi |
-9,223,372,036,854,775,808 (-263) |
+9,223,372,036,854,775,807 (+263-1) |
unsigned long long |
8 |
miliardi di miliardi |
0 |
+18446744073709551615 (+264-1) |
Si tenga a mente l'
ordine di grandezza associato alla dimensione in byte. Sono utilissimi per fare conti grezzi (back of the envelope calculations) sulla fattibilità di una scelta di progetto.
Tipi come interpretazione dei byte di un oggetto
I
tipi definiscono l'interpretazione che si dà della sequenza di byte contenuti in un oggetto. L'intepretazione è definita da un insieme di
regole convenzionali che
trasformano una sequenza di bit in un valore nel dominio del tipo. Il C usa vari standard di interpretazione:
- caratteri (char): si usa la rappresentazione definita dallo standard ASCII a 8 bit
- interi senza segno (unsigned char, unsigned short, unsigned int, unsigned long, unsigned long long): si usa la rappresentazione binaria
- interi con segno (char, short, int, long, long long): si usa la rappresentazione binaria in complemento a due
- virgola mobile (float, double, long double): si usa la rappresentazione definita dallo standard IEEE 754
- Nota bene: Diversamente da Java in C la stessa regione di memoria può essere interpretata in modo diverso. Ad esempio, un blocco di 4 byte può essere interpretato come un oggetto int, come un oggetto float, come due oggetti short memorizzati consecutivamente, oppure come quattro oggetti char memorizzati consecutivamente. Questa caratteristica del C non risulta evidente accedendo agli oggetti tramite variabili, poiché il tipo di una variabile non cambia e quindi la memoria associata all'oggetto viene interpretata sempre allo stesso modo. Tuttavia, usando gli operatori * e & del C, che sono assenti in Java e che vedremo più avanti, è possibile accedere alla memoria interpretandola in qualsiasi modo uno voglia. Questo è allo stesso tempo un'opportunità e un rischio per il programmatore.
La funzione printf
L'interpretazione degli oggetti in base al tipo risulta evidente in C dal modo in cui è progettata la funzione
printf∞. La funzione permette di stampare sul canale standard di output (tipicamente il terminale) una stringa che può essere costruita a partire dai valori passati come parametri.
Esempio:
La funzione prende come primo parametro una stringa chiamata
stringa di formato. La stringa di formato può contenere zero o più specificatori di formato (ad esempio: %c, %d, %u, %ld, %lu %f) che definiscono l'interpretazione dei successivi parametri passati alla
printf. Gli specificatori verranno rimpiazzati nella stringa stampata dai valori dei successivi parametri passati alla funzione, considerandoli nello stesso ordine in cui appaiono nella stringa di formato.
Esempio:
int x =
10;
double y =
2.14;
printf("Il valore di x è %d, mentre il valore di y+1 è %f\n", x, y
+1);
// stampa: Il valore di x è 10, mentre il valore di y è 3.14
Alcuni degli specificatori di formato più usati sono:
Specificatore |
Tipi compatibili |
Interpretazione |
%d |
[signed] char, short, int |
intero a 8, 16 o 32 bit con segno |
%u |
unsigned char, unsigned short, unsigned int |
intero a 8, 16 o 32 bit senza segno |
%ld |
long |
intero a 64 bit con segno |
%lu |
unsigned long |
intero a 8, 16 o 32 bit senza segno |
%c |
[unsigned] char, short, int |
carattere avente come codice ASCII il valore numerico |
%f |
float, double |
numero in virgola mobile a 32 o 64 bit |
Si noti che gli specificatori
%d e
%u funzionano per tutti i valori di tipi a 4 byte o meno poiché in C qualsiasi valore intero a meno di 32 bit viene automaticamente promosso al corrispondente valore a 32 bit durante il passaggio dei parametri alla
printf. Allo stesso modo, ogni
float viene automaticamente promosso a
double.
Attenzione: la concordanza tra lo specificatore usato e il tipo del corrispondente valore passato è interamente
a carico del programmatore! Il massimo che ci si può aspettare dai compilatori più moderni è un avvertimento (warning), che comunque non impedisce di compilare il programma. Ad esempio:
#include <stdio.h>
int main
() {
float x =
6.28;
printf("%d\n", x
);
return 0;
}
Compiliamo ed eseguiamo il programma:
$ gcc test.c
test.c: In function ‘main’:
test.c:5:2: warning: format ‘%d’ expects argument of type ‘int’, but argument 2 has type ‘double’ [-Wformat]
$ ./a.out
-2075587288
Si noti che il programma stampa
-2075587288, che è l'interpretazione in complemento a 2 dei 4 byte dell'oggetto
x che è di tipo
float ed è stato assegnato con il valore
6.28. Quindi
printf interpreta il contenuto l'oggetto
x come se fosse di tipo
int.
Una descrizione dettagliata dell'uso delle stringhe di formato è disponibile
qui∞.
Espressioni in C
In C ci sono tre categorie di espressioni:
- Espressioni che denotano tipi
- Espressioni che denotano oggetti (Lvalue)
- Espressioni che denotano valori (Rvalue)
Espressioni che denotano tipi: sono ad esempio i nomi dei tipi primitivi come
int, ecc. Vedremo altri esempi più avanti. Le espressioni di tipo vengono usate nelle dichiarazioni (ma non solo), ad esempio per specificare il tipo di una variabile o il tipo del valore di ritorno e degli argomenti di una funzione.
Espressioni che denotano oggetti: sono chiamate
LValue∞ e sono caratterizzate da:
- un tipo e
- dall'indirizzo di un oggetto di quel tipo.
Ad esempio,
il nome di una variabile è un'espressione Lvalue che denota l'oggetto ad essa associato. Le espressioni Lvalue sono
modificabili se possono apparire alla sinistra (da cui la L=Left di Lvalue) di un assegnamento (
=) e sono
non modificabili altrimenti. Vedremo esempi più avanti.
Espressioni che denotano valori: sono chiamate
RValue∞ e sono caratterizzate da:
- un tipo e
- da un valore di quel tipo.
Ad esempio, un letterale numerico di tipo
int è un'espressione Rvalue di tipo
int che denota quel valore. Le espressioni Rvalue appaiono sul lato destro (da cui la R=Right di Rvalue) di un assegnamento (
=), come operandi di operatori logici, aritmetici e relazionali, come parametri attuali di funzioni, come espressioni di test per le istruzioni
if e
while, ecc.
Conversione automatica di Lvalue a Rvalue: (dereferenziazione automatica) tranni alcuni casi particolari, un
Lvalue è convertito automaticamente ad Rvalue∞ se usato nel contesto in cui ci si aspetta un Rvalue. Nella conversione, chiamata
dereferenziazione, l'espressione Rvalue risultante denota il valore memorizzato nell'oggetto denotato dall'Lvalue, e
il valore dell'indirizzo viene perso.
Esempio:
int x = 10;
int y = x+1;
Nel secondo assegnamento, la
x alla destra dell'operatore
= è un Lvalue poiché denota un oggetto, e viene dereferenziato all'Lvalue che denota il contenuto della variabile
x prima di essere sommato a 1.
Tipi puntatore: variabili puntatore e indirizzi
In C, un
puntatore è un valore che denota l'indirizzo di un oggetto di un certo tipo, oppure il valore speciale
NULL, usato per indicare che il puntatore non punta ad alcun oggetto.
Tipo puntatore: Se
T è un'espressione di tipo, allora
T* è un'espressione che denota il tipo di un puntatore a un oggetto di tipo
T.
Ad esempio,
int* è un'espressione che denota il tipo "puntatore a int". Inoltre,
int** è un'espressione che denota il tipo "puntatore a puntatore a int".
Una
variabile puntatore è una variabile (di tipo puntatore) che contiene quindi l'indirizzo di un oggetto, o
NULL.
Esempio:
double* p; // variabile puntatore
p = malloc(sizeof(double));
...
free(p);
In questo esempio, viene dichiarata una variabile
p di tipo puntatore a
double. Alla variabile viene assegnato l'indirizzo di un nuovo blocco allocato dinamicamente mediante
malloc. Si noti che l'oggetto viene allocato della dimensione giusta, data da
sizeof(double). La variabile
p "punterà" quindi al nuovo oggetto allocato, permettendo di modificarlo, come vedremo tra poco.
Operatore * ("all'indirizzo")
L'operatore
*, permette di accedere a un oggetto puntato da un puntatore per modificarlo o per leggerne il contenuto. L'operatore è definito come segue:
Operatore *: se
E è un Rvalue di tipo
T*, allora
*E (si legge: "all'indirizzo
E") è un Lvalue che denota l'oggetto di tipo
T all'indirizzo
E.
Esempio:
double* p;
// dichiarazione variabile puntatore
p = malloc
(sizeof(double));
// alloca oggetto double e lo fa puntare da p
*p =
3.14;
// assegna 3.14 all'oggetto puntato da p
printf("valore oggetto puntato da p=%f\n", *p
);
// stampa 3.14
free
(p
);
// dealloca l'oggetto puntato da p
Si noti che l'espressione
*p è un Lvalue modificabile di tipo
double.
Attenzione: nulla vieta al programmatore di scrivere:
double* p; // dichiarazione variabile puntatore
p = malloc(sizeof(int)); // alloca oggetto int e lo fa puntare da p
*p = 3.14; // ***errore: accesso a una zona di memoria in parte non allocata!
...
In questo caso, l'assegnamento
*p = 3.14 scrive gli 8 byte che iniziano all'indirizzo
p, di cui solo i primi 4 sono correttamente allocati! Il problema è che il programma non solo non dà alcun errore a tempo di compilazione, ma potrebbe non dare alcun errore a tempo di esecuzione in alcuni casi, con il rischio di emergere però in momenti inaspettati, rendendo il debugging molto difficoltoso.
Operatore & ("indirizzo di")
L'operatore
&, permette di ottenere l'indirizzo di un oggetto. L'operatore è definito come segue:
Operatore &: se
E è un Lvalue di tipo
T, allora
&E (si legge: "indirizzo di
E") è un Rvalue che denota un puntatore di tipo
T* all'oggetto
E.
Esempio:
double x;
// x è un Lvalue (oggetto) di tipo double
double* p = &x;
// &x è un Rvalue (puntatore) di tipo double* che denota l'indirizzo dell'oggetto x. Quindi p viene a "puntare" a x
*p =
3.14;
// *p è l'oggetto puntato da p, cioè x. Quindi *p e x sono equivalenti
printf("x = %f\n", x
);
// stampa 3.14
Si noti che
* e
& si annullano a vicenda:
- se E è un Lvalue, allora *&E è equivalente a E (all'indirizzo indirizzo di E c'è E)
- se E è un Rvalue di tipo puntatore, allora &*E è equivalente a E (l'indirizzo dell'oggetto all'indirizzo E è E)
Vediamo altri esempi. Sia
p una variabile di tipo
int*:
- &p è un Rvalue di tipo int** che denota l'indirizzo della variabile p
- *p è un Lvalue di tipo int che denota l'oggetto puntato da p
- *p+1 è un Rvalue di tipo int che denota il contenuto dell'oggetto puntato da p, più uno
- &(*p+1) è un'espressione non valida. Infatti, (*p+1) è un Rvalue e non si può prendere l'indirizzo di un Rvalue
- **p è un'espressione non valida. Infatti, *p è non è un puntatore
Aritmetica dei puntatori
In C è possibile sommare o sottrarre un intero a un puntatore, o fare la differenza di due puntatori.
Differenza fra puntatori: se
p e
q sono due puntatori dello stesso tipo
T*, allora
p-q denota la differenza degli indirizzi di
p e di
q, divisa per
sizeof(T). In altre parole,
p-q restituisce il numero di oggetti di tipo
T compresi tra gli indirizzi
p e
q, con il segno meno se
p<
q.
Esempio: se
p è un puntatore di tipo
int* che vale 1000 e
q è un puntatore di tipo
int* che vale 1024, allora
p-q è un intero che vale (1000-1024)/4 = -6.
Somma puntatore+intero: se
p è un puntatore di tipo
T* che denota un indirizzo
x ed
i è un intero, allora
p+i è un puntatore di tipo
T* che denota l'indirizzo
x+i*
sizeof(T). In altre parole,
p+i denota l'indirizzo dell'i-esimo oggetto di tipo
T che si incontra partendo dall'indirizzo
p. Questa operazione viene usata per
indicizzare array di oggetti.
Esempio: se
p è un puntatore di tipo
int* che vale 1000, allora
p+6 è un puntatore di tipo
int* che vale 1000+6*4 = 1024.
In base a quanto detto finora,
l'aritmetica dei puntatori consente di usare un qualsiasi blocco di memoria come se fosse un array!
Esempio: se
p è un puntatore di tipo
int* che vale 1000, allora
*(p+6) è il sesto oggetto
int a partire dall'indirizzo 1000.
Si noti che in C l'espressione
v[i] è equivalente a
*(v+i). Inoltre, per la commutatività della somma,
v[i] e
i[v] sono equivalenti!
Il seguente esempio mostra come allocare dinamicamente un array di 100 interi, che viene poi azzerato:
int i, *v; // dichiara una variabile intera i e un puntatore a intero v
v = malloc(100*sizeof(int)); // alloca un blocco contenente spazio per 100 int
for (i=0; i<100; i++) // scorre i 100 int del blocco e li mette tutti a zero
v[i] = 0;
Compatibilità tra puntatori
A meno di cast espliciti, in C si hanno le seguenti regole:
- Un oggetto puntatore di tipo T* può essere assegnato con:
- la costante 0 (NULL)
- un puntatore a un oggetto di tipo compatibile con T
- un puntatore di tipo void*
- Un oggetto puntatore di tipo void* può essere assegnato con:
- la costante 0 (NULL)
- un puntatore a un oggetto di qualsiasi tipo
Esempi:
int x;
float y;
void* p = &x; // ok, si può assegnare un int* a un void*
int* q = &x; // ok, si può assegnare un int* a un int*
int* r = NULL; // ok, si può assegnare NULL (0) a qualsiasi oggetto puntatore
int* s = &y; // errore, non si può assegnare un float* a un int*
int* t = (int*)&y; // ok, l'espressione a destra è di tipo int*
void* u = t; // ok, si può assegnare un puntatore di qualsiasi tipo a un oggetto void*
int* v = u; // ok, si può assegnare void* a un oggetto puntatore di qualsiasi tipo (in C, non in C++)
Tipo array
Vediamo ora il primo tipo non primitivo in C: l'array. L'array è un
tipo composto che denota una sequenza di oggetti dello stesso tipo, disposti in modo consecutivo in memoria.
Dichiarazione variabile array: Una variabile
v di tipo array di oggetti di tipo
T si dichiara così:
T v[D]
dove
D è un intero che denota la dimensione dell'array, cioè il numero di oggetti di tipo
T in esso contenuto. Secondo lo standard ANSI C (ISO C90),
D deve essere una costante determinabile a tempo di compilazione. Nel C99, può essere invece un valore determinato a tempo di esecuzione, come ad esempio il contenuto di una variabile intera.
Esempio:
int v[10];
In questo caso,
v è una variabile che denota un array di 10
int.
Particolarità degli Lvalue di tipo array. Un'espressione Lvalue di tipo array di
T è un
Lvalue non modificabile.
Ne consegue che
non è possibile copiare un array su un altro mediante un assegnamento, come illustrato dal seguente esempio:
int a[10];
int b[10];
...
a=b; // errore di compilazione qui
Si noti che l'assegnamento
a=b non è ammesso, essendo
a un'espressione Lvalue che denota un oggetto di tipo array.
Conversione automatica da array a puntatore: in ogni contesto in cui ci si aspetta un Rvalue, ogni Lvalue di tipo array di oggetti di tipo
T viene automaticamente convertito a un Rvalue di tipo
T* che denota l'indirizzo del primo oggetto dell'array. Eccezione a questa regola è l'argomento dell'operatore
sizeof.
Esempi:
int x
[10];
int* p=x;
// viene assegnato a p l'indirizzo del primo oggetto di x (equivalente a p=&x[0])
printf("%lu\n",
sizeof(x
));
// stampa 40, ottenuto come 10*sizeof(int)
Non esistono parametri formali di tipo array: se un parametro formale è dichiarato dal programmatore di tipo array di
T, il parametro è trattato dal compilatore come se fosse stato dichiarato di tipo
T*. Non esistono quindi parametri formali che denotano Lvalue di tipo array.
Esempio:
void foo
(int v
[10]) { // parametro formale v è di tipo int*
int x
[10];
// variabile locale x è di tipo array di 10 int
printf("%lu\n",
sizeof(v
));
// stampa 8, cioè il numero di byte di un puntatore (su sistemi operativi a 64 bit)
printf("%lu\n",
sizeof(x
));
// stampa 40, ottenuto come 10*sizeof(int)
}
Si noti che la dimensione dell'array in una dichiarazione di parametro formale viene ignorata. E' possibile quindi scrivere:
void foo(int v[]) { // v è di tipo int*
}
Passaggio di parametro di tipo array: in base alla regola di conversione automatica da array a puntatore, passare come parametro attuale un Lvalue di tipo array di
T equivale a passare l'indirizzo del primo oggetto dell'array. Non è possibile quindi passare array per copia a una funzione e il
passaggio è sempre per indirizzo:
void foo
(int q
[]) { // q è di tipo int*
q
[1]=
971;
}
int main
() {
int v
[20];
foo
(v
);
// passaggio per indirizzo dell'array
printf("%d\n", v
[1]);
// stampa 971
}
Questo è coerente con il fatto che non è possibile assegnare un array ad un altro. Infatti, il passaggio dei parametri implica un assegnamento automatico dei parametri attuali ai corrispondenti parametri formali.
Array multidimensionali
Gli array multidimensionali sono semplicemente array di array. Ad esempio:
int v[3][2];
è una dichiarazione che può essere tradotta come "v è un array di 3 array di 2 int". La variabile
v denota un oggetto aggregato costituito da tre oggetti array, ciascuno formato da due oggetti int. L'oggetto può essere interpretato come una matrice bidimensionale di dimensione 3x2 (3 righe e 2 colonne) rappresentata in memoria secondo il formato row-major, cioè le righe sono disposte consecutivamente in memoria. Nel nostro caso, ogni riga della matrice è di 2*
sizeof(int)=8 byte e l'oggetto
v è di 3*2*
sizeof(int)=24 byte.
Consideriamo ora l'assegnamento:
v[2][1] = 10;
Si noti che è equivalente a:
*(*(v+2)+1) = 10;
che viene valutata come segue:
- Valutazione di v+2: l'Lvalue v di tipo array di array di 2 int viene convertito a Rvalue di tipo puntatore ad array di 2 int, e poi gli viene sommato 2. Per l'aritmetica dei puntatori, se x è l'indirizzo di v, v+2 denota l'indirizzo x+2*sizeof(array di 2 int), cioè x+16.
- Valutazione di *(v+2)+1: l'Lvalue *(v+2) (cioè v[2]) di tipo array di int viene convertito a Rvalue di tipo puntatore a int, e poi gli viene sommato 1. Per l'aritmetica dei puntatori, se y=x+16 è l'indirizzo di *(v+2) (ovvero v[2]), allora *(v+2)+1 (ovvero v[2]+1) denota l'indirizzo y+1*sizeof(int), cioè y+4=x+16+4.
- L'indizzo dell'oggetto *(*(v+2)+1) (cioè *(v[2]+1), ovvero v[2][1]) è pertanto x+16+4, dove x è l'indirizzo di v. L'espressione v[2][1] denota quindi il secondo oggetto della terza riga della matrice, a cui viene assegnato 10.
Si noti che in una dichiarazione di array multidimensionale,
la prima dimensione dell'array può mancare. Consideriamo ad esempio il seguente prototipo di funzione:
void foo(int v[3][2]);
Poiché i parametri formali di tipo array di
T sono interpretati come puntatori a
T, il prototipo è equivalente a:
void foo(int (*v)[2]);
cioè
v è un puntatore ad array di 2
int. Si noti che nella conversione a puntatore, la dimensione dell'array si perde. Pertanto
foo può anche essere scritto come:
void foo(int v[][2]);
La seconda dimensione non può mancare altrimenti nell'aritmetica dei puntatori applicata per valutare l'espressione
v[i][j] non si potrebbe sapere quanti byte saltare per raggiungere la riga di indice i.
Regole di precedenza e associatività degli operatori
Si tenga sempre a portata di mano la
tabella delle precedenze degli operatori C∞.
Precedenza di un operatore rispetto a un altro stabilisce quale operatore va eseguito per primo.
Esempio: in base alla precedenza maggiore del
* rispetto al
+, la seguente espressione aritmetica
a+b*c è equivalente a
a+(b*c).
Associatività di un operatore: ordine in cui vengono eseguiti operatori con la stessa precedenza. Si hanno due possibilità: da sinistra verso destra o da destra verso sinistra.
Esempio: in base all'associatività da destra verso sinistra dell'operatore ternario ? (espressione condizionale), in C l'espressione:
a ? b : c ? d : e
è equivalente a:
a ? b : (c ? d : e).
- Nota: In altri linguaggi, come PHP, l'associatività è da sinistra verso destra, per cui l'espressione precedente sarebbe equivalente a (a ? b : c) ? d : e.
Strutture
Come gli array, anche le strutture sono aggregati di oggetti. Il tipo struttura è un
tipo composto che specifica il numero e tipo dei campi di un aggregato. Diversamente dagli array, che sono aggregati di oggetti di tipo omogeneo, in una struttura i campi possono avere tipo diverso.
Dichiarazione di struttura: una struttura si dichara come nell'esempio seguente.
struct S {
int x, y;
double z;
};
La dichiarazione introduce un tipo struttura
S costituito da due
int e un
double, identificati rispettivamente dai nomi dei campi
x,
y e
z. Il tipo struttura dichiarato è identificato dal
tag struct S.
Spazio dei nomi tag delle strutture: i tag delle strutture formano uno spazio di nomi distinti da quelli degli altri identificatori (es. di variabili, ecc.).
Esempio:
struct x { int a, b; };
int x; // legale, poiché x è diverso da struct x
Dichiarazione di variabili struttura: Il seguente frammento di programma dichiara una variabile
v di tipo
struct S:
struct S v;
La
dimensione di una struttura sarà almeno pari alla somma delle dimensioni dei suoi campi. Nel nostro esempio:
sizeof(struct S) >=
sizeof(int) +
sizeof(int) +
sizeof(double). Vedremo più avanti perché potrebbe non valere l'uguaglianza stretta.
Accesso ai campi di una struttura: per accedere ai campi di una struttura è possibile usare l'operatore . (punto). Se
E è un Lvalue di tipo struttura e
c è un suo campo, allora l'espressione
E.c è un Lvalue che denota il campo
c della struttura
E.
Esempi:
v.
x =
10;
// scrittura campo x
v.
y =
20;
// scrittura campo y
v.
z =
3.14;
// scrittura campo z
printf("x=%d, y=%d, z=%f\n", v.
x, v.
y, v.
z);
// lettura di campi di una struttura
Puntatori a strutture: mostriamo ora come accedere ai campi di una struttura mediante un puntatore.
Esempio:
struct S* p;
p = malloc(sizeof(struct S)); // allocato in heap spazio per contenere un oggetto struct S
(*p).x = 10;
(*p).y = 20;
(*p).z = 3.14;
Si noti che, poiché l'operatore
. (punto) ha priorità più alta dell'operatore
*, è necessario usare le parentesi per specificare che si intende accedere all'oggetto
*p puntato da
p.
Operatore -> per accedere ai campo di una struttura: per rendere più agevole l'accesso ai campi di una struttura, in C è possibile usare l'operatore
-> come forma abbreviata: se
E è un puntatore a una struttura che ha un campo
c, allora
E->c è equivalente a
(*E).c.
Esempio:
struct S* p;
p = malloc(sizeof(struct S)); // alloca in heap spazio per contenere un oggetto struct S
p->x = 10; // equivalente a (*p).x = 10;
p->y = 20; // equivalente a (*p).y = 20;
p->z = 3.14; // equivalente a (*p).z = 3.14;
Strutture sono Lvalue modificabili: le espressioni che denotano oggetti struttura sono
Lvalue modificabili. Pertanto, è possibile assegnare una struttura all'altra:
struct S v;
struct S q;
...
v = q; // ok, copia byte a byte del contenuto di q in v
Allo stesso modo, è possibile
passare una struttura come parametro per copia a una funzione:
void foo
(struct S q
) {
q.
x =
55;
// modifica la copia locale della struttura, non la variabile v del main...
q.
y =
12;
q.
z =
7.9;
}
int main
() {
struct S v;
v.
x =
10;
v.
y =
20;
v.
z =
3.14;
foo
(v
);
// passaggio di parametro struttura per copia: copia byte a byte del contenuto di v nel parametro formale q di foo
printf("x=%d, y=%d, z=%f\n", v.
x, v.
y, v.
z);
// stampa x=10, y=20, x=3.14
return 0;
}
Poiché il passaggio per copia potrebbe essere inefficiente, soprattutto per strutture di grandi dimensioni, è possibile
passare una struttura per indirizzo. Oltre a essere più efficiente, poiché viene passato solo un indirizzo invece dell'intera struttura, questo consente anche di fare side-effect sull'oggetto passato:
void foo
(struct S* q
) {
q->x =
55;
// fa side-effect sulla variabile v del main...
q->y =
12;
q->z =
7.9;
}
int main
() {
struct S v;
v.
x =
10;
v.
y =
20;
v.
z =
3.14;
foo
(&v
);
// passaggio di parametro struttura per indirizzo: copia dell'indirizzo di v nel parametro formale q di foo permette a foo di modificare v
printf("x=%d, y=%d, z=%f\n", v.
x, v.
y, v.
z);
// stampa x=55, y=12, x=7.9
return 0;
}
Esempio delle liste collegate.
Le strutture insieme ai puntatori permettono di realizzare strutture dati collegate. Consideriamo ad esempio una lista collegata semplice di interi. Il tipo nodo della lista può essere dichiarato come segue:
struct node {
int elem;
struct node* next;
};
Dove
elem è il dato contenuto nel nodo e
next il puntatore al prossimo nodo della lista. Un nodo può essere creato e inizializzato come segue:
struct node* p; // dichiara variabile puntatore a nodo
p = malloc(sizeof(struct node)); // alloca spazio in heap per il nodo
p->elem = 10; // inizializza campo dato del nodo a 10
p->next = NULL; // non c'è un successore nella lista
...
free(p); // dealloca il nodo
Dichiarazioni di identificatori di tipo mediante typedef
Usando al parola chiave
typedef, in C è possibile definire degli identificatori di tipo che possono essere usati come
alias di altri tipi, cioè come nomi equivalenti.
Esempio:
typedef int intero; // dichiara l'identificatore intero come alias di tipo per int
intero x[10]; // x è una variabile di 10 int
typedef char* stringa; // dichiara l'identificatore stringa come alias di tipo per char*
stringa s; // s è una variabile di tipo char*
typedef struct S S; // dichiara l'identificatore S come alias di tipo per struct S
S* v; // v è una variabile di tipo puntatore a struct S
Espressioni che denotano tipi
Tutorial∞ (in inglese)
Puntatori a funzione
In C è possibile avere puntatori a funzione che, invece di denotare indirizzi di oggetti, denotano indirizzi di codice. Dereferenziando un puntatore a funzione viene invocata la funzione a cui il puntatore si riferisce. La chiamata avviene come una normale invocazione, passando parametri e ricevendo eventualmente un valore di ritorno. I puntatori a funzione permettono di ottenere il dynamic binding, in cui il codice effettivamente mandato in esecuzione da una chiamata dipende dal valore del puntatore, diversamente dalle chiamate tradizionali in cui la funzione chiamata è nota a tempo di compilazione.
Esempio:
int somma
(int a,
int b
) { return a+b;
}
int prodotto
(int a,
int b
) { return a*b;
}
int main
() {
int (*p
)(int,
int);
// p è una variabile di tipo puntatore a funzione che prende come parametri due interi e restituisce un intero
p = somma;
// p ora punta alla funzione somma
int x = p
(10,
20);
// invocazione di funzione tramite puntatore: viene eseguita la funzione somma
printf("%d\n", x
);
// stampa 30
p = prodotto;
// p ora punta alla funzione prodotto
int y = p
(10,
20);
// invocazione di funzione tramite puntatore: viene eseguita la funzione prodotto
printf("%d\n", y
);
// stampa 200
return 0;
}
Un altro esempio di uso dei puntatori a funzione è quello dei comparatori, in cui è possibile scrivere funzioni polimorfiche che possono manipolare tipi di dato differenti. Consideriamo ad esempio una funzione che calcola il minimo di un array di oggetti di tipo generico presi da un dominio totalmente ordinato. La funzione avrà prototipo:
void* min(void* v, unsigned n, unsigned k, int (*comp)(void* pa, void* pb));
dove:
- v è un array di oggetti
- n è il numero di oggetti contenuti in v
- k è la dimensione in byte di ciascun oggetto di v
- comp è un puntatore a una funzione comparatore che, dati i puntatori pa e pb a due oggetti a e b, restituisce:
- un intero negativo se a è minore di b
- zero se a è uguale a b
- un intero positivo se a è maggiore di b
- la funzione min restituisce l'indirizzo dell'oggetto più piccolo dell'array, o NULL se l'array è vuoto
Questo è un comparatore per oggetti
int:
int int_comp(void* pa, void* pb) {
int a = *(int*)pa;
int b = *(int*)pb;
return a-b;
}
Questo è invece un comparatore per oggetti
double:
int double_comp(void* pa, void* pb) {
double a = *(double*)pa;
double b = *(double*)pb;
return a<b ? -1 : a==b ? 0 : 1;
}
Vediamo ora come definire la funzione che calcola il minimo:
void* min(void* v, unsigned n, unsigned k, int (*comp)(void* pa, void* pb)) {
if (n == 0) return NULL;
void* min = v;
int i;
for (i=k; i<n*k; i+=k)
if (comp(v+i, min) < 0) min = v+i;
return min;
}
Vediamo infine un programma di prova per verificare il funzionamento delle funzioni scritte:
#include <stdio.h>
int main
() {
double v1
[6] =
{ 4.5,
7.1,
9.3,
3.6,
0.7,
2.6 };
double* res1 = min
(v1,
6,
sizeof(double), double_comp
);
printf("min = %f\n", *res1
);
int v2
[6] =
{ 7,
9,
5,
2,
4,
8 };
int* res2 = min
(v2,
6,
sizeof(int), int_comp
);
printf("min = %d\n", *res2
);
return 0;
}
Il preprocessore del linguaggio C: macro e compilazione condizionale
Il preprocessore è il modulo che interviene nel primo stadio della compilazione di un programma e si occupa di trasformare il testo del programma di input elaborando le
direttive di preprocessamento che iniziano per
#. Nel testo "preprocessato" le direttive non compaiono più. Il risultato del preprocessamento viene poi passato al secondo stadio della compilazione, che analizza sintatticamente il programma e lo traduce in codice assembly.
Definizione e uso di macro senza parametri.
Le
macro sono dei nomi simbolici che fungono da alias per sequenze di token. Le macro vengono dichiarate mediante la direttiva
#define.
Il seguente esempio definisce una macro chiamata
PI che denota la sequenza di token
2.14 + 1, e che poi viene usata in un assegnamento:
#define PI 2.14 + 1
double x = PI;
Vediamo come viene elaborato questo testo dal preprocessore:
$ gcc -E esempio.c
# 1 "esempio.c"
# 1 "<built-in>"
# 1 "<command-line>"
# 1 "esempio.c"
double x = 2.14 + 1;
Ignoriamo da ora in poi le righe iniziali della forma
# 1 "...", e concentriamoci sull'ultima riga. Notiamo che l'occorrenza del nome di macro
PI viene rimpiazzato (espanso) con la sequenza di token
2.14 + 1 ad esso associata.
Definizione e uso di macro con parametri.
Le macro con parametri si definiscono come nel seguente esempio:
#define PRODOTTO(a,b) a*b
Si noti che tra
PRODOTTO e
(a,b) non deve esserci alcuno spazio. Nel nostro esempio,
a e
b sono i parametri della macro, analoghi ai parametri formali di una funzione.
Una volta definite, le macro con parametri di possono usate (espanse) come nel seguente esempio, in modo simile a quanto si fa quando si invoca una funzione:
#define PRODOTTO(a,b) a*b
int x = PRODOTTO(2,3);
Il preprocessore sostituisce ogni occorrenza dei parametri della macro con i parametri passati:
$ gcc -E prova2.c
int x = 2*3;
Quindi nell'espansione della macro,
a viene rimpiazzato da
2 e
b da
3.
Tranelli nell'uso di macro.
Consideriamo nuovamente la macro
PRODOTTO definita sopra, e consideriamone il seguente uso:
#define PRODOTTO(a,b) a*b
int x = PRODOTTO(2+3,4);
Preprocessando il testo otteniamo:
$ gcc -E prova3.c
int x = 2+3 * 4;
che fornisce un risultato diverso da quello che ci saremmo aspettati (14 invece di 20)!
Appare evidente che dobbiamo modificare la definizione della macro per tenere conto di eventuali problemi connessi con le priorità degli operatori:
#define PRODOTTO(a,b) (a)*(b)
int x = PRODOTTO(2+3,4);
preprocessando il testo otteniamo il seguente risultato corretto:
$ gcc -E prova4.c
int x = (2+3)*(4);
Sembrerebbe che sia sufficiente parentesizzare ogni occorrenza dei parametri della macro per evitare problemi legati alle priorità degli operatori. Consideriamo però il seguente esempio:
#define SOMMA(a,b) (a)+(b)
int x = SOMMA(2,4)*5;
Preprocessando il testo otteniamo:
$ gcc -E prova5.c
int x = (2)+(4)*5;
che assegna a
x 22 invece di 30 come ci aspetteremmo.
La soluzione per evitere ogni problema legato alle priorità degli operatori è:
- parantesizzare ogni occorrenza degli argomenti della macro
- parantesizzare l'intesa sequenza di token associata alla macro
Esempio:
#define SOMMA(a,b) ((a)+(b))
int x = SOMMA(2,4)*5;
che preprocessato fornisce:
$ gcc -E prova5.c
int x = ((2)+(4))*5;
Che ora assegna a
x il risultato corretto.
Compilazione condizionale.
Il preprocessore C fornisce utilissimi costrutti per la compilazione condizionale, cioè la possibilità di includere/escludere determinate parti di programma dalla compilazione.
La direttiva
#ifdef nome-macro ... #endif consente di includere nel programma una porzione di testo solo se un certo nome di macro è stato definito.
Esempio:
#define PIPPO
pippo
#ifdef PIPPO
pluto
#endif
paperino
Preprocessando il testo otteniamo:
$ gcc -E prova6.c
pippo
pluto
paperino
Si noti che ovviamente pippo pluto paperino non costituiscono un programma C valido, per cui lo stadio successivo (compilazione) fornirebbe un errore. Mostriamo questo esempio volutamente per mostrare come il
preprocessore funzioni come un elaboratore di testi generici, anche non necessariamente programmi C.
Se invece
PIPPO non fosse stata definito:
// #define PIPPO
pippo
#ifdef PIPPO
pluto
#endif
paperino
avremmo ottenuto:
$ gcc -E prova7.c
pippo
paperino
cioè la porzione inclusa tra
#ifdef PIPPO ed
#endif sarebbe stata scartata.
La direttiva
#ifndef nome-macro ... #endif include la porzione compresa tra
#ifndef nome-macro ed
#endif solo se
nome-macro non è stato definito.
Esempio:
#define PIPPO
pippo
#ifndef PIPPO
pluto
#endif
paperino
$ gcc -E prova8.c
pippo
paperino
La direttiva
#if condizione ... #endif include la porzione compresa tra
#if nome-macro ed
#endif se e solo se l'espressione
condizione è diversa da zero.
Esempio:
#define X 10
pippo
#if X > 5 && !defined(Y)
pluto
#endif
paperino
$ gcc -E prova9.c
pippo
pluto
paperino
Dove
defined(nome-macro) vale 1 se e solo se
nome-macro è stata precedentemente definita con
#define.
E' anche possibile avere dei rami "else" ed "elif" per
#ifdef,
#ifndef e
#if come illustrato nei seguenti esempi:
#define X 7
pippo
#if X == 0
X vale zero
#elif X < 5
X è compreso tra 1 e 4
#elif X < 10
X è compreso tra 5 e 9
#else
X è maggiore di 9
#endif
pluto
$ gcc -E prova10.c
pippo
X è compreso tra 5 e 9
paperino
Le direttive di compilazione condizionare possono anche essere annidate:
// #define X 7
#ifdef X
#if X >= 0
X è non negativo
#else
X è negativo
#endif
#else
X è indefinito
#endif
$ gcc -E prova11.c
X è indefinito
L'uso delle direttive di compilazione condizionale è molto utile ad esempio per attivare o disattivare funzionalità di debugging, ad esempio stampe a video inserite in punti opportuni nel programma per analizzarne il funzionamento e il contenuto delle variabili:
#include <stdio.h>
// #define DEBUG
int somma
(int x,
int y
) {
#ifdef DEBUG
printf("in somma(): x=%d, y=%d\n", x, y
);
#endif
return x+y;
}
int main
() {
#ifdef DEBUG
printf("inizio programma\n");
#endif
printf("somma = %d\n", somma
(10,
20));
#ifdef DEBUG
printf("fine programma\n");
#endif
return 0;
}
$ gcc prova-debug.c
$ ./a.out
somma = 30
Se invece la macro
DEBUG fosse stata definita, decommentando la direttiva
#define DEBUG avremmo ottenuto:
$ gcc prova-debug.c
$ ./a.out
inizio programma
in somma(): x=10, y=20
somma = 30
fine programma
Si noti che è possibile definire una macro dalla riga di comando usando l'opzione
-D di
gcc:
$ gcc prova-debug.c -DDEBUG
$ ./a.out
inizio programma
in somma(): x=10, y=20
somma = 30
fine programma
In questo modo, la macro
DEBUG risulta definita già prima dell'inizio del preprocessamento del programma.
Realizzazione di tipi di dato astratti in C
Dispensa∞
Compilazione incrementale (make)
Il programma
make è un interprete di script (i "makefile") che servono per automatizzare processi di costruzione di progetti costituiti da file diversi.
Se eseguito senza parametri, il comando
make esegue lo script chiamato
Makefile (o
makefile) presente nella directory corrente.
Uno script di
make è costituito generalmente da:
- dichiarazioni di macro
- definizione di regole (rules)
Le
regole sono l'unità elementare di esecuzione di un programma
make, e servono per creare file a partire da altri file. Ogni regola è costituita da:
- Un input: sequenza di nomi di file chiamati "dipendenze"
- Un output: nome di file chiamato "target"
- Un'operazione che crea l'output a partire dall'input: uno o più comandi di shell
Una regola ha la seguente struttura sintattica:
output : [ input1 input2 input3 ... ]
[ TAB--> comando1 ]
[ TAB--> comando2 ]
dove:
- output è il nome del file di output della regola (il target)
- input1, input2, ecc. sono i nomi dei file di input della regola (le dipendenze)
- comando1, comando2, ecc. sono le righe di comando che servono per generare il file output (target) a partire dai file input
- TAB--> è il carattere di tabulazione
- le parentesi quadre indicano l'opzionalità di una parte della regola
Si noti che
ciascun comando inizia necessariamente con un carattere di tabulazione (è una vecchia e infelice scelta del programma
make mantenuta per retro-compatibilità).
Esempio:
prog: main.c somma.c somma.h
gcc -o prog main.c somma.c
Si noti che:
- prog è il target, ed è il file generato dal comando gcc
- main.c, somma.c e somma.h sono i file di input (dipendenze), che servono per costruire il target prog
- il comando gcc -o prog main.c somma.c genera il file eseguibile prog (target della regola) compilando il programma formato dai file main.c, somma.c e somma.h (che assumiamo sia incluso da somma.c e main.c)
Esecuzione di una regola. Avviene come segue:
- vengono eseguite tutte le regole aventi come target input1, input2, ecc., se definite
- se non esiste alcun file chiamato output, oppure il file output esiste già e la sua data di modifica è più vecchia di almeno uno dei file input1, input2, ecc., allora vengono eseguiti i comandi comando1, comando2, ecc., nell'ordine in cui appaiono
L'esecuzione di uno script di
make parte dall'esecuzione della prima regola che appare nel testo. E' possibile far partire l'esecuzione da una regola qualunque invocando il comando
make nome, dove
nome è il target della regola che si intende eseguire.