C C
Introduzione al linguaggio
Ugo de’Liguoro
Storia e bibliografia
Il C evolve dal BCPL e dal B. E’stato implementato da Dennis Ritchie sul sistema operativo UNIX per il PDP-11.
Nel 1973 Ritchie e Thompson hanno riscritto il nucleo di UNIX in C; da allora il compilatore C è nativo nel sistema UNIX, e tutte le estensioni e le utility del sistema sono scritte in C.
Nel 1983 un comitato ha definito una versione non ambigua del linguaggio tuttoggi in uso: l’ANSI C.
B. W. Kernighan, D. M. Ritchie, The C programming language (trad. it. Linguaggio C, Jackson Libri)
Deitel & Deitel, C Corso completo di programmazione, Apogeo.
Il C è un linguaggio imperativo:
• strutturato, ma senza gerarchia di funzioni
• tipato, ma con type casting
• consente operazioni a basso livello
Il C ha dunque solo alcune caratteristiche dei linguaggi di alto livello, ma in realtà è piuttosto vicino alla macchina, adatto per programmare anche a livello molto basso,
controllando indirizzi e singoli bit.
/* Programma per il calcolo del MCD */
#include <stdio.h>
void main(void) {
int a, b;
a = b = 0;
while (((a == 0) && (b == 0)) || (a < 0) || (b < 0)) {
printf("Inserire due interi positivi non entrambi nulli\n");
scanf("%d %d", &a, &b);
}
printf("MCD(%d,%d) = %d\n", a, b, MCD(a,b));
}
int MCD (int a, int b) {
int c;
if (b == 0){c = b; b = a; a = c;}
while (b > 0) {
c = b; b = a % b; a = c;
}
return a;
}
Un esempio di programma in C
Il file “stdio.h”
Il C non possiede primitive per l’I/O; queste sono definite nel modulo “Standard I/O”; le dichiarazioni dei nomi e dei tipi delle funzioni sono contenute in un file di intestazione (header, .h) che viene importato dal comando per il preprocessore:
#include <stdio.h>
Tra le funzioni definite in stdio.h vi sono:
• printf funzione di stampa (PASCAL: write)
• scanf funzione di lettura (PASCAL: read)
# è il simbolo con cui cominciano i comandi del preprocessore
i comandi del preprocessore non sono terminati da ;
La funzione “main”
La funzione main è la parte principale del programma: la sua esecuzione coincide con quella del programma.
void main (void)
Indica che la funzione main non ritorna (in questo caso) alcun valore
Indica che la funzione main non ha (in questo caso) argomenti
La dichiarazione delle variabili;
Le variabili si dichiarano indicando prima il tipo e poi il nome degli identificatori:
int a, b; (PASCAL: var a, b: integer;) Se una variabile viene dichiarata all’interno di un blocco:
{...}
allora è locale (visibile in quel blocco); altrimenti è globale.
Nota: le variabili dichiarate nel blocco che segue il main sono
locali nel main, non globali.
Le strutture di controllo “while” e “if”
La sintassi del while e dell’if è simile al Pascal, tuttavia con qualche differenza:
while(condizione) istruzione;
if (condizione) istruzione_1;
else
istruzione_2;
while condizione do istruzione ;
if condizione then istruzione_1
else
istruzione_2 ;
C Pascal
Il comando “return”
Il C non ha la distinzione tra funzioni e procedure: tutti i sottoprogrammi parametrizzati sono funzioni.
Tuttavia tra le funzioni vi sono quelle che ritornano un valore (e che si possono dunque impiegare come espressioni):
return espressione ;
Nota: l’espressione deve avere lo stesso tipo del valore di ritorno della funzione, ad esempio la funzione mcd deve ritornare interi:
int mcd (…)
Le funzioni che non ritornano valori hanno tipo void.
Il comando return interrompe il flusso nella funzione.
Struttura di un (semplice) programma C
/* commenti: nome del programma, sue funzionalità ecc. */
istruzioni per il preprocessore
dichiarazione di tipi, variabili, costanti tipo di ritorno main (elenco argomenti) {
dichiarazione variabili locali sequenza di istruzioni
}
tipo di ritorno funzione_1 (elenco argomenti) {
dichiarazione variabili locali sequenza di istruzioni
}
….
tipo di ritorno funzione_n (elenco argomenti) { … }
begin
end
blocco
programma principale: main
sottoprogrammi
Le funzioni
Sintassi per la dichiarazione di una funzione:
tipo_valore_ritornato nome_funzione (lista_argomenti) {
dichirazione variabili esterne ; definizione variabili automatiche ; corpo_della_funzione ;
}
lista_argomenti ha la forma
tipo1 var1 , … , tipon varn
Funzioni: passaggio di parametri e valori di ritorno
• I paremetri in C sono sempre passati per valore.
• Il passaggio per riferimento si simula passando eseplicitamente l'indirizzo del parametro attuale: quindi il parametro formale sarà di tipo puntatore.
• Non è possibile passare/ritornare parametri di tipo non elementare se non attraverso puntatori (con l'eccezione, per l'ANSI-C, delle struttture).
• Il valore della funzione viene ritornato attraverso l'istruzione return, la cui esecuzione termina, comunque, l'esecuzione del corpo della funzione.
• In C non esiste distinzione tra funzioni e procedure: semplicemente le seconde non contegono l'istruzione return; in tal caso (ANSI-C) il tipo di ritorno
della funzione è void.
La ricorsione
Il C è un linguaggio ricorsivo che permette definizioni implicite:
int fact (int n)
{ if (n == 0) return 1;
return n * fact(n-1);
};
In caso di mutua ricorsione la dichiarazione delle funzioni chiamate dovrebbe precedere la chiamata (il compilatore accetta comunque la definizione, ma segnala un WARNING). E’ buona pratica dichiarare prima tutte le funzioni mutuamente ricorsive :
void p (int n); void q (int n);
void p (int n)
{ if (n > 0) {...q(n-1);}
}
void q (int n)
{ if (n > 0) {...p(n-1);}
}
Il preprocessore
Prima della compilazione il codice di un programma C viene trattato da un preprocessore, il quale ha il compito di modificarlo prima della compilazione vera e propria.
L’azione del preprocessore è determinata dall’uso di comandi dei quali i più comuni sono #include e #define.
Esempi:
#define PI = 3.14159
ha l’effetto di sostituire la stringa “PI”con la stringa “3.14159” ovunque nel codice (che la tratterà correttamente come una costante numerica con virgola);
#include “myfile”
causa la copia del contenuto del file myfile nel punto esatto in cui appare l’istruzione #include.
obsoleta: meglio usare const
Struttura di un programma su più file (1)
Normalmente un programma C si articola in diversi file, i quali concorrono alla formazione del codice di un unico programma in due modi:
1. Per inclusione in fase di preprocessing.
2. Nella fase di linking.
Per favorire la compilazione separata è opportuno suddividere ciascun modulo in due file:
myfile_header.h myfile_code.c
In myfile_header.h saranno contenute le dichiarazioni di tipi, variabili e funzioni che si desidera siano visibili agli altri moduli (ed al modulo che contiene il main): si tratta di una interfaccia.
In myfile_code.c vi saranno le definizioni (quindi il codice) delle funzioni pubbliche e le dichiarazioni di tipi, variabili e funzioni private, ossia conosciute solo all’interno del modulo.
Struttura di un programma su più file (2)
int numero_di_serie:
int serie(void);
...
#include Serie.h
int serie(void) {...};
...
Serie.c
Serie.h
Struttura di un programma su più file (3)
Header_generale.h
Header1.h
Modulo1.c Main.c
Problema. Poiché l’inclusione consiste nella copia del codice nel punto in cui si trova il comando #include, le due inclusioni nel file Main.c provocano un messaggio di errore causato dalla ridefinizione degli identificatori nel file Header_generale.h
Header2.h
Modulo2.c
Struttura di un programma su più file (4)
Per ovviare al problema di possibili ridefinizioni di identificatori causati da inclusioni multiple si può usare il comando del preprocessore #ifindef :
#ifindef identificatore codice
#endif
Il suo effetto è quello di includere il codice racchiuso tra i due comandi
#ifindef e #endif solo se l’identificatore non è stato definito da un comando #define.
Allora la struttura di un file che contenga delle inclusioni multiple dovrebbe essere:
#ifindef HEADER /* id. del file */
#define HEADER
/* codice del file */
#endif
Tipi di base
char carattere
int intero
float virgola mobile, singola precisione double virgola mobile, doppia precisione void tipo di ritorno di una procedura,
tipo generico di puntatore Modificatori:
unsigned (unsigned int)
short (short int)
long (long int, long double)
Dichiarazione delle variabili
tipo elenco_variabili ;
int i, j, l;
short int si;
unsigned int ui;
double bilancio, profitto, perdita;
Dichiarazioni e inizializzazioni:
char car = 'a';
int primo = 0;
float importo = 1230000.0;
definizione del formato delle dichiarazioni delle variabili
La virgola (punto decimale) si aggiunge per indicare che la costante 1230000 deve essere memorizzata in virgola mobile.
Classi di memorizzazione
Le dichiarazioni delle variabili e delle funzioni le classifica secondo due dimensioni ortogonali:
• Visibilità
• globali: sono visibili a tutte le funzioni del programma;
• locali: visibili solo all’interno del blocco di dichiarazione.
• Persistenza
• statiche: restano allocate per tutta la durata dell’esecuzione del programma;
• automatiche: restano allocate solo durante l’esecuzione
del blocco in cui sono dichiarate.
Tre classi di memorizzazione
statiche automatiche globali ✔
locali ✔ ✔
Variabili static locali: sono variabili automatiche che, tuttavia, mantengono il loro valore (e dunque continuano ad esistere) attraverso successive chiamate della funzione in cui si trovano.
int serie(void) {
static int num_serie = 100;
/* iniz. che viene eseguita una sola volta */
return(num_serie++);
}
La struttura della memoria
Il funzionamento delle variabili statiche/automatiche e della parte dinamica della memoria è
illustrato dalla ripartizione della memoria in tre aree
fondamentali: statica, dinamica
ed automatica (pila dei record di
attivazione).
Variabili esterne
Un programma C si articola usualmente in più file. Ciò consente di introdurre una gerarchia di visibilità tra variabili. Essa si basa sulla definizione di varibili esterne:
Variabili extern: una dichirazione extern di una variabile in un file (nell'es. File 2) dice al compilatore che la variabile in questione è stata definita altrove (File 1), e ne estende la visibilità alle funzioni definite in quel file.
File 1 File 2
int x, y;
char car;
void
main(void) {…}
extern int x, y;
extern char car;
int funz(void) {…}
Costanti
Nome delle costanti Tipo
'a' '\n' '\0' char
1 123 - 234 int
35000L -34L long int
10000U 987U unsigned
123.23F 4.34e-3F float 123.23 12312333 double
1001.2L long double
•\0 = fine stringa, e \n = fine riga, sono singoli caratteri;
•L abbrevia long, U unsigned, F float,
•e separa la mantissa dall’esponente
Definizione di identificatori per le costanti
Con comandi per il preprocessore (cioè come alias):
#define nome della costante valore della costante
#define MAXLINEE 100 Usando il modificatore const:
const tipo nome_costante = valore ; Esempi:
const double e = 2.71828182845905;
const char tm[] = “type missmatch”;
Espressioni e assegnazioni
Le espressioni sono definite dalla grammatica:
espressione ::= variabile | costante | operatore(lista_espressioni) Gli operatori aritmetici binari si scrivono in notazione infissa, parentesizzati secondo le usuali convenzioni di precedenza. Ad esempio:
x + 2 5 * (y - 1) x % y (PASCAL x mod y) … Le assegnazioni hanno la forma:
variabile = espresione ;
Un' assegnazione in C è un'espressione, il cui valore è il valore assegnato alla variabile a sinstra del simbolo `=´
if ((n = strlen(s)) > 10) ... equivale al Pascal n:= streln(s); if n > 10 then ...
Ciò permette di fare assegnazioni multiple simultanee:
x = y = z = 0;
I booleani
Il C non ha il tipo dei booleani. Al loro posto si usano gli interi:
0 (false) qualunque intero > 0 (true) onde è preferibile introdurre le costanti:
#define TRUE 1 ovvero const int true = 1;
#define FALSE 0 const int false = 0;
Il metodo migliore per introdurre i booleani usa l’istruzione typedef
Espressioni booleane
Le espressioni booleane si formano utilizzando:
• Simboli relazionali: > >= < <=
• Simboli di eguaglianza e diseguaglianza: == !=
• Connettivi: && (and) || (or) ! (not) Esempi:
x > 3 (y == 1) || (y > 0) !(even(n))
(even() deve essere definita) Attenzione:
if (n == m)
equivale al Pascal
if n = m then ...if (n = m)
equivale al Pascal
n:= m; if n > 0 then ...Conversione automatica del tipo
Implicite nelle espressioni (promotion)
Quando in un'espressione sono utilizzati tipi differenti il compilatore li converte tuti nello stesso tipo, scegliendo quello che occupa più memoria.
char c; int i; float f; double d;
tot = (c / i) + (f * d) - (f + i);
int double float double
double
Nota: poiché un argomento di funzione è un'espressione, le conversioni di tipo avvengono anche con il passaggio di parametri.
Conversione esplicita del tipo
Negli assegnamenti
int i; char c;
i = c;
/* un carattere e´ identificato col suo codice ASCII */
c = i; /* il valore di c e' inalterato */
Nota: la conversione float ⇒ int provoca troncatura;
la conversione double ⇒ float provoca arrotondamento.
Nelle espressioni (cast)
(tipo) espressione
int n;
printf("la radice di %d risulta %f",n,sqrt((double) n));
Iterazione con while
while (condizione) comando ;
Esecuzione: la condizione viene valutata; se il valore ottenuto è true allora viene eseguito il comando e l’istruzione while viene eseguita nuovamente;
altrimenti il controllo passa all’istruzione successiva.
Esempio:
int a, b, c; /* pre: a, b interi positivi */
while (b > 0) { c = b;
b = a % b;
a = c;
}
return a; /* post: a e’ l’MCD tra a e b */
Iterazione con do while
do
istruzione
while (condizione);
equivale al Pascal repeat
istruzione
until not condizione;
Si osservi che la condizione in
PASCAL è negata: questo perché until esce se la condizione è vera, mentre do while esce se la condizione è falsa
Incrementi, decrementi, operazioni riflessive
Incremento di due specie:
preincremento ++i
postincremento i++ significano i:= i+1 in entrambi ì casi ma:
x = ++y equivale a y = y + 1; x = y;
x = y++ equivale a x = y; y = y + 1;
Esistono anche i decrementi in questa forma:
--i ovvero i-- equivalenti a i = i - 1;
Operazioni riflessive:
i += 7 significa i := i +7 utile in contesti del tipo:
numero_addetti += 7; invece di
numero_addetti = numero_addetti + 7;
Il ciclo for
for (inizializzazioni; condizione; operazioni) corpo;
La semantica di for è spiegata dalla seguente formulazione equivalente con il while:
inizializzazioni ;
while (condizione) { corpo ;
operazioni ; }
Non essendovi restrizioni sulle condizioni e sulle operazioni che vengono eseguite al termine di ciascun ciclo, il for ed il while sono in C
equivalenti (a differenza del PASCAL).
Esempio: fibonacciano
/* Fibonacciano:
fib(0) = 0 fib(1) = 1
fib(n+2) = fib(n) + fib(n+1)
*/
int fib (int n)
/* pre: n intero positivo */
{
int a = 0, b = 1, i;
if (n == 0) return 0;
for(i = 1; i < n; i++) /* a = fib(i-1), b = fib(i) */
{
b = a+b;
a = b-a;
}
return b;
}
Esempio: somma di 10 interi
/* legge 10 numeri e ne stampa la somma */
void main (void) {
int i, num, somma;
for (i = 0, somma = 0; i < 10; i++) { scanf("%d", &num);
somma += num;
}
printf("la somma vale: %d \n", somma);
}
Esempio: inversione di una stringa
void inversione (char s[])
/* post: inverte la stringa s sul posto */
{
int c, i, j;
for (i = 0, j = strlen(s) - 1; i < j; i++, j--) {
c = s[i];
s[i] = s[j];
s[j] = c;
} }
Nota. i = 0, j = strlen(s) - 1 sono due comandi che
vengono eseguiti in sequenza; analogamente i++, j--.
If annidati
Attenzione: else si riferisce all'ultimo if che ne è privo
if (n > 0)
for (i = 0; i < n; i++)
if (s + i > 0) { … }
else … /* errore: else non si riferisce all'if esterno */
Correzione:
if (n > 0)
for (i = 0; i < n; i++) {
if (s + i > 0) { … }
}
else ….
Annidamento if (condizione)
istruzione_1;
else if (condizione) istruzione_2;
else istruzione_3 Forma generale
if (condizione) istruzione ; if (condizione)
istruzione_1;
else
istruzione_2;
Selezione con switch (Pascal case)
switch c { case 'a':
case 'b':
case 'c': abc_num = abc_num + 1;
break;
case 'd': d_num = d_num +1;
break;
default: altri_num = altri_num +1;
}
Il comando break serve ad impedire l’esecuzione
sequenziale dei test successivi (tra i quali default avrebbe
sicuramente successo).
Puntatori
Un puntatore è una variabile il cui campo di valori è costituito da indirizzi.
Dichiarazione di un puntatore:
tipo *nome_variabile;
Esempio: int *p; /* p e' un puntatore ad interi */
Operatori su puntatori:
Operatore Semantica Esempio
& var ritorna l'indirizzo di var p = &n;
* var si riferisce al valore nella locazione puntata da var
n = *p;
La costante NULL è un valore comune per i puntatori di qualunque tipo e denota convenzionalmente una locazione indefinita (PASCAL nil).
Esempio
/* esempio dell’uso degli operatori sui puntatori
*/
#include <stdio.h>
void main(void) {
int x;
int *p1, *p2;
p1 = &x;
p2 = p1;
printf("%p", p2);
/* stampa il valore esadecimale dell'ind. di x */
}
Condivisione (sharing)
I puntatori possono condividere l’area di memoria cui puntano:
int *p, *q; int n = 44; p = q = &n;
44 n
p q
Ogni modifica del valore di n che avvenga per assegnazione su *p si
riflette su n e su *q. Questa caratteristica viene sfruttata per ottonere effetti collaterali sui valori dei parametri attauli, passandoli cioè per indirizzo.
Esempio: passaggio di parametri
Poiché in C i parametri delle funzioni sono sempre passati per valore, si impiegano i puntatori per ottenere effetti collaterali sui parametri attuali (passati alla chiamata):
void scambia (int *p, int *q) {
int temp;
temp = *p;
*p = *q;
*q = temp;
}
che, nel contesto di chiamata, si deve usare con l'operatore &:
int n, m; ...
scambia (&n, &m);
Vettori
In C i vettori sono puntatori il cui valore è costante (cioè un indirizzo
fissato); la loro dichiarazione è però anche una definizione, nel senso che il compilatore alloca la memoria per le componenti del vettore:
tipo_elementi nome_vettore [dimensione];
L’accesso in lettura scrittura ha la forma
nome_vettore [indice];
Esempi:
int v[10];
v[3] = 7; printf(“%d”, v[0]);
Nota: in C gli indici variano tra 0 e la dimensione del vett. - 1.
Esempio: dichiarazione e inizializzazione
int giorni_mese[12];
/* vettore di 12 interi */
La definizione può inizializzare gli elementi del vettore (quando siano un numero ragionevole):
int giorni_mese[12] =
{31,28,31,30,31,30,31,31,30,31,30,31};
Poiché in C il range di un array di n elementi è da 0 a n-1 abbiamo:
giorni_mese[3] sono i giorni del quarto mese
Esempio: massimo in un vettore
int maxvett (int n, int v[])
/* pre: v è un vettore di n > 0 interi positivi post: ritorna il massimo in v[0..n-1],
*/
{
int max = 0, i;
for (i = 0; i < n; i++)
if (max < v[i]) max = v[i];
return max;
}
Un tipico errore sui vettori
Un vettore non può essere copiato su un altro, di egual tipo e dimensione, con un’unica operazione di assegnamento:
int v[3] = {0,1,2};
int w[3];
w = v; /* errore */
la copia deve essere effettuata componente per componente:
for (i = 0; i < 3; i++) w[i] = v[i];
Funzioni con vettori quali argomenti
Un array non può essere passato per valore, ma sempre attraverso un puntatore, e dunque per indirizzo (riferimento). Vi sono tre modi per ottenere questo risultato:
void stampa(int num[10]);
/* stampa i dieci elementi dell'array num */
/* num ha una dimesnione fissata */
{
int i;
for (i = 0; i < 10; i++) printf("%d", num[i]);
}
void stampa(int num[]);
/* stampa i dieci elementi dell'array num */
/* num non ha una dimensione fissata */
{
int i;
for (i = 0; i < 10; i++) printf("%d", num[i]);
}
È preferibile aggiungere un parametro che spcifichi la
lunghezza del vettore
Aritmetica dei puntatori:
Sui puntatori sono definite operazioni di
• incremento e decremento: int *p; p = &n; p++;
/* p punta a &n + sizeof(int) */
• somma e sottrazione di un intero:
int n, m, *p; p = &n; p = p + m;
/* p punta a &n + m * sizeof(int) */
• differenze tra puntatori:
int n, a, b, *p, *q; p = &a, q = &b; n = p - q;
/* n è il numero degli interi allocabili tra gli indirizzi di a e di b */
• confronto tra puntatori:
int n, m, *p; p = &n; q = &m;
if (p < q) … /* eseguito se l’indirizzo di n è minore di quello di m */
Esempi
Si può puntare ad un singolo elemento di un array:
int a[10]; int *p; p = &a[2];
Tuttavia il nome di un array è una costante di tipo puntatore, il cui valore è l'indirizzo del primo elemento dell'array (offset 0). Dunque quanto segue è lecito:
p = a; che equivale a p = &a[0];
E' anche possibile accedere all'i+1-esimo elemento di un array usando un puntatore al primo e l'offset i: se p == a allora
n = *(p + i); che equivale a
n = a[i]; che equivale a
n = *(a + i); (!!!) Funziona perché
indirizzo di a[i] = indirizzo di a[0] + (i × sizeof(tipo el. di a)) e perché
valore di p + i = indirizzo di p + (i × sizeof(tipo di *p))
dimensione in byte
Stringhe
Una stringa è un vettore di caratteri non vuoto, il cui ultimo elemento è il terminatore `\0’: quindi una stringa di lunghezza n occupa n+1 byte.
“abc” = [`a’,`b’,`c’,`\0’]
In particolare “” = [`\0’] è la stringa vuota.
Il tipo delle stringhe è char*, ossia il tipo dei puntatori a char; le seguenti dichiarazioni sono corrette, ma hanno significato diverso:
char* s; s è una variabile e può puntare ad una stringa char s[]; s è un parametro di stringa nelle funzioni char s[80]; s è una stringa di 80 caratteri indefiniti char* s = “ciao”; s è la stringa “ciao”
Esempi: lunghezza e copia di una stringa
int lunghezza (char s[]) {
int i = 0;
while (s[i] != ‘\0’) ++i;
return i;
}
void copia(char s[], char t[]) {
int i = 0;
while ((s[i] = t[i]) != '\0') ++i;
}
Nota: queste ed altre funzioni sono nella libreria string, dove
sono chiamate strlen e strcpy rispettivamente.
Le strutture (record)
Un struttura è un insieme finito di valori, possibilmente di tipo differente, raccolti sotto un'unico nome. Ciascun valore è associato ad un campo etichettato da un nome-campo, ed è accessibile attraverso di esso.
struct nome_struttura { tipo nome_campo;
…tipo nome_campo;
} variabili_struttura;
L’istruzione struct definisce un tipo, che può essere impiegato nella dichiarazione di varabili, purché la parola chiave struct sia ripetuta:
struct point {
double x; double y;
};
struct point p;
facoltativo
Strutture: accesso
Una struttura può essere:
• copiata: struct point p, struct point q;… p = q;
• indirizzata mediante l’uso dell’operatore &:
struct point *r; r = &p;
• acceduta attraverso i suoi membri:
p.x, r->x
• passata per parametro e ritornata come valore da una funzione:
struct point move(struct point p, double dx, double dy)
A differenza dei vettori le strutture possono essere copiate con una sola assegnazione
Esempio
struct point {
double x; double y;
} p, q;
struct point move (struct point p, double dx, double dy) {
p.x += dx; p.y += dy;
return p;
}
void showpoint (struct point p) {
printf("(%f,%f)\n", p.x, p.y);
}
void main (void) {
p.x = 1.0; p.y = 2.0;
showpoint(p);
q = move(p,1.5,3.1);
showpoint(p); showpoint(q);
}
p resta invariato, mentre q viene assegnato al valore di move, che è un punto
ottenuto traslando p di dx, dy
Dichiarazioni e definizioni
Dichiarazione: consiste nell’associare un tipo ad un identificatore.
Esempi: int n; int fact(int n);
Definizione: consiste nell’associare un valore ad un identificatore.
Esempi: n = 12; int fact(int n) {…}
Dichiarazioni e definizioni spesso coincidono (e.g. la definizione di fact è anche una dichiarazione).
La distinzione tra dichiarazioni e definizioni è importante per capire l’uso della memoria. Nel caso dei puntatori, ad esempio, una dichiarazione ha l’effetto di riservare uno spazio in memoria che possa contenere un indirizzo; una
definizione invece può (quando non vi sia sharing) comportare un’allocazione, riservando spazio in memoria, il cui indirizzo iniziale diventa il valore del
puntatore.
Gestione dinamica della memoria
Sia data la dichiarazione: T* p; (T sia un tipo)
Per definire *p (cioe’ associargli un valore) si può procedere in tre modi:
1. p = q; /* supponendo che *q sia definito */
2. p = &x; /* se x e’ definita */
3. p = (T*) malloc(sizeof(T));
/* allocando un oggetto di tipo T */
Allocazione: è il processo con cui una porzione della memoria dinamica viene riservata per ospitare dati di una certa dimensione; la dimensione, espressa in byte, dipende dal tipo ed è calcolata dalla funzione sizeof.
La funzione malloc ritorna l’indirizzo del primo byte dell’area allocata: tale indirizzo deve essere qualificato mediante un tipo, per cui si impiega il
casting (T*).
Allocatori
E’ opportuno dedicare a ciascun tipo di struttura dati utilizzato dinamicamente una funzione per l’allocazione:
char* StrAlloc(int len) {
char *p;
p = (char*) malloc((len+1)*sizeof(char));
return p;
}
Questa funzione alloca una stringa di lunghezza len: deve perciò allocare len+1 caratteri, per tener conto del terminatore \0.
Se l’allocazione non va a buon fine (memoria insufficiente) malloc e quindi StrAlloc, ritorna NULL: pertanto l’uso corretto di un allocatore è il seguente:
if ((s = StrAlloc(n)) != NULL) … ;
else StrError(); /* avendo definito tale fun. */
Deallocazione
Per deallocare ciò che è stato allocato da malloc si usa la funzione di libreria:
void free (void *p)
Il puntatore void *p è generico (polimorfo); l’informazione sulla dimensione del blocco puntato da p è stata memorizzata da malloc
nell’intestazione del blocco (header), che è a sua volta parte della free list gestita da malloc; grazie ad essa la funzione free può calcolare le
dimensioni del blocco da deallocare.
Tipi definiti dall’utente
Il C consente all’utente di definire propri tipi mediante il costrutto typedef (PASCAL type):
typedef definizione_del_tipo nome_del_tipo ;
I tipi così dichiarati possono essere impiegati esattamente come i tipi di base:
typedef unsigned int Length;
Length len, maxlen;
Length* lengths[]; /* vettore di puntatori a Length */
Un simile esempio è la definizione del tipo String come puntatore a char:
typedef char* String;
Tipi enumerati
Un tipico impiego della definizione di tipi è il caso dei tipi enumerati, ossia che ammettono un numero finito di valori ordinati linearmente:
typedef enum {lun,mar,mer,gio,ven,sab,dom} Giorni;
In realtà il compilatore attribuisce ai valori di un tipio enumerato un numero intero progressivo a partire da 0. Sfruttando questa particolarità possiamo introdurre il tipo dei booleani:
typedef enum {False, True} Boolean;
Questa dichiarazione attribuisce a False il valore 0 e a True il valore 1: di conseguenza un’istruzione come
if (True) ...
avrà il comportamento atteso.
Tipi e strutture
Sebbene il costrutto struct definisca dei tipi, l’uso del typedf (introdotto nell’ANSI C) consente una scrittura più perspicua del codice. Quindi la
dichiarazione:
typedef struct { double x;
double y;
} Point;
è preferibile alla dichiarazione:
struct point { double x;
double y;
};
Si rammenti che in quest’ultimo caso la dichiarzione di una variabile avrebbe avuto la forma:
struct point p;
Tipi unione
L’insieme dei valori di un tipo unione è esattamente l’unione dell’insieme dei valori delle componenti. Tecnicamente una unione è implementata da una sovrapposizione di formati la cui dimensione è il massimo delle dimensioni:
union nome_tipo { tipo1 flag1; … tipon flagn; };
L’informazione sul tipo del valore attuale deve essere mantenuta in una variabile distinta; il flag serve esclusivamente per l’accesso in lettura/scrittura.
typedef union {int ival; char cval;} U_int_char;
typedef enum {Ival, Cval} U_int_char_flag;
U_int_char u;
U_int_char_flag uf;
if (uf == Ival) u.ival = 0; else u.cval = `0’;
La sintassi per accedere alle componenti di una unione è la stessa usata per le strutture (punto e freccia).
Tipi ricorsivi
Un tipo è ricorsivo quando l’insieme dei sui valori sia definito induttivamente; ad esempio un albero binario con etichette intere ha valori nell’insieme:
EmptyTree ∈ BinTree,
n ∈ Int ∧ tl, tr ∈ BinTree ⇒ ConsTree(n,tl,tr) ∈ BinTree.
La sua definizione in C richiede l’uso delle stutture e dei puntatori:
typedef struct tnode *BinTree;
typedef struct tnode { int Info;
BinTree left;
BinTree right;
} Tnode;
const BinTree EmptyTree = NULL;
Allocazione di tipi ricorsivi
Nel definire l’allocatore per gli alberi binari bisogna fare attenzione che ciò che si alloca è la struttura di tipo Node, non il puntatore:
BinTree BinTreeAlloc (void)
{ BinTree bt;
bt = (BinTree) malloc(sizeof(BinTree));
/* errore */
return bt;
}
La versione corretta è:
BinTree BinTreeAlloc (void)
{ BinTree bt;
bt = (BinTree) malloc(sizeof(Tnode));
return bt;
}
In generale si deve scrivere:
variabile_puntatore = (T*) malloc (sizeof(T));
Nel caso dell’esempio, BinTree è infatti definito come Tnode*.
numero di byte per un indirizzo di memoria
numero di byte per una struttura che rappresenta un nodo dell’albero
I/O da terminale (1)
I/O formattato. Si basa sulle seguenti funzioni, definite in stdio
int printf(char stringa_di_controllo, espressioni_argomenti) stampa il valore delle espressioni argomenti utilizzando la stringa di controllo quale maschera per la formattazione. Retsituisce il numero dei caratteri scritti, oppure un numero negativo in caso di errore.
int scanf(char stringa_di_controllo, puntatori_argomenti) legge stringhe da tastiera, considerando spazi, tabultori e ENTER quali separatori; effettua la conversione secondo la stringa di controllo, e ne memorizza i valori agli indirizzi che sono valore dei puntatori argomenti.
La funzione ritorna il numero degli argomenti cui è stato assegnato un valore corretto; ritorna EOF (ossia -1) in caso d'errore.
I/O da terminale (2)
La stringa di controllo di printf e di scanf contiene, oltre a caratteri che vengono semplicemente riprodotti (solo per printf), caratteri detti specificatori, preceduti dal carattere %. I principali sono:
d numero decimale o numero ottale
x numero esadecimale
f numero in virgola mobile
e numero in mantissa ed esponente c carattere
s stringa
Esempio
#include <stdio.h>
void main(void) {
char str[80];
int i;
printf("inserisci una stringa ed un intero: ");
scanf("%s %d", str, &i);
if (i < strlen(str))
printf("%c risulta %d esimo carattere di %s\n", str[i], i, str);
}
I/O da file (1)
Si basa su due concetti:
Flusso: dispositivo logico, cratterizzato da una sequenza di informazioni bufferizzate; è di due tipi:
testo = sequenza di caratteri, binario = sequenza di byte.
File: dispositivo fisico, quale un file su disco, un terminale, una stampante ecc.
La libreria stdio definisce un tipo FILE, che consente le operazioni di apertura, lettura, scrittura e chiusura su di un file, mediante un puntatore:
FILE *fp; /* fp e' un puntatore a file */
I/O da file (2)
Ritorna un puntatore a FILE (inizio del flusso) ovvero NULL in caso di errore.
int fclose(FILE *fp);
chiude il file puntato da fp, effettuando il flushing del buffer (ossia ricopiando sul dispositivo fisico quanto fosse rimasto sul buffer).
int feof(FILE *fp);
ritorna TRUE (ossia 1) se fp punta alla fine del file, FALSE (0) altrimenti.
Inoltre sono disponibili funzioni I/O verso file di tipo formattato:
int fprintf(FILE *fp, const char *stringa_di_controllo, elenco_espressioni);
int fscanf(FILE *fp, const char *stringa_di_controllo, elenco_puntatori);
"r" Apre file di testo in lettura
"w" Apre file di testo in scrittura
"a" Apre file di testo in scrittura in coda
FILE *fopen(const char *nome_file, const char *modo);
associa un file ad un flusso, fissandone la modalità (elenco parziale):