Supponiamo di avere gia' visto
Il linguaggio C++
Puntatore = variabile che contiener l'indirizzo di una locazione di memoria contenente valori di un certo tipo (per esempio l'indirizzo di una variabile di quel tipo).
Definizione di variabile di tipo puntatore:
int * p; puntatore a int
void * q; puntatore di tipo generico,
il tipo di NULL
Memorizzare in un puntare l'indirizzo di una variabile: int x; p = &x;
Accedere (es. per modificare) il contenuto della locazione puntata da un puntatore: (*p) = 15;
La definizione int *p; dice che *p (il contenuto della locazione puntata da p) e' di tipo int.
Quando la stessa locazione e' accessibile attraverso
piu' variabili, le modifiche si ripercuotono su tutte, esempio:
int x;
int *p;
p = &x;
x = 10; /* qui anche (*p)=10 */
(*p) = 18; /* qui anche x=18 */
Array = sequenza di locazioni di memoria tutte dello stesso tipo.
Definizione:
int a[10]; array di 10 posti, contenuto non inizializzato
int b[10] = { 1, 2, 3 }; array di 10 posti, inizializzati i
primi tre
Solo in inizializzazione posso assegnare tutti gli elementi assieme, altrimenti dovrei scrivere int b[3]; b[0]=1; b[1]=2; b[2]=3;
Array a piu' dimensioni:
float tabella [4][3];
Per i=0...3 tabella[i] e' array di tre float,
tabella[i][j] e' un singolo float.
Anche gli array sono puntatori.
Una variabile di tipo array e' il puntatore alla prima posizione
dell'array.
int a[10];
int * p;
p = a; e' lecito
p[3] = 14; e' lecito
Pero' ci sono differenze:
Array la cui lunghezza e' nota a priori:
#define MY_SIZE 10
int a[MY_SIZE];
Array la cui lunghezza sara' nota solo a run-time.
int my_size; il valore sara' noto dopo
int * a; deve diventare un array lungo my_size
Allocazione dell'array dinamico a:
Deallocazione dell'array dinamico a:
Struttura = tipo composto da variabili di altri tipi (detti "campi" della strutura).
Dichiarazione del tipo: struct point { float x; float y; };
Definizione di variabili: struct point p1;
Accesso ai campi:
In C tutti i parametri delle funzioni sono passati per valore.
Il passaggio per riferimento si ottiene passando (per valore) un puntatore,
esempio:
Attenzione: gli array sono sempre passati per riferimento perche' un array e' il puntatore al suo primo elemento. Esempio:
Se devo passare delle strutture come parametro, per efficienza conviene passare un puntatore alla struttura, non la struttura stessa. Esempio:
In C abbiamo un tipo struttura ed operazioni che vi agiscono, esempio:
struct point { float x; float y; }; void print_point(struct point * p) { printf("Point %f,%f", p->x, p->y); } void set_point(struct point * p, float x0, float yo) { p->x = x0; p->y = y0; }
In java come in C++ abbiamo una classe che ingloba struttura e funzioni.
In Java:
class point { public: float x; float y; void print_point() { System.out.println("Point (" + x + "," + y + ")"); void set_point(float x0, float y0) { x = x0; y = y0; } point(float x0, float y0) { set_point(x0,y0); } };
class point { public: float x; float y; void print_point(void); void set_point(float x0, float y0); point(float x0, float y0); }; void point :: print_point(void) { printf("Point %f,%f", x, y); } void point :: set_point(float x0, float y0) { x = x0; y = y0; } point :: point(float x0, float y0) { set_point(x0,y0); }Sia in java che in C++:
Differenze del C++ rispetto a java:
Se le implementazioni delle funzioni sono molto semplici, posso scriverle direttamente nella dichiarazione della classe premettendo la parola chiave "inline":
class point { public: float x; float y; inline void print_point(void) { printf("Point %f,%f", x, y); } inline void set_point(float x0, float y0) { x = x0; y = y0; } inline point(float x0, float y0) { set_point(x0,y0); } };Questo autorizza (ma non obbliga) il compilatore a sostituire le chiamate alla funzione con la trascrizione delle linee di codice che appaiono nel suo corpo (e' simile a "#define" del C con la differenza che la sostituzione puo' anche non avvenire, dipende dal compilatore usato).
In C++ come in java e' ammesso avere in una classe
piu' funzioni con nome uguale e diversi argomenti.
Il compilatore sceglie quella giusta da chiamare guardando gli argomenti.
Nell'esempio potrei avere un altro costruttore:
inline point(void) { set_point(0,0); }Qui esemplificato su costruttore, vale per qualsiasi funzione definita in una classe.
Le dichiarazioni di variabili e funzioni in una classe possono essere precedute dalle parola chiave:
Nella definizione della variabile bisogna
specificare i parametri per il costruttore.
class point p1(10,24); oppure
class point p1();.
Poiche' esiste costruttore senza parametri e' lecito anche class point p1; che equivale a class point p1();
Come per le struct, spesso non si usano oggetti di una classe ma puntatori ad oggetti. Esiste nuova sintassi per allocare/deallocare puntatori ad oggetti.
Variabile di classe = ne esiste una sola copia
condivisa da tutti gli oggetti della classe.
Va specificata come "static".
Nell'esempio:
class point p1; /* crea p1 (0,0) */ p1.init_val = 16; class point p2; /* crea p1 (16,16) */La variabile di classe e' accessibile tramite qualunque oggetto della classe.
Se nell'esempio dichiariamo set_point come:
void set_point(float x0=0, float y0=0);llora possiamo chiamare:
I parametri di default possono essere piu' di uno ma
devono essere tutti in fondo.
point(float x0, float y0=0); e' lecito
point(float x0=0, float y0); non lo e'
Non si possono saltare parametri nel mezzo di una chiamata.
se ho void set_point(float x0=0, float y0=0);
nella chiamata p1.set_point(26); il parametro
mancante (per cui si usa il default) e' sempre y.
I parametri di default si specificano nella dichiarazione (la parte tra class point { ... };), non nell'implementazione della funzione (la parte con void point :: set_point(...);).
Esempio: la pixmap.
In astratto una pixmap e' una matrice contenente valori di colore RGB
da trascrivere su schermo.
In OpenGL i valori RGB devono essere memorizzati su 8 bit,
gli elementi della matrice vanno memorizzati tutti di seguito
(riga per riga) in un array unidimensionale.
struct OnePixel { unsigned byte r; unsigned byte g; unsigned byte b; }; class Pixmap { public: int w; int h; /* largezza, altezza */ struct OnePixel * pixels; /* array dinamico di pixel */ inline void make(int ww, int hh) { pixels = (struct OnePixel *) calloc(ww*hh, sizeof(struct OnePixel)); if (pixels) { w = ww; h = hh; } else { w = h = 0; } } inline void kill(void) { if (pixels) { free(pixels); pixels = NULL; } w = h = 0; } inline Pixmap(void) { w = h = 0; pixels = NULL; } inline ~Pixmap(void) { kill(); } inline int pos(int i, int j) { return (i+w*j); } };La funzione make alloca l'array dinamico. Oltre al costruttore vi e' un distruttore (funzione con lo stesso nome della classe preceduto da ~, senza parametri ne' in ingresso ne' in uscita) che dealloca l'array dinamico. La funzione pos ritorna la posizione nell'array unidimensionale del pixel che ha coordinate (i,j) nella matrice bidimensionale.
struct Pixmap * m = new Pixmap();
m->make(256,400); alloca l'array
delete m; dealloca chiamando distruttore
Come in java, sottoclasse eredita tutte le variabili e funzioni della
superclasse, puo' aggiungerne altre.
Esempio:
class point3D : public point { public: float z; inline point3D(float x0, float y0, float z0) : point(x,y) { z = z0; } };
La superclasse e' indicata con la sintassi ":", in C posso avere piu' di una superclasse (ereditarieta' multipla).
Ponendo "public" o "private" prima del nome della superclasse si specifica se le parti ereditate (variabili, funzioni) devono essere pubbliche o private nella sottoclasse.
Le parti dichiarate "private" nella superclasse non possono essere usate nella sottoclasse. Usare nella superclasse "protected" per impedirne l'uso all'esterno ma lasciarle usare alle sottoclassi.
La sottoclasse ha bisogno di un suo costruttore.
Il costruttore della sottoclasse puo' richiamare quello della superclasse solo come prima istruzione con sintassi speciale.
Posso reimplementare nella sottoclasse funzioni della superclasse.
La stessa funzione ha piu' implementazioni in classi diverse.
Se nella sottoclasse voglio chiamare una funzione polimorfa
prendendo l'implementazione della superclasse, uso la sintassi
"::".
Es: in point3D posso mettere:
void print_point() { point::print_point(); /* stampa x,y */ printf(",%f", z); /* aggiunge ,z per avere x,y,z */ }
Consideriamo il caso:
class point * ptr; ptr = new point3D(...); ptr->print_point();
La variabile ptr ha tipo superclasse point
ma contiene un oggetto di tipo sottoclasse point3D.
Quale implementazione di print_point viene chiamata?
In java sarebbe chiamata quella della sottoclasse point3D.
Java ha binding dinamico, cioe'
determina l'implementazione da usare a run-time guardando
il tipo dell'oggetto contenuto nella variabile in questo
momento dell'esecuzione.
E' piu' corretto
dal punto di vista object-oriented ma meno efficiente.
In C++ viene chiamata quella della superclasse point.
C++ ha binding statico, cioe' determina l'implementazione
da usare a compile-time in base
al tipo della variabile.
E' meno corretto
dal punto di vista object-oriented ma piu' efficiente.
Posso dichiarare che per una certa funzione voglio binding dinamico mettendo davanti alla dichiarazione della funzione nella superclasse point l'indicazione "virtual":
virtual init_point(...)Allora anche C++ chiama l'implementazione della sottoclasse point3D.
Esempio: classe astratta lettore di pixmap, dichiara una funzione di lettura che avra' implementazioni diverse nelle sottoclassi. Una sottoclasse per ogni formato di file da cui si puo' leggere una pixmap (RGB, GIF, BMP...).
class PixmapReader { public: virtual int readPixmap(FILE *fd, class Pixmap *map) = 0; };La funzione e' dichiarata "virtual" ed inizializzata con "=0".
Non posso creare oggetti di classe PixmapReader, posso crearli solo delle sue sottoclassi.
Esempio: sottoclasse lettore di pixmap da file in formato RGB:
class RGBreader : public PixmapReader { private: ...variabili e funzioni ausiliarie... public: RGBreader(void); /* costruttore */ ~RGBreader(); /* distruttore */ int readPixmap(FILE *fd, class Pixmap *map); /* funzione ereditata e implementata */ }; int RGBreader :: readPixmap(FILE *fd, class Pixmap *map) { ... }Allora posso scrivere:
Modulo = sottocomponente di programma compilabile separatamente.
Un modulo java coincide con una classe e con il file che contiene la classe e che ne porta il nome.
In C++, come in C, il concetto di modulo esiste a prescindere dalle classi.
In generale un programma C (vale anche per C++) e' composto da:
Un modulo principale, contenuto in un file, es. main.c, che definisce la funzione "main"
Vari moduli secondari (anche nessuno), ognuno diviso in due file, es. aux.h e aux.c
Esempio C:
/* file aux.h */ #ifndef AUX_H #define AUX_H struct { float x; float y } point; extern float init_val; extern void init_point(struct point * p); #endif /* file aux.c */ #include "aux.h" float init_val = 0; void init_point(struct point * p) { p->x = p->y = init_val; } /* file main.c */ #include#include "aux.h" int main(void) { struct point a; set_point(&a); }
I sorgenti C++ talvolta hanno estensione .cpp o .cc invece che .c, per distinguerli dai sorgenti C. Tuttavia non e' vincolante.
A differenza di Java, non e' obbligatorio che un modulo C++ coincida con una classe, pero' volendo assumere che sia cosi':
Esempio:
/* file aux.h */ #ifndef AUX_H #define AUX_H class point { public: float x; float y; void print_point(void); void set_point(float x0, float y0); point(float x0, float y0); }; #endif /* file aux.c */ #includeOppure usando il meccanismo "inline".#include "aux.h" void point :: print_point(void) { printf("Point %f,%f", x, y); } void point :: set_point(float x0, float y0) { x = x0; y = y0; } point :: point(float x0, float y0) { set_point(x0,y0); } /* file main.c */ #include "aux.h" int main(void) { class point a(23,12); a.print_point(); }
/* file aux.h */ #ifndef AUX_H #define AUX_H class point { public: float x; float y; inline void print_point(void) { printf("Point %f,%f", x, y); } inline void set_point(float x0, float y0) { x = x0; y = y0; } inline point(float x0, float y0) { set_point(x0,y0); } }; #endif
Il file main.c e' come prima. Siccome qui tutte le funzioni della classe sono "inline", il file aux.c non serve piu'. Se solo alcune funzioni fossero "inline", conterrebbe le implementazioni delle funzioni rimanenti.
Con fiferimento alla distribuzione gratuita della GNU
Supponiamo che il programma consista di un modulo principale (file main.c) e un altro modulo (file aux.h, aux.c). Se vi sono piu' moduli il discorso e' analogo.
Sia in C che in C++ la compilazione puo' avvenire in due modi
g++ aux.c main.c -o maindove -o main dice al compilatore che l'eseguibile risultante deve chiamarsi main (per default si chiama a.out).
g++ -c aux.c g++ -c main.c g++ aux.o main.o -o maindove le prime due linee generano file oggetto (non ancora eseguibili) di estensione .o, l'ultima linea esegue il linking e genera l'eseguibile main.
Il makefile e' un file di nome convenzionalmente makefile o Makefile che specifica una serie di "target" (di solito programmi eseguibili) e i comandi necessari per ottenerli (per compilarli). Per ogni target specifica anche quali file sono necessari per poterlo ottenere (dipendenze).
In generale la sintassi del makefile e':
target: lista di file comandodove
Il makefile per compilare il nostro programma conterra':
main: main.c aux.c aux.h g++ aux.c main.c -o main
main: main.o aux.o aux.h g++ aux.o main.o -o main main.o: main.c aux.h g++ -c main.c aux.o: aux..c aux.h g++ -c aux.c
La compilazione si ottiene poi con
Se il file ha un nome diverso da makefile o Makefile occorre aggiungere l'opzione -f nomefilemake al comando make.
Utile se i comandi di compilazione sono lunghi da digitare. Inoltre makefile si "accorge" se un target ha bisogno di essere rifatto, guardando date e ora dei file elencati nella lista delle dipendenze.
Altre cose da specificare in compilazione (opzioni al comando g++):
Nel makefile e' possibile definire anche costanti
cioe' nomi che si danno a certi pezzi di sintassi
(tipo #define del C).
Esempio: per il compilatore definire
CC = g++
e poi usare $(CC) al posto di g++
In genere si definiscono variabili tutto:
i flag, le directory include,
le directory di libreria, le librerie...
I commenti nel makefile sono preceduti da "#".