LEZIONE del 22 Novembre

Prosegue la breve presentazione del linguaggio C, iniziata nella precedente lezione, con l'analisi delle caratteristiche sintattiche salienti ed il confronto con analoghe di altri linguaggi imperativi. Gli esempi di programma proposti hanno lo scopo di evidenziarne gli aspetti essenziali della pratica implementativa.

Per maggiori approfondimenti sull'impiego del C come linguaggio di programmazione e d'implementazione di sistema si rimanda ai molti manuali disponibili sull'argomento sia cartacei che on-line.

Introduzione al C: seconda parte

La caratteristica fondamentale del linguaggio C è la sua capacità di essere al tempo stesso un linguaggio strutturato, del tutto simile al Pascal ed agli altri linguaggi della famiglia dell'ALGOL, cui appartiene e, al tempo stesso, di poter entrare facilmente nel cuore del sistema (kernel). Ad esempio, il segmento di codice riportato nel seguito


#define N 15

int i, *p;

 p = &i;
*p = N;

if (i == N) printf("ok %d\n", i);
Indirizzi e Variabili


si vede chiaramente qual'è il tipo di meccanismo utilizzato nel C per il trattamento degli indirizzi, o puntatori. I due operatori * e & sono complementari e hanno il significato

*ptr oggetto puntato da ptr
&val indirizzo di val

Il codice seguente implementa un record a due campi, il secondo dei quali contiene l'indirizzo del record successivo e può essere utilizzato per implementare liste di oggetti che, nel caso specifico, sono valori interi.


struct S {
  int a;
  struct S *next;
};
Liste


Per quanto riguarda, invece, l'accesso ai singoli campi del record sono possibili due modalità,

avendo supposto le seguenti dichiarazioni


typedef struct {
  char *name;
  int  val;
} userinfo;

struct info userinfo;

struct user *userinfo;
Record


Non solo il C permette la massima libertà di accesso alla memoria RAM contenente segmenti di dati ma consente, anche, l'accesso all'area del codice con un meccanismo di riferimento che si presenta con modalità inusuale rispetto all'impiego dei puntatori per accedere ai dati.

Si può certamente affermare che i puntatori alle funzioni rappresentano uno degli impieghi più fuorvianti del C e, apparentemente, di scarso ed dubbio utilizzo. Tuttavia il vantaggio di permettere il passaggio di un puntatore ad una funzione come parametro della chiamata di funzione è innegabile. Tale capacità è sicuramente apprezzabile quando si presenta la necessità di impiegare funzioni alternative per eseguire azioni similari sui dati.

La dichiarazione di un puntatore ad una funzione si presenta con uno schema analogo a quello riportato di seguito

int (*pfun) ();

il cui effetto è quello di definire il puntatore *pfun alla funzione che ritorna un valore intero. Nessuna funzione è comunque realmente definita.

Se la funzione è definita con il prototipo int fun() , allora si deve scrivere

pfun = &fun

Tuttavia, per consentire al compilatore di eseguire correttamente la compilazione è meglio definire contemporamente il prototipo della funzione e il puntatore ad essa:

int fun(int);
int (*pfun) (int) = &fun;

Ulteriori applicazioni relative all'impiego dei puntatori alle funzioni verranno discusse nelle prossime lezioni.

Impiego della Memoria Dinamica

L'allocazione dinamica della memoria permette di gestire con assoluta libertà tutte le strutture dinamiche costruibili con i puntatori, tipicamente, liste, alberi e grafi. Lo stesso effetto può ottenersi anche con l'allocazione statica della memoria, ottenuta con l'impiego di array. Tuttavia, in questo caso è necessario gestire esplicitamente lo spazio libero della memoria così allocata.

Nel caso dinamico, grazie all'impiego della funzione malloc è possibile una maggiore flessibilità, come nell'esempio discusso nel seguito.


/*
    Handling a queue by pointers
*/

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

#define   FALSE  0
#define   NIL    0

typedef struct {
          int Val;
          struct Queue *next;
        } Queue ;

Queue *Insert(Queue *Qu, int K)
{
    Queue *Qptr;

    Qptr = Qu;

    if (Qu != NIL) {
      while (Qu->next != NIL)
          Qu = Qu->next;
        Qu->next = (struct Queue *)malloc(sizeof(Queue));
        Qu = Qu->next;

	Qu->next = NIL;
        Qu->Val = K;
	return Qptr;
     }
     else {
        Qu = (struct Queue *)malloc(sizeof(Queue));
        Qu->next = NIL;
        Qu->Val = K;
        return Qu;
    }
}

Queue *Remove (Queue *Qu, int *K)
{
    Queue * Qptr;
    *K = Qu -> Val;
    Qptr = Qu -> next;
    free (Qu);
    return Qptr;
}

void Print (Queue *Qu)
{
    if (Qu == NIL)
        printf ("queue is empty!\n *** Queue = <");
    else {
        printf(" *** Queue = <");
        while (Qu != NIL) {
            printf("%d", Qu -> Val);
            Qu = Qu -> next;
	    if (Qu != NIL) printf("; ");
        }
    }
    printf (">\n\n");
}

void Clear (Queue * Qu)
{
    int i, N;
    i = 0;

    while (Qu != NIL) {
        Qu = Remove (Qu, &N);
        i++;
        printf ("Queue[%d] = %d removed\n", i, N);
    }
}

int main(int argc, char **argv)
{
  int N, k, data;
  Queue *Info;

  Info = NIL;

  N = argc;
  for (k=1; k<N; k++) {
     data = atoi(argv[k]);
     Info = Insert(Info, data);
  }
  Print(Info);
  Clear(Info);
  exit(0);
} 
Coda dinamica


Si noti, inoltre, l'impiego della funzione di libreria free il cui scopo è quello di recuperare lo spazio di memoria precedentemente allocato da malloc e riusabile in successive chiamate della stessa.

Il programma more

Il programma che segue ha un comportamento del tutto analogo al programma Copy, discusso nella lezione precedente, al quale si aggiunge un controllo dell'output in modo da produrre lo stesso file diviso per gruppi di linee, ciascuno contenuto in una "schermata" e permetterne, quindi, una facile lettura sullo schermo. Nella particolare implementazione presentata, il programma appare suddiviso in più file con l'uso appropriato dei necessari include.

Il programma principale


Contiene il ciclo principale e la chiamata alla procedura more che è quella responsabile del comportamento di cui si è detto.


/*
* more
*      main subroutine - copy input to output
*/

#include  <stdio.h>

int main()
{
  int ch, colcnt, rowcnt;

  colcnt = 1;
  rowcnt = 1;
  while ( (ch = getchar()) != EOF )
    {
       putchar(ch);
       if ( more(ch, &colcnt, &rowcnt) == 0 )
       {
          break;
       }
    }
    exit(0);
}
Programma principale


Si noti, rispetto al programma Copy, la presenza della chiamata alla funzione more per la gestione del controllo dello stream di output sullo schermo.

La funzione more


Definisce il tipo di operazioni che devono essere effettuate sul file di output in base al valore corrente del numero di colonne colcnt e di righe rowcnt.


/*
* more
*      more subroutine - controls command prompting
*      base on screen size
*/

#include  "defs.h"

int more(int ch, int *colcnt, int *rowcnt)
{
  int Value;  /* returned by this function  */

  if ( ch == '\n' || *colcnt >= LINESIZE )
    {
        *colcnt = 1;
        (*rowcnt)++;
    } else
    {
        (*colcnt)++;
    }
  if ( *rowcnt == SCRNSIZE )
       {
           Value = command(rowcnt);
       } else
       {
           Value = 1;
       }
    return(Value);
}
La funzione more


La funzione command


La funzione command gestisce il corretto funzionamento del programma, pilotando opportunamente la funzione more.


/*
* more
*      command subroutine - prompts for command
*/

#include  <stdio.h>
#include  "defs.h"

int command(int *rowcnt)
{
  static FILE *fp = NIL;
  int Ch, Value;

  if ( fp == NULL )
    {
        fp = fopen("/dev/tty", "r");
    }
  printf("more?");
  fflush(stdout);
  if ( (Ch = getc(fp)) == ONELINE )
       {
           *rowcnt = SCRNSIZE - 1;
           Value = 1;
       } else if ( Ch == ONESCREEN )
          {
             *rowcnt = 0;
             Value = 1;
          } else
          {
              Value = 0;
          }
   while ( Ch != EOF && Ch != '\n' )
     {
        Ch = getc(fp);
     }
    return(Value);
}
La funzione command


Le definizioni delle costanti impiegate


Sono definite alcune delle costanti impiegate il cui valore non è mai stato precedentemente assegnato.


/*
* more
*      definitions
*/

#define       SCRNSIZE       23
#define       LINESIZE       80
#define       ONELINE        '\n'
#define       ONESCREEN      ' '
Definizioni


Esempio di Makefile per costruire l'eseguibile


Il makefile è un tipo di utility Unix che serve a costruire e mantenere software, tipicamente ma non necessariamente scritto in C, che si basa su alcune regole implicite, come quella dell'aggiornamento della compilazione dei file sorgenti che appaiono essere stati modificati.

L'impiego del makefile è tipico quando l'implementazione è distribuita su un certo numero di file sorgenti che possono contenere sia codice C, o di altro linguaggio, che intestazioni. Generalmente il makefile compare all'interno di un archivio compresso contenente i file sorgenti di un pacchetto di software da compilare. Nell'esempio in questione tale pacchetto, noto anche come tarball, può essere scaricato dal link indicato in un proprio direttorio di lavoro. A questo punto, eseguendo il comando
   
tar zxvf more.tar.gz
si trova che è stata creata la directory more, contenente un certo numero di file sorgenti C fra cui anche makefile. Se si lancia il comando make verrà automaticamente generato in "more" l'eseguibile More che può essere lanciato come
   
More < textfile
con il quale si noterà il classico comportamento del comando more di UNIX. In questo caso il file di testo deve essere passato con la ridirezione dell'input.

Come nota conclusiva si dia un'occhiata al "makefile" nel caso semplice appena discusso. Tuttavia, per una migliore comprensione si consiglia di consultare il manuale il linea "man make" per il comando make.


#
# makefile per costruire il file eseguibile More
#
# Seguono le definizioni di alcune macro
#
CC = gcc
SRC = main.c more.c command.c
DEF = defs.h
OBJ = $(SRC:.c=.o)

# La macro $@ prende il nome del target corrente More

More:	$(OBJ)
	$(CC) $(OBJ) -o $@	
.c.o:
	$(CC) -c $<

# Si puo' utilizzare anche la dipendenza implicita dei target,
# dettata dal suffisso .c, cosicche' e' sufficiente scrivere 
# solamente le regole dove compare la dipendenza esplicita da
# "defs.h"

more.o:	$(DEF)
command.o: $(DEF)


clean:	
	/bin/rm -f $(OBJ) More *~
Makefile


Non è, comunque, necessario comprendere in dettaglio il funzionamento di tale comando ma, più semplicemente, farsi un'idea della gestione del software in ambiente UNIX e provare a realizzare i programmi proposti nel modo indicato.