LLS'05/06: Testi delle esercitazioni


Gli esercizi riportati di seguito devono essere svolti da tutti gli studenti, direttamente in laboratorio o, in seguito, nel corso della settimana. Esercitazione successive assumono che gli studenti abbiano effettuato, in laboratorio o per proprio conto, tutte le esercitazioni precedenti.

Fanno eccezione gli esercizi marcati con Avanzato, che sono opzionali; per questi esercizi, il numero di asterischi indica la difficoltà prevista. Si suggerisce di affrontare gli esercizi avanzati (specialmente quelli con più di un asterisco) dopo aver completato quelli regolari, oppure più avanti nel corso.


Indice esercitazioni:
Prima - Seconda - Terza - Quarta - Quinta
Sesta - Settima - Ottava - Nona - Decima

  1. Prima esercitazione
    1. HelloWorld. Scrivere con un editor di testi a propria scelta (vi, Emacs, ecc.) il codice sorgente del programma HelloWorld.c visto a lezione. Il programma deve stampare in output il testo "Hello, world!" e terminare con codice di ritorno 0.

      Compilare il programma usando il comando (da riga di comando) gcc -o HelloWorld HelloWorld.c (gcc è il compilatore C del progetto GNU, in assoluto il più diffuso al mondo; l'opzione -o indica in che file mettere l'eseguibile risultante) e verificare che funzioni come atteso.

    2. Echo. Ripetere l'esercizio precedente scrivendo il programma Echo.c (che stampa i propri argomenti di riga di comando sull'output, uno per ogni riga, preceduto dal proprio numero d'ordine) visto a lezione. Ci si ricordi di includere il file di intestazione stdio.h per accedere alla funzione di libreria printf().

      Si modifichi poi il programma definendo una macro FORMATO al posto della stringa di formato usata nella funzione printf(), e si usi la macro nella chiamata a printf().

    3. Uso dei file .h. Si sposti la definizione della macro FORMATO in un file formato.h, e si includa questo file all'interno del programma principale echo.c. Verificare che la compilazione produce lo stesso effetto che nel caso precedente.

    4. Fasi della compilazione. Il compilatore gcc normalmente effettua l'intero processo di compilazione, da codice sorgente C ad eseguibile. Tuttavia, è possibile interrompere l'elaborazione in punti intermedi, ed osservare i risultati. In particolare:
      • l'opzione -c termina il processo prima di invocare il linker, lasciando nella directory di lavoro il file .o corrispondente al file .c compilato.
      • l'opzione -E termina il proceso prima di invocare il compilatore, lasciando nella directory di lavoro il file .c risultante dal pre-processor.
      Si esamini il file prodotto dal pre-processor (suggerimento: il vostro codice sarà in fondo al file), e si verifichi che passando al compilatore direttamente questo secondo file, si ottiene lo stesso programma ottenuto in precedenza.

      Avanzato ***: il comando objdump consente di vedere il contenuto dei file .o. Usate objdump -t file.o per vedere i simboli definiti all'interno del file, o objdump -d file.o per vedere il codice assembler compilato dal vostro codice C. Suggerimento: compilate il codice con gcc -g -c file.c e disassemblatelo con objdump -S file.o per vedere, riga per riga, come il vostro codice C viene trasformato in Assembler.

    5. Uso della compilazione condizionale. Si modifichi il programma precedente inserendo delle direttive #if/#else/#endif in modo che, se è definito il simbolo DEBUG, vengano stampati gli argomenti, uno per riga, preceduti dal numero d'ordine (come in precedenza); altrimenti, vengano stampati solo gli argomenti, tutti su una riga e separati da spazi.

      Oltre che con #define, una macro come DEBUG può essere definita direttamente sulla riga di comando del compilatore con l'opzione -Dmacro=definizione o semplicemente -Dmacro. Si provi quindi a compilare il programma passando a gcc l'opzione -DDEBUG e senza e ad osservare il risultato.

      Si usi l'opzione -E di gcc per verificare quale codice viene passato al compilatore dal preprocessore nei due casi.

    6. Uso di Eclipse. Si ripetano i primi due esercizi usando Eclipse come ambiente di sviluppo. Il docente effettuerà una dimostrazione su come creare un progetto C all'interno di Eclipse, come scrivere il codice sorgente, e come compilare ed eseguire il programma corrispondente.

      Avanzato *: si configurino le proprietà del progetto Eclipse per Echo in modo da definire o meno la macro DEBUG, come si è fatto con gcc -D.

  2. Seconda esercitazione

    Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio.

    1. sizeof. L'operatore C sizeof consente di scoprire, a run-time, la dimensione in memoria di un tipo. Per esempio, sizeof(char) restituisce la dimensione, in byte, di un char. L'operatore accetta anche la variante sizeof(variabile) che restituisce la dimensione del tipo della variabile.
      Si scriva un programma C che stampi una tabella con i nomi dei tipi e la relativa dimensione. In particolare, si includano i seguenti tipi:
      • tutti i tipi base;
      • le rispettive varianti short e long (si verifichi anche il supporto per long long con vari tipi base);
      • gli stessi tipi con le varianti signed e unsigned;
      • i tipi puntatore corrispondenti;
      • array di varia dimensione dei tipi base.

      Avanzato *: si compili lo stesso programma con il gcc, in tre varianti: senza opzioni particolari, con l'opzione -m32, con l'opzione -m128bit-long-double, e con l'opzione -m96bit-long-double. Si confrontino i risultati nei diversi casi.

    2. sizeof senza sizeof. Avanzato **: si trovi il modo di stampare una tabella analoga a quella precedente senza usare l'operatore sizeof. È accettabile limitare la tabella ai soli tipi "piccoli" (char, short, etc.).

    3. strlen. La funzione di libreria strlen() prende come parametro una stringa, e restituisce la sua lunghezza. Per esempio, strlen("Ciao mamma!") restituisce 11.

      Si scriva una propria implementazione di questa funzione, chiamandola strlen2(). Si scriva poi una funzione main() che, per ciascuna delle stringhe passate sulla riga di comando, stampi la stringa, la sua lunghezza calcolata da strlen() e quella calcolata da strlen2(). Si verifichi che le due funzioni restituiscano lo stesso risultato.

    4. Caratteri escape. Si scriva un programma che, usando a scelta strlen() o strlen2(), stampi la lunghezza delle seguenti stringhe:
      • "Ciao mamma!"
      • "test\n"
      • "test\\n"
      • "test\\\n"
      • "test\0test"
      • "test\\0test"
      Si commentino i risultati. Sono sempre quelli attesi?

    5. Output di booleani. Si provi a stampare con printf() il valore di alcuni booleani, per esempio con printf(formato,1==1) o printf(formato,1==2). Quale pensate sia il formato più adatto? Come si possono interpretare i risultati?

    6. Valore dei puntatori. Si scriva un programma C che contenga le seguenti dichiarazioni:

      int a=666;
      int *b=&a;
      int **c=&b;
      char *s="Ciao mamma!";

      Il programma deve stampare, per ciascuna delle espressioni a, b, c, s, *b, *c, *s, **c, &a, &b, &c, &s, il testo dell'espressione e il suo valore numerico, usando una riga per ciascuna espressione. Si disegni (su carta) il diagramma dell'ambiente e della memoria corrispondente alle dichiarazioni in questione, usando gli indirizzi e i valori stampati dal programma. Si verifichi che, per ciascuna delle espressioni sopra indicate, la traccia della valutazione delle espressioni nel diagramma coincida con il valore effettivamente stampato dal programma.

    7. Codifica ASCII. Si scriva una funzione C dumpASC() che, ricevuta come parametro una stringa, stampi a video i caratteri che la compongono, uno per riga, ciascuno seguito dal corrispondente codice ASCII. Si scriva poi un main() che chiami dumpASC() su ciascuno degli argomenti passati sulla riga di comando.

    8. atoi. La funzione di libreria atoi() ("ascii-to-integer") riceve come parametro una stringa, e restituisce un intero rappresentante il valore espresso dalla stringa, assumendo che essa contenga un numero in notazione decimale. Per esempio, atoi("123") restituisce l'intero 123. Si scriva una propria funzione C atoi2() che faccia lo stesso. Si scriva poi un main() che, come per gli esercizi precedenti, passi ad atoi2() le stringhe passate sulla riga di comando, e stampi argomento e risultati per ciascuna.

      Avanzato **: si implementi atoi2() in modo tale che essa riproduca esattamente il comportamento di atoi() anche nei casi di errore. Si esegua il comando man atoi per i dettagli su atoi().

  3. Terza esercitazione

    Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio.

    1. strlen(). Se non lo si è già fatto, si svolga l'esercizio su strlen() presentato alla precedente esercitazione. Altrimenti, si passi subito all'esercizio successivo. Il testo dell'esercizio in questione è il seguente:
      La funzione di libreria strlen() prende come parametro una stringa, e restituisce la sua lunghezza. Per esempio, strlen("Ciao mamma!") restituisce 11.

      Si scriva una propria implementazione di questa funzione, chiamandola strlen2(). Si scriva poi una funzione main() che, per ciascuna delle stringhe passate sulla riga di comando, stampi la stringa, la sua lunghezza calcolata da strlen() e quella calcolata da strlen2(). Si verifichi che le due funzioni restituiscano lo stesso risultato.

    2. Equivalenza array-puntatori. Se nello svolgere l'esercizio precedente è stata usata la notazione degli array (a[i] ecc.), si riscriva la strlen2() usando la notazione dei puntatori (*p ecc.); viceversa, se è stata usata la notazione dei puntatori, si riscriva strlen2() usando la notazione degli array.

    3. Allocazione dinamica e strdup(). In C l'allocazione dinamica di memoria avviene tramite la funzione malloc() (definita nel file header stdlib.h. Tale funzione prende in ingresso la dimensione (in byte) del blocco di memoria che si vuole allocare, e restituisce un puntatore al blocco allocato. Per esempio, int *a=(int *)malloc(10*sizeof(int)); restituisce un puntatore a uno spazio di memoria sufficiente a contenere 10 interi. Questa operazione è analoga a dichiarare un array di 10 interi con int a[10]; (ci sono alcune differenze che verranno discusse a lezione).

      Si scriva una funzione strdup2() che, ricevuta come argomento una stringa, restituisca una copia di tale stringa, memorizzata in un'area di memoria di dimensione adeguata allocata dinamicamente. Suggerimento: si ricordi che le stringhe sono gestite come puntatori a caratteri, e che una stringa deve sempre essere terminata da un byte a 0.

    4. strcmp() Scrivere una funzione C con prototipo int strcmp2(char *s, char *t) che restituisca un valore minore di 0 se s<t; 0 se s=t; un valore maggiore di 0 se s>t. In ogni caso, i confronti devono essere fatti seguendo l'ordinamento ASCII dei caratteri; una stringa più corta deve risultare minore di una più lunga che la abbia come prefisso (es: "mamma"<"mammaliturchi").

    5. Uso delle strutture. Si definisca una struttura presenza che contenga le informazioni presenti in una riga del foglio presenze usato nel corso. Se non avete sott'occhio il foglio, tali informazioni sono:
      • un numero progressivo
      • cognome
      • nome
      • account
      • matricola
      • corso
      • gruppo di esercitazione
      • note
      • firma
      • percentuale di presenze
      Si curi di usare il tipo di dato corretto per ogni campo! Si definisca poi una struttura fogliopresenze che descriva l'intero foglio, con spazio per 200 studenti. Si noti che l'intestazione del foglio include data e ora, aula, gruppo e docente. Infine, si scriva una funzione stampaassenti() che, ricevuto come argomento un fogliopresenze, stampi con printf() la sua intestazione nonché i nomi e cognomi di tutti gli assenti.

      Nota: questo esercizio non ha un main(), quindi non può essere eseguito direttamente (perché sia significativo, occorrerebbe scrivere i dati degli studenti all'interno della struttura dati).

      Avanzato *: usando degli assegnamenti, si compilino (con dati di fantasia) alcune righe del fogliopresenze, e si scriva un main() che richiama la stampaassenti(). Si verifichi che il programma si comporta come atteso.

    6. Uso delle union. Un punto sul piano cartesiano è definito con due coordinate (di tipo double), x e y. Si definiscano strutture dati per triangoli, quadrati, pentagoni e cerchi. Si definisca poi una struttura dati figura che possa contenere una qualunque delle figure geometriche sopra indicate (e che si sappia, in ogni istante, quale figura è contenuta nella struttura dati), senza sprecare spazio. Quanto sarà grande questa struttura dati? Si provi a stimare il risultato prima di verificarlo usando l'operatore sizeof().

      Si scriva poi una funzione perimetro() che, ricevuta come parametro una figura, restituisca il suo perimetro (come double). Nota: potreste voler usare la funzione di libreria sqrt(), che restituisce la radice quadrata di un numero. Tale funzione è definita nel file header di sistema math.h. Inoltre, dovete istruire il linker a unire al vostro codice la libreria matematica di sistema: per far ciò, potete usare l'opzione -lm del gcc oppure indicare, fra le proprietà del progetto Eclipse, che volete usare la libreria di nome m (Proprietà del progetto, pagina C/C++ build, scheda "Librerie", inserite m nella lista di librerie "opzione -l").

      Infine, si scriva una funzione main() che dichiari e inizializzi una variabile per ciascun tipo di figura, e stampi il relativo perimetro usando la funzione definita in precedenza.

    7. Equivalenza dei riferimenti. Si aggiungano ai due esercizi precedenti delle operazioni di stampa che mostrino gli indirizzi e i valori corrispondenti a varie espressioni, come le seguenti:
      • struttura.campo
      • arraydistrutture[indice].campo
      • unione.campo
      • puntatoreastruttura->campo
      • (puntatoreadarraydistrutture+indice)->campo
      • (*(puntatoreadarraydistrutture+indice)).campo
      ecc. Suggerimento: si usi la specifica di formato %p di printf() per stampare il valore di un puntatore.

  4. Quarta esercitazione

    Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio.

    1. Nella prima parte dell'esercitazione, verranno illustrate diverse soluzioni per alcuni degli esercizi più significativi presentati nelle esercitazioni precedenti. Si raccomanda di confrontare le soluzioni presentate con le proprie. Quale versione vi sembra più chiara? Quale più efficiente?

    2. Strutturazione di un programma. Si vuole realizzare una piccola libreria per eseguire operazioni su uno stack di interi. Lo stack può essere implementato come un array di interi di lunghezza fissata (ma si usi una costante simbolica definita con #define per indicare questa lunghezza).

      Si crei un file stack.c contenente le definizioni delle strutture dati necessarie (suggerimento: servirà almeno un array per memorizzare gli elementi o un puntatore a memoria allocata dinamicamente con malloc(), nonché un modo per sapere quanti elementi sono contenuti nello stack, o un puntatore alla sua testa) e un file stackop.c contenente le definizioni delle seguenti funzioni: int push(int) impila un intero nello stack; restituisce 0 se l'operazione è andata a buon fine, oppure -1 in caso di errori (per esempio, non c'è spazio nello stack); int pop() toglie l'elemento in cima allo stack e lo restituisce al chiamante; ritorna 0 se lo stack è vuoto; int size() restituisce il numero di elementi presenti al momento nello stack; int empty() restituisce TRUE se lo stack è vuoto, FALSE altrimenti.

      Si abbia cura di definire anche dei file .h contenenti le dichiarazioni e i prototipi corrispondenti ai file .c di cui sopra.

      Infine, si scriva un file main.c contenente (ovviamente) una funzione main() che, dopo aver incluso i .h che si sono definiti e quelli di sistema eventualmente necessari, effettui alcune operazioni sullo stack, stampando i risultati e verificando che tutto funzioni come atteso.

      Suggerimento: si abbia cura di usare in maniera appropriata i prototipi, le dichiarazioni extern e static per ottenere la visibilità richiesta, come visto a lezione.

      Avanzato *: si scriva un main() che, usando la funzione atoi() sviluppata in una delle precedenti esercitazioni o quella di sistema, converta i propri argomenti di riga di comando in interi, e li impili sullo stack con un ciclo simile al seguente:

      for (i=1;i<argc;i++) push(atoi(argv[i])); Dopo aver impilato gli argomenti, la funzione deve stampare la dimensione corrente dello stack, quindi estrarre uno per uno tutti gli argomenti con pop() stampando, dopo ogni estrazione, l'argomento e la dimensione dello stack. Il ciclo va ripetuto finché empty() non restituisce TRUE.
    3. Super stack! Si aggiunga al progetto sviluppato nell'esercizio precedente un ulteriore file, rpnop.c che implementi alcune operazioni in notazione polacca inversa. In particolare, si vogliono implementare le seguenti operazioni:
      • add() sostituisce i due elementi in cima allo stack con la loro somma;
      • sub() sostituisce i due elementi in cima allo stack con la loro differenza;
      • neg() cambia il segno dell'elemento in cima allo stack;
      • print() estrae l'elemento in cima allo stack e lo stampa con printf();
      • clear() azzera lo stack.
      Si curi che rpnop.c acceda allo stack esclusivamente tramite l'interfaccia data da stackop.h.

      Avanzato *: si aggiungano altre operazioni aritmetico/logiche a piacere: moltiplicazione, divisione, and, or, not, ecc. Si aggiungano poi alcuni operatori relazionali, per esempio: = (sostituisce i due elementi in cima allo stack con TRUE se i due elementi hanno lo stesso valore, con FALSE altrimenti), !=, >, <, ecc.

    4. Calcolatore RPN: (Avanzato **) Si scriva un main che riceva sulla riga di comando una serie di operandi e operatori RPN; gli operandi saranno numeri interi (da convertire con atoi()) e gli operatori saranno simboli o keyword corrispondenti alle operazioni definite in rpnop.c. Il programma deve scorrere la lista dei suoi argomenti; quando incontra un operando, esso va messo sullo stack, mentre quando incontra un operatore, esso va eseguito. Alla fine della lista, il programma deve stampare il contenuto corrente dello stack.

      Esempio: il comando

      rpn 21 2 5 + / print 3 5 + esegue le seguenti operazioni: mette in pila 21, 2 e 5 (in quest'ordine), quindi chiama add() che lascia sullo stack 21 e 7; quindi chiama la funzione di divisione (che lascia sullo stack 3) e stampa tale valore. Lo stack e' adesso vuoto; vengono messi nella pila 3 e 5 e si chiama nuovamente la add() (che lascia 8). Ora la lista di argomenti è esaurita, e il programma stampa lo stato dello stack (cioe', solo 8).

    5. Quinta esercitazione

      Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio.

      1. Se non si sono completati gli esercizi precedenti, si svolga adesso almeno l'esercizio numero 2. della precedente esercitazione.

      2. Quoziente e resto. Si scriva un file quozrest.c contenente una funzione C di nome qr con le seguenti caratteristiche:
        • La funzione deve prendere in ingresso due interi a e b, e restituire al chiamante il quoziente e il resto della divisione di a per b. Naturalmente, dovrà essere vero che a=b*q+r, dove q e r sono, rispettivamente, il quoziente e il resto restituiti dalla funzione.
        • Ci sono tuttavia casi in cui il calcolo del quoziente e del resto non può essere effettuato. La funzione deve restituire al chiamante TRUE se il calcolo può essere fatto (e in questo caso, deve restituire q e r come detto sopra), FALSE in caso contrario (e in questo caso q e r non saranno significativi).
        Si scriva poi un file quozresttest.c contenente una funzione main() che, ricevuti sulla riga di comando due argomenti rappresentanti interi, li converta in interi, ne calcoli quoziente, resto e flag di validità usando qr(), e stampi i risultati.

      3. Uso efficiente della memoria. Si dichiari una struttura dati che contenga i seguenti dati relativi al profilo sanitario (immaginario) di una persona:
        • il suo sesso (maschio o femmina);
        • il suo gruppo sanguigno (gruppo A, B, AB o 0, con fattore RH positivo o negativo);
        • la sua età (entro limiti ragionevoli);
        • il suo codice di servizio sanitario locale, che è composto da 5 cifre decimali.
        Si cerchi di codificare i dati in maniera più compatta possibile, ovvero occupando il minimo di memoria per ogni persona.

        Si scrivano poi una funzione creass() che, ricevuti come argomenti i vari dati, restituisca una struttura dati opportunamente riempita, e una funzione stampass() che, ricevuta una struttura dati, ne stampi i campi in maniera leggibile usando printf(). Infine, si scriva una funzione main() che riceva sulla riga di comando i dati in questione, crei un'istanza della struttura prescelta che memorizzi i dati forniti usando creass(), e poi la stampi usando stampass().

        Avanzato *: si confronti (eventualmente usando sizeof()) la dimensione della struttura dati definita sopra con quella della struttura "ovvia" (una struct con un campo intero per ogni dato) e con quella minima teorica. Si è raggiunto l'ottimo teorico? Se no, come si può compattare ulteriormente la struttura?

        Avanzato **: se si è raggiunto l'ottimo teorico, si provi a trovare un modo diverso dal precedente per codificare la stessa informazione. Quale delle due versioni conduce a codice più efficiente? Quale versione conduce a codice più leggibile?

      4. Filtro parametrico. Si vuole realizzare un sistema parametrico di filtraggio di testi. Il filtraggio è affidato a una funzione di selezione: una funzione che, ricevuta in ingresso una stringa, restituisca TRUE o FALSE a seconda che la stringa soddisfi o meno il criterio di selezione. Si scrivano le seguenti funzioni di selezione (avranno tutte la stessa segnatura):
        • vocale() restituisce TRUE se la stringa inizia per vocale;
        • corta() restituisce TRUE se la stringa è lunga al più 6 caratteri;
        • italiese() restituisce TRUE se la stringa finisce per vocale;
        • sardese() restituisce TRUE se la stringa contiene soltanto consonanti e "u";
        • lunga() restituisce TRUE se la stringa è più lunga di 8 caratteri.
        Si scriva poi una funzione filter() che, ricevuti come argomento un array di stringhe e un puntatore a una funzione di selezione, applichi la funzione di selezione a tutte le stringhe dell'array, restituendo un array di stringhe contenente solo le stringhe dell'array originale che soddisfano il criterio di selezione espresso dalla funzione di selezione passata come argomento. (Suggerimento: si ricordi che in C gli array non hanno una lunghezza implicita, quindi occorre un modo per informare il chiamante della lunghezza dell'array risultante).

        Infine, si scriva -- in un file separato -- un main() che chiami la filter() su argv, passando una per una le varie funzioni di selezione di cui sopra, e stampando il risultato.

    6. Sesta esercitazione

      Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio, scegliendo un nome che non contenga spazi o caratteri speciali, e che sia diverso da quello dei singoli file .c che saranno contenuti al suo interno, onde evitare conflitti di nome.

      Importante: alcuni degli esercizi fanno riferimento a file, comandi o librerie disponibili in ambiente UNIX. Si consiglia fortemente di svolgere gli esercizi in questione su Linux (in alternativa, gli esercizi andranno adattati al sistema operativo utilizzato).

      1. Contenuto delle librerie. Avanzato **. Tradizionalmente, una libreria consiste di un archivio (da cui l'estensione .a) contenente un certo numero di file oggetto (estensione .o), ciascuno risultante dalla compilazione di un file sorgente .c. Sui sistemi UNIX, le librerie sono contenute della directory /usr/lib (tipicamente, questa directory contiene sia file .a che .so, che sono invece librerie a caricamento dinamico). Per esempio, le funzioni della libreria standard del C si trovano in libc.a, la libreria matematica in libm.a, le funzioni che convertono nomi di host in indirizzi IP stanno in libresolv.a, ecc.

        Si usi (da shell) il comando objdump -t nomelibreria.a per esaminare il contenuto della libreria libcrypt.a. Questo comando mostra tutti i nomi dei simboli definiti da ciascun file .o presente all'interno della libreria; fra questi, i nomi di funzione sono identificati da un flag F accanto al nome (si noti anche che, per convenzione, i nomi di funzione inizianti per "_" si riferiscono a funzioni interne, non destinate ad essere chiamate da programmi esterni).

        Armati da queste informazioni, si usi il comando man nomefunzione per capire quali, fra le funzioni definite da libcrypt.a, sono documentate e destinate ad essere chiamate dai programmi utente. Si scriva un programma C che dimostra l'uso di una di queste funzioni (si scelga pure quella che sembra più semplice da usare) e lo si esegua, verificando che i risultati siano conformi a quanto atteso. Suggerimento: si ricordi che occorrerà includere il file .h che definisce la funzione usata, e compilare passando l'opzione -lcrypt al compilatore.

      2. Filtri di I/O. Molti programmi e semplici utility possono essere realizzati come filtri, ovvero programmi che leggono dei dati in input e li riproducono, dopo aver compiuto operazioni varie, sull'output. Spesso, i filtri lavorano su un carattere alla volta, o su una riga di testo alla volta. Si scrivano dei programmi C che effettuino le seguenti operazioni (è possibile riusare parte del codice dall'ultimo esercizio della quinta esercitazione), assumendo che le righe in ingresso siano lunghe al massimo 512 caratteri:
        • un programma di traduzione da qualunque lingua a Sardese, che copia sull'output ogni riga di testo ricevuto in input, ma con tutte le vocali sostituite da "u";
        • un programma "TG4", che copia sull'output il proprio input, omettendo però tutte le righe che contengono le parole "crisi", "declino", "poveri" e "debito" (suggerimento: si studi la pagina di manuale della funzione strstr());
        • un programma "TG3", che copia sull'output il proprio input, inserendo la parola "crisi" fra tutte le parole in input (es: "oggi si riunisce" diventa "oggi crisi si crisi riunisce");
        • un programma "simplegrep" che copia sull'output solo le righe dell'input che contengono la stringa passata come argomento della riga di comando.
        Suggerimento: per ciascuno dei programmi in questione, la scelta di quale funzione di input viene usata può rendere il codice più o meno semplice o complicato: si scelga con cura...

      3. Stampa formattata. Si scriva un programma che stampi una tabella di conversione delle temperature fra gradi Kelvin (°K), gradi Celsius (°C) and gradi Fahrenheit (°F). Le formule di conversione sono le seguenti:

        Formule di conversione delle temperature
        Celsius/Kelvin Kelvin = Celsius + 273.15 Celsius = Kelvin - 273.15
        Celsius/Fahrenheit Celsius = 5/9 * (Fahrenheit -32) Fahrenheit =( Celsius/(5/9)) + 32

        Il programma deve stampare la tabella di conversione per temperature che vanno da -10°C a +32°C, a intervalli di 2°C; si usi per i valori numerici una precisione di due cifre dopo la virgola. La tabella deve avere tre colonne, una per ciascuna scala, e una riga di intestazione in cima, recante per ogni colonna il nome per esteso della scala di temperatura corrispondente; le tre colonne devono avere la stessa larghezza, e i valori numerici devono essere centrati nella colonna di appartenenza e allineati alla virgola decimale.

      4. Uso di scanf(). Si scriva un semplice calcolatore in notazione infissa. Il calcolatore legge in ingresso espressioni con due soli operandi e un operatore aritmetico, come "24-8" o "13.3*3.2"; calcola il valore risultante, stampa in output una copia dell'espressione letta seguita dal carattere "=" e dal risultato, e infine torna a leggere l'espressione successiva. Il programma termina quando legge in ingresso la stringa "fine"; ogni altro input deve essere segnalato come errore e ignorato. Suggerimento: si scelga con cura quale formato usare per stampare il risultato: ne serve uno che sia al tempo stesso ragionevolmente compatto e preciso.

      5. Quinizzazione. Avanzato *** Si scriva un programma C che, quando eseguito, stampi esattamente il proprio sorgente. Per rendere il problema interessante, il programma non deve accedere in alcun modo al file contenente il proprio sorgente.

    7. Settima esercitazione

      Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio, scegliendo un nome che non contenga spazi o caratteri speciali, e che sia diverso da quello dei singoli file .c che saranno contenuti al suo interno, onde evitare conflitti di nome.

      Importante: alcuni degli esercizi fanno riferimento a file, comandi o librerie disponibili in ambiente UNIX. Si consiglia fortemente di svolgere gli esercizi in questione su Linux (in alternativa, gli esercizi andranno adattati al sistema operativo utilizzato).

      1. Stampa di file. Si scriva in C un programma cat2 che stampi su stdout il contenuto dei file (di testo) il cui nome è stato passato sulla riga di comando, ciascuno preceduto da una riga con un "*" e il suo nome. Per esempio, il comando cat2 /etc/filesystems /etc/fstab potrebbe produrre in output * /etc/filesystems: vfat iso9660 ext3 ext2 * /etc/fstab: # /etc/fstab: static file system information. # # <file system> <mount point> <type> <options> <dump> <pass> /dev/hda1 /boot ext2 defaults 0 1 /dev/hda6 / ext3 defaults,errors=remount-ro 0 1 /dev/hda5 none swap sw 0 0 /dev/fd0 /floppy auto rw,user,noauto 0 0 /dev/sda /flash auto rw,user,noauto 0 0 /dev/hdc /cdrom auto ro,user,noauto 0 0 proc /proc proc defaults 0 0

      2. Copia di file. Si scriva un programma cp2 che effettui una copia del file il cui nome è passato come primo argomento sulla riga di comando nel (nuovo) file il cui nome è passato come secondo argomento. Il file di destinazione deve essere troncato o creato se non esiste prima della scrittura.

        Per esempio, il comando cp2 cp2.c cp2.copy, eseguito nella directory in cui si trova il sorgente di cp2, ne crea una copia identica sotto il nome cp2.copy. Si usi il comando di Shell diff cp2.c cp2.copy (vedere man diff per dettagli sull'uso di diff) per verificare che i due file siano realmente identici quanto al contenuto. Suggerimento: cp2 deve essere in grado di copiare file di lunghezza qualunque, non necessariamente multipla di qualche valore particolare, e di contenuto qualunque. Si consideri con cura quali funzioni di I/O usare allo scopo.

      3. Effetto del buffering. Avanzato * I programmi scritti finora, e in particolare cp2, fanno uso del buffer predefinito, di dimensione BUFSIZ (sulla versione di Linux nei laboratori, BUFSIZ è pari a 8192). Si usi la funzione setvbuf() per impostare invece un buffer di dimensione diversa, e si verifichino i tempi di copia di un file molto grande, per esempio /usr/local/bin/mplayer (un player di musica e filmati per Linux) usando il comando time. Nota: l'impostazione del buffer deve essere effettuata subito dopo la fopen() relativa, e prima di ogni operazione di I/O sul file in questione.

        Per esempio, si provino i seguenti casi con il comando time cp2 /usr/local/bin/mplayer ~/test:

        • buffer sul file di input di 256 bytes, buffer sul file di output di 65536 bytes
        • buffer sul file di input di 65536 bytes, buffer sul file di output di 256 bytes
        • nessun buffer (modo _IONBF) su entrambi i file
        • buffer di 65536 bytes sia sull'input che sull'output
        • buffer di 512*1024 (mezzo megabyte) bytes sia sull'input che sull'output
        Quale è l'impostazione più conveniente? Oltre quale dimensione dei buffer gli incrementi in velocità non valgono l'occupazione di memoria?

      4. I Poeti Stinti. Un vostro amico scozzese (in Erasmus), poeta dilettante, è in crisi di ispirazione. Salvate la sua carriera scrivendo un programma in C che lo aiuti a trovare le rime. Il programma (chiamiamolo poeta) prende come argomento sulla riga di comando una parola (Inglese), e cerca all'interno del file /usr/share/dict/american-english, che contiene un dizionario inglese, con una parola su ogni riga, tutte le parole che "rimano" con quella fornita sulla riga di comando. La definizione di "rima" è la seguente: il programma deve prendere come secondo argomento un numero intero n (opzionale; per default si assuma il valore 1); una parola rima con quella data se la sua parte finale, a partire dalla n-esima vocale dal fondo, coincide con l'analogo frammento della parola data.

        Per esempio, se n=1 flower rima con zipper (il frammento di rima è er), mentre se n=2, flower rima con power (il frammento di rima è ower). Il comando

        poeta hooligan 2 restituisce Gilligan Michigan Mulligan cardigan hooligan ptarmigan shenanigan Avanzato ****: si scriva una poesia in inglese, di senso compiuto, in cui hooligan rimi con shenanigan. Si usi il comando gnome-dictionary o si acceda al sito
        www.m-w.com per consultare un vero dizionario inglese, recante anche il significato delle parole.

      5. File di password. Sui sistemi UNIX, le password degli utenti sono memorizzate nel file /etc/passwd. Si tratta di un file di testo, in cui ogni riga contiene le informazioni di un utente, e i campi all'interno di una riga sono separati da ':'. Per esempio, la riga operator:x:37:37:Operator:/var:/bin/sh ha il seguente significato:
        • operator è il nome di login dell'utente;
        • x rappresenta il campo password (sui sistemi del Centro di Calcolo sono in uso le shadow password, per cui la password non è visibile; in altri casi questo campo contiene la password dell'utente, in forma crittografata tramite la funzione crypt() vista nell'esercitazione precedente);
        • 37 è l'ID numerico dell'utente;
        • 37 è l'ID numerico del gruppo a cui appartiene l'utente (la coincidenza dei valori non è significativa);
        • Operator è il nome in chiaro dell'utente;
        • /var è l'home directory dell'utente;
        • /bin/sh è la shell di login (e di default) dell'utente.
        Si scriva un programma C che legga il file /etc/passwd, spezzando opportunamente i campi, e stampi in output i nomi in chiaro (quinto campo) di tutti gli utenti che hanno /bin/false come shell di default (ultimo campo), indicando che questi utenti non possono in nessun caso connettersi al sistema (si tratta di utenti "virtuali").

        È possibile affrontare l'esercizio in (almeno) due modi:

        1. leggendo le righe del file con fgets(), e usando poi dei puntatori a carattere all'interno della stringa per identificare i campi, oppure
        2. (Avanzato *) usando la funzione fscanf() con le specifiche di formato %[^...] e %* (si veda il manuale: man fscanf) per spezzare automaticamente le righe al momento della lettura e assegnare solo i campi di interesse.
        Il secondo metodo consente di scrivere molto meno codice...

      6. Editor programmabile. Avanzato **. Si vuole realizzare un editor programmabile di file binari. L'editor deve leggere dal file il cui nome è passato come primo argomento, o da stdin se non ci sono argomenti, una serie di comandi, uno per riga, che operano su file, e interpretare le istruzioni ricevute. L'editor deve supportare i seguenti comandi:
        • o nomefile: apre (open) il file nomefile; il file aperto diviene il file corrente.
        • s pos: si posiziona (seek) all'offset pos (dall'inizio del file) nel file corrente.
        • r size: legge (read) size bytes dal file corrente, nella posizione corrente, e li stampa su stdout.
        • w stringa: scrive (write) la stringa data (fino a fine riga) nel file corrente, nella posizione corrente.
        • c: chiude (close) il file corrente.
        • q: esce (quit); il programma termina stampando il numero totale di byte scritti nel corso della sessione.
        L'editor programmabile deve stampare su stderr i messaggi d'errore nel caso di comandi errati, per esempio un comando s, r, w o c quando non c'è nessun file corrente aperto; deve altresì stampare messaggi d'errore nel caso di comandi inesistenti (es.: t), nonché segnalare tutti i casi in cui le funzioni di I/O hanno restituito errori, con il relativo codice errno.

        Avanzato ***. L'editor programmabile così definito non è in grado di scrivere byte con valore \0 o \n (perché?); si introduca un meccanismo di escape per il comando w che consenta di scrivere caratteri arbitrari all'interno del file.

    8. Ottava esercitazione

      Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio, scegliendo un nome che non contenga spazi o caratteri speciali, e che sia diverso da quello dei singoli file .c che saranno contenuti al suo interno, onde evitare conflitti di nome.

      Importante: alcuni degli esercizi fanno riferimento a file, comandi o librerie disponibili in ambiente UNIX. Si consiglia fortemente di svolgere gli esercizi in questione su Linux (in alternativa, gli esercizi andranno adattati al sistema operativo utilizzato).

      1. Split. Si scriva una implementazione C della funzione String.split() di Java. La funzione prende come argomento una stringa sorgente e una stringa di separatori, e restituisce un array di stringhe, ciascuna costituente un token della stringa originale, delimitato da uno o più separatori. L'array deve essere allocato dinamicamente (della dimensione corretta), e la stringa originale non deve essere modificata.

        Suggerimento: si ricordi che occorrerà informare il chiamante riguardo alla lunghezza dell'array restituito.

        Avanzato *: si implementi la split usando solo funzioni di libreria, e in particolare evitando di usare l'operatore di indirezione * (si può invece usare nelle dichiarazioni di tipo). Suggerimento: possono essere utili le funzioni strspn() e strcspn().

        Si scriva poi un main() con cui verificare il funzionamento della split().

      2. Banner CNN. Si scriva in C un programma banner con il seguente comportamento: il programma riceve come unico argomento il nome di un file di testo; questo file conterrà delle "notizie", una per ogni riga. Il programma deve stampare queste notizie, sulla stessa riga e una dopo l'altra, separate da tre spazi, per una lunghezza massima di 70 caratteri. Inoltre, le notizie devono scorrere da destra verso sinistra di un carattere alla volta, come avviene in vari telegiornali, finché non sono state tutte visualizzate; a quel punto vanno visualizzati (sempre con le stesse modalità) 10 spazi, e quindi il ciclo si ripete. Il programma non termina mai (si usi Ctrl-C da tastiera o il pulsante di arresto rosso in Eclipse per terminarlo).

        Suggerimenti: Potrà essere necessario inserire dei ritardi all'interno del programma per evitare che lo scorrimento sia troppo veloce; si usino a questo fine le funzioni sleep() o usleep(), che sospendono l'esecuzione per un tempo indicato, o un ciclo for a vuoto (ma attenzione: il compilatore potrebbe ottimizzarlo eliminandolo...).

        Per ristampare sopra una riga già stampata si termini la stampa con il carattere \r (ritorno a capo) invece che con \n (nuova linea e a capo).

        Esempio: se il file tg4.txt contiene

        Benedetto XVI: ricrescita miracolosa. Bandana esposta ai fedeli a Palazzo Chigi. Vince ancora il Milan, promosso in serie @. Il comando banner tg4.txt dovrà visualizzare la sequenza Benedetto XVI: ricrescita miracolosa. Bandana esposta ai fedeli a P enedetto XVI: ricrescita miracolosa. Bandana esposta ai fedeli a Pa nedetto XVI: ricrescita miracolosa. Bandana esposta ai fedeli a Pal edetto XVI: ricrescita miracolosa. Bandana esposta ai fedeli a Pala ... in serie @. Benedetto XVI: ricrescita miracolosa. Bandana in serie @. Benedetto XVI: ricrescita miracolosa. Bandana ... (ma, ovviamente, sempre sulla stessa riga e opportunamente temporizzata per rendere le notizie leggibili).

      3. Statistiche lessicali. Si scriva un programma C lexstat che riceve come argomenti sulla linea di comando 0 o più nomi di file di testo, e per ciascuno di essi calcoli e stampi separatamente i seguenti conteggi e le relative percentuali sul numero totale di caratteri:
        • Numero di caratteri alfabetici
        • Numero di caratteri numerici
        • Numero di caratteri di punteggiatura
        • Numero di spazi e altri caratteri blank
        • Numero totale di caratteri
        Si formatti adeguatamente l'output, curando in particolare l'allineamento e la precisione dei dati numerici. Se il programma riceve 0 argomenti, allora dovrà stampare le statistiche relative allo stdin.

      4. Word count. Si scriva un programma wc2 di funzionamento analogo al comando Linux wc (si veda man wc per i dettagli). Non è necessario implementare tutte le opzioni di wc; sono sufficienti le opzioni -c, -l e -w.

      5. Reverse. Si scriva un programma reverse che riceva come argomento un nome di file (testuale), e stampi come risultato lo stesso file, ma con le righe in ordine inverso. Per esempio, con riferimento all'esercizio precedente sul banner, il comando reverse tg4.txt dovrà produrre in output Vince ancora il Milan, promosso in serie @. Bandana esposta ai fedeli a Palazzo Chigi. Benedetto XVI: ricrescita miracolosa. Si verifichi il funzionamento efficiente del programma sul file /usr/share/dict/american-english, o su altro file testuale di grandi dimensioni.

    9. Nona esercitazione

      Salvo ove diversamente indicato, gli esercizi seguenti possono essere risolti lavorando su Eclipse. Si suggerisce di creare un progetto distinto per ogni esercizio, scegliendo un nome che non contenga spazi o caratteri speciali, e che sia diverso da quello dei singoli file .c che saranno contenuti al suo interno, onde evitare conflitti di nome.

      Importante: alcuni degli esercizi fanno riferimento a file, comandi o librerie disponibili in ambiente UNIX. Si consiglia fortemente di svolgere gli esercizi in questione su Linux (in alternativa, gli esercizi andranno adattati al sistema operativo utilizzato).

      1. Mazzo. Si definisca (in un file .h) una struttura dati che descriva una singola carta da gioco di un mazzo francese (con quattro semi e 13 carte per seme), e una che descriva un intero mazzo. Si scrivano poi (in un file .c) le seguenti funzioni:
        • creamazzo() restituisce un mazzo da gioco, allocato ex novo, in cui le carte hanno un ordine non specificato (possono per esempio essere ordinate per seme e per valore);
        • mescolamazzo() prende come argomento un mazzo da gioco, e lo rimescola in maniera casuale; il risultato è un mazzo in cui le carte hanno un ordinamento non prevedibile;
        • ordinamazzo() prende come argomento un mazzo da gioco (in ordine qualunque) e lo ordina per seme e per valore, con la convenzione cuori<quadri<fiori<picche e asso<2<3<...<10<J<Q<K.La funzione deve usare funzioni della libreria di sistema per l'ordinamento.
        • distruggimazzo() prende come argomento un mazzo da gioco e lo "distrugge" liberando la memoria che occupava.
        Si scriva infine, in un file .c distinto, un main() di prova che crei un nuovo mazzo, ne stampi sinteticamente il contenuto, lo mescoli e lo ristampi tre volte, lo ordini, e infine lo stampi ancora per la quinta volta. Si verifichi che le funzioni precedenti operino come atteso.

      2. Differenza di Date. Si scriva un programma che legge sullo stdin una sequenza di linee, ciascuna delle quali contenente una data espressa in formato testo (potete definire a piacere lo specifico formato); il programma deve produrre in output la stessa sequenza, in cui però tutte le righe dalla seconda in poi contengono sia la data originale, sia il numero di giorni che la separano dalla precedente (il numero può anche essere negativo se la data precedente è posteriore a quella corrente).

        Per esempio (ma il formato delle date è a piacere),
        10/1/2005 4/2/2005 12/5/2005 6/3/2005 produce in output 10/1/2005 4/2/2005 25 12/5/2005 96 6/3/2005 -66

      3. Spell checker Avanzato * Si scriva uno spell checker. Il programma deve leggere uno o più file di testo, il cui nome è passato come argomento sulla riga di comando (leggendo stdin se non sono presenti argomenti), e per ogni parola presente nel file, verificare che la parola sia compresa nel dizionario di sistema; in caso contrario, la parola sgarrupata deve essere mandata su stdout, preceduta dal nome del file e dal numero di linea in cui la parola è stata trovata (si usi come nome "stdin" se l'input proveniva da stdin).

        Il programma deve usare efficientemente il dizionario di sistema.

        Avanzato ** Il programma deve mantenere alcune statistiche globali, quali: numero di parole comprese nel dizionario, numero di parole controllate, numero di parole corrette, numero di parole errate, ecc. All'uscita (intenzionale) dal programma, in qualunque condizione si verifichi, lo spell checker deve stampare una tabellina opportunamente formattata contenente tali statistiche.

    10. Decima esercitazione

      Durante questa esercitazione ciascuno studente potrà iniziare il progetto finale del corso, con l'assistenza del docente e degli esercitatori.