Esercitazioni per il laboratorio di Informatica e Sistemi
a cura del prof. Francesco Pecoraro
Esercizi di programmazione in linguaggio C: scrivere codice compatto
Premessa
Il linguaggio C possiede poche istruzioni, ma molti operatori e molte funzioni.

Alcuni degli operatori corrispondono direttamente a un'istruzione macchina. Inoltre sono ammesse espressioni che permettono al compilatore di produrre un codice ottimizzato.


Esempi:
gli operatori di autoincremento e autodecremento hanno la corrispondente istruzione assembler: INC e DEC.
In un comune linguaggio di programmazione per incrementare una variabile bisogna usare un espressione come: i = i + 1, che il compilatore potrebbe tradurre con una sequenza di istruzioni come le seguenti:
MOV AX, valore della variabile i
ADD AX, 1
MOV variabile i, AX
Mentre in linguaggio C il compilatore traduce direttamente: INC variabile i
In questo modo il programma è più compatto, ma anche più veloce. Infatti l'istruzione INC può essere lunga anche solo un byte (se l'operando si trova in un registro) e viene eseguita con il minimo numero di cicli di clock.


Tra le espressioni che il compilatore riconosce e sfrutta per ottimizzare il codice vi è l'assegnazione multipla.
Normalmente l'inizializzazione di variabili comporta un lungo elenco di assegnazioni del tipo:
a = 0
b = 0
c = 0
In linguaggio C, poichè l'operatore di assegnazione si comporta come una funzione, cioè restituisce il risultato dell'espressione a secondo membro, allora sono ammesse espressioni della forma:
a = b = c = 0
Anche in questo caso il compilatore produce una traduzione più compatta, della forma:
MOV AX, 0
MOV variabile a, AX
MOV variabile b, AX
MOV variabile c, AX
In ciascuna istruzione un operando è un registro contenente il valore da assegnare alle variabili, quindi le istruzioni sono lunghe al massimo 2 byte. In un altro linguaggio di programmazione, invece, il compilatore avrebbe prodotto istruzioni contenenti ciascuna lo stesso valore dell'operando, e quindi lunghe anche 3 byte.

Operatore ?:
Un'istruzione di diramazione if che ad una stessa variabile, assegna in un caso il risultato di un'espressione e nell'altro caso il risultato di un'altra espressione:
if (a<10) sconto = 0.05;
  else sconto = 0.10;

in linguaggio C viene tradotta con l'operatore ? : nel modo seguente:
sconto = (a<10) ? 0.05 : 0.10;

Esempio. Si supponga di poter applicare uno tra tre possibili sconti, a seconda della quantità di articoli acquistati.
La soluzione generale consiste nell'usare una serie di if … else if …
if (quantita>50) Totale = quantita*PrezzoUnitario*(1.0-ScontoMax);
  else if (quantita>20) Totale = quantita*PrezzoUnitario*(1.0-ScontoMed);
    else if (quantita>10) Totale = quantita*PrezzoUnitario*(1.0-ScontoMin);
      else Totale = quantita*PrezzoUnitario;



Si mostra la soluzione in C, che fa uso dell'operatore ?:

Preparare il file input.txt contenente le righe:
2.50 3
4.20 15
8.30 25
5.20 60
in cui su ogni riga vi è una coppia di valori separati da uno spazio: il primo valore è l'importo unitario, il secondo valore è la quantità

Il programma C:

Commenti:
  1. Si dichiara un puntatore a file.
  2. la funzione main
  3. si apre in lettura il file input.txt
  4. si dichiara la variabile di tipo double acquisita da input
  5. si dichiarano variabili di tipo double
  6. che in effetti però, sono costanti
  7. contenenti le fasce di sconto
  8. si dichiara la variabile di output Totale
  9. si dichiara la variabile di input quantità

  10. il ciclo di lettura

    Il ciclo while prosegue fintanto che ci sono dati da leggere dal file.
    Si legge un double, specificando il formato %lf, poi si legge lo spazio, e lo si scarta, infine si legge la quantità
    Al termine del ciclo si chiude il file.

    nelle righe da 15 a 19 inserire le seguenti istruzioni, che calcolano il totale, a seconda dello sconto da applicare:

Dichiarazioni
Una dichiarazione in linguaggio C contiene il tipo della variabile e, in un'espressione contenente alcuni operatori, l'identificatore della variabile.

Gli operatori usati in una dichiarazione sono:
OperatoreSignificato
*è un puntatore a …
()è una funzione che restituisce un …
[]è un array di …



Esempi:
int x;x è un intero.
int *x;x è un puntatore a intero.
int x[];x è un array di interi.
int x();x è una funzione che restituisce un intero.
int *x();x è una funzione che restituisce un puntatore a intero.
Nell'ultima dichiarazione sono presenti due operatori. L'interpretazione della dichiarazione si ottiene applicando le regole di precedenza degli operatori. In questo caso le parentesi hanno la precedenza sull'asterisco.


Si supponga di avere un array di puntatori a stringhe:
x[0]Napoli
x[1]Roma
x[2]Torino
Tale array viene elaborato da una funzione che ritorna il puntatore all'array. La dichiarazione completa è:
f è una funzione che restituisce un puntatore ad un array di puntatori a char.
La dichiarazione che si vuole costruire quindi viene ottenuta dalle specifiche individuando gli operatori presenti:
  1. f è una funzione che restituisce un
  2. puntatore ad un
  3. array di
  4. puntatori a
  5. char.
L'espressione al punto 1 si scrive: f(),
al punto 2 diventa: *f(),
Al punto 3 bisogna introdurre le parentesi quadre: *f()[], Però potrebbe sorgere un dubbio: si tratta di un array di funzioni che restituiscono un puntatore a char o si tratta di una funzione che restituisce un array di puntatori a char?
Per evitare l'ambiguità, come in un'espressione algebrica, si racchiude tra parentesi l'espressione da sviluppare per prima, quindi: (*f())[],
Al punto 4 la dichiarazione richiede un asterisco: *(*f())[],
Infine, al punto 5 l'espressione completa è: char *(*f())[],

Puntatore a funzione
Si scrivano tre semplici funzioni che ricevono lo stesso numero e tipo di parametri e restituiscono lo stesso tipo di parametro.
Ad esempio una funzione che restituisce la somma dei due parametri ricevuti,


un'altra restituisce il prodotto tra i due parametri


e un'altra restituisce la differenza tra i due parametri:


L'uso di un puntatore al posto di un nome ha il vantaggio di poter calcolare al momento dell'esecuzione del programma la funzione da richiamare.
Nelle righe 1-3 si dichiarano le variabili e si assegnano i valori iniziali a quelle di input.

Nella riga 4 si dichiara che pfun è un puntatore a una funzione che riceve due interi.

Nella riga 5 (e similmente nelle righe 8 e 11) si deve notare che il nome di funzione viene usato senza le parentesi tonde. In questo caso, il compilatore usa l'indirizzo della funzione. Quindi l'espressione pfun = Somma significa che a pfun si assegna il puntatore alla funzione Somma.

Nelle righe 6, 9 e 12 viene richiamata una funzione tramite il puntatore.


La modifica seguente consente di dichiarare un array di puntatori a funzione e, quindi, richiamare le funzioni in un ciclo:



I tre puntatori a funzione possono anche essere usati in espressioni. Ad esempio per ottenere il prodotto tra la somma e la differenza dei due valori:
Risultato = pfun[1](pfun[0](a, b), pfun[2](a, b));


Come ultima variante si dichiara una funzione che riceve il puntatore ad una funzione:


e viene usata in un'espressione nel modo seguente (sostituire le righe da 4 alla fine):

Lista concatenata
Una lista è un insieme di elementi, detti anche nodi, composti ciascuno da una parte contenente le informazioni e da una parte indicante qual è l'elemento che segue. L'elemento successivo viene indicato tramite il puntatore.

Le informazioni aggregate vengono memorizzate in un array, ma si ricorre ad una lista quando il numero di elementi da memorizzare non deve essere limitato dalla dimensione dell'array.

L'accesso ad un elemento di array avviene specificando l'indice dell'elemento, mentre l'accesso ad un elemento di una lista avviene attraversando tutti gli elementi che lo precedono nella lista.

la scansione degli elementi di una lista avviene partendo dal puntatore al primo elemento della lista. Si passa da un elemento a quello successivo leggendo il campo pun dell'elemento puntato da p e, per mantenere una maniera ricorsiva, il puntatore letto viene assegnato ancora alla stessa variabile p:
p ← pun(p)
Ad esempio il primo elemento sia memorizzato all'indirizzo 100, per sapere dove si trova l'elemento successivo si deve leggere il campo pun. Il valore 80 in questo campo indica l'indirizzo del secondo elemento.


Costruzione di una lista.
Un generico elemento della lista, nella sua forma più semplice contiene il campo Info e il campo pun

Il campo Info contiene tutte le informazioni che descrivono l'elemento (nell'esempio ci si è limitati a un intero), il campo pun contiene l'indirizzo dell'elemento successivo. Esso è dichiarato come il puntatore ad una struttura Nodo.

La procedura di inserimento di un nodo in una lista
Un elemento potrebbe venire inserito in una lista in base ad un ordine, nel qual caso bisogna scandire la lista fino a raggiungere l'elemento che lo deve seguire nell'ordine stabilito, ed inserire l'elemento tra il suo successivo e il suo precedente.

Nell'esempio il nuovo elemento da inserire si trova all'indirizzo 60. Dopo la scansione, ad esempio, si trova che deve essere inserito tra l'elemento all'indirizzo 100 e quello all'indirizzo 80.
Cioè nel campo pun dell'elemento da inserire si scrive 80 (il puntatore contenuto nell'elemento che lo precede) e nel campo pun dell'elemento che lo precede si scrive 60: l'indirizzo dell'elemento da inserire.


Coda bidirezionale
Una lista bidirezionale consente di avanzare nella lista, ma anche di invertire la direzione di scansione.

Ogni nodo di una lista bidirezionale possiede due campi puntatore: un puntatore a sinistra (ps) e un puntatore a destra (pd).
Per una lista bidirezionale conviene creare un nodo capolista. Inizialmente i campi puntatore del nodo capolista puntano al nodo stesso.

il nodo capolista

Nell'esempio che si propone i nodi vengono inseriti ipotizzando che il campo tempo rappresenti l'orario in cui svolgere una certa attività e il campo ident rappresenti il codice dell'attività.
Per inserire un nodo nella lista;
Si scandisce la lista alla ricerca del nodo che lo precede in ordine cronologico,
Per estrarre un nodo nella lista;
Si preleva il nodo a destra del nodo capolista.
Cioè per l'inserimento si accede ai nodi consideradoli in una lista, mentre per l'estrazione si accede ai nodi come se formassero una coda.

Nella figura seguente si deve inserire il nodo puntato da p tra il nodo puntato da ptmp e il nodo puntato da prec.


L'operazione di inserimento e quella di estrazione sono mostrate nel programma seguente:
Si dichiara un record la cui struttura viene denominata Nodo: Possiede due campi dati e due campi puntatore. La variabile primo è un puntatore ad una tale struttura.


La funzione inscrono riceve il parametro p, che è un puntatore ad una struttura, e non restituisce parametri.
Si scandisce la lista a iniziare dal nodo più a destra e si prosegue fino a quando si trova un nodo con valore del tempo minore di quello da inserire.
Nei campi puntatore del nodo da inserire si scrivono gli indirizzi dei nodi laterali, mentre nei campi puntatore dei nodi laterali si scrive l'indirizzo del nodo da inserire.


La funzione che preleva un nodo dalla lista, acquisisce l'indirizzo del nodo a destra del capolista. Se legge lo stesso valore significa che la lista è vuota.
i campi puntatore laterali del nodo di testa venogono scritti nel campo puntatore dei nodi laterali, in modo da escludere questo nodo dalla concatenazione.
La funzione ritorna il puntatore al nodo estratto.


Nella funzione main si costruisce, dapprima, il nodo capolista, inserendo nel campo tempo un valore di riferimento minimo.


vengono creati 5 nodi, in ognuno dei quali viene assegnato un valore casuale al tempo e un numero crescente alla variabile ident
Per creare i nodi si usa l'operatore new del C++, anzichè la funzione malloc del C. I nodi poi vengono inseriti in ordine cronologico.


Segue una semplice stampa di verifica del corretto inserimento dei nodi nella lista. Si vede infatti che i nodi vengono mostrati in ordine cronologico, anzichè nell'ordine di generazione.


Infine i nodi vengono estratti dalla lista e lo spazio di memoria occupato viene liberato con l'operatore delete del C++, in luogo della funzione free del C.
La stampa permette di verificare che vengono prelevati dalla testa, nell'ordine previsto.