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.
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 |
*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 |
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
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
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.
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 |
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.
/* * 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 |
/* * 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 |
/* * 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 |
/* * more * definitions */ #define SCRNSIZE 23 #define LINESIZE 80 #define ONELINE '\n' #define ONESCREEN ' ' |
Definizioni |
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 |
More < textfile |
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 |