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)
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 W
arning 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
- 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 nellaprintf
(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.
Modifica il programma rimuovendo la riga della #include
e ri-compilando il programma. Cosa noti?
Modifica l'importo assegnato alla variabile euro
e fatti stampare il valore in dollari.
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!
Testa il funzionamento del programma con i seguenti importi: 2
, 10
, 100
, 1000
.
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
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:
- Se si ha un importo
X=123
euro, per calcolare il numero di banconote da 50 euro, assegnandone il valore alla variabileb50
, bisogna fare la divisioneb50 = X/50
, il cui risultato è 2 (ATTENZIONE: la divisione deve essere fatta tra numeri interi!) - Di seguito si calcola il resto con
resto = X % 50
(il risultato è 23), assegnando il risultato alla variabileresto
- 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
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!
Testa il funzionamento del programma con i seguenti importi: 7
, 51
, 88
, 223
, 1011
.
Usa il valore 1025679
per il test. Riesci a verificare velocemente che il numero di banconote da 50 euro
calcolato dal programma è corretto?
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
)?
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;
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
ec
) - leggi il valore delle tre variabili da tastiera usando
fgets
eatoi
(ricorda di dichiarare la stringa "di supporto"), vedi S4.1 - utilizza opportune combinazioni di costrutti
if
eif-else
(vedi rispettivamente S4.3 e S4.4 per la sintassi) per assegnare il valore mediano ad una variabilem
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 diatoi
(e, come già sai, distdio.h
perprintf
efgets
)
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 sea
è il minimo globale (e la mediana è il minimo trab
ec
) - alternativamente, con
b <= a && b <= c
si stabilisce se èb
ad essere il minimo globale (e la mediana è il minimo traa
ec
) - in caso entrambe le precedenti siano false, allora è
c
ad essere il minimo globale (e la mediana è il minimo traa
eb
)
- con
Hands-on
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.
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.
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.
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.
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 pera
solo seb
è dispari. - successivamente, si aggiorna il valore di
a
calcolandone il quadrato, mentreb
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
erisultato
) - leggi il valore delle variabili necessarie da tastiera usando
fgets
,atoi
eatof
(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 stringanon 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 diatoi
eatof
(e, come già sai, distdio.h
perprintf
efgets
) - 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
cheatof
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
eb
sono contemporaneamente nulli: in questo caso la potenza non è definita e si stampa il messaggionon calcolabile
- si noti l'espressione
b % 2 == 1
per determinare seb
è dispari - l'operazione
b = b / 2
fa già l'arrotondamento per difetto (si ricordi che la divisione intera tronca la parte decimale)
Hands-on
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 »
Modifica il programma per gestire correttamente anche gli esponenti interi negativi.
Sfrutta la relazione a^b = (1/a)^(−b)
.
In questo caso, è possibile utilizzare il ciclo do-while
al posto del ciclo while
utilizzato nella soluzione proposta?
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?
- 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 ciclowhile
(S4.7) odo-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, sevet[i] < min
allora si assegnamin = vet[i]
, altrimenti di prosegue ad esaminare l'elemento successivo; per effettuare queste operazioni servirà l'uso di un costruttoif
, presentato in S4.3.
- assegnare a
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?
- l'uso del vettore è tale per cui
freq[0]
conterrà il conteggio del voto18
,freq[1]
quello dei19
, ... efreq[12]
il conteggio dei30
- pertanto, se il voto corrente è contenuto nella variabile
voto
, l'elemento giusto del vettorefreq
da incrementare èfreq[voto - 18]
, che si incrementa di uno confreq[voto - 18]++
- infatti:
- se
voto
vale18
, si incrementafreq[18 - 18]
, cioèfreq[0]
- se
voto
vale19
, si incrementafreq[19 - 18]
, cioèfreq[1]
- ...
- se
voto
vale30
, si incrementafreq[30 - 18]
, cioèfreq[12]
- se
- pertanto, se il voto corrente è contenuto nella variabile
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 costruttoif
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 memorizzareNMAX
elementi
- il controllo è necessario in quanto il vettore
- 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 costruttoif
annidato neldo-while
)
- si noti che l'indice
- la stampa dei valori (marcatore
[VALORI]
) è un semplice ciclofor
da0
an-1
che stampa tutti i valori nel vettorevoti
Per cercare il minimo
- viene assegnato alla variabile
min
il valore del primo elemento del vettorevoti[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 di1
l'elemento del vettorefreq
di indicevoti[i]-MINVOTO
voti[i]
è il valore del voto corrente, mentreMINVOTO
è il valore minimo (18
)- pertanto e il voto vale
18
verrà incrementato il valore di indice0
, se vale19
quello di indice1
, ... e se vale30
il valore di indice13
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 chevoti
- 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
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 »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 adouble
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.
ATTENZIONE: non dimenticare di inizializzare la variabile totale
assegnando il valore 0
(zero).
Cosa succederebbe altrimenti?
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 nelmain
- realizza una funzione
leggi_voti
all'interno della quale sarà riempito il vettorevet
leggendon
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 funzionechist
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 restituirevoid
(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 chiamarestampa_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 funzionechist
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 chiamareminimo
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 funzionefrequenze
- 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
- va fatta nel
- la stampa delle frequenze avvenga con un ciclo
for
nelmain
, 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 funzionechist
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 chiamaremax_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 conlung
) - usa un ciclo
while
che faccia partire un contatore (es.pos
) dalung
e lo decrementi (conpos--
) 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 stringab
usando lastrcpy
(vedi S5.11) - modifica la stringa
b
ponendob[sep] = 0
; in questo modo, i caratteri significativi dib
vengono "troncati" al carattere di indicesep
, poiché lo0
diviene il carattere di terminazione della stringab
(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
) usandotrova_ultimo_separatore
- con
strcpy
viene copiata la porzione di stringa che va dapos + 1
fino alla fine della stringapercorso
all'interno della stringab
- questa istruzione funziona perché
percorso
è un indirizzo, ovvero l'indirizzo del primo carattere della stringa (percorso
è un puntatore achar
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)
- questa istruzione funziona perché
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 stringapercorso
, usando lastrlen
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'indicepos > 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 tipochar
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 primin
caratteri dapercorso
ab
- 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.
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 - restituendo0
conreturn 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 - restituendo0
- 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 uscitaext
se c'è un punto nel percorso e restituisce1
(attenzione che si arriva a questo punto quando le condizioni dei precedenti dueif
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 contatorei
per esaminare tutte le righe della matricemat
- per ciascuna riga, il ciclo
for
più interno esamina i valori di ciascuna colonna usando il contatorej
- per ciascun elemento, il ciclo
do-while
continua a ripetere la lettura di un numero fintanto che questo non è compreso nell'intervallo[MIN, 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 contatorei
per esaminare tutte le righe della matricemat
- per ciascuna riga, il ciclo
for
più interno esamina i valori di ciascuna colonna usando il contatorej
- ad ogni ciclo l'elemento
mat[i][j]
viene sommato asomma
- in corrispondenza della
return
il valore della media viene calcolato dividendosomma
per il numero totale di elementi, ovveroNRIG * 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 contatorei
per esaminare tutte le righe della matricemat
- per ciascuna riga, il ciclo
for
più interno esamina i valori di ciascuna colonna usando il contatorej
- ad ogni ciclo l'elemento
mat[i][j]
viene usato come indice nel vettorefreq
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 contatorei
per esaminare tutte le righe della matriceinput
- per ciascuna riga, il ciclo
for
più interno esamina i valori di ciascuna colonna usando il contatorej
- ad ogni ciclo l'elemento
input[i][j]
viene assegnato all'elementooutput[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
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 chiamistruct.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
.
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
eB
utilizzando la funzioneleggi_punto
già implementata - può essere utile creare una
struct rettangolo_t
i cui due campi siano di tipostruct 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 coni
che va da0
an-1
, mentre quello più interno conj
che va dai+1
an
- una variabile
max
memorizzerà il valore massimo corrente
- per questo sono sufficienti due cicli
- 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 variabilimax_i
emax_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 lasscanf
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 dellasscanf
in abbinamento allestruct
) - nella
sscanf
viene usato lo specificatore%lf
per estrarre due numeri in virgola mobile dalla stringa letta con lafgets
- i valori estratti vengono memorizzati nelle variabili
p.x
ep.y
che sono passate allasscanf
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 puntop
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 rettangolor
- si verifica the la
x
dip
sia compresa tra lax
diA
e quella diB
(A
eB
sono i campi dir
)- stessa cosa per la
y
- stessa cosa per la
- 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 coordinatex
dei punti estremi - la stessa cosa avviene con la differenza tra le coordinate
y
per l'altezzah
- lo stesso risultato si sarebbe ottenuto anche senza l'uso delle variabili temporanee
b
eh
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 lunghezzalen
, e gli indici dei punti più distantimax_i
emax_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 puntoi
e ogni puntoj
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 da0
e arriva alen-1
(escluso) - il contatore
j
parte dai+1
fino ad arrivare alen
(escluso)
- il contatore
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)?
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:
- i dati possono essere letti riga per riga ed elaborati senza necessità di caricare in memoria tutti i dati dal file;
- 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
esscanf
(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 dichar
non serve la&
)
- ricorda che per estrarre una stringa con
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 dichar
non serve la&
)
- ricorda che per estrarre una stringa con
- viene chiamata la funzione
notturno
, la quale restituisce1
oppure0
(cioè vero oppure falso) se l'orario indicato dal parametroh
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
esscanf
(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 dichar
non serve la&
)
- ricorda che per estrarre una stringa con
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 dichar
non serve la&
)
- ricorda che per estrarre una stringa con
- viene chiamata la funzione
notturno
, la quale restituisce1
oppure0
(cioè vero oppure falso) se l'orario indicato dal parametroh
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 lafgets
restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso daNULL
). Se non ci sono altre righe da leggere, lafgets
restituisceNULL
. Dal momento che la costanteNULL
equivale al numero0
, la condizione del ciclowhile
diventa0
(cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclowhile
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
esscanf
(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
eb
- per fare questo, si può usare la funzione
fgets
(S8.6) senza la necessità di un ciclowhile
- 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 dichar
non serve la&
)
- ricorda che per estrarre una stringa con
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 laleggi_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 lafgets
restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso daNULL
). Se non ci sono altre righe da leggere, lafgets
restituisceNULL
. Dal momento che la costanteNULL
equivale al numero0
, la condizione del ciclowhile
diventa0
(cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclowhile
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
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 lamalloc
;dim
è una variabile il cui valore iniziale può essere un numero a piacere (per esempio4
); - comincia a leggere il file con il solito ciclo
while
e le funzionifgets
esscanf
(vedi S8.6) - all'interno del ciclo
while
, quando il numero di righe lette e memorizzate è pari adim
, aumenta il valore didim
(per esempio raddoppiandolo) e usa larealloc
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 dichar
non serve la&
)
- nella
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 chiamareleggi_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 laleggi_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 lafgets
restituisce l'indirizzo del puntatore usato per contenere la stringa letta da file (quindi, in ogni caso, un puntatore diverso daNULL
). Se non ci sono altre righe da leggere, lafgets
restituisceNULL
. Dal momento che la costanteNULL
equivale al numero0
, la condizione del ciclowhile
diventa0
(cioè falsa) quando la lettura del file è terminata. E di conseguenza il ciclowhile
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":
- una struttura dati che conservi gli elementi in modo ordinato;
- una funzione che permetta di confrontare due elementi, per determinare se sono uguali, oppure quale è il maggiore/minore;
- 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
ec2
possono essere coincidentic1
ec2
possono intersecarsi (si considera che non siano coincidenti)- il cerchio
c1
contiene il cerchioc2
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à:
- al meglio dei 3 set, dove vince il giocatore che si aggiudica per primo 2 set su 3, oppure
- 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
egioc2
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 ripeterev + (*count)
in tutti i punti necessari nella funzione; - la
sscanf
tenta sempre di convertire 5 set; solo successivamente, con l'istruzionepartita->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 e
v2`:
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 tipostruct 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 vettorepiloti
- 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 dichar
non serve la&
) - il nome va copiato tramite
strcpy
nel campo della struttura giusta del vettorepiloti
- ricorda che per estrarre una stringa con
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 tipostruct 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 tipostruct classificato
per memorizzare i punteggi dei piloti, passato per riferimento (S6.6) - il vettore
scuderie
di strutture di tipostruct 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 vettorepiloti
- 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 dichar
non serve la&
) - il nome va copiato tramite
strcpy
nel campo della struttura giusta del vettorepiloti
- si ricorda che per estrarre una stringa con
Nel main
:
- chiamare la funzione
leggi_gara
in un ciclo come segue (piloti
escuderie
sono vettori opportunamente dichiarati nelmain
):
while (leggi_gara(fin, piloti, scuderie) != 0) {
/* quando leggi_gara ritorna, una gara intera e` stata letta */
.....
}
- elaborare opportunamente il contenuto dei vettori
piloti
escuderie
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 ricercasize
è il numero di strutture presenti nel vettorenome
è 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:
- un elemento con il nome desiderato è già presente in
elenco
, nel qual caso si ritorna l'indirizzo di quell'elemento - un elemento con il nome desiderato non è già presente in
elenco
(questo si verifica durante la lettura della prima gara nel file); quindi la funzionecerca_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 tipostruct 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 camponome
corrisponde alla stringanome
passata come terzo argomento; - il vettore
elenco
ha dimensione pari asize
elementi; - il ciclo
for
esamina i vari elementi del vettoreelenco
; - usa la
strcmp
per confrontarenome
conelenco[i].nome
, che è il camponome
dell'elemento i-esimo del vettore; - se la
strcmp
restituisce0
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 stringanome
(constrcpy
); - 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) eaaaa
è l'anno;nbadge
è il numero identificativo del badge, un intero compreso tra 1 e 1000;h1:m1
eh2:m2
sono rispettivamente gli orari di ingresso e di uscita, nel formatoora:minuto
;h1
eh2
rappresentano l'ora, e sono numeri interi compresi nell'intervallo[0, 23]
;m1
em2
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 aFILE
che identifica il file da leggere; il file deve essere già aperto in lettura prima di chiamare la funzione (usandofopen
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 chiamaleggi_file
- la rispettiva variabile deve essere dichiarata nel
- 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 funzioneleggi_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:
- 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
- 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 funzionemalloc
(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 disize
elementi restituito dalla funzioneleggi_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:
- calcolare e memorizzare la durata della permanenza per ciascun ingresso
- calcolare la tariffa per ciascun ingresso in funzione della durata della permanenza
- 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 disize
elementi restituito dalla funzioneleggi_file
incassi
è un vettore didouble
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
, contenentesize
elementi, dove ogni elemento contiene il prezzo pagato e calcolato nella funzioneleggi_file
- il puntatore
incassi
, che passa alla funzione un vettore di 12 valoridouble
, 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 a modificare la funzione per sommare solo gli incassi dei mesi nei quali il parco giochi è aperto.
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.