Tutorato di Fondamenti di Informatica

Benvenuto nella pagina iniziate dei tutorial del corso di Fondamenti di Informatica della Facoltà di Ingegneria dell'Università di Pavia.

Ogni settimana verranno proposti esercizi relativi agli argomenti presentati a lezione, con suggerimenti per scrivere autonomamente una soluzione, ed esempi di soluzioni. Inoltre, vengono proposti spunti di riflessione, anche basati sulla modifica delle soluzioni fornite.

Credits

Parte di questo tutorial è realizzato rielaborando problemi formulati dai Prof. Claudio Cusano e Cristiana Larizza.

I programmi vengono verificati con pvcheck.

Le pagine del tutorial sono state realizzate con mdbook, un ottimo framework realizzato in Rust, un splendido linguaggio che non ho ancora avuto modo di approfondire adeguatamente.

Strumenti per lo sviluppo

Lo sviluppo di programmi in linguaggio C richiede due programmi specifici:

  • un editor per la scrittura dei file sorgente, cioè i file che contengono le istruzioni che il programma dovrà eseguire;
  • un compilatore che traduca le istruzioni del linguaggio di programmazione in istruzioni in linguaggio macchina che il calcolatore è in grado di eseguire.

Questi tutorial vengono utilizzati sia per i tutorati "tradizionali" che per i tutorati remoti. L'ambiente di sviluppo è Codenvy. Maggiori informazioni sono disponibili sugli strumenti utilizzabili per fruire del tutorato.

Per entrambe le modalità di tutorato verrà utilizzato il compilatore gcc.

Riferimenti al libro di testo

Nei tutoriati, un riferimento del tipo SX.Y (S sta per Sezione) indica il capitolo X.Y del libro di testo nel quale trovare maggiori informazioni riguardo all'argomento trattato. Per esempio, i riferimenti S10.1 e S9.5.2 indicano rispettivamente le sezioni 10.1 e 9.5.2 del libro.

Si possono trovare anche riferimenti a tabelle e figure con il formato TX.Y e FX.Y, che fanno riferimento alla tabella X.Y oppure alla figura X.Y. Per esempio, i riferimenti T2.2 e F7.1 indicano rispettivamente la tabella 2.2 e la figura 7.1 nel libro.

Libro di testo

Il libro di testo di riferimento per questi tutorial è il seguente:

Tullio Facchinetti, Cristiana Larizza e Alessandro Rubini
PROGRAMMARE IN C
CONCETTI BASE E TECNICHE AVANZATE
pubblicato da Apogeo (ISBN 9788891611208)

copertina

Il libro nasce con l'intento di essere una guida esauriente ma compatta al linguaggio C, e al contempo di insegnare le basi della programmazione di computer in generale.

Il testo è disponibile presso il sito web di Apogeo Editore, così come presso numerosi altri distributori di libri di testo universitari.

Tutorial T01 - Primi programmi

Questo primo tutorial presenta semplici programmi utili a prendere confidenza con la sintassi del linguaggio C, e con la compilazione e l'esecuzione di programmi.

Hello World

Il primo programma che storicamente viene presentato quando si introduce un nuovo linguaggio di programmazione è quello che stampa a video il testo Salve, mondo, o Hello, world in inglese.

Variabili ed espressioni

Tra gli applicativi informatici più diffusi vi sono quelli che elaborano informazioni di tipo economico. Gli esercizi proposti richiedono la manipolazione di questo tipo di informazione per la conversione di una valuta e il calcolo dei resti di un pagamento.

Hello world

ATTENZIONE: queste istruzioni sono valide soltanto se si sta utilizzando un sistema operativo Linux tradizionale. In particolare, non funzionano in ambiente Openshift. Openshift contiene già l'ambiente per la scrittura del codice!

Si usi l'ambiente di sviluppo per scrivere le seguenti righe nel file sorgente, salvandolo con il nome hello.c:

#include <stdio.h>

int main()
{
  puts("Hello, world!");
}

Il programma utilizza la funzione puts per la stampa del testo Hello, world!. La funzione main, che è obbligatoria in ogni programma C.

Uso di un editor "tradizionale": gedit

Il programma gedit un editor di testo che consente la scrittura del file sorgente. gedit è un editor che può essere utilizzato sulle più comuni piattaforme Linux.

Con il seguente comando, impartito nella shell si avvia il programma gedit:

gedit hello.c &

Il carattere & in coda al comando evita che la shell rimanga bloccata nell'esecuzione del comando e fa quindi in modo che si possano inviare altri comandi mentre l'editor rimane aperto.

I commenti

I commenti vengono utilizzati nel file sorgente per spiegarne i dettagli. Non sono istruzioni e vengono ignorati dal compilatore. Sono utili ai programmatori per comprendere meglio il funzionamento dei programmi.

Il programma che segue svolge le stesse operazioni del programma hello.c già visto, ma include dei commenti che spiegano alcuni semplici dettagli.

#include <stdio.h>

/* La funzione main deve essere sempre presente e specifica il punto di
 * inizio dell'esecuzione del programma.
 */
int main()
{
  // questo è un commento "inline"
  puts("Hello, world!");    // ricordare il punto e virgola dopo l'istruzione
}

I commenti possono essere di due tipi:

  • inline: introdotti dai caratteri //, si estendono fino a fine riga (andata a capo);
  • multi-linea: è testo racchiuso tra i delimitatori /* e */; il testo può essere distribuito su più linee del codice sorgente.

La compilazione

Dopo aver salvato il file sorgente si usi il comando gcc per compilare il programma.

Il comando più compatto per compilare il programma è:

gcc -Wall hello.c

In questo modo il compilatore genera un file eseguibile di nome standard chiamato a.out, che può essere eseguito con il comando

./a.out

L'opzione -Wall, che sta per Warning all, ovvero "tutti gli avvertimenti", istruisce il compilatore a generare tutti i possibili messaggi di avvertimento. Alcuni messaggi, infatti, normalmente non vengono generati. Pertanto è consigliabile utilizzare sempre l'opzione -Wall per ottenere maggiori informazioni riguardo a possibili problemi del programma.

Per completezza di informazione, si tenga presente che -Wall è opzionale. Questo significa che anche il comando (senza -Wall)

gcc hello.c

può essere usato per la compilazione, ma produce un numero più limitato di messaggi di avvertimento.

ATTENZIONE: per eseguire una versione modificata del programma, oltre a salvare il programma su disco, bisogna sempre ri-compilare il sorgente, altrimenti si eseguirà la versione compilata precedentemente.

Scelta del nome del file eseguibile

L'opzione del compilatore -o (segno meno seguito da 'o' minuscola) permette di indicare al compilatore il nome del file da generare.

ATTENZIONE: l'opzione del compilatore -o deve essere seguita dal nome del file eseguibile da generare sulla linea di comando.

Il comando di compilazione

gcc -Wall -o hello hello.c

genera un file di output chiamato hello, che può essere eseguito con

./hello

NOTA: ovviamente si potrebbe scegliere un nome qualsiasi per il file di output, per esempio:

gcc -Wall -o eseguibile hello.c

per eseguirlo con

./eseguibile

Precauzione nell'uso dell'opzione -o

Il compilatore considera il file hello.c come file di input, e il file hello come file di output.

Pertanto, se la compilazione va a buon fine, il file hello viene creato oppure, se già ne esiste uno, viene sovrascritto.

Quindi, il seguente comando di compilazione (o sue varianti)

gcc -Wall -o hello.c hello.c

nel quale dopo il -o è presente hello.c è errato e potenzialmente molto pericoloso, perché dice al compilatore che il file di output è hello.c. Quindi il compilatore SOVRASCRIVE il file hello.c, causando la perdita del suo contenuto.

Se il file è ancora aperto nell'editor, allora questo può essere nuovamente salvato, ma se l'editor è stato chiuso, allora il contenuto del file sorgente hello.c viene perduto!

T01 - Conversione di valute

Un Euro vale 1,18213 dollari (Ottobre 2017). Si scriva un programma che effettui la conversione da Euro a Dollaro. Il programma dovrà dichiarare almeno due variabili per memorizzare gli importi in euro e in dollari, e un'espressione che esegue la conversione. Gli importi nelle due valute dovranno essere stampati a terminale.

Indicazioni per lo svolgimento

Salva il programma in un file chiamato valuta.c. Puoi ovviamente utilizzare un nome diverso; in tal caso, ricordati di adattare le istruzioni che verranno fornite nel seguito.

ATTENZIONE: non dimenticare di salvare il programma prima di compilarlo. Per la compilazione, il compilatore "vede" il contenuto del file salvato su disco: se non salvi il programma dopo averlo modificato, le eventuali modifiche al file non verranno considerate dal compilatore.

Il programma valuta.c può essere compilato con il comando

gcc -Wall -o valuta valuta.c

e può essere eseguito con il comando

./valuta

Indicazioni per la soluzione

  • scrivi la funzione main (vedi S2.1)
  • dichiara le variabili necessarie a contenere i dati del problema: un valore per gli euro, uno per i dollari e uno per il tasso di conversione
    • usa gli identificatori, la cui sintassi è spiegata in S2.7
    • dichiara le variabili come in S2.9
    • attenzione al tipo utilizzato per le variabili!
  • scrivi l'espressione per convertire il valore da euro a dollari (per esempi di espressioni: S2.11)
  • stampa il risultato utilizzando la funzione printf (vedi S2.12)

Suggerimenti

  • non dimenticare di includere stdio.h per l'uso delle istruzioni di stampa
  • ricorda che il valore da stampare è di tipo double, quindi usa lo specificatore di formato corretto nella printf (tabella T2.2 in S2.12)

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Conversione di valute - Soluzione

Una possibile soluzione al problema è la seguente:

#include <stdio.h>

int main()
{
  /* dichiarazione delle variabili utilizzate nel programma */
  double euro = 123.10;                   // valore da convertire
  double cambio_euro_dollaro = 1.18213;   // tasso di cambio
  double dollari;                         // variabile per memorizzare il risultato

  /* conversione da euro a dollari */
  dollari = euro * cambio_euro_dollaro;

  /* stampa dei risultati */
  printf("Importo in euro : %.2f\n", euro);
  printf("Importo in dollari : %.2f\n", dollari);
}

In questo primo esempio possiamo cominciare a vedere le parti in cui è suddiviso un programma nel linguaggio C.

Una prima parte usa la direttiva #include per specificare le librerie necessarie alla seconda parte. La direttiva viene specificata nel seguente modo:

#include <stdio.h>

Questa direttiva serve ad "importare" le funzioni che vengono utilizzate nel programma, in particolare la funzione printf per la stampa a video.

Le direttive non sono vere e proprie istruzioni, e si riconoscono poichè sono precedute dal carattere '#'. Maggiori informazioni si trovano nel capitolo S3.2 del libro.

La funzione main invece contiene le istruzioni per effettuare i calcoli. All'interno del main sono per prima cosa dichiarate e inizializzate delle variabili di tipo double, cioè l'importo in euro e il tasso di cambio, utilizzando i dati forniti nel testo dell’esercizio (S2.9). Una volta dichiarata la variabile che verrà utilizzata per contenere il risutato della conversione, è possibile effettuare il calcolo vero e proprio, cioè una semplice moltiplicazione. Alla fine vengono stampati a video i risultati, utilizzando la funzione printf, e il programma termina. La funzione printf utilizza un particolare formato del testo da stampare per poter visualizzare il contenuto delle variabili. Trovi maggiori informazioni nel libro (S2.12).

Hands-on

Esercitati apportando alcune semplici modifiche al programma.

prova-tu Modifica il programma rimuovendo la riga della #include e ri-compilando il programma. Cosa noti?

prova-tu Modifica l'importo assegnato alla variabile euro e fatti stampare il valore in dollari.

prova-tu Modifica il nome della variabile cambio_euro_dollaro dichiarandola invece come segue:

  double tasso = 1.18213;

Adatta il programma di conseguenza.

CONSIGLIO: per verificare il funzionamento del programma, utilizza un valore dell'importo che renda semplice il calcolo manuale e quindi una semplice verifica della correttezza del risultato!

prova-tu Testa il funzionamento del programma con i seguenti importi: 2, 10, 100, 1000.

prova-tu Prova con euro = 123961.50. Riesci a verificare velocemente che il valore restituito in dollari è corretto?

Tieni conto che normalmente ci si può fidare del risultato di un calcolo aritmetico, quindi non serve il controllo manuale! Se però i calcoli sono complessi e articolati, un controllo sui risultati può aiutare ad escludere possibili bug del programma

prova-tu Ora viene utilizzato lo specificatore %.2f per stampare i risultati:

  • prova ad utilizzare lo specificatore %d oppure %i
  • prova ad utilizzare uno dei seguenti: %1.0f, %10.2f, %20.8f

Vedi S2.12 per una spiegazione di come utilizzare gli specificatori di formato.

Calcolo dei resti

Si supponga di dover pagare un certo importo in euro (intero, senza centesimi) utilizzando banconote da 50, 20, 10 e 5 euro, e monete da 2 e da 1 euro. Si scriva un programma che determini quante monete e banconote di ciascun tipo bisogna utilizzare se si vuole massimizzare il numero di banconote e monete di taglio maggiore (che equivale ad utilizzarne, complessivamente, il minor numero possibile).

Ad esempio, dato l'importo di 123 euro il programma dovrà stampare:

Importo totale: 123 euro
2 banconote da 50 euro
1 banconote da 20 euro
0 banconote da 10 euro
0 banconote da 5 euro
1 monete da 2 euro
1 monete da 1 euro

Indicazioni per la soluzione

  • scrivi la funzione main (vedi S2.1)
  • dichiara le variabili necessarie a contenere i dati del problema:
    • un valore per l'importo, una variabile per calcolare il numero di banconote/monete (una per ciascun tipo di banconota/moneta), una per contenere il resto "corrente"
    • usa gli identificatori, la cui sintassi è spiegata in S2.7
    • dichiara le variabili come in S2.9
    • attenzione al tipo utilizzato per le variabili: intere o in virgola mobile?
  • scrivi le giuste espressioni per calcolare il numero di banconote/monete (esempi di espressioni: S2.11)
  • stampa il risultato utilizzando la funzione printf (vedi S2.12)

Suggerimenti

  • non dimenticare di includere stdio.h
  • per calcolare il numero di banconote/monete si procede con una serie di divisioni e di calcoli del resto
  • serviranno gli operatori di divisione / (vedi S11.2.1) e di resto della divisione intera % (vedi S11.2.2)

Per il calcolo del numero di banconote/monete si può procedere come segue:

  1. Se si ha un importo X=123 euro, per calcolare il numero di banconote da 50 euro, assegnandone il valore alla variabile b50, bisogna fare la divisione b50 = X/50, il cui risultato è 2 (ATTENZIONE: la divisione deve essere fatta tra numeri interi!)
  2. Di seguito si calcola il resto con resto = X % 50 (il risultato è 23), assegnando il risultato alla variabile resto
  3. Il resto diviene il nuovo importo su cui calcolare il numero di banconote da 20 euro, e così via, riprendendo dal punto (1)

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Calcolo dei resti - Soluzione

Un esempio di soluzione per l'esercizio del calcolo dei resti è la seguente:

#include <stdio.h>

int main()
{
  int importo = 123;
  int b50, b20, b10, b5, m2, m1;
  int resto;

  /* calcolo del numero di monete e banconote */
  b50 = importo / 50;     // numero di banconote da 50 euro
  resto = importo % 50;
  b20 = resto / 20;       // numero di banconote da 20 euro
  resto = resto % 20;
  b10 = resto / 10;       // numero di banconote da 10 euro
  resto = resto % 10;
  b5 = resto / 5;         // numero di monete da 5 euro
  resto = resto % 5;
  m2 = resto / 2;         // numero di monete da 2 euro
  m1 = resto % 2;

  /* stampa del numero di banconote calcolate */
  printf("Importo totale : %d euro\n", importo);
  printf("%d banconote da 50 euro\n", b50);
  printf("%d banconote da 20 euro\n", b20 );
  printf("%d banconote da 10 euro\n", b10);
  printf("%d banconote da 5 euro\n", b5);
  printf("%d monete da 2 euro\n", m2);
  printf("%d monete da 1 euro\n", m1);

  return 0;
}

Commenti

Come nell'esercizio precedente, sono presenti le istruzioni per la dichiarazione e l'inizializzazione delle variabili, in questo caso degli interi. In questo secondo esercizio è necessario effettuare più operazioni. In particolare, verranno calcolati una divisione tra interi e un resto per ogni taglio di banconote. Infine è possibile stampare a schermo il testo formattato come richiesto, usando la funzione printf il numero necessario di volte.

Si noti come venga utilizzata una sola variabile resto per memorizzare l'importo rimanente dopo ogni calcolo del numero di banconote/monete.

Hands-on

prova-tu Cambia il valore dell'importo per calcolare il numero di banconote/monete corrispondente

  • non dimenticare di salvare il file modificato e di ri-compilare

CONSIGLIO: per verificare il funzionamento del programma, utilizza un valore dell'importo che renda semplice il calcolo manuale e quindi una semplice verifica della correttezza del risultato!

prova-tu Testa il funzionamento del programma con i seguenti importi: 7, 51, 88, 223, 1011.

prova-tu Usa il valore 1025679 per il test. Riesci a verificare velocemente che il numero di banconote da 50 euro calcolato dal programma è corretto?

prova-tu L'ordine con cui si svolgono i calcoli è solitamente molto importante. Talvolta si può cambiare l'ordine di alcune operazioni, mentre in altri casi no, in quanto si introducono errori nel calcolo.

Prova a spostare o eliminare una qualsiasi delle istruzioni che calcolano il numero di banconote/monete e i resti.

Talvolta il compilatore fornisce indicazioni utili che aiutano a scoprire gli errori logici. Per esempio, che succede compili il programma dopo aver eliminato la prima istruzione (soltanto) che calcola il resto (resto = importo % 50)? E che succede invece se compili dopo aver eliminato soltanto la seconda (resto = importo % 20)?

prova-tu Invece di utilizzare una variabile per ciascun numero di banconote/monete, è possibile ridurre considerevolmente il numero di variabili utilizzate ri-assegnando alla variabile importo il resto dell'operazione precedente e stampando i valori man mano che vengono calcolati.

# include <stdio.h>

int main() {
  int importo = 123;   // importo iniziale
  int n;               // numero di banconote/monete

  printf("Importo totale: %d euro\n", importo);

  n = importo / 50;                         // numero di banconote da 50 euro
  printf("%d banconote da 50 euro\n", n);   // stampa
  importo = importo % 50;                   // il resto è il nuovo importo
  n = importo / 20;                         // numero di banconote da 20 euro
  printf("%d banconote da 20 euro\n", n);
  importo = importo % 20;
  n = importo / 10;                         // numero di banconote da 10 euro
  printf("%d banconote da 10 euro\n", n);
  importo = importo % 10;
  n = importo / 5;                          // numero di banconote da 5 euro
  printf("%d banconote da 5 euro\n", n);
  importo = importo % 5;
  n = importo / 2;                          // numero di monete da 2 euro
  printf("%d monete da 2 euro\n", n);
  n = importo % 2;                          // numero di monete da 1 euro
  printf("%d monete da 1 euro\n", n);

  return 0;
}

Uso degli operatori in forma abbreviata

Vari operatori utilizzabili nelle espressioni hanno una forma abbreviata. Vengono utilizzati quando il risultato dell'operazione effettuata su una variabile va ri-assegnato alla variabile stessa. Nella sezione S11.3.2 ci sono tutti gli esempi di forme abbreviate.

In pratica, le due seguenti espressioni sono equivalenti:

dato = dato OP valore
dato OP= valore

dove la seconda espressione usa l'operatore OP in forma abbreviata. L'operatore OP può essere uno tra i vari operatori del C.

Per esempio

resto = resto % 10;
resto %= 10;

sono equivalenti.

Sono molto utili, per esempio, per incrementi:

x = x + 10;
x += 10;

e decrementi:

y = y - 2;
y -= 2;

prova-tu Modifica la soluzione proposta per il calcolo dei resti al fine di utilizzare gli operatori in forma abbreviata dove possibile.

Costrutti condizionali e cicli

I costrutti di controllo condizionali e iterativi costituiscono gli strumenti base per poter realizzare programmi che implementano algoritmi articolati.

Gli esercizi proposti sono il calcolo della mediana tra tre numeri e il calcolo della potenza con il metodo del contadino russo.

Prima di presentare gli esercizi, però, verrà introdotto il metodo utilizzato in tutti i successivi tutorial per la verifica automatica della correttezza dei calcoli del programma. I prossimi programmi, infatti, svolgeranno calcoli un po' più articolati di quelli visti sinora, e diventa quindi tedioso verificare manualmente la correttezza del risultato generato.

Il testing automatico

L'esecuzione manuale del programma per verificare la correttezza dei risultati generati è una procedura comune, ma talvolta inaffidabile e spesso tediosa, in quanto va ripetuta molto spesso durante lo sviluppo del programma.

L'utilizzo di strumenti per la verifica automatica consente una scrittura rapida dei programmi ed una riduzione sistematica del numero di bug. Questa è una pratica ormai consolidata nel moderno sviluppo software, e viene chiamato testing automatico.

Uso di pvcheck per il testing

In questa esercitazione, e in quelle successive, si farà uso del programma pvcheck per l'esecuzione automatica di una sequenza di test predefiniti. Il software esegue il programma in esame e confronta i valori che produce con quelli corretti. Eventuali errori sono stampati a video.

pvcheck è un programma open source il cui codice sorgente completo è disponibile gratuitamente su github. Per essere eseguito richiede l'installazione del linguaggio Python (versione 3.4 o superiore). Python solitamente si trova già installato nei sistemi Linux. È già installato nell'ambiente online configurato per le esercitazioni. Se si utilizza invece Cygwin sotto Windows, Python può essere installato mediante l'apposito programma di setup.

Per comodità, è possibile scaricare una versione eseguibile, invece di utilizzare il codice sorgente linkato precedentemente, che richiede una conoscenza un po' più avanzata degli strumenti necessari. pvcheck viene utilizzato da terminale. Per verificare se è installato e correttamente eseguibile, si può invocare con il seguente comando da terminale:

pvcheck --help

Varianti del comando

Normalmente il comando pvcheck viene invocato come segue:

pvcheck ./nome-del-file-eseguibile

dove nome-del-file-eseguibile è, appunto, il nome del file eseguibile.

Tieni presente che pvcheck è un comando che si esegue da terminale, come tanti altri (ls, cp, ecc.). il comando precedente funziona quando pvcheck è installato in directory di sistema.

Talvolta pvcheck viene fornito come programma collocato nella directory corrente. In questo caso il comando per l'invocazione è leggermente diverso:

./pvcheck ./nome-del-file-eseguibile

dove i caratteri ./ iniziali servono ad indicare alla shell che va eseguito il programma pvcheck contenuto nella directory corrente.

File di test

Per poter utilizzare pvcheck occorre disporre dei dati, opportunamente codificati, relativi ai test da condurre. Tali dati verranno messi a disposizione insieme al testo degli esercizi sotto forma di file con l'estensione .test.

Formato dell'output del programma

Per poter effettuare la verifica è necessario che l'output del programma da verificare sia formattato secondo le modalità previste da pvcheck. Per fare questo è sufficiente far precedere ciascun risultato da un opportuno marcatore, racchiuso tra parentesi quadre. Per esempio, il seguente output potrebbe essere stampato dal programma che calcola la conversione da euro in dollari:

[DOLLARI]
145.52

Mediana di tre numeri

Si scriva un programma che legga 3 valori interi e che ne stampi la mediana. La mediana è il valore che occuperebbe la seconda posizione se ordinassimo i tre numeri. Si nomini il sorgente mediana.c.

La stampa del risultato dovrà essere preceduta da uno specifico marcatore MEDIANA scritto tra parentesi quadre. Ad esempio, se i tre numeri letti sono 30, 10, 20, il programma dovrà stampare le righe:

[MEDIANA]
20

Si compili il programma con il comando

gcc -Wall mediana.c

Se non ci sono errori di compilazione, si esegua il programma a.out generato per verificarne la correttezza:

./a.out

Ricorda che l'opzione -Wall abilita l’output, da parte del compilatore, di messaggi di avvertimento che segnalano situazioni insolite, che potrebbero corrispondere ad errori di programmazione.

Verifica automatica

I casi di test sono descritti nei file pvcheck.test. Un "caso di test" contiene i dati di input per il programma e il corrispondente output corretto atteso. In questo modo pvcheck può verificare automaticamente la correttezza dei risultati a fronte dei dati di ingresso.

Il file di test può contenere vari casi di test.

Scarica una versione eseguibile di pvcheck. Dopo aver copiato lo script pvcheck nella directory corrente (se non è già presente), verifica la correttezza del programma tramite il comando

./pvcheck ./a.out

e osservando l'output del programma.

NOTA: se il programma non viene eseguito correttamente, su alcuni sistemi potrebbe essere necessario eseguire da shell il comando

chmod +x pvcheck

per impostare i permessi di esecuzione di pvcheck.

Se pvcheck indica la presenza di errori, correggi il programma, verificando la correttezza della nuova versione utilizzando pvcheck.

Continua il processo di correzione e verifica finché pvcheck non indica che l'output generato dal programma è corretto.

NOTA: pvcheck cerca automaticamente un file di test chiamato pvcheck.test nella directory corrente, utilizzando quel file per eseguire i test sul programma. Verifica eventualmente che il file di test sia presente nella directory in cui esegui pvcheck.

Indicazioni per la soluzione

  • scrivi la funzione main (vedi S2.1)
  • dichiara le variabili necessarie a contenere i dati del problema (es, a, b e c)
  • leggi il valore delle tre variabili da tastiera usando fgets e atoi (ricorda di dichiarare la stringa "di supporto"), vedi S4.1
  • utilizza opportune combinazioni di costrutti if e if-else (vedi rispettivamente S4.3 e S4.4 per la sintassi) per assegnare il valore mediano ad una variabile m

Suggerimenti

  • nei costrutti condizionali potrebbero servire delle condizioni logiche "complesse"; gli operatori logici necessari per realizzarle sono descritti in S4.5;
  • non dimenticare di includere stdlib.h per l'uso di atoi (e, come già sai, di stdio.h per printf e fgets)

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Mediana tra tre numeri - Soluzione

Un esempio di soluzione per l'esercizio per il calcolo della mediana è la seguente:

#include <stdio.h>
#include <stdlib.h>

int main()
{
    int a, b, c;  /* Dati in ingresso */
    int m;        /* Risultato */
    char s[80];

    /* Lettura dei dati */
    printf("Inserire il primo valore: ");
    fgets(s, sizeof(s), stdin);
    a = atoi(s);
    printf("Inserire il secondo valore: ");
    fgets(s, sizeof(s), stdin);
    b = atoi(s);
    printf("Inserire il terzo valore: ");
    fgets(s, sizeof(s), stdin);
    c = atoi(s);

    /* Ci sono sei modi diversi in cui si possono ordinari 3 elementi */
    if (a <= b && a <= c) {
        /* a e` il minimo */
        if (b < c)
            m = b;
        else
            m = c;
    } else if (b <= a && b <= c) {
        /* b e` il minimo */
        if (a < c)
            m = a;
        else
            m = c;
    } else {
        /* c e` il minimo */
        if (a < b)
            m = a;
        else
            m = b;
    }

    /* Stampa del risultato nel formato previsto da pvcheck */
    printf("[MEDIANA]\n");
    printf("%d\n", m);
    return 0;
}

Commenti

  • viene usata fgets per la lettura di una stringa da tastiera, per cui serve anche la dichiarazione della stringa (s in questo caso) per memorizzare i caratteri inseriti da tastiera
  • vengono usata atoi per convertire le stringhe in numeri interi
  • la strategia della soluzione prevede di trovare prima il minimo globale, e la mediana risulta essere il minimo tra i rimanenti due numeri; le operazioni svolte sono:
    • con a <= b && a <= c si stabilisce se a è il minimo globale (e la mediana è il minimo tra b e c)
    • alternativamente, con b <= a && b <= c si stabilisce se è b ad essere il minimo globale (e la mediana è il minimo tra a e c)
    • in caso entrambe le precedenti siano false, allora è c ad essere il minimo globale (e la mediana è il minimo tra a e b)

Hands-on

prova-tu Riscrivi il primo costrutto condizionale if (a <= b && a <= c) { omettendo le parentesi tonde come segue:

if a <= b && a <= c {

Ricompila il programma e verifica cosa succede. Perché?

La sintassi del costrutto condizionale è if ( espr ). Le parentesi tonde sono obbligatorie. Omettere le parentesi tonde corrisponde ad un errore di sintassi, che viene segnalato automaticamente dal compilatore.

prova-tu Verifica il funzionamento della soluzione proposta con pvcheck modificando il programma per omettere la stampa del marcatore [MEDIANA]. Cosa ottieni da pvcheck?

I marcatori sono indispensabili a pvcheck per identificare i diversi risultati attesi. Se viene omesso il marcatore pvcheck segnala la sua mancanza.

prova-tu Verifica il funzionamento della soluzione proposta con pvcheck modificando il messaggio di stampa del risultato in La mediana è %d\n, e mantenendo invariata la stampa del marcatore. Cosa ottieni da pvcheck?

pvcheck si aspetta che il risultato venga stampato ESATTAMENTE con il formato specificato. Se viene stampato del testo in eccesso (o manca del testo) la risposta viene considerata sbagliata.

prova-tu Verifica il funzionamento della soluzione proposta con pvcheck modificando il marcatore, stampando per esempio [RISULTATO] al posto di [MEDIANA]. Cosa ottieni da pvcheck?

Il marcatore deve essere esattamente quello atteso da pvcheck; se viene usato un nome diverso, pvcheck considera assente il marcatore necessario.

prova-tu Verifica la soluzione proposta in modo da individuare prima il massimo globale, e poi cercando il massimo tra i rimanenti due numeri. Verifica poi con pvcheck che la nuova soluzione sia corretta!

Potenza col metodo del contadino russo

Il "metodo del contadino russo" è un procedimento efficiente per il calcolo della potenza a^b (a elevato alla potenza di b).

NOTA: nel seguito, per chiarezza i numeri verranno scritti con notazione anglosassone, ovvero usando il punto per separare i decimali e la virgola per separare le migliaia (es. 1,564,373.03).

Il metodo consiste nell'avere una variabile risultato inizializzata pari a 1. Dopodichè, si realizza un ciclo nel quale

  • la variabile risultato viene moltiplicata per a solo se b è dispari.
  • successivamente, si aggiorna il valore di a calcolandone il quadrato, mentre b viene dimezzato, arrotondando per difetto

Il ciclo termina quando b si annulla.

Esempio

Si supponga di voler calcolare la potenza 3^12 (3 elevato a 12). I passi dell'algoritmo determinerebbero la seguente sequenza di valori (risultato è abbreviato in r, mentre le "freccette" servono a indicare la valutazione delle espressioni):

r     <- 1
a     <- 3
b     <- 12

r non viene modificato perché b è pari
a = a * a    <- 3 * 3     <- 9
b = b / 2    <- 12 / 2    <- 6

r non viene modificato perché b è ancora pari
a = a * a    <- 9 * 9    <- 81
b = b / 2    <- 6 / 2    <- 3

r = r * a    <- 1 * 81     <- 81 (in questo caso b è dispari)
a = a * a    <- 81 * 81    <- 6,561
b = b / 2    <- 3 / 2      <- 1

r = r * a    <- 81 * 6,561       <- 531,441 (in questo caso b è dispari)
a = a * a    <- 6,561 * 6,561    <- 43,046,721
b = b / 2    <- 1 / 2            <- 0

A questo punto il procedimento termina, in quanto b si annulla, e il risultato è 531,441.

NOTA: il risultato è stato ottenuto, in pratica, moltiplicando i due valori 81 e 6561 corrispondenti agli esponenti dispari 3 e 1.

Richiesta

Si scriva un programma in linguaggio C (chiamiamolo potenza.c) che legga da terminale la base a, anche non intera, e l'esponente b (deve essere intero positivo) e che calcoli la potenza a^b utilizzando il metodo appena descritto.

La stampa del risultato dovrà essere preceduta da uno specifico marcatore POTENZA scritto tra parentesi quadre. Ad esempio, a fronte della sequenza di input 2.5 e 3 il programma dovrà stampare le righe:

[POTENZA]
15.625

Si compili il programma con il comando

gcc -Wall potenza.c

e lo si esegua per verificarne la correttezza.

Verifica automatica

Scarica una versione eseguibile di pvcheck.

I casi di test sono descritti nei file potenza.test. Si verifichi la correttezza del programma scritto precedentemente tramite il comando

./pvcheck -f potenza.test ./a.out

Si corregga il programma finché non fornisce la risposta corretta.

NOTA: l'opzione -f potenza.test è necessaria in quanto il nome del file differisce da quello predefinito pvcheck.test. Infatti, se pvcheck viene invocato come segue

./pvcheck ./a.out

non bisogna specificare l'opzione -f perché pvcheck cerca automaticamente un file di test chiamato pvcheck.test.

Indicazioni per la soluzione

  • scrivi la funzione main (vedi S2.1)
  • dichiara le variabili necessarie a contenere i dati del problema (es., a, b e risultato)
  • leggi il valore delle variabili necessarie da tastiera usando fgets, atoi e atof (ricorda di dichiarare la stringa "di supporto") (vedi S4.1
  • utilizza il costrutto iterativo while per effettuare il ciclo (vedi S4.7 per la sintassi)
  • utilizza vari costrutti if (vedi S4.3 per la sintassi)

Suggerimenti

  • il programma deve considerare il caso in cui l'utente inserisca un valore di b negativo, nel qual caso stampi la stringa non calcolabile nella riga successiva al marcatore
  • potrebbe servire l'impostazione di condizioni logiche "complesse" nei costrutti condizionali; gli operatori logici necessari sono descritti in S4.5;
  • non dimenticare di includere stdlib.h per l'uso di atoi e atof (e, come già sai, di stdio.h per printf e fgets)
  • dal momento che serve individuare la condizione per cui b è dispari, si può usare l'operatore % oppure il metodo del troncamento della divisione intera (vedi gli esempi in S4.3 e S4.4)

ATTENZIONE: l'espressione a^b può essere fuorviante:

  • anche se viene usata spesso in informatica per indicare l'elevamento a potenza, in C non ha questo significato: non indica l'elevamento a potenza!
  • si tratta comunque di una espressione valida in linguaggio C, perché l'operatore ^ viene usato per l'operazione di XOR binario (è illustrato in S11.6.3)
  • in C non esiste un operatore specifico per l'elevamento a potenza, quindi questa operazione va svolta esplicitamente con dei cicli (come in questo esercizio)

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Potenza col metodo del contadino russo - Soluzione

Un esempio di soluzione per l'esercizio del calcolo della potenza con il metodo del contadino russo è la seguente:

#include <stdio.h>
#include <stdlib.h>

int main() {
    char s[80];
    double a;
    int b;
    double risultato;

    /* Lettura dei dati */
    printf("Inserisci la base: ");
    fgets(s, sizeof(s), stdin);
    a = atof(s);
    printf("Inserisci l'esponente: ");
    fgets(s, sizeof(s), stdin);
    b = atoi(s);

    if ((b < 0) || (a == 0 && b == 0)) {
        /* Il calcolo non puo` essere eseguito */
        printf("[POTENZA]\n");
        printf("non calcolabile\n");
    } else {
        /* Parte principale dell'algoritmo */
        risultato = 1;
        while (b > 0) {
            /* Quando l'esponente e` dispari si esegue la moltiplicazione
               che produce il risultato */
            if (b % 2 == 1)
                risultato *= a;
            a = a * a;  /* quadrato della base */
            b = b / 2;  /* dimezzamento dell'esponente con arrotondamento */
        }

        /* Stampa del risultato nel formato previsto da pvcheck */
        printf("[POTENZA]\n");
        printf("%f\n", risultato);
    }

    return 0;
}

Commenti

  • viene usata fgets per la lettura di una stringa da tastiera, per cui serve anche la dichiarazione della stringa (s in questo caso) per memorizzare i caratteri inseriti da tastiera
  • vengono usate sia atoi che atof per convertire numeri che devono essere rispettivamente interi e in virgola mobile
  • la condizione b < 0 prima del ciclo serve per evitare di eseguirlo in caso l'esponente sia negativo (con relativa stampa di output)
  • sempre prima del ciclo, si verifica, nella stessa condizione, se a e b sono contemporaneamente nulli: in questo caso la potenza non è definita e si stampa il messaggio non calcolabile
  • si noti l'espressione b % 2 == 1 per determinare se b è dispari
  • l'operazione b = b / 2 fa già l'arrotondamento per difetto (si ricordi che la divisione intera tronca la parte decimale)

Hands-on

prova-tu Riscrivi il costrutto iterativo while (b > 0) { omettendo le parentesi tonde come segue:

while b > 0 {

Ricompila il programma e verifica cosa succede. Perché?

Scoprilo cliccando qui »

prova-tu Modifica il programma per gestire correttamente anche gli esponenti interi negativi. Sfrutta la relazione a^b = (1/a)^(−b).

Scopri esattamente come »

prova-tu In questo caso, è possibile utilizzare il ciclo do-while al posto del ciclo while utilizzato nella soluzione proposta?

Scopri di più »

Break e continue - Refactoring

Uno degli scopi del testing automatico, quello che in queste esercitazioni viene realizzato per mezzo di pvcheck, consiste nella possibilità di partire da un programma funzionante e di farne il refactoring. Fare il refactoring di un programma significa "modificare la struttura interna di porzioni di codice senza modificarne il comportamento esterno".

Il refactoring di un programma viene fatto per vari motivi: per semplificare il codice, magari accorciandone la lunghezza sintetizzando gli algoritmi, per migliorarne la chiarezza, per esempio cambiando nomi a variabili e funzioni, ecc.

La pagina Wikipedia sul refactoring può essere un utile punto di partenza per approfondire questo importante aspetto del moderno sviluppo software.

Il testing automatico permette di verificare che un programma il cui codice è stato rifattorizzato fornisca lo stesso output del programma originale, cosa che garantisce (entro certi limiti) che il refactoring non abbia introdotto degli errori nel codice.

Break e continue

Il programma (break-continue.c), di cui si riporta la parte rilevante della funzione main, rappresenta una versione semplificata del programma riportato nel libro di testo (vedi S4.10) per spiegare l'uso delle istruzioni break e continue:

    puts("[OUTPUT]");
    do {
        fgets(s, sizeof(s), stdin);
        n = atoi(s);

        if (n < 0) {
          printf("%d < 0: uscita dal loop...\n", n);
          break;
        }
        
        if (n > 10) {
          printf("%d > 10: non ammesso\n", n);
          continue;
        }
        
        /* utilizza il valore di n */
        printf("Valore immesso: n = %d\n", n); 
    } while (1);
    puts("Programma terminato");

Il programma esegue un ciclo do-while infinito, che ad ogni iterazione, legge un valore numerico da tastiera traminte fgets e atoi. A seconda del valore letto e delle condizioni presenti nel programma, vengono stampati a video dei messaggi, ed eventualmente il programma termina.

Si compili il programma con il comando

gcc -Wall break-continue.c

Se non ci sono errori di compilazione, si può eseguire il programma a.out generato per verificarne la correttezza. Un esempio di ciò che avviene nel terminale durante l'esecuzione del programma è il seguente:

$ ./a.out
[OUTPUT]
5
Valore immesso: n = 5
50
50 > 10: non ammesso
-500
-500 < 0: uscita dal loop...
Programma terminato

dove

  • la riga $ ./a.out è il comando eseguito per lanciare il programma (include il prompt $, che non fa parte del comando da impartire);
  • le linee che riportano i valori 5, 50 e -500 sono inserite da tastiera dall'utente;
  • il resto è stampato a video dal programma.

Il contenuto del terminale riportato sopra si ottiene lanciando il programma e inserendo, tra un output e l'altro, i tre numeri 5, 50 e -500, in questo ordine. La stampa di [OUTPUT] è necessaria per poter far valutare il testo stampato tramite pvcheck.

Verifica automatica

Il programma può essere testato utilizzando il file di test break-continue.test, ed eseguendo il comando:

pvcheck -f break-continue.test ./a.out

Si ricordi che il file break-continue.test deve essere presente nella directory nella quale si sta lanciando pvcheck per testare il programma a.out precedentemente ottenuto con la compilazione di break-continue.c.

L'opzione -f seguita dal nome del file di test è necessaria in quanto pvcheck cerca normalmente un file di test chiamato pvcheck.test, e se non lo trova, termina con un errore. Per utilizzare un file di test diverso da pvcheck.test si una l'opzione -f per specificarne il nome.

Equivalenza tra programmi

Per il Teorema di Jacopini-Bohm, le strutture fondamentali della programmazione strutturata sono sufficienti per realizzare qualsiasi algoritmo. Quindi che le strutture "base" del linguaggio C (strutture condizionali e iterative) sono sufficienti per realizzare il programma break-continue.c.

Richiesta

Fare il refactoring del programma break-continue.c in modo da eliminare l'uso sia di break che di continue. Il nuovo programma dovrà essere equivalente a quello di partenza, ovvero dovrà produrre lo stesso risultato (il testo stampato a video) a fronte di qualsiasi sequenza di valori inseriti da tastiera.

L'equivalenza del nuovo programma con quello esistente può essere verificata per mezzo di pvcheck: se i test applicati al nuovo programma vengono superati tutti, significa che (entro i limiti di verifica dei test) il nuovo programma è equivalente a quello originale.

Suggerimenti

Per ottenere un programma equivalente eliminando l'uso di break e continue bisogna introdurre una condizione opportuna di terminazione del ciclo do-while, che quindi non potrà più essere un ciclo infinito.

Inoltre, sarà necessario riorganizzare opportunamente le istruzioni condizionali if all'interno del corpo del ciclo, eventualmente introducendo se necessario delle clausole else.

I vettori

I vettori permettono di memorizzare un insieme di elementi di tipo omogeneo.

Nel tutorial che seguirà verranno utilizzati per memorizzare dei valori inseriti da tastiera, valori che verranno poi utilizzati per ricavare i risultati richiesti.

Memorizzazione in un vettore

Progettare e implementare in linguaggio C un programma che acquisisca da tastiera un numero n. Se n <= 0 o se n > 100 stampi a video il testo

[RISULTATO]
errore

e termini immediatamente.

Se invece n > 0 e n <= 100, allora il programma deve acquisire e memorizzare esattamente n numeri interi inseriti da tastiera. Ogni numero corrisponde al voto di un esame universitario. Il valore di un voto è considerato valido se è contenuto nell'intervallo [18, 30]. I voti non validi eventualmente immessi devono essere scartati e non contribuiscono al conteggio dei voti inseriti.

Una volta letti i voti, si utilizzino i valori memorizzati per rispondere alle richieste presentate di seguito.

NOTA: in fondo a questa pagina sono fornite le indicazioni per il test automatico con l'uso di pvcheck.

Suggerimenti

  • Dal momento che l'operazione di lettura deve essere svolta per almeno n volte, va utilizzato un ciclo per ripetere le istruzioni necessarie (i cicli sono presentati in S4.6, S4.7 e S4.8).

DOMANDA: perché il ciclo di lettura deve essere eseguito per almeno n volte?

Scopri perché »
  • Benché possano essere usati tutti e tre i tipi di ciclo del C, tieni presente che il ciclo for (S4.6) viene usato tipicamente quando si conosce il numero di iterazioni da svolgere, pertanto in questo caso è meglio optare per un ciclo while (S4.7) o do-while (S4.8).
  • Utilizza un vettore per memorizzare i valori letti (i vettori sono spiegati in S5.9).

DOMANDA: ogni volta che si dichiara un vettore bisogna dichiararne la dimensione, cioè il numero massimo di elementi memorizzabili; quanti elementi dovrà contenere al massimo il vettore necessario per risolvere questo esercizio?

Verifica la risposta esatta »
  • Dichiara una variabile intera da usare come contatore per memorizzare quanti valori validi sono stati letti; usa un costrutto condizionale (S4.3) per verificare le condizioni per l'incremento
  • il valore del contatore verrà incrementato solo se viene inserito un voto valido
  • La variabile contatore può essere opportunamente utilizzata come indice del vettore, cioè per contenere la posizione del vettore nella quale inserire il voto valido appena letto.
  • É possibile utilizzare le macro (#define, vedi S3.1)) per definire i termini che rappresentano dei numeri significativi nel programma, per esempio i limiti relativi al massimo numero di voti inseribili, oppure i limiti dell'intervallo dei voti validi, evitando l'introduzione di numeri magici nel programma

Richieste

1) Valori memorizzati

Il programma stampi a video i voti memorizzati usando il seguente formato:

[VALORI]
19
28
24

assumendo che i voti validi inseriti siano stati 19, 28 e 24.

Suggerimenti

  • Si può utilizzare un ciclo for per stampare tutti i valori memorizzati nel vettore (il ciclofor è spiegato in S4.6).
  • Utilizzare la variabile contatore del ciclo per indicizzare l'elemento del vettore da stampare.
  • Si ricordi che i vettori vanno indicizzati partendo da zero (vedi S5.9.1).

2) Minimo

Il programma stampi a video il minimo dei voti memorizzati usando il formato:

[MINIMO]
19

Suggerimenti

  • Per calcolare il minimo si devono esaminare tutti i valori del vettore.
  • Utilizza una variabile (es. min) che tenga traccia del valore minimo corrente durante il ciclo.
  • Un ciclo for (S4.6) può essere usato per scorrere tutti i valori memorizzati nel vettore.
  • L'algoritmo migliore per calcolare il minimo è
    • assegnare a min il valore del primo elemento del vettore
    • per ciascuno dei valori seguenti elementi nel vettore, indicando con vet[i] l'i-esimo elemento, se vet[i] < min allora si assegna min = vet[i], altrimenti di prosegue ad esaminare l'elemento successivo; per effettuare queste operazioni servirà l'uso di un costrutto if, presentato in S4.3.

3) Frequenze

Il programma stampi a video le frequenze di occorrenza dei voti memorizzati usando il formato:

[FREQUENZE]
0
1
0
0
0
0
1
0
0
0
1
0
0

Nella prima riga si riporti le frequenze dei 18, nella seconda le frequenze dei 19, ecc.

Suggerimenti

  • Le frequenze (o istogramma) sono il conteggio di quante volte compaiono i numeri 18, 19, ..., 30.
  • Per poter memorizzare questo conteggio, l'ideale è usare un vettore di numeri interi (chiamalo per esempio freq), in cui ciascun elemento memorizza il conteggio per uno specifico valore.

DOMANDA: quale dimensione dovrà avere il vettore freq per memorizzare le frequenze dei voti?

Verifica la risposta esatta »
  • l'uso del vettore è tale per cui freq[0] conterrà il conteggio del voto 18, freq[1] quello dei 19, ... e freq[12] il conteggio dei 30
    • pertanto, se il voto corrente è contenuto nella variabile voto, l'elemento giusto del vettore freq da incrementare è freq[voto - 18], che si incrementa di uno con freq[voto - 18]++
    • infatti:
      • se voto vale 18, si incrementa freq[18 - 18], cioè freq[0]
      • se voto vale 19, si incrementa freq[19 - 18], cioè freq[1]
      • ...
      • se voto vale 30, si incrementa freq[30 - 18], cioè freq[12]

ATTENZIONE: ricorda che un vettore di 13 elementi può essere indicizzato da 0 a 12 compresi (vedi S5.9)

  • un esempio di calcolo delle frequenze è riportato in S6.8

    • ATTENZIONE: l'esempio indicato è applicato ad una stringa, ed è riportato nel capitolo relativo alle funzioni; pertanto occorre isolare opportunamente il codice che gestisce l'istogramma dal resto
  • attenzione all'inizializzazione dei valori del vettore

    • come tutte le variabili, un vettore non inizializzato contiene elementi i cui valori sono aleatori
    • l'inizializzazione dei vettori è illustrata in S5.9.2

4) Massima frequenza

Il prgramma stampi a video il voto con la massima frequenza usando il formato:

[MAXFREQ]
19

In caso più voti abbiano la stessa frequenza, si stampi il primo trovato.

Suggerimenti

  • Per trovare la massima frequenza bisogna trovare il valore massimo nel vettore utilizzato per calcolare l'istogramma.
  • Per il metodo che può essere impiegato per calcolare il massimo valore presente in un vettore, fare riferimento alla soluzione del punto (2) dove, invece di tenere traccia del minimo valore, si calcola il massimo.
  • Il metodo si applicherà al vettore delle frequenze, invece che a quello dei valori, ma l'algoritmo non cambia.

Verifica automatica

Utilizza pvcheck per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test, da scaricare per utilizzare pvcheck, è vettori.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f vettori.test ./a.out

Il riepilogo dovrà essere come il seguente:

SUMMARY
<program>:   4 successes,   0 warnings,   0 errors
FREQUENZE:   3 successes,   0 warnings,   0 errors
MAXFREQ  :   3 successes,   0 warnings,   0 errors
MINIMO   :   3 successes,   0 warnings,   0 errors
RISULTATO:   1 successes,   0 warnings,   0 errors
VALORI   :   3 successes,   0 warnings,   0 errors

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

I vettori - Soluzione

Un esempio di soluzione per l'esercizio sui vettori è il seguente:

#include <stdlib.h>
#include <stdio.h>

#define MINVOTO 18
#define MAXVOTO 30
#define NVOTI   (MAXVOTO - MINVOTO + 1)
#define NMAX    100

int main() {
    char s[80];
    int n;
    int voti[NMAX];
    int i, voto;

    /* input dei dati */
    fgets(s, sizeof(s), stdin);
    n = atoi(s);

    /* se n <= 0 stampo una stringa di errore ed esco */
    if (n <= 0 || n > NMAX) {
        printf("[RISULTATO]\nerrore\n");
        /* NOTA: sarebbe bene restituire un numero != 0, ma pvcheck da errore */
        return 0;
    }

    /* ciclo di lettura dei voti validi */
    i = 0;
    do {
        fgets(s, sizeof(s), stdin);
        voto = atoi(s);

    /* memorizzo il voto e incremento i solo se il voto è valido */
        if (voto >= MINVOTO && voto <= MAXVOTO) {
            voti[i] = voto;
            i++;
        }
    } while (i < n);

    /* stampa valori */
    printf("[VALORI]\n");
    for (i = 0; i < n; i++)
        printf("%d\n", voti[i]);

    /* minimo */
    int min = voti[0];
    for (i = 1; i < n; i++)
        if (voti[i] < min)
            min = voti[i];
    printf("[MINIMO]\n");
    printf("%d\n", min);

    /* frequenze */
    int freq[NVOTI] = {0};
    for (i = 0; i < n; i++)
        freq[voti[i] - MINVOTO]++;
    printf("[FREQUENZE]\n");
    for (i = 0; i < NVOTI; i++)
        printf("%d\n", freq[i]);

    /* indice del voto piu` frequente */
    int maxIndice = 0;
    for (i = 1; i < NVOTI; i++)
        if (freq[i] > freq[maxIndice])
            maxIndice = i;

    /* devo sommare MINVOTO a maxIndice per ottenere il voto */
    printf("[MAXFREQ]\n");
    printf("%d\n", maxIndice + MINVOTO);

    return 0;
}

Commenti

Il funzionamento del programma è molto lineare:

  • all'inizio sono dichiarate una serie di macro, con le direttive #define, che definiscono le costanti relative ai limiti dei valori necessari
    • le macro sono utili per evitare di inserire direttamente nel codice i cosiddetti numeri magici, che rendono problematica la comprensione del codice e eventuali successive modifiche
  • il valore di n viene letto da testiera, e il costrutto if successivo serve a terminare il programma qualora il valore non sia compreso nell'intervallo [1,NMAX]
    • il controllo è necessario in quanto il vettore voti viene dimensionato per memorizzare NMAX elementi
  • il ciclo do-while permette di leggere i valori dei voti
    • si noti che l'indice i che conteggia il numero di voti validi letti viene incrementato solo nel caso in cui il voto letto sia corretto (quindi all'interno del costrutto if annidato nel do-while)
  • la stampa dei valori (marcatore [VALORI]) è un semplice ciclo for da 0 a n-1 che stampa tutti i valori nel vettore voti

Per cercare il minimo

  • viene assegnato alla variabile min il valore del primo elemento del vettore voti[0]
  • con un ciclo for si controllano tutti i voti successivi al primo
  • se si trova un valore inferiore a min ne si aggiorna il valore
  • alla fine si stampa il valore min

Il calcolo della frequenza dei voti è interessante:

  • l'istruzione freq[voti[i] - MINVOTO]++ incrementa di 1 l'elemento del vettore freq di indice voti[i]-MINVOTO
  • voti[i] è il valore del voto corrente, mentre MINVOTO è il valore minimo (18)
  • pertanto e il voto vale 18 verrà incrementato il valore di indice 0, se vale 19 quello di indice 1, ... e se vale 30 il valore di indice 13

Infine, per il calcolo del massimo delle frequenze:

  • si opera come per il minimo, ma invertendo il test per cercare il massimo, e analizzando il vettore freq invece che voti
  • da notare che in questo caso non si tiene traccia del valore massimo, ma dell'indice del valore massimo
  • tale indice viene poi utilizzato alla fine per recuperare il valore massimo dal vettore

Hands-on

La soluzione che include anche il calcolo richiesto dai due seguenti hands-on è vettori-completo.test.

Il programma completo si può testare con

pvcheck -f vettori-completo.test ./a.out

prova-tu Modifica la soluzione dell'esercizio per calcolare anche il massimo dei voti.

Stampi a video il massimo calcolato usando il formato:

[MASSIMO]
28

Suggerimenti

Si proceda come per il minimo, modificando opportunamente i test necessari.

Scopri un esempio di soluzione »

prova-tu Modifica la soluzione dell'esercizio per calcolare anche il voto medio.

Stampa a video la media dei voti usando il formato:

[MEDIA]
23.7

Suggerimenti

Come si vede nell'esempio di output, viene richiesto di stampare un numero con una sola cifra dopo la virgola; per farlo utilizzare lo specificatore di formato %.1f nella printf (gli specificatori di formato sono trattati in S2.12).

Il calcolo della media richiede la somma di tutti i valori, che viene divisa per il numero di valori sommati. Presta attenzione ai seguenti aspetti:

  • la divisione non deve essere fatta tra interi, in quanto il risultato verrebbe arrotondato
  • si può utilizzare un totale di tipo double, oppure fare il cast da intero a double in modo opportuno

Per calcolare la somma si usi un ciclo for (S4.6) e una variabile (es. totale) nella quale sommare il valore di tutti i numeri nel vettore.

Scopri un esempio di soluzione »

ATTENZIONE: non dimenticare di inizializzare la variabile totale assegnando il valore 0 (zero). Cosa succederebbe altrimenti?

Scopri un esempio di soluzione »

Le funzioni

Le funzioni permettono di racchiudere delle porzioni di codice che realizzano determinate operazioni, e di richiamarle zero o più volte all'interno del programma.

Il tutorial che segue riprenderà quello sui vettori, e richiederà di realizzare delle funzioni per l'implementazione delle funzionalità necessarie.

Le funzioni

Realizzare un programma in linguaggio C che acquisisca da tastiera un numero n. Se n <= 0 o se n > 100 stampi a video la stringa:

[RISULTATO]
errore

e termini immediatamente.

Se invece n > 0 e n <= 100, allora il programma deve acquisire e memorizzare esattamente n numeri interi inseriti da tastiera. Ogni numero corrisponde al voto di un esame universitario. Il valore di un voto è considerato valido se è contenuto nell'intervallo [18, 30]. I voti non validi eventualmente immessi devono essere scartati e non contribuiscono al conteggio dei voti inseriti.

Una volta letti i voti, si utilizzino i valori memorizzati per rispondere alle richieste presentate di seguito. Si implementi il programma scrivendo una funzione per risolvere ciascun punto dell'esercizio.

NOTA: in fondo a questa pagina sono fornite le indicazioni per il test automatico con l'uso di pvcheck.

Suggerimenti

Alcuni suggerimenti per l'impostazione dell'esercizio sono illustrati tra quelli forniti per il tutorial sui vettori. Di seguito sono riportati i suggerimenti specifici per l'uso delle funzioni.

Realizza una funzione dedicata per calcolare il risultato di ciascuno dei punti richiesti. Le funzioni devono solo svolgere il compito strettamente indispensabile; la stampa dei risultati (tranne casi specifici) deve avvenire nella funzione chiamante, ovvero nel main.

Può essere utile realizzare anche una funzione per la lettura dei voti:

  • il numero di voti n che verranno inseriti va letta nel main
  • realizza una funzione leggi_voti all'interno della quale sarà riempito il vettore vet leggendo n valori; la dichiarazione può essere come segue:
void leggi_voti(int vet[], int n);

1) Valori memorizzati

Il programma stampi a video i voti memorizzati usando il seguente formato:

[VALORI]
19
28
24

assumendo che i voti validi inseriti siano stati 19, 28 e 24.

Suggerimenti

Per suggerimenti relativi al codice da inserire nella funzione si può fare riferimento alla domanda corrispondente nel tutorial sui vettori.

Per quanto riguarda la realizzazione della funzione:

  • implementare una funzione (es. stampa_vettore) che prenda in input un vettore di interi e ne stampi i valori, uno per riga (un esempio di passaggio di un vettore ad una funzione è usato nella funzione chist in S6.8)
  • per creare una funzione generica, è bene che il numero di elementi del vettore sia passato come argomento alla funzione, oltre al vettore di elementi; per esempio:
void stampa_vettore(int vet[], int n);
  • non è necessario che la funzione stampa_vettore ritorni alcun valore; può quindi restituire void (vedi S6.2)
  • la funzione dovrà solo stampare l'elenco dei numeri;
    • non stampare il marcatore all'interno della funzione: questo rende la funzione più generale e riutilizzabile per la stampa di altri vettori, se necessario
    • il marcatore venga stampato nella funzione main prima di chiamare stampa_vettore

2) Minimo

Stampi a video il minimo dei voti memorizzati usando il formato:

[MINIMO]
19

Suggerimenti

Per suggerimenti relativi al codice da inserire nella funzione si può fare riferimento alla domanda corrispondente nel tutorial sui vettori.

Per quanto riguarda la realizzazione della funzione:

  • implementare una funzione (es. minimo) che prenda in input un vettore di interi e restituisca il minimo dei valori presenti nel vettore (un esempio di passaggio di un vettore ad una funzione è usato nella funzione chist in S6.8)
  • per creare una funzione generica, è bene che il numero di elementi del vettore sia passato come argomento alla funzione, oltre al vettore di elementi; per esempio:
int minimo(int vet[], int n);
  • per restituire il valore desiderato si usi l'istruzione return (S6.3)
  • la funzione dovrà solo restituire il valore minimo; la stampa del valore venga fatta nel main
    • non stampare il marcatore all'interno della funzione: questo rende la funzione più generale e riutilizzabile per la stampa di altri vettori, se necessario
    • il marcatore venga stampato nella funzione main prima di chiamare minimo e di stamparne il valore restituito

3) Frequenze

Stampi a video le frequenze di occorrenza dei voti memorizzati usando il formato:

[FREQUENZE]
0
1
0
0
0
0
1
0
0
0
1
0
0

Nella prima riga si riporti le frequenze dei 18, nella seconda le frequenze dei 19, ecc.

Suggerimenti

Per suggerimenti relativi al codice da inserire nella funzione si può fare riferimento alla domanda corrispondente nel tutorial sui vettori.

Per quanto riguarda la realizzazione della funzione:

  • un esempio di calcolo delle frequenze è riportato in (S6.8)
  • creare una funzione frequenze che riceve tre parametri: i numeri di cui calcolare la frequenza, il numero di elementi del vettore, e il vettore delle frequenze; esempio;
void frequenze(int vet[], int n, int freq[]);
  • tutti i parametri devono essere dichiarati nel main che chiama la funzione frequenze
  • attenzione all'inizializzazione dei valori del vettore
    • va fatta nel main
    • come tutte le variabili, un vettore non inizializzato contiene elementi i cui valori sono aleatori
    • l'inizializzazione dei vettori è illustrata in S5.9.2
  • la stampa delle frequenze avvenga con un ciclo for nel main, dopo aver stampato il marcatore

4) Massima frequenza

Stampi a video il voto con la massima frequenza usando il formato:

[MAXFREQ]
19

In caso più voti abbiano la stessa frequenza, si stampi il primo trovato.

Suggerimenti

Per suggerimenti relativi al codice da inserire nella funzione si può fare riferimento alla domanda corrispondente nel tutorial sui vettori.

Per quanto riguarda la realizzazione della funzione:

  • implementare una funzione (es. max_indice) che prenda in input un vettore di interi e restituisca l'indice del primo valore massimo tra i valori presenti nel vettore (un esempio di passaggio di un vettore ad una funzione è usato nella funzione chist in S6.8)
  • per creare una funzione generica, è bene che il numero di elementi del vettore sia passato come argomento alla funzione, oltre al vettore di elementi; per esempio:
int max_indice(int vet[], int n);
  • per restituire il valore desiderato si usi l'istruzione return (S6.3)
  • la funzione dovrà solo restituire l'indice del valore massimo; la stampa del valore venga fatta nel main
    • non stampare il marcatore all'interno della funzione: questo rende la funzione più generale e riutilizzabile per la stampa di altri vettori, se necessario
    • il marcatore venga stampato nella funzione main prima di chiamare max_indice e di stamparne il valore restituito

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Dal momento che le richieste sono le medesime, il file contenente i test è vettori.test, lo stesso utilizzato per il tutorial sui vettori.

Il comando da eseguire per il test è il seguente:

./pvcheck -f vettori.test ./a.out

Il riepilogo dovrà essere come il seguente:

SUMMARY
<program>:   4 successes,   0 warnings,   0 errors
FREQUENZE:   3 successes,   0 warnings,   0 errors
MAXFREQ  :   3 successes,   0 warnings,   0 errors
MINIMO   :   3 successes,   0 warnings,   0 errors
RISULTATO:   1 successes,   0 warnings,   0 errors
VALORI   :   3 successes,   0 warnings,   0 errors

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Le funzioni - Soluzione

Da questo tutorial non verrà più presentato il codice completo della possibile soluzione, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Si supponga che siano dichiarate le seguenti macro:

#define MINVOTO 18
#define MAXVOTO 30
#define NVOTI   (MAXVOTO - MINVOTO + 1)
#define NMAX    100

Lettura dei voti

La seguente funzione riempie il vettore vet con n voti letti da tastiera. La funzione si preoccupa di verificare che i valori inseriti siano contenuti nell'intervallo richiesto.

void leggi_voti(int vet[], int n)
{
    char s[80];
    int i, voto;

    /* ciclo di lettura dei voti validi */
    i = 0;
    do {
        fgets(s, sizeof(s), stdin);
        voto = atoi(s);

    /* memorizzo il voto e incremento i solo se il voto è valido */
        if (voto >= MINVOTO && voto <= MAXVOTO) {
            vet[i] = voto;
            i++;
        }
    } while (i < n);
}

Il valore di n è passato per valore (S6.5), poiché non serve modificarne il valore ll'interno della funzione.

Il vettore vet è passato per riferimento (S6.6), in questo modo la funzione può modificarne i valori e questi saranno "visibili" anche dalla funzione chiamante (il main).

Stampa di un vettore

La funzione di stampa:

void stampa_vettore(int vet[], int n)
{
    int i;
    for (i = 0; i < n; i++)
        printf("%d\n", vet[i]);
}

ATTENZIONE: formulata in questo modo, può essere utilizzata sia per la stampa dei voti che per la stampa delle frequenze!
Basta passare i giusti argomenti (il vettore e la sua dimensione) alla funzione.

Calcolo del minimo valore nel vettore

Per il calcolo del minimo si può usare la seguente funzione:

int minimo(int vet[], int n)
{
    int i;
   
    min = vet[0];   // il minimo "iniziale" è primo valore del vettore
    for (i = 1; i < n; i++)
        if (vet[i] < min)
            min = vet[i];
    return min;
}

La funzione restituisce un valore int che è il minimo tra i valori presenti nel vettore.

NOTA: la funzione opera su un generico vettore di int composto da n elementi; può quindi essere usata non soltanto su un vettore di voti, ma su un qualsiasi vettore, indipendentemente dal significato dei valori in esso contenuti.

Riempimento del vettore delle frequenze

Le frequenze possono essere calcolate con la seguente funzione:

void frequenze(int vet[], int n, int freq[])
{
    int i;
    for (i = 0; i < n; i++)
        freq[vet[i] - MINVOTO]++;
}

Oltre a vet e n, viene passato per riferimento (il passaggio per riferimento è spiegato in S6.5) anche il vettore freq, che alla fine dell'esecuzione della funzione conterrà il conteggio (ovvero le frequenze) dei valori contenuti in vet; questo metodo viene usato in modo identico a quanto fatto in S6.8.

Non dimenticare di inizializzare a 0 tutti gli elementi del vettore che verrà passato come freq (come accade in S6.8), altrimenti il conteggio partirà da valori aleatori.

Individuazione dell'indice corrispondente al massimo valore

Per stampare il voto che ha frequenza massima, avendo utilizzato un vettore per memorizzare le frequenze come descritto al punto precedente, conviene andare a cercare l'indice dell'elemento di valore massimo nel vettore, invece che il valore massimo stesso:

int max_indice(int vet[], int n)
{
    int i, maxIndice = 0;
    for (i = 1; i < n; i++)
        if (vet[i] > vet[maxIndice])
            maxIndice = i;
    return maxIndice;
}

In questo modo, il voto con frequenza massima non è altro che il valore restituito da max_indice sommato a MINVOTO (che vale 18).

Le stringhe

In generale, le stringhe sono sequenze di caratteri. In C sono memorizzate in vettori di char.

La differenza tra un generico vettore di char e una stringa è che i caratteri che compongono la stringa all'interno del vettore sono delimitati dal carattere '\0' (il valore intero 0). Pertanto, il vettore vet dichiarato come segue:

char vet[20] = "Test";

è composto da 20 valori char, ma soltando i primi 4 caratteri costituiscono la stringa Test. Il carattere vet[4] (il quinto elemento di vet) è il carattere '\0', che corrisponde al valore 0 (zero) decimale, che delimita la stringa. I rimanenti 20 - 5 = 15 caratteri appartengono al vettore ma non alla stringa.

Il tutorial proposto richiede l'elaborazione di stringhe che contengono il nome di percorsi di directory nel file system.

Le stringhe

Nei sistemi operativi della famiglia Unix i file sono indicati tramite percorsi di directory delimitate dal carattere '/'. Spesso è utile poter decomporre un percorso nelle sue componenti. Di particolare interesse sono:

  • il basename del percorso, cioè il nome "puro" del file, senza le componenti che rappresentano le directory
  • il dirname, ovvero il percorso completo senza il nome del file
  • il tipo del percorso, che può essere assoluto (se inizia con il carattere '/') oppure relativo
  • l'estensione del file, che è la parte del nome di un file dopo l'ultimo carattere '.' (punto)

Ad esempio, nel caso del file /home/pippo/percorsi.c, si ha che:

  • il basename è percorsi.c
  • il dirname è /home/pippo
  • il percorso è di tipo assoluto
  • l'estensione è c

Analisi semplificata di un percorso

Si scriva un programma C che esamini un percorso fornito come dato di ingresso. Il percorso deve essere passato al programma come argomento sulla riga di comando. Per esempio, se il programma compilato è a.out, esso deve poter essere eseguito col comando

./a.out /home/pippo/percorsi.c

Si assuma che il percorso non superi i 1024 caratteri.

Suggerimenti

Per poter leggere il percorso da linea di comando, la funzione main deve essere definita come int main(int argc, char *argv[]) (vedi S6.9). Il parametro argv[1] (vedi S6.9.1) conterrà la stringa fornita come argomento.

Realizza una funzione per il calcolo di ciascuno dei risultati richiesti. Vedi S6.3 per la dichiarazione di funzioni e S6.5, S6.6 e S6.7 per il passaggio dei parametri alle funzioni. Tieni conto dei seguenti aspetti:

  • le funzioni dovranno essere richiamate nel main oppure da altre funzioni (se necessario)
  • la stringa contenente il percorso dovrà essere passata a tali funzioni, che restituiranno il risultato calcolato
  • i dettagli su come impostare le varie funzioni verranno forniti di seguito

1) Basename

Determinare il basename e stamparlo col seguente formato:

[BASENAME]
percorsi.c

Suggerimenti

Per iniziare, conviene realizzare una funzione la quale, ricevuto un percorso come argomento, restituisca l'indice dell'ultimo separatore (il carattere /) presente nella stringa, oppure un valore negativo se nessun separatore è presente. La funzione può essere dichiarata come segue:

int trova_ultimo_separatore(const char *percorso);

Per trovare l'ultimo separatore, scrivi un ciclo che parte dall'ultimo carattere della stringa e, procedendo all'indietro, si fermi quando incontra il separatore, restituendo l'indice del carattere nella stringa. Per fare questo:

  • usa la funzione strlen (vedi S5.11) per determinare l'indice corrispodente all'ultimo carattere della stringa (indichiamolo con lung)
  • usa un ciclo while che faccia partire un contatore (es. pos) da lung e lo decrementi (con pos--) fino ad incontrare il primo separatore partendo da destra (quindi l'ultimo della stringa)
  • il ciclo deve terminare se pos diviene negativo (nessun separatore è presente)

Alla fine, la funzione deve restituire il valore di pos.

Crea una funzione basename la quale, usando la funzione trova_ultimo_separatore per determinare la posizione del separatore, restituisce il basename copiandolo in una stringa che viene restituita tramite passaggio per indirizzo. La funzione basename sia dichiarata come segue:

void basename(const char *percorso, char *b);

Puoi usare strcpy (vedi S5.11) per copiare il basename nella stringa da restituire.

IMPORTANTE: Entrambe le stringhe passate per indirizzo devono essere dichiarate nella funzione chiamante (il main).

2) Dirname

Determinare il dirname e stamparlo col seguente formato:

[DIRNAME]
/home/pippo

Suggerimenti

Dichiara una funzione dirname come segue:

void dirname(const char *percorso, char *b);

All'interno della funzione dirname puoi utilizzare la funzione trova_ultimo_separatore spiegata al punto precedente per trovare l'indice del carattere corrispondente all'ultimo separatore (chiamiamo sep l'indice trovato). Un modo per copiare il dirname nella stringa b è il seguente:

  • copia l'intera stringa percorso nella stringa b usando la strcpy (vedi S5.11)
  • modifica la stringa b ponendo b[sep] = 0; in questo modo, i caratteri significativi di b vengono "troncati" al carattere di indice sep, poiché lo 0 diviene il carattere di terminazione della stringa b (il concetto di carattere di terminazione è spiegato in S5.11)

3) Tipo

Determinare il tipo di percorso, relativo oppure assoluto, e stampare rispettivamente relativo oppure assoluto col seguente formato:

[TIPO]
assoluto

Suggerimenti

Puoi dichiarare una funzione chiamata assoluto come segue:

int assoluto(const char *percorso);

La funzione dovrà:

  • ritornare 1 (a significare "vero") in caso si tratti di un persorso assoluto
  • ritornare 0 (a significare "falso") in caso contrario

Utilizza la funzione nel main per stampare il risultato richiesto a seconda del valore restituito dalla funzione assoluto.

Per capire se un percorso è assoluto o relativo basta confrontare il primo carattere della stringa (cioè percorso[0]) con il carattere /: se sono uguali allora il percorso è assoluto, altrimenti è relativo. Ricorda che il confronto tra caratteri viene effettuato usando l'operatore ==.

NOTA: quando si decide un nome per una funzione che debba ritornare un valore vero o falso (1 o 0), è bene pensare a come si usa la funzione in una istruzione del tipo

if (assoluto(percorso)) { ... }

che va letta come "se percorso è assoluto allora ...". In questo caso il nome scelto è corretto dal punto di vista semantico nella frase precedente. Sarebbe andato bene anche il nome relativo, ovviamente avendo cura di invertire il valore restituito dalla funzione.

Non sarebbe andato bene un nome come tipo_di_percorso, perché dal punto di vista semantico "tipo_di_percorso" non è vero/falso, ma può essere relativo/assoluto.

4) Estensione

Determinare l'estensione del file e stamparla con il seguente formato:

[ESTENSIONE]
c

Se l'estensione non è presente, stampare

[ESTENSIONE]
nessuna

Suggerimenti

Puoi realizzare una funzione che restituisce l'indice dell'ultimo punto della stringa (simile a quella realizzata per trovare l'ultimo separatore), chiamandola per esempio trova_ultimo_punto.

Fai attenzione che anche i nomi delle directory possono contenere il carattere punto. Devi quindi gestire opportunamente il caso in cui sia presente un file che non ha estensione mentre una delle directory precedenti include il punto nel proprio nome! (es. /home/utente.con.punto/mio_file). In pratica, devi controllare che l'ultimo punto nel percorso compaia dopo l'ultimo separatore.

Puoi creare una funzione estensione la quale, usando la funzione trova_ultimo_punto per determinare la posizione del punto, restituisce l'estensione copiandola in una stringa che viene restituita tramite passaggio per indirizzo. La funzione estensione può essere dichiarata come segue:

void estensione(const char *percorso, char *b);

Usa la funzione strcpy (vedi S5.11) per copiare l'estensione nella stringa da restituire b Fai attenzione al fatto che entrambe le stringhe passate per indirizzo devono essere dichiarate nella funzione chiamante (il main).

ATTENZIONE: se sono stati seguiti i suggerimenti fino a questo punto, sono state create due funzioni trova_ultimo_separatore e trova_ultimo_punto che sono praticamente identiche nel funzionamento, e si differenziano soltanto per quanto riguarda il carattere cercato.

Riformulare il programma sostituendo queste due funzioni con una sola funzione chiamata

int trova_ultimo_carattere(const char *percorso, char c);

la quale cerca l'ultimo generico carattere c nella stringa percorso. Questa funzione si può usare per cercare sia l'ultimo separatore che l'ultimo punto, in questo modo:

ultimo_sep = trova_ultimo_carattere(percorso, '/');
ultimo_punto = trova_ultimo_carattere(percorso, '.');

In questo modo il programma è più elegante, visto che si evita la duplicazione di codice (due funzioni invece di una) che può essere invece incluso in una unica funzione opportunamente parametrizzata.

Verifica automatica

Utilizza il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è percorsi1.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f percorsi1.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Le stringhe - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Estrazione del basename

La funzione che determina il basename può essere la seguente:

void basename(const char *percorso, char *b) {
    int pos = trova_ultimo_separatore(percorso);
    strcpy(b, percorso + pos + 1);
}

che opera come segue:

  • prima viene trovato l'indice dell'ultimo separatore (pos) usando trova_ultimo_separatore
  • con strcpy viene copiata la porzione di stringa che va da pos + 1 fino alla fine della stringa percorso all'interno della stringa b
    • questa istruzione funziona perché percorso è un indirizzo, ovvero l'indirizzo del primo carattere della stringa (percorso è un puntatore a char e viene usato per effettuare un passaggio di parametri per riferimento, vedi S6.6)
    • quindi spostandosi di pos + 1 caratteri si ottiene l'indirizzo del primo carattere del basename
    • il tutto è reso possibile dal fatto che i caratteri sono memorizzati nella stringa in posizioni contigue della memoria (si ricordi che si tratta di un vettore, vedi S5.9.1)

La funzione trova_ultimo_separatore può essere definita come segue:

int trova_ultimo_separatore(const char *percorso)
{
    int pos = strlen(percorso) - 1;
    while (pos >= 0 && percorso[pos] != '/')
        pos--;
    return pos;
}

e può operare come segue:

  • pos viene inizializzato pari all'indice dell'ultimo carattere della stringa percorso, usando la strlen la quale restituisce la lunghezza della stringa in numero di caratteri (S6.11)
  • il ciclo while (S4.7) continua a decrementare di uno (pos--) l'indice fintanto che non viene incontrato il primo separatore (partendo da destra) oppure si arriva al valore zero (si sono verificati tutti i caratteri della stringa)
  • viene restituito il valore di pos, che varrà 0 se non ci sono separatori, oppure l'indice pos > 0 corrispondente al primo carattere '/' incontrato (il più a destra)

Ricorda che

  • l'operatore && è l'AND logico (S11.5.1)
  • l'operatore != significa "diverso da" (S11.4.1)
  • la costante '/'è una costante intera di tipo char che corrisponde al carattere ASCII / (S5.5)

Estrazione del dirname

Per l'estrazione del dirname si può usare la seguente funzione:

void dirname(const char *percorso, char *b) {
    int i;
    int n = trova_ultimo_separatore(percorso);

    /* Scrittura del terminatore tenendo anche conto del caso n == -1 */
    if (n >= 0) {
        /* Copia in b i primi n caratteri del percorso */
        for (i = 0; i < n; i++)
            b[i] = percorso[i];
        b[n] = '\0';
    } else
        b[0] = '\0';
}

Prima viene trovato l'indice n dell'ultimo separatore usando la funzione trova_ultimo_separatore illustrata nel punto precedente.

Se il valore di n è positivo, si svolgono le seguenti operazioni:

  • con il ciclo for (S4.6) si copiano i primi n caratteri da percorso a b
  • con b[n] = 0 si pone il carattere successivo all'ultimo copiato pari a zero, terminando la stringa

Altrimenti di pone a zero il primo carattere della stringa con b[0] = '\0', che corrisponde a impostare la stringa b pari alla stringa vuota. Questo significa che il percorso non contiene directory ma solo un nome di file.

ATTENZIONE: Ricorda che la costante '\0'è una costante intera di tipo char che corrisponde al carattere ASCII di valore numerico zero (S5.5). Non è il carattere '0', che invece ha valore numerico 48 (T5.3).

Estrazione del tipo di percorso

La funzione può essere realizzata come segue:

int assoluto(const char *percorso)
{
    if (percorso[0] == '/') return 1;
    return 0;
}

Essa restituisce 1 (e quindi "vero") se il primo carattere della stringa percorso, che è percorso[0], corrisponde al carattere '/', altrimenti restituisce 0.

La return 1 termina la funzione, quindi la return 0 viene eseguita solo se il primo carattere percorso[0] è diverso da '/'. Di conseguenza, in questo caso l'else non serve. Un esempio simile è riportato in T5.3.

Nel main bisognerà stampare il valore corretto in output a seconda del valore restituito da assoluto.

Estrazione dell'estensione

Per determinare l'estensione, conviene riformulare la funzione trova_ultimo_separatore generalizzandola per cercare l'ultimo carattere, che specificato come argomento:

int trova_ultimo_carattere(const char *percorso, char c)
{
    int pos = strlen(percorso) - 1;
    while (pos >= 0 && percorso[pos] != c)
        pos--;
    return pos;
}

Questa operazione di "riformulazione" di una funzinalità esistente si chiama in gergo refactoring. Si notino le modifiche minime alla funzione originale.

prova-tu Modifica il programma per usare la nuova funzione trova_ultimo_carattere anche nelle altre funzioni già sviluppate.

La funzione trova_ultimo_carattere può essere usata nella seguente funzione estensione:

int estensione(char *percorso, char *ext) {
    int pos_punto = trova_ultimo_carattere(percorso, '.');
    int pos_sep = trova_ultimo_carattere(percorso, '/');
    int len = strlen(percorso);
    
    /* il punto più a destra è a sinistra del separatore più a destra */
    if (pos_punto < pos_sep)
        return 0;
    /* il punto più a destra è l'ultimo carattere della stringa (no estensione) */
    if (pos_punto + 1 == len)
        return 0;
    /* c'è almeno un punto nel percorso */
    if (pos_punto > 0) {
        strcpy(ext, percorso + pos_punto + 1);
        return 1;
    }
    return 0;
}

la quale opera nel modo seguente:

  • usando la funzione trova_ultimo_carattere, trova la posizione dell'ultimo punto (pos_punto) e dell'ultimo separatore (pos_sep)
  • il primo if causa la terminazione dalla funzione segnalando che non c'è estensione - restituendo 0 con return 0 - se il carattere punto più a destra rimane a sinistra del separatore più a destra (es. /home/dir.con.punto/file); altrimenti...
  • il secondo if esce dalla funzione segnalando che non c'è estensione - restituendo 0 - se il punto più a destra è l'ultimo carattere della stringa (es. /home/utente/nomefile., dove ovviamente non c'è estensione); altrimenti...
  • il terzo if copia l'estensione della stringa di uscita ext se c'è un punto nel percorso e restituisce 1 (attenzione che si arriva a questo punto quando le condizioni dei precedenti due if sono false)
  • altrimenti termina senza trovare un'estensione, con l'ultimo return 0

Analisi completa

Nel calcolo di basename e dirname bisogna fare attenzione ad alcuni casi particolari:

  • se il percorso indica un nome di file "puro", cioè senza alcun separatore, allora il suo dirname è la directory corrente . (punto)
  • se il percorso termina in coda con uno o più separatori, essi devono essere eliminati sia dal dirname che dal basename (attenzione che in questo caso il basename è corriponde al nome di una directory, non di un file; questo però non cambia nulla nella suddivisione tra dirname e basename)
  • se il percorso coincide con la directory speciale /, allora sia il basename che il dirname sono pari a /
  • se c'è un unico separatore, e si trova all'inizio del percorso, allora il dirname è la directory radice /

Si modifichi il programma in modo che basename e dirname tengano in considerazione questi casi particolari. I nuovi elementi calcolati dovranno essere stampati dopo i marcatori:

[BASENAME_COMPLETO] e [DIRNAME_COMPLETO]

in modo da consentire la verifica tramite il comando

./pvcheck -f percorsi2.test ./percorsi

utilizzando il file di test percorsi2.test

Le matrici

Le matrici, o array bi-dimensionali, sono strutture dati che permettono di memorizzare valori di tipo omogeneo organizzati in righe e colonne.

Il tutorial sulle matrici richiede di svolgere varie operazioni su questa struttura dati.

Le matrici

Realizzare un programma in linguaggio C che acquisisca da tastiera una matrice di interi 4x4. I valori degli elementi devono essere compresi nell'intervallo [-5, 5]. I valori non validi eventualmente immessi devono essere scartati e non contribuire al conteggio dei valori inseriti.

La seguente sequenza di valori immessi:

0
10
1
2
3
1
3
2
4
2
3
4
5
-8
3
5
4
0

corrisponderà alla matrice:

0 1 2 3
1 3 2 4
2 3 4 5
3 5 4 0

Suggerimenti

Per rendere il programma pronto a gestire matrici di dimensione diversa da quella specificata, conviene definire le seguenti costanti in forma di macro:

#define NRIG (4)
#define NCOL (4)

e formulare tutto il programma utilizzando opportunamente questi valori. Se un domani servisse per esempio gestire delle matrici 6x3 (o qualsiasi altra dimensione) basterà modificare queste definizioni per adattare automaticamente l'intero programma alla nuova richiesta.

Nella funzione main si dichiari una matrice NRIG x NCOL di interi per ospitare i valori letti (vedi S7.1.1 per la sintassi della dichiarazione).

La lettura dei dati può essere svolta con due cicli for annidati, i cui contatori vengono usati come indici di riga e colonna (si vedano per esempio i cicli in S9.3). Per esempio

for (i = 0; i < NRIG; i++) {
  for (j = 0; j < NCOL; j++) {
    do {
      /* legge un numero e e lo memorizza */
      mat[i][j] = ....
    } while(mat[i][j] > MAX || mat[i][j] < MIN);
  }
}

All'interno del ciclo annidato si dovrà ripetere la lettura del valore - con un altro ciclo - fintanto che esso non è incluso nell'intervallo richiesto [-5, 5], opportunamente indicati con MIN e MAX. Questo terzo ciclo può essere implementato con il costrutto do-while (S4.8), in modo da eseguirlo almeno una volta, il quale contiene le istruzioni di lettura fgets e atoi (S4.8).

1) Stampa della matrice

Stampare la matrice letta col seguente formato:

[MATRICE]
0 1 2 3
1 3 2 4
2 3 4 5
3 5 4 0

Suggerimenti

Supponendo che i valori memorizzati nella matrice siano di tipo int, si può definire una funzione dichiarata come segue:

void stampa_matrice(int mat[NRIG][NCOL]);

La stampa dei dati può essere svolta con due cicli for annidati, i cui contatori vengono usati come indici di riga e colonna, in modo simile all'esempio sopra riportato (si veda anche l'esempio in S9.3).

Si può utilizzare l'istruzione putchar('\n') per stampare l'andata a capo dopo aver stampato i 4 valori su una riga. La funzione putchar è una versione specializzata della putc presentata in S8.3; la funzione precedente equivale a putc('\n', stdout).

2) Media per riga

Calcolare la media di ogni riga della matrice.

Stampare il risultato con il formato seguente (nel caso della matrice di esempio):

[MEDIA_RIGHE]
1.500
2.500
3.500
3.000

Suggerimenti

Un modo intelligente di risolvere questo problema è di definire una funzione dichiarata come segue:

double media_riga(int mat[NRIG][NCOL], int riga);

che calcola la media dei valori della matrice mat sulla riga riga.

La funzione media_riga conterrà un ciclo for che, tenendo fisso l'indice per la riga pari a riga, somma i 4 elementi della riga e divide la somma per 4, restituendo il risultato. Non dimenticare di inizializzare a 0 la variabile che serve a memorizzare la somma dei valori, altrimenti il calcolo si baserà su un valore iniziale aleatorio.

La funzione media_riga verrà richiamata da un ciclo for nella funzione main per la stampa della media di ciascuna delle quattro righe.

3) Media per colonna

Calcolare la media di ogni colonna della matrice.

Stampare il risultato con il formato seguente (nel caso della matrice di esempio):

[MEDIA_COLONNE]
1.500
3.000
3.000
3.000

Suggerimenti

Un modo intelligente di risolvere questo problema è di definire una funzione dichiarata come segue:

double media_colonna(int mat[NRIG][NCOL], int col);

che calcola la media dei valori della matrice mat sulla colonna col.

La funzione media_colonna conterrà un ciclo for che, tenendo fisso l'indice per la colonna pari a col, somma i 4 elementi della colonna e divide la somma per 4, restituendo il risultato. Non dimenticare di inizializzare a 0 la variabile che serve a memorizzare la somma dei valori, altrimenti il calcolo si baserà su un valore iniziale aleatorio.

La funzione media_colonna verrà richiamata da un ciclo for nella funzione main per la stampa della media di ciascuna delle quattro colonne.

La logica interna della funzione media_colonna è simile a quella di media_riga, solo che il ciclo all'interno di media_colonna tiene fissa la colonna e ne somma gli elementi, mentre media_riga tiene fissa la riga.

4) Media di tutti gli elementi

Calcolare la media di tutti gli elementi della matrice.

Stampare il risultato con il formato seguente (nel caso della matrice di esempio):

[MEDIA]
2.625

Suggerimenti

Si definisca una funzione dichiarata come segue:

double media(int mat[NRIG][NCOL]);

Nella funzione media potranno essere utilizzati due cicli for annidati per indicizzare tutti gli elementi e sommarne il valore ad una opportuna variable somma. Il valore di somma andrà diviso per 16 per il calcolo della media.

Non dimenticare di inizializzare la variabile somma a zero, altrimenti il calcolo della media si baserà su un valore iniziale aleatorio.

5) Frequenza dei valori

Stampi a video le frequenze di occorrenza dei valori usando il formato:

[FREQUENZE]
0
0
0
0
0
2
2
3
4
3
2

Nella prima riga si riporti le frequenze dei -5, nella seconda le frequenze dei -4, ecc.

Suggerimenti

La soluzione può essere realizzata con una funzione come la seguente:

void frequenze(int mat[NRIG][NCOL], int freq[]);

La funzione frequenze riceve due parametri: la matrice mat contenente i numeri di cui calcolare la frequenza, e il vettore delle frequenze. Un esempio di calcolo delle frequenze è riportato in (S6.8).

Tutti i parametri devono essere dichiarati nel main che chiama la funzione frequenze.

Fai attenzione all'inizializzazione dei valori del vettore che conterrà le frequenze, in quanto:

  • l'inizializzazione va fatta nel main;
  • come tutte le variabili, un vettore non inizializzato contiene elementi i cui valori sono aleatori;
  • l'inizializzazione dei vettori è illustrata in S5.9.2.

La stampa delle frequenze avvenga con un ciclo for nel main, dopo aver stampato il marcatore.

La logica per il calcolo delle frequenze è la stessa suggerita nel tutorato relativo ai vettori. La differenza è che ora si dovranno svolgere due cicli per esaminare tutte le righe e le colonne, invece che un solo ciclo che esamina gli elementi del vettore.

6) Rotazione della matrice

Si ruoti la matrice di 90 gradi in senso orario.

Data la matrice d'esempio:

0 1 2 3
1 3 2 4
2 3 4 5
3 5 4 0

la matrice ruotata deve essere:

3 2 1 0
5 3 3 1
4 4 2 2
0 5 4 3

e il programma deve stampare:

[RUOTA]
3 2 1 0 
5 3 3 1 
4 4 2 2 
0 5 4 3

Suggerimenti

Si crei una funzione come la seguente:

void ruota(int output[NRIG][NCOL], const int input[NRIG][NCOL]);

La funzione conterrà gli usuali due cicli for annidati che, per ciascun valore della matrice input lo assegnano all'elemento corretto della matrice output. Si ricordi che i due argomenti sono degli array, pertanto il passaggio dei parametri avviene per riferimento. Pertanto, modificando nella funzione ruota il valore di un elemento di output, ne risulterà modificato il valore nella variabile passata come argomento. Non c'è rischio di modificare i valori della matrice input in quanto l'argomento è dichiarato const.

NOTA: per la rotazione, il segreto è gestire correttamente gli indici delle matrici!

Se non riesci a trovare la soluzione.... scopri un piccolo aiuto »

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è matrici.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f matrici.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Le matrici - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano dichiarate le seguenti macro:

#define MIN    (-5)
#define MAX    (5)

#define NCOL    (4)
#define NRIG    (4)

Le funzioni descritte di seguito dovranno essere oppotunamente richiamate nel main. Ad esse dovranno essere passati gli argomenti corretti. In particolare, una matrice di dimensione NRIG x NCOL può essere dichiarata come segue:

int mat[NRIG][NCOL];

Lettura della matrice

La funzione che stampa una matrice può essere come la seguente:

void leggi_matrice(int mat[NRIG][NCOL])
{
    int i, j;
    char linea[1024];

    for (i = 0; i < NRIG; i++) {
        for (j = 0; j < NCOL; j++) {
            do {
                fgets(linea, sizeof(linea), stdin);
                mat[i][j] = atoi(linea);
            } while(mat[i][j] > MAX || mat[i][j] < MIN);
        }
    }
}

Il funzionamento del programma è il seguente:

  • il ciclo for più esterno usa il contatore i per esaminare tutte le righe della matrice mat
  • per ciascuna riga, il ciclo for più interno esamina i valori di ciascuna colonna usando il contatore j
  • per ciascun elemento, il ciclo do-while continua a ripetere la lettura di un numero fintanto che questo non è compreso nell'intervallo [MIN, MAX]
    • per la lettura si usa la combinazione di funzioni fgets e atoi (vedi S4.1)
    • si noti l'uso dell'operatore || (OR logico, vedi S11.5.2) che permette di verificare che mat[i][j] non sia ne' < MIN ne' > MAX
  • al termine della funzione, il contenuto di mat viene restituito alla funzione chiamante in quanto la matrice è stata passata per riferimento

Stampa della matrice

La funzione che stampa una matrice può essere come la seguente:

void stampa_matrice(int mat[NRIG][NCOL])
{
    int i, j;
    for (i = 0; i < NRIG; i++) {
        for (j = 0; j < NCOL; j++) {
            printf("%d ", mat[i][j]);
        }
        putchar('\n');
    }
}

Il ciclo for più esterno usa il contatore i per esaminare tutte le righe della matrice mat. Per ciascuna riga, il ciclo for più interno esamina i valori di ciascuna colonna usando il contatore j. Viene stampato ad ogni ciclo l'elemento mat[i][j] seguito da uno spazio per separare gli elementi su ciascuna riga. Alla fine di ogni riga, viene stampata una andata a capo con la putchar, che stampa un carattere '\n' sul terminale.

Media per riga

Per il calcolo della media di ciascuna riga si può usare la seguente funzione:

double media_riga(int mat[NRIG][NCOL], int riga)
{
    int i, somma = 0;
    for (i = 0; i < NCOL; i++) {
        somma += mat[riga][i];
    }
    return ((double) somma)/NCOL;
}

La funzione quale calcola la somma degli elementi sulla riga specificata da riga, e restituisce tale somma divisa per il umero di colonne NCOL.

NOTA: in corrispondenza della return avviene il cast (vedi S5.13.3), ovvero la conversione di tipo esplicita, da int a double di somma. Questo cast è indispensabile in quanto altrimenti verrebbe troncata la parte decimale del risultato). In alternativa si sarebbe potuta dichiarare la variabile somma di tipo double per evitare il cast.

Media per colonna

Per il calcolo della media di ciascuna colonna si può usare la seguente funzione:

double media_colonna(int mat[NRIG][NCOL], int col)
{
    int i, somma = 0;
    for (i = 0; i < NRIG; i++) {
        somma += mat[i][col];
    }
    return ((double) somma)/NRIG;
}

La funzione calcola la somma degli elementi sulla colonna specificata da col, e restituisce tale somma divisa per il umero di colonne NRIG.

NOTA: in corrispondenza della return avviene il cast (vedi S5.13.3), ovvero la conversione di tipo esplicita, da int a double di somma. Questo cast è indispensabile in quanto altrimenti verrebbe troncata la parte decimale del risultato). In alternativa si sarebbe potuta dichiarare la variabile somma di tipo double per evitare il cast.

Media di tutti gli elementi

Per il calcolo della media di tutti gli elementi si può usare la seguente funzione:

double media(int mat[NRIG][NCOL])
{
    int i, j, somma = 0;
    for (i = 0; i < NRIG; i++) {
        for (j = 0; j < NCOL; j++)
            somma += mat[i][j];
    }
    return ((double) somma)/(NRIG * NCOL);
}

il cui funzionamento è il seguente:

  • il ciclo for più esterno usa il contatore i per esaminare tutte le righe della matrice mat
  • per ciascuna riga, il ciclo for più interno esamina i valori di ciascuna colonna usando il contatore j
  • ad ogni ciclo l'elemento mat[i][j] viene sommato a somma
  • in corrispondenza della return il valore della media viene calcolato dividendo somma per il numero totale di elementi, ovvero NRIG * NCOL

NOTA: in corrispondenza della return avviene il cast (vedi S5.13.3), ovvero la conversione di tipo esplicita, da int a double di somma. Questo cast è indispensabile in quanto altrimenti verrebbe troncata la parte decimale del risultato). In alternativa si sarebbe potuta dichiarare la variabile somma di tipo double per evitare il cast.

Frequenze

Per il calcolo della frequenze degli elementi si può usare la seguente funzione:

void frequenze(const int mat[NRIG][NCOL], int freq[])
{
    int i, j;
    for (i = 0; i < NRIG; i++) {
        for (j = 0; j < NCOL; j++) {
            freq[mat[i][j] - MIN]++;
        }
    }
}

Il funzionamento è il seguente:

  • il ciclo for più esterno usa il contatore i per esaminare tutte le righe della matrice mat
  • per ciascuna riga, il ciclo for più interno esamina i valori di ciascuna colonna usando il contatore j
  • ad ogni ciclo l'elemento mat[i][j] viene usato come indice nel vettore freq per conteggiare il numero di occorrenze di tale valoer (la sua frequenza)
  • il dato restituito dalla funzione è il contenuto del vettore freq, il quale è stato passato per riferimento

ATTENZIONE: il valore di MIN deve essere dichiarato come il minimo valore possibile per i numeri nella matrice. Per esempio in questo modo (come mostrato all'inizio del tutorial):

#define MIN   (-5)

Rotazione della matrice

Per il calcolo della frequenze degli elementi si può usare la seguente funzione:

void ruota(int output[NRIG][NCOL], const int input[NRIG][NCOL])
{
    int i, j;
    for (i = 0; i < NRIG; i++) {
        for (j = 0; j < NCOL; j++) {
            output[j][NRIG - i - 1] = input[i][j];
        }
    }
}

Il funzionamento è il seguente:

  • il ciclo for più esterno usa il contatore i per esaminare tutte le righe della matrice input
  • per ciascuna riga, il ciclo for più interno esamina i valori di ciascuna colonna usando il contatore j
  • ad ogni ciclo l'elemento input[i][j] viene assegnato all'elemento output[j][NRIG - i - 1]
  • il dato restituito dalla funzione è il contenuto della matrice output, la quale è stata passata per riferimento

NOTA: per la stampa della matrice usata per passare l'argomento output alla funzione, si può usare la funzione stampa_matrice definita più sopra.

Hands-on

prova-tu Il calcolo della media per riga (o per colonna) e quello della media totale hanno molto in comune: per calcolare la media della riga i-esima bisogna sommare i numeri sulla riga, prima di dividere per il numero di elementi, mentre la media totale può essere vista come la somma delle somme di tutte le righe, che poi viene divisa per il numero totale di elementi della matrice.

Prova quindi a ristrutturare il programma creando una funzione che si limita a calcolare la somma dei valori su una riga (invece che direttamente la media). La funzione che restituisce la somma della riga il cui indice è riga può essere dichiarata come segue:

double somma_riga(int mat[NRIG][NCOL], int riga);

Questa stessa funzione può essere chiamata nel main per calcolare la somma delle righe, e ottenere la media dopo aver diviso (sempre nel main) per il numero di elementi.

Può essere anche chiamata per calcolare la somma di tutte le righe, da dividere poi per il numero totale di elementi della matrice per calcolare la media totale.

In questo modo, si realizza una unica funzione per la soluzione di due quesiti.

Le struct

Le strutture dati, o struct, permettono di memorizzare tipi di dati eterogenei in un unico "contenitore". I campi delle strutture possono essere acceduti attraverso il loro nome.

Le struct

Realizzare un programma che acquisisca da tastiera un numero intero senza segno n. Se l'utente inserisce un numero maggiore di 10 si assuma n = 10. Successivamente il programma deve acquisire una lista di coordinate di n punti nel piano; le coordinate di ogni punto, che sono dei numeri in virgola mobile, devono essere inserite sulla stessa riga per es.:

1.23 4.5678

Suggerimenti generali

Conviene dichiarare una struttura dati, denominata per esempio struct punto_t, che contenga i due campi x e y di tipo double (in S7.2 c'è un esempio di dichiarazione con campi di tipo int). È possibile utilizzare anche la typedef spiegata in S7.3, ma in questo tutorial non verrà utilizzata. Dichiara nel main un vettore di MAXN strutture di tipo struct punto_t, per es.

struct punto_t vett[MAXN];

MAXN sia una macro dichiarata con #define e posta uguale al massimo numero di punti da memorizzare (10). Dopodiché va letto il valore di di n.

Per la lettura dei singoli punti può essere creata una funzione come la seguente:

struct punto_t leggi_punto(void);

La funzione utilizzerà la fgets per leggere dal file una riga di testo alla volta, e per ciascuna riga letta verrà chiamata la sscanf per estrarre dalla riga i due numeri corrispondenti alle coordinate del punto (vedi S7.2.4 per un esempio di uso della sscanf in abbinamento alle struct). Dichiarerà una variabile di tipo struct punto_t i cui campi saranno assegnati con la sscanf; tale variabile verrà restituita con la return.

1) Stampa dei punti

Stampare a video (uno per riga) i punti acquisiti, utilizzando il seguente formato:

[PUNTI]
(1.000, 2.000)
(3.000, 4.000)
(5.000, 6.000)

Suggerimenti

Visto che nel corso del programma servirà stampare varie volte i punti, conviene definire una funzione come

void stampa_punto(struct punto_t p);

La funzione stamperà le coordinate di un singolo punto p passato come parametro usando il formato richiesto, ovvero:

  • due numeri double, tra parentesi tonde e separati da virgola
    • lo spazio dopo la virgola è importante, non dimenticarlo!
  • ciascun numero deve avere 3 cifre dopo la virgola
  • ricorda che per stampare un singolo numero con il formato richiesto si può usare lo specificatore %.3lf.

La funzione stampa_punto verrà chiamata in un ciclo for all'interno del main per stampare tutti i punti memorizzati nel vettore vett dichiarato precedentemente.

2) Distanza dall'origine

Calcolare la distanza dall'origine (il punto (0, 0)) di ogni punto acquisito. Stampare le distanze in ordine corrispondente a quello dei rispettivi punti. Utilizzare il seguente formato:

[DISTANZE]
2.236
5.000
7.810

Suggerimenti

Crea innanzitutto una funzione chiamata distanza, come la seguente:

double distanza(struct punto_t p1, struct punto_t p2);

La funzinoe calcola la distanza tra due punti generici p1 e p2 (una funzione simile è mostrata in S7.2).

La distanza d tra i punti p_1 = (x_1,y_1) e p_2 = (x_2, y_2) è data dalla formula (attenzione che non è codice C):

d = sqrt((x_2-x_1)^2 + (y_2-y_1)^2)

dove sqrt è la funzione che calcola la radice quadrata, mentre ^ indica un elevamento a potenza.

Per poter usare la funzione per il calcolo della radice quadrata in C, bisogna:

  • includere math.h (con la direttiva #include)
  • compilare il programma aggiungendo l'opzione -lm (elle minuscola + emme minuscola) alla linea di comando (su alcuni sistemi non è necessario), ovvero, supponendo che il programma si chiami struct.c, l'istruzione per la compilazione è:
cc -Wall struct.c -lm

Per risolvere il problema, la funzione distanza verrà chiamata per ciasun elemento del vettore vett con un ciclo for nel main. L'istruzione che calcola la distanza d da stampare sarà simile alla seguente:

d = distanza(origine, vett[i]);

dove origine è una struttura che contiene le coordinate dell'origine e può essere dichiarata come segue:

struct punto_t origine = {0, 0};

3) Punti interni

Si richieda in input i dati di un rettangolo avente i lati paralleli agli assi cartesiani. Dire quali sono i punti contenuti nel rettangolo. Il rettangolo parallelo agli assi sia definito da una coppia di punti A e B.

rettangolo

Per acquisire il rettangolo si acquisiscano quindi i 2 punti A e B come ad esempio:

0.0 5.0
5.0 -1.0

Il programma dovrà cercare quali punti, tra quelli acquisiti in precedenza, siano interni al rettangolo. Si considerino contenuti anche i punti che si trovino sul bordo del rettangolo. Si stampino i punti interni usando il formato:

[INTERNI]
(1.000, 2.000)
(3.000, 4.000)

Suggerimenti

  • si leggano i due punti A e B utilizzando la funzione leggi_punto già implementata
  • può essere utile creare una struct rettangolo_t i cui due campi siano di tipo struct punto_t (un esempio simile è riportato nella Sezione di approfondimento 7, nel capitolo delle strutture)
  • si può allora definire una funzione come
int interno(struct punto_t p, struct rettangolo_t r);

che, dato un punto p e un rettangolo r restituisce 1 se il punto è interno al rettangolo, e 0 altrimenti.

Questa funzione può essere chiamata in un ciclo for nel main passando di volta in volta tutti i punti nel vettore di punti, e stampando il punto con la funzione stampa_punto se le coordinate sono interne.

4) Area del rettangolo

Calcolare l'area del rettangolo acquisito al punto precedente. Stampare il valore dell'area col seguente formato:

[AREA]
30.000

Suggerimenti

Creare una funzione tipo:

double area_rettangolo(struct rettangolo_t r);

la quale, dato un rettangolo r, ne restituisce l'area.

NOTA: la lunghezza di base e altezza del rettangolo, essendo i suoi lati paralleli agli assi, si può calcolare ripettivamente come la differenza delle coordinate x e y dei punti estremi (in valore assoluto).

5) Coppia dei punti più lontani

Trovi la coppia di punti acquisiti più lontani tra loro. Si stampino i due punti con il seguente formato:

[COPPIA]
(5.000, 6.000)
(1.000, 2.000)

Suggerimenti

  • bisogna confrontare ciascun punto nel vettore con tutti gli altri
    • per questo sono sufficienti due cicli for annidati: quello più esterno con i che va da 0 a n-1, mentre quello più interno con j che va da i+1 a n
    • una variabile max memorizzerà il valore massimo corrente
  • la distanza tra due punti può essere calcolata con la funzione distanza implementata per risolvere il punto precedente corrispondente
  • quando la distanza tra i due punti correnti supera max, gli indici dei punti nel vettore che danno luogo alla distanza corrente sono memorizzati in due variabili max_i e max_j

Questo procedimento può essere implementato in una funzione come la seguente:

void indici_max_dist(struct punto_t vett[], int len, int *max_i, int *max_j);

Essa restituisce gli indici max_i e max_j nel vettore vett di lunghezza len che corrispondono ai punti che si trovano alla maggior distanza tra loro.

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è struct.test.

Il comando da eseguire per il test è il seguente:

./pvcheck -f struct.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Le struct - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltanto le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano effettuate le seguenti dichiarazioni:

#define MAXN   (10)

struct punto_t {
    double x; 
    double y; 
};

struct rettangolo_t {
    struct punto_t A;   // alto-sinistra
    struct punto_t B;   // basso-destra
};

Le funzioni descritte di seguito dovranno essere oppotunamente richiamate nel main. Ad esse dovranno essere passati i corretti parametri.

Lettura della matrice

La funzione per la lettura di un punto può essere come segue:

struct punto_t leggi_punto(void)
{
    char linea[1024];
    struct punto_t p;

    fgets(linea, sizeof(linea), stdin);
    sscanf(linea, "%lf %lf", &p.x, &p.y);
    return p;
}
  • utilizza la fgets e la sscanf per leggere una riga di testo ed estrarre i due numeri corrispondenti alle coordinate del punto (vedi S7.2.4 per un esempio di uso della sscanf in abbinamento alle struct)
  • nella sscanf viene usato lo specificatore %lf per estrarre due numeri in virgola mobile dalla stringa letta con la fgets
  • i valori estratti vengono memorizzati nelle variabili p.x e p.y che sono passate alla sscanf per riferimento (si noti l'uso dell'operatore &, vedi S7.2.4)

Stampa di un punto

La funzione per stampare un singolo punto è la seguente:

void stampa_punto(struct punto_t p)
{
    printf("(%.3lf, %.3lf)\n", p.x, p.y);
}
  • la printf stampa le coordinate del punto p passatole come parametro, col giusto formato
  • in particolare, con %.3lf vengono stampate 3 cifre dopo la virgola (S2.12)

Distanza dall'origine

La funzione per calcolare la distanza tra due generici punti è la seguente:

double distanza(struct punto_t p1, struct punto_t p2)
{
    return sqrt((p1.x-p2.x)*(p1.x-p2.x) + (p1.y-p2.y)*(p1.y-p2.y));
}

I punti p1 e p2 di cui calcolare la distanza sono passati come argomento.

Nel calcolo viene usata la funzione sqrt della libreria matematica:

  • bisogna includere math.h per poter usare la funzione
  • bisogna compilare con -lm per evitare errori di compilazione

Punti interni

La funzione che permette di stabilire se un punto è interno o meno ad un rettangolo è la seguente:

int interno(struct punto_t p, struct rettangolo_t r)
{
    if (p.x >= r.A.x && p.x <= r.B.x && p.y >= r.B.y && p.y <= r.A.y)
        return 1;
    else
        return 0;
}
  • gli argomenti sono il punto p e il rettangolo r
  • si verifica the la x di p sia compresa tra la x di A e quella di B (A e B sono i campi di r)
    • stessa cosa per la y
  • tutte e quattro le condizioni devono essere vere contemporaneamente, da cui l'uso dell'operatore && (AND logico, vedi S11.5.1)
  • attenzione all'uso di <= e >=, in quanto un punto è considerato interno anche se giace sul perimetro del rettangolo

Area del rettangolo

La funzione per il calcolo dell'area del rettangolo è la seguente:

double area_rettangolo(struct rettangolo_t r)
{
    double b, h;
    b = r.B.x - r.A.x;
    h = r.A.y - r.B.y;
    return b*h;
}
  • viene calcolata la lunghezza della base b come differenza tra le coordinate x dei punti estremi
  • la stessa cosa avviene con la differenza tra le coordinate y per l'altezza h
  • lo stesso risultato si sarebbe ottenuto anche senza l'uso delle variabili temporanee b e h

Coppia dei punti più lontani

La funzione per il calcolo degli indici dei punti più distanti può essere come la seguente:

void indici_max_dist(struct punto_t vett[], int len, int *max_i, int *max_j)
{
    double max, dist;
    int i, j;

    *max_i = 0;
    *max_j = 0;
    max = distanza(vett[0], vett[1]);
    for (i = 0; i < len - 1; i++) {
        for (j = i + 1; j < len; j++) {
            dist = distanza(vett[i], vett[j]);
            if (dist > max) {
                max = dist;
                *max_i = i;
                *max_j = j;
            }
        }
    }
}
  • gli argomenti sono il vettore vett, la sua lunghezza len, e gli indici dei punti più distanti max_i e max_j
  • questi ultimi sono passati per riferimento, in modo che la funzione possa modificarne il valore (S6.6)
  • la variabile max tiene traccia della massima distanza registrata
  • max viene inizializzata ponendola uguale alla distanza tra i primi due punti nel vettore
  • i due cicli for annidati calcolano la distanza tra ciascun punto i e ogni punto j che lo segue nel vettore
  • se la distanza è maggiore di max il nuovo massimo viene memorizzato, così come gli indici dei punti corrispondenti
  • da notare i limiti dei contatori nei cicli for:
    • il contatore i parte da 0 e arriva a len-1 (escluso)
    • il contatore j parte da i+1 fino ad arrivare a len (escluso)

NOTA: l'uso della variabile temporanea dist è degno di nota; quel pezzo di codice si sarebbe potuto anche scrivere così:

    if (distanza(vett[i], vett[j]) > max) {
        max = distanza(vett[i], vett[j]);
        *max_i = i;
        *max_j = j;
    }

Così facendo, però, nel caso in cui la condizione dell'if risultasse vera, la funzione distanza verrebbe chiamata due volte per svolgere lo stesso calcolo, cosa che allungherebbe il tempo di esecuzione del programma. Usando la variabile dist, il calcolo viene fatto una volta sola e il risultato viene usato due volte (quando serve).

DOMANDA: che succederebbe se entrambi i contatori i e j partissero da 0 e arrivassero a len (escluso)?

Scopri la risposta »

I file

I file sono uno dei metodi per effettuare l'input/output di dati nei programmi. Il C, come tutti i linguaggi di programmazione, mette a disposizione funzioni dedicate alla lettura dei dati da file e alla scrittura su file.

Questo tutorial richiede la lettura di dati da file di testo.

La lettura da file

Vengono proposti diversi esercizi, di limitata complessità, per mostrare i vari casi di organizzazione di un file da leggere e i relativi approcci per la lettura.

Possiamo distinguere due casi:

  1. i dati possono essere letti riga per riga ed elaborati senza necessità di caricare in memoria tutti i dati dal file;
  2. i dati devono essere necessariamente tutti caricati in memoria per procedere con l'elaborazione.

Nel secondo caso, il modo più semplice di procedere è quello di caricare i dati in un vettore opportunamente dichiarato. Si possono distinguere i seguenti sotto-casi:

  • il file è composto da un numero fisso e noto (o calcolabile a-priori) di righe; in questo caso il vettore può essere allocato staticamente;
  • il numero di righe è riportato, in qualche forma, nel file da leggere; il vettore può essere quindi allocato dinamicamente una volta per tutte prima di iniziare la lettura;
  • il numero di righe non è noto; il vettore utilizzato per caricare i dati in memoria deve essere dimensionato dinamicamente durante la lettura.

Questo tutorial considererà il caso (1) e il primo sotto-caso del caso (2). Il secondo e il terzo sotto-caso del caso (2) verranno trattati nel tutorial dedicato all'allocazione dinamica.

Per semplicità, si assumerà che tutte le righe del file abbiano la medesima struttura, ovvero conterranno dati organizzati nello stesso modo.

AVVERTENZA

Anche se i dati da leggere da file nei diversi tutorial possono sembrare identici, vi sono delle piccole differenze sull'organizzazione del contenuto del file. Queste differenze sono importanti per l'impostazione della soluzione del problema.

Si raccomanda pertanto di leggere attentamente il testo di ciascun tutorial.

Stazione di monitoraggio 1

..:: Versione con numero di righe ininfluente (lettura-elaborazione) ::..

Un dispositivo di rilevazione misura diversi parametri ambientali per mezzo di sensori. I dati misurati vengono memorizzati in un file insieme all'istante temporale nel quale sono rilevati (detto, in gergo, timestamp).

Ciascuna riga del file ha il seguente formato:

AAAA-MM-GG hh:mm:ss.ms ID TEMP UMID VEL

Il significato dei diveri elementi è riportato nella seguente tabella.

Valore Significato Tipo Intervallo/dimensione
AAAA anno intero
MM mese intero [1..12]
GG giorno intero [1..31]
hh ore intero [0..23]
mm minuti intero [0..59]
ss secondi intero [0..59]
ms millisecondi intero [0..999]
ID identificativo del dispositivo stringa max 10 caratteri
TEMP temperatura [gradi] float
UMID umidità [%] intero [0..100]
VEL velocità del vento [m/s] float >= 0

Un esempio di contenuto del file è il seguente:

2019-03-03 23:59:10.120 R101 17.5 20% 0.4

2019-03-02 07:41:59.001 X023 16.9 22% 0.2
2019-03-01 12:10:00.000 X023 22.5 21% 0.3
...

Scrivere un programma che legga il file nel formato illustrato e stampi a video i valori richiesti nei seguenti quesiti.

ATTENZIONE: nel file possono essere presenti delle righe vuote, ovvero che non contengono alcun dato.

Suggerimenti generali

Come si vedrà, per risolvere i quesiti proposti non è necessario caricare in memoria tutti i dati presenti nel file, per esempio in un vettore, per poi elaborarlo. È sufficiente invece leggere una riga alla volta, elaborare i dati letti della singola riga, e passare alla riga successiva.

1) Misure notturne

Il programma deve stampare a video le righe corrispondenti a misurazioni svolte in orario notturno. L'orario notturno va dalle ore 22:00 alle ore 06:00 (escluso).

Le righe vengano stampate nello stesso formato presente nel file stesso, come nell'esempio seguente:

[NOTTURNO]
2019-03-03 23:59:10.120 R101 17.5 20% 0.4
...

Suggerimenti generali

  • realizzare un ciclo di lettura utilizzando fgets e sscanf (vedi S8.6)
  • ad ogni riga letta, verificare se corrisponde ad un orario notturno e, nel caso, effettuare la stampa

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura con fopen (S8.2.1).

Funzione di lettura

La parte principale dell'elaborazione viene fatta durante la lettura del file. Quando viene letta una riga, questa viene elaborata per calcolare i risultati richiesti.

Si può creare una funzione di lettura dichiarata come la segue:

void leggi_file(FILE * fin);

La funzione richiede un argomento:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_file (S8.2.1)

Nella funzione leggi_file:

  • il file può essere letto con un ciclo attraverso fgets (S8.6)
  • da ogni riga, si estraggono i dati tramite sscanf;
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)

Nel main:

  • bisogna chiamare la funzione leggi_file

2) Massima temperatura

Determinare la temperatura massima tra tutte quelle misurate, indicata con max_temp, e stamparne a video il valore con il seguente formato:

[MAX-TEMP]
max_temp

Suggerimenti generali

Per l'individuazione del valore massimo di temperatura basta effettuare i necessari controlli mentre viene letto il file.

È possibile modificare la funzione di lettura implementata al punto precedente per tenere traccia del valore massimo letto, e stamparne il valore al termine della lettura.

3) Numero di righe lette

....

[RIGHE]
n

Verifica manuale

La verifica del funzionamento del programma può essere fatta manualmente, utilizzando il file di input file1.txt come esempio, richiamando l'eseguibile come segue:

./a.out file1.txt

Verifica automatica

Si può utilizzare il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è misure1.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f misure1.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Stazione di monitoraggio 1 - Soluzione

Il tipo di richieste formulate nel problema suggerisce la possibilità di effettuare l'elaborazione delle righe durante la lettura del file. In particolare, non serve caricare in memoria tutti i dati per farne l'elaborazione. Pertanto verranno lette le righe nel file una ad una, e ciascuna riga letta verrà subito elaborata per svolgere i calcoli necessari.

In pratica, l'intera elaborazione - o quasi - del file viene svolta nella funzione di lettura elabora_file seguente:

void elabora_file(FILE *f)
{
    char buf[1000];
    int n;                 // numero di righe lette
    int nconv;             // numero di elementi convertiti
    float max_temp;        // temperatura massima
    int aa, MM, gg;        // data
    int hh, mm, ss, ms;    // orario
    char id[11];           // identificativo del dispositivo
    float temp;            // temperatura
    int umid;              // umidità
    float vel;             // velocità del vento

    puts("[NOTTURNO]");
    n = 0;
    max_temp = -300.0;
    /* ogni ciclo legge una riga dal file */
    while (fgets(buf, sizeof(buf), f)) {
        /* conversione dei dati contenuti nella riga letta */
        nconv = sscanf(buf, "%d-%d-%d %d:%d:%d.%d %s %f %d%% %f",
            &aa, &MM, &gg,
            &hh, &mm, &ss, &ms,
            id, &temp, &umid, &vel);

        /* i dati completi sono 11;
         * se sono di meno è una riga vuota
         */
        if (nconv != 11) continue;
        /* tiene traccia della temperatura massima */
        if (temp > max_temp) max_temp = temp;
        /* stampa la riga corrente se in fascia notturna */
        if (notturno(hh)) printf("%s", buf);
        /* incrementa il numero di righe lette correttamente */
        n++;
    }
    /* stampa i risultati */
    printf("\n[MAX-TEMP]\n%.1f\n", max_temp);
    printf("\n[RIGHE]\n%d\n", n);
}

La funzione è ben commentata, e dovrebbe essere semplice seguirne la logica.

La funzione richiede un solo argomento: il file da leggere f, già aperto in lettura tramite fopen prima di chiamare elabora_file (S8.2.1).

Nella funzione elabora_file:

  • il file viene letto con un ciclo attraverso fgets (S8.6);
  • da ogni riga, si estraggono i dati utili tramite sscanf, e si memorizzano in variabili locali alla funzione;
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)
  • viene chiamata la funzione notturno, la quale restituisce 1 oppure 0 (cioè vero oppure falso) se l'orario indicato dal parametro h corrisponde ad un orario notturno o meno; in tal caso, per semplicità , viene stampata direttamente la stringa caricata da file.

La funzione notturno può essere implementata come segue:

int notturno(int h)
{
    if ((h >= 22) || (h <= 5)) return 1;
    return 0;
}

Infine, un esempio di main che utilizza le varie funzioni è il seguente:

int main(int argc, const char *argv[])
{
    FILE *infile;

    /* termina se il numero di parametri è errato */
    if (argc != 2)
        return 1;

    /* apre il file in lettura */
    infile = fopen(argv[1], "r");
    /* termina in caso di errore */
    if (infile == NULL) {
        fprintf(stderr, "# Errore apertura file\n");
        return 1;
    }
    elabora_file(infile);
    fclose(infile);

    return 0;
}

La parte più interessante è l'uso della funzione fopen per aprire il file (vedi S8.2.1) Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9).

In caso di errore nell'apertura del file, per esempio se il file con il nome specificato non esiste, la fopen restituisce NULL. In tal caso, il programma termina senza proseguire con l'elaborazione, stampando un messaggio di errore tramite la fprintf. Questa funzione, illustrata in S8.7, stampa il messaggio sul file standard stderr, chiamato standard error (vedi S8.2.3). Lo standard error è un file associato al terminale; ovvero, quando viene scritto, il testo viene visualizzato a video. È un file comunemente utilizzato per i messaggi di errore o avvertimento.

Se invece il file viene aperto correttamente, viene chiamata la funzione elabora_file illustrata precedentemente.

Infine, il file viene chiuso con fclose prima di terminare il programma.

Stazione di monitoraggio 2

..:: Versione con numero di righe calcolabile a priori ::..

Un dispositivo di rilevazione misura diversi parametri ambientali per mezzo di sensori. I dati misurati vengono memorizzati in un file insieme all'istante temporale nel quale sono rilevati (detto, in gergo, timestamp).

Ciascuna riga del file ha il seguente formato:

AAAA-MM-GG hh:mm:ss.ms ID TEMP UMID VEL

Il significato dei diveri elementi è riportato nella seguente tabella.

Valore Significato Tipo Intervallo/dimensione
AAAA anno intero
MM mese intero [1..12]
GG giorno intero [1..31]
hh ore intero [0..23]
mm minuti intero [0..59]
ss secondi intero [0..59]
ms millisecondi intero [0..999]
ID identificativo del dispositivo stringa max 10 caratteri
TEMP temperatura [gradi] float
UMID umidità [%] intero [0..100]
VEL velocità del vento [m/s] float >= 0

Un esempio di contenuto del file è il seguente:

2019-03-03 23:59:10.120 R101 17.5 20% 0.4

2019-03-02 07:41:59.001 X023 16.9 22% 0.2
2019-03-01 12:10:00.000 X023 22.5 21% 0.3
...

Scrivere un programma che legga il file nel formato illustrato e stampi a video i valori richiesti nei seguenti quesiti.

IMPORTANTE: per questo tutorial si assuma che il file contenga i dati di un solo dispositivo. Le misurazioni vengono effettuate nell'arco di una settimana esatta, una volta ogni ora.

Suggerimenti generali

Si vedrà che in questo tutorial è richiesto il calcolo di un risultato che richiede di caricare in memoria tutti i dati prima di elaborarli.

Per fare questo, il metodo più comodo è quello di utilizzare un vettore di strutture, e poi elaborare i dati presenti nel vettore. Un esempio di struttura è la seguente:

struct misura {
    /* opportuni campi */
};

Ciascuna struttura conterrà i dati di una riga. Per poter utilizzare un vettore, bisogna dichiararlo, e per farlo bisogna conoscerne la dimensione.

Il testo del problema permette di calcolare la dimensione del vettore. Infatti, se le misurazioni provengono da un dispositivo soltanto, e si effettua una misurazione all'ora per una settimana, il numero di righe presenti nel file sarà pari a:

n = (# dispositivi) * (ore/giorno) * (giorni/settimana)

ovvero

n = 1 * 24 * 7 = 168

1) Stampa invertita

Si stampino le prime 3 righe e le ultime 3 righe del file. Le ultime 3 righe vengano stampate in ordine inverso, partendo dall'ultima riga presente nel file fino alla terz'ultima.

Il formato delle singole righe sia identico a quello del file di input.

[INVERTITA]
riga_prima
riga_seconda
riga_terza
riga_ultima
riga_penultima
riga_terzultima

Se nel file sono complessivamente presenti meno di 6 righe, le si stampi tutte in ordine invertito, ovvero dall'ultima alla prima.

Suggerimenti generali

Dovendo stampare i dati in ordine inverso, conviene prima caricare in memoria tutti i dati. Per fare questo:

  • realizzare un ciclo di lettura utilizzando fgets e sscanf (vedi S8.6)

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura con fopen (S8.2.1).

Funzione di lettura

La parte principale dell'elaborazione viene fatta durante la lettura del file. Quando viene letta una riga, questa viene elaborata per estrarre e memorizzare i dati letti.

Si può creare una funzione di lettura dichiarata come la segue:

void leggi_file(FILE *f, struct misura *elenco, int *n);

La funzione richiede tre argomenti:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_file (S8.2.1)
  • un puntatore all'elenco di stutture struct misura da leggere, all'interno del quale caricare i dati letti
  • un puntatore a intero int per restituire, tramite passaggio del parametro per indirizzo (S6.6), il numero di dati effettivamente letti

Viste le assunzioni fatte sul contenuto del file, il numero di elementi letti è calcolabile a priori. Quindi il numero di dati effettivamente letti potrebbe non servire tra gli argomenti della funzione di lettura. In questo caso si è scelto di prevederne la restituzione del valore per una maggiore generalità della soluzione.

Nella funzione leggi_file:

  • il file può essere letto con un ciclo attraverso fgets (S8.6)
  • da ogni riga, si estraggono i dati tramite sscanf;
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)

Nel main:

  • bisogna chiamare la funzione leggi_file

2) Massima temperatura

Determinare la temperatura massima tra tutte quelle misurate, indicata con max_temp, e stamparne a video il valore con il seguente formato:

[MAX-TEMP]
max_temp

Suggerimenti generali

In questo caso, si assume che i dati siano stati caricati in un vettore come spiegato nel punto (1).

È possibile modificare la funzione di lettura implementata al punto precedente per tenere traccia del valore massimo letto, e stamparne il valore al termine della lettura.

Verifica manuale

La verifica del funzionamento del programma può essere fatta manualmente, utilizzando il file di input file2.txt come esempio, richiamando l'eseguibile come segue:

./a.out file2.txt

Verifica automatica

Si può utilizzare il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è misure2.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f misure2.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

La stazione di monitoraggio 2 - Soluzione

Dovendo stampare i dati in ordine invertito rispetto a quello del file, è necessario caricare in memoria tutti i dati per poi stamparli in senso inverso. Il modo più semplice per farlo è memorizzare un vettore di strutture, nel quale ciascuna struttura memorizza i dati di una riga.

La prima cosa da fare, è quindi di realizzare la struttura che ospiterà i dati delle singole righe. Un esempio è il seguente:

struct misura {
    int aa, MM, gg;             // data
    int hh, mm, ss, ms;         // orario
    char id[MAX_ID_LUNG + 1];   // identificativo del dispositivo
    float temp;                 // temperatura
    int umid;                   // umidità
    float vel;                  // velocità del vento
};

NOTA: il nome dei campi è abbreviato per velocizzare la scrittura del codice. Ciononostante, gli identificatori utilizzati sono sufficientemente esplicativi da permetterne una immediata comprensione.

La formulazione del problema permette di stimare il numero massimo di righe nel file. Questo permette di allocare un vettore staticamente, con una dimensione che permette di ospitare il massimo numero di elementi. Il vettore deve essere dichiarato nel main.

Un esempio di funzione di lettura è il seguente:

void leggi_file(FILE *f, struct misura *elenco, int *n)
{
    char buf[1000];
    int nconv;        // numero di elementi convertiti

    /* inizializza a zero il numero di strutture caricate */
    int i = 0;
    /* ad ogni iterazione legge una nuova riga */
    while (fgets(buf, sizeof(buf), f)) {
        /* punta all'elemento corrente da memorizzare */
        nconv = sscanf(buf, "%d-%d-%d %d:%d:%d.%d %s %f %d%% %f",
                &(elenco[i].aa), &(elenco[i].MM), &(elenco[i].gg),
                &(elenco[i].hh), &(elenco[i].mm), &(elenco[i].ss), &(elenco[i].ms),
                (elenco[i].id), &(elenco[i].temp), &(elenco[i].umid), &(elenco[i].vel));
        /* passa alla riga successiva se non ci sono esattamente 11
         * elementi da convertire */
        if (nconv != 11) continue;
        /* se si arriva qui, la riga è stata letta e memorizzata
         * correttamente; incremento il numero di righe lette */
        i++;
        /* per precauzione controlla che non si leggano più
         * dati di quelli che possono essere ospitati nel vettore;
         * questo non dovrebbe mai succedere se il file è corretto */
        if (i >= MAX_DATI) break;
    }
    *n = i;
}

La funzione è ben commentata, e dovrebbe essere semplice seguirne la logica.

La funzione richiede tre argomenti: il file da leggere f, già aperto in lettura tramite fopen prima di chiamare elabora_file (S8.2.1); il puntatore elenco, che punta al vettore di strutture nel quale verranno memorizzati i dati letti; il puntatore all'intero n che permette alla funzione di restituire il numero di elementi letti tramite passaggio per riferimento.

Nella funzione elabora_file:

  • il file viene letto con un ciclo attraverso fgets (S8.6);
  • da ogni riga, si estraggono i dati utili tramite sscanf, e si memorizzano in variabili locali alla funzione;
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)
  • viene chiamata la funzione notturno, la quale restituisce 1 oppure 0 (cioè vero oppure falso) se l'orario indicato dal parametro h corrisponde ad un orario notturno o meno; in tal caso, per semplicità , viene stampata direttamente la stringa caricata da file.
  • l'ultima istruzione if all'interno del ciclo consente di terminare l'elaborazione se per errore dovessero essere letti più dati di quelli memorizzabili nel vettore.

Per quanto riguarda la condizione del ciclo while, ricorda che la fgets restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso da NULL). Se non ci sono altre righe da leggere, la fgets restituisce NULL. Dal momento che la costante NULL equivale al numero 0, la condizione del ciclo while diventa 0 (cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclo while termina a sua volta.

Una variante della funzione di lettura è la seguente, nella quale si sua l'aritmetica dei puntatori per rendere più concisa la scrittura degli argomenti della sscanf:

void leggi_file(FILE *f, struct misura *elenco, int *n)
{
    char buf[1000];
    int nconv;        // numero di elementi convertiti
    struct misura *m;

    /* inizializza a zero il numero di strutture caricate */
    *n = 0;
    /* ad ogni iterazione legge una nuova riga */
    while (fgets(buf, sizeof(buf), f)) {
        /* punta all'elemento corrente da memorizzare */
        m = elenco + (*n);
            nconv = sscanf(buf, "%d-%d-%d %d:%d:%d.%d %s %f %d%% %f",
                &(m->aa), &(m->MM), &(m->gg),
                &(m->hh), &(m->mm), &(m->ss), &(m->ms),
                (m->id), &(m->temp), &(m->umid), &(m->vel));
        /* passa alla riga successiva se non ci sono esattamente 11
         * elementi da convertire */
        if (nconv != 11) continue;
        /* se si arriva qui, la riga è stata letta e memorizzata
         * correttamente; incremento il numero di righe lette */
        (*n)++;
        /* per precauzione controlla che non si leggano più
         * dati di quelli che possono essere ospitati nel vettore;
         * questo non dovrebbe mai succedere se il file è corretto */
        if ((*n) >= MAX_DATI) break;
    }
}

La differenza consiste nell'assegnare alla variabile m, nella riga appena prima dell'istruzione sscanf, l'indirizzo dell'elemento corrente da leggere. L'indirizzo è calcolato sommando all'indirizzo base elenco il valore puntato da n. In modo da indicare i vari campi, nella sscanf, con m->....

Stampa in senso inverso

La funzione stampa_elenco può essere implementata come segue:

void stampa_elenco(struct misura *elenco, int n)
{
    int i;
    if (n <= 6) {
        /* stampa tutte le righe in ordine inverso */
        for (i = n - 1; i >= 0; i--)
            stampa_riga(elenco + i);
    } else {
        /* stampa le prime tre righe */
        for (i = 0; i < 3; i++)
            stampa_riga(elenco + i);
        /* stampa le ultime tre righe in ordine inverso */
        for (i = n - 1; i >= n - 3; i--)
            stampa_riga(elenco + i);
    }
}

Come si vede, un primo test viene svolto per controllare se ci sono meno di 6 righe. In tal caso vengono stampate tutte con il ciclo for.

Altrimenti, con il primo ciclo for nel ramo else vengono stampate le prime tre righe, mentre il secondo ciclo stampa le ultime tre righe.

Temperatura massima

Per il calcolo della temperatura massima può essere implementata una funzione come la seguente:

float max_temp(struct misura *elenco, int n)
{
    float max_temp;
    int i;

    /* esce in caso non vi siano elementi nel vettore */
    if (n <= 0) return -1000;
    /* il massimo viene inizializzato con il primo valore
     * disponibile */
    max_temp = elenco[0].temp;
    /* una iterazione per ogni struttura successiva alla
     * prima, se ce ne sono */
    for (i = 1; i < n; i++) {
        /* se la temperatura corrente è maggiore del
         * massimo attuale, aggiorna il massimo */
        if ((elenco + i)->temp > max_temp)
            max_temp = (elenco + i)->temp;
    }
    return max_temp;
}

Si faccia riferimento ai commenti nel codice per comprenderne il significato.

Allocazione dinamica della memoria

L'allocazione dinamica permette di riservare la quantità di memoria necessaria a memorizzare i dati nel momento in cui ne si conosce la dimensione.

Molto spesso, la quantità di memoria necessaria diviene nota soltanto al momento dell'esecuzione del programma. È questo il caso che si presenta negli esercizi di questo tutorial, nel quale la quantità di memoria dipende da valori che sono presenti nel file da leggere, del quale non si conosce il numero totale di righe.

Stazione di monitoraggio 3

..:: Versione con numero di righe riportate nel file ::..

Un dispositivo di rilevazione misura diversi parametri ambientali per mezzo di sensori. I dati misurati vengono memorizzati in un file insieme all'istante temporale nel quale sono rilevati (detto, in gergo, timestamp).

Ciascuna riga del file ha il seguente formato:

AAAA-MM-GG hh:mm:ss.ms ID TEMP UMID VEL

Il significato dei diveri elementi è riportato nella seguente tabella.

Valore Significato Tipo Intervallo/dimensione
AAAA anno intero
MM mese intero [1..12]
GG giorno intero [1..31]
hh ore intero [0..23]
mm minuti intero [0..59]
ss secondi intero [0..59]
ms millisecondi intero [0..999]
ID identificativo del dispositivo stringa max 10 caratteri
TEMP temperatura [gradi] float
UMID umidità [%] intero [0..100]
VEL velocità del vento [m/s] float >= 0

Le prime due righe del file contengono rispettivamente i valori numerici interi a e b. Si ha che 0 <= a <= b. Essi delimitano l'intervallo di righe [a,b] da leggere tra quelle presenti successivamente nel file. La prima riga contenente una misura è quella di indice 0 (zero).

Il numero totale di righe n non è noto. Potrebbe accadere che n <= a e/o n <= b.

Un esempio di contenuto del file è il seguente:

1
1000
2019-03-03 23:59:10.120 ID000 17.5 20% 0.4
2019-03-02 07:41:59.001 ID001 16.9 22% 0.2
2019-03-01 12:10:00.000 ID002 22.5 21% 0.3

Nel caso in esempio, vanno considerate soltanto la seconda e la terza riga contenente i valori misurati.

Scrivere un programma che legga il file nel formato illustrato e stampi a video i valori richiesti nei seguenti quesiti.

Suggerimenti generali

1) Identificativi

Il programma deve stampare a video gli indentificativi contenuti nelle righe nell'intervallo [a,b] in ordine invertito rispetto al loro ordine nel file. La stampa avvenga utilizzando il formato come nell'esempio seguente:

[IDENTIFICATIVI]
ID002
ID001

Suggerimenti generali

Dovendo stampare i dati in ordine inverso rispetto al loro ordine nel file, i dati vanno prima caricati in memoria per poterli poi stampare.

In questo caso, il numero massimo di righe da leggere viene indicato esplicitamente nel file. Si può quindi allocare dinamincamente tramite malloc un vettore che possa ospitare tale quantità di dati.

  • realizzare un ciclo di lettura utilizzando fgets e sscanf (vedi S8.6)
  • ad ogni riga letta, verificare se corrisponde ad un orario notturno e, nel caso, effettuare la stampa

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura con fopen (S8.2.1).

Funzione di lettura

La parte principale dell'elaborazione viene fatta durante la lettura del file. Quando viene letta una riga, questa viene elaborata per memorizzare i punteggi di piloti e scuderie.

Si può creare una funzione di lettura dichiarata come la segue:

struct misura *leggi_file(FILE *f, int *n);

La funzione restituisce un puntatore a struttura di tipo struct misusa, che conterrà l'indirizzo del primo elemento del vettore di strutture allocato tramite malloc all''interno della funzione e usato per ospitare i dati letti.

Gli argomenti della funzione, invece, sono quattro:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_file (S8.2.1)

Nella funzione leggi_file:

  • vanno lette le prime due righe per estrarre il valore di a e b
  • per fare questo, si può usare la funzione fgets (S8.6) senza la necessità di un ciclo while
  • da ogni riga, si estraggono i dati tramite sscanf;
  • il vettore deve essere allocato con malloc per una dimensione adeguata
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)

Nel main:

  • bisogna chiamare la funzione leggi_file

2) Massima temperatura

Determinare la temperatura massima tra tutte quelle misurate, indicata con max_temp, e stamparne a video il valore con il seguente formato:

[MAX-TEMP]
max_temp

Suggerimenti generali

Per l'individuazione del valore massimo di temperatura basta effettuare i necessari controlli mentre viene letto il file.

È possibile modificare la funzione di lettura implementata al punto precedente per tenere traccia del valore massimo letto, e stamparne il valore al termine della lettura.

3) Numero di righe lette

....

[RIGHE]
n

Verifica manuale

La verifica del funzionamento del programma può essere fatta manualmente, utilizzando il file di input file3.txt come esempio, richiamando l'eseguibile come segue:

./a.out file3.txt

Verifica automatica

Si può utilizzare il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è misure3.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f misure3.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Stazione di monitoraggio 3 - Soluzione

La prima cosa da fare è realizzare la struttura che ospiterà i dati delle singole righe. Un esempio è il seguente:

struct misura {
    int aa, MM, gg;             // data
    int hh, mm, ss, ms;         // orario
    char id[MAX_ID_LUNG + 1];   // identificativo del dispositivo
    float temp;                 // temperatura
    int umid;                   // umidità
    float vel;                  // velocità del vento
};

NOTA: il nome dei campi è abbreviato per velocizzare la scrittura del codice. Ciononostante, gli identificatori utilizzati sono sufficientemente esplicativi da permetterne una immediata comprensione.

La parte più interessante di questo problema è la funzione che si occupa della lettura del file. Un esempio di funzione di lettura è la seguente:

struct misura *leggi_file(FILE *f, int *n)
{
    char buf[1000];
    int nconv;        // numero di elementi convertiti
    struct misura *m, *elenco;
    int size, i;
    int a, b;

    /* inizializza a zero il numero di strutture caricate */
    *n = 0;

    /* legge e converte i limiti presenti ad inizio file */
    fgets(buf, sizeof(buf), f);
    sscanf(buf, "%d", &a);
    fgets(buf, sizeof(buf), f);
    sscanf(buf, "%d", &b);
    /* verifica che i limiti siano validi */
    if ((b < a) || (a < 0) || (b < 0)) return NULL;

    /* calcola e alloca la dimensione massima dell'array */
    size = (b - a + 1);
    elenco = malloc(size * sizeof(*elenco));

    /* legge e scarta le prime "a" righe */
    for (i = 0; i < a; i++)
        fgets(buf, sizeof(buf), f);

    /* ad ogni iterazione, legge una riga fino alla fine del file */
    while (fgets(buf, sizeof(buf), f)) {
        /* punta all'elemento corrente da memorizzare */
        m = elenco + (*n);
        nconv = sscanf(buf, "%d-%d-%d %d:%d:%d.%d %s %f %d%% %f",
            &(m->aa), &(m->MM), &(m->gg),
            &(m->hh), &(m->mm), &(m->ss), &(m->ms),
            (m->id), &(m->temp), &(m->umid), &(m->vel));
        /* passa alla riga successiva se non ci sono esattamente 11
         * elementi da convertire */
        if (nconv != 11) continue;
        /* se si arriva qui, la riga è stata letta e memorizzata
         * correttamente; incremento il numero di righe lette */
        (*n)++;
        /* per precauzione controlla che non si leggano più
         * dati di quelli che possono essere ospitati nel vettore;
         * questo non dovrebbe mai succedere se il file è corretto */
        if ((*n) >= size) break;
    }
    /* riduce la dimensione del vettore se sono stati letti meno
     * di "size" righe */
    if ((*n < size))
        elenco = realloc(elenco, (*n) * sizeof(*elenco));
    /* restituisce il vettore con i dati letti */
    return elenco;
}

Non c'è da spaventarsi della lunghezza, in quanto buona parte del codice è costituito da commenti. I commenti dovrebbero essere sufficienti per capire la logica del programma.

La funzione restituisce un puntatore al vettore allocato all'interno della funzione.

I parametri sono due:

  • il file f, già aperto in lettura prima di chiamare la leggi_file;
  • il puntatore all'intero n che serve a restituire il numero di righe lette tramite il passaggio di parametri per indirizzo.

La parte più significativa e istruttiva è l'uso di fgets e sscanf come istruzioni singole, all'interno di un ciclo for, e nel classico ciclo while.

Per quanto riguarda la condizione del ciclo while, ricorda che la fgets restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso da NULL). Se non ci sono altre righe da leggere, la fgets restituisce NULL. Dal momento che la costante NULL equivale al numero 0, la condizione del ciclo while diventa 0 (cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclo while termina a sua volta.

Si ricordi che devono essere lette dal file le righe nell'intervallo [a,b] a partire dalla prima riga contenente le misurazioni, e che le tali righe sono numerate a partire da 0.

Le istruzioni singole vengono usate per leggere le prime due righe del file, e assegnare il valore alle variabili a e b. Dopodiché si verifica che i valori siano coerenti. In caso non lo siano, la funzione termina restituendo NULL.

Alla variabile size viene assegnata la dimensione del vettore, calcolata come b - a + 1, in quanto gli estremi dell'intervallo [a,b] sono inclusi. In altri termini, se l'intervallo è - ad esempio - [3,3], il vettore deve contenere un singolo valore. La malloc viene poi utilizzata per l'allocazione del vettore.

Il primo ciclo for si "mangia" (ovvero salta) le prime a righe: le legge con la fgets ma non viene fatto nulla con il contenuto delle righe.

il ciclo while, invece, legge una riga per ogni iterazione, effettua la conversione del contenuto dei valori presenti nella riga, e li memorizza nell'elemento del vettore di indice *n.

Alla fine del ciclo while viene ridimensionata la lunghezza del vettore tramite la realloc, in modo che il vettore sia della esatta dimensione pari al numero di righe lette dal file.

Stampa invertita degli identificativi

La funzione che stampa gli identificativi in senso inverso è molto semplice e, ad esempio, può essere implementata come segue:

void stampa_identificativi(struct misura *elenco, int n)
{
    int i;

    for (i = n - 1; i >= 0; i--)
        printf("%s\n", (elenco + i)->id);
}

Essa riceve come argomenti il vettore delle strutture caricate da file e il numero di elementi nel vettore.

Effettua un ciclo for con contatore che parte dall'ultimo elemento del vettore (indice n - 1) e decrementa il contatore fino ad indirizzare l'elemento di indice 0 del vettore.

Hands-on

prova-tu Si consideri una variante del problema proposto. Il file di input contenga, prima delle righe con le misurazioni, un solo numero, che indica il numero totale di misurazioni presenti nel file. Un esempio di file con questo formato è il seguente:

3
2019-03-03 23:59:10.120 ID000 17.5 20% 0.4
2019-03-02 07:41:59.001 ID001 16.9 22% 0.2
2019-03-01 12:10:00.000 ID002 22.5 21% 0.3

Modifica la soluzione realizzata per questo tutorial in modo da leggere tutte le righe e stampare gli identificativi in ordine invertito. Utilizza lo stesso formato adottato in questo tutorial.

Puoi utilizzare pvcheck per testare il corretto funzionamento del programma. Il file contenente i test è misure-handson.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f misure-handson.test ./a.out

Stazione di monitoraggio 4

..:: Versione con numero di righe arbitrario ::..

Un dispositivo di rilevazione misura diversi parametri ambientali per mezzo di sensori. I dati misurati vengono memorizzati in un file insieme all'istante temporale nel quale sono rilevati (detto, in gergo, timestamp).

Ciascuna riga del file ha il seguente formato:

AAAA-MM-GG hh:mm:ss.ms ID TEMP UMID VEL

Il significato dei diveri elementi è riportato nella seguente tabella.

Valore Significato Tipo Intervallo/dimensione
AAAA anno intero
MM mese intero [1..12]
GG giorno intero [1..31]
hh ore intero [0..23]
mm minuti intero [0..59]
ss secondi intero [0..59]
ms millisecondi intero [0..999]
ID identificativo del dispositivo stringa max 10 caratteri
TEMP temperatura [gradi] float
UMID umidità [%] intero [0..100]
VEL velocità del vento [m/s] float >= 0

Un esempio di contenuto del file è il seguente:

2019-03-03 23:59:10.120 ID000 17.5 20% 0.4
2019-03-02 07:41:59.001 ID001 16.9 22% 0.2
2019-03-01 12:10:00.000 ID002 22.5 21% 0.3

Scrivere un programma che legga il file nel formato illustrato e stampi a video i valori richiesti nei seguenti quesiti.

Suggerimenti generali

In questo esercizio il numero di righe presenti nel file non è noto a priori.

La lettura di tutte le righe può essere fatta allocando con malloc il vettore che ospita i dati, e ri-allocando con realloc il vettore per estenderne la dimensione qualora il numero di righe da leggere crescesse oltre le dimensioni del vettore correntemente allocato.

1) Identificativi

Il programma deve stampare a video tutti gli indentificativi in ordine invertito rispetto al loro ordine nel file. La stampa avvenga utilizzando il formato come nell'esempio seguente:

[IDENTIFICATIVI]
ID002
ID001
ID000

Suggerimenti generali

Dovendo stampare i dati in ordine inverso rispetto al loro ordine nel file, i dati vanno prima caricati in memoria per poterli poi stampare.

In questo caso, non c'è modo di conoscere a priori il numero di righe presenti nel file, ne' è possibile calcolarne il numero massimo. Pertanto il vettore per ospitare i dati letti non può essere allocato prima di iniziare la lettura perché non se ne conosce la dimensione.

Puoi utilizzare il seguente metodo:

  • alloca inizialmente un vettore di dim elementi utilizzando la malloc; dim è una variabile il cui valore iniziale può essere un numero a piacere (per esempio 4);
  • comincia a leggere il file con il solito ciclo while e le funzioni fgets e sscanf (vedi S8.6)
  • all'interno del ciclo while, quando il numero di righe lette e memorizzate è pari a dim, aumenta il valore di dim (per esempio raddoppiandolo) e usa la realloc per aumentare la dimensione del vettore
  • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura con fopen (S8.2.1).

Puoi creare una funzione di lettura dichiarata come la segue:

struct misura *leggi_file(FILE *f, int *n);

La funzione restituisce un puntatore a struttura di tipo struct misura, che contiene l'indirizzo del primo elemento del vettore di strutture allocato tramite malloc all''interno della funzione e usato per ospitare i dati letti.

Gli argomenti della funzione, invece, sono due:

  • f: il file da leggere, già aperto in lettura prima di chiamare leggi_file (S8.2.1)
  • n: l'indirizzo di un intero che viene usato per restituire, tramite passaggio per riferimento, il numero di elementi memorizzati nel vettore.

Per la stampa degli identificativi puoi definire una funzione come la seguente:

void stampa_elenco(struct misura *elenco, int n);

alla quale passare il vettore (parametro elenco) e il numero di elementi in esso contenuti (n).

Nel main:

  • dichiara la variabile puntatore a struct misura per puntare al vettore di strutture;
  • apri il file con fopen
  • chiama la funzione leggi_file ed effettua la lettura
  • passa il vettore alla stampa_elenco per la stampa

Verifica manuale

La verifica del funzionamento del programma può essere fatta manualmente, utilizzando il file di input file4.txt come esempio, richiamando l'eseguibile come segue:

./a.out file4.txt

Verifica automatica

Si può utilizzare il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è misure4.test.

Il comando da eseguire per il test è il seguente:

pvcheck -f misure4.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Stazione di monitoraggio 4 - Soluzione

La prima cosa da fare è realizzare la struttura che ospiterà i dati delle singole righe. Un esempio è il seguente:

struct misura {
    int aa, MM, gg;             // data
    int hh, mm, ss, ms;         // orario
    char id[MAX_ID_LUNG + 1];   // identificativo del dispositivo
    float temp;                 // temperatura
    int umid;                   // umidità
    float vel;                  // velocità del vento
};

NOTA: il nome dei campi è abbreviato per velocizzare la scrittura del codice. Ciononostante, gli identificatori utilizzati sono sufficientemente esplicativi da permetterne una immediata comprensione.

La parte più interessante di questo problema è la funzione che si occupa della lettura del file. Un esempio di funzione di lettura è la seguente:

struct misura *leggi_file(FILE *f, int *n)
{
    struct misura *m, *elenco, *tmp_ptr;
    char buf[1000];
    int nconv;        // numero di elementi convertiti
    int dim;

    /* inizializza a zero il numero di strutture caricate */
    *n = 0;
    /* dimensionamento e allocazione iniziale del vettore */
    dim = 4;
    elenco = malloc(dim * sizeof(*elenco));

    /* ad ogni iterazione, legge una riga fino alla fine del file */
    while (fgets(buf, sizeof(buf), f)) {
        /* punta all'elemento corrente da memorizzare */
        m = elenco + (*n);
        nconv = sscanf(buf, "%d-%d-%d %d:%d:%d.%d %s %f %d%% %f",
            &(m->aa), &(m->MM), &(m->gg),
            &(m->hh), &(m->mm), &(m->ss), &(m->ms),
            (m->id), &(m->temp), &(m->umid), &(m->vel));
        /* passa alla riga successiva se non ci sono esattamente 11
         * elementi da convertire */
        if (nconv != 11) continue;
        /* se si arriva qui, la riga è stata letta e memorizzata
         * correttamente; incremento il numero di righe lette */
        (*n)++;
        /* verifica se il vettore è riempito */
        if ((*n) >= dim) {
            /* raddoppia il valore della variabile */
            dim *= 2;
            /* ri-alloca lo spazio per il vettore */
            tmp_ptr = realloc(elenco, dim * sizeof(*elenco));
            /* controlla che la ri-allocazione abbia avuto successo */
            if (tmp_ptr == NULL) return NULL;
            /* se è tutto a posto, assegna il puntatore */
            elenco = tmp_ptr;
        }
    }
    /* riduce la dimensione del vettore fissandola pari a *n */
    elenco = realloc(elenco, (*n) * sizeof(*elenco));
    /* restituisce il vettore con i dati letti */
    return elenco;
}

Non c'è da spaventarsi della lunghezza, in quanto buona parte del codice è costituito da commenti. I commenti dovrebbero essere sufficienti per capire la logica del programma.

La funzione restituisce un puntatore al vettore allocato all'interno della funzione.

I parametri sono due:

  • il file f, già aperto in lettura prima di chiamare la leggi_file;
  • il puntatore all'intero n che serve a restituire il numero di righe lette tramite il passaggio di parametri per indirizzo.

Il puntatore elenco, all'interno della funzione leggi_file, viene assegnato tramite la malloc, che alloca una quantità di memoria pari a dim elementi di tipo struct misura. Il valore di dim è inizialmente pari a 4.

Il ciclo while legge dal file una riga ad ogni iterazione tramite la fgets.

Per quanto riguarda la condizione del ciclo while, ricorda che la fgets restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso da NULL). Se non ci sono altre righe da leggere, la fgets restituisce NULL. Dal momento che la costante NULL equivale al numero 0, la condizione del ciclo while diventa 0 (cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclo while termina a sua volta.

Con la sscanf vengono estratti i valori dalla riga letta e assegnati ai vari campi della struttura corrente nel vettore.

La parte più interessante è il ridimensionamento del vettore in caso siano stati lette un numero di righe pari a dim. In tal caso il valore di dim viene raddoppiato con l'istruzione dim *= 2 (vedi S11.3.2 per la descrizione delle forme abbreviate di assegnamento). Dopodiché la realloc cambia la dimensione del vettore utilizzando la dimensione raddoppiata, assegnando l'indirizzo ottenuto a tmp_ptr. Se quest'ultimo vale NULL allora l'allocazione non ha avuto successo, e la funzione termina restituendo NULL, altrimenti si assegna tmp_ptr ad elenco e si passa a leggere la riga successiva.

Stampa invertita degli identificativi

La funzione che stampa gli identificativi in senso inverso è molto semplice e, ad esempio, può essere implementata come segue:

void stampa_identificativi(struct misura *elenco, int n)
{
    int i;
    for (i = n - 1; i >= 0; i--)
        printf("%s\n", (elenco + i)->id);
}

Essa riceve come argomenti il vettore delle strutture caricate da file e il numero di elementi nel vettore.

Effettua un ciclo for con contatore che parte dall'ultimo elemento del vettore (indice n - 1) e decrementa il contatore fino ad indirizzare l'elemento di indice 0 del vettore.

Ordinamento

L'ordinamento di un insieme di elementi è uno dei problemi più comuni. Inoltre, molto frequentemente per risolvere un problema più complesso è necessario svolgere un ordinamento.

Per ordinare un insieme di elementi sono sempre necessari tre "ingredienti":

  1. una struttura dati che conservi gli elementi in modo ordinato;
  2. una funzione che permetta di confrontare due elementi, per determinare se sono uguali, oppure quale è il maggiore/minore;
  3. la possibilità di scambiare la posizione di due elementi nella struttura dati utilizzata.

Il problema proposto richiederà, tra le altre cose, di ordinare degli elementi secondo opportuni criteri.

Cerchi

Un file contiene le specifiche di un insieme di cerchi descritti nel piano cartesiano, un cerchio per riga. Ciascun cerchio è specificato da una stringa, che ne definisce il nome (max 127 caratteri senza spazi) e da 3 valori interi (x, y, r), dove (x, y) sono le coordinate del centro (interi con segno) e r è il raggio (intero maggiore di zero). I valori sono separati da spazi.

Per esempio:

C0 -1 -3 9
C1 0 0 10
C2 0 0 10
C3 -100 10 10
C4 -101 10 9
C5 1 1 1

Il nome del file viene passato al programma da linea di comando.

Suggerimenti generali

Definire una struttura dati struct cerchio al fine di rappresentare i dati letti da file.

1) Elenco

Si stampi a video l'elenco dei cerchi nello stesso ordine con il quale sono stati letti da file. Si usi il seguente formato:

[CERCHI]
C0 -1 -3 9
C1 0 0 10
C2 0 0 10
C3 -100 10 10
C4 -101 10 9
C5 1 1 1

Suggerimenti

Definire una funzione carica_elenco come la seguente:

struct cerchio *carica_elenco(FILE *infile, int *n);

Riceve come parametro il nome del file contenente l'elenco dei cerchi e carica un array di strutture struct cerchio, allocato dinamicamente tramite malloc e realloc al suo interno. La funzione deve ritornare al programma chiamante il vettore così allocato e il numero di elementi caricati, quest'ultima cosa può essere fatta con il passaggio per riferimento di n.

2) Ordinamento

Definire la funzione ordina che ordini i cerchi in ordine crescente di area. Si stampi a video l'elenco ordinato usando il formato:

[ORDINAMENTO]
C5 1 1 1
C0 -1 -3 9
C4 -101 10 9
C1 0 0 10
C2 0 0 10
C3 -100 10 10

Suggerimenti

Avendo caricato tutti i dati in memoria, è possibile utilizzare la qsort. L'unica cosa da definire è la funzione che confronta delle strutture struct cerchio sulla base della loro area. Per semplificare i calcoli (e renderli più veloci) si tenga presente che il confronto delle aree equivale al confronto dei raggi. In altre parole, il cerchio con il raggio maggiore è anche quello con l'area maggiore.

Alternativamente, si può implementare la propria funzione di ordinamento, per esempio utilizzando il metodo bubblesort.

3) Relazioni

Individuare le relazioni tra tutte le coppie di cerchi presenti nel file. Considerati due generici cerchi c1 e c2, le relazioni di interesse sono tre:

  • c1 e c2 possono essere coincidenti
  • c1 e c2 possono intersecarsi (si considera che non siano coincidenti)
  • il cerchio c1 contiene il cerchio c2

Stampare a video i nomi delle coppie di cerchi che presentano una delle relazioni indicate, usando il seguente formato:

[RELAZIONI]
C0 INTERSECA C1
C0 INTERSECA C2
C0 CONTIENE C5
C1 INTERSECA C0
C1 COINCIDE C2
C1 CONTIENE C5
C2 INTERSECA C0
C2 COINCIDE C1
C2 CONTIENE C5
C3 INTERSECA C4
C4 INTERSECA C3

Suggerimenti

Definire la funzione relazione che, ricevendo come parametri due strutture di tipo struct cerchio, restituisca un codice che indica le seguenti relazioni tra coppie di cerchi: INTERSECA, CONTIENE, COINCIDE. Il "codice" che identifica la relazione può essere definito tramite delle #define (vedi S3.1 per una spiegazione di queste direttive del preprocessore).

Un esempio è il seguente:

#define NESSUNA    (0)
#define INTERSECA  (1)
#define CONTIENE   (2)
#define COINCIDE   (3)

int relazione(struct cerchio *c1, struct cerchio *c2);

Le relazioni tra i cerchi possono essere determinate semplicemente verificando delle relazioni sulla lunghezza dei raggi e le distanze tra i centri.

Disponendo della funzione relazione si può realizzare un ciclo for che itera su tutti i cerchi, con all'interno un altro ciclo for che confronta il cerchio di indice i con quello di indice j se i != j.

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è cerchi.test. Per eseguire i test è necessario scaricare anche il seguente file di dati che è da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

pvcheck -f cerchi.test ./a.out

Il programma può anche essere verificato manualmente eseguendo:

./a.out cerchi.txt

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Cerchi - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si può dichiarare la seguente struttura:

struct cerchio {
    char *nome;
    int x;
    int y;
    unsigned int r;
};

Da notare che il nome viene gestito con un puntatore a carattere. Potrebbe anche essere dichiarato come char nome[128], modificando opportunamente la funzione di lettura riportata di seguito.

1) Numero di ingressi

struct cerchio* carica_elenco(FILE *infile, int *n)
{
    char buf[1000];
    struct cerchio *v;
    int dim;
    char nome[128];

    dim = 128;
    if (!(v = malloc(dim * sizeof(struct cerchio)))) {
        *n = -1;
        return NULL;
    }

    *n = 0;
    while(fgets(buf, sizeof(buf), infile)) {
        sscanf(buf, "%127s %d %d %d", 
                nome,
                &(v + *n)->x,
                &(v + *n)->y,
                &(v + *n)->r);
        (v + *n)->nome = strdup(nome);
        (*n)++;
        if (*n >= dim) {
            dim *= 2;
            if (!(v = realloc(v, dim * sizeof(struct cerchio)))) {
                *n = -1;
                return NULL;
            }
        }
    }
    v = realloc(v, (*n) * sizeof(struct cerchio));
    return v;
}

La funzione inizialmente usa la malloc (presentata in S9.5) per allocare spazio per dim strutture di tipo struct ingresso. L'indirizzo dell'area di memoria allocata è assegnato al puntatore v.

Si ricordi che malloc alloca una quantità di memoria espressa in byte. Pertanto la sizeof (introdotta in S5.10) viene usata per determinare la dimensione di una singola struttura di tipo struct cerchio. Il valore restituito da sizeof viene moltiplicato per dim per ottenere il numero totale di byte necessari per la memorizzazione del vettore di strutture.

Se v vale NULL allora non è stato possibile allocare correttamente la memoria e la funzione ritorna (termina), restituendo NULL per indicare al chiamante il fallimento della lettura.

Se l'allocazione iniziale della memora ha successo, il ciclo while usa la fgets per leggere una riga alla volta dal file infile. In ciascuna iterazione del ciclo, la funzione sscanf viene usata per estrarre i dati dalla riga letta e per memorizzarli in una struttura memorizzata a sua volta nell'area appena allocata. L'area di memoria viene acceduta tramite il puntatore v come fosse un vettore (vedi equivalenza tra puntatori e vettori illustrata in S9.2).

Da notare che il nome del cerchio viene caricato temporaneamente nel vettore nome dichiarato nella carica_elenco, e il valore della stringa viene assegnato al campo nome della struttura corrente con l'istruzione:

  (v + *n)->nome = strdup(nome);

che usa la strdup per duplicare la stringa contenuta in nome (la funzione strdup è spiegata in S9.5) e assegnare il puntatore risultante al campo nome dell'elemento di indice *n del vettore v.

Se, durante la lettura, il numero di elementi caricati nel vettore v eccede la dimensione del vettore dim, il valore di dim viene raddoppiato e la memoria allocata per v viene ridimensionata con la realloc. Il ridimensionamento della memoria allocata non tocca il contenuto della memoria precedentemente utilizzata, cosicché quanto è stato memorizzato fino a questo punto rimane tale, e si ha spazio per memorizzare ulteriori dati. Questo metodo viene illustrato in S9.5.1.

Alla fine del ciclo, il valore di *n conterrà il numero di cerchi letti dal file. Si ricordi che n è un puntatore, che viene usato con il passaggio per riferimento al fine di permette alla funzione carica_elenco di "restituire" il valore opportunamente modificato per riflettere il numero righe lette. Il passaggio di parametri per riferimento è spiegata in S6.6. Ricorda che, se n è un puntatore, il valore intero da esso puntato viene acceduto con la notazione *n (anteponendo l'asterisco - vedi S5.8.1).

Infine, il vettore v viene ridimensionato con realloc per contenere esattamente un numero di strutture pari a *n, e viene restituito l'indirizzo della memoria allocata e contenente i dati letti nella funzione.

Questa funzione può essere chiamata nel main. Il codice interessante è come segue:

    FILE *f;
    struct cerchio *cerchi;
    int n;

    /* ... apertura del file ... */

    cerchi = carica_elenco(f, &n);
    fclose(f);

    /* ... verifica della corretta lettura ... */

    /* ... rimanente codice nel main ... */

Per la stampa si puoi usare una funzione come la seguente:

void stampa_cerchi(struct cerchio *v, int n)
{
    int i;
    for (i = 0; i < n; i++) {
        printf("%s %d %d %d\n", (v+i)->nome, (v+i)->x, (v+i)->y, (v+i)->r);
    }
}

2) Ordinamento

Per ordinare il file si può implementare una funzione ordina come la seguente:

void ordina(struct cerchio *v, int size)
{
    int i, j;
    struct cerchio temp;

    for (i = 0; i < size; i++) {
        for (j = i + 1; j < size; j++) {
            /*
             * confronto le aree
             * N.B. non serve fare il quadrato, ma basta confrontare i raggi
             */
            if (v[i].r > v[j].r) {
                temp = v[i];
                v[i] = v[j];
                v[j] = temp;
            }
        }
    }
}

Alternativamente, dal momento che i dati sono memorizzati in un vettore, è possibile utilizzare la funzione di libreria qsort nel modo seguente:

    qsort(cerchi, n, sizeof(*cerchi), cmp_cerchi);

dopo aver definito la funzione cmp_cerchi di confronto tra strutture, per esempio come segue:

int cmp_cerchi(const void *p1, const void *p2)
{
    const struct cerchio *c1 = p1, *c2 = p2;

    if (c1->r < c2->r) return -1;
    if (c1->r > c2->r) return 1;
    return 0;
}

Ricorda che la funzione di confronto deve restituire un valore negativo se il primo parametro è minore del secondo, un valore maggiore di zero se il primo è maggiore del secondo, oppure zero se sono uguali. In questo caso vengono confrontati i raggi dei due cerchi.

Attenzione che i parametri passati devono essere dei puntatori a void. Maggiori chiarimenti circa la funzione qsort sono disponibili in S13.3.

3) Relazioni

Le funzioni utili per il calcolo e la stampa delle relazioni sono le seguenti:

#define NESSUNA    (0)
#define INTERSECA  (1)
#define CONTIENE   (2)
#define COINCIDE   (3)

char *nome_relazione[] = { "", "INTERSECA", "CONTIENE", "COINCIDE" };
double dist(int x1, int y1, int x2, int y2)
{
    return sqrt((x1 - x2) * (x1 - x2) + (y1 - y2) * (y1 - y2));
}

La funzione relazione restituisce un valore intero che codifica la relazione tra i due cerchi passati come parametro:

int relazione(struct cerchio *c1, struct cerchio *c2)
{
    int rel = NESSUNA;
    int sd;
    
    /* sd e` la distanza tra i centri */
    sd = sqrt(SQR(c1->x - c2->x) + SQR(c1->y - c2->y));
    /* se la distanza è minore della somma dei raggi c'è "interazione" tra i due cerchi */
    if ( sd <= (c1->r + c2->r) ) {
        /* se centro e raggio sono identici, i cerchi coincidono */
        if ( c1->x == c2->x && c1->y == c2->y && c1->r == c2->r ){
            rel = COINCIDE;
        /* se la differenza tra i raggi è minore di sd c'è intersezione */
        } else if ( sd >= abs(c1->r - c2->r) ) {
            rel = INTERSECA;
        /* se un raggio è minore dell'altro il primo contiene il secondo */
        } else if ( c1->r > c2->r) {
            rel = CONTIENE;
        }
    }
    return rel;
}

La funzione è sufficientemente commentata da non richiedere ulteriori spiegazioni.

Nella funzione, SQR è uan macro che calcola il quadrato di un numero. Essa deve essere definita come segue:

#define SQR(x) ((x)*(x))

Per creare una macro come questa si può consultare S3.1.

I codici sono definiti tramite delle #define (vedi S3.1 per una spiegazione di queste direttive del preprocessore). È importante che i codici abbiano valori che partono da 0 in avanti, e non ci sono "buchi" nella sequenza di valori, in quanto in questo modo il valore restituito da relazione può essere usato come indice nel vettore nome_relazione che contiene le stringhe che poi vengono stampate a video nella funzione stampa_relazioni seguente:

void stampa_relazioni(struct cerchio *v, int n)
{
    int i, j, rel;

    for (i = 0; i < n; i++) {
        for (j = 0; j < n; j++) {
            if (i != j) {
                rel = relazione(v+i, v+j);
                if ( rel != NESSUNA ) 
                    printf("%s %s %s\n", (v+i)->nome, nome_relazione[rel], (v+j)->nome);
            }
        }
    }
}

Tennis

Un torneo di tennis virtuale viene giocato attraverso una piattaforma web che permette agli utenti di registrarsi e giocare partite contro avversari scelti casualmente dal sistema o tra utenti che si accordano per giocare una partita. Le partite possono essere giocate in una delle seguenti due modalità:

  1. al meglio dei 3 set, dove vince il giocatore che si aggiudica per primo 2 set su 3, oppure
  2. al meglio dei 5 set, dove vince chi si aggiudica 3 set su 5.

I risultati delle partite vengono registrati in un file di testo avente il seguente formato:

gioc1 gioc2 nset x1-y1 x2-y2 ...

Il significato dei campi di ciascuna riga è il seguente:

  • gioc1 e gioc2 sono i nomi del primo e del secondo giocatore, rispettivamente; sono stringhe di massimo 50 caratteri senza spazi;
  • nset è il numero massimo di set da giocare (3 o 5), espresso come numero intero;
  • x1-y1, il punteggio del primo set;
  • x2-y2, il punteggio del secondo set;
  • seguono i punteggi degli altri set, se presenti.

Esempi di riga:

Nole Roger 5 7-6 6-7 6-4 6-3
Rafa Nole 5 6-7 5-7 7-6 6-1 6-0

Si scriva un programma in linguaggio C che legga il file dei risultati e calcoli le quantità specificate nelle varie richieste. Il nome del file da leggere deve essere fornito al programma come unico argomento sulla linea di comando.

NOTA: per chi non conoscesse sufficientemente le regole del tennis, basti sapere che un set è composto da giochi, o game; il set viene vinto dal giocatore che si aggiudica per primo 6 giochi, oppure 7 giochi se si giunge al punteggio di 5 pari o 6 pari. Per esempio, un set che termina 7-5 significa che il primo giocatore ha vinto 7 giochi, mentre il secondo ne ha vinti 5. Il set è vinto dal primo giocatore.

Suggerimenti per la lettura

Per memorizzare i dati di una partita è possibile usare una struttura dati come la seguente:

struct partita {
    char g1[51], g2[51];  // nomi dei giocatori
    int maxset;           // numero massimo di set da giocare
    int n_set;            // numero di set giocati
    int score[5][2];      // game giocati nei vari set
};

Il significato dei campi è nei commenti. In particolare score[i][j] memorizza il punteggio della partica nel set i (con i compreso in [0,4]) per il giocatore j (con j compreso in {0,1}).

La funzione leggi_file dichiarata come segue può essere usata per la lettura del file:

struct partita *leggi_file(FILE *f, int *count);

Essa restituirà l'indirizzo di un vettore di strutture struct partita, allocato al suo interno tramite malloc e realloc. Il file da leggere è f, già aperto in lettura con fopen prima di chiamare la leggi_file. Il parametro count passato per riferimento conterrà il numero di partite lette, ovvero il numero di elementi nel vettore restituito.

1) Partita con più giochi

Determinare la partita nella quale sono stati giocati più giochi in totale (max_giochi).

Stampare i dati nel seguente formato:

[MAX-GIOCHI]
gioc1 gioc2 max_giochi

Se più partite hanno totalizzato un numero di giochi pari a max_giochi, stampare la prima che compare nel file.

Suggerimenti

È utile definire una funzione come la seguente:

int giochi_in_partita(struct partita *partita);

la quale riceve in ingresso una struttura che memorizza i dati di una partita, il cui indirizzo è partita, e restituisce il numero di games giocati nella partita. La funzione conterrà un ciclo for che itera da 0 a partita->n_set (il numero di set nella struttura puntata da partita) e somma i punteggi dei due giocatori per ciascun set.

La funzione giochi_in_partita potrà essere usata all'interno di un'altra funzione, dichiarata come segue:

int indice_partita_max_giochi(struct partita *elenco, int n, int *max);

la quale riceve come parametro il vettore elenco di tutte le partite lette da file, e il loro numero n, e restituisce l'indice della prima partita che contiene il numero massimo di giochi. Il numero di giochi per partita viene calcolato usando la giochi_in_partita suggerita precedentemente. La funzione restituisce il valore intero max, che le viene passato per riferimento, contenente il numero massimo di giochi tra tutte le partite.

La funzione indice_partita_max_giochi può essere richiamata a cua volta nel main:

indice = indice_partita_max_giochi(partite, n, &max_giochi);

dove partite è il vettore restituito dalla leggi_file. indice può essere usato per stampare il nome dei giocatori della partita corrispondente.

2) Giochi totali

Determinare il numero totale di game (tot_giochi) giocati tra tutte le partite memorizzate nel file.

Si stampi a video tale valore nel seguente formato:

[TOT-GIOCHI]
tot_giochi

Suggerimenti

Si può creare una funzione come la seguente:

int tot_giochi(struct partita *elenco, int n);

la quale, dato il vettore elenco di partite lette da file, e il suo numero n, restituisce il numero totale di games giocati. Per realizzarla basta scrivere un ciclo for che itera su tutte le n partite (ricorda che n è il secondo parametro) e, al suo interno, un ciclo for che itera su tutti i set di ciascuna partita (campo n_set nella partita), e aggiunge il punteggio di tutti i set in tutte le partite ad una variabile locale alla funzione, che conterrà il conteggio totale.

ATTENZIONE: ricorda di inizializzare a zero il valore della variabile che viene usata per il conteggio.

3) Media set

Determinare il numero medio di set giocati in tutte le partite giocate al meglio dei 3 set (media3). Effettuare lo stesso calcolo per le partite giocate al meglio dei 5 set (media5).

Si stampi a video i valori calcolati nel seguente formato:

[MEDIA]
media3
media5

Se non è possibile calcolare la media per una tipologia di partite, stampare il testo NULL.

Suggerimenti

Si può creare una funzione con il seguente prototipo:

double media(struct partita *elenco, int n, int n_set);

Essa riceve l'elenco elenco delle n partite da considerare (n è il secondo parametro). Inoltre, n_set indica il numero di set delle partite da considerare nel calcolo della media. La funzione conterrà un ciclo for che itera sulle n partite, e sommerà il numero di set giocati (campo n_set nella struttura struct partita) solo se tale numero è uguale al valore del parametro n_set.

Oltre al conteggio della somma, bisogna contare il numero di set considerati nella sommatoria, in modo da calcolare la media.

Il calcolo della media può non essere possibile se il numero di set di interesse è pari a zero, ovvero sarebbe zero il denominatore nel calcolo della media. In tal caso la funzione può restituire un valore minore di zero, per "comunicare" al codice chiamante che il valore della media non è da considerarsi valido.

La funzione media può essere richiamata dal main due volte, per calcolare il numero medio di set in partite com massimo 3 e 5 set come segue:

media3 = media(partite, n, 3);
media5 = media(partite, n, 5);

4) Numero di tie break

Determinare il numero totale di set che è terminato al tie-break (n_tiebreak). Un set termina al tie break se viene vinto col punteggio di 7-6 (oppure 6-7).

Si stampi a video il valore nel seguente formato:

[TIE]
n_tiebreak

Suggerimenti

Si può creare una funzione con il seguente prototipo:

int tie(struct partita *elenco, int n);

Essa riceve l'elenco elenco delle n partite da considerare (n è il secondo parametro). La funzione conterrà un ciclo for che itera sulle n partite, e conteggerà il numero di partite il cui numero totale di games giocati è pari a 13 (7+6).

5) Numero utenti unici

Calcolare il numero n_utenti di utenti unici che hanno giocato almeno una partita elencata nel file. Il conteggio degli utenti unici prevede che se un utente ha giocato più di una partita, esso deve essere conteggiato una volta sola.

Si stampi il risultato col seguente formato:

[UTENTI]
n_utenti

Suggerimenti

Per risolvere questo punto e il successivo conviene dichiarare una nuova struttura dati come segue:

struct utente {
    char nome[51];
    int punteggio;
};

Essa servirà per creare un vettore di queste strutture, ciascuna delle quali si riferisce ad un utente, per il quale si conserverà anche il punteggio utile per rispondere al punto (6).

La funzione seguente servirà per restituire l'elenco degli utenti unici:

struct utente *elenco_utenti_unici(struct partita *elenco, int n, int *count);

Essa riceve come parametri il vettore elenco di n partite lette da file (n è il secondo parametro), e restituisce un vettore di strutture struct utente, allocato tramite malloc e realloc all'interno della funzione stessa. Il parametro count, passato per riferimento, serve a restituire il numero di utenti trovati.

Il valore puntato dal parametro count, al termine dell'esecuzione della funzione, conterrà il risultato da stampare per risolvere questo punto.

NOTA: al suo interno, la funzione elenco_utenti_unici funziona più o meno come la leggi_file, solo che i dati da "leggere" non sono in un file ma all'interno del vettore elenco. Tra l'altro, la realloc per aumentare la dimensione del vettore degli utenti non serve, in quanto si sa già quale è il numero massimo di utenti: esso sarà pari al numero di partite, ovvero n, moltiplicato per due!

La funzione elenco_utenti_unici può essere resa elegante usando una funzione che restituisce l'indice di un utente già eventualmente presente nel vettore, oppure -1 se l'utente non è presente, nel qual caso può essere aggiunto al vettore. Questa funzione potrà essere dichiarata come segue:

int indice_utente(struct utente *elenco, int n, char *nome);

e prende come parametri il vettore elenco degli n utenti attualmente presenti nel vettore, mentre nome è il nome dell'utente che deve essere cercato nel vettore.

La funzione elenco_utenti_unici farà un ciclo su tutte le partite, e per ciascuna partita potrà controllare se l'utente è già presente con delle istruzioni simili alle seguenti:

    struct utente *utenti;
    ...
    presente = indice_utente(utenti, *count, (elenco + i)->g1);
    if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g1);

Le due istruzioni saranno all'interno del ciclo che itera su tutte le partite. Come si vede, le due istruzioni servono per gestire la verifica della presenza del primo giocatore, e vanno ripetute anche per il secondo giocatore. Il valore di *count viene incrementato, e il nome del giocatore viene copiato nel vettore, solo se il valore restituito da indice_utente è negativo, il che significa che il nome non è già presente nel vettore.

6) Classifica

Si determini la classifica calcolata sulla base dei risultati presenti nel file. Un giocatore guadagna un punto per ogni set vinto (attenzione, non per ogni game!), indipendentemente dalla vittoria finale della partita.

Stampare i primi 10 giocatori classificati nel seguente formato:

[CLASSIFICA]
punteggio nome_giocatore
...
punteggio nome_giocatore

Se più giocatori hanno totalizzato il medesimo punteggio finale, li si ordini in ordine alfabetico crescente.

Se il numero totale di giocatori è minore di 10, stampare solo i giocatori presenti.

Suggerimenti

Per risolvere questo punto va utilizzato il vettore degli utenti unici creato come spiegato nel punto (5).

Inoltre, si può dichiarare una funzione come la seguente:

void n_set_vinti(struct partita *partita, int *v1, int *v2);

Essa calcola il numero di set vinti dal primo e dal secondo giocatore nella partita puntata da partita. I due valori sono memorizzati rispettivamente all'indirizzo puntato da v1 e v2.

La funzione n_set_vinti può essere usata all'interno di una funzione che valuta, per tutte le partite, quanti set sono stati vinti dai due giocatori che giocano la partita. La funzione può essere dichiarata come segue:

void calcola_punteggi(struct partita *elenco, int n, struct utente *utenti, int n_utenti);

Essa riceve come parametri il vettore elenco delle n partite giocate, e il vettore utenti degli n_utenti identificati al punto (5).

La funzione calcola_punteggi effettuerà un ciclo per tutte le partite, e per ciascuna chiamerà la funzione n_set_vinti per ottenere il numero di set vinti dai due giocatori. Dopodichè, cercherà i due nomi dei giocatori nel vettore utenti e incrementerà il punteggio dei due giocatori in base al numero di set vinti da ciascuno.

Per determinare la classifica, basta ordinare il vettore utenti al termine dell'esecuzione di calcola_punteggi e stampare i dati delle prime 10 strutture presenti nel vettore ordinato.

Per l'ordinamento del vettore si può usare la qsort, facendo attenzione a ordinare per punteggio, e in caso di parità, ordinando alfabeticamente per nome. Si può usare una funzione di confronto come la seguente:

int cmp_utenti(const void *p1, const void *p2)
{
    const struct utente *u1 = p1;
    const struct utente *u2 = p2;
    if (u1->punteggio < u2->punteggio)
        return 1;
    else if (u1->punteggio > u2->punteggio)
        return -1;
    else {   // stessi punti: ordino per nome
        return strcmp(u1->nome, u2->nome);
    }
}

Si ricordi che la funzione di confronto deve restituire:

  • un risultato positivo se il primo argomento è maggiore del secondo
  • un risultato negativo se il primo argomento è minore del secondo
  • un risultato pari a zero se sono uguali

Due argomenti sono uguali solo se hanno lo stesso punteggio e lo stesso nome.

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma (maggiori informazioni circa l'uso di pvcheck sono disponibili qui).

Il file contenente i test è tennis.test. Per eseguire i test è necessario scaricare anche i seguenti file di dati che sono da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

./pvcheck -f tennis.test ./a.out

Il programma può anche essere verificato manualmente, per esempio utilizzando il primo dei due file di dati, eseguendo:

./a.out test_2gioc_10000par.txt

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Tennis - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si può dichiarare la seguente struttura:

struct partita {
    char g1[51], g2[51];   // nomi dei giocatori
    int maxset;            // numero massimo di set da giocare
    int n_set;             // numero di set giocati
    int score[5][2];       // game giocati nei vari set
};

La lettura del file

La funzione di lettura può essere come la seguente:

struct partita *leggi_file(FILE *f, int *count)
{
    struct partita *v, *partita;
    int conv, dim;
    char buf[1000];

    *count = 0;
    dim = 4;
    if (!(v = malloc(dim * sizeof(*v)))) {
        return NULL;
    }

    while (fgets(buf, sizeof(buf), f)) {
        /* evito di riscriverlo nella sscanf */
        partita = v + (*count);
        conv = sscanf(buf, "%50s %50s %d %d-%d %d-%d %d-%d %d-%d %d-%d",
                partita->g1, partita->g2, &partita->maxset,
                &partita->score[0][0], &partita->score[0][1],
                &partita->score[1][0], &partita->score[1][1],
                &partita->score[2][0], &partita->score[2][1],
                &partita->score[3][0], &partita->score[3][1],
                &partita->score[4][0], &partita->score[4][1]);

        /* Devono essere convertiti almeno 7 valori. */
        if (conv < 7) {
            free(v);
            return NULL;
        }
        partita->n_set = (conv - 3) / 2;

        /*
         * Nel caso in cui il vettore attualmente allocato
         * sia stato completamente riempito, lo rialloca
         * raddoppiando la dimensione.
         */
        if (*count + 1 >= dim) {
            dim *= 2;
            if (!(v = realloc(v, dim * sizeof(*v)))) {
                free(v);
                return NULL;
            }
        }
        (*count)++;
    }
    v = realloc(v, (*count) * sizeof(*v));
    return v;
}

La funzione è sufficientemente commentata da risultare di immediata comprensione.

Da notare due dettagli:

  • l'istruzinoe partita = v + (*count) serve per assegnare ad un puntatore l'indirizzo dell'elemento corrente da riempire, in modo da evitare di ripetere v + (*count) in tutti i punti necessari nella funzione;
  • la sscanf tenta sempre di convertire 5 set; solo successivamente, con l'istruzione partita->n_set = (conv - 3) / 2, si va a impostare il numero effettivo di set giocati in base al numero di elementi convertiti

ATTENZIONE: il numero di set effettivamente giocati può essere inferiore al numero massimo di set da giocare. Infatti, per esempio, una partita che prevede al massimo 5 set può terminare dopo 3 soli set, se questi vengono vinti tutti dallo stesso giocatore. Pertanto è importante calcolare il numero di set effettivamente letti da file.

1) Partita con più giochi

La seguente funzione serve per calcolare il numero di games giocati in una partita, eseguendo un ciclo sul numero di set della partita e sommando i valori del vettore score per ciascun set:

int giochi_in_partita(struct partita *partita)
{
    int i, games = 0;

    for (i = 0; i < partita->n_set; i++) {
        games += partita->score[i][0] + partita->score[i][1];
    }
    return games;
}

La funzione giochi_in_partita viene usata dalla seguente:

int indice_partita_max_giochi(struct partita *elenco, int n, int *max)
{
    int games;
    int i, id = 0;

    *max = giochi_in_partita(elenco);
    for (i = 1; i < n; i++) {
        games = giochi_in_partita(elenco + i);
        if (games > *max) {
            id = i;
            *max = games;
        }
    }
    return id;
}

Questa funzione esamina tutte le partite nel vettore elenco. Calcola per ciascuna il numero di games giocati usando la giochi_in_partita, e conserva l'indice della partita con il massimo numero di games giocati. Questo indice può essere usato nel main per accedere all'elemento del vettore elenco e stampare il nome dei giocatori.

Si noti che la condizione nell'if è games > *max, con il minore stretto. In questo modo, in caso ci siano più elementi del vettore aventi il numero massimo di games, si conserva l'indice del primo elemento che compare nel vettore. Se si fosse messo il maggiore-uguale (games >= *max) si sarebbe mantenuto l'indice dell'ultimo elemento del vettore avente il massimo numero di games, che però non è quanto richiesto nel quesito.

2) Giochi totali

Avendo realizzato la funzione giochi_in_partita per il punto precedente, la si può usare anche per calcolare il numero totale di games giocati in tutte le partite, come segue:

int tot_giochi(struct partita *elenco, int n)
{
    int i, s = 0;

    for (i = 0; i < n; i++) {
        s += giochi_in_partita(elenco + i);
    }
    return s;
}

Ovviamente non è indispensabile aver risposto al punto precedente per poter realizzare una funzione come tot_giochi. Per esempio, la variante che segue non fa uso della funzione giochi_in_partita:

int tot_giochi(struct partita *elenco, int n)
{
    struct partita *partita;
    int i, j, s = 0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        for (j = 0; j < partita->n_set; j++)
            s += partita->score[j][0] + partita->score[j][1];
    }
    return s;
}

Sicuramente, però, il fatto di usare la funzione giochi_in_partita anche per risolvere questo punto rappresenta un ottimo esempio di riutilizzo di funzioni. Inoltre, come si nota, rende il codice della tot_giochi più compatto ed elegante.

3) Media set

La seguente funzione calcola la media delle partite giocate al meglio di n_set set, in quanto la media dei set giocati è richiesto che sia distinta tra partite al meglio dei 3 o dei 5 set.

double media(struct partita *elenco, int n, int n_set)
{
    struct partita *partita;
    int i, count = 0;
    double s = 0.0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        if (partita->maxset == n_set) {
            s += partita->n_set;
            count++;
        }
    }
    if (count > 0)
        return s / count;
    else
        return -1.0;
}

Il ciclo for all'interno della funzione esamina tutte le partite, e somma il numero di set giocati soltanto se maxset è uguale a n_set, ovvero solo se stiamo considerando il tipo di partita desiderato in base al massimo numero di set da giocare.

Questo metodo è il più immediato e auto-contenuto (nel senso che tutto il codice è contenuto nella funzione), ma non è il solo possibile. In alternativa, si potrebbe implementare una funzione che si limita a calcolare il numero di set totali su tutti gli elementi del vettore passato come parametro, evitando quindi il parametro n_set. In questo caso, il codice che chiama la funzione ha la responsabilità di creare un vettore con le sole partite di cui fare la somma (eseguendo un cosiddetto "filtro"), prima di passare questo vettore alla funzione media.

4) Numero di tie break

La seguente funzione calcola il numero di tie-break giocati in tutte le partite:

int tie(struct partita *elenco, int n)
{
    struct partita *partita;
    int i, j, s = 0;

    for (i = 0; i < n; i++) {
        partita = elenco + i;
        for (j = 0; j < partita->n_set; j++)
            if (partita->score[j][0] + partita->score[j][1] == 13)
                s++;
    }
    return s;
}

Viene fatto un ciclo per tutte le partite, che contiene un ciclo per tutti i set di ciascuna partita. Se la somma dei games vinti dai due giocatori nel set è pari a 13 significa che il set è terminato al tie-break, cioè con punteggio pari a 7-6 oppure 6-7. In tal caso viene incrementato il valore di s per conteggiare il tie-break.

5) Numero utenti unici

Per determinare il numero di utenti unici serve una struttura che conservi l'elenco degli utenti che man mano vengono incontrati mentre si esaminano tutte le partite caricate da file. Serve quindi una struttura dati che contenga i dati degli utenti.

Per identificare l'utente basta il nome, ma come vedremo è utile aggiungere anche il punteggio di classifica per usare questa stessa struttura anche per la soluzione del punto (6):

struct utente {
    char nome[51];
    int punteggio;
};

La seguente funzione crea un vettore di struct utente contenente l'elenco dei nomi dei giocatori:

struct utente *elenco_utenti_unici(struct partita *elenco, int n, int *count)
{
    int i, presente;
    struct utente *utenti; // vettore di stringhe da allocare successivamente

    /* per semplicita` viene allocato spazio come se ogni
     * partita fosse giocata da tutti giocatori diversi */
    if (!(utenti = malloc(2 * n * sizeof(*utenti))))
        return NULL;

    *count = 0;
    /* un ciclo per ogni partita giocata */
    for (i = 0; i < n; i++) {
        presente = indice_utente(utenti, *count, (elenco + i)->g1);
        if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g1);
        presente = indice_utente(utenti, *count, (elenco + i)->g2);
        if (presente < 0) strcpy(utenti[(*count)++].nome, (elenco + i)->g2);
    }
    utenti = realloc(utenti, (*count) * sizeof(*utenti));
    return utenti;
}

Per fare questo, prima alloca il vettore utenti per contenere un numero di utenti pari al doppio del numero di partite giocate, in caso tutte le partite siano giocate da giocatori tutti diversi tra loro. Il numero di utenti attualmente contenuti nel vettore è conservato in *count.

Successivamente vengono esaminate tutte le partite con il ciclo for. Per ciascuna partita, viene chiamata la funzione indice_utente, riportata sotto, per determinare se un utente è già presente nel vettore. Se è già presente, la funzione indice_utente restituisce l'indice dell'elemento, altrimenti restituisce -1. Pertanto verrà inserito un nuovo elemento nel vettore utenti solo se l'indice restituito dalla indice_utente è negativo. Ovviamente il controllo della presenza dei giocatori nel vettore utenti deve essere fatta per entrambi i giocatori, per cui nel ciclo for si ripetono le istruzioni per i due giocatori.

Alla fine della funzione, la realloc ridimensiona il vettore in modo da avere esattamente la dimensione necessaria a contenere *count strutture.

La funzione indice_utente non fa altro che svolgere una ricerca sequenziale nel vettore passatole come paramentro, che contiene i nomi trovati. In altri termini, esamina sequenzialmente tutte le strutture per cercarne una che contenga il nome specificato. Viene usata la strcmp per confrontare il nome cercato con quello all'interno del vettore elenco. Se trova tale struttura, ne restituisce l'indice. Se viene terminato il ciclo senza aver trovato il nome cercato, viene restituito -1, che corrisponde ad un indice non valido per un vettore, in modo da indicare la mancata individuazione del giocatore.

int indice_utente(struct utente *elenco, int n, char *nome)
{
    int i;
    for (i = 0; i < n; i++) {
        /* verifica la presenza del giocatore nel vettore */
        if (!strcmp(elenco[i].nome, nome)) return i;
    }
    return -1;
}

6) Classifica

Per calcolare la classifica è fondamentale avere un elenco di giocatori unici che hanno giocato le partite.

Dal momento che la classifica è basata sul numero di set vinti, si può partire da una funzione come la seguente la quale, data una partita (un puntatore ad essa, per la precisione), restituisce il numero di set vinti dai due giocatori.

I due valori vengono restituiti per mezzo del passaggio per indirizzo delle variabili puntate da v1 ev2`:

void n_set_vinti(struct partita *partita, int *v1, int *v2)
{
    int i;

    *v1 = *v2 = 0;
    for (i = 0; i < partita->n_set; i++) {
        if (partita->score[i][0] > partita->score[i][1]) (*v1)++;
        else (*v2)++;
    }
}

La funzione calcola_punteggi calcola la classifica considerando tutte le partite memorizzate nel vettore elenco (di n elementi) e il vettore utenti (di n_utenti elementi) contente la lista dei giocatori univoci trovata nel punto (5).

Per ciascuna partita si determinano i set vinti con n_set_vinti. Con il ciclo for più interno si vanno a individuare i due elementi nel vettore utenti che corridpondono ai due giocatori della partita considerata, confrontando con strcmp il nome del giocatore corrente con quello nel vettore. Quando un giocatore viene trovato, ne si incrementa il punteggio di classifica.

void calcola_punteggi(struct partita *elenco, int n, struct utente *utenti, int n_utenti)
{
    int i, j;
    int p1, p2;

    /* azzera il punteggio di tutti i giocatori */
    for (j = 0; j < n_utenti; j++)
        utenti[j].punteggio = 0;

    for (i = 0; i < n; i++) {
        /* assegna il numero di set vinti da ciascun giocatore */
        n_set_vinti(elenco + i, &p1, &p2);
        for (j = 0; j < n_utenti; j++) {
            if (!strcmp(utenti[j].nome, elenco[i].g1)) utenti[j].punteggio += p1;
            if (!strcmp(utenti[j].nome, elenco[i].g2)) utenti[j].punteggio += p2;
        }
    }
}

Le due funzioni precedenti servono per creare un vettore di giocatori con i rispettivi punteggi di classifica. Per effettuare l'ordinamento si può usare la qsort nel modo seguente:

qsort(utenti, n_utenti, sizeof(*utenti), cmp_utenti);

dove utenti è il vettore di n_utenti elementi, con i rispettivi punteggi di classifica.

La parte fondamentale di questa istruzione è la funzione di confronto cmp_utenti, che può essere formulata come segue:

int cmp_utenti(const void *p1, const void *p2)
{
    const struct utente *u1 = p1;
    const struct utente *u2 = p2;
    if (u1->punteggio < u2->punteggio)
        return 1;
    else if (u1->punteggio > u2->punteggio)
        return -1;
    else {   // stessi punti: ordino per nome
        return strcmp(u1->nome, u2->nome);
    }
}

Viene restituito 1 se il punteggio del primo giocatore è minore del secondo (perché si vuole l'ordinamento in senso decrescente di punteggio), oppure -1 se il primo è maggiore del secondo. In caso i punteggi siano uguali, si restituisce il valore a sua volta restituito dalla strcmp, in quanto si desidera l'ordinamento in senso crescente per nome a parità di punteggio.

Appendice

Questo capitolo contiene vari esercizi "bonus" relativi a lettura dei file e allocazione dinamica della memoria.

Formula 1, Gara singola

Al campionato del mondo di Formula 1 partecipano 10 squadre, formate da 2 piloti ciascuna. L'ordine di arrivo di una gara viene memorizzato in un file di testo col seguente formato:

PILOTA1 SQUADRA1
PILOTA2 SQUADRA2
...

dove i nomi di piloti e squadre sono stringhe senza spazi, e dove ciascuna riga contiene al più 50 caratteri.

La prima riga contiene il nome del vincitore, la seconda quello del secondo arrivato, e così via. Tutti i piloti sono presenti nell'elenco, anche quelli che non sono giunti al traguardo. In ciascuna gara sono assegnati dei punti validi per la classifica mondiale: dal primo al decimo arrivato si assegnano 25, 18, 15, 12, 10, 8, 6, 4, 2, 1 punti; i concorrenti che arrivano oltre la decima posizione non ottengono nessun punto.

1) Classifica della gara

Scrivere un programma C che legga il contenuto del file il cui nome è specificato come parametro della riga di comando e stampi i dati letti nel formato:

[ORDINE_PILOTI]
pilota1 punteggio
pilota2 punteggio
...
pilota20 punteggio

Suggerimenti generali

  • si ricordi che le righe nel file rispecchiano l'ordine di arrivo nella gara!
  • memorizzare il punteggio assegnato per ciascuna posizione di classifica in un vettore, in modo che l'indice del vettore, legato alla posizione in classifica, permetta di recuperare con una istruzione il punteggio del pilota; per esempio:
int punti_per_pos[POS_A_PUNTI] = {
    25, 18, 15, 12, 10, 8, 6, 4, 2, 1
};

dove POS_A_PUNTI è una costante definita con una #define (vedi S3.1) che indica il numero di posizioni che permettono di ottenere dei punti.

Utilizza una struttura dati per memorizzare l'associazione tra pilota e punteggio; per esempio:

struct classificato {
    char nome[MAX_RIGA + 1];
    int punti;
};

dove MAX_RIGA è una costante definita con una #define (vedi S3.1) che indica la dimensione massima della riga di testo.

Sarà necessario (nel main) un vettore di tali strutture per memorizzare i punteggi dei piloti:

struct classificato piloti[N_PILOTI];

dove N_PILOTI è una costante definita con una #define (vedi S3.1) che indica il numero di piloti che gareggiano.

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura con fopen (S8.2.1).

Funzione di lettura

La parte principale dell'elaborazione viene fatta durante la lettura del file. Quando viene letta una riga, questa viene elaborata per memorizzare i punteggi di piloti e scuderie.

Si può creare una funzione di lettura dichiarata come la segue:

void leggi_gara(FILE * fin, struct classificato *piloti);

La funzione richiede due argomenti:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_gara (S8.2.1)
  • il vettore piloti di strutture di tipo struct classificato per memorizzare i punteggi dei piloti, passato per riferimento (S6.6)

Nella funzione leggi_gara:

  • il file può essere letto con un ciclo attraverso fgets (S8.6)
  • da ogni riga, si estrae il nome del pilota tramite sscanf, e si determina il punteggio e si memorizzano questi valori in un elemento del vettore piloti
    • ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s (S8.6!
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (e visto che si tratta di un vettore di char non serve la &)
    • il nome va copiato tramite strcpy nel campo della struttura giusta del vettore piloti

Nel main:

  • bisogna chiamare la funzione leggi_gara
  • va stampato il contenuto del vettore con un ciclo for

Verifica automatica

Si può utilizzare il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma. Il file contenente i test è formula1_1.test. È necessario scaricare anche il file di dati campionato1.txt, da salvare nella medesima directory del file di test.

Il comando da eseguire per il test è il seguente:

pvcheck -f formula1_1.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Formula 1, Gara singola - Soluzione

Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano effettuat le seguenti dichiarazioni:

#define N_SQUADRE (10)
#define N_PILOTI (20)
#define MAX_RIGA (50)

#define POS_A_PUNTI (10)        /* Posizioni a cui si assegnano punti */

int punti_per_pos[POS_A_PUNTI] = {
    25, 18, 15, 12, 10, 8, 6, 4, 2, 1
};

I dati saranno memorizzati in array di strutture. Piloti e squadre richiedono lo stesso tipo di informazioni (nome e punteggio). Pertanto conviene utilizzare lo stesso tipo, in questo modo si eviterà di replicare le funzioni che effettuano le elaborazioni:

struct classificato {
    char nome[MAX_RIGA + 1];
    int punti;
};

Le funzioni descritte di seguito dovranno essere oppotunamente richiamate nel main. Ad esse dovranno essere passati gli argomenti corretti.

1) Lettura e calcolo del punteggio

La funzione per la lettura può essere realizzata come segue:

void leggi_gara(FILE *fin, struct classificato *piloti)
{
    char buffer[MAX_RIGA + 1];
    int pos, punti;
    char nome_pilota[MAX_RIGA + 1];
    char nome_scuderia[MAX_RIGA + 1];

    pos = 0;
    while (pos < N_PILOTI && fgets(buffer, MAX_RIGA + 1, fin) != NULL) {
        sscanf(buffer, "%s %s", nome_pilota, nome_scuderia);

        if (pos < POS_A_PUNTI) {
            punti = punti_per_pos[pos];
        } else {
            punti = 0;
        }
        
        strcpy(piloti[pos].nome, nome_pilota);  /* Memorizza il nome */
        piloti[pos].punti += punti;             /* Assegna i punti */
        pos++;                                  /* Passaggio alla posizione successiva */
    }
}

La funzione richiede due argomenti:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_gara (S8.2.1);
  • il vettore piloti di strutture di tipo struct classificato per memorizzare i punteggi dei piloti, passato per riferimento (S6.6).

Il ciclo while viene eseguito finché si registra il risultato di tutti i piloti attesi (pos < N_PILOTI) e, contemporaneamente, finché ci sono righe nel file. Ricorda che fgets restituisce NULL (quindi 0, quindi "falso") quando si è giunti alla fine del file e non ci sono più righe da leggere (S8.6).

Per scrivere il nome del pilota nel campo della struttura va usata la strcpy, perché l'assegnamento (operatore =) non funziona con le stringhe (S5.11).

ATTENZIONE: l'istruzione che memorizza i punti del pilota è un incremento, quindi questa funzione si aspetta che il valore dei punti sia correttamente inizializzato a zero!

Inizializzazione del vettore di strutture

Visto che la funzione per il calcolo dei punti si attende un vettore correttamente inizializzato, è possibile incapsulare le istruzioni per l'inizializzazione in una funzione come la seguente:

void inizializza_elenco(struct classificato *elenco, int size)
{
    int i;
    for (i = 0; i < size; i++) {
        elenco[i].nome[0] = '\0';
        elenco[i].punti = 0;
    }
}

Il campo stringa nome viene posto uguale alla stringa vuota, mettendo il terminatore come primo carattere del vettore. Il campo punti viene inizializzato a zero.

Formula 1, Campionato

Si adatti il programma che risolve il problema della singola gara al fine di leggere da un file l'ordine d'arrivo di un intero campionato di Formula 1 composto da più gare. Le prime 20 righe sono relative alla prima gara. Seguono 20 righe relative alla seconda gara, e così via.

Il programma dovrà essere in grado di svolgere le seguenti elaborazioni.

Suggerimenti generali

Ricorda che le righe nel file rispecchiano l'ordine di arrivo nella gara! Puoi memorizzare il punteggio assegnato per ciascuna posizione di classifica in un vettore, in modo che l'indice del vettore, legato alla posizione in classifica, permetta di recuperare con una istruzione il punteggio del pilota; per esempio:

int punti_per_pos[POS_A_PUNTI] = {
    25, 18, 15, 12, 10, 8, 6, 4, 2, 1
};

dove POS_A_PUNTI è una costante definita con una #define (vedi S3.1) che indica il numero di posizioni che permettono di ottenere dei punti.

Puoi utilizzare una struttura dati per memorizzare l'associazione tra pilota e punteggio, per esempio:

struct classificato {
    char nome[MAX_RIGA + 1];
    int punti;
};

dove MAX_RIGA è una costante definita con una #define (vedi S3.1) che indica la dimensione massima della riga di testo.

Sarà necessario un vettore di tali strutture per memorizzare i punteggi dei piloti (nel main):

struct classificato piloti[N_PILOTI];

dove N_PILOTI è una costante definita con una #define (vedi S3.1) che indica il numero di piloti che gareggiano.

Il nome del file da leggere sarà l'argomento argv[1] del main (vedi S6.9), mentre per aprire il file in lettura si usa fopen (S8.2.1).

Funzione di lettura

Creare una funzione di lettura dichiarata come la segue:

int leggi_gara(FILE * fin,
        struct classificato *piloti,
        struct classificato *scuderie);

La funzione richiede tre argomenti:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_gara (S8.2.1)
  • il vettore piloti di strutture di tipo struct classificato per memorizzare i punteggi dei piloti, passato per riferimento (S6.6)
  • il vettore scuderie di strutture di tipo struct classificato per memorizzare i punteggi delle squadre, passato per riferimento (S6.6)

La funzione leggi_gara restituirà il numero di piloti caricati per la gara corrente. Quando restituisce 0, significa che è stata raggiunta la fine del file.

Nella funzione leggi_gara:

  • leggere il file con un ciclo attraverso fgets (S8.6)
  • da ogni riga, estrarre il nome del pilota tramite sscanf, determinare il punteggio e memorizzare questi valori in un elemento del vettore piloti
    • si ricorda che per estrarre una stringa con sscanf si usa lo specificatore %s
    • nella sscanf, la variabile stringa nella quale memorizzare il nome estratto va passato per riferimento (se si tratta di un vettore di char non serve la &)
    • il nome va copiato tramite strcpy nel campo della struttura giusta del vettore piloti

Nel main:

  • chiamare la funzione leggi_gara in un ciclo come segue (piloti e scuderie sono vettori opportunamente dichiarati nel main):
    while (leggi_gara(fin, piloti, scuderie) != 0) {
        /* quando leggi_gara ritorna, una gara intera e` stata letta */
        .....
    }
  • elaborare opportunamente il contenuto dei vettori piloti e scuderie

Per poter funzionare, la leggi_gara deve poter cercare un elemento dentro un vettore di strutture struct classificato il cui campo nome corrisponde ad un nome specificato. La funzione di ricerca può essere dichiarata come segue:

struct classificato *cerca_classificato(struct classificato
        *elenco, int size,
        char *nome);

I parametri sono i seguenti:

  • elenco è il vettore di strutture sul quale fare la ricerca
  • size è il numero di strutture presenti nel vettore
  • nome è la stringa che specifica il nome da ricercare

La funzione restituisce l'indirizzo dell'elemento all'interno di elenco che abbia il campo nome uguale alla stringa nome passata come argomento. La funzione deve considerare due casi:

  1. un elemento con il nome desiderato è già presente in elenco, nel qual caso si ritorna l'indirizzo di quell'elemento
  2. un elemento con il nome desiderato non è già presente in elenco (questo si verifica durante la lettura della prima gara nel file); quindi la funzione cerca_classificato dovrà:
  • restituire l'indirizzo del primo elemento del vettore che non contiene alcun nome
  • inizializzare tale elemento inserendo il nome (con strcpy) e inizializzando a 0 il valore dei punti

La funzione cerca_classificato potrà essere chiamata all'interno di leggi_gara sia per cercare il nome di un pilota all'interno del vettore piloti che per cercare il nome di una squadra all'interno del vettore scuderie.

Per capire meglio come operano le funzioni leggi_gara e cerca_classificato si veda l'esempio di soluzione qui.

1) Numero di gare

Determinare il numero di gare presenti nel file, stampandone il valore usando il formato:

[NUMERO_GARE]
2

Suggerimenti

La funzione leggi_gara dovrà leggere al massimo un numero di righe pari al numero di piloti che gareggiano in ciascuna gara, in questo modo la funzione può essere chiamata più volte in un ciclo while nel main per sommare i punteggi delle varie gare.

Si può contare il numero di volte che la funzione leggi_gara viene chiamata per sapere il numero di gare memorizzate nel file.

2) Pilota vincitore

Determinare il pilota vincitore del campionato Il punteggio di un pilota è la somma dei punti ottenuti nelle singole gare. A parità di punteggio vince il pilota che viene prima in ordine alfabetico.

Stampare il nome del pilota usando il seguente formato:

[PILOTA_VINCITORE]
pilota punteggio

3) Squadra vincitrice

Determinare la squadra vincitrice del campionato. Considerando che il punteggio di una squadra consiste nella somma dei punti ottenuti dai suoi piloti, determinare la squadra vincitrice.

Stampare il nome della squadra usando il seguente formato:

[SQUADRA_VINCITRICE]
squadra punteggio

A parità di punteggio vince la squadra che viene prima in ordine alfabetico.

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma. Il file contenente i test è formula1_2.test. Per poter eseguire i test con pvcheck è necessario scaricare anche i seguenti file di dati, da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

pvcheck -f formula1_2.test ./a.out

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Formula 1, Campionato - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano effettuate le seguenti dichiarazioni:

#define N_SQUADRE (10)
#define N_PILOTI (20)
#define MAX_RIGA (50)

#define POS_A_PUNTI (10)        /* Posizioni a cui si assegnano punti */

int punti_per_pos[POS_A_PUNTI] = {
    25, 18, 15, 12, 10, 8, 6, 4, 2, 1
};

I dati saranno memorizzati in array di strutture. Piloti e squadre richiedono lo stesso tipo di informazioni (nome e punteggio). Pertanto conviene utilizzare lo stesso tipo, in questo modo si eviterà di replicare le funzioni che effettuano le elaborazioni:

struct classificato {
    char nome[MAX_RIGA + 1];
    int punti;
};

Le funzioni descritte di seguito dovranno essere oppotunamente richiamate nel main. Ad esse dovranno essere passati gli argomenti corretti.

Lettura del file

La funzione per la lettura e l'elaborazione del file può essere come segue:

int leggi_gara(FILE * fin,
        struct classificato *piloti,
        struct classificato *scuderie)
{
    char buffer[MAX_RIGA + 1];
    int pos, punti;
    char nome_pilota[MAX_RIGA + 1];
    char nome_scuderia[MAX_RIGA + 1];

    pos = 0;
    while (pos < N_PILOTI && fgets(buffer, MAX_RIGA + 1, fin) != NULL) {
        sscanf(buffer, "%s %s", nome_pilota, nome_scuderia);

        if (pos < POS_A_PUNTI)
            punti = punti_per_pos[pos];
        else
            punti = 0;

        /* Cerca il pilota nell'elenco e gli assegna i punti. */
        struct classificato *pilota =
            cerca_classificato(piloti, N_PILOTI, nome_pilota);
        pilota->punti += punti;

        /* Cerca la squadra nell'elenco e gli assegna i punti. */
        struct classificato *scuderia =
            cerca_classificato(scuderie, N_SQUADRE,
                    nome_scuderia);
        scuderia->punti += punti;

        /* Passaggio alla posizione successiva. */
        pos++;
    }
    return pos;
}

La funzione legge al massimo N_PILOTI righe dal file, corrispondenti al numero di piloti che partecipano ad una sola gara, quindi ad ogni chiamata vengono letti i dati di una sola gara. Questo significa che deve essere chiamata più volte per leggere tutti i dati dal file, cosa che essere fatta in questo modo:

    while (leggi_gara(fin, piloti, scuderie) != 0) {
        /* quando leggi_gara ritorna, una gara intera e` stata letta */
        .....
    }

La funzione richiede due argomenti:

  • il file da leggere, già aperto in lettura prima di chiamare leggi_gara (S8.2.1);
  • il vettore piloti di strutture di tipo struct classificato per memorizzare i punteggi dei piloti, passato per riferimento (S6.6).

Inoltre: il ciclo while viene eseguito finché si registra il risultato di tutti i piloti attesi (pos < N_PILOTI) e, contemporaneamente, finché ci sono righe nel file. Ricorda che fgets restituisce NULL (quindi 0, quindi "falso") quando si è giunti alla fine del file e non ci sono più righe da leggere (S8.6).

Per scrivere il nome del pilota nel campo della struttura va usata la strcpy, perché l'assegnamento (operatore =) non funziona con le stringhe (S5.11).

ATTENZIONE: l'istruzione che memorizza i punti del pilota è un incremento, quindi questa funzione si aspetta che il valore dei punti sia correttamente inizializzato a zero!

Ricerca di un elemento nel vettore di piloti e scuderie

La leggi_gara usa la funzione cerca_classificato per farsi restituire l'elemento del vettore del quale aggiornare il punteggio. Questo viene fatto due volte: per il vettore piloti e per il vettore scuderie.

La funzione cerca_classificato è la stessa in entrambi i casi, in quanto opera sempre su vettori di tipo struct classificato, nel quale cerca il generico elemento il cui campo nome sia uguale al nome specificato.

La funzione cerca_classificato può essere formulata come segue:

struct classificato *cerca_classificato(
        struct classificato *elenco,
        int size,
        char *nome)
{
    int i;
    for (i = 0; i < size; i++) {
        if (strcmp(elenco[i].nome, nome) == 0)
            return &elenco[i];    /* Trovato */
        if (elenco[i].nome[0] == '\0') {
            /* Trovato un record vuoto, da inizializzare 
               con il nome prima di restituirlo. */
            strcpy(elenco[i].nome, nome);
            elenco[i].punti = 0;
            return &elenco[i];
        }
    }
    /* Non si dovrebbe mai arrivare qui. */
    return NULL;
}

Alcune delle sue peculiarità:

  • la funzione cerca nel vettore elenco l'elemento il cui campo nome corrisponde alla stringa nome passata come terzo argomento;
  • il vettore elenco ha dimensione pari a size elementi;
  • il ciclo for esamina i vari elementi del vettore elenco;
  • usa la strcmp per confrontare nome con elenco[i].nome, che è il campo nome dell'elemento i-esimo del vettore;
  • se la strcmp restituisce 0 vuol dire che l'elemento è stato trovato, e ne viene restituito l'indirizzo;

Se nel ciclo viene raggiunto un elemento il cui campo nome è la stringa vuota, allora questo elemento viene inizializzato come segue:

  • nel campo nome viene copiata la stringa nome (con strcpy);
  • il campo punti viene inizializzato a zero;
  • viene restituito l'indirizzo di questo elemento.

Il funzionamento di cerca_classificato implica che gli elementi del vettore elenco siano stati tutti inizializzati con una funzione come la seguente:

void inizializza_elenco(struct classificato *elenco, int size)
{
    int i;
    for (i = 0; i < size; i++)
        elenco[i].nome[0] = '\0';
}

1) Numero di gare

Per come è strutturato il programma, per conoscere il numero di gare è sufficiente contare - nel main - il numero di volte che viene chiamata la funzione leggi_gara, per esempio come segue:

    int nGare = 0;
    while (leggi_gara(fin, piloti, scuderie) != 0) {
        /* quando leggi_gara ritorna, una gara intera e` stata letta */
        nGare++;
    }

2) Pilota vincitore

La ricerca del pilota vincitore può essere svolta con una funzione come la seguente:

int cerca_vincitore(struct classificato *elenco, int size) {
    int vincitore = 0, i;
    for (i = 1; i < size; i++) {
        if (elenco[i].punti > elenco[vincitore].punti)
            vincitore = i;
        /*
         * se due piloti hanno lo stesso punteggio vince chi viene prima
         * in ordine alfabetico
         */
        else if (elenco[i].punti == elenco[vincitore].punti &&
             strcmp(elenco[i].nome, elenco[vincitore].nome)< 0)
            vincitore = i;
    }
    return vincitore;
}

La funzione ricerca nel vettore elenco l'indice dell'elemento che ha il valore del campo punti più elevato. L'indice potrà essere usato per recuperare il nome del vincitore nel vettore piloti.

Per gestire il caso in cui due elementi abbiano lo stesso numero di punti, viene usata la funzione strcmp per determinare se un nome viene prima dell'altro, e viene mantenuto l'indice opportuno.

Ricorda che la strcmp restituisce:

  • 0 se le due stringhe sono uguali
  • un valore minore di zero se la prima stringa viene prima in ordine alfabetico rispetto alla seconda
  • un valore maggiore di zero viceversa

La funzione andrà richiamata passandole come primo argomento il vettore piloti riempito dalle chiamate alla funzione leggi_gara illustrata precedentemente. Per esempio:

    pilota_vinc = cerca_vincitore(piloti, N_PILOTI);

Attenzione alla dimensione del vettore, che in questo caso è pari a N_PILOTI.

3) Squadra vincitrice

L'individuazione della scuderia vincente avviene come per il pilota, usando la funzione cerca_vincitore illustrata precedentemente. Verrà richiamata però passandogli il vettore dei punti delle squadre, per esempio come segue:

    scuderia_vinc = cerca_vincitore(scuderie, N_SQUADRE);

Attenzione alla dimensione del vettore, che in questo caso è pari a N_SQUADRE.

Allocazione dinamica della memoria - Ingressi

Un piccolo parco giochi a pagamento di una località balneare gestisce il flusso di clienti fornendo un badge identificativo numerato a ciascun utente. La tariffazione avviene a tempo. A ciascun ingresso vengono pertanto associati l'orario di ingresso e di uscita, oltre che la data.

Tutti i dati vengono memorizzati in un file di testo. Questo è organizzato in modo da memorizzare i dati dei singoli utenti su righe separate. Le righe del file hanno il seguente formato:

gg/mm/aaaa nbadge h1:m1 h2:m2

dove:

  • gg/mm/aaaa è la data d'ingresso; gg è il giorno (il primo giorno del mese è 1), mm è il mese (gennaio è il mese numero 1, dicembre il 12) e aaaa è l'anno;
  • nbadge è il numero identificativo del badge, un intero compreso tra 1 e 1000;
  • h1:m1 e h2:m2 sono rispettivamente gli orari di ingresso e di uscita, nel formato ora:minuto; h1 e h2 rappresentano l'ora, e sono numeri interi compresi nell'intervallo [0, 23]; m1 e m2 rappresentano i minuti, e sono numeri interi nell'intervallo [0, 59].

Un esempio di file di input è il seguente:

01/04/2018 500 20:30 20:50
01/04/2018 510 20:30 21:00
01/05/2018 600 20:30 21:15
01/06/2018 610 20:30 21:30
01/07/2018 610 20:30 21:50

Lo stesso numero di badge può risultare associato a più ingressi differenti, purché gli orari non si sovrappongano nello stesso giorno. Come anticipato, la tariffazione avviene a tempo secondo il seguente tariffario:

  • fino a 20 minuti compresi: 3.5 euro
  • fino a 30 minuti compresi: 4 euro
  • fino a 45 minuti compresi: 4.5 euro
  • fino a 60 minuti compresi: 5.5 euro
  • oltre i 60 minuti: 7 euro (fino alla chiusura del parco)

Il parco apre tutti i giorni alle 19 e chiude alle 24. Il periodo di attività del parco va dal 1 aprile al 30 settembre. Il file memorizza i dati relativi ad un solo anno di gestione del parco.

Suggerimenti generali

Leggere tutto il testo prima di iniziare a risolvere i vari punti, in quanto alcune richieste potrebbero influenzare il modo di strutturare il programma.

In questo caso, per indirizzare il punto 2 è necessario caricare in memoria tutti i dati letti dal file, per poterne stampare i valori in ordine inverso! È la stessa situazione riportata in S5.9.3 dove, per poter stampare la sequenza di Fibonacci in ordine inverso, bisogna prima generarla e memorizzarla in un vettore, per poi stamparne i valori dall'ultimo al primo. Anche nel caso del punto 2 bisogna prima leggere tutti i dati dal file per poterne stampare alcuni valori in ordine inverso.

Per la lettura del file si può implementare una funzione con il seguente prototipo:

struct ingresso *leggi_file(FILE *fp, int *size);

dove:

  • fp è la struttura di tipo puntatore a FILE che identifica il file da leggere; il file deve essere già aperto in lettura prima di chiamare la funzione (usando fopen come in S8.2.1)
  • size è un intero passato per riferimento (vedi S6.6), che al termine dell'esecuzione della funzione conterrà il numero di dati letti
    • la rispettiva variabile deve essere dichiarata nel main, o comunque nella funzione che chiama leggi_file
  • la funzione restituisce un puntatore a struct ingresso
    • la struttura dovrà essere dichiarata per contenere tutti i dati necessari per ciascun ingresso (vedi sotto)
    • l'indirizzo restituito dalla leggi_file sarà quello del primo elemento di un vettore di strutture che viene allocato e riempito all'interno della funzione leggi_file

ATTENZIONE: la memoria necessaria per il vettore restituito dalla leggi_file dovrà essere allocata all'interno della funzione stessa tramite malloc/realloc (vedi S9.5) per due motivi:

  1. non è noto a priori quale è il numero di righe da leggere dal file, quindi non si può sapere da quanti elementi deve essere composto il vettore per contenere tutti i dati da leggere; questa informazione diviene nota quando sono state lette TUTTE le righe del file
  2. nella funzione non si dichiarerà un vettore (che - tra l'altro - potrebbe essere usato solo nella funzione), ma un puntatore a struct ingresso il cui indirizzo verrà inizializzato con la funzione malloc (vedi S9.5)

Un possibile esempio di dichiarazione della struttura struct ingresso può essere la seguente:

struct data {
    int giorno, mese, anno;
};

struct ingresso {
    struct data data;
    int nbadge;
    int permanenza;    // in minuti
    double prezzo;
};

In caso di problemi a formulare la funzione richiesta, si veda la soluzione proposta.

1) Numero di ingressi

Determinare il numero d'ingressi registrati nel file. Stamparne il valore usando il formato:

[INGRESSI]
5

Suggerimenti

Per come è formulata la funzione leggi_file suggerita sopra, il numero di ingressi registrati è esattamente il valore del parametro size che viene "restituito" tramite passaggio per riferimento (vedi S6.6).

2) Stampa in ordine inverso

Stampare gli ultimi 10 badge presenti nel file, in ordine inverso. In altre parole, stampare per primo il badge contenuto nell'ultima riga del file, e via via gli altri fino a stampare per ultimo il badge contenuto nella riga posta a 10 righe dalla fine del file. Se il file contiene meno di 10 righe, stampare tutti e soli i badge presenti, sempre in ordine inverso.

Si usi il seguente formato:

[INVERSIONE]
610
610
600
510
500

Suggerimenti

Una soluzione elegante è quella di definire una funzione come la seguente:

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare);

dove:

  • ingressi è il vettore di size elementi restituito dalla funzione leggi_file
  • num_da_stampare è il numero di badge da stampare a partire dall'ultimo, in ordine inverso

La funzione verrà richiamata con num_da_stampare pari a 10.

Per implementarla, basta realizzare un ciclo che stampi il valore del campo nbadge della struttura struct ingresso suggerita precedentemente, partendo dall'ultima struttura memorizzata nel file, e terminando il ciclo quando sono stati stampati num_da_stampare valori oppure sono stati stampati size valori se size < num_da_stampare.

3) Incasso mensile

Determinare gli incassi effettuati nei mesi di apertura. Stamparne il valore usando il seguente formato:

[INCASSO_MENSILE]
Aprile 0.00
Maggio 12.00
Giugno 5.50
Luglio 7.00
Agosto 0.00
Settembre 0.00

Si stampino i valori in virgola mobile con esattamente 2 cifre dopo la virgola.

Suggerimenti

Per risolvere questo punto bisogna tenere presente che:

  • l'incasso di un mese è la somma degli incassi di tutti gli ingressi in quel mese
  • l'incasso relativo ad un singolo ingresso dipende dalla durata dello stesso
  • la durata non è direttamente letta dal file, nel quale sono invece presenti gli orari di ingresso e di uscita

Pertanto, per calcolare l'incasso di ciascun mese bisogna procedere come segue:

  1. calcolare e memorizzare la durata della permanenza per ciascun ingresso
  2. calcolare la tariffa per ciascun ingresso in funzione della durata della permanenza
  3. scorrere tutti gli ingressi e sommare gli incassi relativi al mese corrispondente

Conviene quindi, innanzitutto, definire una funzione la quale, dati due orari (ore e minuti), calcoli la durata dell'intervallo corrispondente in minuti. Questo serve per determinare la durata dell'ingresso e quindi la fascia di tariffa da applicare. La funzione può essere dichiarata come segue:

int durata(int h1, int m1, int h2, int m2);

che restituisce il numero di minuti intercorsi tra i due orari h1:m1 e h2:m2. Il risultato può essere memorizzato nel campo permanenza all'interno della struttura struct ingresso utilizzata per conservare tutti i dati.

Successivamente, si può definire una funzione che restituisce la tariffa dovuta in funzione della durata della permanenza. Per esempio:

double calcola_tariffa(int tempo);

dove tempo è la durata in minuti della permanenza.

Infine, si può realizzare una funzione per calcolare l'incasso di ciascun mese di interesse. La funzione può essere dichiarata come segue:

void calcola_incassi_mensili(struct ingresso *elenco, int size, double *incassi);

dove:

  • ingressi è il vettore di size elementi restituito dalla funzione leggi_file
  • incassi è un vettore di double che conterrà l'incasso del mese corrispondente.
    • per poter usare il valore del mese come indice in un vettore in modo semplice, conviene usare per gli incassi un vettore di 12 elementi, uno per mese, anche per i mesi dei quali si sa che non c'è incasso (ricorda che il parco giochi è aperto solo un numero limitato di mesi durante l'anno!)

NOTA: per stampare il valore dell'incasso con esattamente 2 cifre decimali si usi lo specificatore %.2lf (vedi S2.12).

4) Incasso totale

Determinare l'incasso totale conseguito nell'anno. Stamparne il valore usando il seguente formato:

[INCASSO_TOTALE]
24.50

Si stampino i valori in virgola mobile con esattamente 2 cifre dopo la virgola.

Suggerimenti

L'incasso totale si può calcolare in due modi equivalenti.

Se sono già stati calcolati gli incassi mensili al punto 3, basta sommare il contenuto del vettore che memorizza tali incassi per ottenere l'incasso totale.

Altrimenti si possono sommare gli incassi dei singoli ingressi, che vanno calcolati come spiegato nella soluzione del punto 3.

5) Mese con più ingressi

Determinre il mese con il maggior numero d'ingressi. Stamparne il nome usando il seguente formato:

[MESE_MAX_INGRESSI]
Maggio

Suggerimenti

Per calcolare il mese con maggiori ingressi conviene calcolare l'istogramma degli ingressi relativi a tutti i mesi dell'anno. L'istogramma degli ingressi non è altro che il conteggio del numero di ingressi registrati per ciascun mese.

Memorizzando l'istogramma in un vettore, per trovare il mese con il maggior numero di ingressi basta trovare l'indice dell'elemento di valore maggiore all'interno del vettore.

Per semplicità nella gestione degli indici, è possibile utilizzare un vettore di 12 elementi per memorizzare l'istogramma. Questo significa che verranno memorizzati anche i conteggi dei mesi nei quali il parco giochi è chiuso, ma tali conteggi risulteranno pari a zero e non daranno quindi problemi per il calcolo del massimo.

Si può pertanto creare una funzione come

int calcola_mese_max_ingressi(struct ingresso *elenco, int size);

la quale prende in ingresso il vettore elenco, contente size elementi, il quale contiene i valori letti dal file. Può restituire un valore compreso tra 0 e 11 (estremi compresi) che indica il mese avente il maggior numero di ingressi.

Dal momento che bisogna stampare il nome del mese è sufficiente creare un vettore di stringhe contente i nomi dei 12 mesi, e usare il valore restituito dalla funzione sopra suggerita come indice nel vettore. Un esempio di dichiarazione e inizializzazione di vettore di stringhe è riportato in S9.4, mentre un esempio specifico di stampa del nome di un mese è riportato in SA.3.

6) Giorno con più ingressi

Determinare il giorno con il maggior numero d'ingressi. Stampare il giorno usando il seguente formato:

[GIORNO_MAX_INGRESSI]
01/05/2018

Giorno e mese vanno stampati usando esattamente 2 cifre. In caso il numero corrispondente presenti una sola cifra, si anteponga lo zero.

Suggerimenti

Per risolvere questo punto si può definire una funzione come la seguente:

struct data calcola_giorno_max_ingressi(struct ingresso *elenco, int size);

La funzione accetta l'elenco dei dati relativi agli ingressi letti con la funzione leggi_file, la quale contiene size elementi. Restituisce una struttura di tipo struct data, opportunamente dichiarata per contenere i dati di una data (giorno, mese e anno), che indica il giorno dell'anno con il maggior numero di ingressi.

Per trovare tale giorno, all'interno della funzione, si può calcolare l'istogramma degli ingressi nei vari giorni dell'anno e trovarne il massimo.

Per facilitare l'identificazione del giorno, consiglio di usare una matrice per la memorizzazione dell'istogramma, nella quale i due indici indicano rispettivamente il mese e il giorno. La matrice può essere dichiarata e i tutti i suoi elementi inizializzati a zero come segue:

int ingressi[12][31] = { {0} };

Con un semplice ciclo su tutti gli elementi del vettore elenco passato come primo argomento della funzione, si usano i valori di mese e giorno nella struttura struct ingresso come indici nella matrice, andando a incrementare il valore del giorno corrispondente. In pratica, invece di usare un singolo indice per il calcolo dell'istogramma, se ne usano due.

Per esempio, se è avvenuto un ingresso il giorno 10 aprile, si incrementrà di 1 il valore di ingressi[3][9], dove 3 indica il mese e 9 il giorno (si ricordi che matrici e vettori si indicizzano partendo da 0, quindi gennaio corrisponde all'indice 0, così come il primo giorno del mese corrisponde all'indice 0).

Dopodichè, con due cicli for annidati, uno che itera sui mesi da 0 a 11 e l'altro sui giorni da 0 a 31, si trovano i due indici dell'elemento della matrice avente il massimo valore di ingressi. Indichiamo rispettivamente con m_max e g_max gli indici corrispondenti al mese e al giorno. Per esempio, se m_max = 5 e g_max = 9, allora il giorno con il massimo numero di accessi sarà il 10 giugno, in quanto gennaio corrisponde all'indice 0, così come il primo giorno del mese corrisponde all'indice 0.

L'anno della data, invece, è uguale per tutti gli ingressi, e può quindi essere facilmente recuperato da uno qualsiasi degli elementi del vettore elenco (si suggerisce dal primo elemento, di indice 0, che sarà sicuramente presente nel vettore elenco).

Gil indici possono quindi essere usati per assegnare i valori alla struttura struct data che viene restituita dalla funzione.

NOTA: per stampare un numero intero usando esattamente 2 cifre e anteponendo lo 0 in caso di numeri con una sola cifra, usare lo specificatore di formato %02d (vedi S2.12).

Verifica automatica

Si utilizzi il tool pvcheck di verifica automatica per testare il corretto funzionamento del programma. Il file contenente i test è ingressi.test. Per eseguire i test è necessario scaricare anche i seguenti file di dati che sono da salvare nella medesima directory del file di test:

Il comando da eseguire per il test è il seguente:

pvcheck -f ingressi.test ./a.out

Il file esempio.txt contiene invece i dati usati nell'esempio proposto in questa pagina, e può essere usato per testare il programma senza la necessità di utilizzare pvcheck.

Nella prossima pagina potrai esaminare un esempio di soluzione dell'esercizio.

Ingressi - Soluzione

In questo tutorial non verrà presentata la possibile soluzione nella sua interezza, ma soltano le parti interessanti, che dovranno essere opportunamente integrate per fornire la soluzione completa e compilabile. Per ottenere un programma completo e funzionante, si tratta pertanto di scrivere la funzione main che dichiara le variabili necessarie e che chiama opportunamente le funzioni descritte.

Per cominciare, si assume che vengano effettuate le seguenti dichiarazioni:

struct data {
    int giorno, mese, anno;
};

struct ingresso {
    struct data data;
    int nbadge;
    int permanenza;   // in minuti
    double prezzo;
};

Lettura del file

La funzione per la lettura e l'elaborazione del file può essere come segue:

struct ingresso *leggi_file(FILE * fp, int *size)
{
    int h1, m1, h2, m2;
    int letti = 0;
    char linea[1024];
    int dim = 8;

    /* allocazione iniziale del vettore */
    struct ingresso *vect = malloc(dim * sizeof(struct ingresso));
    if (vect == NULL) {
        /* segnala al chiamante che non ho potuto allocare memoria */
        *size = 0;
        return NULL;
    }
    while (fgets(linea, sizeof(linea), fp) != NULL) {
        sscanf(linea, "%d/%d/%d %d %d:%d %d:%d",
               &(vect[letti].data.giorno), &(vect[letti].data.mese),
               &(vect[letti].data.anno), &(vect[letti].nbadge), &h1, &m1,
               &h2, &m2);
        vect[letti].permanenza = durata(h1, m1, h2, m2);
        vect[letti].prezzo = calcola_tariffa(vect[letti].permanenza);
        letti++;
        /* raggiunta la dimensione massima allocata per vect? */
        if (letti >= dim) {
            /* se si`, ne raddoppio la dimensione */
            dim *= 2;
            vect = realloc(vect, dim * sizeof(struct ingresso));
            if (vect == NULL) {
                return NULL;
            }
        }
    }
    *size = letti;
    vect = realloc(vect, letti * sizeof(struct ingresso));
    return vect;
}

La funzione inizialmente usa la malloc (presentata in S9.5) per allocare spazio per dim strutture di tipo struct ingresso. L'indirizzo dell'area di memoria allocata è assegnato al puntatore vect. Si ricordi che malloc alloca una quantità di memoria espressa in byte, quindi la sizeof (introdotta in S5.10) viene usata per determinare la dimensione di una singola struttura di tipo struct ingresso. Se vect vale NULL allora non è stato possibile allocare la memoria e la funzione ritorna, restituendo NULL per indicare al chiamante il fallimento della lettura.

Se l'allocazione iniziale della memora ha successo, il ciclo while usa la fgets per leggere una riga alla volta dal file. In ciascuna iterazione, la funzione sscanf viene usata per estrarre i dati dalla riga letta e per memorizzarli nell'area appena allocata, che viene acceduta come fosse un vettore tramite il puntatore vect (vedi equivalenza tra puntatori e vettori illustrata in S9.2).

La durata della permanenza e l'incasso vengono calcolati rispettivamente con le funzioni durata e calcola_tariffa, illustrate di seguito.

Se il numero di elementi memorizzati nel vettore vect eccede la dimensione del vettore dim, il valore di dim viene raddoppiato e la memoria allocata per vect viene ridimensionata con la realloc. Il ridimensionamento della memoria allocata non tocca il contenuto della memoria precedente, cosicché quanto è tato memorizzato rimane tale, e si ha spazio per memorizzare ulteriori dati.

Alla fine del ciclo, il valore di *size (il valore puntato dal puntatore size) viene posto pari al numero di dati letti (variabile letti). Si ricordi che size è un puntatore, che viene usato con il passaggio per riferimento al fine di permette alla funzione leggi_file di "restituire" il valore opportunamente modificato per riflettere il numero righe lette.

Infine, il vettore vect viene ridimensionato con realloc per contenere esattamente un numero di strutture pari a letti, e viene restituito l'indirizzo della memoria allocata e contenente i dati letti nella funzione.

Funzioni di supporto alla lettura

Durante la lettura vengono usate due funzioni che aiutano a calcolare ivalori da inserire nella struttura struct ingresso: la funzione durata e calcola_tariffa.

La durata può essere calcolata con una funzione come la seguente:

int durata(int h1, int m1, int h2, int m2)
{
    int min1 = (h1 * 60) + m1;
    int min2 = (h2 * 60) + m2;
    return min2 - min1;
}

I valori in ore e minuti vengono convertiti in minuti, e viene restituita la differenza tra l'orario h2:m2 e h1:m1.

NOTA: per svolgere i calcoli su date e orari, conviene convertirne il valore in una unità di misura che permetta di svolgere i calcoli tra singoli numeri interi. Nell'esempio, gli orari espressi in ore e minuti sono stati convertiti in minuti trascorsi dalla mezzanotte, che è sempre un numero intero di minuti.

Il calcolo della tariffa si può effettuare come segue:

double calcola_tariffa(int tempo)
{
    if (tempo <= 20) return 3.5;
    if (tempo <= 30) return 4.0;
    if (tempo <= 45) return 4.5;
    if (tempo <= 60) return 5.5;
    return 7.0;
}

È importante notare che non è elegante mischiare i dati (cioè i valori delle fasce orarie e delle tariffe) con il codice. Quindi è possibile impostare una soluzione alternativa e migliore che separa i due aspetti.

Scopri un esempio di soluzione alternativa »

1) Numero di ingressi

Il numero di ingressi corrisponde semplicemente al valore di size restituito dalla leggi_file illustrata più sopra.

2) Stampa in ordine inverso

Per mostrare come anche il problema più semplice possa ammettere diverse logiche di implementazione, vengono mostrate due possibili implementazioni della funzione stampa_ultimi_inverso, che possono essere usare in modo comletamente intercambiabile, dal momento che le funzioni accettano gli stessi parametri e producono lo stesso risultato a parità di dati in ingresso.

I parametri sono l'elenco di size ingressi, memorizzato in un vettore di strutture struct ingresso puntato da ingressi, e il numero di badge da stampare a partire dall'ultimo. Questo parametro, benchè non indispensabile poiché nel tutorial si richiede di stampare al massimo 10 valori, mostra come è possibile rendere parametrica e generica la funzione, facendo in modo che il numero di dati da stampare venga passato alla funzione, in modo da poterla usare per stampare un qualsiasi numero di badge.

La prima versione di stampa_ultimi_inverso inizia determinando il numero esatto di badge da stampare, verificando se size è minore di num_da_stampare, nel qual caso num_da_stampare viene posto uguale a size. In questo modo, se non ci sono num_da_stampare elementi da stampare, vengono stampati tutti, in numero pari a size. Dopodiché, il ciclo for viene eseguito un numero di volte pari a num_da_stampare, stampando con printf il badge a partire dall'ultimo, procedendo all'indietro man mano che il valore di i viene incrementato. In questa soluzione è importate gestire correttamente l'indice per indicizzare il vettore ingressi.

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare)
{
    int i;
    if (size < num_da_stampare)
        num_da_stampare = size;
    for (i = 0; i < num_da_stampare; i++)
        printf("%d\n", ingressi[size - i - 1].nbadge);
}

La seconda versione utilizza un ciclo while per stampare i badge partendo dal fondo del vettore man mano che il valore di i viene incrementato ad ogni iterazione. Il ciclo while continua fintanto che i rimane minore di num_da_stampare e allo stesso tempo i rimane minore di size. Quando una delle due condizioni diviene falsa, non importa quale, il ciclo termina. Implicitamente, se size è minore di num_da_stampare la condizione che diventerà falsa sarà la seconda. Viceversa, ovvero se size è maggiore o uguale a num_da_stampare sarà la prima a far terminare il ciclo.

void stampa_ultimi_inverso(struct ingresso *ingressi, int size, int num_da_stampare)
{
    int i = 0;
    while ((i < num_da_stampare) && (i < size)) {
        printf("%d\n", ingressi[size - i - 1].nbadge);
        i++;
    }
}

3) Incasso mensile

Una possibile funzione che calcola l'incasso mensile è la seguente:

void calcola_incassi_mensili(struct ingresso *elenco, int size, double *incassi)
{
    int i;
    for (i = 0; i < size; i++) {
        incassi[elenco[i].data.mese - 1] += elenco[i].prezzo;
    }
}

I suoi parametri sono:

  • il vettore puntato da elenco, contenente size elementi, dove ogni elemento contiene il prezzo pagato e calcolato nella funzione leggi_file
  • il puntatore incassi, che passa alla funzione un vettore di 12 valori double, uno per mese, che verranno aggiornati con il totale dell'incasso mensile

Il vettore incassi sarà dichiarato nella funzione che chiama calcola_incassi_mensili come segue:

    double incasso_mensile[12] = { 0.0 };

La funzione non fa altro che sommare all'elemento giusto il valore del prezzo. L'elemento viene determinato usando come indice il valore del mese. Tale valore è decrementato di uno perchè i mesi letti dal file vanno da 1 a 12, mentre gli indici nel vettore devono andare da 0 a 11.

4) Incasso totale

Posto di aver calcolato gli incassi mensili risolvendo il punto 3, gli incassi annuali si possono calcolare con una funzione come la seguente:

double incasso_annuale(double *incasso_mensile)
{
    int i, incasso;
    for (i = 0; i < 12; i++) {
        incasso += incasso_mensile[i];
    }
    return incasso;
}

Questa funzione somma gli incassi anche dei mesi nei quali il parco giochi è chiuso, ma tali valori sono pari a zero e non modificano il valore dell'incasso.

prova-tu Prova a modificare la funzione per sommare solo gli incassi dei mesi nei quali il parco giochi è aperto.

prova-tu Come potrebbe essere modificata la funzione nel caso in cui i mesi di apertura non fossero consecutivi (per esempio, apertura da marzo a giugno e da settembre a novembre).

5) Mese con più ingressi

Per calcolare il mese con il maggior numero di ingressi si potrebbe usare una funzione come la seguente:

#define MESI_X_ANNO  (12)

int calcola_mese_max_ingressi(struct ingresso *elenco, int size)
{
    int i;
    int ingressi[MESI_X_ANNO] = { 0 };    /* per registrare gli ingressi */
    int max = 0;
    int mese_max;

    for (i = 0; i < size; i++) {
        ingressi[elenco[i].data.mese - 1]++;
    }
    /* cerco il massimo */
    for (i = 0; i < MESI_X_ANNO; i++) {
        if (ingressi[i] > max) {
            max = ingressi[i];
            mese_max = i;
        }

    }
    return mese_max;
}

Il primo ciclo for calcola l'istogramma degli ingressi nell'anno, contenuti nel vettore puntato da elenco di dimensione size (quello che viene prodotto dalla funzione leggi_file), memorizzandolo nel vettore ingressi di 12 elementi (in virtù del valore della macro MESI_X_ANNO).

Il secondo ciclo for trova l'indice dell'elemento maggiore nel vettore ingressi. Questo corrisponde all'indice mese_max del mese con più ingressi, che viene restituito alla funzione chiamante.

Per poter stampare il nome del mese, conoscendone l'indice, basta una istruzione del tipo

    printf("%s\n", NOMI_MESI[meseMaxIngr]);

dove meseMaxIngr è il valore restituito da calcola_mese_max_ingressi, e NOMI_MESI è dichiarato prima della funzione come segue:

char *NOMI_MESI[] = {
    "Gennaio", "Febbraio", "Marzo",
    "Aprile", "Maggio", "Giugno",
    "Luglio", "Agosto", "Settembre",
    "Ottobre", "Novembre", "Dicembre"
};

6) Giorno con più ingressi

Per calcolare la data con il maggior numero di ingressi si potrebbe usare una funzione come la seguente:

struct data calcola_giorno_max_ingressi(struct ingresso *elenco, int size)
{
    int i, g, m;
    /* per registrare gli ingressi di tutti i giorni dell'anno
     * per semplicita` e generalita` considero tutti e 12 i mesi */
    int ingressi[MESI_X_ANNO][GIORNI_X_MESE] = { {0} };
    int max = 0;
    int g_max, m_max;   /* indici del giorno e mese di massimo ingresso */
    struct data data;

    for (i = 0; i < size; i++) {
        data = elenco[i].data;
        ingressi[data.mese - 1][data.giorno - 1]++;
    }

    max = 0;
    for (m = 0; m < MESI_X_ANNO; m++) {
        for (g = 0; g < GIORNI_X_MESE; g++) {
            if (ingressi[m][g] > max) {
                max = ingressi[m][g];
                m_max = m;
                g_max = g;
            }
        }
    }
    data.anno = elenco[0].data.anno;   /* tutte le date dello stesso anno */
    data.mese = m_max + 1;
    data.giorno = g_max + 1;
    return data;
}

Viene calcolato l'istogramma degli ingressi a partire dai size elementi del vettore elenco passato come argomento.

L'istogramma è memorizzato nella matrice ingressi di 12 righe e 31 colonne, avendo avuto cura di definire le due macro:

#define GIORNI_X_MESE  (31)
#define MESI_X_ANNO  (12)

I due cicli for annidati cercano l'elemento di valore massimo nella matrice. Gli indici di tale elemento, memorizzati nelle variabili m_max e g_max per il mese e il giorno rispettivamente, indicano direttamente la data cercata.

Terminati i cicli for, vengono assegnati i campi di una struttura di tipo struct data per restituire il giorno desiderato.

Gli indici m_max e g_max vengono incrementati di 1 in quanto il loro valore è rispettivamente nell'intervallo [0, 11] e [0, 30], mentre le date vengono poi stampate con valori che partono da 1, sia per i giorni che per i mesi.

Il valore dell'anno viene preso dalla struttura memorizzata come primo elemento del vettore, dal momento che tutti gli anni sono gli stessi.