Scarica Tecniche di Programmazione – Corso completo e più Appunti in PDF di Programmazione C solo su Docsity!
Da Python a C
#include <stdio.h> direttiva per includere la definizione di funzioni e variabili predefiniti nel linguaggio #include <math.h> include la definizione di funzioni matematiche int main() { … } definizione della funzione main che racchiude il programma principale
- (^) printf(stringa, par1, par2) = in stringa possono essere contenuti caratteri che si vogliono stampare e il formato dei dati che si vuole stampare, indicati dai parametri (espressioni)
- (^) \n = carattere di ritorno a casa
- (^) /* … */ = commento su più righe gcc -o NomeFile NomeFile.c compila il programma producendo un file NomeFile che contiene i comandi eseguibili dal calcolatore ./NomeFile esegue il programma In C tutte le variabili devono essere dichiarate prima dell’uso Una dichiarazione può contenere un elenco di variabili separate da virgole rappresentazione del nome, tipo, valore e indirizzo di una variabile double pow(double b, double e) intestazione di una funzione che consiste nella segnatura più la descrizione (il tipo) del risultato i parametri passati a una funzione possono anche essere funzioni a loro volta
- (^) %d = stringa che indica che il valore da stampare è un intero
- (^) %f = stringa che indica che il valore da stampare è un reale
- (^) scanf(stringa,par1,par2,…) = permette di eseguire l’input da tastiera, specificando una stringa di caratteri che rappresenta il formato dei dati letti (%…), ed i parametri con espressioni della forma &nome-variabile che identificano le variabili in cui i dati letti devono essere memorizzati: Es. scanf(“%f%f”, &a, &b) legge da tastiera due valori reali e li memorizza nelle variabili (reali) a e b (secondo l’ordine di inserimento). la stringa %f indica che il carattere da leggere è di tipo reale int var
Le variabili dichiarate all’interno di un blocco di istruzioni non sono visibili all’esterno del blocco In C si possono introdurre delle dichiarazioni di variabile al di fuori della dichiarazione di funzioni, che vengono dette variabili globali Dichiarando all’inizio solo l’intestazione di una funzione, posso postporre la definizione della funzione stessa anche se questa viene chiamata prima in qualche altra funzione o nel main Semantica if-else:
- serve la parentesi per la condizione, e non ci sono i “:” alla fine
- se ci sono più istruzioni bisogna metterle in un blocco, mentre con una singola istruzione non serve
- nelle condizioni il valore 0 corrisponde a false e qualunque altro valore a true La condizione può essere costruita usando:
- operatori di confronto (==, !=, >, <, >=, <=) applicati a variabili (o espressioni)
- chiamate a funzioni
- espressioni booleane complesse, ottenute applicando gli operatori booleani (!, &&, ||) a espressioni più semplici
- (^) elif = else if
- (^) switch(espressione){ case etichetta-1: istruzioni- 1 break; … case etichetta-n: istruzioni-n break; default: istruzioni-default (opzionale) } espressione è un’espressione di tipo int o char (non reale), etichetta sono espressioni intere (o carattere) costanti, o costanti inizializzate con espressioni costanti, non possono essere quindi espressioni che fanno riferimento a variabili valuta espressione e cerca la prima etichetta il cui valore è uguale a espressione se non trova nessuna etichetta a cui corrisponde il valore di espressione, vengono eseguite istruzioni-default se ci sono più valori per cui verrebbero eseguite le stesse istruzioni, allora si possono raggruppare i rispettivi case Es. switch(mese){ case 4: case 6: case 9: case 11: GiorniDelMese = 30; break; case … } printf(“Giorni del mese %d: %d \n”, mese, GiorniDelMese) se non ci fosse il comando break, una volta trovata l’etichetta corrispondente all’espressione, ed una volta eseguite le sue istruzioni, verranno eseguite anche tutte quelle delle etichette seguenti finché non si incontrerà un break o sarà terminata la switch Semantica ciclo while: vale la stessa semantica dell’istruzione condizionale if
Il tipo char rappresenta in realtà un tipo intero a 8 bit (i cui valori vengono interpretati come caratteri del codice ASCII) Es. char c = “A”; printf(“%d\n”, c); // Stampa: 6 5 Le operazioni disponibili per i char sono le stesse disponibili per i tipi interi const = indica una variabile il cui contenuto non può cambiare durante l’esecuzione del programma #define = altra direttiva che definisce costanti, e che sostituisce a ogni occorrenza del nome definito il valore corrispondente Es. #define COMPENSO_ORARIO 2000 0 —> sostituisce 20000 a tutte le occorrenze nel programma con il nome COMPENSO_ORARIO Poiché non è una costante del programma, essa non ha alcun tipo associato, pertanto il compilatore non verifica alcuna corrispondenza tra tipi quando il valore viene sostituito all’etichetta COMPENSO_ORARIO Operatori aritmetici:
- +, - (unari), applicabili a tutti i tipi numerici
- +, -, *, / (binari), applicabili a tutti i tipi numerici
- % (resto della divisione, binario), applicabile solo agli interi Quando un valore di tipo char occorre in un’espressione aritmetica, esso viene interpretato come un intero Es. char a = “A”; (65 in ASCII) char b = “B”; (66 in ASCII) printf(“%d\n”, a+b); //Stampa 13 1 Le espressioni che, oltre a calcolare un valore, effettuano operazioni sulla memoria (ad esempio un’assegnazione) sono chiamate espressioni con side-effect: il termine side-effect si usa proprio per indicare una modifica nella memoria Nelle espressioni del tipo variabile = espressione, l’espressione a destra può a sua volta essere un’espressione con side-effect, in particolare contenente l’operatore = Per ciascun operatore +, -, *, /, % esiste il corrispondente +=, -=, *=, /=, %= La specifica di formato inizia con il carattere % ed è seguita da una combinazione di caratteri che dipendono dal tipo di dato da memorizzare:
- (^) Cast = converte dati da un tipo ad un altro in maniera esplicita: ( tipo ) espressione —> converte il tipo di espressione in tipo l’operatore di cast ha precedenza più alta rispetto agli operatori aritmetici Es. float x = 7.0f; float y = 2.8f; int a = x / y; //Div.reale —> 2. 5 int a = (int) x / (int) y; //Div.int. —> 3 printf(“%d\n”, a); //Stampa 2 printf(“%d\n”, a); //Stampa 3 ! x e y rimangono di tipo float, è solo il valore restituito dalle espressioni ad essere convertito
- (^) sizeof() = applicato ad un tipo, restituisce la quantità in memoria, in byte, necessaria a memorizzare un valore del tipo specificato come parametro il valore restituito da sizeof è di tipo intero senza segno sizeof può anche essere applicato ad espressioni (incluse costanti e variabili) Es. i = sizeof(char); // i vale 1 (Byte)
- (^) typedef = definisce nuovi tipi che sono sempre compatibili con il tipo base: Es. typedef int TipoA; typedef int TipoB; TipoA a = …; TipoB b = …; l’istruzione int x = a+b; è valida e ritorna un risultato di tipo int
Puntatori
MEMORIA DI UNA
MACCHINA
I valori delle variabili di tipo puntatore sono indirizzi di memoria, cioè valori numerici che fanno riferimento a specifiche locazioni di memoria Es. int i = 1 5 printf(“L’indirizzo di i è %p\n”, &i); //Stampa “L’indirizzo di i è 0xbffff47c” printf(“mentre il valore di i è %d\n”, i); //Stampa “mentre il valore di i è 1 5
- (^) L’operatore & si chiama operatore “indirizzo-di”, e restituisce l’indirizzo della prima cella del gruppo identificativo della variabile a cui viene applicato
- (^) %p = specifica di formato dei puntatori (indipendentemente dal tipo di dato a cui il puntatore fa riferimento)
- (^) l’operatore * si chiama operatore di “dereferenziamento”, e permette di accedere al valore contenuto nella locazione identificata da un certo indirizzo Es. int j = 1 int i = *&j —> * restituisce il valore contenuto nella locazione di memoria puntata dal suo argomento (cioè il risultato dell’espressione &j) —> l’istruzione int i = *&j; equivale a i = j; Quando si dichiara una variabile di tipo puntatore è necessario specificare che tipo di dato è contenuto nella locazione puntata ! La dichiarazione di una variabile puntatore non “crea” la variabile puntata Es. int i; // Alloca memoria per un intero int *p; // Crea la variabile puntatore p = &i; // assegna a p l’indirizzo della locazione associata ad i *p = 10; // OK printf(“i = %d\n”, i); // Stampa 1 0 Avrei potuto anche scrivere “int *p, i;”, ma i sarebbe comunque stato semplicemente un intero Es. int i, j, k; int *pt_i, *pt_j; pt_i = &i; pt_j = &j i = 1; j = 2; k = *pt_i + *pt_j; pt_i = 10; printf(“i = %d\n”, i); // Stampa i = 1 0 printf(“k = %d\n”, k); // Stampa k = 3 Se con due puntatori faccio riferimento ad una stessa area di memoria, quell’area è detta “condivisa”, e le modifiche effettuate tramite un puntatore sono visibili tramite l’altro (e viceversa) Es. int p; char c = ‘x’; p = &c; // WARNING! p punta a int mentre c è char L’operatore di uguaglianza (==) tra puntatori verifica l’uguaglianza tra gli indirizzi contenuti nelle variabili puntatore, non tra il contenuto delle variabili puntate Es. int p=1, q=1, r=3; int *p1=&p, *p2=&p, *p3=&q, p4=&r; if (p1 == p2){…} // TRUE: p1 e p2 puntano alla stessa variabile if (p1 == p2){…} // TRUE: le variabili puntate da p1 e p2 hanno stesso valore if (p2 == p3){…} // FALSE: p2 e p3 puntano a variabili diverse if (p2 == *p3){…} // TRUE!: le variabili puntate da p2 e p3 hanno stesso valore
Si può anche definire un puntatore ad una variabile di tipo puntatore Es. double x; double* pt; double** ptpt; x = 4; pt = &x; ptpt = &pt; printf(“%lf\n”, **ptpt);
- (^) NULL = Le variabili di tipo puntatore possono assumere anche il valore speciale NULL, specificando di conseguenza che la variabile non punta ad alcuna locazione di memoria, ma comunque inizializzandola —> il confronto con NULL può essere usato per verificare se una variabile di tipo puntatore punta ad una locazione di memoria Es. int* pt = NULL; if (pt != NULL) *pt = 10; l’istruzione *pt = 10, sebbene in questo caso non venga eseguita, genererebbe un errore
- (^) Void = poiché la memoria allocata per i puntatori è fissa, si può omettere la specifica del tipo della variabile puntata, dichiarando un puntatore di tipo void* i puntatori di tipo void sono utilizzati quando il tipo dei valori da trattare non è noto a priori, e il contenuto della locazione da loro puntata non può essere assegnato direttamente ad un’altra variabile Es. void* pt; … int j = pt; //ERRORE! per poter effettuare questa assegnazione, è necessario prima eseguire una conversione: Es. void pt; … int* pti = pt; // Conversione automatica int* pti2 = (int*) pt; // Casting esplicito int j = pti2; // pti2 è un puntatore a int o più brevemente int j = ( (int) pt ); // ( (int) pt ) è un puntatore a int
- (^) malloc(n) = definita nella libreria stdlib.h, permette di allocare dinamicamente spazio di memoria, con n byte contigui di memoria, e restituisce un puntatore di tipo void, contenente l’indirizzo alla prima cella allocata —> per accedere correttamente alla memoria allocata, il puntatore restituito deve essere convertito nel tipo opportuno Es. #include <stdlib.h> … char p = (char*) malloc(sizeof(char)); …
- (^) free() = la memoria allocata e non più utilizzata deve essere rilasciata con questa funzione Es. #include <stdlib.h> char* p = (char*) malloc(sizeof(char)); … free(p); // rilascia la memoria puntata da p ! La funzione free può essere invocata solo su un puntatore che fa riferimento ad una locazione precedentemente allocata dinamicamente Una volta rilasciata, la memoria libera può essere riallocata, ma si noti che dopo il rilascio, in questo caso, p punta ad una locazione libera
- (^) scanf() pt.2 = come visto, data una variabile x, il rispettivo indirizzo di memoria può essere ottenuto tramite l’operatore &, ma se il riferimento è memorizzato in una variabile puntatore, può essere usato direttamente Es. int* p = (int*) malloc(sizeof(int)); printf(“inserisci un valore intero\n”); scanf(“%d”, p); printf(“Il valore inserito è: &d\n”, *p)
Funzioni
Sintassi: tipoRisultato nomeFunzione(parametriFormali) { istruzioni } Se la funzione non restituisce risultati, allora viene utilizzata per effettuare delle operazioni (ed il tipo del risultato è void) Se il tipo della funzione è void, l’istruzione return si può omettere, oppure può essere usata semplicemente per interrompere l’esecuzione della funzione stessa Segnatura di una funzione: consiste nel nome della funzione e nella descrizione(tipo, numero e posizione) dei suoi parametri —> Il C non consente l’overloading (sovraccarico), in quanto non si possono definire funzioni con lo stesso nome e diversa segnatura Es. int somma(int x, int y) { return x+y; } int somma(double x, double y) { return (int) (x+y); } Le due definizioni non sono compatibili in C!
B. Passaggio di parametri tramite puntatori Con il passaggio di puntatori diventa possibile utilizzare il puntatore per restituire al programma chiamante il risultato di una funzione Il passaggio tramite puntatori risulta necessario anche quando i dati che devono essere scambiati tra funzione chiamata e programma chiamante sono voluminosi ed il passaggio del puntatore risulta molto più efficiente sia in termini di occupazione di memoria che di tempo di calcolo (non occorre la copia del parametro) Es. void swap(int* a, int* b) { int temp = *b; *b = a; a = temp; } int main() { int i = 1, j = 2; printf(“i = %d\n”, i); // i = 1 printf(“j = %d\n”, j); // j = 2 swap(&i, &j); printf(“dopo swap\n”); printf(“i = %d\n”, i); // i = 2 printf(“j = %d\n”, j); // j = 1 } Es. #include <math.h> #include <stdio.h> void puntoMedio(double x1, double y1, double, x2, double y2, double xr, double yr) { *xr = (x1 + x2)/2; *yr = (y1 + y2)/2; } int main() { double x1 = 1, y1 = 1, x2 = 3, y2 = 5, xm, ym; puntoMedio(x1, y1, x2, y2, &xm, &ym); printf(“punto medio = (%lf,%lf)\n”, xm, ym); }
quando il parametro non deve essere modificato dalla funzione, si può usare lo specificatore const Es. void f(const double* x) { *x = 2.0; //errore di compilazione: x non può essere modificato return; } int main() { double d; d = 1.0; f(d); printf(“d = %f\n”, d); return 0; } Le funzioni C possono anche restituire valori di tipo puntatore o di tipo riferimento Es. double *puntatore(double a) { double r = (double) malloc(sizeof(double)); r = a2; return r; } int main() { double pd = puntatore(5.4); printf(“pd = %p\n”, pd); printf(“pd = %f\n”, *pd); return 0; in questo esempio la funzione puntatore crea un puntatore ad una variabile di tipo double e la inizializza con il valore passato come argomento ! la variabile a cui punta il risultato della funzione deve essere allocata dinamicamente: double puntatore(double a) { double d = a2; double *r = &d; return r; } in questo esempio la funzione puntatore restituisce il puntatore ad una variabile che viene rilasciata al termine dell’esecuzione della funzione! Il corpo di una funzione può contenere come abbiamo visto dichiarazioni di variabili, dette variabili locali, per le quali consideriamo due aspetti fondamentali:
- campo d’azione (nozione statica, che dipende solo dal testo del programma)
- tempo di vita (nozione dinamica, che dipende dall’esecuzione del programma) —> Il campo d’azione (o scope) di una variabile è l’insieme delle unità di programma in cui la variabile è visibile (cioè accessibile e utilizzabile): una variabile dichiarata in un qualsiasi blocco (istruzione {…}) è visibile in quel blocco (inclusi eventuali blocchi interni), ma non è visibile all’esterno del blocco stesso
Organizzazione di un programma in file multipli
Programmi complessi possono essere organizzati in più file —> i file contenenti specifiche di programmi si distinguono in:
- file di intestazione o header (estensione .h)
- file di implementazione (estensione .c) Nei file header vengono inserite solamente le dichiarazioni delle funzioni, mentre nei file di implementazione anche il corpo che implementa la funzione Solitamente la funzione main si trova in un file separato da quello in cui vengono definite altre funzioni ! Ogni file che usa funzioni definite altrove deve includere il relativo file header (ma non il file con l’implementazione c!) Es. File f.h // dichiarazione delle funzioni radiceQuadrata e stampa double radiceQuadrata(double x); void stampa(double x); File f.c #include <stdio.h> #include <math.h> #include “f.h” // implementazione della funzione radiceQuadrata e stampa double radiceQuadrata(double x) { return sqrt(x); } // implementazione della funzione stampa void stampa(double x) { pritnf(“x = %f\n”, x); } File main.c #include “f.h” // implementazione della funzione main int main(int x) { stampa(radiceQuadrata(25)); } File multipli vengono compilati indicando tutti (e soli) i file .c Es. gcc -o test f.c main.c Es. #define MAX(x,y) ((x)>(y)?(x):(y)) —> = x maggiore di y? se vero, (max =) x, altrimenti y
Tipi di dato strutturati
ARRAY
Un’array è una struttura contenente una collezione di elementi dello stesso tipo, ciascuno indicizzato da un valore intero Una variabile di tipo array è un riferimento alla collezione di elementi che costituisce l’array Per usare un array in C occorre:
- dichiarare una variabile di tipo array specificandone la dimensione (numero di elementi contenuti)
- accedere mediante la variabile agli elementi dell’array per assegnare o leggerne i valori (trattando ciascun elemento come se fosse una variabile Semantica Array:
- tipo nomeArray[n];
- tipo è il tipo degli elementi contenuti nell’array
- nomeArray è il nome della variabile (riferimento ad) array che si sta dichiarando
- n è un’espressione costante che rappresenta il numero di elementi dell’array
- —> alloca un’array di n elementi di tipo tipo e crea la variabile nomeArray di tipo array Es. int a[5] //a è una variabile di tipo array di 5 elementi interi Una dichiarazione di variabile di tipo array comporta un’allocazione dell’array di tipo statico: ciò significa che lo spazio di memoria contenente l’array è fissato al momento della dichiarazione e non varia durante l’esecuzione del programma (contrariamente al suo contenuto, che è ovviamente modificabile) —> la dimensione dell’array rimane invariata per tutto il tempo di vita, che corrisponde all’inizio e al termine del blocco in cui è stato dichiarato Es. int a[5]; printf(“%d byte”, sizeof(a)); // Stampa 20 byte (5 int da 4 byte) printf(%d elementi”, sizeof(a)/sizeof(int)); // Stampa 5 elementi (20/4) Si può accedere ai singoli elementi di un array tramite l’operatore di subscripting (o indicizzazione) [ ]: nomeArray [ indice ], dove nomeArray è l’identificatore della variabile array che contiene un riferimento all’array a cui si vuole accedere, e indice è un’espressione di tipo int non negativa che specifica l’indice dell’elemento a cui si vuole accedere —> accede all’elemento di indice indice dell’array nomeArray per leggerlo o modificarlo Es. int a[5]; // a è una variabile di tipo array di 5 interi a[0] = 23; // assegnazione al primo elemento dell’array a[4] = 92; // assegnazione all’ultimo elemento dell’array a[5] = 16; // ?????: l’indice 5 non è nell’intervello [0,4] In C è possibile inizializzare gli elementi di un array usando espressioni numeriche: tipo Array[] = {espressione-0, espressione-1, …, espressione-k-1} —> Array viene inizializzato ad un array di k elementi di tipo tipo , dove l’elemento di indice i ha valore pari al valore restituito dall’espressione espressione-i (opportunamente convertita, laddove necessario) Es. int v[4] = { 4, 6, 3, 1 };
Poiché un array identifica un insieme di blocchi di memoria contigui, avendo a disposizione il puntatore ad uno dei suoi elementi, è possibile accedere agli altri elementi applicando le operazioni di somma e sottrazione Es. int a[4] = {2, 4, 6, 8}; int* b = a; b = b+2; printf(“*b = %d\n”, b); // Stampa: b = 6 int c = &a[3]; c -= 2; printf(“c = %d\n”, *c); // Stampa: *c = 4 L’operatore [ ] può essere applicato ad un puntatore, indipendentemente dal fatto che esso punti ad un array o meno: il valore restituito da un’espressione della forma puntatore [indice], è pari al valore restituito dall’espressione *(puntatore+indice) —> l’espressione restituisce il contenuto della locazione puntata da puntatore+indice, non il valore del puntatore Es. int v, i, *p; … v = p[i]; sono v = (p+i); equivalenti Es. char v[3] = {‘a’, ‘b’, ‘c’}; char p = v; printf(“%c\n”, v[2]); // stampa c printf(“%c\n”, p[2]); // stampa c La differenza tra puntatori che puntano ad elementi di uno stesso array è pari alla differenza tra gli indici degli elementi puntati Es. int a[4] = {2, 4, 6, 8}; int b = &a[2]; int c = &a[1]; printf(“c-b = %ld\n”, c-b); // stampa - 1 printf(“b-c = %ld\n”, b-c); // stampa 1 La sottrazione tra puntatori che non fanno riferimento ad elementi di uno stesso array produce un comportamento indefinito
Anche gli array possono essere usati come parametri di funzione Es. int sommaValoriArray(int v[], int n) { int somma = 0; for (int i=0; i < n; i++) somma += v[i]; return somma; } int main() { const int n = 4; int x[n] = {0, 1, 2, 3}; printf(“somma = %d\n”, sommaValoriArray(x,n)); } È anche possibile indicare esplicitamente, nell’intestazione della funzione, il numero di elementi contenuti nell’array di input Es. void f(int v[3]) {…} —> questa funzione prende in input solo array con 3 elementi Poiché i parametri di tipo array vengono considerati a tutti gli effetti puntatori, è anche possibile usare un parametro di tipo puntatore, nella segnatura di una funzione, in luogo di un parametro di tipo array È anche possibile usare un puntatore in luogo di un array in un’invocazione a funzione Es. void f(char[] s) { … } int main() { char t[3] = {1, 2, 3}; char *p = t; f(p); // OK! ! L’invocazione della funzione sizeof su un parametro di tipo array, all’interno di una funzione, non restituisce la dimensione dell’array a cui il parametro fa riferimento, ma restituisce semplicemente la dimensione (in byte) dello spazio contenente il valore (int, char, double…) del parametro, essendo il parametro trattato come una variabile di tipo puntatore Es. void f(int v[], int n) { printf(“Dimensione di v: %lu bytes\n”, sizeof(v)); // stampa 4 printf(“Numero di elementi in v[]: %d\n”, n); }