Documenti di Didattica
Documenti di Professioni
Documenti di Cultura
DISPENSE DI C
INDICE
GENERALITA'
PARTE I:
- strutture dati
- operatori
- strutture di controllo
- procedure e funzioni
- input/output
PARTE II:
LINGUAGGIO C: GENERALITA'
u Generalità
Come molti altri linguaggi, il C non si presenta sempre nella stessa forma,
ha subito cioè modifiche nel corso della sua esistenza.
ð nella forma che risale alla sua definizione da parte degli autori, che
chiameremo per brevità K&R-C.
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.
ð imperativo
Esempio:
ð sequenziale
Esempio:
A = 22 ; /* PRIMA, assegna 22 ad A */
B = -3 ; /* POI, assegna -3 a B */
#include <stdio.h>
main()
{ printf("Hello, world !\n") ; }
- #include <stdio.h>
- main ()
è una chiamata alla funzione di sistema printf, che scrive una stringa
sullo standard-output (tipicamente il video).
$ cc Hello.c
$ cc -o Hello Hello.c
$ Hello
Hello, world !
- int i=0 ;
#include <stdio.h>
- 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).
LINGUAGGIO C: PARTE I
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.
- semplici / strutturati
I tipi built-in sono già noti al compilatore del linguaggio utilizzato per la
codifica del programma.
- char, carattere
- int, intero
- float, floating-point in singola precisione
- double, floating point in doppia precisione
- short ³ 16 bit
- int ³ 16 bit
- long ³ 32 bit
- un nome
- un tipo
- una visibilità (scope)
- un tempo di vita (extent)
- un indirizzo di memoria
- un valore
Esempio:
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:
Esempio:
/* File pippo.c */
/* File pluto.c */
Dall'esempio precedente:
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 !
Esempio:
Ogni volta che la funzione azzeraVettore viene chiamata viene creata una
variabile automatica i (in questo caso, con valore iniziale zero).
Esempio:
/* Sequenza di Fibonacci */
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.
Esempio:
Vedremo in seguito che in realtà esiste una stretta correlazione tra tali
funzionali e -rispettivamente- gli operatori & (indirizzo) e * (dereferenza).
Esempio:
Esempi:
ð enumerazioni [semplice]
ð array (mono / multi-dimensionali) [strutturato]
ð record [strutturato]
ð puntatori [strutturato]
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.
Esempi:
Esempi:
#define RMAX 30
#define CMAX 10
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).
/* K&R-C */
/* ANSI-C */
#include <string.h>
...
...
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:
Esempio:
...
int X = 35, *Xptr = &X ;
...
printf("Scrivo X: %d\n",X) ;
printf("Scrivo ancora X: %d\n",*Xptr) ;
...
Definisco una variabile puntatore (ad int) di nome Xptr il cui valore
iniziale è l'indirizzo della variabile X.
Scrivo X: 35
Scrivo ancora X: 35
L'operatore & (indirizzo) fornisce l'indirizzo del suo argomento. Cosi' &X
fornisce l'indirizzo della variabile X.
Esempio:
Xptr X
1931E4 0A2248 35 0A2248
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
/* ANSI-C */
int V1[5] = { -2, -1, 0, 1, 2 } ;
/* ANSI-C */
const int nullVector[5] = { 0, 0, 0, 0, 0 } ;
/* ANSI-C */
int V1[] = { -2, -1, 0, 1, 2 } ;
/* ANSI-C */
/* ANSI-C */
/* ANSI-C */
u Operatori
ð operatori aritmetici
ð operatori relazionali e logici
ð operatori di incremento e decremento
ð operatori che operano sui bit
ð operatori di assegnamento ed espressioni
u Operatori aritmetici
“*” e “/” sono prioritari rispetto al “+” e “-” e rappresentano gli usuali
operatori aritmetici binari.
“>“, “>=“, “<“, “<=“ hanno priorità superiore rispetto a “==“ e “!=“
versione #1:
versione #2:
Alcune osservazioni:
Semantica dell'assegnazione:
ì EV (* SRC + + )
EV (* DST + + = * SRC + + ) = í
îstore EV (* SRC + + ) into E A (* DST + + )
(*)
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.
ìE A (*DST ) = EV ( DST )
E A (*DST + + ) = í
îinc DST
ð operatori logici bit-a-bit binari: “&” (and), “|” (or), “^” (xor)
ð operatori di shift bit-a-bit binari “<<” (shift sx), “>>” (shift dx)
Esempi:
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 */
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 )
Un altro esempio:
X = *Yptr = ++Z - 2 ;
Semantica:
ì 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 )
Ci sono poi altri 10 operatori che costituiscono delle forme contratte per le
assegnazioni che utilizzano operatori aritmetici o bit-a-bit binari.
u Espressioni Condizionali
Sintassi:
Semantica:
= V(<expr0>) altrimenti
Esempi:
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
u Strutture di Controllo
- sequenza
- if_then_else
- switch
- while_do
- do_while
- for
- goto
int X, Y, Z ;
...
Z = X + Y ;
X = 2 * Y ;
int X, Y ;
...
{ int tmp = X; X = Y; Y = tmp; } /* scambio X con Y */
...
Sintassi:
if ( <expr> ) <instr1>
oppure
Dove:
Semantica:
Esempio:
int X, Y ;
...
if ( X < Y ) X = Y ; else { int tmp=X; X=Y; Y=tmp; }
...
Sintassi (semplificata):
switch ( <expr> )
{ case <expr-const1> : <instrList1>
case <expr-const2> : <instrList2>
...
case <expr-constN> : <instrListN>
default : <instrListN+1> }
Dove:
Semantica:
Esempio:
int X, Y ;
...
switch ( X ) {
case 10 : X++ ; Y = 2 * X ; break ;
case 11 :
case 12 : Y = X ;
default : Y++ ;
}
...
Sintassi:
Dove:
Semantica:
Esempio:
Esempio:
Sintassi:
Dove:
Semantica:
Come lo while_do, con la differenza che qui il test viene eseguito dopo.
Sintassi:
Dove:
<init>
while ( <expr> ) { <instr> ; <update> ; }
Esempio:
Sintassi:
goto label ;
Semantica:
Esempio:
u Procedure e Funzioni
Esempio:
Una funzione può essere vista come una procedura che restituisce un
valore (tipicamente il risultato della computazione effettuata).
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).
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;
Esempio:
Output:
Esempio:
Output:
X = -1.0, Y = 1.0
V[0] = 1.0
V[1] = 3.0
V[2] = 2.0
V[3] = 4.0
Output:
X = 1.0, Y = -1.0
V[0] = 1.0
V[1] = 2.0
V[2] = 3.0
V[3] = 4.0
Un esame più attento della corretta procedura di scambio dei valori di due
variabili.
Esempio:
/* 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) ) ;
...
}
u Input/Output
Il tipo FILE, insieme alle funzioni che realizzano l'I/O, viene definito nel
file di include <stdio.h>.
u Input/Output [II]
- caratteri
- stringhe
- generalizzato
- bufferizzato
u Input/Output di stringhe
u Input/Output generalizzato
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 %
u Input/Output bufferizzato
/* Seek */
int fseek ( FILE * stream, long offset, int org );
u Gestione file
/* Apre un file */
FILE * fopen ( char *name, char *mode ) ;
/* Chiude un file */
int fclose ( FILE *stream ) ;
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)
LINGUAGGIO C: PARTE II
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.
1
Ovviamente: calloc(num,size) 1 malloc(num*size), ma anche: malloc(size) 1 calloc(1,size).
#include <stdlib.h>
#include <stdlib.h>
#include <stdlib.h>
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:
u Libreria matematica
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. */
}
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).
u Librerie: miscellanea
/* 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.
#include <stdarg.h>
Esempi:
Esempio:
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