Dal C al C++ Corso di Linguaggi di Programmazione ad Oggetti 1 a cura di Giancarlo Cherchi 1
Commenti Non aumentano la dimensione del programma eseguibile ma si rivelano utili per riassumere il comportamento degli algoritmi Il C++ ammette due delimitatori di commento: la coppia /* e */ (ereditata dal C) la doppia barra // La coppia può trovarsi ovunque sia consentito porre uno spazio, una tabulazione, una nuova riga e può occupare più righe di programma 2
Commenti Le coppie di delimitatori NON possono essere annidate tra loro! Esempi di utilizzo: int a = 1, /* commento interno */ m = 2; void foo() { /* questa funzione calcola il valore */ } int c = 3; // commento sino a fine riga 3
Tipi primitivi Oltre ai tipi tradizionali del C (int, float, char, double), sono stati introdotti i tipi bool e wchar_t; bool può assumere soltanto i due valori false e true: bool res = (2 > 3); if (res) f(); I valori false e true corrispondono rispettivamente alle costanti 0 e 1. Il compilatore è in grado di effettuare delle conversioni implicite tra i tipi bool e int. 4
Tipi primitivi wchar_t definisce un carattere esteso (detto wide-character ) ed è nato per supportare insiemi di caratteri a 16 bit per la gestione di simboli in diverse lingue, come i caratteri in cinese. Le costanti sono precedute dal carattere L e sono ammesse anche stringhe tra doppi apici (terminanti con un NULL, anch esso a 16 bit). wchar_t ch = L a ; wchar_t wstr = L Stringa a caratteri estesi ; 5
Qualificatore const Permette di trasformare un oggetto in una costante: const int bufsize = 512; Ogni tentativo di modificare l oggetto produce un errore in fase di compilazione Un oggetto const va sempre inizializzato: const int a; // errore! 6
Qualificatore const Attenzione ai puntatori! const int len = 10; int * pl = &len; *pl += 2; // len viene modificato!!! La soluzione è di dichiarare la variabile pl come un puntatore ad una costante intera: const int * pl = & len; *pl = 5; // errore di compilazione 7
Prototipi La dichiarazione di una funzione comprende il tipo di ritorno, il nome e la lista dei parametri. Questi tre elementi costituiscono il prototipo della funzione NON è possibile usare una funzione e/o variabile senza che sia stato incontrato prima il suo prototipo/definizione La soluzione tipica è di collocare le dichiarazioni delle funzioni e la definizione delle funzioni inline in file header che possono essere inclusi in ogni file in cui vengono chiamate le funzioni 8
Prototipi Esempio: ERRATO: CORRETTO: int main() { int b = foo(); b += a; } int a = 2; int foo() { return 3; } extern int a; int foo(); int main() { int b = foo(); b += a; } 9
File Header Per evitare doppie definizioni di classi/funzioni/variabili, a causa della ripetuta inclusione di un file.h (e/o annidamento), è possibile sfruttare le direttive condizionali del preprocessore: #ifndef FILEH #define FILEH /* contenuto dell header */ #endif scrivendo il codice precedente in ogni file header, le definizioni sono compilate una volta soltanto. 10
Header standard E diventato obsoleto l uso dei file di libreria con suffisso.h, come ad esempio: #include <stdio.h> E preferibile invece usare: #include <cstdio> Ricordarsi però che i simboli sono definiti all interno del namespace std, quindi occorre anteporre std:: ai simboli o importare tutto il namespace tramite la direttiva using 11
Parametri di funzione void In C++ la dichiarazione void foo(); equivale a void foo(void); Cioè: la funzione foo NON accetta alcun parametro e non (come si intendeva nelle prime versioni di C) un numero imprecisato di parametri! 12
Tipo di ritorno Il tipo di ritorno di una funzione deve essere sempre specificato (NON è int di default) L unica eccezione è la funzione main: main() { /* */ } equivale a int main() { /* */ } Nota: i costruttori/distruttori di un oggetto NON restituiscono alcun tipo. 13
Funzioni inline E possibile snellire l overhead di una chiamata di funzione/metodo tramite la direttiva inline (usata di default per i metodi implementati all interno di una classe). Una funzione inline viene espansa nel punto del programma in cui viene chiamata (come se fosse una macro) Il meccanismo di inline è comunque una direttiva per il compilatore (ad esempio, le funzioni ricorsive non possono essere espanse) 14
Funzioni inline Esempio: inline int min( int v1, int v2 ) { return ( v1 < v2? v1 : v2 ); } l istruzione min (a,b) viene sostituita dall espressione: a < b? a : b; Nota: a differenza della classica macro con #define, il compilatore è in grado di effettuare un rigoroso controllo di tipo sui parametri 15
Overloading di funzioni E possibile utilizzare lo stesso nome per due o più funzioni che differiscono per numero e/o tipo dei parametri in ingresso. Esempio: int somma (int a, int b); float somma (float a, float b); int somma (int a, int b, int c); 16
Overloading di funzioni Attenzione! Due funzioni devono differire nella signature. Non possono differire solo nel tipo restituito: int somma (int a, int b); float somma (int a, int b); Scrivendo, infatti: somma (3,4); quale deve essere richiamata delle due? 17
Input/Output L input e output sono forniti dalla libreria iostream, una gerarchia di classi offerta come libreria standard. E possibile utilizzare in alternativa a printf e scanf del C, gli oggetti predefiniti cin e cout, insieme agli operatori << e >>: #include <iostream> using namespace std; /* */ cout << la somma è << 4 <<. ; char ch; cout << scrivere una lettera: ; cin >> ch; 18
Input/Output Gli operatori << e >> sono anche definiti di inserimento ed estrazione. Per andare a capo, è possibile utilizzare il simbolo endl: cout << Scrivo << endl << su due righe! << endl; 19
Input/Output su file L I/O su file è supportato dalla libreria fstream che estende iostream. I file di output sono oggetti di tipo ofstream I file di input sono oggetti di tipo ifstream Esempio: #include <fstream> // ofstream outfile ( prova.txt ); if (!outfile ) cout << Errore! ; outfile << Scrivo sul file! << endl; ifstream infile ( prova.dat ); if (!infile ) cout << File non trovato! ; while ( infile >> ch ) cout >> ch; // Stampo il file sul video 20
Dichiarazione variabili locali A differenza del C più tradizionale, è possibile dichiarare le variabili locali vicino al punto in cui vengono effettivamente utilizzate (ed è consigliabile farlo) e non necessariamente all inizio di un blocco. Esempio: for (int i = 0; i < 10; ++i) { cout << Ciao! ; cout << endl; int a = 2; a += i; } 21
Allocazione di memoria Al posto di malloc/calloc e free è possibile (e preferibile) utilizzare i nuovi costrutti new e delete. ptr = new type (init_value); delete ptr; Esempi: char * str = new char( A ); int * p_int = new int(10); float *p_float = new float; *p_float = 1.2f; delete str; NOTA: non è necessario specificare init_value. 22
Allocazione di memoria E possibile allocare/deallocare degli array utilizzando: aptr = new array_type[dim]; delete[] aptr; Esempio: char * mystr = new char[20]; delete[] mystr; Importante: MAI mescolare tra loro new/delete con malloc/free! 23
Allocazione di memoria Attenzione alla differenza tra: new int(10); new int[10]; La prima istruzione alloca lo spazio per un intero e gli assegna il valore 10; La seconda istruzione alloca lo spazio per un array di 10 interi (senza inizializzarne il valore). 24
Reference I riferimenti possono essere considerati come nomi alternativi (alias) per un oggetto: int ival = 1024; int & refval = ival; // refval è un rif. ad intero refval = 5; // anche ival assumerà il valore 5 Nota: agiscono come puntatori impliciti, nel senso che l effetto è simile ma non viene utilizzata esplicitamente la notazione dei puntatori. 25
Reference Un riferimento deve essere sempre inizializzato ad un oggetto e una volta definito, non può essere riferito ad un altro oggetto Principalmente i riferimenti sono usati come parametri formali di una funzione (di solito per passare oggetti come parametri, evitandone la copia) 26
Passaggio di argomenti Le funzioni usano uno spazio di memoria allocato sullo stack del programma, chiamato record di attivazione Lo spazio resta associato alla funzione sino al termine della sua esecuzione Ad ogni parametro è riservato uno spazio nel record di attivazione Il metodo predefinito per il passaggio di argomenti è per valore (i valori che la funzione manipola sono copie locali) 27
Passaggio di argomenti Il passaggio per valore non è adatto ad ogni occasione; in particolare, quando: l oggetto passato per argomento è molto grande (costo elevato in termini di spazio e tempo per allocare e copiare l oggetto sullo stack) devono essere modificati i valori degli argomenti passanti (come nella funzione swap) 28
Passaggio di argomenti Esempio: // swap errata: non scambia le variabili in ingresso! void swap ( int v1, int v2 ) { int tmp = v2; v2 = v1; v1 = tmp; } /*. */ int i = 10, j = 12; swap ( i, j ); // i e j continuano a valere 10 e 12 perché sono state modificate solo le copie locali della 29
Passaggio di argomenti Variante1: Uso di puntatori (come in C) void swap ( int * v1, int * v2 ) { int tmp = *v2; *v2 = *v1; *v1 = tmp; } /*. */ int i = 10, j = 12; swap ( &i, &j ); // si passano alla funzione swap gli indirizzi delle variabili da scambiare in modo che possa accedere alle corrispondenti locazioni di memoria 30
Passaggio di argomenti Variante2: Uso dei riferimenti void swap ( int & v1, int & v2 ) { int tmp = v2; v2 = v1; v1 = tmp; } /*. */ int i = 10, j = 12; swap ( i, j ); // il compilatore usa dei puntatori impliciti in quanto v1 e v2 diventano sinonimi dei parametri attuali i e j. 31
Passaggio per riferimento Quando si passa un oggetto per riferimento ad una funzione, questa ha accesso diretto all argomento e non ne viene creata alcuna copia. Se la funzione deve accedere solo in lettura all argomento ma si vuole comunque sfruttare l efficienza del passaggio per riferimento, si utilizza la direttiva const: void f ( const mytype & obj ) { mytype obj2; obj = obj2; // errore di compilazione: obj non è modificabile } 32
Riferimenti o puntatori? Un riferimento NON può essere riferito ad un oggetto diverso da quello a cui era stato inizializzato. Un puntatore può riferirsi ad oggetti diversi o addirittura a nessuno (puntatore nullo) Quindi, il puntatore è più flessibile, ma più scomodo da usare e può portare ad espressioni difficili e poco intuitive. 33
Riferimenti o puntatori? Esempio (per valore): class Matrix { /* */ Matrix operator+ (Matrix m1, Matrix m2 ); }; /* */ Matrix a, b, c, d; d = a + b + c; /* Corretto ma è inefficiente: ogni chiamata dell operatore richiede la copia delle matrici! */ 34
Riferimenti o puntatori? Esempio (con i puntatori): class Matrix { /* */ Matrix operator+ (Matrix * m1, Matrix *m2 ); }; /* */ Matrix a, b, c, d; d = a + b + c; // Non funziona! Occorre passare gli indirizzi d = & ( &a + &b ) + &c; // Corretto ma molto difficile da leggere! 35
Riferimenti o puntatori? Esempio (con i puntatori): class Matrix { /* */ Matrix operator+ (const Matrix &m1, const Matrix &m2); }; /* */ Matrix a, b, c, d; d = a + b + c; /* Ora è corretto, più intuitivo e ugualmente efficiente! */ 36
Parametri di default E possibile assegnare dei valori predefiniti ai parametri di una funzione (o metodo), in modo da non specificarli tutti al momento della chiamata. Esempio: int f (int a, int b = 1, int c = 3) { return a+b+c; } f(3) -> 7 f(2,1) -> 6 f(3,4,5) -> 12 f() -> errore: il primo parametro è obbligatorio! 37
Parametri di default Limitazione: i parametri di default devono essere specificati per ultimi. In altre parole, non è possibile intervallarli ai parametri standard. int f (int a, int b = 1, double c) ; // errore! int f (float a = 1.1, int c, int d = 0) ; // errore! int f (int a = 2, char c, double d); // errore! int f (int a = 0) ; // corretto int f (int a = 1, float b = 2.5) ; // corretto 38
Parametri di default Per convenzione, l argomento di default è specificato nel prototipo della funzione e può essere specificato una sola volta all interno di un file: void f (int, double = 1.2); void f (int a, double b) { } // OK void f (int a, double b = 1.2) { } // errore! 39