Sei sulla pagina 1di 108

Università degli Studi di Cagliari

Anno Accademico 2003-04

DISPENSE DI C

Prof. Giuliano Armano

Docente G. Armano -1- Appunti sul linguaggio C


Università degli Studi di Cagliari

INDICE

GENERALITA'

PARTE I:

- strutture dati
- operatori
- strutture di controllo
- procedure e funzioni
- input/output

PARTE II:

- gestione della memoria dinamica


- funzioni di libreria
- altre caratteristiche del linguaggio (macro e direttive)

Docente G. Armano -2- Appunti sul linguaggio C


Università degli Studi di Cagliari

LINGUAGGIO C: GENERALITA'

Docente G. Armano -3- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Generalità

Il C è un linguaggio imperativo-sequenziale, appartiene cioè alla stessa


classe di linguaggi a cui appartengono Pascal e Fortran.

Il C è stato definito nei primi anni 70 da Brian W. Kernighan e Dennis M.


Ritchie con l'obiettivo di fornire da supporto per l'implementazione del
sistema operativo UNIX.

Il sistema operativo (eccetto il nucleo), il compilatore C ed essenzialmente


tutti i programmi applicativi di UNIX sono scritti in C.

Molte delle caratteristiche del C discendono dal linguaggio BCPL


(Richards), attraverso il linguaggio B (Thompson) sviluppato nel 1970 per
il primo sistema operativo UNIX su un calcolatore DEC PDP-11.

Come molti altri linguaggi, il C non si presenta sempre nella stessa forma,
ha subito cioè modifiche nel corso della sua esistenza.

In particolare, il linguaggio si può presentare ...

ð nella forma che risale alla sua definizione da parte degli autori, che
chiameremo per brevità K&R-C.

ð nella forma proposta dall'ANSI (American National Standards


Institute), che nel 1983 ha costituito un comitato con l'obiettivo di
dare una definizione del linguaggio C non ambigua e non dipendente
dalla macchina, che chiameremo ANSI-C.

Docente G. Armano -4- Appunti sul linguaggio C


Università degli Studi di Cagliari

Il C ha estensioni standard anche verso paradigmi di programmazione


alternativi. Ad esempio:

ð Parallel-C, per la programmazione concorrente

ð C++, per la programmazione object-oriented (OOP)

Nel seguito, non verrà fatto alcun accenno alle possibili estensioni del
linguaggio per la programmazione concorrente, mentre ricordiamo una
volta per tutte che il linguaggio C++ ha avuto una notevole importanza per
la definizione dello standard ANSI-C.

Infatti, molte innovazioni introdotte dal C++ sono state recepite dal
comitato ANSI-C ed incorporate nello standard corrispondente.

L'ANSI-C sarà il linguaggio di riferimento anche per questi appunti.

Docente G. Armano -5- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Che cosa si intende quando si dice che il C è un linguaggio imperativo-


sequenziale ?

ð imperativo

le istruzioni del programma specificano cosa il programma deve fare

Esempio:

A = 22 ; /* Assegna 22 alla var. A */

ð sequenziale

le istruzioni del programma specificano vengono fornite ed eseguite


in sequenza

Esempio:

A = 22 ; /* PRIMA, assegna 22 ad A */
B = -3 ; /* POI, assegna -3 a B */

Docente G. Armano -6- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Un semplice esempio di programma C: Hello, world !

Vediamo come si fa in C a visualizzare sul monitor del computer la


fatidica frase “Hello, world !”

#include <stdio.h>

main()
{ printf("Hello, world !\n") ; }

Docente G. Armano -7- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Hello, world ! [II]

Qualche nota sul programma che stampa “Hello, world !”

- #include <stdio.h>

è una direttiva per il compilatore. Si richiede di includere (nel file


corrente) il file di libreria "stdio.h" che contiene la specificazione dei
prototipi di funzione e delle costanti che gestiscono l'I/O

- main ()

è una tra le possibili specificazioni -la più semplice- dell'interfaccia


della funzione main (da cui parte l'esecuzione del programma). Il
corpo della funzione segue immediatamente ed è contenuto tra
parentesi graffe

- printf("Hello, world !\n") ;

è una chiamata alla funzione di sistema printf, che scrive una stringa
sullo standard-output (tipicamente il video).

Vedremo in seguito come la funzione printf può essere utilizzata per


effettuare output generalizzato (formattazione e scrittura di costanti,
espressioni e variabili di vario tipo)

Docente G. Armano -8- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Hello, world ! [III]

Supponiamo di aver salvato il programma nel file Hello.c. Il suffisso “.c”


serve per ricordarci che il contenuto del file è un programma sorgente
scritto in C.

Per compilare il programma sorgente, sotto il prompt del sistema operativo


(sia ad esempio il carattere “$”), dovremo attivare il compilatore C
digitando:

$ cc Hello.c

In assenza di errori, il programma cc tenta di generare anche l'eseguibile,


che -per default- si chiamerà a.out.

Per dare al file eseguibile un nome diverso, ad esempio Hello, dovremo


scrivere:

$ cc -o Hello Hello.c

A questo punto, digitando:

$ Hello

si ottiene sul video la scritta:

Hello, world !

Docente G. Armano -9- Appunti sul linguaggio C


Università degli Studi di Cagliari

u Un altro esempio: azzeramento del contenuto di un vettore

Supponiamo di dover scrivere una funzione che azzera il contenuto di un


vettore di numeri interi (vett). La dimensione del vettore (NMAX) viene
passata come parametro alla funzione.

void azzeraVettore ( int vett[], int NMAX )


{ int i=0 ;
while ( i < NMAX )
{ vett[i] = 0 ; i = i+1 ; } }

Risultato dell'applicazione della funzione ad un vettore di interi


qualunque: tutti gli elementi del vettore (dall'elemento di indice 0 a quello
di indice NMAX-1) vengono azzerati.

Docente G. Armano - 10 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Azzeramento del contenuto di un vettore [II]

Qualche nota sulla funzione che azzera il contenuto di un vettore di interi:

- void azzeraVettore ( int vett[ ], int NMAX )

è l'interfaccia della funzione azzeraVettore. Tale interfaccia specifica


che la funzione non ritorna nulla (void), si chiama azzeraVettore, e i
suoi parametri sono -nell'ordine- un vettore di interi (vett) e la
dimensione del vettore (NMAX).

- int i=0 ;

definisce una variabile intera i, locale alla funzione azzeraVettore,


che assume come valore iniziale zero.

- while ( i < NMAX ) { ... }

forza la ripetizione del codice tra parentesi graffe finché è verificata la


condizione i < NMAX.

- while ( ... ) { vett[i]=0 ; i = i + 1 ; }

finché la condizione del test tra parentesi rotonde è verificata, assegna


zero all'elemento vett[i] (elemento del vettore con indice i) e poi
incrementa i.

Docente G. Armano - 11 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Azzeramento del contenuto di un vettore [III]

Un esempio di programma che utilizza la funzione azzeraVettore:

#include <stdio.h>

void azzeraVettore ( int vett[], int NMAX )


{ int i=0 ;
while ( i < NMAX )
{ vett[i] = 0 ; i = i+1 ; } }

void main ( void )


{ int V1[10] ; /* Vettore V1 di 10 interi */
int V2[50] ; /* Vettore V2 di 50 interi */
... /* Altre dichiarazioni */
azzeraVettore(V1,10) ; /* Azzera gli elem. di V1 */
azzeraVettore(V2,50) ; /* Azzera gli elem. di V2 */
... /* Altre istruzioni */ }

Docente G. Armano - 12 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Azzeramento del contenuto di un vettore [IV]

Si noti che nella chiamata azzeraVettore(V1,10):

- V1 è un parametro attuale che viene fatto corrispondere con il


parametro formale vett

- 10 è un parametro attuale che viene fatto corrispondere con il


parametro formale NMAX

Per il momento, annotiamo che, in C:

- i vettori vengono passati per indirizzo (ovvero tutto va come se la


funzione lavorasse direttamente sul parametro attuale).

- tutti gli altri tipi di parametri vengono passati per valore (ovvero si
calcola il valore del parametro attuale e si usa tale valore per
inizializzare la variabile locale equivalente che ha il nome del
parametro formale).

Docente G. Armano - 13 - Appunti sul linguaggio C


Università degli Studi di Cagliari

LINGUAGGIO C: PARTE I

Docente G. Armano - 14 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati

Per i linguaggi delle classe del C, una struttura dati è caratterizzata dalla
sua specificazione (tipo), ed è rappresentabile tramite le sue istanze, che
sono –in ultima analisi– gli oggetti (valori) ammissibili dalla
specificazione. Ad esempio, oggetti ammissibili per un tipo intero sono le
costanti 0, 1, -24, +3.

u Strutture Dati: Tipi, Costanti e Variabili

Un tipo di dato (concreto) è la specificazione delle caratteristiche fisiche


che tutte le istanze che apparterranno al tipo devono avere.

Una costante (di un certo tipo) è un elemento che appartiene al dominio


definito dal tipo corrispondente. Una costante non cambierà il suo valore
nel corso dell'esecuzione del programma.

Una variabile (di un certo tipo) è un identificatore a cui è associato un


indirizzo di memoria e che può ospitare valori appartenenti al dominio
definito dal tipo corrispondente. Una variabile potrà assumere diversi
valori nel corso dell'esecuzione del programma (o meglio durante il suo
tempo di vita). Vedremo in seguito una definizione più dettagliata del
concetto di variabile.

Docente G. Armano - 15 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi

I tipi possono essere:

- built-in (predefiniti) / user-defined (derivati)

- semplici / strutturati

I tipi built-in sono già noti al compilatore del linguaggio utilizzato per la
codifica del programma.

I tipi user-defined sono definiti dall'utente sulla base di regole di


composizione prefissate (in particolare: array, record ed enumerazioni).

I tipi semplici sono caratterizzati dal fatto di avere un dominio non


strutturato (ad esempio: interi, caratteri, enumerazioni).

I tipi strutturati sono caratterizzati dal fatto di avere un dominio


strutturato (ad esempio: array e records). La definizione di un tipo
strutturato viene mappata sui tipi semplici, direttamente (facendo ricorso a
tipi built-in) oppure indirettamente (facendo riferimento a tipi definiti
dall'utente).

Docente G. Armano - 16 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Built-In

In C esiste un ristretto numero di tipi predefiniti fondamentali:

- char, carattere
- int, intero
- float, floating-point in singola precisione
- double, floating point in doppia precisione

Docente G. Armano - 17 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Qualificatori di Tipi Built-In

Alcuni qualificatori (short / long, signed / unsigned) possono essere


applicati ai tipi predefiniti fondamentali, generando restrizioni / estensioni
di tali tipi.

Tabella di applicabilità dei qualificatori short e long ai tipi predefiniti


fondamentali:

char int float double


short no si no no
long no si no si

Le uniche restrizioni imposte dallo standard ANSI sono le seguenti:

- short ³ 16 bit
- int ³ 16 bit
- long ³ 32 bit

Normalmente l'ampiezza di un int rispecchia quella degli interi nella


macchina utilizzata. Spesso short indica un intero a 16 bit, long uno di 32,
e int occupa 16-32 bit. Ogni compilatore è comunque libero di scegliere la
dimensione degli interi in relazione all'hardware sul quale opera.

Il tipo long double caratterizza una precisione tipicamente superiore a


quella fornita dal tipo double.

Le notazioni short int e long int possono essere rispettivamente


abbreviate in short e long.

L'aritmetica corrispondente è quella binaria pura per i numeri non segnati


e quella complemento a due per i numeri con segno.

char int short int long int


signed si (default) (default) (default)
unsigned (default) si si si

Docente G. Armano - 18 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Costanti Built-In: Caratteri

Le costanti built-in di tipo char costituiscono il dominio del tipo di dato


char.

La notazione tipica utilizzata è quella di porre il carattere tra apici. In


alternativa si possono specificare (sempre tra apici) il valore ottale /
esadecimale (entrambi preceduti dal carattere “\”) della codifica ASCII
corrispondente.

Esempi: 'A', 'a', '\065', '\0x41'

Alcuni caratteri speciali sono codificati a parte (sequenze di escape):

\0 NULL (carattere nullo)


\a bell (allarme)
\b BS backspace
\f FF Form Feed (salto pagina)
\n LF Line Feed (salto riga)
\r CR Carriage Return (ritorno di carrello)
\t HT Horizontal TAB (tabulazione orizzontale)
\v VT Vertical TAB (tabulazione verticale)
\\ \
\? ?
\' '
\" "

Docente G. Armano - 19 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Costanti Built-In: Numeri Interi

Le costanti built-in numeriche intere costituiscono il dominio dei tipi di


dato int / short / long (eventualmente unsigned).

La notazione tipica utilizzata è la sequenza di cifre decimali,


opzionalmente preceduta dagli operatori unari “+” e “-”. Alcuni prefissi e
suffissi predefiniti consentono di specificare rispettivamente il sistema di
numerazione utilizzato e il tipo di riferimento. In assenza di suffissi
espliciti, il tipo di riferimento dipende dal numero rappresentato (int /
long, unsigned int / long).

Esempi: 0, 1, -1, +32512, -37

- prefisso 0x / 0X, utilizzato per specificare costanti esadecimali


esempi: 0xA925, 0x1B, 0x2154

- prefisso 0, utilizzato per specificare costanti ottali


esempi: 072, 0515

- suffisso l / L, utilizzato per specificare costanti di tipo long


esempi: 0L, -1l, 072L, 0x453AL

- suffisso u / U, utilizzato per specificare costanti unsigned


esempi: 3589u, 190U, 034U, 0x49B6U

- suffissi ul / UL, utilizzati congiuntamente per specificare costanti


unsigned long.
esempi: 1882591UL, 32ul, 06UL, 0xFFFF0000ul

Docente G. Armano - 20 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Costanti Built-In: Numeri Reali

Le costanti built-in numeriche reali costituiscono il dominio dei tipi di


dato float / double / long double.

La notazione tipica utilizzata è la sequenza di cifre decimali, separata dal


punto decimale, ed opzionalmente preceduta dagli operatori unari “+” e
“-”.

Anche la notazione esponenziale è ammessa. L'esponente presuppone una


base 10.

Alcuni prefissi e suffissi predefiniti consentono di specificare


rispettivamente il tipo di riferimento. Il tipo di default è sempre double
(infatti nel calcolo delle espressioni floating, tutto viene trasformato in
double).

Esempi: 0., .1, -1.357, +325e-4, -37.1e2

- suffisso f / F, utilizzato per specificare costanti float


esempi: -72.85F, 32e-3f

- suffisso l / L, utilizzato per specificare costanti di tipo long double


esempi: 0.1L, -1547.38e-2l

Docente G. Armano - 21 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Costanti di Tipo Espressione e di Tipo Stringa

Oltre alle costanti built-in, altri tipi di costanti meritano immediata


attenzione:

- costanti di tipo espressione

sono espressioni calcolabili in tempo di compilazione, e possono


essere inserite in ogni punto in cui può trovarsi una costante
Esempi: 10*(356-31), -82e-3+71

- costanti di tipo stringa

sono costanti costituite da sequenze di caratteri stampabili, con


eventuali sequenze di escape (\n, \t, ecc.), il tutto racchiuso tra doppi
apici.

Esempi: “Hello world !\n”, “Pippo”, “Pluto”, “Pippo, Pluto, Paperino”

Le costanti di tipo stringa sono memorizzate come vettori di caratteri


e terminate automaticamente con il carattere '\0' (detto anche “fine
stringa”).

Docente G. Armano - 22 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Costanti Simboliche (User-Defined)

Il K&R-C mette a disposizione la direttiva #define. Esempi:

#define pigreca 3.1415


#define NMAX 100

In pratica #define stabilisce una regola di riscrittura per cui il compilatore


sostituisce ogni occorrenza di un certo simbolo con la corrispondente
definizione (nell'esempio sopra, rispettivamente, ogni occorrenza di
pigreca e NMAX con 3.1415 e 100).

L'ANSI-C mette anche a disposizione costruttori di costanti che utilizzano


le definizioni di tipo e la parola chiave const. Ad esempio, per i tipi
semplici:

const double pigreca = 3.1515 ;


const double e = 2.71828182845905;
const int NMAX = 100 ;
const short NumChars = 255 ;

Vedremo poi qualche esempio di uso dell'uso di const applicata anche a


tipi derivati.

Docente G. Armano - 23 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili

In generale, ad una variabile vengono associati:

- un nome
- un tipo
- una visibilità (scope)
- un tempo di vita (extent)
- un indirizzo di memoria
- un valore

Docente G. Armano - 24 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Nome e Tipo

Il nome e il tipo di una variabile vengono specificati durante la sua


dichiarazione. Una dichiarazione di variabile viene fatta specificando
innanzitutto il tipo di riferimento, seguito da un nome (con associata
un'eventuale espressione di inizializzazione).

Esempio:

int K ; char ch='A' ;

Più variabili dello stesso tipo possono essere dichiarate insieme. In tal
caso, alla specificazione del tipo di riferimento, segue una lista di nomi
(con associate eventuali espressioni di inizializzazione) separati da virgole.
Esempio:

int X, Y = -1; /* Inizializzo soltanto Y */


float F1, F2 = 1.0e-2, F3 ; /* Inizializzo soltanto F2 */

Docente G. Armano - 25 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Visibilità

La visibilità di una variabile è lo spazio di programma nel quale la


variabile è accessibile. In C la visibilità di una variabile può essere relativa
al blocco di istruzioni / alla funzione / al file / al programma nel quale la
variabile stessa è stata dichiarata.

Esempio:

/* File pippo.c */

int X, Y=-1; /* X ed Y sono variabili globali */

static float F1 = 1.0 ; /* F1 nota solo in pippo.c */

void simpleF ( int NMAX ) {


int I=0 ; /* I visibile in simpleF */
while ( I < NMAX ) {
int J = I+1 ; /* J visibile nel ciclo while */
... ; /* Istruzioni del ciclo while */
}
}

/* File pluto.c */

extern int X, Y; /* X ed Y sono definite in pippo.c */

static float F1 = 1.0 ; /* un'altra F1 in pluto.c ! */

void complexF ( int K, int NMAX ) {


int I=0 ; /* I visibile in complexF */
int X=-1 ; /* Questa def.ne di X oscura l'altra */
if ( NMAX > 0 ) {
int J = 0 ; /* J visibile solo in questo blocco */
... ; /* Istruzioni del blocco */
}
}

Docente G. Armano - 26 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Visibilità [II]

Dall'esempio precedente:

X e Y sono variabili globali al programma (statiche e potenzialmente


visibili ovunque). Nel file pippo.c sono visibili direttamente, mentre in
pluto.c vanno dichiarate extern per renderle visibili.

F1 è una variabile statica con visibilità all'interno del file nel quale è
definita. Identiche dichiarazioni (con il qualificatore static) in file diversi
identificano variabili diverse !

Le variabili I definite in simpleF e complexF sono automatiche. La loro


visibilità è all'interno della funzione nella quale sono definite.

Le variabili J definite in simpleF e complexF sono automatiche. La loro


visibilità è all'interno del blocco nel quale sono definite.

La dichiarazione della variabile X all'interno della funzione complexF


oscura quella della variabile globale (extern) con lo stesso nome.

Docente G. Armano - 27 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Tempo di Vita

Il tempo di vita di una variabile è l'intervallo di tempo in cui la variabile


esiste all'interno del programma. Può essere coincidente con l'intero
periodo in cui il programma è in esecuzione (variabili statiche), oppure
limitato al tempo in cui viene eseguito codice appartenente al blocco in cui
è stata definita la variabile (variabili automatiche).

Ogni volta che si entra in un blocco / funzione vengono dunque create


nuove istanze delle variabili automatiche, mentre all’uscita del blocco /
funzione tali istanze vengono distrutte.

Esempio:

void azzeraVettore ( int vett[], int NMAX ) {


int i=0 ;
while ( i < NMAX ) { ... ; }
}

Ogni volta che la funzione azzeraVettore viene chiamata viene creata una
variabile automatica i (in questo caso, con valore iniziale zero).

NB in realtà, ogni volta che si entra in azzeraVettore, oltre ad i, vengono


create 2 variabili locali “equivalenti” addizionali: vett e NMAX. Come
vedremo meglio in seguito, vett viene inizializzata con l'indirizzo del
parametro attuale, mentre NMAX viene inizializzata con il valore del
corrispondente parametro attuale (in generale un'espressione).

Docente G. Armano - 28 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Tempo di Vita [II]

Per default, le variabili definite all'interno di un blocco / una funzione sono


automatiche, mentre quelle definite al di fuori sono statiche.

Per modificare la scelta di default relativa ad una variabile locale ad un


blocco / funzione, si può usare la parola chiave static che, senza cambiarne
le regole di visibilità, le associa un extent statico.

Esempio:

/* Sequenza di Fibonacci */

int Fibonacci( void ) {


static int F0=0, F1=0; int Fnext ;
if ( F0 == 0 ) { F0 = 1; return F0 ; }
if ( F1 == 0 ) { F1 = 1; return F1 ; }
Fnext = F1 + F0 ; F0 = F1 ; F1 = Fnext ;
return Fnext ;
}

La funzione Fibonacci utilizza due variabili statiche (F0 ed F1) per


memorizzare gli ultimi due numeri della sequenza Fn-1 ed Fn-2.

Le variabili F0 ed F1 hanno un tempo di vita che coincide con quello del


programma e vengono esplicitamente inizializzate a zero (in realta -come
vedremo- l'inizializzazione a zero è la scelta di default, quindi potrebbe
essere omessa). Concettualmente, l'inizializzazione viene effettuata alla
prima chiamata, concretamente -come tutte le variabili statiche- F0 ed F1
vengono inizializzate nel momento in cui il programma viene mandato in
esecuzione.

Il loro valore iniziale è stato posto a zero per poter effettuare il test relativo
ai primi due valori. Alla prima chiamata il test sul valore di F0 ha esito
positivo e la funzione ritorna 1. Alla seconda chiamata il test sul valore di
F1 ha esito positivo e la funzione ritorna ancora 1. Alle successive
chiamate la funzione ritorna Fn-1+Fn-2.

Docente G. Armano - 29 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Indirizzo di Memoria

L'indirizzo di memoria di una variabile non viene gestito dal


programmatore. Le variabili statiche (globali o no) vengono create una
sola volta e quindi il loro indirizzo è fisso. Le variabili automatiche
vengono create ogni volta che si entra nel blocco in cui sono definite, e
quindi ad ogni creazione riceveranno -in generale- un indirizzo di memoria
diverso.

Tipicamente, le variabili statiche vengono allocate in un'area di memoria


riservata, mentre le variabili automatiche vengono allocate nell'area di
stack del programma, oppure in un registro del processore. La decisione di
allocare nello stack o in un registro viene presa dal compilatore
(tipicamente in fase di ottimizzazione), anche se si può suggerire al
compilatore stesso l'allocazione in un registro tramite il qualificatore
register.

Esempio:

int azzeraVettore( int vett[], int NMAX ) {


register int i=0 ;
while ( i < NMAX ) {
... ; /* Istruzioni del blocco while */
}
}

NB Nonostante il suggerimento, il compilatore rimane comunque libero di


decidere la reale allocazione della variabile.

Docente G. Armano - 30 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Valore (Inizializzazione)

Il valore iniziale può essere specificato esplicitamente nella dichiarazione,


oppure seguire delle regole di default.

Le regole di default sono le seguenti:

- le variabili globali o statiche vengono inizializzate a zero

- le variabili automatiche vengono lasciate indefinite.

Docente G. Armano - 31 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Variabili: Valore (Aggiornamento)

Il valore di una variabile può essere aggiornato tramite un'istruzione di


assegnazione o un'istruzione specializzata di incremento / decremento.

La più semplice forma di assegnazione è quella che assegna un valore ad


una variabile.

Sintassi di una semplice assegnazione: <varIdentifier> = <expr>


Semantica corrispondente: store V(<expr>) into A(<varIdentifier>)

Dove A(.) ed V(.) sono due funzionali che restituiscono -rispettivamente-


l'indirizzo e il valore del loro argomento. I due funzionali sono stati da noi
definiti per poter trattare in modo esplicito la semantica delle assegnazioni,
e -più in generale- quella delle istruzioni del linguaggio.

Vedremo in seguito che in realtà esiste una stretta correlazione tra tali
funzionali e -rispettivamente- gli operatori & (indirizzo) e * (dereferenza).

Esempio:

int azzeraVettore( int vett[], int NMAX ) {


int i=0 ;
while ( i < NMAX ) {
vett[i] = 0 ; i++ ;
}
}

vett[i] = 0 assegna zero all'elemento del vettore vett di indice i.

i++ incrementa il valore della variabile i (più semplicemente, diremo che


incrementa i). Per il momento, possiamo considerarla equivalente
all'istruzione di assegnazione i=i+1. Vedremo meglio in seguito la
semantica delle istruzioni di incremento / decremento.

Docente G. Armano - 32 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Dichiarazione di Variabili Definite su Tipi Built-In

Esempi:

int X, Y = -1; /* Inizializza soltanto Y */


short S1 = 25, S2 = -1 ;
unsigned long distanza ;
float F1 = -12.58e-3 ;
float F2 = F1 * 2 ;
long double raggio = 1.0 ;

Docente G. Armano - 33 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati (User-Defined)

In C i tipi derivati sono sostanzialmente:

ð enumerazioni [semplice]
ð array (mono / multi-dimensionali) [strutturato]
ð record [strutturato]
ð puntatori [strutturato]

Docente G. Armano - 34 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Enumerazioni

Il C mette a disposizione il costruttore di tipi enumerativi enum per


definire costanti di tipo simbolico. Esempi:

enum bool { false, true } ;


enum Giorno { dom, lun, mar, mer, gio, ven, sab } ;
enum Mese { Gen=1, Feb, Mar, Apr, ..., Dic } ;
enum Escape { BELL = '\a', BACKSPACE = '\b',
TAB = '\t', NEWLINE = '\n' } ;

In pratica, enum stabilisce una regola di riscrittura e mappa i suoi valori


sugli interi.

Nel tipo enumerativo bool, false e true valgono rispettivamente 0 e 1,


poiché l'assegnazione di default parte da 0 e procede per incrementi
unitari. Si noti che il tipo bool è definito in C++.

Nel tipo enumerativo Giorno, dom, lun, mar, ..., sab valgono
rispettivamente 0, 1, 2, ..., 6.

Nel tipo enumerativo Mese, Gen, Feb, Mar, ..., Dic valgono
rispettivamente 1, 2, 3, ..., 12.

Nel tipo enumerativo (sequenza di) Escape, a BELL, BACKSPACE, TAB


e NEWLINE sono esplicitamente assegnati i valori dei corrispondenti
caratteri speciali.

Docente G. Armano - 35 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Array Monodimensionali

Il C mette a disposizione il costruttore di strutture omogenee (vettori)


mono-dimensionali.

La definizione di strutture omogenee sia mono che multidimensionali


(matrici) avviene specificando le dimensioni tra parentesi quadre.

Esempi:

#define FMAX 1000

typedef int vettInt [100] ;

vettInt V1 ; float F[FMAX] ;

vettInt è un tipo di dato che caratterizza un array di 100 int.

V1 è una variabile di tipo vettInt array di int (di dimensione 100).

F è una variabile di tipo array di float (di dimensione 1000).

Gli indici di un array di dimensione N sono 0,1, ..., N-1.

Per accedere all'elemento di indice i di un array, basta specificare l'indice


tra parentesi quadre. Esempio:

void copiaVettore ( int DST[], int SRC[], int NMAX ) {


int i=0 ;
while ( i < NMAX ) {
DST[i] = SRC[i] ; i = i + 1 ;
}
}

Docente G. Armano - 36 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Array Multidimensionali

Il C mette a disposizione il costruttore di strutture omogenee (vettori)


multi-dimensionali.

La definizione di strutture omogenee multidimensionali (matrici) avviene


specificando, nell'ordine, le varie dimensioni tra parentesi quadre.

Esempi:

#define RMAX 30
#define CMAX 10

typedef int matrInt [RMAX][CMAX] ;

int V[30][10] ; matrInt V1 ;

Per accedere all'elemento di indice i di un array, basta specificare l'indice


tra parentesi quadre. Esempio:

void azzeraMatrice ( int matr[][100], int NMAX ) {


int i ; /* righe */
int j ; /* colonne */
i=0 ;
while ( i < NMAX ) {
j=0 ;
while ( j < 100 ) {
matr[i][j] = 0 ; j = j+1 ;
}
i = i+1 ;
}
}

NB Come si può notare, nel passare una matrice come parametro, la prima
dimensione può essere omessa. Infatti le matrici vengono memorizzate per
righe, e quindi è necessario conoscere soltanto la dimensione di una riga
(ovvero il numero di colonne).

Docente G. Armano - 37 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Array Multidimensionali [II]

Una dichiarazione alternativa del prototipo della funzione azzeraMatrice


potrebbe essere:

void azzeraMatrice ( int (*matr)[100], int NMAX )

che specifica che il parametro matr è un puntatore ad un vettore di interi di


100 posizioni. Si noti invece che la dichiarazione:

void azzeraMatrice ( int *matr[100], int NMAX )

specifica che mat è un vettore di puntatori ad un intero ! Questo perche' la


priorità dell'operatore [] è superiore a quella dell'operatore di *
dereferenza.

Docente G. Armano - 38 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Record

Il C mette a disposizione il costruttore di strutture composite (record).


Esempi:

/* K&R-C */

enum Mese { ... } ;

struct Data { unsigned short giorno; enum Mese mese;


unsigned short anno };

struct Persona { char cognome[25] ; char nome[20] ;


struct Data dataNascita ;
char codiceFiscale[16+1] ; } ;

typedef struct Persona PERSONA ;

struct Persona P1 ; PERSONA P2, dataBase[100] ;

Docente G. Armano - 39 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Record [II]

Mese è un type-TAG che caratterizza un tipo enumerativo con i seguenti


valori: Gen = 1, Feb = 2, ..., Dic = 12.

Data è un type-TAG che caratterizza un record con i campi giorno (1-31),


mese (1-12), anno.

Persona è un type-TAG che caratterizza un record con i campi cognome,


nome, dataNascita e codiceFiscale.

PERSONA è un identificatore di tipo che corrisponde alla struttura di tipo


(struct) Persona.

P1 è una variabile di tipo (struct) Persona.

P2 è una variabile di tipo PERSONA.

dataBase è una variabile di tipo array di PERSONA (di dimensione 100).

Docente G. Armano - 40 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Record [II]

Per accedere ad un elemento di un record (struct), occorre specificare la


variabile coinvolta ed il nome del campo. Esempi:

/* ANSI-C */

#include <string.h>

enum Mese { ... } ;


struct Data { ... } ;
struct Persona { ... } ;

...

struct Persona P1, dataBase[100] ;

...

strcpy(P1.cognome, "Dessi") ;
strcpy(P1.nome, "Sandro") ;
P1.dataNascita.giorno = 15;
P1.dataNascita.mese = Nov ;
P1.dataNascita.anno = 1968
strcpy(P1.codiceFiscale, "DSS SND 68S15 C006V") ;

Dove strcpy è una funzione di libreria del C che copia una stringa
sorgente in una stringa destinazione, la cui interfaccia è definita (nel file
string.h) come segue:

char * strcpy ( char DST[], const char SRC[] ) ;

La funzione restituisce il puntatore al primo carattere della stringa


destinazione, e copia il contenuto del vettore SRC nel vettore DST (sino al
carattere di fine stringa, ovvero '\0').

Docente G. Armano - 41 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Puntatori

Un puntatore è un indirizzo di memoria. Tipicamente sono sufficienti 32


bit per caratterizzare un puntatore, anche se il numero di bit riservati ad un
indirizzo di memoria dipende ovviamente dall'hardware della macchina (e
a volte dal compilatore utilizzato).

Una variabile di tipo puntatore è una variabile il cui valore è un indirizzo


di memoria, ma è caratterizzata anche dal tipo di dato puntato.

Esempio:

...
int X = 35, *Xptr = &X ;
...
printf("Scrivo X: %d\n",X) ;
printf("Scrivo ancora X: %d\n",*Xptr) ;
...

Definisco una variabile intera X con valore iniziale 35.

Definisco una variabile puntatore (ad int) di nome Xptr il cui valore
iniziale è l'indirizzo della variabile X.

Le istruzioni printf scrivono entrambe sullo standard-output il valore della


variabile X.

Scrivo X: 35
Scrivo ancora X: 35

Docente G. Armano - 42 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Puntatori [II]

Per capire bene il meccanismo della dichiarazione ed inizializazione della


variabile Xptr occorre conoscere la semantica degli operatori unari “&” ed
“*”.

L'operatore & (indirizzo) fornisce l'indirizzo del suo argomento. Cosi' &X
fornisce l'indirizzo della variabile X.

L'operatore * (dereferenza) forza un livello di valutazione in più rispetto al


suo argomento. Così *Xptr non fornisce il valore di Xptr (che è un
puntatore), bensì il valore intero allocato all'indirizzo di memoria il cui
valore è contenuto in Xptr.

Esempio:

Xptr X
1931E4 0A2248 35 0A2248

con riferimento alla figura, possiamo scrivere:

A(Xptr) = 0x1931E4 /* indirizzo della variabile Xptr */


V(Xptr) = 0x0A2248 /* valore della variabile Xptr */

A(X) = 0x0A2248 = V(Xptr) /* indirizzo della variabile X */


V(X) = 35 = V(*Xptr) /* valore della variabile X */

NB Esiste uno stretto legame tra i funzionali A(.) ed V(.) da una parte, e
gli operatori & ed * dall'altra. Con riferimento alle variabili X ed Xptr
sopra definite, tale legame può essere esplicitato dalle seguenti
uguaglianze:

A(*Xptr) = (Xptr)
V

V(&X) = (X)
A

Docente G. Armano - 43 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Inizializzazione

L'ANSI-C consente di inizializzare -in fase di definizione- non solo


costanti e variabili appartenenti a tipi semplici, ma anche a tipi derivati.

In particolare, per quanto riguarda i tipi strutturati, l'inizializzazione


procede secondo la struttura dati di riferimento. Ad ogni livello di
annidamento della struttura dati corrisponde un livello in più di parentesi
graffe.

L'inizializzazione può avvenire per definire il valore iniziale di una


variabile o quello di una costante di programma (in tal caso dovremo
premettere la parola chiave const).

Docente G. Armano - 44 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Inizializzazione di Vettori

Per inizializzare una variabile di tipo vettore, in fase di definizione,


scriveremo, ad esempio:

/* ANSI-C */
int V1[5] = { -2, -1, 0, 1, 2 } ;

Inizializza V1[0], V1[1], ..., V1[4] rispettivamente a -2, -1, 0, 1, 2. Non è


necessario specificare un valore iniziale per tutti gli elementi del vettore.
Gli elementi di cui non viene specificato il valore iniziale seguono le
regole di inizializzazione di default per le variabili.

Per inizializzare una costante di tipo vettore scriveremo, ad esempio:

/* ANSI-C */
const int nullVector[5] = { 0, 0, 0, 0, 0 } ;

Inizializza tutti gli elementi del vettore a zero.

Se la dimensione del vettore viene omessa, il compilatore ne calcola le


dimensioni sulla base del numero di valori iniziali.

/* ANSI-C */
int V1[] = { -2, -1, 0, 1, 2 } ;

Dichiara un vettore di 5 elementi con i valori iniziali specificati tra


parentesi graffe.

Docente G. Armano - 45 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture Dati: Tipi Derivati: Inizializzazione di Record

Per inizializzare una variabile di tipo record, in fase di definizione,


scriveremo, ad esempio:

/* ANSI-C */

enum Mese { ... } ;


struct Data { ... } ;
struct Persona { ... } ;
...
struct Persona P1 = { "Dessi","Sandro", { 15, Nov, 1968 },
"DSS SND 68R15 C006V" } ;

Per inizializzare una costante di tipo record scriveremo, ad esempio:

/* ANSI-C */

enum Mese { ... } ;


struct Data { ... } ;
struct Persona { ... } ;
...
const struct Persona emptyPerson = { "", "", { 0,0,0 }, "" } ;

Se alcuni campi della struttura vengono omessi, l'inizializzazione procede


seguendo le regole di inizializzazione di default per le variabili. Ad
esempio:

/* ANSI-C */

struct Persona P1 = { "Dessi", "Sandro" }

Alla variabile P1 di tipo (struct) Persona vengono assegnati esplicitamente


soltanto i campi cognome e nome. Se la variabile è statica (globale o no),
gli altri campi vengono inizializzati a zero, altrimenti il loro contenuto sarà
-in generale- imprecisato.

Docente G. Armano - 46 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori

Gli operatori in C si dividono nei seguenti gruppi principali:

ð operatori aritmetici
ð operatori relazionali e logici
ð operatori di incremento e decremento
ð operatori che operano sui bit
ð operatori di assegnamento ed espressioni

Docente G. Armano - 47 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori aritmetici

ð operatori aritmetici unari: “+”, “-”

“+” introdotto dall’ANSI-C per motivi di “simmetria” rispetto alla


notazione con il “-” unario

“-” già presente anche nel K&R-C per specificare l’inversione di


segno applicata a costanti / variabili / espressioni aritmetiche.

ð operatori aritmetici binari: “+”, “-”, “*”, “/”, “%”

sono tutti operatori infissi.

“*” e “/” sono prioritari rispetto al “+” e “-” e rappresentano gli usuali
operatori aritmetici binari.

“%” è l’operatore di modulo che restituisce il resto della divisione tra


il primo e il secondo operando.

Esempio: X % 4 => 1 se X = 5 oppure 9 oppure 13 oppure ...

Tabella di applicabilità degli operatori binari

“+”, “-” “*”, “/” “%”


int, short, long si si si
float, double si si no

Docente G. Armano - 48 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori relazionali e logici

Sono operatori che si integrano perfettamente all’interno del meccanismo


di valutazione di espressioni aritmetiche.

Infatti, come vedremo, i test realizzati in linguaggio C sono considerati


falliti (esito negativo) se e solo se il risultato della valutazione
dell’espressione corrispondente e’ zero. Un risultato non nullo determina
l’esito positivo del test corrispondente.

I valori “logici” false e true ottenuti come risultato della valutazione di


espressioni che coinvolgono operatori relazionali e/o logici vengono
mappati, rispettivamente, su 0 e 1.

ð operatori relazionali sono: “>“, “>=“,“<“,“<=“,“==“,“!=“

Si noti che l’operatore di (verifica di) uguaglianza e’ “==“, mentre


quello di (verifica di) disuguaglianza e’ “!=“

“>“, “>=“, “<“, “<=“ hanno priorità superiore rispetto a “==“ e “!=“

Tutti gli operatori relazionali hanno priorità inferiore a quelli


aritmetici.

Esempio: l’espressione i < lim-1 equivale a i < (lim-1)

ð operatori logici unari: “!” (negazione)

ð operatori logici binari: “&&” (and) e “||” (or)

le espressioni connesse da “&&” e/o “||” vengono valutate da sinistra


a destra e la valutazione si blocca non appena si determina la falsità
(per “&&”) o verità (“||”) dell’intera espressione (questo fenomeno
viene chiamato “lazy evaluation”)

Docente G. Armano - 49 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di incremento e decremento

Il C offre operatori specializzati che realizzano l’operazione di incremento


o decremento.

Gli operatori sono “++” (incremento) e “--” (decremento) e possono essere


applicati prima di valutare la variabile corrispondente (pre-incremento /
decremento) o dopo (post-incremento / decremento).

L’operazione di incremento o decremento va vista come un effetto


collaterale che si scatena durante la valutazione di espressioni in cui sono
coinvolte le variabili / locazioni di memoria corrispondenti.

Esempio (uso dell’operatore di post-incremento):

int N = 10 ; int X[10] ;


...
while ( N-- > 0 ) X[N] = 0 ;
...

Come risultato dell’esecuzione del ciclo while, tutte le componenti del


vettore X sono state poste a zero. Perché ?

R.: l’istruzione N-- specifica un’operazione di post-decremento, ovvero:


prima si valuta N e poi la si decrementa.

Si noti che la prima assegnazione viene fatta sull’elemento di indice 9 del


vettore e l’ultima sull’elemento di indice 0.

Docente G. Armano - 50 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di incremento e decremento [II]

Un altro esempio (copia di un vettore):

versione #1:

void copiaVettore( int DST[], int SRC[], int NMAX ) {


int i=-1 ;
while ( ++i < NMAX ) DST[i] = SRC[i] ;
}

versione #2:

void copiaVettore( int DST[], int SRC[], int NMAX ) {


while ( NMAX-- ) *DST++ = *SRC++ ;
}

Si noti l’assegnazione: *DST++ = *SRC++ che sarà presto usata in molti


esercizi.

La semantica associata all’istruzione e’: “prima estrai il valore contenuto


nella locazione puntata da SRC e poi incrementa SRC. Deposita tale
valore nella locazione puntata da DST e poi incrementa DST”.

Docente G. Armano - 51 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di incremento e decremento [III]

Alcune osservazioni:

- un'istruzione di assegnazione ritorna sempre come valore quello della


sua parte destra. (*) In questo caso, viene restituito il valore calcolato
per SRC. Le operazioni di assegnazione e di incremento vanno
considerate come effetti collaterali (side-effect);

- DST e SRC sono trattati come se fossero dei puntatori. Questo e’


sempre possibile in C (almeno quando stiamo considerando parametri
formali di una funzione);

- sono i puntatori e non le locazioni puntate ad essere (post-)


incrementati poiché l’operatore ++ (incremento) si applica soltanto a
quello che compare immediatamente alla sua sinistra. Per
incrementare le locazioni puntate avremmo dovuto scrivere (*DST)
++ oppure (*SRC)++.

Semantica dell'assegnazione:

Dopo ogni operazione in cui si verifica un side-effect inseriremo in alto il


valore ritornato e in basso il side-effect.

Quando necessario si possono usare le indicazioni {before} e {after} per


sottolineare che il side-effect interviene -rispettivamente- prima o dopo la
corrispondente valutazione. Assumiamo che se non viene indicato nulla il
side-effect intervenga dopo.

ì EV (* SRC + + )
EV (* DST + + = * SRC + + ) = í
îstore EV (* SRC + + ) into E A (* DST + + )

ìEV (*SRC ) = EV (EV (SRC ))


EV (*SRC + + ) = í
îinc SRC

(*)
Se il compilatore utilizzato segue l'ANSI C 89 allora restituisce la parte destra; però occorre notare che molti
compilatori restituiscono invece la parte sinistra –in accordo con lo standard C++. Verificare qual'è la
semantica dell'istruzione di assegnazione seguita dal proprio compilatore è molto semplice: basta cercare di
compilare istruzioni del tipo (x=1)++ oppure (x=0)=1. Il compilatore genera errore soltanto se segue lo
standard ANSI C 89.

Docente G. Armano - 52 - Appunti sul linguaggio C


Università degli Studi di Cagliari

ìE A (*DST ) = EV ( DST )
E A (*DST + + ) = í
îinc DST

Docente G. Armano - 53 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori che operano sui bit

Sono 6 operatori che si possono applicare soltanto ad operandi interi


(ovvero char, short, int, e long segnati o no).

ð operatori logici bit-a-bit unari: “~” (inversione, ovvero complem. a 1)

ð operatori logici bit-a-bit binari: “&” (and), “|” (or), “^” (xor)

ð operatori di shift bit-a-bit binari “<<” (shift sx), “>>” (shift dx)

Esempi:

int X1, X2 = -1 ; /* ogni bit di X2 è posto ad 1 */


char ch1 = '9' ; /* ch1 vale 0x39 = codif. ASCII di 9 */

X1 = ~X2 ; /* risultato 0 */
X1 = X1 | X2 ; /* risultato -1 */
X1 = ch1 & 0x0F ; /* risultato 9 */
X1 = ch1 ^ 0x1C ; /* risultato 0x25 */
X1 = X1 << 2 ; /* risultato 0x94 */
X1 = X1 >> 4 ; /* risultato 9 */

Docente G. Armano - 54 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di assegnazione ed espressioni

L'operatore di assegnazione è stato illustrato in una forma semplificata


trattando l'aggiornamento delle variabili.

Più in generale la sintassi di un'assegnazione è la seguente:

<multipleAssign> ¬ <mem> = <multipleExpr>


<multipleExpr> ¬ <expr> | <multipleAssign>

Si noti che la definizione è ricorsiva.

Semantica corrispondente (limitata al side-effect):

store V (<multipleExpr>) into A(<mem>)

Docente G. Armano - 55 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di assegnazione ed espressioni [II]

Un esempio:

...
int V[10], X, Y, Z=5 ;
...
V[0] = X = Y = 3*Z - 1 ; /* Inizializzazione multipla */
... /* A questo punto, V[0], X e Y valgono tutti 14 = 3*5-1 */

Semantica:

ì E ( X = Y = 3*Z - 1)
EV (V[ 0 ] = X = Y = 3*Z - 1)= í V
îstore EV ( X = Y = 3*Z - 1) into EA (V[ 0 ] )

ì E (Y = 3*Z - 1)
EV ( X = Y = 3*Z - 1)= í V
îstore EV (Y = 3*Z - 1) into EA ( X )

ì EV (3*Z - 1) = 3*EV (Z ) - 1 = 14
EV (Y = 3*Z - 1)= í
îstore EV (3*Z - 1) into E A (Y )

La valutazione della parte più a destra di un’assegnazione multipla viene


quindi propagata a sinistra, effettuando separatamente lo store su tutti gli
indirizzi di memoria specificati dall’assegnazione multipla.

Docente G. Armano - 56 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di assegnazione ed espressioni [III]

Un altro esempio:

int X, Y, *Yptr = &Y ; int Z = 0;

X = *Yptr = ++Z - 2 ;

Semantica:

ad X e Y viene assegnato il valore V(++Z - 2) = -1. C'è un side-effect su


Z che viene pre-incrementata. L'inizializzazione di Y avviene tramite l'uso
della variabile puntatore Yptr, inizializzata all'indirizzo di Y.

ì E (* Yptr = + + Z -2 )
EV ( X = *Yptr = + + Z -2 ) = í V
î store EV (* Yptr = + + Z -2 ) into EA (X )

ì EV (+ + Z -2) = EV (Z ) - 2 = 1 - 2 = -1
EV (*Yptr = + + Z -2 ) = í
îinc Z {before}, store EV (+ + Z -2) into EA (*Yptr )

E A (*Yptr ) = EV (Yptr )

Docente G. Armano - 57 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di assegnazione ed espressioni [IV]

Ci sono poi altri 10 operatori che costituiscono delle forme contratte per le
assegnazioni che utilizzano operatori aritmetici o bit-a-bit binari.

ð operatori aritmetici di assegnazione: “+=“, “-=“, “*=“, “/=“, “%=“


ð operatori bit-a-bit di assegnazione: “&=“, “|=“, “^=“, “<<=“, “>>=“

La loro semantica è quella della forma corripondente espansa. Ad esempio:


X1 += 2 corrisponde a X1 = X1 + 2

Docente G. Armano - 58 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori di assegnazione ed espressioni [V]

Importanti note sugli effetti collaterali:

il linguaggio C non specifica l'ordine di valutazione degli argomenti di una


funzione e neppure quello relativo all'esecuzione degli effetti collaterali.
Diversi compilatori possono quindi produrre risultati diversi eseguendo
-ad esempio- le seguenti istruzioni:

int n=3; int i=0; int A[10];


...
printf("%d %d\n",++n,power(2,n)) ; /* power(2,3) */
/* o power(2,4) ? */
...
A[i] = i++ ; /* A[0] o A[1] ? */
...

Docente G. Armano - 59 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Espressioni Condizionali

Sintassi:

<expr> ? <expr1> : <expr0>

Semantica:

V(<expr> ? <expr1> : <expr0>) = (<expr1>) se V(<expr>) ¹ 0


V

= V(<expr0>) altrimenti

Esempi:

int X, Y, Z ; char ch1 ;


...
Z = ( X < Y ) ? X : Y ; /* assegna a Z min(X,Y) */
...
ch1 = ( ch1 >= 'a' && ch1 <= 'z' ) ? ch1-'a'+'A' : ch1 ;
/* trasforma ch1 in char maiuscolo */

Docente G. Armano - 60 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Operatori: Tabella delle Priorità

Operatore Associatività
() [] -> . da SX a DX
! ~ ++ -- (tipo) * & sizeof da DX a SX
*/% da SX a DX
+- da SX a DX
<< >> da SX a DX
<<= >>= da SX a DX
== != da SX a DX
& bit-a-bit da SX a DX
^ da SX a DX
| da SX a DX
&& da SX a DX
|| da SX a DX
?: da DX a SX
= += -= ecc. da DX a SX
, da SX a DX

Docente G. Armano - 61 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo

Le strutture di controllo sono:

- sequenza
- if_then_else
- switch
- while_do
- do_while
- for
- goto

Docente G. Armano - 62 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: Sequenza

In C le istruzioni vengono eseguite in sequenza. Il carattere ";" è il


separatore di istruzioni semplici.

Esempio di istruzioni semplici in sequenza:

int X, Y, Z ;
...
Z = X + Y ;
X = 2 * Y ;

Istruzioni complesse (blocchi) sono delimitate dai caratteri {} e vanno


considerate a tutti gli effetti come istruzioni "singole". All'interno di un
blocco possono essere dichiarate variabili la cui visibilità rimane confinata
all'interno del blocco stesso (inoltre, variabili esterne al blocco con lo
stesso nome vengono oscurate).

Esempio di istruzione complessa:

int X, Y ;
...
{ int tmp = X; X = Y; Y = tmp; } /* scambio X con Y */
...

NB Nel seguito, con istruzione denoteremo istruzioni semplici o


complesse. Si ricordi che soltanto le istruzioni semplici devono essere
sempre terminate dal carattere ";". Anche l'istruzione vuota (ovvero un
semplice ";") è ammessa.

Docente G. Armano - 63 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: if_then_else

Sintassi:

if ( <expr> ) <instr1>

oppure

if ( <expr> ) <instr1> else <instr0>

Dove:

<instr1> e <instr0> sono istruzioni.

Semantica:

se V(<expr>) ¹ 0 allora V(<instr1>) altrimenti V(<instr0>)

Esempio:

int X, Y ;
...
if ( X < Y ) X = Y ; else { int tmp=X; X=Y; Y=tmp; }
...

Docente G. Armano - 64 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: switch

Sintassi (semplificata):

switch ( <expr> )
{ case <expr-const1> : <instrList1>
case <expr-const2> : <instrList2>
...
case <expr-constN> : <instrListN>
default : <instrListN+1> }

Dove:

<expr> è un'espressione intera (char, short, int).


<expr-const> è un'espressione di costanti.
<instrList> è una lista di istruzioni.

Semantica:

Si valuta V(<expr>) e si cerca sequenzialmente la prima espressione


costante <expr-const> (in corripondenza delle label case) tale che V
(<expr-const>) = V(<expr>).

Se tale espressione esiste, allora si eseguono le istruzioni associate, e tutte


quelle successive sino (i) alla fine del blocco dello switch o (ii) al primo
break (o return) incontrato. Altrimenti, se esiste l'opzione di default, si
eseguono le istruzioni corrispondenti oppure si esce dal blocco senza
eseguire nulla.

Docente G. Armano - 65 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: switch [II]

Esempio:

int X, Y ;
...
switch ( X ) {
case 10 : X++ ; Y = 2 * X ; break ;
case 11 :
case 12 : Y = X ;
default : Y++ ;
}
...

NB quando X = 10 eseguo le istruzioni corrispondenti e poi esco dal


blocco switch (per la presenza dell'istruzione break). Quando invece X =
11 oppure X = 12 eseguo l'assegnazione Y = X e poi incremento Y
(ovvero, dato che il “case 12” non è chiuso da un break, eseguo anche
l'istruzione associata alla label default). Altrimenti incremento Y.

Docente G. Armano - 66 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: while_do

Sintassi:

while ( <expr> ) <instr>

Dove:

<expr> è un'espressione, <instr> è un'istruzione.

Semantica:

Se V(<expr>) ¹ 0 allora V(<instr>). Si ripetono tali operazioni finché non


si verifica una delle due eventualità seguenti: (i) V(<expr>) = 0, oppure
(ii) nel corpo dell'istruzione <instr> viene eseguito un break (o un
return). Una delle due eventualità sopra riportate forza l'uscita dal ciclo.

Esempio:

/* cerca un char in una stringa. Se trovato,


ne restituisce l'indice, altrimenti -1 */

int cercaChar ( char *str, int ch) {


int i=0 ;
while ( str[i] != '\0' ) {
if ( str[i] == ch ) break ; else i++ ;
}
return ( str[i] != '\0' ) ? i : -1 ;
}

Finche' non sono arrivato al fine stringa ('\0') verifico se il carattere


corrente ' uguale a quello cercato. Se uguale, allora esco dal ciclo (break).
Ritorno l'indice i del char cercato oppure -1 (se non trovato).

Docente G. Armano - 67 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: while_do [II]

Esiste anche la possibilità di forzare la ripetizione anticipata del test V


(<expr>). In tal caso occorre usare l'istruzione continue che -eseguita
all'interno del blocco di istruzioni del ciclo while- rimanda ad effettuare il
test senza eseguire le eventuali istruzioni successive.

Esempio:

/* cerca un char in una stringa. Se trovato,


ne restituisce l'indice, altrimenti -1 */

int cercaChar ( char *str, int ch) {


int i=-1 ;
while ( str[++i] ) {
if ( str[i] != ch ) continue ;
return i ;
}
return -1 ; /* non trovato */
}

Docente G. Armano - 68 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: do_while

Sintassi:

do <instr> while ( <expr> )

Dove:

<expr> è un'espressione, <instr> è un'istruzione.

Semantica:

Come lo while_do, con la differenza che qui il test viene eseguito dopo.

Docente G. Armano - 69 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: for

Sintassi:

for ( <init> ; <expr> ; <update> ) <instr>

Dove:

<init> è un'espressione (o una sequenza di espress. separate da virgole)


<expr> è un'espressione.
<update> è un'espressione (o una sequenza di espress. separate da virgole).
<instr> è un'istruzione.

Semantica (fornita in termini dello while_do):

<init>
while ( <expr> ) { <instr> ; <update> ; }

Esempio:

void azzeraVettore ( int vett[], int NMAX) {


int i ;
for ( i=0; i < NMAX; i++ ) vett[i] = 0 ;
}

NB In <init> e <update> è possibile specificare anche più di una istruzione


semplice. In tal caso, le virgole sono usate come separatori.

Docente G. Armano - 70 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Strutture di Controllo: goto

Sintassi:

goto label ;

Semantica:

È l'istruzione di salto incondizionato. L'etichetta (label) deve ovviamente


essere stata definita.

Esempio:

/* cerca un char in una stringa. Se trovato,


ne restituisce l'indice, altrimenti -1 */

int cercaChar ( char *str, int ch) {


int i=0 ;
while ( str[i] != '\0' ) {
if ( str[i] == ch ) goto trovato ; else i++ ;
}
return -1 ; /* non trovato */
trovato:
return i ; /* trovato */
}

NB Si consiglia di utilizzare l'istruzione di goto soltanto per implementare


strutture di controllo che il linguaggio non offre esplicitamente (ad es.
repeat-exit a più livelli).

Docente G. Armano - 71 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni

Le procedure storicamente sono nate (anni 60) per evitare la duplicazione


di codice, per poi diventare (anni 80) strumenti per realizzare delle
astrazioni funzionali.

Una procedura è un costrutto linguistico che specifica una trasformazione


-tipicamente non disponibile a livello del linguaggio scelto.

La trasformazione è caratterizzata da un nome e da eventuali parametri,


che possono essere di ingresso, di ingresso-uscita, o di uscita.

I parametri presenti nella specificazione della procedura sono detti


parametri formali e consentono di esplicitare operazioni senza dover
anticipare su quali dati "effettivi" tali operazioni devono essere eseguite.

L'esecuzione di una procedura procede attraverso la sua invocazione (call),


in cui si specificano il nome della procedura e i suoi parametri attuali, cioè
quei dati sui quali verrà eseguita la trasformazione.

Docente G. Armano - 72 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni [II]

Esempio:

/* incrementa tutti gli elementi di un vettore */

void incVettore ( int vett[], int NMAX) {


while ( NMAX-- > 0 ) vett[NMAX]++ ;
}

void main ( void ) {


int V1[100], V2[10] ;
...
incVettore(V1,100) ; /* call su V1 */
incVettore(V2,10) ; /* call su V2 */
...
}

- incVettore è una procedura con due parametri formali: vett (di


ingresso-uscita) e NMAX di ingresso.

- la procedura incVettore viene invocata due volte: la prima su V1 e la


seconda su V2.

Docente G. Armano - 73 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni [III]

Una funzione può essere vista come una procedura che restituisce un
valore (tipicamente il risultato della computazione effettuata).

Formalmente, in C non ci sono procedure, poiché esistono soltanto


funzioni.

Rovesciando l'interpretazione usuale, una procedura può quindi essere


vista come una funzione che non restituisce alcun valore.

Dichiarazione di funzione: sintassi semplificata

<function-dcl> ::= <type> <function-id> ( <parList> ) <function-


body>

<type> ::= <type-id> | <type-dcl>


<parList> ::= <parList1> | e
<parList1> ::= <par> , <parList1> | <par>
<par> ::= <type> <par-id>
<function-body> ::= { <varList> <instrList> }

Dove <varList> e <instrList> rappresentano, rispettivamente, una o più


dichiarazione di variabili e una o più istruzioni.

Per evidenziare il fatto che una funzione è in realtà una procedura, è


consigliato utilizzare la parola chiave void. Nell'esempio precedente
incVettore è una procedura poiché “restituisce” un tipo void.

Per omogeneità, riteniamo opportuno inglobare void all'interno dei tipi


predefiniti, con il significato di "nessun tipo".

La scelta di default per il tipo restituito da una funzione C è int.


Ciononostante suggeriamo di specificare sempre il tipo restituito, anche
quando è int.

Docente G. Armano - 74 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: prototipi

Quando l'uso di una funzione precede la sua definizione (o quando la


definizione è riportata in un altro file), occorre specificarne il prototipo.

Sostanzialmente, il prototipo di una funzione ne indica l'interfaccia, ovvero


il tipo dei parametri e il tipo del valore restituito.

Esempio:

/* incrementa tutti gli elementi di un vettore */

void incVettore ( int vett[], int NMAX) ;

void main ( void ) {


int V1[100], V2[10] ;
...
incVettore(V1,100) ; /* call su V1 */
incVettore(V2,10) ; /* call su V2 */
...
}

void incVettore ( int vett[], int NMAX) {


while ( NMAX-- > 0 ) vett[NMAX]++ ;
}

Docente G. Armano - 75 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: prototipi [II]

Poiché il prototipo riguarda soltanto l'interfaccia di funzione, il nome dei


parametri può anche essere omesso. Ad esempio, il prototipo di incVettore
può essere definito come segue:

void incVettore ( int [], int ) ;

Per compatibilità con il K&R-C è stata mantenuta la possibilità di


dichiarare l'esistenza di una funzione senza specificare la lista dei suoi
parametri. Ad esempio:

void incVettore () ;

specifica che incVettore è una funzione che non restituisce nulla (ovvero è
una procedura) e di cui -al momento- non si specifica l'elenco dei
parametri (cosa che andrà comunque fatta successivamente).

Si noti che, invece, la dichiarazione:

void incVettore ( void ) ;

specifica che la funzione incVettore non ha parametri. Contrariamente alla


precedente, questa definizione sarebbe incompatibile con la dichiarazione
di funzione fornita in precedenza.

Docente G. Armano - 76 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri

Il passaggio di parametri avviene in C secondo una scelta di default,


ovvero:

- i vettori vengono passati "per indirizzo"

ciò significa che alla funzione viene passato l'indirizzo del primo
elemento del vettore. Tale indirizzo viene caricato -come valore
iniziale- nella variabile locale equivalente che ha il nome del
parametro formale;

- tutti gli altri parametri vengono passati “per valore”

ciò significa che alla funzione viene passato il valore dell'espressione


specificata nell'invocazione della funzione. Tale valore viene caricato
-come valore iniziale- nella variabile locale equivalente che ha il
nome del parametro formale.

Docente G. Armano - 77 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri [II]

Esempio:

/* copia una stringa sino ad NMAX caratteri*/

char * stringCopyLim( char DST[], const char SRC[], int NMAX) {


char * ptr = DST ;
while ( NMAX-- > 0 ) {
if ( *SRC == '\0' ) break ;
*ptr++ = *SRC++ ;
}
*ptr = '\0' ;
return DST ;
}

void main ( void ) {


char S1[128], S2[] = "pippo e pluto" ;
stringCopyLim(S1,S2,5) ;
printf("Stringa copiata = %s\n",S1) ;
}

Output:

Stringa copiata = pippo

Docente G. Armano - 78 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri [III]

- la funzione stringCopyLim prende in ingresso due stringhe, SRC e


DST passate per indirizzo (sono due vettori di caratteri) e la
lunghezza massima della stringa destinazione, NMAX, passata per
valore.

- durante l'attivazione di stringCopyLim vengono create le variabili


automatiche: ptr (dichiarata esplicitamente), DST, SRC, e NMAX
(parametri).

- ptr, DST, SRC sono tutte variabili di tipo puntatore a carattere,


mentre NMAX è una variabile intera.

- i valori iniziali di DST, SRC, e NMAX dipendono dai parametri


attuali specificati nell'invocazione della funzione. In questo caso, le
inizializzazioni sono, rispettivamente: A(S1), A(S2), V(5)=5

- per sottolineare che un parametro non cambierà nel corso


dell'esecuzione della funzione si può usare il qualificatore const. Per
questo motivo, nell'esempio precedente SRC è stato dichiarato const.

Docente G. Armano - 79 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri [IV]

Per modificare la scelta di default relativa alle modalità di passaggio dei


parametri, occorre ricorrere agli operatori “&” (indirizzo) e “*”
(dereferenza).

Esempio:

/* scambia il contenuto di due variabili */

void floatExchange ( float *F1, float *F2) {


float tmp = *F1; *F1=*F2; *F2 = tmp ;
}

void main ( void ) {


float V1[] = { 1.0, 2.0, 3.0, 4.0 } ;
float X = 1.0, Y = -1.0 ; int i ;
...
floatExchange(&X,&Y) ;
floatExchange(&V1[1],&V1[2]) ;
printf("X = %4.1f, Y = %4.1f\n",X,Y) ;
for ( i=0; i < 4; i++)
printf("V1[%d] = %3.1f\n", i, V1[i]) ;
}

Output:

X = -1.0, Y = 1.0
V[0] = 1.0
V[1] = 3.0
V[2] = 2.0
V[3] = 4.0

Docente G. Armano - 80 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri [V]

L'uso degli operatori “&” e “*” è necessario per evitare di lavorare su


copie dei parametri attuali che verrebbero distrutte all'uscita dalla
funzione.

Ecco come NON si deve fare per scambiare due variabili:

/* NON scambia il contenuto di due variabili */

void wrongFloatExchange ( float F1, float F2) {


float tmp = F1; F1=F2; F2 = tmp ;
}

void main ( void ) {


float V1[] = { 1.0, 2.0, 3.0, 4.0 } ;
float X = 1.0, Y = -1.0 ; int i ;
...
wrongFloatExchange(X,Y) ;
wrongFloatExchange(V1[1],V1[2]) ;
printf("X = %4.1f, Y = %4.1f\n",X,Y) ;
for ( i=0; i < 4; i++)
printf("V1[%d] = %3.1f\n", i, V1[i]) ;
}

Output:

X = 1.0, Y = -1.0
V[0] = 1.0
V[1] = 2.0
V[2] = 3.0
V[3] = 4.0

NB non è cambiato assolutamente nulla nei parametri attuali ! Tutte le


modifiche sono state infatti effettuate su copie dei parametri attuali (che
vengono distrutte all'uscita della funzione).

Docente G. Armano - 81 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Procedure e Funzioni: passaggio di parametri [VI]

Un esame più attento della corretta procedura di scambio dei valori di due
variabili.

void floatExchange ( float *F1, float *F2) {


float tmp = *F1; *F1=*F2; *F2 = tmp ;
}

- la funzione prende in ingresso due puntatori a float.

- durante l'attivazione di floatExchange vengono create le variabili


automatiche: tmp (dichiarata esplicitamente), F1, F2 (parametri).

- F1 ed F2 sono variabili di tipo puntatore a float.

- i valori iniziali di F1 ed F2 dipendono dai parametri attuali specificati


nell'invocazione della funzione.

- ad esempio, nella prima chiamata (scambio dei valori di X e di Y), le


inizializzazioni di F1 ed F2 sono, rispettivamente: V(&X)= A(X), V
(&Y)= A(Y). In altri termini, tramite F1 ed F2, alla procedura
vengono passati gli indirizzi delle variabili di cui si vuole scambiare il
valore.

Docente G. Armano - 82 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Parametri di tipo puntatore a funzione

Le procedure e funzioni possono anche essere passate come parametri. In


tal caso, la specificazione del parametro di tipo funzione dovrà
comprendere anche il tipo dei parametri da passare alla funzione puntata.

Esempio:

Supponiamo di dover realizzare una funzione di sort che ordini un vettore,


e di voler passare come parametri le funzioni che eseguono il confronto e
lo swap.

int compInt ( int *k, int *w ) ; /* comparaz. di interi */


void swapInt ( int *k, int *w ) ; /* swap di interi */

int compDouble ( double *k, double *w ) ; /* comparaz. di double */


void swapDouble ( double *k, double *w ); /* swap di double */

void sort ( void * array, int dim,


int (*comp) (void *, void *),
void (*swap) ( void *, void * ),
int item_size) {
int i, j;
for ( i=0; i < dim-1 ; i++ )
for ( j=i+1; j < dim; j++ )
if ( (*comp)( array+j*item_size, array+i*item_size ) )
(*swap)( array+j*item_size, array+i*item_size ) ;
}

void main ( void ) {


int V1[100] ; /* array di interi */
double F1[200] ; /* array di double */

... /* inizializzazione vettori */

/* sort di interi */
sort ( V1, 100,
int (*)(int *, int *)) compInt,
void (*)(int *, int *) swapInt,
sizeof(int) ) ;

/* sort di double */
sort ( F1, 200,
int (*)(double *, double*)) compDouble,
void (*)(double *, double*)) swapDouble,
sizeof(double) ) ;
...
}

Docente G. Armano - 83 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output

Le primitive di I/O del C non fanno parte del linguaggio, ma vengono


definite come funzioni di libreria.

In C, come in Unix, l'I/O viene realizzato da / su file. La tastiera e il


monitor sono infatti trattati come file speciali (standard input e standard
output).

Le funzioni C che realizzano l'I/O hanno, tipicamente, una versione


generalizzata per i file e una versione specializzata per lo standard input /
output.

Il tipo FILE, insieme alle funzioni che realizzano l'I/O, viene definito nel
file di include <stdio.h>.

Docente G. Armano - 84 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output [II]

L'I/O viene solitamente suddiviso in tre categorie:

- caratteri
- stringhe
- generalizzato
- bufferizzato

Docente G. Armano - 85 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output di caratteri [II]

Esiste una particolarità del C che riguarda l'I/O di caratteri: quando si


vuole realizzare l'I/O di caratteri occorre trattare i caratteri singoli (char)
come se fossero interi (int).

La motivazione di questa scelta risiede nel fatto che (storicamente) il


carattere di EOF non era un carattere del set ASCII originario.

Si noti che sulle macchine in cui sono stati implementati i primi


compilatori C la lunghezza degli interi era tipicamente 16 bit, di cui la
parte alta veniva lasciata a zero per tutti i caratteri eccetto che per il
carattere di EOF (tipicamente codificato con -1 su 16 bit, ovvero 0xFFFF).

int getchar(void) ; /* Legge un char da stdin */


int putchar(int ch) ; /* Scrive un char su stdout */

int getc(FILE *F) ; /* Legge un char da file */


int putc(int ch, FILE *F) ; /* Scrive un char su file */

int fgetc(FILE *F) ; /* Legge un char da file */


int fputc(int ch, FILE *F) ; /* Scrive un char su file */

Si noti che getchar e putchar possono essere definiti in termini di getc e


putc come segue:

#define getchar() getc(stdin)


#define putchar(ch) putc(ch,stdout)

NB getc e putc sono quasi equivalenti a fgetc e fputc. Si consulti un


manuale C per ulteriori informazioni. In particolare, si veda la funzione
ungetc che “restituisce” un carattere al file da cui si sta prelevando l'input.

Docente G. Armano - 86 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output di stringhe

/* Legge una stringa da stdin */


char * gets ( char * ) ;

/* Scrive una stringa su stdout */


char * puts ( char * );

/* Legge sino a num char da file */


char * fgets ( char *str, int num, FILE *stream ) ;

/* Scrive una stringa su file */


char * fputs ( char *str, FILE *stream );

Docente G. Armano - 87 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output generalizzato

La specificazione dei prototipi delle primitive che realizzano l'I/O


generalizzato viene fatta utilizzando la notazione ANSI-C "..." che indica
un numero variabile di parametri.

La notazione K&R-C corrispondente utilizza il parametro speciale arglist.

/* Legge da stdin un numero variabile di param. */


int scanf ( char *format, ... ) ;

/* Scrive su stdout un numero variabile di param. */


int printf ( char *format, ... ) ;

/* Legge da file un numero variabile di param. */


int fscanf ( FILE *stream, char *format, ... ) ;

/* Scrive su file un numero variabile di param. */


int fprintf ( FILE *stream, char *format, ... ) ;

Esiste anche la possibilità di effettuare un I/O da / su stringa, con le stesse


modalità viste per i file:

/* Legge da file un numero variabile di parametri */


int sscanf ( char *str, char *format, ... ) ;

/* Scrive su file un numero variabile di parametri */


int sprintf ( char *str, char *format, ... ) ;

Docente G. Armano - 88 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output generalizzato: formattazione

Il parametro format specifica il formato per eseguire l'operazione di I/O.

All'interno della stringa di formattazione, le specificazioni di formato


vengono fornite facendole precedere dal carattere “%”.

format % conversione
d, i intero, notazione decimale con segno
o intero, notazione ottale priva segno
x, X intero, notazione esadecimale priva segno
u intero, notazione decimale priva segno
c char, dopo la conversione a unsigned char
s stringa, stampa sino al char speciale '\0'
f floating / double (default 6 cifre decimali)
e, E float / double notazione esponenziale
g, G float / double usa alternativamente %e o %f
p scrive un indirizzo di memoria
n memorizza nell'argomento corrispondente il
numero di char scritti dalla printf (sino al
momento corrente)
% stampa un %

Docente G. Armano - 89 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Input/Output bufferizzato

Viene utilizzato per realizzare I/O (tipicamente in formato binario) da / su


file.

/* Legge da file count elem.,ognuno di dimensione size */


int fread ( void *buffer,
int size, int count, FILE *stream );

/* Scrive su file count elem.,ognuno di dimensione size */


int fwrite ( const void *buffer,
int size, int count, FILE *stream );

/* Seek */
int fseek ( FILE * stream, long offset, int org );

NB la funzione fseek si sposta lungo il file a partire dall'origine, dalla


posizione corrente o dalla fine del file. La codifica dei valori che può
assumere il parametro org è la seguente:

SEEK_SET Inizio file


SEEK_CUR Posizione corrente
SEEK_END Fine file

Docente G. Armano - 90 - Appunti sul linguaggio C


Università degli Studi di Cagliari

u Gestione file

Per accedere ad un file, occorre innanzitutto aprirlo, poi usarlo e poi


chiuderlo.

Per l'utilizzazione possono essere utilizzate le funzioni viste in precedenza.

L'apertura e la chiusura vengono realizzate tramite le seguenti funzioni:

/* Apre un file */
FILE * fopen ( char *name, char *mode ) ;

/* Chiude un file */
int fclose ( FILE *stream ) ;

Modalità di apertura di un file:

mode commento
"r", "w", "a" lettura, scrittura, append (text file)
"rb", "wb", "ab" lettura, scrittura, append (binary file)
"r+", "w+", "a+" lettura / scrittura text file (open,
create, append)
"r+b", "w+b", "a+b" lettura / scrittura binary file (open,
create, append)

Docente G. Armano - 91 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

LINGUAGGIO C: PARTE II

Docente G. Armano - 92 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Gestione della memoria dinamica

Oltre alle zone di memoria riservate ai dati statici e allo stack, esiste
un'altra zona di memoria, chiamata heap, destinata a contenere strutture
dati create dinamicamente durante l'esecuzione di un programma.

La memoria dinamica viene gestita in C come un dato astratto; in altri


termini, l'implementazione della struttura dati dove vengono allocate le
variabili dinamiche viene nascosta (ovvero resa “trasparente”)
disciplinandone l'accesso tramite opportune primitive.

Sostanzialmente il programmatore può allocare strutture dati e


successivamente disallocarle (quando non servono più).

I prototipi delle primitive messe a disposizione dal linguaggio C per


gestire la memoria dinamica sono definiti nel file di include stdlib.h.

Tra esse ricordiamo le principali (size_t è un tipo predefinito che


tipicamente coincide con il byte):

void * malloc ( size_t size ) ;


void * calloc ( size_t num, size_t size ) ;
void * realloc ( void * ptr, size_t size ) ;
void free ( void * ptr ) ;

Docente G. Armano - 93 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Gestione della memoria dinamica [II]

- malloc alloca uno spazio di memoria di dimensione size nello heap e


ne restituisce il puntatore. Se l'operazione non va a buon fine (ovvero
se non c'è memoria sufficiente per soddisfare la richiesta) viene
restituito NULL. Lo spazio di memoria non viene inizializzato.

- calloc è una versione specializzata di malloc per allocare vettori.


Infatti, occorre specificare il numero di elementi (num) da allocare e
la dimensione (size) del singolo elemento. 1

- realloc modifica, portandola a size, l'ampiezza della struttura dati


puntata da ptr. I contenuti restano invariati per uno spazio pari al
minimo tra la vecchia e la nuova ampiezza. L'eventuale nuovo spazio
non viene inizializzato. La funzione ritorna il puntatore alla nuova
area, oppure NULL (in caso di insuccesso).

- free libera la memoria precedentemente allocata, rendendola di nuovo


disponibile per eventuali successive richieste. La funzione prende in
ingresso un puntatore che deve essere rigorosamente quello restituito
da una delle primitive di allocazione. In caso contrario è molto
probabile che -prima o poi- seguirà un crash del programma
(terminazione anormale).

1
Ovviamente: calloc(num,size) 1 malloc(num*size), ma anche: malloc(size) 1 calloc(1,size).

Docente G. Armano - 94 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Gestione della memoria dinamica: qualche esempio

Supponiamo di voler allocare lo spazio per un dato di tipo Persona e di


inizializzare la struttura dati con il contenuto di una variabile automatica
dello stesso tipo. Successivamente, quando il dato non serve più vogliamo
disallocarlo.

#include <stdlib.h>

typedef struct Persona


{ char cognome[25] ; char nome [20] ; ... } PERSONA ;

Persona * allocaPersona ( char * cognome, char * nome )


{ /* alloca una struttura di tipo Persona nello heap */
PERSONA * ptr = ( PERSONA * ) malloc ( sizeof(PERSONA) ) ;
/* inizializza la struttura dati - non ci interessa come */
...
return ptr ; }

void main ( void )


{
PERSONA * ptr1 ; /* definisce un puntatore a Persona */
...
ptr1 = allocaPersona("Salis","Carlo") ; /* crea il dato */
... /* usa il dato */
free(ptr1) ; /* disalloca il dato che non serve più */
...
}

Supponiamo ora di voler allocare lo spazio per un vettore di 100 elementi


tipo Persona. In tal caso, è consigliabile usare la calloc:

#include <stdlib.h>

typedef struct Persona


{ char cognome[25] ; char nome [20] ; ... } PERSONA ;

void main ( void ) {


PERSONA * ptr1 ; /* definisce un puntatore a Persona */
...
/* crea un vettore di 100 elementi di tipo Persona */
ptr1 = (PERSONA *) calloc(100,sizeof(PERSONA)) ;
...
}

Docente G. Armano - 95 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Gestione della memoria dinamica: qualche esempio [II]

Supponiamo ora che, dopo aver allocato un vettore di 100 elementi, ci si


accorga che è invece necessario uno spazio superiore (ad es. 150). In tal
caso, dopo aver usato calloc o malloc, si può usare realloc.

#include <stdlib.h>

typedef struct Persona


{ char cognome[25] ; char nome [20] ; ... } PERSONA ;

void main ( void ) {


PERSONA * ptr1 ; /* definisce un puntatore a Persona */
...
/* crea un vettore di 100 elementi di tipo Persona */
ptr1 = (PERSONA *) calloc(100,sizeof(PERSONA)) ;
...
/* alloca spazio per altre 50 persone */
ptr1 = ( PERSONA * ) realloc ( ptr1, 150 * sizeof(PERSONA) );
...
}

Docente G. Armano - 96 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Altre funzioni di libreria

Oltre alle funzioni per la gestione della memoria dinamica e a quelle per
l'I/O, il linguaggio C mette a disposizione altre librerie di funzioni. In
particolare, ricordiamo le seguenti:

- libreria per la gestione delle stringhe e dei caratteri


- libreria matematica
- libreria per la gestione del tempo
- libreria delle chiamate di sistema
- miscellanea

Docente G. Armano - 97 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria per la gestione delle stringhe e dei caratteri

Tra le funzioni messe a disposizione per gestire caratteri (prototipi in


ctype.h), ricordiamo le seguenti:

int isalpha ( int ch ) ; /* verifica char alfabetico */


int isdigit ( int ch ) ; /* verifica cifra decimale */
int isxdigit ( int ch ); /* verifica cifra esadecimale */
int islower ( int ch ) ; /* verifica char minuscolo */
int isupper ( int ch ) ; /* verifica char maiuscolo */
int tolower ( int ch ) ; /* converte in maiuscolo */
int toupper ( int ch ) ; /* converte in minuscolo */

Tra le funzioni messe a disposizione per gestire stringhe (prototipi in


string.h), ricordiamo le seguenti:

int strlen ( const char *str1 ) ;

char * strcat ( char *str1, const char *str2 ) ;


char * strcmp ( const char *str1, const char *str2 ) ;
char * strcpy ( char *str1, const char *str2 ) ;
int strcspn ( const char *str1, const char *str2 ) ;

char * strncat ( char *str1, const char *str2, size_t size ) ;


char * strncmp ( const char *str1, const char *str2, size_t size ) ;
char * strncpy ( char *str1, const char *str2, size_t size ) ;
int strnspn ( const char *str1, const char *str2 ) ;

char * strchr ( const char *str1, int ch ) ;


char * strrchr ( const char *str1, int ch ) ;

char * strstr ( const char *str1, const char *str2 ) ;

Docente G. Armano - 98 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria per la gestione delle stringhe e dei caratteri [II]

- strlen calcola la lunghezza di una stringa.

- strcat concatena str2 a str1;


- strcmp confronta str1 e str2 secondo l'ordinamento alfabetico e
ritorna -1 se str1 < str2, +1 se str2 < str1, 0 se le due stringhe sono
uguali;
- strcpy copia str2 in str1;
- strcspn ritorna l'indice del primo carattere di str1 che appartiene a
str2 (che viene usata come bag, ovvero come "contenitore" di
caratteri). In caso di fallimento ritorna NULL.

- strncat, strncmp, strncpy, strnspn sono le versioni di strcat,


strcmp, strcpy, strspn limitate ai primi size caratteri (ad es.: strncat
concatena al più size caratteri di str2 a str1).

- strchr (strrchr) ritorna il puntatore alla prima (all'ultima) occorrenza


del carattere ch nella stringa str. In caso di fallimento ritorna NULL.

- strstr ritorna il puntatore alla prima occorrenza della sottostringa str2


trovata in str1. In caso di fallimento ritorna NULL.

Docente G. Armano - 99 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria matematica

Tra le funzioni messe a disposizione dalla libreria matematica (prototipi in


math.h), ricordiamo le seguenti:

double cos ( double x ) ; /* coseno, seno, tangente */


double sin ( double x ) ;
double tan ( double x ) ;

double acos ( double x ) ; /* arcocoseno, arcoseno, arcotangente */


double asin ( double x ) ;
double atan ( double x ) ;

double atan2 ( double x, double y ) ; /* arcotangente di x/y */

double cosh ( double x ) ; /* coseno, seno, tangente iperbolici */


double sinh ( double x ) ;
double tanh ( double x ) ;

double ceil ( double x ) ; /* ritorna il più piccolo intero >= x */


double floor ( double x ) ; /* ritorna il più grande intero <= x */

double exp ( double x ) ; /* esponenziale */


double pow ( double x, double y ) ; /* x elevato ad y */
double sqrt ( double x ) ; /* radice quadrata */

double fabs ( double x ) ; /* valore assoluto (double) */


double fmod ( double x, double y ) ; /* modulo (double) */

double log ( double x ) ; /* logaritmo naturale */


double log10 ( double x ) ; /* logaritmo base 10 */

Docente G. Armano - 100 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria per la gestione del tempo

Le strutture dati coinvolte nelle operazioni che riguardano il tempo sono


(i) time_t, tipicamente espresso tramite un unsigned long / long, e
(ii) struct tm, che riporta il tempo sotto forma di struttura (con anno, mese,
giorno, ore, minuti, secondi, ecc.). Il tempo viene misurato a partire dal
1900.

Vediamo meglio la forma della struct tm:

struct tm {
int tm_sec ; /* secondi dopo il minuto [0-59] */
int tm_min ; /* minuti dopo l'ora [0-59] */
int tm_hour ; /* ore dopo la mezzanotte [0-23] */
int tm_mday ; /* giorno del mese [1-31] */
int tm_mon ; /* mese a partire da Gennaio [0-11] */
int tm_year ; /* anno dopo il 1900 */
int tm_wday ; /* giorno dopo la Domenica [0-6] */
int tm_yday ; /* giorno a partire da Gennaio [0-365] */
int tm_isdst ; /* ora legale 1=si, 0=no, -1=info non disp. */
}

Tra le funzioni messe a disposizione per gestire il tempo (prototipi in


time.h), ricordiamo le seguenti:

time_t time ( time_t * ptr ) ;

struct tm * localtime ( const time_t * ptr ) ;


struct tm * gmltime ( const time_t * ptr ) ;

char * asctime ( struct tm * ptr ) ;


char * ctime ( const time_t * ptr ) ;

Docente G. Armano - 101 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria per la gestione del tempo [II]

- time ritorna l'ora corrente, oppure -1 se l’ora non è disponibile. Se


l'argomento ptr è diverso da NULL il valore di ritorno viene anche
assegnato alla locazione puntata da ptr.

- localtime converte in ora locale (in forma di struct tm) l'ora


contenuta in ptr (in forma di time_t).

- gmltime converte in ora assoluta (in forma di struct tm) l'ora


contenuta in ptr (in forma di time_t).

- asctime converte in ASCII un tempo espresso in forma di struct tm.

- ctime converte in ASCII un tempo espresso in forma di time_t.


Si noti che: ctime(ptr) º asctime(localtime(ptr))

Docente G. Armano - 102 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Libreria delle chiamate di sistema

Tra le funzioni messe a disposizione per effettuare chiamate al sistema


operativo (prototipi in stdlib.h e in process.h), ricordiamo le seguenti:

void abort ( void ) ; /* termina il programma in modo anormale */


void exit ( int status ) ; /* termina il programma normalmente */

NB exit può anche essere usata per terminare il programma in modo


“anormale”. Infatti, per convenzione, status = 0 indica una terminazione
normale, mentre altri valori indicano una terminazione anormale.

int execl ( char *fname, char * arg0, ..., char * argN, NULL ) ;

Il gruppo di funzioni exec (ce ne sono altre oltre a quella indicata) viene
usato per far partire un altro processo (child process) durante l'esecuzione
del programma. Il nome del file che contiene il nuovo programma da
eseguire è puntato da fname e gli eventuali argomenti sono specificati di
seguito (la lista termina con NULL).

Docente G. Armano - 103 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Librerie: miscellanea

Tra le altre funzioni di libreria ricordiamo le seguenti:

int atoi ( const char * anInt ) ; /* conv. stringa in int */


long atol ( const char * aLong ) ; /* conv. stringa in long */
double atof ( const char * aFloat ); /* conv. stringa in double */

int rand ( void ) ; /* genera num. casuale tra 0 e RAND_MAX */


int random ( int num ) ; /* genera num. casuale tra 0 e num-1 */
void randomize ( void ) ; /* iniz. il generatore di numeri random */

Per gestire funzioni con un numero di parametri variabile occorre ricorrere


alle seguenti macro (= macro istruzioni), definite in stdarg.h:

void va_start ( va_list argPtr, lastParam ) ;


void va_arg ( va_list argPtr, type ) ;
void va_end ( va_list argPtr ) ;

Le macro utilizzano il tipo va_list definito anch'esso in stdarg.h. Non


occorre e non si deve conoscere l'effettiva implementazione di va_list. Le
funzioni con numero variabile di parametri devono avere almeno un
parametro che precede la lista dei parametri di cui non si conosce nome e
numero.

Un tipico esempio di funzione con numero variabile di parametri è la


funzione di libreria di I/O printf. Il suo prototipo è il seguente:

/* K&R-C */
int printf ( const char * format, arg_list ) ;

/* ANSI-C */
int printf ( const char * format, ... ) ;

Si noti che QUI la notazione "...", che fa parte del linguaggio C, indica un
numero variabile di parametri. Nel resto di questi appunti è invece stata
spesso usata per indicare qualcosa di non interessante ai fini dell'esempio
considerato.

Docente G. Armano - 104 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Librerie: miscellanea [II]

Una versione semplificata di printf (per illustrare l'uso di va_start,


va_arg, va_end) che stampa soltanto interi, double e stringhe di caratteri
(si fa l’ipotesi di avere a disposizione funzioni specializzate per la stampa
di interi, double e caratteri):

#include <stdarg.h>

extern void printInt(int value) ; /* stampa un int */


extern void printDouble(double value) ; /* stampa un float */
extern void printChar(int value) ; /* stampa un char */

void simplePrintf ( char * format, ... ) {


va_list argPtr ; char *p, *sval ; int ival; double dval;
va_start ( argPtr, format ) ;
for ( p = format ; *p ; p++ ) {
if ( *p != '%' ) { putchar(*p) ; continue ; }
switch ( *++p ) {
case 'd' : /* specifica di stampa di numero intero */
ival = va_arg ( argPtr, int ) ;
printInt ( ival ) ;
break ;
case 'f' : /* specifica di stampa di numero float */
fval = va_arg ( argPtr, double ) ;
printFloat ( fval ) ;
break ;
case 's' : /* specifica di stampa di una stringa */
sval = va_arg ( argPtr, char * ) ;
while ( *sval ) printChar ( *sval++ ) ;
break ;
default :
putchar(*p) ;
break ; /* per stampare il % */
}
}
va_end(argPtr) ; /* altrimenti possibile crash del programma */
}

Docente G. Armano - 105 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Altre caratteristiche del linguaggio (macro e direttive)

Il linguaggio C consente di definire macro di utilizzo generale, ovvero


costanti o sequenze di caratteri che prima della compilazione vera e
propria vengono “espanse”.

Esempi:

#define pigreca 3.1415


#define currentPath "pippo/source/"
#define max(x,y) ( ( (x) > (y) ) ? (x) : (y) )
#define New(type) (type *) malloc ( sizeof(type) )
#define NewVector(dim,type) (type *) calloc ( dim, sizeof(type) )

Si noti che max, New e NewVector hanno anche parametri. La loro


“espansione” comporta la sostituzione della parte sinistra con la parte
destra prima che venga effettuata la compilazione vera e propria.

Docente G. Armano - 106 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

u Altre caratteristiche del linguaggio (macro e direttive) [II]

Al compilatore possono essere anche fornite direttive condizionali, che


possono fare cambiare una compilazione senza che il testo sorgente venga
modificato.

Esempio:

void foo ( int value )


{
#ifdef DEBUG
printf("pluto: value = %d\n",value) ;
#endif
... /* codice della funzione */
}

In questo caso, l'effetto della direttiva #ifdef è quello di valutare se esiste


una definizione di DEBUG oppure no. Se esiste, allora viene compilata
anche la parte tra #ifdef e #endif, altrimenti si compila soltanto il (vero e
proprio) codice della funzione.

La DEBUG dell'esempio può essere definita una volta per tutte nel main,
oppure specificata all'atto della compilazione. Facendo l'ipotesi che il
nome del file contenente la funzione foo sia pippo.c, la sua compilazione
potrebbe essere effettuata nel modo seguente (usando l'opzione -D del
compilatore):

$ cc -DDEBUG -c pippo.c

Docente G. Armano - 107 - Appunti sul linguaggio C


Univ. degli Studi di Cagliari

¨ Bibliografia sul Linguaggio C

1 B. W. Kernighan, D.M. Ritchie, Linguaggio C, Jackson

1 Davies, The Indispensable Guide to C, Addison Wesley

1 B. Gottfried, Programmare in C, McGraw Hill (Collana Schaum)

1 H. Schildt, Linguaggio C, McGraw Hill

Docente G. Armano - 108 - Appunti sul linguaggio C

Potrebbero piacerti anche