LEZIONE del 20 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).

Trattamento degli Indirizzi

Il primo aspetto del C su cui porre attenzione ` quello evidenziato dal semplice programma riportato nel seguito


/*
  pointer: test the pointer access into memory
*/

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

  int main(int argc, char **argv) {

     int N;
     int i, *p;

     if (argc < 2) {
       printf("Insufficient Parameters\n");
       printf("Usage:  pointer <int>\n");
       exit(3);
     }

     N = atoi(argv[1]);
     p = &i;
     *p = N;

     if (i == N) {
       printf("ok %d\n", i);
       exit(1);}
     else {
       exit(2);}
  }
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

Nell'esempio che segue si mostra l'utilizzo standard degli operatori unari * e &; il primo usato per definire un parametro by reference mentre il secondo utilizzato come parametro attuale nella chiamata


/*
  swap: swap two numbers
*/

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

  void Swap(int *p, int *q) {
    int T;
    T = *p; *p = *q; *q = T;
  }


  int main(int argc, char **argv) {

     int N1, N2;

     if (argc < 3) {
       printf("Insufficient Parameters\n");
       printf("Usage:  swap <int1> <int2>\n");
       exit(1);
     }

     N1 = atoi(argv[1]);
     N2 = atoi(argv[2]);
     printf("<%d, %d> swapped into ", N1, N2);
     Swap( &N1, &N2);
     printf("<%d, %d>\n", N1, N2);
  }
Swapping di due variabili


Si osservi come sia necessario usare gli indirizzi delle variabili N1 e N2 se si vuole che le modifiche apportate sopravvivano alla chiamata della funzione Swap.

Strutture a Record

L'aggregazione di più variabili, possibilmente di tipo diverso, in una entità organizzata di livello superiore è reso possibile dal costrutto generale struct. 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

Si noti, fra l'altro, l'impiego della parola chiave typedef per introdurre una nuova struttura dati dopodichè il compilatore sostituisce ogni occorrenza del nuovo tipo con la sua definizione.

Riferimenti all'Area del Codice

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>
#include      <stdlib.h>
#include      <string.h>
#include      <error.h>

#define       NAMESIZE  40

int main(int argc, char ** argv) {

  int ch, colcnt, rowcnt;
  char msg[NAMESIZE];
  static FILE *fin;

  if (argc < 2) {
    printf("Insufficient Parameter\n");
    printf("Usage: More <textfile>\n");
    exit(1); }

  strcpy(msg, argv[1]);
  fin = fopen(msg, "r");
  colcnt = 1;
  rowcnt = 1;
  while ( (ch = getc(fin)) != EOF ) {
    putchar(ch);
    if ( more(ch, &colcnt, &rowcnt) == 0 ) {
      break; }
  }
  fclose(fin);
  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; il file di testo, analogamente al comando originale, deve essere passato come primo ed unico argomento. Si noterà, fra l'altro, la non completa aderenza di More ai comandi della shell, in quanto non è in grado di ricevere input tramite pipe.

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.