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.
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.
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.
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> 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.
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.
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"); } 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 $*.
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"); } 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.
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.
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.
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.