Breve Ripasso del linguaggio C
Michelangelo Diligenti Ingegneria Informatica e
dell'Informazione
diligmic@diism.unisi.it
Compilazione, linking, ecc
editor
compiler
linker librerie
binario sorgente
sorgente
sorgente
sorgente
oggetto
oggetto
oggetto
oggetto .obj, .o .cpp, .cc, .c
Compilare
●
Se avete più sorgenti:
g++ -o nome_binario nome_programma1.cc ...
nome_programmaN.cc
●
Vedremo modi migliori per gestire progetti con un elevato numero di file sorgente
●
Attenzione: uno ed uno solo file sorgente deve contenere la funzione main
– La funzione main fornisce il punto iniziale per l'esecuzione del programma
– In caso di definizioni multiple il linker non saprebbe scegliere quale usare per far iniziare il programma
Funzioni
●
Le funzioni permettono di suddividere il codice in sottoparti modulari e combinabili
– Riutilizzabili evitanto la ripetizione del codice
– Permettono di contenere la dimensione degli eseguibili
– Il compilatore crea un insieme di istruzioni a cui e' possibile saltare nel momento in cui la funzione e' chiamata
Funzioni e compilatore
istruzione 1 istruzione 2
Istruzione 3: Chiamata funzione
Flusso Programma
istruzione Funzione 1 istruzione Funzione 2 istruzione Funzione 3
...
...
●
Compilatore trasforma codice in sequenza di istruzioni in linguaggio macchina
– Istruzioni sono eseguite in sequenza
Funzioni
istruzione 1 istruzione 2
Istruzione 3: Chiamata funzione
istruzione Funzione 1 istruzione Funzione 2 istruzione Funzione 3
...
...
●
Chiamata a funzione cambia flusso programma
– Salto alle istruzioni che implementano la funzione
– Terminata la funzione si risalta a dopo la chiamante
...
...
Funzione Salto (Jump)
Funzioni
●
Funzioni si definiscono ed usano come
tipo_ritornato NomeFunzione(tipo param1, …, tipo paramN);
Esempio
int Fact(int k) { int res = 1;
for (int i = 2; i <=k; ++i) res *= i;
return res;
}
int k = Fact(4);
Main
●
Main è una funzione speciale definita come:
int main(int argc, char** argv) { … codice … }
argc: numero argomenti passati
argv: argomenti passati. argv[0] nome del programma
#include <stdlib.h>
#include <stdio.h>
int main(int argc, char** argv) { int k = atoi(argv[1]);
printf(”%d\n”, k);
return 0;
}
Librerie
Funzioni di libreria
Valore ritornato: 0 vuol dire tutto OK
Main e funzioni
●
Esercizio
Compilate il programma precedente ed eseguitelo
Cosa succede se non si passano argomenti dalla linea di comando?
Correggete il programma in modo che funzioni anche se non sono passati argomenti
Aggiungete al programma la funzione per calcolare il fattoriale e ritornate il numero fattoriale
dell'argomento
Puntatori e references
●
& prende indirizzo di una variabile
●
* dereferenzia un indirizzo (puntatore) e prende il contenuto della cella di memoria puntata
●
Esempi
int i = 3; valore di i = 3
int *pi = &i; pi uguale ad indirizzo di i (punta ad i)
int &ri = i; ri riferisce all'indirizzo di i
ri = 4; cambio valore alla cella riferita da ri, cosa succede al valore di i?
*pi = 5; dereferenzio pi, e prendo il valore della cella e lo pongo pari a 5.
Cosa succede ad i?
Passaggio di parametri
Chiamante
Funzione chiamata void Func(int i)
Se func modifica i, cambia una sua copia non k stesso
int k
Func(k) int k
Func(k)
i copia separata di k
Passaggio di parametri
Chiamante
Funzione chiamata void Func(int i)
Se func modifica i, cambia una sua copia non k stesso
int k
Func(k) int k
Func(k)
i copia separata di k
Passaggio di parametri
Chiamante
Funzione chiamata void Func(int i)
Quando func esce la copia viene
distrutta int k
Func(k)
i copia di k
Passaggio di parametri
Chiamante
Funzione chiamata void Func(int* i)
int puntato da k
Adesso anche i punta alla stessa cella di k.
Se func modifica il contenuto di i, modifica anche il contenuto di k!
int k
Func(&k)
i* copia del
puntatore
&k
Passaggio di parametri
Chiamante
Funzione chiamata void Func(int* i)
Quando la funzione esce la copia viene eliminata
Ma la modifica della cella
puntata resta!
int puntato da k
int* k Func(k)
i copia del
puntatore k
Passaggio di parametri
●
Tre modi di passare i parametri, per valore, per indirizzo o per riferimento
●
Passaggio per valore
void Function(int i); // passo il valore di i, che viene
// copiato e reso disponibile nella funzione
ATTENZIONE: copia di i distrutta uscendo da Function
#include <stdio.h>
void Function(int i) { i = 5; } int main() {
int i = 0;
Function(i);
printf(”%d\n”, i);
}
Passaggio di parametri
●
Passaggio per indirizzo
void Function(int* i); // passo l'indirizzo di i, il puntatore eliminato uscendo da Function (ma non la cella puntata)
#include <stdio.h>
void Function(int* i) { *i = 5; } int main() {
int i = 0;
Function(&i);
printf(”%d\n”, i);
}
Passaggio di parametri
●
Passaggio per riferimento
● void Function(int &i); // passo il riferimento all'indirizzo di i
#include <stdio.h>
void function(int& i) { i = 5; } int main() {
int i = 0;
Function(i);
printf(”%d\n”, i);
}
Passaggio di parametri
●
Esempio
#include <stdio.h>
#include <sys/time.h>
#include <vector>
bool function1(std::vector<int> vec) { return (vec.size() == 0); }
int main() {
struct timeval starttime, endtime;
gettimeofday(&starttime, NULL);
std::vector<int> vec(1000000, 1);
for (int i = 0; i < 10000; ++i) function1(vec);
gettimeofday(&endtime, NULL);
long msec = 1000000*(endtime.tv_sec - starttime.tv_sec) + (endtime.tv_usec - starttime.tv_usec);
printf("%ld\n", msec);
}
Dato grande 4*1000000 bytes, lo copio 10000 volte chiamando la funzione
Passo il vettore per valore
Misuro il tempo in microsecondi
Passaggio di parametri
●
Esempio, con passaggio per indirizzo
#include <stdio.h>
#include <sys/time.h>
#include <vector>
bool function2(std::vector<int>* vec) { return (vec->size() == 0); }
int main() {
struct timeval starttime, endtime;
gettimeofday(&starttime, NULL);
std::vector<int> vec(1000000, 1);
for (int i = 0; i < 10000; ++i) function2(&vec);
gettimeofday(&endtime, NULL);
long msec = 1000000*(endtime.tv_sec - starttime.tv_sec) + (endtime.tv_usec - starttime.tv_usec);
printf("%ld\n", msec);
}
Copio 10000 volte 8 bytes Chiamando la funzione
Ora Copio solo il puntatore (8 bytes)
Misuro il tempo in microsecondi
Passaggio di parametri
●
Quando si passa un parametro, si fa una copia dello stesso
●
In genere questo non costa molto: per tipi come int, float, long in, ecc. Si tratta di 4-8 byte.
●
Ma questo può essere costoso. Ad esempio, se un
tipo è composto da 4*106 byte
Ritornare i risultati
●
Fare attenzione a come si ritornano i risultati di una funzione
Stesse attenzioni del passaggio di parametri
Ritornare i risultati per valore
●
Per valore: il dato ritornato viene copiato in nuova variabile
– Esempio
int function() { int i = 0;
return i;
}
int j = function(); // CORRETTO: contenuto di i // copiato in j e poi i viene distrutto
Il valore copiato resta disponibile fuori
Ritornare i risultati
●
Per indirizzo, si ritorna l'indirizzo della variabile
– indirizzo viene copiato e ritornato
#include <stdio.h>
int* function() {
int* i = new int; // puntatore i copiato in j e poi eliminato in return i; // uscita ma la cella resta allocata
}
int main() {
int* j = function(); // OK!
printf(”%d\n”, *j)
delete j; // ricordarsi di deallocare }
Ritornare i risultati
●
Per riferimento, fare molta attenzione int& function() {
int i = 0;
return i;
}
int& j = function(); // NO! i viene distrutto per cui il // riferimento non valido fuori
●
Ritornare per riferimento sembra poco utile adesso,
ma lo rivedremo quando studieremo il concetto di
classe
Ritornare i risultati
●
Per indirizzo o riferimento. Attenzione!
int* function() { int i = 9; return &i; }
int* j = function(); // attenzione i viene deallocato all'uscita della funzione, j punta ad una cella
deallocata!!!
int& function() { int i = 9; return i; }
int& j = function(); // anche qui riferimento verso a cella deallocata!
Ritornare i risultati
●
Per indirizzo
●
Attenzione di nuovo...
●
int* function() { int i = 0; return &i; } Come chiamo la funzione?
int* j = function(); // NO! i viene eliminato all'uscita.
// j punta ad una cella di memoria
// non più allocata
Ritornare i risultati
●
Alternativa: passare un puntatore alla funzione
– Specifica dove la funzione mi scrive l'output
– Esempio
void function(int* i) { *i = 0;
}
int i = 5;
function(&i);
printf(“%d”, i);
Ritornare i risultati
●
Una funzione deve fornirmi un risultato complesso
●
Alternativa 1
struct Point {int x; int y; };
Point* function() {
Point* p = new Point;
p->x = 0; p->y = 0;
return p;
}
Point* p = function(); … // do something with p
delete p;
Ritornare i risultati
●
Alternativa 2
struct Point {int x; int y; };
void function(Point* p) { p->x = 0; p->y = 0; };
Point p;
function(&p);
●
Alternativa 2 va quasi sempre preferita
Non si deve allocare dinamicamente
si evitano bugs di memoria o leaks perché si scorda di deallocare
Chiarezza e leggerezza sintattica
Ritornare i risultati
●
Esercizio: scrivere un programma che
usa la struct per rappresentare un Punto data in precedenza
contiene una funzione che assegna alle coordinate del punto dei valori passati come argomenti
la funzione ritorna il punto usando l'alternativa 2
il Main legge le coordinate (x,y) da linea di comando, chiama la funzione e stampa le coordinate del punto sullo schermo
Constness
●
Previene che variabile possa essere modificata
●
Utile per prevenire cambiamenti ad una variabile a livello di compilazione.
Talvolta si cambia accidentalmente una variabile e si creano bugs difficilmente trovabili
Const garantisce che tali bug sono trovati in compilazione
void Function (int &i) {
i = 5; // credo di fare un'operazione locale ma invece cambio all'esterno }
void Function (const int &i) {
i = 5; // Errore in compilazione, i non può essere cambiata }
Constness
void Function (const int *i) { int j;
*i = 5; // errore in compilazione, il puntatore è const i = &j; // si può fare
}
void Function (const int * const i) { int j;
*i = 5; // Errore in compilazione i = &j; // Errore in compilazione }
Constness
void Function (const int i) {
i = 5; // Errore in compilazione
int* j = &i; // Errore in compilazione const int* j = &i; // OK
const int k = 3;
k = 5; // Errore }
Non solo per i parametri.
Lo si può usare per qualsiasi variabile
Passaggio di parametri e const
●
Esempio, con passaggio per riferimento
#include <stdio.h>
#include <sys/time.h>
#include <vector>
bool function2(const std::vector<int>& vec) { return (vec.size() == 0); }
int main() {
struct timeval starttime, endtime;
gettimeofday(&starttime, NULL);
std::vector<int> vec(1000000, 1);
for (int i = 0; i < 10000; ++i) function2(vec);
gettimeofday(&endtime, NULL);
long msec = 1000000*(endtime.tv_sec - starttime.tv_sec) + (endtime.tv_usec - starttime.tv_usec);
printf("%ld\n", msec);
}
Copio 10000 volte 8 bytes Chiamando la funzione
Ora Copio solo il puntatore (8 bytes)
Misuro il tempo in microsecondi
Passaggio di parametri e const
●
Il C/C++ permette di passare i parametri in molti modi simili (es. Riferimento o puntatore)
●
Consiglio di usare solo 2 modi per tipi complessi
Passaggio per riferimento const per parametri che non devo poter essere cambiati
Passaggio per indirizzo per parametri che devono essere cambiati dalla funzione
Vantaggi di questo stile:
Massima velocita visto che tutto è passato per indirizzo
Chiarezza per il chiamante di cosa potenzialmente cambia
Uso consistente del const che evita bugs
Passaggio di parametri e const
●
Esempio (seguirò questo stile in seguito)
Funzione
p1, p2 dati passati che sono non modificabili
variabile center viene modificata dalla funzione
struct Point {int x; int y; };
void Center(const Point& p1, const Point& p2, Point* center);
Finite l'esercizio a casa...
Consigli di stile (IMPORTANTE)
●
Spezza linee lunghe in più righe che stanno sullo schermo (mio consiglio max 80 caratteri per riga)
– Codice rimane leggibile se lavorate su un laptop
●
One definizione/comando per riga
●
Usa le maiuscole per i nomi di tipo (struct Point) o di funzioni (void Center(…))
●
Usa le minuscole per i nomi delle variabili
●
Non usare nomi brevi, ma nomi che spiegano
immediatamente cosa fa una funzione o variabile
Consigli di stile (IMPORTANTE)
●
Usa indentazione in modo consistente
●
Usa linee bianche per spezzare il codice in unità semantiche
●
Commenta il codice in modo appropriato
●