Scope (visibilità)
Scope di un identificatore
¨
Lo scope (visibilità) di un identificatore è la porzione di codice in cui quell’identificatore è definito e ha senso
¤
Se l’identificatore si riferisce a una variabile, lo scope è la porzione di codice che inizia dal punto in cui viene definita la variabile e termina alla fine del blocco che la contiene (chiusura della parentesi graffa) Non è possibile utilizzare una variabile al di fuori del suo scope
Si può pensare che la variabile, raggiunto il termine
dello scope, venga distrutta
Scope di un identificatore
Scope di a nel ramo-if
Scope di a nel ramo-else Scope di b int b = 4;
if (b == 3) { int a = 5;
printf("b è uguale a 3\n e a è uguale a %d", a);
} else {
int a = 1;
printf("b non è uguale a 3 e a è uguale a %d\n", a);
}
Il codice è corretto, però bisogna prestare attenzione:
¨
La variabile a del ramo-if non corrisponde alla variabile a del ramo-else
¨
Entrambe le variabili a vengono distrutte al termine del blocco, quindi qualsiasi riferimento a queste variabili dopo questo blocco di istruzioni sarebbe un errore
Scope di un identificatore
¤
Se l’identificatore si riferisce a una funzione, lo
scope è la porzione di codice che inizia dal punto in
cui viene definita e termina alla fine del file sorgente,
cioè la zona in cui il compilatore è in grado di
recuperare la definizione della funzione (il body) e
il tipo e il numero di argomenti che la funzione
stessa richiede, nonché il tipo del valore di ritorno
Esempio
¨
Non si può invocare una funzione non dichiarata (è fuori dallo scope) Invocazione senza dichiarazione
int main(void) { int a=f(3,5);
printf("%d",a);
return 0;
}
int f(int x, int y){
return 2*x + 3*y;
}
ERRORE!
¨
Anche le funzioni di libreria (printf, strlen, …) sottostanno a questa regola: i file header (stdio.h, string.h, …) contengono le dichiarazioni delle funzioni
Esempio
¨
La definizione di una funzione è implicitamente una dichiarazione:
Dichiarazione, invocazione,
definizione
int f(int x, int y);
int main(void) { int a = f(3,5);
printf("%d", a);
return 0;
}
int f(int x, int y) { return 2*x + 3*y;
Definizione (con dichiarazione implicita) e invocazione
int f(int x, int y) { return 2*x + 3*y;
}
int main(void) { int a = f(3,5);
printf("%d", a);
return 0;
}
Tipi derivati: struct
Strutture
¨
Gli array sono utili quando dobbiamo aggregare informazione omogenea
¨
Quando invece dobbiamo gestire informazione eterogenea è necessario utilizzare una struttura: una collezione finita di
“variabili” (chiamati membri) non necessariamente dello stesso tipo, ognuna identificata da un nome
¨
Sintassi
struct <etichetta> {
<definizione-di-variabili>
};
¨
L’etichetta è opzionale e rappresenta il nome della struttura
Dichiarazione di Strutture
¨
Esempio di dichiarazione di una struttura
struct studente { char nome[20];
int eta;
char sesso;
float media;
};
studente è il nome della struttura composta da tre membri: nome, eta , sesso e media
¨
N.B.: La definizione della struttura non provoca allocazione di memoria, ma introduce un nuovo tipo di dato
¨
La memoria viene allocata solo al momento della
dichiarazione di una variabile del tipo struct studente
Definizione di variabili di tipo struttura
¨
Dichiarazione di due variabili di tipo struct studente:
struct studente p1, p2;
¨
Dichiarazione di un array di 50 elementi ciascuno dei quali è di tipo struct studente:
struct studente elenco[50];
Definizione di variabili di tipo struttura
¨
In memoria i membri di una struttura vengono allocati nello stesso ordine di dichiarazione
¨
Tra un membro e l’altro, a seconda dell’architettura, possono esserci spazi intermedi di allineamento di memoria inutilizzabili e di
contenuto indefinito (sono utili per rendere più efficiente la copia dei dati)
¨
Es. su una macchina a 64 bit con char da 1 byte, int da 4 byte e float da 4 byte, sizeof(struct studente) == 32 invece di 29
Regole per nominare i membri
¨
I membri devono avere nomi univoci all’interno di una struttura, ma strutture diverse possono avere membri con lo stesso nome
¨
I nomi dei membri possono coincidere con nomi di variabili o funzioni
int x;
struct a { char x; int y; }; //corretto
struct b { int w; float x; }; //corretto
Accesso ai membri di una struttura
¨
Si accede ai membri mediante la notazione con punto:
<nome-variabile>.<nome-mebro>
¨
Ogni membro si usa come una normale variabile del tipo corrispondente a quello del membro
struct punto { int x, y;
};
struct punto p1, p2;
p1.x = 10;
p1.y = -2;
p2.x = 5;
p2.y = 7;
struct carta { int valore;
char seme;
};
struct carta c1, c2;
c1.valore = 5;
c1.seme = 'Q';
Puntatori a strutture
¨
Se ho un puntatore a una struct, per accedere ai suoi membri posso usare l’operatore -> (freccia, scritta con meno e maggiore)
struct data {
int giorno, mese, anno;
};
struct data d;
struct data *pd = &d; //pd è un puntatore alla
struttura d…
pd->giorno = 7; //equivalente a (*p).giorno
pd->mese = 1; //equivalente a (*p).mese
pd->anno = 2009; //equivalente a (*p).anno
Operazioni su Strutture
¨
Si possono assegnare variabili di un tipo struttura a variabili dello stesso tipo struttura.
struct data d1, d2;
d1 = d2;
¨
Non è possibile effettuare il confronto tra due variabili di tipo struttura.
struct data d1, d2;
if(d1 == d2) … //Errore!
ma è necessario confrontare ogni singolo membro:
if(d1.giorno==d2.giorno && d1.mese==d2.mese &&
d1.anno==d2.anno)…
Il motivo è legato all’allineamento in memoria (dato che lo spazio intermedio tra i membri ha contenuto indefinito e il C non può confrontare efficientemente le due zone di memoria, delega il compito all’utente)
Esempio
#include <stdio.h>
struct complesso { float im;
float re;
};
int main(void) {
struct complesso c1,c2,somma;
c1.re = 3.; c1.im = 4.;
c2.re = 2.; c2.im = 3.;
somma.im = c1.im + c2.im;
somma.re = c1.re + c2.re;
printf("la somma di %f+i%f e %f+i%f è %f+i%f\n", c1.re,c1.im,c2.re,c2.im,somma.re,somma.im);
return 0;
}
Attenzione
struct pippo { int codice;
char nome[20];
float dato;
};
struct pippo vrb1, vrb2;
...
vrb2 = vrb1;
CORRETTO
struct pippo { int codice;
char nome[20];
float dato;
};
struct pluto { int codice;
char nome[20];
float dato;
};
struct pippo vrb1;
struct pluto vrb2;
...
vrb2 = vrb1;
ERRATO
Perché sia ammesso l’assegnamento, non è sufficiente che le due strutture abbiano gli stessi campi, ma devono essere dichiarate con lo stesso tipo
(il motivo è ancora legato al fatto che l’allineamento dei dati in memoria potrebbe essere diverso)Definizione di nuovi tipi
Definizione nuovo tipo
Con la parola chiave typedef si definisce un “nuovo tipo” (in realtà un’abbreviazione per un tipo esistente):
typedef <tipo_esistente> <nuovo_tipo>;
Ad esempio
typedef int votoesame;
definisce un nuovo tipo, votoesame , che corrisponde al tipo
int
Quando dichiarerò una variabile potrò scrivere
votoesame esame1, esame2;
Definizione nuovo tipo
Definire un nuovo tipo è utile per rendere più leggibile il
codice o per “nascondere” al programmatore che usa un
nostro modulo come realizziamo internamente al modulo
una certa funzionalità
Definizione nuovo tipo
Posso usare typedef anche sulle strutture:
typedef struct {
int codice;char nome[20];
float dato;
} nuovotipo;
nuovotipo var1, var2;
nuovotipo var3;
Abbiamo definito un nuovo tipo derivato
nuovotipoGrazie a typedef possiamo evitare di scrivere la parola chiave struct quando dichiariamo le variabili
Esempio: Vettori parzialmente riempiti
Vettori parzialmente riempiti
¨
Definiamo una struttura dati in cui usiamo un vettore a cui aggiungiamo via via degli interi fino a
riempire interamente il vettore
Vettori parzialmente riempiti
¨
Il vettore occupa in memoria N celle (es. N interi)
¨
Ma contiene dati solo per una parte
¨
ff (first free) è l’indice della prima posizione libera, dove si dovrà eventualmente inserire il prossimo elemento del vettore
0 ff N-1
Vettori parzialmente riempiti
#define N 20 typedef struct {
int vettore[N];
int ff;
} vpr_int;
int main(void) { …
vpr_int miovettore;
… }
Vettori parzialmente riempiti
¨
Devo però preoccuparmi di inizializzare la struttura
¨
Quanto vale all’inizio ff?
¨
Se non la inizializzassi, potrebbe avere
“casualmente” qualsiasi valore, anche negativo
¨
È necessario, quindi, definire una funzione di
inizializzazione da invocare prima di utilizzare un
vettore parzialmente riempito
Vettori parzialmente riempiti
¨
Inizializzazione:
vpr_int init(vpr_int vpr) { vpr.ff = 0;
return vpr;
}
¨
La funzione verrà invocata in questo modo:
int main(void) { …
vpr_int miovettore;
miovettore = init(miovettore);
… }
Vettori parzialmente riempiti
¨
Stampa del vettore:
void print(vpr_int vpr) { int i;
for(i=0; i<vpr.ff; i++)
printf("elemento %d: %d\n", i, vpr.vettore[i]);
}
Vettori parzialmente riempiti
¨
Funzione che determina se il vettore è pieno o se c’è ancora spazio per inserire elementi:
int pieno(vpr_int vpr) { if (vpr.ff >= N)
return 1;
else
return 0;
}
Vettori parzialmente riempiti
¨
Inserimento di un elemento:
vpr_int inserisci(vpr_int vpr, int n) { if (pieno(vpr))
return vpr;
vpr.vettore[vpr.ff] = n;
vpr.ff++;
return vpr;
}
Vettori parzialmente riempiti
¨
Ricerca dell’indice in cui è memorizzato un elemento:
int cerca(vpr_int vpr, int n) { int i;
for (i = 0; i < vpr.ff; i++) if (vpr.vettore[i] == n) return i;
return -1;
}
Vettori parzialmente riempiti
¨
Esempio di un main in cui testiamo il codice
int main(void) { vpr_int miovettore;
miovettore = init(miovettore);
int i;
for (i = 0; i <= N; i++) { printf("inserisco %d.\n", i);
miovettore = inserisci(miovettore, i);
}
…
Vettori parzialmente riempiti
…
i = cerca(miovettore, N);
if (i == -1)
printf("%d non trovato.\n", N);
else
printf("%d trovato all'indice %d.\n", N, i);
i = cerca(miovettore, 0);
if (i == -1)
printf("%d non trovato.\n", 0);
else
printf("%d trovato all'indice %d.\n", 0, i);
return 0;
}
Strutture come parametri
¨
Attenzione, quando si passa come parametro una struttura, questa viene copiata interamente anche quando un membro della struttura è un array
¨
Passare come parametro una struttura può quindi risultare inefficiente
¨
Usando i puntatori, si possono passare i parametri in modo più
efficiente: per “riferimento”
Strutture come parametri
¨
Esempio di funzione stampa modificata con il passaggio per riferimento:
void stampa(vpr_int *pv) { int i;
for(i=0; i < pv->ff; i++)
printf("elemento %d: %d\n", i, pv->vettore[i]);
}
int main(void) { vpr_int miovettore;
…
stampa(&miovettore);
}
Conversione stringhe
Conversione degli argomenti
Per convertire gli argomenti da stringa in tipi primitivi si possono usare le funzioni di stdlib.h:
¨
int atoi(char *s) : da stringa a intero
¨
long atol(char *s) : da stringa a long
¨
double atof(char *s) : da stringa a double Esempio:
¨
int ivalue = atoi(argv[1]);
Conversione degli argomenti
¨
Purtroppo atoi, atol e atod non effettuano controlli: se il parametro s non può essere convertito, viene comunque restituito un valore senza segnalare errori
¨
Quindi sono da usare solo quando si è certi che l’argomento sia una stringa convertibile in intero
¨
In generale bisognerebbe invece usare le più
complesse strtol e strtod (consultare il man per il
loro uso)
Argomenti dalla linea di comando
Argomenti dalla linea di comando
¨
Permettono di passare a un programma parametri da linea di comando. Ad esempio:
$myprog par1 par2
¨
Bisogna definire il main in questo modo:
int main(int argc, char *argv[])
¨
Il C mette a disposizione del programma un vettore di stringhe
(cioè un vettore di vettori di char); ogni stringa corrisponde a
un parametro
Argomenti dalla linea di comando
¨
Più in dettaglio:
¤
argc è il numero di parametri, incluso l’eseguibile stesso (quindi argc >= 1)
¤
argv è un vettore di stringhe: ogni suo elemento argv[i]
è una stringa che corrisponde a un parametro (casi particolari:
n
argv[0] è il nome dell’eseguibile stesso,
n
argv[argc] è NULL)
Argomenti dalla linea di comando
¨
Ad esempio, con $myprog par1 par2 il C crea le seguenti variabili:
NULL
argv
m y p r o g
p a r 1
\0 p a r 2
\0 argc 3
Argomenti dalla linea di comando
¨
Esempio di un programma che stampa gli argomenti dalla linea di comando:
#include <stdio.h>
int main(int argc, char *argv[]) {
int i;
printf("argc = %d\n", argc);
for(i=0; i<argc; i++)
printf("argv[%d] = %s\n", i, argv[i]);
return 0;
}
Controllo degli argomenti
Esempio di programma che calcola la somma dei suoi argomenti interi
#include <stdio.h>
#include <stdlib.h>
int main (int argc, char* argv[]) { int sum = 0, i;
if (argc < 3) { //controllo della cardinalità degli argomenti
printf("Sono richiesti almeno due parametri.\nUso: %s num1 num2 ... numN\n", argv[0]);
exit(EXIT_FAILURE);
}
for (i=0; i<argc; i++) sum += atoi(argv[i]);
printf("La somma totale è %d.\n", sum);
return EXIT_SUCCESS;
}
v
I/O su canali standard
v
I/O su file
v
I/O su stringhe
I/O
Funzioni di I/O
¨
Abbiamo già visto l’uso di scanf e printf per fare input/output sui canali standard
¨
Esistono altre funzioni che permettono di fare I/O
Scrittura:con uso implicito di file (stdout) int putchar(int c);
int puts(char *s);
int printf(const char
*format,...);
con uso esplicito di file int fputc(int c, FILE
*stream);
int fputs(char *s, FILE
*stream);
int putc(int c, FILE
Lettura:
con uso implicito di file (stdin) char* gets(char* s);
int getchar(void);
int scanf(const char *format,…);
con uso esplicito di file
int fgetc(FILE *stream);
char *fgets(char *s, int size, FILE *stream);
int ungetc(int c, FILE *stream);
Scrivere sullo standard output
Oltre alla printf ci sono:
int putchar(int c);
¤
scrive un carattere sullo standard output (file speciale corrispondente al terminale)
¤
restituisce un numero intero corrispondente alla codifica ASCII del carattere scritto, EOF in caso di errore
int puts(char *s);
¤
scrive una stringa sullo standard output
¤
restituisce un numero non negativo in caso di successo, EOF in caso di errore
Leggere dallo standard input
Oltre alla scanf ci sono:
¨
int getchar(void);
¤
legge un carattere dallo standard input (tastiera) e lo restituisce come
carattere, EOF in caso di errore.
Esempi d’uso
#include <stdio.h>
int main(void) {
char c = getchar(); //legge un carattere da
terminale (anche "a capo" è un carattere: è '\n')putchar(c); //stampa a video il carattere in c return 0;
}
Molto spesso getchar è usata per sospendere l’esecuzione di un programma in modo che l’utente possa visionare risultati intermedi
I/O su FILE
¨
Ogni funzione che opera su file utilizza una struttura chiamata FILE definita in stdio.h
¨
Senza entrare nei dettagli, è sufficiente sapere che FILE contiene informazioni riguardanti un file e che ci permette di operare su di esso. Per esempio, tiene traccia dell’offset, cioè il punto del file a cui si è arrivati a leggere o scrivere. FILE viene allocata quando si apre un file e deallocata quando il file viene chiuso
¨
Prima di operare su un file, occorre aprirlo con la funzione fopen
¨
Si ottiene un puntatore alla struttura FILE che bisognerà poi passare come parametro alle funzioni di lettura/scrittura
¨
Quando si termina, occorre chiudere il file con fclose
Apertura di FILE
FILE *fopen(const char *path, const char *mode);
¤ argomenti:
n il percorso (nome compreso) del file che si vuole aprire
n il modo:
n "r" à lettura (il file deve esistere) [esistono anche "rb", "r+", "rb+"]
n "w" à scrittura (crea un file vuoto ed elimina un eventuale file già esistente) [esistono anche "wb", "w+", "wb+"]
n "a" à accoda (append; se il file esiste, scrive in coda al file; se non esiste lo crea vuoto) [esistono anche "ab", "a+", "ab+"]
n Consultare il man per saperne di più
¤ restituisce un puntatore a una struttura di tipo FILE se l’istruzione va a buon fine. Lo stream è posizionato:
n al primo byte del file (read/write)
n all’ultimo byte del file (append)
n NULL se si verifica qualche errore (ad es. non si hanno i diritti per leggere o scrivere il file o il file da leggere non esiste)
I/O su FILE
¨
fscanf e fprintf sono le versioni relative ai file di scanf e printf
¨
int fscanf(FILE *fp, formato, argomenti)
¨
int fprintf(FILE *fp, formato, argomenti)
¨
I parametri e il funzionamento sono identici a quelli delle funzioni scanf e printf tranne per la presenza del parametro di tipo FILE* che identifica il file su cui le funzioni devono operare
¨
Abbiamo già visto fgets:
¨
char* fgets(char *str, int size, FILE
*stream);
¨
Ricordate: In UNIX un file è un flusso (stream) di caratteri
terminato dal carattere speciale EOF
Chiusura di FILE
int fclose(FILE *fp);
¤
Richiede un puntatore a un file (aperto)
¤
Restituisce 0 se la chiusura avviene con successo, EOF se con fallimento.
In entrambi i casi, successivi tentativi di accedere al file chiuso falliranno
¨
È molto importante usare sempre fclose quando si è terminato di usare un file:
¤
l'accesso a un file (qualunque linguaggio di programmazione si usi) è gestito dal sistema operativo, che deve allocare apposite strutture per lo specifico processo richiedente
¤
fopen effettua una richiesta di allocazione al sistema operativo
¤
fclose rilascia al sistema operativo le strutture usate fino a quel momento; poiché le risorse di un computer sono sempre limitate, se non si dealloca mai si rischia di terminare le risorse
Accesso ai FILE
¨
fscanf/fprintf successive fanno avanzare lo stream legato al file
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = fopen("dati.txt", "r");
if(fp == NULL) {
perror("Errore nell’apertura del file");
exit(EXIT_FAILURE);
} int x;
while(fscanf(fp, "%d", &x) != EOF) printf("\n x= %d", x);
fclose(fp);
return EXIT_SUCCESS;
void perror(char* s) è una funzione il cui prototipo è in stdio.h che, invocata dopo un errore in una system call, stampa la stringa passata come argomento seguita da un messaggio sulle cause dell’errore
EXIT_FAILURE e EXIT_SUCCESSS sono costanti simboliche definite in stdlib.h
Esempio alternativo
#include <stdio.h>
#include <stdlib.h>
int main(void) {
FILE *fp = fopen("dati.txt", "r");
if (fp == NULL){…} //come nella slide precedente int x;
while(!feof(fp)) { fscanf(fp, "%d", &x);
printf("\n x=%d", x);
}
fclose(fp);
return EXIT_SUCCESS;
}
int feof(FILE *f)è una funzione il cui prototipo è in stdio.h.
Restituisce un numero diverso da zero (e quindi corrispondente a vero per il C) quando siamo arrivati alla fine del file *f
Flussi standard
¨
Ogni processo in esecuzione è associato a tre FILE* speciali, che risultano già aperti quando si lancia il programma:
¤
FILE *stdin //standard input, di solito la
tastiera¤
FILE *stdout //standard output, di solito lo
schermo¤
FILE *stderr //standard error, di solito lo
schermo¨
scanf è quindi un caso particolare di fscanf , in cui si usa
stdin come parametro FILE *
¤
scanf("…", …) equivale a fscanf(stdin, "…", …)
¨
Analogamente printf è un caso particolare di fprintf :
¤
printf("…",…) equivale a fprintf(stdout, "…", …)
I/O su stringa
¨
Tramite le funzioni sscanf e sprintf è possibile operare (cioè leggere e scrivere dati) su una stringa anziché su file
¨
int sscanf(char *str, formato, argomenti);
¤
lettura da stringa; utile per parsificare dati con formato
¨
int sprintf(char *str, formato, argomenti);
¤
scrittura su stringa; utile per creare stringhe concatenando dati con formato
I/O su stringa: sprintf
char linea[128];
int x, y;
float val;
x = 25; y=7; val = 7.5F;
sprintf(linea, "%d %d :: %f", x, y, val);
Per effetto della sprintf linea corrisponde alla stringa
"25 7 :: 7.5"
che può essere mandata a video o scritta su file
I/O su stringa: sscanf
char linea[128] = "25 7 :: 7.5";
int x,y;
float val;
sscanf(linea, "%d %d :: %f", &x, &y, &val);
Per effetto della
sscanfle variabili
x, ye
valsaranno inizializzate come:
X ß 25, y ß 7, val ß 7.5F
¨
Attenzione: affinché la
sscanfabbia successo la sottostringa " :: "
indicata nella stringa formato deve essere presente in
linea¨