Soluzioni 23/07/2014

1) Un calcolatore ha in memoria, nei byte a partire dall’indirizzo 100, i valori: 54, 57, 24 memorizzati su 8-bit. Si calcoli nel byte 200 il valore di x = 54 - 57, nel byte 201 il valore di y = -24*x , e nei byte da 204–208 il valore di y/5.0, in IEEE 754 su singola precisione.

Soluzione:

x = 54-57 = 54 + (-57).  54 = 32 + 16 + 4 + 2 = 00110110   57 = 32 + 16 + 8 + 1 = 00111001  -> -57 = 11000111.
Quindi 54-57 = 00110110 + 11000111 = 11111101 = 0xFD = -3   infatti 1111101+00000011 = 1 00000000 = 0 (su 8 bit). (I numeri esadecimali sono indicati dal prefisso 0x..)
Nota: Molti studenti scrivono un numero in esadecimale su 8 bit come: 000000xx, questo è del tutto scorretto poichè una cifra esadecimale rappresenta sa sola 4 bit (i valori da 0 a 15) quindi un numeri di 8 bit richiede SOLO DUE cifre esadecimali, non otto).

y = -24 * -3 = 72  = 64+8 = 01001000 = 0x48. Quindi y/5.0 = 72/5.0 = 14.4 = 14 + 0.4  14 = 8+4+2 = 00001110, 0.4 *2 = 0.8 + 0,  0.8*2 = 0.6 + 1, 0.6*2 = 0.2 + 1, 0.2*2 = 0.4 + 0   il numero è quindi periodico.
14.4 = 14+0.4 = 1110.01100110011001100110011....= 1.11001100110011....*2^3 quindi:
segno = 0, exp = 3+127=130=128+2=10000010, mantissa =11001100110011001100110. Ricordiamo che la mantissa è costituita in singola precisione da 23 bit, ed il primo bit della rappresentazione si omette nella mantissa per aumentare la precisione.
Concludendo in memoria a partire dalla locazione 200,considerando una rappresentazione little-endian (processori x86) si ha in esadecimale : 0xFD, 0x48, 0x66, 0x66, 0x66, 0x41


2) Descrivere i componenti e l’organizzazione di un modermo calcolatore secondo l’architettura di Von Neumann. Spiegare quali parametri di ogni singolo componente concorrono alle performance complessive del calcolatore. Descrivere l’architettura interna della CPU necessaria per i calcoli dell’esercizio precedente, il corrispondete set di istruzioni in linguaggio assembly, e una loro possibile rappresentate in codice macchina (binario)

Soluzione:

Un calcolatore che segue l'architettura di Von Neumann è composto dai seguenti componenti: Un processore o CPU, una memoria (RAM) e un insieme di linee di interconnesione (bus) che connettono i due componenti e/o altri componenti come tutte le unità di I/O. Il bus e ogni altro tipo di interconnessione sono realizzati su un componente denominato "piastra madre". Le caratteristiche quantitative fondamentali della memoria sono la sua dimensione in byte, e la sua velocità (esprimibile attraverso due parametri: il ritardo necessario ad accedere ad un byte dato il suo indirizzo, e la velocità di trasferimento dei byte successivi, durante una lettura di più byte) Questi numeri possono essere espressi come cicli di clock della CPU, ad esempio una memoria con un ritardo t = 8, implica che la CPU attende 8 cicli di clock per poter leggere un dato. La dimensione D della memoria in byte determina inoltra la dimensione del bus indirizzi che sarà necessariamente bit ampio. Esempio: con 16GB di RAM  dobbiamo avere necessariamente almeno bit per indirizzo, tipicamente viene usata la potenza di 2 successiva, quindi il bus indirizzi dovrà avere 64 bit. La velocità della CPU, può essere legata ad una prima approssimazione alla sua velocità di clock, espressa di solito in GHz, ricordiamo che l'inverso della velocità di clock non è altro che il ritardo massimo necessario ad attraversare il datapath (alu+controlli), e quindi per l'esecuzioni delle istruzioni più semplici, per altre istruzioni potrebbe essere necessario usare più cicli della cpu (ecco perche la velocità di clock non è il solo parametro che influenzi la velocità della cpu). Inoltre all'interno della CPU sono presenti delle cache sia per le istruzioni che per i dati, e l'interazione tra la dimensione della cache, la velocità della memoria, e il tipo di programma sono i fattori fondamentali che determinano le prestazioni del sistema.
All'interno della CPU, possiamo identificare i registri di uso generale (Rx), l'ALU, il registro di stato  (SR) che segnala condizioni come l'overflow, lo zero, l'underflow, etc. il registro istruzione (IR) (che contiene il codice macchina dell'istruzione da eseguire), ed il program counter (PC) che contiene l'indirizzo della prossima istruzione in memoria. In molti processori sono presenti altri registri per la rappresentazione di numeri in virgola mobile, un ALU separata per le istruzioni in virgola mobile, e un altro corrispondente registro di stato.

Per i calcoli dell'esercizio (1) la CPU, per calcolare x, dovrà avere una ALU che compie almeno le operazioni di somma bit a bit, di complemento, il successore (+1) di solito un circuito ottimizzato a aparte rispetto alla somma, in modo da poter trasformare il contenuto di un registro in complemento a 2 senza dover usare l'ALU. L'operazione di prodotto per calcolare y, può essere presente dentro l'ALU, oppure può essere microprogrammata: nel primo caso, l'ALU ''sa fare'' la moltiplicazione direttamente in hardware, nel secondo caso, la moltiplicazione non è presente in hardware, ma viene eseguito un ''microprogramma'' memorizzato dentro la parte controllo della CPU, che calcola il prodotto come somme successive, richiedendo quindi più cicli di clock. Infine per il calcolo di y/5.0, la CPU deve avere un completo supporto per i numeri IEEE/754, pertanto un set di registri che contengono numeri fp. ed un ALU separata per svolgere le operazioni fp.

Se consideriamo le operazioni sia su interi che su fp abbiamo almeno 8 istruzioni diverse, aggiungiamo: test logici (=,>,<,>=,<=), istruzioni di salto (in pratica una modifica del registro PC) assoluto o condizionato ad un test, le istruzioni bit a bit (complemento, and, or, xor), un set tipico potrebbe racchiudere non meno di 64 istruzioni. Per rappresentare 64 istruzioni diverse possiamo codificarle con bit ciascuna, quindi ad esempio: 101111 =  47 = moltiplicazione intera (questa corrispondenza è del tutto arbitraria, dipenderà in realtà dal processore).

Resta da rappresentare per ogni operazione da dove provengono gli operandi: se il numero di registri è 16 (da 0 a 15), bastano 4 bit, per rappresentare un registro. Quindi con 3*4 bit rappresentiamo due registri sorgente (operandi) e un registro destinazione. Il formato del codice macchina sarà pertanto:

6 bit             4    4     4
op                 s1  s2    r

E l'operazione sarà r = s1 op s2. Ogni istruzione sarà rappresentabile su 12+6 = 18 bit. Questo richiederebbe 3 byte, per comodità conviene tenere le istruzioni allineate a multipli pari, e quindi usare 32 bit = 4 byte per ogni istruzione.


Esercizio 3:  Scrivere un programma che riceve dalla linea di comando due interi: N, k. Genera un vettore V di N interi casuali nell’intervallo , il vettore è poi scritto in un file binario 'numeri.dat', preceduto dal valore di N. Dopo il salvataggio, si vuole risolvere il seguente problema: si cerca, se esiste, un sottovettore di V di somma massima che divida (multiplo di) k. Il programma deve stampare gli indici s, t i.e. l’inizio e la fine (inclusi) del sottovettore, la somma degli elementi del sottovettore ed il valore .

Soluzione:

Prima di tutto, notiamo un refuso nel testo che tengo a sottolineare poichè nessun studente allo scritto ha pensato di evidenziare tale refuso (evidente), o di chiedermi chiarimenti su quell'intervallo abbastanza strano. Il refuso riguarda l'intervallo di valori da generare che evidentemente è da interpretare come , e non come alcuni hanno fatto come o , ma passiamo oltre. Il secondo refuso è indicato nel sottolineato, il valore di k è inteso minore dei valori della somma di un sottovettore, quindi cerchiamo i sottovettori la cui somma è un multiplo di k, tra tutti questi, vogliamo identificare quello di somma massima. Questo refuso in realtà non è particolarmente importante, poichè cambia solo l'ordine degli operandi del modulo nella condizione da testare sul valore della somma e su k.

Il problema della somma massima di un sottovettore è stato trattato a lezione, o dato come esercizio. Infatti è presente online su questo blog, tra gli esercizi del corso. Si trattava quindi di adattare il codice presente al testo dell'esercizio e svolgere la prima parte riguardante la generazione del vettore ed il salvataggio su file.


#include 
#include 
#define L 100000

void SegmentoMassimok(int v[], int N, int k, int *start, int *end)
{}

int main(int argc, char *argv[])
{
    if (argc < 3) {
        printf("Esercizio2 N k\n"); return -1;
    }
    int N = atoi(argv[1]);
    int k = atoi(argv[2]);
    printf("\nGenero vettore di %d interi in [-%d,%d], k=%d\n\n",N,L,L,k);
    
	// la dichiarazione int v[N] non è adeguata 
	// poichè N non è costate a tempo di compilazione.
	// int v[N];  <- v cosi e' un VLA (vedi libro).
	
	int *v = malloc(sizeof(int)*N);
	if (v == NULL) { printf("Malloc error\n"); return -2; }
	
	// inizializzo il generatore dei numeri pseudocasuali con un seme qualunque.
	int seme = time(NULL);
	printf("seme = %d\n",seme);
	srand(seme);
	
	// nota: a % b appartiene a [0..b-1] quindi rand() % (2*L+1) sta in [0...2L] - L = [-L,L]
	for (int i=0; i<N; i++) v[i] = rand() % (2*L + 1) - L;
	
	// stampo il risultato (non richiesto, ma utile)
	for (int i=0; i<N; i++) printf("%6d%c",v[i],((i+1)%10==0 || i==N-1)?'\n':' ');
	
	// Adesso salviamo i dati in un file binario 'numeri.dat'
	
	FILE *f = fopen("numeri.dat","w");
	if (f == NULL) { printf("Errore creazione file 'numeri.dat'\n"); return -3; }
	// salvo prima il valore di N
	fwrite(&N,1,sizeof(int),f);
	fwrite(v,N,sizeof(int),f);
	fclose(f);
	
	// Finito il salvataggio, adesso mi occupo del segmento massimo
	int start = N, end = N;    // nota se start = end = N, segmento non valido.
	SegmentoMassimok(v,N,k,&start,&end);
	
	if (start != N) {
		printf("Segmento: [%d,%d] ",start,end);
		int s = 0;
		for (int i=start; i<=end; i++) s += v[i];
		printf(" somma = %d, p = %d\n",s,s/k);
	} else {
		printf("Nessun segmento di somma massima, con somma multiplo di %d\n",k);
	}
	return 0;
}

Alcune osservazioni:

1) Molti studenti, nello scritto allocano i vettori con una dichiarazione tipo:
int v[N];
per quanto questo codice sia valido in C99/C11 esso coinvolge un concetto non banale, infatti poichè il valore di N non è una costante nota a tempo di compilazione (N è noto solo durante l'esecuzione), il vettore risulta essere un VLA (Variable Lenght Array).
Tale tipo di array, sono un argomento avanzato, e deve essere sempre chiaro allo studente se la dichiarazione di un array crea o meno un VLA, in altre parole: lo studente deve essere capace di distinguere tra un vettore di dimensione costante a tempo di compilazione, come ad esempio int v[100] o meno, ed usare in alternativa la malloc; come nella soluzione indicata.
(Leggere attentamente: Gli studenti che continuano a usare dichiarazione come int v[N], possono farlo solo se sono in grado di riconoscere che si tratta di un VLA, marcare nel compito tutte queste dichiarazioni, e spiegare in fondo che si tratta di VLA, spiegando brevemente in cosa consiste la differenza rispetto ad array non VLA. Senza questi accorgimenti, tali dichiarazioni sono ERRORI GRAVI, traduzione esplicita: se lo fate ancora, appena lo trovo, valuto come insufficiente il compito senza dover leggere altro.)

2) Per generare i numeri casuali possiamo usare la rand(), ho usato una semplice trasformazione lineare, se [0,2L] allora [-L,L]. Inoltre per generare numeri in [0,2L] basta prendere gli interi rand() (2L + 1). In generale queste trasformazioni sono presenti in molti esercizi sia fatti a lezione che su web.

3) I file binari si usano in modo molto semplice, si apre un file, e si usano una sequenza di operazioni di fwrite o fread. Se non ci si deve spostare dentro il file queste sono le UNICHE operazioni valide. Alla fine si chiude il file con fclose (nota: in molti O.S. il file non viene creato fino al momento in cui non viene fatta la fclose, per questioni legate al buffering, quindi quando si scrive su file, è particolarmente importante non dimenticare le fclose). Sempre sui file binari, non è necessario scrivere gli interi uno alla volta, come fatto da alcuni studenti, per quanto non sbagliato, è poco efficiente, basta scrivere l'intero vettore usando correttamente gli argomenti della fwrite.

4) L'interfaccia della funzione SegmentoMassimak prende due interi per riferimento (uso i puntatori), in modo da cambiarli dentro la funzione e 'vedere' il cambiamento nella main. L'alternativa: definire una struttura con una coppia di interi e farla restituire dalla funzione è perfettamente valida.

Adesso vediamo l'implementazione della funzione: non ci preoccupiamo della complessità computazionale, ma della sua corretta implementazione in C. Studiamo matematicamente il problema: una soluzione è chiaramente un intervallo [s,t] con 0 <= s, t < N, con s <= t. Indichiamo con S[s,t] la somma degli elementi del vettore da s a t compresi. Allora la soluzione ottima, soddisfa: S[s,t] % k == 0 e a,b, 0<= a,b < N con a < b; S[s,t] >= S[a,b]. Quindi un approccio "forza bruta", enumera tutti questi segmenti, calcola la somma, verifica se è multiplo di k, e ne tiene il massimo. Ecco il codice in C:

void SegmentoMassimok(int v[], int N, int k, int *start, int *end)
{
	int max = 0;
	int smax = N, tmax = N;
	
	for (int s=0; s<N; s++) {
		for (int t=s; t<N; t++) {
			// fissati i valori di s,t, troviamo la somma del segmento:
			int somma = 0;
			for (int k = s; k<=t; k++) somma += v[k];
			// debug
			printf("[%d,%d] somma = %d, modulo %d = %d\n",s,t,somma,k,somma%k);
			if (somma % k == 0 && somma > max) {
				smax = s; tmax = t; max = somma;
			}
		}
	}
	if (smax != N) { *start = smax; *end = tmax; }
}

Naturalmente questa soluzione, sfruttando direttamente la definizione matematica della soluzione del problema, senza indagare attentamente sulle proprietà della stessa, è del tutto inappropriata dal punto di vista della complessità: infatti essa richiede operazioni, in quanto per tutte le sottosequenze, si calcola la somma in , e quindi complessivamente con un costo che cresce come il cubo del numero degli elementi.

Possiamo facilmente abbassare la complessità della soluzione precedente, notando che non ha senso calcolare per ogni segmento la sua somma da zero, tenendo conto del fatto che la somma di un segmento S[s,t+1] = S[s,t]+v[t+1], e che S[s-1,t] = v[s-1]+S[s,t]. Un altro modo per vedere questo è calcolare la funzione di somma cumulata P[i] = S[0,i] per ogni valore di i. Questo si fa banalmente in tempo lineare, ed a questo punto S[0,b] e S[a,b]=P[b]-P[a-1] se a>0; si calcola in tempo costate. Questo abbassa la complessità da ad .

Notiamo che nell'esercizio, il sottovettore di lunghezza massima (senza ulteriori condizioni) può essere trovato in tempo lineare. In questo problema la condizione di divisibilità per k, introduce in qualche modo un vincolo non locale alla soluzione.

Domanda: E' possibile abbassare la complessità ulteriormente? sembra difficile che si riesca a trovare un algoritmo lineare (davvero?), si potrebbe quindi tentare di abbassare la complessità a . (Un aiuto: è possibile abbassare la complessità, ma non è per nulla banale trovare l'algoritmo di complessità non quadratica)

Competizione: il primo che spedisce un email con una soluzione di complessità (o piccolo) avrà la sua soluzione pubblicata qui. La soluzione deve essere codice C corretto, compilabile, e testato su diversi valori di N,k.


Esercizio 4:
Un officina è composta da due macchine M1,M2. Un file di testo 'lavori.txt' contiene in ogni riga due interi che rappresentano i tempi necessari per completare il lavoro su M1 e M2. (Vedi testo pdf per l'esempio). I lavori che richiedono due macchine devono passare prima per M1 e poi per M2. Scrivere un programma che legge il file e inserisce i dati in una lista di lavori, ogni elemento della lista contiene 4 campi: tempo_inizio, num, t1, t2. ...(vedi testo pdf).

Soluzione:

Prima di tutto definiamo la struttura per la lista dei lavori:

typedef
struct le {
	int start, num, t1, t2;
	struct le *prev, *next;
} Work;

Leggendo il testo dell'esercizio, possiamo dare il codice della main, in stile top-down.

int main(int argc, char *argv[])
{
	FILE *f = fopen("lavori.txt","r");
	if (f == NULL) { 
		fprintf(stderr,"Errore: file lavori.txt non leggibile\n");
		return 1;
	}
	Work *lavori = ReadFile(f);
	fclose(f);
    print(lavori);
	PrintWorkTable(lavori);
	return 0;
}

Quindi, si apre il file, la funzione ReadFile legge i lavori e li inserisce in una lista; la lista verrà restituita alla main. Infine stampiamo la lista, e poi la tabella dei tempi di lavorazione con la funzione PrintWorkTable.

Cominciamo con ReadFile:

Work *ReadFile(FILE *f)
{
	Work *lavori = NULL;
	int count = 1, t1 = 0, t2 = 0;
	while (fscanf(f,"%d %d\n",&t1,&t2)>0) {
		Work *l = CreaLavoro(count++,t1,t2);
		lavori = append(lavori,l);
	}
	return lavori;
}

Leggiamo il file di testo con delle fscanf, creiamo un lavoro, e lo inseriamo in fondo alla lista con la funzione 'append', infine restituiamo la testa della lista.
Le funzioni CreaLavoro, e append sono le seguenti:

bool isempty(Work *list)  { return list == NULL; }

// Costruttore
Work *CreaLavoro(int w, int t1, int t2)
{
	Work *new = malloc(sizeof(Work));
	assert(new != NULL);
	new->start = 0;
	new->num = w;
	new->t1 = t1;
	new->t2 = t2;
	new->prev = new->next = NULL;
	return new;
}
	
Work *append(Work *lista, Work *new)
{
	Work *testa = lista;
	if (isempty(lista)) return new;
	while (lista->next != NULL) lista = lista->next;
	lista->next = new;
	new->prev = lista;
	return testa;
}

Naturalmente possiamo ottimizzare questa operazione se oltre alla testa della lista, tenessimo anche un puntatore all'ultimo elemento, in questo modo non dovremmo ogni volta scorrere dalla testa fino alla coda per poter appendere l'elemento. Ma la soluzione sopra data va comunque bene.

A questo punto, abbiamo la lista dei lavori, e possiamo preoccuparci della funzione che stampa l'elenco indicato nel testo pdf dell'esame.

enum { E1=1, S1,  E2, S2};
define min(a,b)  ((a)<(b)?(a):(b))
#define LARGEINT 100000

void PrintWorkTable(Work *lista)
{
	printf("Tempo\tM1\t\t M2\n");
	printf("      \tFine\tInizio\tFine\tInizio\n");
	int T = 0;
	Work *c1 = NULL, *c2 = NULL;
	
	while (!isempty(c1) || !isempty(c2) || !isempty(lista))
	{
		int f1 = 0, f2 = 0, min = LARGEINT;
		int w[5] = { 0 };
		
		// Prima controlla se ci sono lavori da terminare e aggiorna il termpo.
		if (!isempty(c1)) min = f1 = c1->start + c1->t1; 
		if (!isempty(c2)) { 
			f2 = c2->start + c2->t2;
			min = (min < f2) ? min : f2;
		}
		if (min != LARGEINT)
		{
			// t != 0 se termina qualche lavoro
			T = min;	
			if (f1 == T) {
				w[E1] = c1->num;
				c1->t1 = 0;
				if (c1->t2 != 0) lista = insert(lista, c1);
				c1 = NULL;
			}
			if (f2 == T) {
				w[E2] = c2->num;
				c2->t2 = 0;
				c2 = NULL;
			}
		}
		// Dopo assegna lavori alle macchine libere.
		if (isempty(c1))  {
			c1 = AssegnaLavoro(&lista,1);
			if (c1 != NULL) {
				c1->start = T;
				w[S1] = c1->num;
			} 
		}
		if (isempty(c2)) {
			c2 = AssegnaLavoro(&lista, 2);
			if (c2 != NULL) {
				c2->start = T;
				w[S2] = c2->num;
			}
		}
		printf("%2d:\t",T);
		for (int i=1; i<5; i++) if (w[i]) printf("L%d\t",w[i]); else printf(" -\t");
		printf("\n");
	}
}

Analizziamo questa funzione molto lunga, ma semplice: La funzione stampa l'intestazione della tabella, per ogni macchina teniamo un suo 'lavoro corrente', c1 e c2. La variabile T è come un orologio, ma non viene incrementato a passi di uno, ma in base agli eventi di inizio e fine dei lavori. Il ciclo while quindi sarà attivo fin quando 1) c'è un lavoro nella lista dei lavori da evadere (lista), oppure c'è una macchina occupata (perche bisogna stampare la fine di ogni lavoro, e inoltre, quando un lavoro termina potrebbe passare nell'altra macchina).

Dentro il ciclo usiamo il vettore w[5] per contenere i valori da stampare per una riga, il resto del codice serve a riempire con i valori corretti questo vettore, e nelle righe finali del ciclo (53-55) stampare una riga della tabella.

Pensiamo all'inizio alle macchine libere, allora dobbiamo assegnare dei lavori, gli if che iniziano alla riga 39 e 46 fanno questo, usando la funzione ausiliaria AssegnaLavori. Questa funzione preleva il primo lavoro per la macchina indicata dalla lista dei lavori e lo assegna alla macchina, scrivendo dentro il lavoro il tempo di inizio T, inoltre se inizia un lavoro, abbiamo uno o due degli elementi della tabella (w[S1], o w[S2], usiamo un enum per evitare di usare indici, dove S1,S2, indicano i tempi di partenza, e E1, E2 i tempi di fine).

I primi due if, linee 17,18, controllano se vi sono dei lavori già assegnati e calcolano il minimo tra i tempi di completamento dei lavori in corso, saltando direttamente a quel tempo, in modo che ad ogni ciclo, si finisce un lavoro e se ne inizia un altro se disponibile. La condizione alla riga 22, è vera se c'è un lavoro completato: se era assegnato alla prima macchina dobbiamo verificare se ha un tempo di lavorazione per la seconda macchina, in caso positivo, dobbiamo re-inserirlo nella lista dei lavori da svolgere, azzerando il tempo per la prima macchina già svolto, inoltre segnaliamo dentro w[E1] che abbiamo completato il lavoro. Nel caso della seconda macchina invece, se si completa un lavoro dobbiamo solo aggiornare w[E2] poichè di quel lavoro non ci dobbiamo più preoccupare. Notiamo che con l'inserimento in lista, nel caso di lavori completati per la prima macchina, dobbiamo rispettare l'ordine cioè inserirlo nella testa della lista, compito svolto dalla funzione insert.

Ecco le funzioni mancanti:

Work *insert(Work *lista, Work *new)
{
	new->next = new->prev = NULL;
	if (!isempty(lista)) {
		new->next = lista;
		lista->prev = new;
	}
	return new;
}


Work *delete(Work *testa, Work *l)
{
	if (l->prev) l->prev->next = l->next;
	if (l->next) l->next->prev = l->prev;
	if (l->prev == NULL && l->next == NULL && l == testa) 
		return NULL;
	if (l == testa && l->next != NULL)
		return l->next;
	return testa;
}

void print(Work *lista) 
{
	while (!isempty(lista)) {
		printf("L%d %d %d\n",lista->num, lista->t1, lista->t2);
		lista = lista->next;
	}
}

Work *AssegnaLavoro(Work **h, int macchina)
{
	Work *l = *h;
	while (!isempty(l)) {
		if (macchina == 1 && l->t1 != 0) {
			*h = delete(*h,l);
			return l;
		}
		else if (macchina == 2 && l->t1 == 0 && l->t2 != 0) {
			*h = delete(*h, l);
			return l;
		}
		else l = l->next;
	}
	return NULL;
}

La funzione AssegnaLavoro estrae dalla lista un lavoro che abbia tempo di lavorazione per la macchina indicata, prendendo il primo nell'ordine della lista. Notate l'operazione delete per rimuovere un elemento dalla lista lavori, l'implementazione passa per riferimento la testa della lista in modo da poter modificare il contenuto anche nel chiamante (nel caso in cui la lista diventi vuota). Questa scelta è stata obbligata dall'interfaccia di AssegnaLavoro, il codominio di quest'ultima infatti non è una lista, ma un lavoro da assegnare.

Mettendo in ordine adeguato le funzioni, si ottiene il codice complessivo della soluzione. Ricordando che non si può chiamare una funzione senza averla prima dichiarata tramite un prototipo. Complessivamente le funzioni che lavorano sulla lista dei lavori sono del tutto standard, le due funzioni specifiche al compito sono la AssegnaLavori e la stampa della tabella.

Lascia un commento