====== Esercitazione 3 ====== Dove si approfondisce la conoscenza delle fasi di preprocessing, compilazione e linking, si prova ad implementare delle liste generiche in C e si studiano piu' approfonditamente le caratteristiche dei ''valgrind'' ed alcune opzioni di ''gcc''. Rassegnadoci all'idea che un programmo che passa tutti test puo' nondimeno essere scorretto .... ===== Esercizio 1: Getting started -- Preprocessing, compilazione e linking ===== 1) Compilare ed eseguire il seguente programma: #include #include int main (void) { double x=3.0; printf("Radice = %f\n",sqrt(x)); return 0; } salvato nel file //ff.c// con gcc -Wall -pedantic ff.c Chi segnala un errore? E' fallita la fase di preprocessing, la compilazione o il linking? Cosa contine il modulo oggetto se specifico l'opzione -c? Come si risolve il problema? 2) Cosa accade se eliminiamo la linea #include ? A questo punto cosa va storto? Sapete interpretare i messaggi a video e stabilire chi li ha scritti e perche'? Viene generato l'eseguibile? 3) Generare il modulo oggetto con gcc -Wall -pedantic -c ff.c Utilizzare //objdump, nm, readelf// per capire cosa contengono la tabella di rilocazione, la tabella dei simboli esportati ed esterni, le sezioni data, BSS e codice. (utilizzare il man e cercare su google). 4) Usare l'opzione //-E// e la //-S// del gcc: che cosa succede? Cosa accade specificando il flag -g assieme a -S? ===== Esercizio 2: Macro con parametri, macro SOMMA ===== Usare le macro con parametri per definire una macro che somma (operatore +) i propri argomenti #define SOMMA(X,Y,Z) ...... e testarla in un opportuno main. Valutare le differenze con una funzione di prototipo int SOMMA(int X,int Y, int Z); ===== Esercizio 3: Macro con parametri, macro FATTORIALE ===== Scrivere una macro con parametri che calcoli il fattoriale di un numero N, passato come parametro e ne stampi il risultato. Ad esempio, posso utilizzare la macro per calcolare il fattoriale di 4+1 con FATTORIALE(4+1) La macro non deve fare assunzioni su come verranno passati i parametri. Che accade annidando due chiamate della macro? Ad esempio FATTORIALE(FATTORIALE(4+1)) ===== Esercizio 4. Liste generiche in C ===== In questo esercizio si richiede di realizzare alcune funzioni che lavorano su liste generiche in C. Una lista generica e' rappresentata con la seguenti struct typedef struct elem { /** chiave */ void * key; /** informazione */ struct elem * next; } elem_t; typedef struct { /** la testa della lista */ elem_t * head; /** la funzione per confrontare due chiavi */ int (* compare) (void *, void *); /** la funzione per copiare una chiave */ void * (* copyk) (void *); } list_t; la prima struttura (''elem_t'') rappresenta un nodo della lista generica. Ogni nodo contiene una chiave ''key'' che puo' avere tipo qualsiasi. La seconda struttura (''list_t'') permette di definire una particolare lista a partire da quella generica. Per farlo bisogna fornire due funzioni: * ''compare'' permette di confrontare due chiavi, ritorna 0 se sono uguali ed un valore diverso da 0 altrimenti * ''copyk'' crea una copia della chiave (allocando la memoria necessaria) e ritorna il puntatore alla copia (se tutto e' andato bene) o NULL (se si e' verificato un errore) Si chiede di realizzare le funzioni che permettono di creare/distruggere una lista generica e quelle per inserire ed estrarre un elemento generico. Realizzare un main di test che prova ad istanziare la lista in due versioni: una prima a valori interi usando le seguenti funzioni per il confronto e la copia: /** funzione di confronto per lista di interi \param a puntatore intero da confrontare \param b puntatore intero da confrontare \retval 0 se sono uguali \retval p (p!=0) altrimenti */ int compare_int(void *a, void *b) { int *_a, *_b; _a = (int *) a; _b = (int *) b; return ((*_a) - (*_b)); } /** funzione di copia di un intero \param a puntatore intero da copiare \retval NULL se si sono verificati errori \retval p puntatore al nuovo intero allocato (alloca memoria) */ void * copy_int(void *a) { int * _a; if ( ( _a = malloc(sizeof(int) ) ) == NULL ) return NULL; *_a = * (int * ) a; return (void *) _a; } e una seconda che ha come chiavi stringhe usando analoghe funzioni per la copia ed il confronto. ===== Esercizio 5. Approfondiamo l'uso di valgrind (e alcune opzioni utili di gcc ...) ===== Compilare ed eseguire il codice seguente usando valgrind #include #include #include #include #define N 5 int main(void) { int * a; int i; if ( ( a = malloc(N*sizeof(int))) == NULL ) return EXIT_FAILURE; srand(time(NULL)); i=0; while (i che problemi vengono segnalati ? Perche' ? Provare a compilare (dopo averlo salvato in file.c) usando le seguenti opzioni di "gcc" che permettono di rilevare altri errori statici: bash$ gcc -O -pedantic -Wall -Wextra -Wformat=2 -ggdb -o exe file.c viene segnato qualcosa ? Perche' ? Utilizzare le opzioni sopra e valgrind per analizzare il comportamento dei programmi in {{:informatica:sol:laboratorio11:esercitazioni:vgrind.tar|vgrind.tar}}. Alcuni errori sono segnalati dal compilatore, altri da valgrind, altri no nsono segnalati da nessuno dei due, a significare che una programma che compila senza warning e viene eseguito senza proteste da valgrind puo' comunque essere scorretto ... Attenzione: * non confondete l'opzione ''-o'' (minuscolo) con l'opzione ''-O'' (maiuscolo) * la ''-o'' serve a dare un nome specifico al file eseguibile (''exe'' nel comando sopra) * L'opzione ''-O'' richiede l'esecuzione dell'ottimizzatore del codice, il che ha, come effetto collaterale, quello di permettere la rilevazione di un maggior numero di situazioni che meritano di essere segnalate dalle opzioni di warning. Il ''gcc'' e` un sistema ricco di opzioni (qualche centinaio) le quali possono essere attivate o meno (oltre che esplicitamente a linea di comando) come default a seconda della compilazione a cui il compilatore stesso e` stato sottoposto. Mentre il risultato della compilazione - di un programma corretto ;) - non viene influenzato da un punto di vista funzionale, l'uso di opzioni diverse o di versioni diverse del compilatore puo` comportare una maggiore o minore capacita` di rilevazione di possibili "anomalie" del codice a tempo di compilazione (come anche una diversa capacita` di ottimizzazione - nel tempo o nello spazio - dell'eseguibile prodotto) e addirittura eseguibili con un comportamento funzionalmente diverso se il codice sorgente contiene errori.