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: Function ereg() is deprecated in /home/demetres/public_html/didattica/ae/libs/Wakka.class.php on line 648 Ingegneria degli Algoritmi: Appunti sul linguaggio C

Ingegneria degli Algoritmi

Corso di Laurea in Ingegneria Informatica e Automatica - A.A. 2014-2015

HomePage | Avvisi | Diario lezioni | Programma | Materiale didattico | Esami | Forum | Login
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":

hello.c
#include <stdio.h>

int main() {
    printf("Hello World\n"); // stampa
    return 0;
}


Mettiamolo a confronto con la relativa versione Java:

HelloWorld.java
public class HelloWorld {
    public static void main(String[] args) {
        System.out.println("Hello World"); // stampa
    }
}


Osserviamo che:


Usando i compilatore gcc in ambienti come Linux, UNIX, MacOS, CygWin, ecc., il programma può esserere compilato con il comando:

$ gcc hello.c


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:

$ ./a.out
Hello World


Per creare un eseguibile con un nome diverso, usare l'opzione -o:

$ gcc hello.c -o hello


Ora verrà creato un file eseguibile chiamato hello:

$ ./hello
Hello World


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:


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:
  1. 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!
  2. 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:


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:



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:
printf("Prova\n");


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:

test.c
#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:

  1. Espressioni che denotano tipi
  2. Espressioni che denotano oggetti (Lvalue)
  3. 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:

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:

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:

Vediamo altri esempi. Sia p una variabile di tipo int*:

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:

  1. 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*
  2. 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:


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


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:
  1. v è un array di oggetti
  2. n è il numero di oggetti contenuti in v
  3. k è la dimensione in byte di ciascun oggetto di v
  4. 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
  5. 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:

esempio.c
#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:

prova2.c
#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:

prova3.c
#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:

prova4.c
#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:

prova5.c
#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 è:

Esempio:

prova6.c
#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:
prova6.c
#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:

prova7.c
// #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:
prova8.c
#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:
prova9.c
#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:

prova10.c
#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:

prova11.c
// #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:

prova-debug.c
#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:


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:


Una regola ha la seguente struttura sintattica:

output : [ input1 input2 input3 ... ]
[ TAB--> comando1 ]
[ TAB--> comando2 ]


dove:

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:

Makefile
prog: main.c somma.c somma.h
    gcc -o prog main.c somma.c


Si noti che:

Esecuzione di una regola. Avviene come segue:
  1. vengono eseguite tutte le regole aventi come target input1, input2, ecc., se definite
  2. 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.

Valid XHTML 1.0 Transitional :: Valid CSS :: Powered by Wikka Wakka Wiki 1.1.6.3
Page was generated in 0.1522 seconds