LEZIONE del 19 Novembre

Inizia da oggi per continuare nelle lezioni successive, la presentazione del linguaggio C del quale, però, verrà data solamente una breve introduzione senza entrare nel dettaglio delle sue strutture dati e dei diversi costrutti di controllo che lo caratterizzano. Si procederà, invece, il più possibile per esempi avendo cura di fare riferimento a quegli aspetti che più di altri lo contraddistinguono come linguaggio di implementazione di sistema. Per una trattazione esauriente degli argomenti si rimanda ai molti manuali disponibili sia in formato cartaceo che on-line come quello indicato.

Introduzione al C: prima parte

Il linguaggio C, al pari di molti linguaggi del suo gruppo, evolve dall'ALGOL come linguaggio di programmazione strutturata. Si presenta molto simile al PASCAL del quale mostra la stessa capacità di sviluppare programmi in forma altamente strutturata ma con caratteristiche di flessibilità maggiore per quanta riguarda gli accessi in memoria e l'utilizzo diretto ed immediato dei puntatori come indirizzi.

Il programmatore C, comunque, deve stare molto attento alle libertà che tale linguaggio gli permette per la maggiore facilità a commettere errori. Naturalmente c'è un uso molto flessibile dell'input/output e di tutte le risorse del sistema, direttamente accessibili mediante appropriate librerie.

In realtà, essendo il C linguaggio di implementazione dei sistemi per eccellenza, questa sua caratteristica favorisce una forte strutturazione dei programmi che si lasciano facilmente dividere in molti file, spesso di dimensioni ridotte la cui compilazione seperata in moduli oggetto permette di ottenere l'eseguibile finale attraverso un meccanismo generale di combinazione con le appropriate librerie che il sistema mette a disposizione.

Esempi di Programmi

I sottoprogrammi del C sono le funzioni e, fra queste, deve esisterne una che costituisce il programma principale e che, per questo, prende il nome di main. Più in generale si può dire che un programma C è rappresentato da una collezione di definizioni di funzioni, una delle quali deve essere il main, alla quale si aggiungono dichiarazioni di tipo e di variabili, costanti comprese, con regole ben specificate che ne definiscono lo scope.

Le regole non sono rigide come quelle che valgono per i linguaggi orientati agli oggetti ma possono essere utilizzate per ottenere analoghi risultati con l'aiuto delle cosiddette direttive.

I programmi che seguono servono a farsi una prima idea dell'impiego del linguaggio C e del tipo di paradigma che soggiace alla scrittura di tali programmi. Si consiglia l'uso di una piattaforma Unix e di un editor testuale come vi oppure emacs.

COPIA L'INPUT SULL'OUTPUT

Il codice C riportato nel seguito è un semplice esempio di programma che legge il contenuto dello standard input e lo trasferisce sullo standard output. Si noti l'uso della direttiva include necessaria ogni qualvolta si intendono impiegare funzioni di librerie delle quali è sempre necessario conoscere l'intestazione e la definizione dei tipi utilizzati dai parametri che ivi compaiono.

Per la compilazione e la sua esecuzione si può procedere nel modo seguente

gcc -o copy copy.c
copy < copy.c

dove si suppone che copy.c sia il nome del file sorgente mentre copy è il risultato della compilazione e del link editing, cioè il programma eseguibile. La seconda riga esegue il programma Copy prendendo come input il file copy.c.


 
/*
* copy program
*      copy standard input to standard output
*/

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

main()
{
  int ch;
  while ( (ch = getchar()) != EOF )
    {
       putchar(ch);
    }
    exit(0);
}
Il programma Copy


Nel semplice esempio proposto si utilizzano due semplici funzioni di libreria per l'acquisizione dei caratteri dallo standard input e il loro traferimento successivo verso lo standard output.

ESTRAZIONE DELLA RADICE QUADRATA : PRIMA VERSIONE

Nell'esempio precedente si è utilizzato come struttura di controllo principale il ciclo while. Ma questa, com'è noto, non è l'unica possibile perchè, ad esempio, si può impiegare anche un ciclo for.

Nell'esempio proposto nel seguito compare proprio un ciclo for in conseguenza del fatto che è definito il numero di volte con cui eseguire una lista di istruzioni su un insieme di dati. Nello specifico si tratta di numeri interi in ingresso e reali in uscita.


 
/*
* sqrt1.c
*      find out square root of the first N ints
*/

#include <math.h>
#include <stdio.h>
#define  N 25

  main()

  { int i;
    float p;

    printf("\t Number \t Square Root\n\n");

    for (i = 0; i <= N; ++i) {
       p=sqrt(i);
       printf("\t %d \t\t %f \n",i, p); }
  }
Radice Quadrata: prima stesura


Si noti, fra l'altro, l'utilizzo della funzione sqrt per il calcolo della radice quadrata. Poichè il C non ha funzioni predefinite nel linguaggio il loro impiego richiede che sia già stata definita una libreria che le contiene e da cui richiamarla. Ciò è possibile solamente se si dispone dell'opportuna intestazione attraverso la quale il compilatore è in grado di predisporre il corretto passaggio dei parametri. Nel caso in esame le informazioni richieste son contenute nel file math.h, che deve essere incluso nel testo del programma.

ESTRAZIONE DELLA RADICE QUADRATA : SECONDA VERSIONE

Analogo al precedente ma il limite superiore dell'intervallo, su cui calcolare la radice quadrata, viene passato direttamente dalla shell.


 
/*
* sqrt2.c
*      find out square root of an given initial 
*      integer range
*/

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

  int main(int argc, char **argv)

  { int i; float p; long MaxI;

    if (argc < 2) {
         perror("insufficient parameter");
         exit(1); }

    printf("\t Number Square Root\n\n");

    MaxI = atoi(argv[1]);
    for (i = 0; i <= MaxI; ++i) {
       p=sqrt(i);
       printf("\t %d \t %f \n", i, p); }

    exit(0);
  }
Radice Quadrata: seconda stesura


Si noti la presenza delle variabili predefinite argc e argc che hanno il compito di passare i parametri dalla shell al programma chiamato e mandato in esecuzione come processo. Tali parametri sono la controparte in C delle variabili predefinite di shell $# e $*.

ESTRAZIONE DELLA RADICE QUADRATA : TERZA VERSIONE

Nel nuovo programma proposto vengono passati come parametri sia il limite inferiore che quello superiore dell'intervallo di interi su cui calcolare la radice quadrata.


 
/*
* sqrt3.c
*      find out square root of a given integer range
*/

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

  int main(int argc, char **argv)

  { int i; float p;
    int MinI, MaxI;

    if (argc < 3) {
         perror("insufficient parameter");
         exit(1); }

    printf("\t Number Square Root\n\n");

    MinI = atoi(argv[1]);
    MaxI = atoi(argv[2]);
    for (i = MinI; i <= MaxI; ++i) {
       p=sqrt(i);
       printf("\t %d \t %f \n", i, p); }

    exit(0);
  }
Radice Quadrata: terza stesura


Si noti come argv[1] e argv[2] rappresentino, rispettivamente, il primo ed il secondo parametro passato. I corrispondenti della shell sono $1 e $2.

UTILIZZO NEGLI SHELL SCRIPT

Con questo esempio si vuole sottolineare come, a tutti gli effetti, gli eseguibili generati col C secondo le convenzioni appena viste, rendano tali programmi del tutto indistinguibili da un qualunque comando di shell.


 
#!/bin/bash
#  sqroot : square root
#      Usage: sqroot <integer>
#
if [ $# -eq 0 ]
  then
    echo "insufficient parameters"
    exit 1
fi
declare -a T
T=(`./sqrt $1 $1 | grep -v "Number" | sed 1d`)
echo ${T[1]}
exit 0
Radice Quadrata: shell script


A questo punto si capisce facilmente in che modo sono strutturati i cosiddetti comandi primitivi della shell: sono realizzati come eseguibili compilati dal C nel modo che si è detto.

SERIE DI FIBONACCI

Negli esempi precedenti è stata utilizzata una funzione di libreria il cui impiego richiedeva l'inclusione delle necessarie definizioni contenute nel file di intestazione math. Nell'esempio che segue, invece, la funzione è direttamente implementata nel programma.


 
/*
* fibo.c
*      find out the n-th term of the Fibonacci's series
*/

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

  int fib(int N) {
    int i;
    int A, B, C;
    if (N < 2) return N;
    else {
      A = 1;
      B = 1;
      for (i = 2; i < N; i++) {
        C = A + B;
	A = B;
	B = C; }
      return B; }
  }

  int main(int argc, char **argv)
  
  { int i; int p;
    int MinP, MaxP;
    
    if (argc < 3) {
         perror("insufficient parameter");
	 exit(1); }
    
    printf("\t Number Fibonacci\n\n");

    MinP = atoi(argv[1]);
    MaxP = atoi(argv[2]);
    for (i = MinP; i ≤ MaxP; i++) {
       p=fib(i);
       printf("\t %d \t %d \n", i, p); }

    exit(0);
  }
Serie di Fibonacci

Come è facile intuire dalla definizione della funzione fib(N) si tratta della generazione di una sequenza di numeri interi dove ciascuno è la somma degli ultimi due che lo precedono, a partire dai valori iniziali 0 e 1. Si noti come, in questo caso, la valutazione del termine N-esimo non richiede un ciclo while ma, più semplicemente, un ciclo for.

Si noti, infine, l'impiego della funzione exit(N) con N valore intero con significato analogo a quello già visto per il comando di shell exit. Si ottiene, in questo modo, un eseguibile completamente compatibile con gli shell script.

RADICI DI UN POLINOMIO DI SECONDO GRADO

Nell'ultimo esempio proposto, piuttosto che mettere l'accento sull'algoritmo in sè, del resto abbastanza semplice e realizzato da cinque righe di codice, si vuol introdurre alcuni degli aspetti salienti del C e che verranno ripresi nelle prossime lezioni. Come si può notare da una rapida occhiata al programma la maggior parte dello stesso è destinata a fornire una interfaccia adeguata fra l'utente e la routine in questione.

Poichè si è voluto deliberatamente permettere all'utente di introdurre i dati come se stesse ponendo il problema su supporto cartaceo, appare ovvia la necessità di porre la maggior parte dell'attenzione su come realizzare l'estrazione dei dati. Nel caso del C abbiamo a disposizione molte funzioni di libreria su cui basare l'implementazione richiesta e che, in questo esempio specifico si traduce nell'impiego delle funzioni di libreria dello standard input e della manipolazione delle stringhe. Ad esse si aggiungono la gestione degli indirizzi in memoria, compresso l'accesso a dati immagazzinati in sequenza, ossia, gli array.

L'esempio mostra, inoltre, l'utilizzo di funzioni, preventivamente dichiarate, e poi chiamate in cascata. Le regole di scope delle variabili sono quelle note per i linguaggi che definiscono, per ogni funzione, il contesto dell'ambiente su cui operare.


 
/*
* snord.c
*      find out the roots of a second degree equation
*/

#include <stdio.h>
#include <string.h>
#include <math.h>
#include <stdlib.h>
#include <errno.h>

  int a, b, c;

  float Solve(int a, int b, int c, float *p) {
    int Delta;
    float rDelta;
    Delta = b*b - 4*a*c;
    if (Delta < 0) rDelta = -sqrt(-Delta);
    else rDelta = sqrt(Delta);
    *p = -b/(2.0*a);
    return rDelta/(2.0*a); }

  int Signed(char CH) {
  return (CH == '+') || (CH == '-');
  }

  void Banner(int argc, char **argv) {
    int p, i;
    char msg[20];

    printf("Soluzione dell'equazione :\n");
    strcpy(msg, "   ");
    for (i = 1; i < argc; i++) {
      printf("%s ", msg);
      strcpy(msg, argv[i]);
      p = strlen(argv[i]);
      msg[p] = '\0'; }
      msg[p-1] = '\0';
    printf("%s", msg);
    printf(" = 0\n");
  }

  int ChangeSign(char ch) {
    int Sw;
    switch (ch) {
      case '+' :
	Sw = +1;
	break;
      case '-' :
	Sw = -1;
	break; }
#ifdef DEBUG
    printf("|   sign[%c] = %d\n", ch, Sw);
#endif
    return Sw;
  }

  int IsNull(char CH) {
    return CH == ' ';
  }

  void NextParm(char msg[20], int *gs) {
    int n;
    char t[4];
#ifdef DEBUG
    int p;
    p = strlen(msg);
    printf("|   length[%s] = %d\n", msg, p);
#endif
    t[0] = ' ';
    if (Signed(msg[0])) *gs = ChangeSign(msg[0]);
    else {
      sscanf(msg, "%d%[^0-9]", &n, &t);
      if (IsNull(t[0])) {
        sscanf(msg, "%[^0-9]", &t);
	n = 1; }
#ifdef DEBUG
      printf("|   T.val = %d   T.name = %s   T.ini = %d\n", n, t, t[0]);
#endif
      if (t[0] == 'x') {
	 if (t[1] == '^') a = *gs*n; 
	 else b = *gs*n; }
      else c = *gs*n; }
  }

  int main(int argc, char **argv) { 
    int i;
    int Si;
    char msg[20];
    float Rx, Ry;

    if (argc == 1) {
      printf("enter equation in the form ax^2 + bx + c.\n");
      printf("   where: a, b, c are given signed integers\n");
      exit(1);
    }

    Banner(argc, argv);
    Si = 1;
#ifdef DEBUG
    printf("\n");
    printf("-------------------------------------------------\n");
    printf("                Debug Information\n");
    printf("-------------------------------------------------\n");
#endif
    for (i = 1; i < argc; i++) {
      strcpy(msg, argv[i]);
      NextParm(msg, &Si); }
#ifdef DEBUG
    printf("|   a = %d, b = %d, c = %d\n", a, b, c);
    printf("-------------------------------------------------\n\n");
#endif
    Ry = Solve(a, b, c, &Rx);
    printf("Le radici sono : \n");
    if (Ry < 0) {
      printf("     x1 = %f - i%f\n", Rx, -Ry);
      printf("     x2 = %f + i%f\n", Rx, -Ry); }
    else {
      printf("     x1 = %f\n", Rx - Ry);
      printf("     x2 = %f\n", Rx + Ry); }
    exit(0);
  }
Radici di un Polinomio di secondo grado


Sebbene l'argomento verrà ripreso nel seguito, si noti l'impiego dell'operatore unario &, che permette di determinare l'indirizzo base di ogni variabile precedentemente definita, e il suo duale * che, viceversa, permette di accedere al contenuto di una variabile, definita come un indirizzo (puntatore). Il C, per sua natura, non opera una divisione netta fra spazio delle variabili e spazio degli indirizzi, evitando di trasformarli in variabili di tipo puntatore.

Si noti anche l'utilizzo degli array e delle istruzioni di selezione switch. Poichè è stata utilizzata la funzione di libreria sscanf per l'acquisizione dell'input si osservi l'impiego di espressioni regolari per istruire la funzione stessa alla conversione delle varie componenti in interi, caratteri, stringhe, ecc... Un'ulteriore caratteristica del C è quella dell'impiego delle direttive di compilazione come #define oppure #ifedf, quest'ultima molto utile per scrivere codice adatto per diverse situazione. Nel caso specifico, la seconda direttiva è stata utilizzata per generare codice con azioni di debugging o meno. Questo si ottiene scivendo la riga di comando

gcc -o snord -DDEBUG -lm snord.c

nel primo, e

gcc -o snord -lm snord.c

nel secondo caso.