Materiale Ingegneria Informatica
Fondamenti di Programmazione
Introduzione al C++
Teoria Fondamenti di Programmazione
Il linguaggio C++ si basa sulla grammatica BNF (grammatica che definisce come deve essere strutturato il linguaggio di programmazione, dalla sua definizione sino alla sua scrittura vera e propria).
Elementi Letterali
Molto importanti in C++ sono gli elementi letterali, abbiamo:
- literals: gli elementi immutabili scritti nel codice, determinano costanti ma senza nome. Esempio:
char a = 'c',int b = 5. - identificatori: i nomi che un programmatore assegna a delle "celle di memoria". Esempio:
int questo_identificatore = 5. - keywords: parole chiave riservate al linguaggio (come
if,while) che il programmatore non può usare per gli identificatori. Identificano azioni che il calcolatore dovrà eseguire. - operator: i classici simboli "+", "-", "/", "*" che rappresentano le operazioni. Hanno priorità (es. "*" e "/" hanno priorità su "+" e "-"). Esempio:
int a = 4*9 + 3/4. - separator: elementi che separano stringhe di codice o altri elementi, come
();,{},[]. Esempio:int a = (b+c)/2;int v[5].
Definizione e Dichiarazione
La differenza sostanziale è che con la dichiarazione si dichiara l'esistenza di un oggetto (senza assegnargli spazio in RAM), mentre con la definizione si assegna un vero e proprio spazio in memoria.
Esempio: extern int a; è una dichiarazione. Stiamo dicendo che la variabile a esisterà, ma non avrà una locazione in memoria fino alla sua definizione.
Struttura della memoria:
- Memoria: insieme di celle.
- Cella: in genere dimensione di un byte (8 bit).
I Tipi in C++
Categorie di Tipi
Ci sono due diverse categorie di tipi:
- Tipi fondamentali
- Tipi predefiniti:
int,char,float,bool,double,unsigned - Tipi di enumerazione (
enum)
- Tipi predefiniti:
- Tipi derivati: si ottengono a partire dai tipi predefiniti e permettono di creare strutture complesse.
N.B:
- I tipi fondamentali sono chiamati anche tipi aritmetici.
- Il tipo intero e il tipo reale sono detti tipi numerici.
- Intero, booleano, carattere ed enumerati sono detti tipi discreti.
Intero (int)
La rappresentazione degli interi (con segno), dato N il numero di bit, è nell'intervallo:
[ -2(N-1), +2(N-1) - 1 ]
Di solito, per un int si usano 4 byte (N=32 bit).
// tipo short
short int s1 = 1; // letterale
short s2 = 2;
// tipo long int
long int ln1 = 6543; // letterale int
long ln2 = 6543L; // letterale long int (suffisso L)
long ln3 = 6543l; // letterale long int (suffisso l)
// letterale int ottale, prefisso 0 (zero)
int ott = 011; // ott = 9 (decimale)
// letterale int esadecimale, prefisso 0x o 0X
int esad1 = 0xF; // esad1 = 15 (decimale)
int esad2 = 0XF; // esad2 = 15 (decimale)
Intero Senza Segno (unsigned)
La rappresentazione degli interi unsigned, dato N bit, è nell'intervallo: [0, 2N - 1].
// tipo unsigned int
unsigned int u1 = 1U; // letterale unsigned, suffisso U
unsigned u2 = 2u; // letterale unsigned, suffisso u
// tipo unsigned short int
unsigned short int u3 = 3;
unsigned short u4 = 4;
// tipo unsigned long int
unsigned long int u5 = 5555;
unsigned long u6 = 6666UL;
unsigned long u7 = 7777LU; // letterale unsigned long, suffisso UL (o lu)
// Errore comune: overflow
unsigned short int u8 = -0x0001; // Warning: OVERFLOW (avrà valore 65535)
Reali (float, double)
Letterale reale (forma estesa):
[Parte Intera] . [Parte Frazionaria] E [+/- Esponente]
Esempio: 10.56 E-3 (Componente in virgola fissa)
- La parte intera o la parte frazionaria, se valgono zero, possono essere omesse.
// tipo double (precisione doppia)
double d1 = 3.3;
double d2 = -12.14e-3; // Notazione scientifica (-12.14 * 10^-3)
double d3 = 1.51;
// tipo float (precisione singola)
float f = -2.2f; // letterale float, suffisso F (o f)
float g = f - 12.12F;
// tipo long double (precisione estesa)
long double h = +0.1;
long double k = 1.23e+12L; // letterale long double, suffisso L (o l)
// 1.23e+12L significa 1.23 * 10^12
// 1.23e-12L significa 1.23 * 10^-12
Operazioni tra Reali e Interi
Le operazioni tra reali e interi, sebbene usino gli stessi simboli (+, /, ...), sono operazioni diverse (overloading).
int i = 1, j = 2;
int z = i / j;
// z = 0 (divisione INTERA, la parte decimale viene troncata)
double d1 = 1.0 / 2.0;
// d1 = 0.5 (divisione REALE)
double d2 = 1 / 2;
// d2 = 0.0 (ERRORE! 1 e 2 sono interi -> divisione intera -> 0. Poi 0 viene convertito in double 0.0)
double d3 = (double)i / j;
// d3 = 0.5 (Cast: i viene convertito in double -> divisione reale)
Booleani (bool)
I tipi booleani possono assumere solo due valori: true e false.
Quando convertiti da/in interi:
- 0 (zero) corrisponde a false.
- Qualsiasi numero diverso da zero (es. 6, -5, 1000) corrisponde a true.
Operazioni sui bool (logiche):
||: OR logico (disgiunzione)&&: AND logico (congiunzione)!: NOT logico (negazione)
Tabella di verità:
| p | q | p || q (OR) | p && q (AND) | !p (NOT) |
|---|---|---|---|---|
| false | false | false | false | true |
| false | true | true | false | true |
| true | false | true | false | false |
| true | true | true | true | false |
bool b1 = true, b2 = false;
bool b3 = b1 && b2; // b3 = false
bool b4 = b1 || b2; // b4 = true
// Precedenza: NOT (!) > AND (&&) > OR (||)
bool b5 = b1 || b2 && false; // b5 = true || (false && false) -> true
bool b6 = !b2 || b2 && false; // b6 = (!false) || (false && false) -> true || false -> true
N.B: Quando si stampa un'espressione logica con cout, usare sempre le parentesi!
bool b1 = true, b2 = false;
// L'operatore << ha precedenza su &&
cout << endl << b1 && b2; // STAMPA 1 (true)!
cout << endl << (b1 && b2); // STAMPA 0 (false) (corretto)
L'Implicazione (Logica)
L'implicazione (p → q, "se p allora q") non è un operatore C++ nativo per i bool.
- IMPLICAZIONE FALSA: solo quando p è vero e q è falso.
- IMPLICAZIONE VERA: In tutti gli altri casi (in particolare, se p è falso, l'implicazione è sempre vera).
Dalla tabella di verità si evince che p → q è logicamente equivalente a !p || q.
| p | q | p → q | !p | !p || q |
|---|---|---|---|---|
| false | false | true | true | true |
| false | true | true | true | true |
| true | false | false | false | false |
| true | true | true | false | true |
Tipi di Carattere (char)
Un char è un tipo intero (solitamente 1 byte) usato per memorizzare caratteri (es. in codifica ASCII).
Ordinamento:
- Le codifiche rispettano l'ordine alfabetico (
'a' < 'b') e numerico ('1' < '2'). - La relazione tra maiuscole e minuscole dipende dalla codifica (es. in ASCII
'A' < 'a').
Valore:
- Un carattere può essere rappresentato dal suo valore decimale, ottale o esadecimale.
- Ottale: preceduto da
\(es.'\143'). - Esadecimale: preceduto da
\x(es.'\x63'). - Nota: I letterali carattere vanno racchiusi fra apici (es.
'\n','\15').
Sequenze di escape
Combinazioni speciali per caratteri non direttamente rappresentabili:
- Nuova riga (LF):
\n - Tabulazione orizzontale:
\t - Ritorno carrello (CR):
\r(permette la sovrascrittura della riga corrente) - Barra invertita:
\\ - Apice:
\' - Virgolette:
\"
Esempi (char):
char c1 = 'c', t = '\t', d = '\n';
char c2 = '\x63'; // 'c' (esadecimale)
char c3 = '\143'; // 'c' (ottale)
char c4 = 99; // 'c' (decimale, codice ASCII)
cout << c1 << t << c2 << t << c3 << t << c4 << d; // Stampa: c c c c
// Aritmetica sui char (basata sui loro codici ASCII)
char c5 = c1 + 1; // 'd' (99 + 1 = 100)
char c6 = c1 - 2; // 'a' (99 - 2 = 97)
char c7 = 4 * d + 3; // '+' (basato sul valore ASCII di '\n')
int i = c1 - 'a'; // 2 (99 - 97)
cout << c5 << t << c6 << t << c7 << t << i << d;
bool m = 'a' < 'b'; // m = true
bool n = 'a' > 'c'; // n = false
Tipo Enumerazione (o enumerati)
Sono tipi definiti dal programmatore, costituiti da un set di costanti intere (enumeratori).
Vengono usati per variabili che possono assumere un numero limitato di valori e per rappresentare informazioni non numeriche. Si possono usare operatori di confronto e operazioni intere.
int main() {
// Di default, LUN=0, MAR=1, MER=2...
enum Giorni { LUN, MAR, MER, GIO, VEN, SAB, DOM };
Giorni oggi = MAR;
oggi = MER;
int i = oggi; // OK: conversione implicita enum -> int (i = 2)
// ERRORE: non si può convertire implicitamente int -> enum
// oggi = 3; // ERRORE! 3 è una costante intera
// oggi = i; // ERRORE! i è un intero, non un Giorni
// oggi = MER - MAR; // ERRORE! MER-MAR è un intero
cout << int(oggi) << endl; // 2
cout << oggi << endl; // 2 (conversione implicita in int per cout)
enum { ROSSO, GIALLO, VERDE } semaforo; // ROSSO=0, GIALLO=1, ...
semaforo = GIALLO;
cout << semaforo << endl; // 1
// Si possono assegnare valori specifici (i successivi seguono)
enum { INIZ1 = 10, INIZ2, INIZ3 = 9, INIZ4 }; // INIZ2=11, INIZ4=10
cout << INIZ1 << '\t' << INIZ2 << '\t'; // 10 11
cout << INIZ3 << '\t' << INIZ4 << endl; // 9 10
}
Operatori in C++
Operatori Bitwise (Logici)
Operano sui singoli bit dei tipi interi:
|: OR bit a bit&: AND bit a bit^: XOR (OR esclusivo) bit a bit~: Complemento (NOT) bit a bit<<: Shift (traslazione) a sinistra>>: Shift (traslazione) a destra
Tabelle di verità (Bit a Bit):
| a | b | a | b | a & b | a ^ b |
|---|---|---|---|---|
| 0 | 0 | 0 | 0 | 0 |
| 0 | 1 | 1 | 0 | 1 |
| 1 | 0 | 1 | 0 | 1 |
| 1 | 1 | 1 | 1 | 0 |
unsigned short a = 0xFFF9; // 1111 1111 1111 1001 (65529)
unsigned short b = ~a; // 0000 0000 0000 0110 (6)
unsigned short c = 0x0013; // 0000 0000 0001 0011 (19)
unsigned short d = 021; // in ottale (17)
unsigned short e = 0b0000000000010010; // in binario (18)
unsigned short c1, c2, c3;
c1 = b | c; // 0000 0000 0001 0111 (23)
c2 = b & c; // 0000 0000 0000 0010 (2)
c3 = b ^ c; // 0000 0000 0001 0101 (21)
unsigned short b1, b2;
b1 = b << 2; // 0000 0000 0001 1000 (24)
b2 = b >> 1; // 0000 0000 0000 0011 (3)
Operatori di Confronto
Restituiscono false se la relazione è falsa, true altrimenti. I tipi aritmetici possono usare:
==(uguale)!=(diverso)>(maggiore)>=(maggiore o uguale)<(minore)<=(minore o uguale)
int a = -1;
bool b1 = a > 0; // false (0 > -1)
bool b2 = a < 0; // true (-1 < 0)
Operatore sizeof()
Ritorna la grandezza (in byte) di un tipo o di una variabile.
int main() {
cout << "char \t" << sizeof(char) << endl; // 1
cout << "short \t" << sizeof(short) << endl; // 2
cout << "int \t" << sizeof(int) << endl; // 4
cout << "long \t" << sizeof(long) << endl; // 4 (o 8)
cout << "unsigned char \t" << sizeof(unsigned char) << endl; // 1
cout << "unsigned short \t" << sizeof(unsigned short) << endl; // 2
cout << "unsigned int \t" << sizeof(unsigned int) << endl; // 4
cout << "unsigned long \t" << sizeof(unsigned long) << endl; // 4 (o 8)
cout << "float \t" << sizeof(float) << endl; // 4
cout << "double \t" << sizeof(double) << endl; // 8
cout << "long double \t" << sizeof(long double) << endl; // 12
cout << "costante carattere " << sizeof 'c' << endl; // 1
cout << "costante carattere " << sizeof('c') << endl; // 1
char c = 0;
cout << "variabile carattere " << sizeof c << endl; // 1
// cout << "char" << sizeof char << endl; // ERRORE!
}
Operatore Assegnamento (=)
Assegna a una variabile un valore o il valore di un'altra variabile.
int i = 0, j = 1, k;
i = 3;
cout << i << endl; // 3
j = i;
cout << j << endl; // 3
// L'assegnamento è ASSOCIATIVO A DESTRA
k = j = i = 5; // Equivale a: k = (j = (i = 5))
cout << i << '\t' << j << '\t' << k << endl; // 5 5 5
// L'espressione (i = 3) vale 3
k = j = 2 * (i = 3); // Equivale a: k = (j = (2 * 3))
cout << i << '\t' << j << '\t' << k << endl; // 3 6 6
// k = j + 1 = 2 * (i = 100); // ERRORE!
// L'assegnamento restituisce un l-value (la variabile stessa)
(j = i) = 10; // (j=i) assegna i (3) a j. (j) = 10 assegna 10 a j.
cout << j << endl; // 10
Altri Operatori di assegnamento (+=, -=, *=, /=):
int i = 0, j = 5;
i += 5; // i = i + 5
cout << i << endl; // 5
i *= j + 1; // i = i * (j + 1) -> i = 5 * (6)
cout << i << endl; // 30
// Associativo a DESTRA
i -= j -= 1; // j = j - 1 (j=4). Poi i = i - j (i=30-4)
cout << i << endl; // 26
(i += 12) = 2; // Associativo a destra; restituisce l-value
cout << i << endl; // 2
Operatori Incremento e Decremento (++ e --)
Permettono di scrivere somme/sottrazioni in forma abbreviata.
int i, j;
i = 0; j = 0;
++i; // PRE-incremento (i ora è 1)
--j; // PRE-decremento (j ora è -1)
cout << i << '\t' << j << endl; // 1 -1
i = 0;
// PRE-incremento: 1. i diventa 1. 2. j = i
j = ++i;
cout << i << '\t' << j << endl; // 1 1
i = 0;
i++;
cout << i << endl; // 1
i = 0;
// POST-incremento: 1. j = i (j=0). 2. i diventa 1.
j = i++;
cout << i << '\t' << j << endl; // 1 0
// j = ++i++; // ERRORE!
j = (++i)++; // CORRETTO
// j = i++++; // ERRORE!
int k = ++++i; // (Se i era 1, i diventa 3, k diventa 3)
cout << i << '\t' << j << '\t' << k << endl; // Es: 5 2 5 (dipende dallo stato prec.)
Perché ++++i è valido e i++++ no?
i++(post-incremento) restituisce un valore temporaneo (r-value), cioè una copia del vecchio valore dii.++i(pre-incremento) restituisce la variabile stessa (l-value), già incrementata.- Gli operatori
++e--richiedono un l-value (una variabile) su cui operare. i++ ++fallisce perché tenta di incrementare (il secondo++) un valore temporaneo (il risultato dii++).++++i(cioè++(++i)) è valido perché++irestituiscei(un l-value), che può essere incrementato di nuovo.
Operatore Condizionale (Ternario)
Usato per scrivere in modo compatto le strutture if-else. La sintassi è:
(<condizione>) ? (<condizione_vera>) : (<condizione_falsa>)
Operatore Virgola (,)
Permette di inserire più espressioni dove ne sarebbe prevista solo una. È un operatore binario, associativo a sinistra.
Sintassi: esp1, esp2
Viene prima valutata esp1, poi esp2. Il risultato dell'intera espressione è il risultato di esp2 (il risultato di esp1 viene ignorato).
int main() {
int a = 2;
int b = 3;
a = (b++, 5); // b++ viene eseguito (b diventa 4), poi a = 5
cout << b << endl; // stampa 4
cout << a << endl; // stampa 5
}
N.B: L'operatore virgola ha la priorità più bassa, inferiore anche all'assegnamento =.
int main() {
int a = 2;
int b = 3;
// Questo è come scrivere (a = b++), 5;
a = b++, 5;
cout << b << endl; // stampa 4 (b++ eseguito)
cout << a << endl; // stampa 3 (a = vecchio valore di b)
}
Espressioni e Conversioni
Espressioni Aritmetiche e Logiche
Nelle espressioni aritmetiche e logiche vengono rispettate la precedenza e le associatività tra operatori.
Precedenza
- Fattori: Funzioni, operatori unari (
++postfisso,--postfisso, poi++prefisso,--prefisso,!,-unario,+unario) - Termini (operatori binari):
- Moltiplicativi (
*,/,%) - Additivi (
+,-) - Relazione (
<,>=, ...) - Uguaglianza (
==,!=) - Logici (
&&, poi||) - Assegnamento (
=,+=, ...) - Parentesi
(): usate per forzare un'espressione a essere valutata per prima (come "fattore").
int i = 2, j;
j = 3 * i + 1; // * ha precedenza: (3 * 2) + 1
cout << j << endl; // 7
j = 3 * (i + 1); // () hanno precedenza: 3 * (2 + 1)
cout << j << endl; // 9
Associatività
Indica la direzione (a sinistra o a destra) in cui vengono valutati operatori dello stesso livello di precedenza.
- Operatori aritmetici binari (
+,-,*,/): a sinistra. - Operatori unari (
!,++): a destra. - Operatori di assegnamento (
=,+=): a destra.
Conversioni in C++ (Cast)
Ci sono due tipi di conversioni in C++:
Conversioni Implicite
Fatte automaticamente dal linguaggio.
Attenzione: la conversione da double a int causa perdita di informazione (troncamento della parte decimale).
int main() {
int i = 10, j;
float f = 2.5f, h;
double d = 1.2e+1; // 12.0
char c = 'd'; // Codice ASCII 100
h = f + 1; // 1 (int) viene promosso a float -> 2.5f + 1.0f = 3.5f
cout << h << '\t'; // 3.5
j = f + 3.1f; // f + 3.1f = 5.6f. Assegnato a int 'j', viene troncato
cout << j << endl; // 5
d = i + 1; // 10 + 1 = 11. 11 (int) promosso a double 11.0
cout << d << '\t'; // 11
d = f + d; // f (2.5) promosso a double -> 2.5 + 11.0 = 13.5
cout << d << endl; // 13.5
j = c - 'a'; // 'd'(100) - 'a'(97) = 3
cout << j << endl; // 3
}
Regole (semplificate):
- Operatori binari: Se gli operandi hanno tipi diversi, quello "inferiore" viene promosso al tipo "superiore" (
short→int,int→double). - Assegnamento: Il valore a destra viene convertito al tipo della variabile a sinistra (anche con perdita di dati).
- A una variabile
enumsi può assegnare solo un valore dello stesso tipoenum(non unint) senza un cast esplicito. - In sequenza:
charpuò essere assegnato adouble(char→int→double).
Conversioni Esplicite (Cast)
Forzate volontariamente dal programmatore.
Operatore static_cast:
- Effettua una conversione di tipo quando esiste la conversione implicita inversa.
- Può essere usato per effettuare conversioni di tipo previste dalla conversione implicita.
int main() {
enum Giorni { LUN, MAR, MER, GIO, VEN, SAB, DOM };
int i;
Giorni g1 = MAR, g2, g3;
i = g1; // OK: conversione implicita enum -> int
// Stile C++ moderno (preferito e sicuro)
g1 = static_cast<Giorni>(i);
// Stile C (sconsigliato in C++)
g2 = (Giorni)i;
// Notazione funzionale
g3 = Giorni(i);
cout << g1 << '\t' << g2 << '\t' << g3 << endl; // 1 1 1
// Cast per troncare (double -> int)
int j = (int)1.1; // j = 1
float f = float(2); // f = 2.0
cout << j << '\t' << f << endl;
}
N.B: Si utilizza la sintassi (int)a o int(a), ma static_cast<> è il metodo più sicuro.
Strutture di Controllo
Istruzione IF
Le istruzioni if (o istruzioni strutturate) sono fondamentali. Per il Teorema di Bohm-Jacobini, per ogni problema complesso sono necessarie al più tre istruzioni strutturate (sequenza, selezione e iterazione).
La sintassi è:
if (<condizione>) {
<istruzioni per la condizione vera>
} else {
<istruzioni per la condizione falsa>
}
// Forma compatta (operatore condizionale)
<condizione> ? <vera> : <falsa>;
Istruzione SWITCH-BREAK
L'istruzione switch permette una selezione multipla basata sul valore di un'espressione.
- Espressione: Solitamente una variabile a valori discreti (
int,char,enum). - Etichette (
case): Contengono espressioni costanti. I valori devono essere distinti. default: Eseguita se nessuncasecorrisponde. Deve essere unica. Se manca e nessun case corrisponde, lo switch termina.break: Istruzione di salto che fa terminare l'istruzioneswitch.
Attenzione: Se un'alternativa non termina con break, l'esecuzione "cade" (fall-through) ed esegue anche le istruzioni dell'alternativa successiva.
Esempi di switch:
char c;
// ... cin >> c; ...
switch (c) {
case 'a':
case 'A':
cout << "Prima alternativa" << endl;
break;
case 'b':
case 'B':
cout << "Seconda alternativa" << endl;
break;
case 'c':
case 'C':
cout << "Terza alternativa" << endl;
break;
// Manca il caso di default
// Se non è una delle alternative, non scrive niente
}
int i;
cout << "Quanti asterischi? " << endl;
cin >> i;
switch (i) {
case 3: // in cascata (fall-through)
cout << "*";
case 2: // in cascata
cout << "*";
case 1:
cout << "*";
break;
default:
cout << "!"; // Se i non è 1, 2, o 3
}
cout << endl;
Istruzioni per i Cicli
Ci sono 3 diversi tipi di cicli:
| WHILE | FOR | DO-WHILE |
|---|---|---|
| Usato quando non sappiamo quante volte il ciclo dovrà ripetersi. | Usato quando sappiamo quante volte il ciclo dovrà ripetersi. | Usato quando non sappiamo quante volte, ma siamo sicuri che verrà eseguito almeno una volta. |
|
|
|
break: Istruzione che permette di saltare all'istruzione immediatamente successiva al corpo del ciclo/switch in cui è collocato.continue: Istruzione che provoca la terminazione dell'iterazione corrente e salta all'iterazione successiva del ciclo, valutando di nuovo la condizione.
N.B: continue "ignora" l'eventuale presenza di uno switch al suo interno e agisce solo sul ciclo.
Esempio break:
int main() {
int j;
// Ciclo infinito; altra forma: while(true)
for (;;) {
cout << "Inserisci un numero intero " << endl;
cin >> j;
if (j < 0)
break; // Esce dal ciclo se j è negativo
cout << j << endl;
}
return 0;
}
/* Output:
Inserisci un numero intero
3
3
Inserisci un numero intero
5
5
Inserisci un numero intero
-1
(Process finished)
*/
Esempio continue:
int main() {
const int N = 5;
for (int i = 0, j; i < N; i++) {
cout << "Inserisci un numero intero" << endl;
cin >> j;
if (j < 0)
continue; // Salta il resto del blocco e passa a i++
cout << j << endl;
}
return 0;
}
/* Output:
Inserisci un numero intero
1
1
Inserisci un numero intero
2
2
Inserisci un numero intero
-2
Inserisci un numero intero
-3
Inserisci un numero intero
4
4
*/
Stream (I/O) e File
Concetto di Stream
Uno stream (flusso) in C++ è un'astrazione che rappresenta un canale di dati attraverso il quale i dati vengono trasferiti in sequenza da una sorgente (source) a una destinazione (sink).
Immagina uno stream come una tubatura: i dati scorrono in un'unica direzione.
- Sorgente: Da dove provengono i dati (es. tastiera, file, variabile).
- Destinazione: Dove i dati sono diretti (es. schermo, file, variabile).
Questa astrazione permette di usare le stesse funzioni (>>, <<) indipendentemente dal dispositivo.
In parole semplici, lo stream è un buffer (memoria temporanea) che permette al programma di "parlare" con l'esterno.
Struttura dello Stream
Ogni stream è costituito da una sequenza di celle (byte) e termina con una marca di fine stream.
Nel momento in cui il programma incontra cin >> <variabile>, andrà a leggere nel buffer. Il buffer è gestito con politica FIFO (First In First Out), quindi il valore assegnato sarà il primo inserito.
I Principali Stream
I principali stream sono 3 (dalla libreria <iostream>):
cout: Stream di output standard (di solito, lo schermo).cerr: Stream di errore standard (non bufferizzato, per messaggi urgenti).cin: Stream di input standard (di solito, la tastiera).
CIN (Operatore di Lettura >>)
cin viene utilizzato per prelevare input ed esegue due azioni:
- Preleva dallo stream una serie di caratteri (ignorando spazi, tabulazioni e "a capo" iniziali).
- Interpreta i dati in base al tipo della variabile (se
int, cerca un numero; sechar, prende un carattere, ecc.).
I caratteri digitati riempiono lo stream solo quando si preme INVIO. È definito per interi, reali, caratteri e costanti stringa.
1) Lettura Caratteri (cin)
char c;
cin >> c;
Questo codice: salta tutti gli spazi bianchi finché non trova un carattere "visibile", lo preleva e lo assegna a c. Il puntatore avanza.
Per leggere qualsiasi carattere (incluso lo spazio bianco):
char c;
cin.get(c);
cin.get(c) è una funzione membro che legge il prossimo byte dallo stream, qualunque esso sia.
Esempi cin >> c vs cin.get(c)
// Esempio con cin >> c (salta gli spazi)
int main() {
char c;
while (true) {
cout << "Inserisci un carattere " << endl;
cin >> c;
if (c != 'q')
cout << c << endl;
else
break;
}
return 0;
}
/* Output se inserisco "a b q":
Inserisci un carattere
a b q
a
Inserisci un carattere
b
Inserisci un carattere
(termina)
*/
// Esempio con cin.get(c) (legge tutto)
int main() {
char c;
while (true) {
cout << "Inserisci un carattere " << endl;
cin.get(c); // Legge il prossimo carattere, INCLUSI gli spazi
if (c != 'q')
cout << c << endl;
else
break;
}
return 0;
}
/* Output se inserisco "a bq":
Inserisci un carattere
a bq
a
Inserisci un carattere
(legge lo spazio)
Inserisci un carattere
b
Inserisci un carattere
(termina)
*/
2) Lettura Interi e Reali (cin)
int i;
cin >> i;
float f;
cin >> f;
cin salta gli spazi bianchi, poi legge caratteri finché sono consistenti con il tipo (cifre e segno per int; cifre, segno, punto decimale e 'e' per float). Si ferma al primo carattere non valido, che rimane nel buffer.
COUT (Operatore di Uscita <<)
cout calcola il valore dell'espressione a destra, la converte in una sequenza di caratteri e la inserisce nello stream di output.
Si possono scrivere più valori in sequenza (concatenazione): cout << b << c;
cout scrive anche i booleani (convertiti in intero: 0=false, 1=true).
Per formattare l'output (richiede <iomanip>):
setw(n): Imposta l'ampiezza del campo (numero di caratteri) *solo per la prossima stampa*.setprecision(n): Imposta il numero di cifre significative (o decimali se usato confixed).fixed: Forza la notazione decimale (non scientifica) per i float.hex,oct,dec: Cambia la base numerica (esadecimale, ottale, decimale) per i numeri interi da quel punto in poi.
#include <iostream>
#include <iomanip>
using namespace std;
int main() {
double d = 1.564e-2; // 0.01564
double f = 1.2345;
cout << d << endl; // 0.01564
// setprecision(n) = n cifre SIGNIFICATIVE
cout << setprecision(2) << d << '\t' << f << endl; // 0.016 1.2
// setprecision rimane attivo!
cout << d << endl; // 0.016
// setw(n) riserva n caratteri totali (8 spazi + "43")
cout << setw(10) << 43 << endl; // " 43"
cout << hex << 10 << '\t' << 11 << endl; // a b
cout << oct << 10 << '\t' << 11 << endl; // 12 13
cout << dec << 10 << '\t' << 11 << endl; // 10 11
// fixed + setprecision(n) = n cifre DOPO la virgola
cout << fixed << setprecision(2) << 345.3262 << endl; // 345.33 (arrotonda)
return 0;
}
Errori degli Stream (cin)
Lo stream cin va in uno stato di errore (cin.fail() restituisce true) quando i dati nel buffer non sono consistenti con il tipo della variabile che deve leggerli.
Esempio: Si cerca di leggere un int ma nel buffer c'è "ciao".
int a;
double b;
cin >> a >> b;
// Input utente: 10 ciao
// Risultato: 'a' viene letto (10). 'b' fallisce (ciao non è double).
// Lo stream è ora in stato di errore.
Uno stream in errore blocca tutte le successive operazioni di input. Per ripristinarlo:
cin.clear(): Resetta lo stato di errore (cin.fail()tornafalse).cin.ignore(N, '\n'): Pulisce il buffer, ignorando i prossimiNcaratteri (es. 1000) o fino a che non trova un "a capo" (\n).
È possibile usare cin stesso come condizione:
int i;
cout << "Inserisci un numero intero (carattere per terminare):" << endl;
while (cin >> i) { // Questa condizione è VERA se la lettura va a buon fine
cout << "Numero letto: " << i << endl;
cout << "Inserisci un numero intero:" << endl;
}
// Se inserisco 'a', cin >> i fallisce, la condizione è FALSA, il ciclo termina.
cout << boolalpha << cin.fail() << endl; // Stampa "true"
cin.clear(); // Ripristina lo stato
cin.ignore(1000, '\n'); // Pulisce il buffer da 'a'
cout << cin.fail() << endl; // Stampa "false"
L'utente può forzare la fine dello stream (EOF - End Of File) da tastiera con Ctrl+D (Linux/Mac) o Ctrl+Z (Windows).
Manipolazione File (<fstream>)
Per leggere e scrivere su file si usa la libreria <fstream>, che fornisce ifstream (input), ofstream (output) e fstream (entrambi).
open(nome_file, modalità): Associa lo stream a un file.close(): Chiude lo stream.
Modalità di apertura:
ios::in: (Lettura) Il file deve esistere. Puntatore all'inizio.ios::out: (Scrittura) Se il file esiste, lo cancella (sovrascrive). Se non esiste, lo crea. Puntatore all'inizio.ios::app: (Append) Se il file esiste, si posiziona alla fine per aggiungere dati (i dati non vengono persi). Se non esiste, lo crea. (Si usa conios::out:ios::out | ios::app).
Una volta aperto, si usano >> e << come con cin e cout. Gli errori funzionano allo stesso modo.
Esempio 1: Scrittura e Lettura
#include <fstream>
#include <iostream>
#include <cstdlib> // per exit()
using namespace std;
int main() {
fstream fs;
int num;
// Apertura in scrittura
fs.open("esempio.txt", ios::out);
if (!fs) { // Controlla se l'apertura è fallita
cerr << "Il file non puo' essere aperto" << endl;
exit(1); // Termina il programma
}
// Scrive i numeri da 0 a 3 nel file
for (int i = 0; i < 4; i++)
fs << i << endl; // ATT: separa i numeri con newline
fs.close();
// Apertura in lettura
fs.open("esempio.txt", ios::in);
if (!fs) {
cerr << "Il file non puo' essere aperto" << endl;
exit(1);
}
// Legge i numeri fino alla fine del file e li stampa
while (fs >> num) { // Stessa logica di cin!
cout << num << '\t';
}
cout << endl;
fs.close();
return 0;
}
/* Output a schermo:
0 1 2 3
*/
Esempio 2: Controllo Errori Lettura
int main() {
fstream fs;
int i, num;
const int N = 6; // Il file "esempio.txt" ne ha solo 4
fs.open("esempio.txt", ios::in);
if (!fs) {
cerr << "Il file non puo' essere aperto" << endl;
exit(1);
}
// Legge finché i < N E finché la lettura (fs >> num) va a buon fine
for (i = 0; i < N && fs >> num; i++)
cout << num << '\t';
cout << endl;
if (i != N) { // Il ciclo si è interrotto prima di arrivare a N
cerr << "Errore nella lettura del file" << endl; // (o EOF)
}
return 0;
}
/* Output a schermo:
0 1 2 3
Errore nella lettura del file
*/
Esempio 3: Append (Aggiungere)
int main() {
fstream fs;
char c;
// Apertura in scrittura (sovrascrive)
fs.open("esempio.txt", ios::out);
for (int i = 0; i < 4; i++)
fs << i << '\t';
fs.close();
// Apertura in append (aggiunge)
fs.open("esempio.txt", ios::out | ios::app);
fs << 5 << '\t' << 6 << endl;
fs.close();
// Lettura (con fs.get per leggere anche \t e \n)
fs.open("esempio.txt", ios::in);
while (fs.get(c)) // Legge ogni singolo carattere
cout << c;
fs.close();
return 0;
}
/* Output a schermo:
0 1 2 3 5 6
*/
Sintassi abbreviata:
ifstream ifs("file.in");
Equivale a fstream fs; fs.open("file.in", ios::in);
ofstream ofs("file.out");
Equivale a fstream fs; fs.open("file.out", ios::out);
ofstream ofs("file.out", ios::app);
Equivale a fstream fs; fs.open("file.out", ios::out | ios::app);
Librerie
Sono un insieme di funzioni (sottoprogrammi) precompilate.
- Formate da coppie di file: un file compilato e un file di intestazione (header, es.
.h) che contiene le dichiarazioni. - Si usano includendo il file header (es.
#include <cmath>) e, in fase di collegamento (linking), specificando la libreria da usare.
Librerie C++ standard principali:
<iostream>: Percin,cout,cerr.<fstream>: Perifstream,ofstream.<iomanip>: Persetw,setprecision.<cmath>: Funzioni matematiche (sqrt,pow,sin...).<cstdlib>: Funzioni di utilità generale (exit,rand...).<cctype>: Funzioni sui caratteri (isdigit,tolower...).<vector>: Per il tipo di datovector.
Tipi Derivati
Dai tipi fondamentali si possono derivare altri tipi. I meccanismi di derivazione possono essere composti (es. array di puntatori a interi, puntatori ad array di interi, ecc.).
- Dal tipo
intsi deriva il tipo puntatore a int (int*). Una variabile di questo tipo contiene l'indirizzo di un intero. - Dal tipo
intsi deriva il tipo array di int (int[]). Una variabile di questo tipo contieneninteri.
Principali tipi derivati:
- Riferimenti (
&) - Puntatori (
*) - Array (
[]) - Strutture (
struct) - Unioni (
union) - Classi (
class)
Riferimenti (&)
Un riferimento è un alias (un sinonimo) per una variabile esistente. Non occupa una nuova cella di memoria, ma "punta" alla stessa cella della variabile originale.
int a = 5;
int &a2 = a; // a2 è ora un alias per a
- Modificare
a2modificaa, e viceversa. - Il tipo del riferimento deve essere congruo (
int &a2 = a;OK;char &c2 = a;ERRORE). - Non si possono creare riferimenti a riferimenti.
- Un riferimento deve essere inizializzato al momento della dichiarazione.
- Non si può riferire a un valore costante (letterale), a meno che il riferimento non sia
const.
int main() {
int i = 10;
int &r = i;
int &s = r; // s è un altro alias per i
// int &err1; // ERRORE: deve essere inizializzato
// int &err2 = 10; // ERRORE: non si può riferire a un letterale
cout << i << '\t' << r << '\t' << s << endl; // 10 10 10
r++; // Modifica r, ma in realtà modifica i
cout << i << '\t' << r << '\t' << s << endl; // 11 11 11
int h = 0, k = 1;
int &r1 = h, &r2 = k; // due riferimenti
int &r3 = h, j = 3; // un riferimento e un intero
return 0;
}
Riferimenti Const
Un riferimento const è un alias che non può essere usato per modificare il valore della variabile originale.
int main() {
int i = 1;
const int &r = i; // r è un riferimento "read-only" a i
cout << r << endl; // 1
// r = 2; // ERRORE! r è costante, non possiamo cambiare i tramite r.
i = 2; // OK: i non è costante, possiamo cambiarla direttamente
cout << r << endl; // 2 (r "vede" la modifica)
int j = r; // OK: j = 2
cout << j << endl; // 2
const int k = 10;
const int &t = k; // OK: riferimento const a variabile const
// int &err3 = k; // ERRORE! non si può creare un rif. non-const a una var. const
return 0;
}
Riferimenti come Argomenti (Passaggio per Riferimento)
- Passaggio per Valore: (es.
void Scambia(int a, int b)). La funzione riceve una copia della variabile del main. Le modifiche fatte suaebnella funzione non si riflettono suiejnel main. Vengono create nuove celle di memoria. - Passaggio per Riferimento: (es.
void Scambia(int &a, int &b)). La funzione riceve un alias della variabile del main.adiventa un sinonimo dii,bun sinonimo dij. Le modifiche fatte suaebagiscono direttamente sulle variabiliiejdel main. Non viene creata nuova memoria peraeb.
Esempio Passaggio per Valore (NON FUNZIONA):
void Scambia (int a, int b) { // a e b sono copie
int c = a;
a = b;
b = c;
}
int main() {
int i = 2, j = 3;
cout << "Inserisci due interi: " << i << " " << j << endl;
Scambia (i,j);
cout << i << '\t' << j << endl; // Stampa ancora 2 3
}
Esempio Passaggio per Riferimento (FUNZIONA):
void Scambia(int &a, int &b) { // a e b sono alias di i e j
// scambio i valori degli oggetti riferiti
int c = a;
a = b;
b = c;
}
int main() {
int i, j;
cout << "Inserisci due interi: ";
cin >> i >> j; // Es: i=2, j=3
Scambia(i, j);
cout << i << '\t' << j << endl; // Stampa: 3 2
}
Riferimenti const come Argomenti:
Si usa quando non si vuole modificare la variabile originale, ma si vuole evitare il costo di copiarla (specialmente se è un oggetto grande). Dà alla funzione l'accesso in "sola lettura".
float Interesse (int importo, const float& rateo) {
float inter = rateo * importo;
// rateo += 0.5; // ERRORE: rateo è const!
return inter;
}
Riferimenti come Risultato di Funzione
Una funzione può restituire un riferimento a una variabile. Questo permette alla funzione di essere usata a sinistra di un assegnamento.
// Funzione che restituisce un RIFERIMENTO al maggiore tra a e b
int& Massimo (int &a, int &b) {
return a > b ? a : b;
}
int main() {
int i, j;
cout << "Inserisci due interi: "; // Es: 1 3
cin >> i >> j;
cout << "Valore massimo: ";
cout << Massimo(i,j) << endl; // Stampa 3
// Posso assegnare un valore al risultato della funzione!
Massimo(i,j) = 5; // 'j' (il massimo) diventa 5
cout << i << '\t' << j << endl; // Stampa 1 5
Massimo(i,j)++; // 'j' (il massimo) viene incrementato
cout << i << '\t' << j << endl; // Stampa 1 6
}
Attenzione: Per restituire un riferimento, gli argomenti devono essere anch'essi riferimenti (o variabili globali/statiche). Restituire un riferimento a una variabile locale (passata per valore) è un errore gravissimo (non segnalato dal compilatore!) perché la variabile locale "muore" alla fine della funzione.
Operatore const_cast
Questa operazione converte un riferimento const in un riferimento non const. È un'operazione pericolosa e generalmente sconsigliata, da usare solo se si sa esattamente cosa si sta facendo.
// Funzione che accetta const ma restituisce non-const
int& Massimo (const int &a, const int &b) {
return const_cast<int>(a > b ? a : b); // Rimuove il const
}
Puntatori (*)
Un puntatore è una variabile che contiene l'indirizzo di memoria di un'altra variabile. A differenza di un riferimento, un puntatore ha una sua cella di memoria.
&var: Operatore "indirizzo di". Prende l'indirizzo divar.*puntatore: Operatore di "deferenzazione" o "indirezione". Accede al valore contenuto nella cella di memoria a cuipuntatorepunta.
int var;
int* puntatore = &var;
Un puntatore deve essere associato a una variabile o inizializzato a nullptr (dalla versione C++11, sostituisce NULL o 0). Un puntatore non inizializzato punta a una cella di memoria casuale, il che è pericolosissimo.
Differenza con i Riferimenti:
int* p = &n;- Se stampo
&p(indirizzo del puntatore) e&n(indirizzo della variabile), sono diversi. - Se stampo
p(valore del puntatore) e&n, sono uguali. *p = 10;assegna il valore 10 alla variabile puntata (n).- La forza dei puntatori è che possono cambiare la cella di memoria a cui puntano (un riferimento è "bloccato" all'inizializzazione).
int n = 0, k = 0;
int* p = &n; // p punta a n
*p = 10; // n ora vale 10
p = &k; // p ora punta a k (questo è impossibile con un riferimento)
*p = 5; // k ora vale 5
// Risultato: n = 10, k = 5
Operazioni sui Puntatori
- Assegnamento: Si può assegnare solo un valore che rappresenta un indirizzo (
int* p = &x;). - Confronto:
==e!=verificano se due puntatori puntano allo stesso indirizzo. - Stampa:
cout << p;stampa l'indirizzo (es. 0x7FFF...);cout << *p;stampa il valore (es. 10).
Puntatore come argomento di funzione
Passare un puntatore (f(&x)) è un modo per ottenere il "passaggio per riferimento". La funzione riceve l'indirizzo e può modificare la variabile originale tramite *.
// Esempio di errore: passare un puntatore a 'const int' a una funzione
// che si aspetta un puntatore a 'int'
int massimo (int* a, int* b) {
return *a > *b ? *a : *b;
}
int main() {
int i = 2;
const int N = 3;
// ERRORE: invalid conversion from 'const int*' to 'int*'
// cout << *massimo(&i, &N) << endl;
// La funzione 'massimo' NON promette di non modificare *b,
// quindi non le si può passare l'indirizzo di una 'const'.
return 0;
}
Puntatori a Funzione
In C++, un puntatore a funzione è una variabile che memorizza l'indirizzo di memoria di una funzione anziché di un dato. Questo permette di "parametrizzare" il comportamento del codice, decidendo quale operazione eseguire solo a runtime.
Sintassi ed Esempi Pratici
- 1. Dichiarazione
- Bisogna specificare il tipo di ritorno e i parametri della funzione che si vuole puntare:
double (*puntoOperazione)(double, double); - 2. Assegnazione
- Il nome di una funzione senza parentesi rappresenta il suo indirizzo:
puntoOperazione = somma; // 'somma' è una funzione definita altrove - 3. Invocazione
- Si usa il puntatore come se fosse il nome della funzione:
double risultato = puntoOperazione(5.5, 10.2);
Esempio di Applicazione: Il "Callback"
I puntatori a funzione sono fondamentali per scrivere funzioni generiche. Considera una funzione che esegue un calcolo tra due numeri, ma lascia decidere a chi la chiama quale operazione usare:
// Definiamo due funzioni con la stessa firma
int Somma(int a, int b) { return a + b; }
int Moltiplica(int a, int b) { return a * b; }
// Funzione che usa un puntatore a funzione come parametro (Callback)
void EseguiOperazione(int x, int y, int (*f_ptr)(int, int)) {
// Invochiamo la funzione puntata da f_ptr
cout << "Risultato: " << f_ptr(x, y) << endl;
}
int main() {
// Dichiarazione di un puntatore a funzione
int (*ptr)(int, int);
// Puntiamo alla funzione Somma e la eseguiamo
ptr = Somma;
EseguiOperazione(10, 5, ptr); // Output: 15
// Puntiamo alla funzione Moltiplica
ptr = Moltiplica;
EseguiOperazione(10, 5, ptr); // Output: 50
return 0;
}
Un puntatore a funzione è "schizzinoso": può puntare solo a funzioni che hanno esattamente lo stesso tipo di ritorno e lo stesso numero/tipo di parametri dichiarati. Se provi ad assegnare una funzione con parametri diversi, il compilatore genererà un errore di tipo.
Puntatori a Puntatori (Indirezione Multipla)
Un puntatore può puntare a un altro puntatore. Questo è indicato con il doppio asterisco **.
È utile per modificare l'indirizzo contenuto in un puntatore all'interno di una funzione, o per gestire matrici dinamiche.
int main() {
int i = 2, j = 7;
int* pi = &i;
int* pj = &j;
int** q1 = π // q1 punta a pi
cout << **q1 << endl; // Stampa 2 (valore di i)
*q1 = pj; // Modifica pi! Ora pi punta a j
cout << **q1 << endl; // Stampa 7
cout << *pi << endl; // Stampa 7 (pi è cambiato)
}
Funzioni
Concetto e Chiamata
- Visibilità (Scope): Nomi di variabili locali e argomenti formali sono "visibili" solo all'interno della funzione in cui sono definiti.
- Chiamata:
- Argomenti formali e variabili locali vengono allocati in memoria.
- Argomenti formali vengono inizializzati (copiati) con i valori degli argomenti attuali (passaggio per valore).
- La funzione esegue le sue elaborazioni.
- Al termine (
return), argomenti e variabili locali vengono deallocati.
- Istanza di funzione: Ogni chiamata crea una nuova "istanza" con una nuova copia delle variabili. I valori non vengono conservati tra chiamate diverse.
int fatt (int n) {
int ris = 1;
for(int i = 2; i <= n; i++)
ris *= i;
return ris;
}
int main() {
cout << fatt(4); // Stampa 24
}
N.B: Una funzione deve essere dichiarata (o definita) prima di essere usata (chiamata). È prassi comune mettere la "firma" (prototipo) della funzione sopra il main e il corpo (definizione) sotto.
Argomenti e Variabili Locali
//funzione che somma gli interi compresi fra due interi dati (con m>=n)
int SommaInteri (int n, int m) {
int s = n;
for(int i = n + 1; i <= m; i++) {
s += i;
}
return s;
}
int main() {
int a, b;
cout << "Inserisci interi: ";
cin >> a >> b;
cout << SommaInteri(a, b); // passaggio di variabili locali per valore
}
In questo caso, n e m sono copie di a e b. Vengono create celle differenti in memoria.
Funzioni Void
Una funzione void è una funzione che non restituisce alcun valore con return. Esegue solo azioni (es. stampa).
void ScriviAsterischi (int n) {
for(int i = 0; i < n; i++) {
cout << "*";
}
}
int main() {
int i;
cout << "Quanti asterischi? ";
cin >> i;
ScriviAsterischi(i);
}
Funzioni senza Argomenti
Una funzione può essere definita senza argomenti (void Funzione(void)). Questo è utile se la funzione usa variabili globali o non necessita di input.
using namespace std;
const int N = 20; // Variabile globale
void ScriviAsterischi (void) {
for(int i = 0; i < N; i++) {
cout << "*";
}
cout << endl;
}
int main() {
ScriviAsterischi(); // Non servono parametri
}
Funzioni Ricorsive
Una funzione ricorsiva è una funzione che, al suo interno, richiama sé stessa. È essenziale che ci sia un caso base che termina la ricorsione.
Esempio: Fattoriale
int Fattoriale (int n) {
if (n == 0) {
return 1; // Caso Base
} else {
return n * Fattoriale(n - 1); // Passo Ricorsivo
}
}
int main() {
int n;
cout << "Inserisci un valore per calcolarne il fattoriale: ";
cin >> n;
cout << "Il fattoriale di " << n << " e': " << Fattoriale(n);
}
Modalità di Esecuzione (per Fattoriale(3)):
Fattoriale(3)chiamareturn 3 * Fattoriale(2)Fattoriale(2)chiamareturn 2 * Fattoriale(1)Fattoriale(1)chiamareturn 1 * Fattoriale(0)Fattoriale(0)(Caso Base) restituisce1Fattoriale(1)calcolareturn 1 * 1(restituisce1)Fattoriale(2)calcolareturn 2 * 1(restituisce2)Fattoriale(3)calcolareturn 3 * 2(restituisce6)
Ogni funzione può essere formulata in maniera iterativa (con un ciclo for o while), che è spesso più efficiente. Tuttavia, la ricorsione è più chiara per certi problemi.
Array
Definizione di Array
Un array è un vettore composto di celle contigue in memoria (una attaccata all'altra). Il tipo di un array dipende dai sui elementi. La dichiarazione e definizione sono contestuali (non si può dichiarare un array senza definirne la dimensione).
int main() {
const int N = 6;
int a[4] = {0, 1, 2, 3}; // array di 4 elementi
int b[N] = {0, 1, 2, 3}; // array di 6 elementi (gli ultimi 2 sono 0)
//b[0]++; // OK: restitusce un l-value
cout << "Dimensioni array (bytes): ";
cout << sizeof a << '\t' << sizeof b << endl; // 16 24 (4*4 e 6*4)
cout << "Numero di elementi: ";
cout << sizeof a / sizeof(int) << '\t'; // 4
cout << sizeof b / sizeof(int) << endl; // 6
// Stampa array b (elementi 0, 1, 2, 3, 0, 0)
for (int i = 0; i < N; i++)
cout << b[i] << '\t';
cout << endl;
// ERRORE! Accesso fuori dai limiti (Out of Bounds)
// Stampa array a (elementi 0, 1, 2, 3, e poi spazzatura)
// Nessun controllo sul valore degli indici in C++!
for (int i = 0; i < N; i++) // ERRORE: i < N (6) invece di i < 4
cout << a[i] << '\t';
cout << endl;
return 0;
}
Operatori su Array
Gli operatori di assegnamento (=), confronto (==, >) e aritmetici (+) non sono definiti per gli array.
Quando si usa il nome di un array (es. v) senza indici, questo "decade" a un puntatore alla sua prima cella (&v[0]).
Di conseguenza, i confronti (v == u) confrontano gli indirizzi di memoria, non il contenuto dell'array.
int main() {
const int N = 5;
int u[N] = {0, 1, 2, 3, 4};
int v[N] = {5, 6, 7, 8, 9};
// v = u; // ERRORE: assegnamento non permesso tra array
cout << "Ind. v: " << v << "\t Ind. u: " << u << endl;
if (v == u) // Confronta gli indirizzi di v[0] e u[0]
cout << "Array uguali" << endl;
else
cout << "Array diversi" << endl; // Stampa questo
return 0;
}
Puntatori e Aritmetica degli Array
Si può accedere alle celle di un array usando un puntatore e l'aritmetica dei puntatori.
Se p è un puntatore a T (T*), p+1 rappresenta l'indirizzo dell'oggetto T consecutivo in memoria. Nei sistemi a 32 bit (int = 4 byte), p+1 equivale all'indirizzo di p + 4 byte.
Le seguenti notazioni sono equivalenti:
v[i]è equivalente a*(v + i)&v[i]è equivalente av + i
int main() {
int v[4];
int* p = v; // p punta a v[0] (v decade a &v[0])
*p = 1; // v[0] = 1
*(p + 1) = 10; // v[1] = 10
p += 3; // p ora punta a v[3]
*(p - 1) = 100; // v[2] = 100
*p = 1000; // v[3] = 1000
p = v; // p torna a puntare a v[0]
// Stampa: v[4] = [1 10 100 1000]
cout << "v[4] = [" << *p;
for (int i = 1; i < 4; i++)
cout << '\t' << *(p + i);
cout << "]" << endl;
int* p1 = &v[1];
int* p2 = &v[2];
cout << p2 - p1 << endl; // 1 (differenza di puntatori in elementi)
cout << (long)p2 - (long)p1 << endl; // 4 (differenza in byte, su 32bit)
return 0;
}
Array come Argomenti di Funzioni
Quando un array viene passato a una funzione, "decade" automaticamente in un puntatore al suo primo elemento. La funzione non conosce la dimensione dell'array.
Per questo motivo, è necessario passare la dimensione dell'array come argomento separato.
Le seguenti firme di funzione sono equivalenti:
int somma(int v[4], int dim)(Il 4 viene ignorato!)int somma(int v[], int dim)int somma(int* v, int dim)
// Funzione che calcola la somma degli elementi
int somma (int v[], int dim) { // o int* v
int s = 0;
for (int i = 0; i < dim; i++)
s += v[i]; // v[i] è *(v+i)
return s;
}
int main() {
int vett[] = {10, 11, 12, 13};
int dim = sizeof(vett) / sizeof(int); // Calcola la dimensione (4)
cout << "La somma degli elementi e': ";
cout << somma(vett, dim) << endl; // 46
return 0;
}
Per evitare modifiche accidentali all'array originale, si può dichiarare l'argomento come const:
// Gli elementi sono const: non possono essere modificati
int somma (const int v[], int n) {
int s = 0;
for (int i = 0; i < n; i++)
s += v[i];
return s;
}
void incrementa (const int v[], int n) {
for (int i = 0; i < n; i++)
// v[i]++; // ERRORE: v[i] è costante!
}
Array Multidimensionali (Matrici)
Sono array di array, identificati da Righe (R) e Colonne (C). int matr[R][C];
In memoria, la matrice non è bidimensionale. È memorizzata come un unico blocco contiguo di R * C celle, per righe (prima tutti gli elementi della riga 0, poi tutti quelli della riga 1, ecc.).
m[i][j] è equivalente a *(p + i*C + j), dove p è il puntatore a m[0][0].
Array Multidimensionali come Argomenti
Quando si passa una matrice a una funzione, la dimensione della prima dimensione (righe) può essere omessa, ma tutte le altre (colonne) devono essere specificate.
void stampa(const int m[][3], int r)
Questo è necessario perché il compilatore deve sapere la dimensione delle colonne (C) per calcolare la posizione di m[i][j] (come i*C + j).
using namespace std;
const int C = 3; // La dimensione delle colonne DEVE essere nota
// Inizializza la matrice m con valori i + j
void inizializza (int m[][C], int r) {
for (int i = 0; i < r; i++)
for (int j = 0; j < C; j++)
m[i][j] = i + j;
}
// Stampa la matrice
void stampa (const int m[][C], int r) {
for (int i = 0; i < r; i++) {
for (int j = 0; j < C; j++)
cout << m[i][j] << '\t';
cout << endl;
}
}
int main() {
const int R = 2;
int mat1[R][C];
int mat2[2][5]; // ha 5 colonne, non 3
inizializza(mat1, R);
stampa(mat1, R);
// inizializza(mat2, 2); // ERRORE: tipo argomento diverso (int[][5] vs int[][3])
return 0;
}
Algoritmi Fondamentali
Ordinamento (Sorting)
Gli algoritmi di ordinamento servono a organizzare gli elementi di un vettore in ordine (crescente o decrescente). Vediamo i due principali trattati.
Selection Sort
L'ordinamento per selezione si basa sulla ricerca iterativa dell'elemento più piccolo all'interno della parte non ordinata del vettore.
Logica dell'algoritmo:
- Si considera l'intero vettore. Si cerca il valore minimo.
- Si scambia il minimo trovato con l'elemento nella posizione corrente (inizialmente la posizione 0).
- Si restringe il campo di ricerca escludendo l'elemento appena ordinato.
- Si ripete il procedimento fino alla fine del vettore.
void selectionSort(T v[], int n) {
int min;
// Scorre tutto il vettore tranne l'ultimo elemento
for (int i = 0; i < n - 1; i++) {
min = i; // Ipotizzo che il primo sia il minimo
// Cerco il vero minimo nella parte restante
for (int j = i + 1; j < n; j++) {
if (v[j] < v[min])
min = j;
}
// Scambio l'elemento corrente con il minimo trovato
scambia(v, i, min);
}
}
Bubble Sort
Il Bubble Sort (ordinamento a bolle) consiste nello scorrere il vettore più volte. A ogni passaggio, confronta coppie di elementi adiacenti e li scambia se non sono nell'ordine corretto.
Ottimizzazione: Se durante una passata completa non avvengono scambi, significa che il vettore è già ordinato ed è inutile continuare. Per questo si usa una variabile booleana (ordinato) per interrompere il ciclo anticipatamente.
void bubbleSort(T v[], int n) {
bool ordinato = false;
// Continua finché non è ordinato O finché non finiscono i passaggi
for (int i = 0; i < n - 1 && !ordinato; i++) {
ordinato = true; // Ipotizzo che sia ordinato
// Scorre dal fondo verso l'inizio della parte non ordinata
for (int j = n - 1; j >= i + 1; j--) {
if (v[j] < v[j - 1]) {
scambia(v, j, j - 1);
ordinato = false; // Ho fatto uno scambio, non era ordinato
}
}
}
}
Ricerca (Searching)
Ricerca Lineare (Sequenziale)
È il metodo più semplice: si scorre il vettore elemento per elemento partendo dall'inizio.
- Funzionamento: Confronta ogni valore fino a trovare l'elemento cercato o raggiungere la fine del vettore.
- Svantaggio: Il tempo di esecuzione cresce linearmente con la dimensione dell'array (O(N)). Nel caso peggiore bisogna controllare tutti gli elementi.
Ricerca Binaria (Dicotomica)
Molto più efficiente della ricerca lineare, ma ha un prerequisito fondamentale: il vettore deve essere già ordinato.
Logica (Divide et Impera):
- Si confronta l'elemento cercato con quello al centro del vettore.
- Se sono uguali, fine (trovato).
- Se l'elemento cercato è minore del centro, si ripete la ricerca solo nella metà sinistra.
- Se l'elemento cercato è maggiore del centro, si ripete la ricerca solo nella metà destra.
- Si ripete finché l'intervallo non si annulla.
bool ricbin(T v[], int inf, int sup, T k, int& pos) {
// Calcola l'indice centrale
int medio = (inf + sup) / 2;
while (inf <= sup) {
if (k > v[medio])
inf = medio + 1; // Cerca nella metà destra
else if (k < v[medio])
sup = medio - 1; // Cerca nella metà sinistra
else {
pos = medio; // Trovato
return true;
}
medio = (inf + sup) / 2; // Ricalcola il centro
}
return false; // Non trovato
}
Stringhe
Concetto di Stringa (C-String)
In C++ (storicamente), le stringhe sono gestite in due modi: la libreria <string> (moderna) o gli array di caratteri (stile C).
Una C-String è un array di char che termina con il carattere nullo ('\0').
Operatori di I/O per C-String
- Operatore di ingresso (
cin >> str):- Ignora spazi bianchi iniziali.
- Legge caratteri e li memorizza nell'array
str. - Si ferma al primo spazio bianco (spazio, tab, invio).
- Aggiunge
'\0'alla fine. - Rischio di buffer overflow se la parola è più lunga dell'array!
- Operatore di uscita (
cout << str):- Scrive i caratteri dell'array finché non incontra
'\0'(escluso).
- Scrive i caratteri dell'array finché non incontra
int main () {
const int LETTERE = 26;
char str[100];
int conta[LETTERE];
for (int i = 0; i < LETTERE; i++)
conta[i] = 0;
cout << "Inserisci una stringa: ";
cin >> str; // Legge solo fino al primo spazio
for (int i = 0; str[i] != '\0'; i++) {
if (str[i] >= 'a' && str[i] <= 'z')
++conta[str[i] - 'a'];
else if (str[i] >= 'A' && str[i] <= 'Z')
++conta[str[i] - 'A'];
}
for (int i = 0; i < LETTERE; i++)
cout << (char)('a' + i) << ": " << conta[i] << '\t';
cout << endl;
return 0;
}
/* Output:
Inserisci una stringa: prova
a: 1 b: 0 c: 0 d: 0 e: 0 f: 0 g: 0 h: 0 i: 0 j: 0 k: 0 l: 0 m: 0
n: 0 o: 1 p: 1 q: 0 r: 1 s: 0 t: 0 u: 0 v: 1 w: 0 x: 0 y: 0 z: 0
*/
Libreria <cstring>
Fornisce funzioni per manipolare le C-String.
1. strcpy(char* dest, const char* sorg)
- Copia
sorgindest, incluso'\0'. - Restituisce
dest. - Attenzione: Rischio di buffer overflow se
destè più piccolo disorg.
2. strcat(char* dest, const char* sorg)
- Concatena (aggiunge in fondo)
sorgadest. - Restituisce
dest. - Attenzione: Rischio di buffer overflow se
destnon è grande abbastanza per contenere entrambe le stringhe.
3. strlen(const char* string)
- Restituisce la lunghezza della stringa (escluso
'\0').
4. strcmp(const char* s1, const char* s2)
- Confronta
s1es2in ordine lessicografico (ASCII). - Restituisce:
- Valore negativo se
s1 < s2 - 0 (zero) se
s1 == s2 - Valore positivo se
s1 > s2
- Valore negativo se
- È case-sensitive (
"a" != "A").
5. strchr(const char* string, char c)
- Cerca il primo carattere
cinstring. - Restituisce un puntatore alla prima occorrenza, o
NULL(nullptr) se non lo trova.
#include <iostream>
#include <cstring>
using namespace std;
int main() {
const int N = 30;
char s1[] = "Corso";
char s2[] = "di";
char s3[] = "Informatica\n";
char s4[N] = "Corso";
cout << "Dimensione array s1 e s4" << endl;
cout << sizeof(s1) << " " << sizeof(s4) << endl; // 6 30 (sizeof include '\0')
cout << "Lunghezza stringhe s1 e s4" << endl;
cout << strlen(s1) << " " << strlen(s4) << endl; // 5 5 (strlen NON include '\0')
if (strcmp(s1, s4) == 0)
cout << "Stringhe uguali" << endl;
else
cout << "Stringhe diverse" << endl;
if (strcmp(s1, s2) == 0)
cout << "Stringhe uguali" << endl;
else
cout << "Stringhe diverse" << endl << endl;
char s5[N];
strcpy(s5, s1); // s5 = "Corso"
strcat(s5, s2); // s5 = "Corso di"
strcat(s5, s3); // s5 = "Corso di Informatica\n"
cout << "Concatenazione di s1, s2 e s3" << endl;
cout << s5 << endl;
char* s = strchr(s5, 'I'); // Trova la 'I' maiuscola
cout << "Stringa dalla prima istanza di I" << endl;
cout << s << endl;
return 0;
}
Strutture e Unioni
Strutture (struct)
Una struttura è un'aggregazione di elementi (detti membri) anche di tipo diverso, identificati da un nome. Rappresenta un oggetto complesso (es. un punto, una data, un record).
Ogni membro ha il proprio spazio di memoria; i membri esistono contemporaneamente.
struct punto {
double x; // campo membro
double y; // campo membro
};
int main() {
punto r;
r.x = 3.0; // Accesso con selettore punto (.)
r.y = 10.5;
punto* p = &r;
cout << p->x; // Accesso tramite puntatore con freccia (->)
}
Allineamento e Padding nelle Strutture
L'occupazione di memoria di una struct non è sempre la semplice somma delle dimensioni dei suoi membri. Per ottimizzare l'accesso ai dati (allineamento), il compilatore inserisce dei byte "vuoti" chiamati padding.
Le CPU leggono la memoria in "parole" (word) di 4 o 8 byte. Se un membro inizia a un indirizzo non allineato, la CPU dovrebbe eseguire due letture invece di una. Il padding garantisce che ogni membro inizi a un indirizzo di memoria multiplo della sua dimensione, migliorando drasticamente le prestazioni.
struct Esempio {
char a; // 1 byte
// --- 3 byte di padding inseriti qui ---
int b; // 4 byte (deve iniziare su un indirizzo multiplo di 4)
char c; // 1 byte
// --- 3 byte di padding finale ---
};
int main() {
// Risultato: 12 byte, non 6! (1+3+4+1+3)
cout << "Dimensione struct: " << sizeof(Esempio);
}
double, poi gli int, infine i char).
Operazioni su Strutture
- Assegnamento (
=): È permesso. Copia i valori membro a membro.r2 = r1; // OK - Confronto (
==,!=): NON è definito di default. Non si possono confrontare due strutture direttamente. - Funzioni: Possono essere passate per valore (copia) o per riferimento/puntatore (modifica/efficienza).
struct vettore {
int vv[3];
};
// Passaggio per Riferimento (modifica l'originale)
void incrementa(vettore* v) {
v->vv[0]++;
}
// Passaggio per Valore (copia, non modifica l'originale)
void stampa(vettore v) {
cout << v.vv[0] << endl;
}
Unioni (union)
Le Unioni sono simili alle strutture, ma con una differenza fondamentale nella gestione della memoria.
- Differenza Chiave: In una
union, tutti i membri condividono la stessa area di memoria. La dimensione dell'unione corrisponde alla dimensione del suo membro più grande. - Utilizzo: Solo un membro alla volta può contenere un valore valido. Scrivere su un membro sovrascrive gli altri.
union Dato {
int i; // 4 byte
double d; // 8 byte
}; // Dimensione totale: 8 byte (condivisi)
int main() {
Dato val;
val.i = 10;
cout << val.i << endl; // Stampa 10
val.d = 20.5; // Sovrascrive i!
cout << val.d << endl; // Stampa 20.5
cout << val.i << endl; // Stampa valore "sporco" (bit di double letti come int)
}
Strutture Dati
Pila (Stack)
La Pila è una struttura dati che segue la logica LIFO (Last In First Out). Come una pila di libri, il primo libro messo (push) sarà l'ultimo a essere tolto (pop).
- Si basa su un puntatore (o indice)
topche indica la cima della pila. - Quando la pila è vuota,
top = -1. push: Inserimento del valore nella posizionetop + 1.pop: Estrazione del dato all'indicetop.
Può essere implementata sia con un Vettore (array) sia con una Lista.
Coda (Queue)
La Coda è una struttura dati che segue la logica FIFO (First In First Out). Come la coda al supermercato, il primo elemento inserito è il primo a uscire.
- Si basa su due puntatori/indici:
front(testa, da dove si estrae) eback(coda, dove si inserisce). - Per implementarla su un array si usa un "buffer circolare" (con l'operatore modulo
%). - Coda vuota:
front == back - Coda piena:
front == (back + 1) % DIM(si lascia sempre uno spazio vuoto per distinguere piena da vuota).
Liste Collegate
Concetto e Struttura
Le Liste sono strutture dati dinamiche molto versatili. Dobbiamo immaginarle come dei “vagoni” carichi di informazioni che si agganciano l’uno all’altro tramite puntatori.
- Caratteristiche: A differenza dei vettori, le liste hanno dimensione dinamica (può aumentare o diminuire).
- Implementazione: La logica si basa sulla creazione di un oggetto nella memoria dinamica (heap) che rappresenterà il nodo.
- Nodi: Ogni nodo contiene l'informazione (
inf) e punta al nodo successivo (pun).
Creazione e Operazioni in Testa
Creazione di una lista:
- Leggere l’informazione.
- Allocare un nuovo elemento con l’informazione da inserire.
- Collegare il nuovo elemento al primo elemento della lista.
- Aggiornare il puntatore di testa della lista a puntare al nuovo elemento.
typedef elem* lista; // Il tipo lista è un puntatore al primo nodo
lista crealista(int n) {
lista p0 = 0;
elem* p;
for (int i = 0; i < n; i++) {
p = new elem;
cin >> p->inf;
p->pun = p0; // Collega al vecchio primo elemento
p0 = p; // Aggiorna la testa
}
return p0;
}
Inserimento ed Estrazione in Testa:
// INSERIMENTO IN TESTA
void instesta(lista& p0, T a) {
elem* p = new elem;
p->inf = a;
p->pun = p0;
p0 = p;
}
// ESTRAZIONE DALLA TESTA
bool esttesta(lista& p0, T& a) {
elem* p = p0;
if (p0 == 0)
return false;
a = p0->inf;
p0 = p0->pun;
delete p;
return true;
}
Inserimento ed Estrazione in Coda
Operare in coda richiede la scansione della lista (se non si usa un puntatore ausiliario).
// INSERIMENTO IN CODA
void insfondo(lista& p0, T a) {
elem* p;
elem* q;
// Scorre fino alla fine
for (q = p0; q != 0; q = q->pun)
p = q;
q = new elem;
q->inf = a;
q->pun = 0;
if (p0 == 0)
p0 = q; // Lista vuota
else
p->pun = q; // Collega all'ultimo
}
// ESTRAZIONE DALLA CODA
bool estfondo(lista& p0, T& a) {
elem* p = 0;
elem* q;
if (p0 == 0)
return false;
// q scorre fino all'ultimo, p lo segue
for (q = p0; q->pun != 0; q = q->pun)
p = q;
a = q->inf;
// Controlla se si estrae il primo elemento
if (q == p0)
p0 = 0;
else
p->pun = 0;
delete q;
return true;
}
Estrazione da Lista Ordinata
bool estrordinata(lista& p0, T a) {
elem* p = 0;
elem* q;
// Scorre finché non trova l'elemento o supera il valore
for (q = p0; q != 0 && q->inf < a; q = q->pun)
p = q;
if ((q == 0) || (q->inf > a))
return false;
if (q == p0)
p0 = q->pun; // Estrazione in testa
else
p->pun = q->pun; // Estrazione intermedia
delete q;
return true;
}
Liste con Puntatore Ausiliario e Complesse
Per ottimizzare l'inserimento in coda (rendendolo O(1)), si mantiene un puntatore all'ultimo elemento.
struct lista_n {
elem* p0; // Testa
elem* p1; // Coda
};
// Creazione lista con puntatore ausiliario
lista_n crealista1(int n) {
elem* p;
lista_n li = {0, 0};
if (n >= 1) {
p = new elem;
cin >> p->inf;
p->pun = 0;
li.p0 = p;
li.p1 = p; // p1 punta all'unico elemento
for (int i = 2; i <= n; i++) {
p = new elem;
cin >> p->inf;
p->pun = li.p0;
li.p0 = p; // Inserimento in testa, p1 non cambia
}
}
return li;
}
// Inserimento in fondo O(1)
void insfondo1(lista_n& li, T a) {
elem* p = new elem;
p->inf = a;
p->pun = 0;
if (li.p0 == 0) {
li.p0 = p;
li.p1 = p;
} else {
li.p1->pun = p; // Collega l'attuale ultimo al nuovo
li.p1 = p; // Aggiorna p1
}
}
Liste Complesse (Doppiamente Collegate):
Sono liste con due puntatori per nodo: uno all'elemento precedente (prec) e uno a quello successivo (succ).
struct nu_elem {
T inf;
elem* prec;
elem* succ;
};
struct lista_c {
nu_elem* p0;
nu_elem* p1;
};
Argomenti di Default
Un parametro con valore di default è un parametro formale a cui viene assegnato un valore nell’intestazione della funzione. Se alla chiamata non si fornisce quell’argomento, il compilatore inserisce automaticamente il valore di default nella chiamata al momento della compilazione.
double peso(double lung, double diam = 10, double dens = 15);
// Chiamate:
peso(125) // diventa peso(125, 10, 15)
peso(35, 5) // diventa peso(35, 5, 15)
Regole Importanti (Riassunto)
- Possono avere inizializzatori nella dichiarazione (es.
double diam = 10). - I valori di default vengono inseriti dal compilatore nelle chiamate in cui gli argomenti attuali sono omessi (non è una assegnazione a runtime dentro la funzione).
- Gli argomenti con default devono trovarsi dopo tutti gli argomenti senza default; se si forniscono default solo per alcuni, questi devono essere gli ultimi parametri.
- Corretto:
f(int a, int b = 2, int c = 3). - Errato:
f(int a = 1, int b, int c = 3)→ non permesso.
- Corretto:
- Quando si chiama la funzione è possibile omettere tutti o solo gli ultimi argomenti con default; gli argomenti omessi devono essere quelli finali nell’elenco.
- Gli inizializzatori dei default non possono usare variabili locali né altri parametri formali della stessa funzione. Possono usare costanti globali o `constexpr` (valori noti a compile time).
- I default vengono specificati di solito nella dichiarazione/prototipo (header), e non ripetuti nella definizione (.cpp).
// header double perimetro(int nLati = 3, double lunghLato = 1.0); // .cpp double perimetro(int nLati, double lunghLato) { // senza =... return nLati * lunghLato; } - I default sono risolti al punto della chiamata, quindi devono essere visibili al punto di uso.
- I default possono creare ambiguità con l'overload, e ciò deve essere evitato.
Ambiguità e Correttezza Formale
L'ambiguità si verifica quando non tutti i parametri hanno un valore di default:
// 1) Dichiarazione con un parametro obbligatorio (nLati):
perimetro(int nLati, double lunghLato = 1.0);
void f() {
double p = perimetro(); // ERRORE: manca nLati e nLati non ha default
}
// 2) Dichiarazione con tutti i parametri di default:
double perimetro(int nLati = 3, double lunghLato = 1.0);
void g() {
double p = perimetro(); // OK: entrambi i parametri hanno default
}
Nella prima dichiarazione, nLati è obbligatorio, rendendo la chiamata perimetro() senza argomenti errata. Nella seconda, il compilatore sostituisce la chiamata con perimetro(3, 1.0).
Esempio pratico con calcolo:
double peso(double lung, double diam = 10, double dens = 15) {
diam /= 2;
return (diam * diam * 3.14 * lung * dens);
}
// double p1 = peso(125); // peso(125,10,15)
// double p2 = peso(35, 5); // peso(35,5,15)
// double p3 = peso(20, 4, 8); // peso(20,4,8)
Overloading (Sovraccarico)
L’overloading è una funzionalità del linguaggio C++ che permette di definire più funzioni o metodi con lo stesso nome ma con parametri diversi (firma diversa).
Una funzione è overloadata se differisce per:
- Numero argomenti (es.
somma(int a, int b)vssomma(int a, int b, int c)). - Tipo argomenti (es.
stampa(int i)vsstampa(std::string s)). - Ordine argomenti (es.
funz(int a, double b)vsfunz(double a, int b)).
NB: Il tipo di ritorno non conta; non è possibile avere due funzioni chiamate nello stesso modo (stessa firma) ma con un tipo di ritorno diverso.
È possibile applicare l’overloading anche ad operatori (come +, -, *, / e altri simboli) utilizzando come argomenti oggetti di classi o strutture per rendere il codice più leggibile ed intuitivo.
Argomenti Differenti (Esempio Tipo)
int massimo(int a, int b) {
cout << "Massimo per interi " << endl;
return a > b ? a : b;
}
double massimo(double a, double b) {
cout << "Massimo per double" << endl;
return a > b ? a : b;
}
int main() {
cout << massimo(10, 15) << endl; // Chiama int massimo(int, int)
cout << massimo(12.3, 13.5) << endl; // Chiama double massimo(double, double)
// cout << massimo(12.3, 13) << endl; // ERRORE: ambiguo
return 0;
}
Const e non Const
Si può overloadare una funzione se una ha tipo const e una no, sono differenti.
int massimo(const int v[], int n) {
cout << "Array const ";
int m = v[0];
for (int i = 1; i < n; i++)
m = (m >= v[i]) ? m : v[i];
return m;
}
int massimo(int v[], int n) {
cout << "Array non const ";
int m = v[0];
for (int i = 1; i < n; i++)
m = (m >= v[i]) ? m : v[i];
return m;
}
int main() {
const int N = 5;
const int cv[N] = {1, 10, 100, 10, 1};
cout << massimo(cv, N) << endl;
int v[N] = {1, 10, 100, 10, 1};
cout << massimo(v, N) << endl;
return 0;
}
Overloading e Argomenti di Default
L’aggiunta di un argomento di default può impedire la compilazione per ambiguità nella chiamata.
Scenario 1: Overload senza ambiguità
double perimetro(int nLati, double lunghLato) {
return nLati * lunghLato;
}
int perimetro(int lato) {
return 4 * lato; // perimetro del quadrato
}
int main() {
cout << perimetro(5); // OK: stampa 20, chiama perimetro(int lato)
return 0;
}
Scenario 2: Overload con Ambiguita (Aggiunta del Default)
double perimetro(int nLati, double lunghLato = 2.5) { // Aggiunto default
return nLati * lunghLato;
}
int perimetro(int lato) {
return 4 * lato; // perimetro del quadrato
}
int main() {
cout << perimetro(5); // ERRORE! Chiamata ambigua.
// Ci sono due funzioni che possono prendere un int come valore!
return 0;
}
Scenario 3: Overload con Tipi Distinti (Risoluzione Ambiguita)
double perimetro(int nLati, double lunghLato = 2.5) {
return nLati * lunghLato;
}
int perimetro(double lato) { // Firma diversa (double)
return 4 * lato; // perimetro del quadrato
}
int main() {
cout << perimetro(5.7); // OK: stampa 22 (22.8 troncato a 22), chiama perimetro(double lato)
return 0;
}
Questa versione del codice funziona perché possono coesistere due funzioni con singolo argomento, una avente argomento intero (la prima, dopo aver usato il default per il secondo) e l'altra argomento double (overloading delle funzioni).
Dichiarazioni Typedef
Le dichiarazioni typedef sono uno strumento per creare degli identificatori rispetto ai tipi in C++ (chiamati nomi typedef) e che possiamo usare per riferirsi ai tipi durante le dichiarazioni.
NB: typedef non introduce un nuovo tipo!
int main() {
int i = 1;
typedef int* intP;
intP p = &i; // p è un puntatore a int
cout << *p << endl; // 1
typedef int vett[5]; // vett è un alias per un array di 5 interi
vett v = {1, 10, 100, 10, 1};
cout << "v = [" << v[0];
for (int j = 1; j < 5; j++)
cout << ' ' << v[j];
cout << ']' << endl;
typedef int intero;
int a = 4;
intero b = a; // OK, typedef non introduce un nuovo tipo
cout << a << '\t' << b << endl; // 4 4
return 0;
}
Effetti Collaterali (Side Effects)
Definizione e Modalità di Generazione
Una funzione in C++ produce due tipi di risultati:
- Effetto Principale: Il calcolo del valore restituito tramite return.
- Effetto Collaterale: Qualsiasi modifica allo stato del programma che persiste dopo la fine della funzione (es. modifica di variabili non locali).
Una funzione void senza argomenti e senza effetti collaterali è considerata inutile. Tuttavia, funzioni come quelle di stampa (cout) sono utili proprio perché il loro effetto collaterale è modificare lo stream di output (una variabile globale).
Gli effetti collaterali si generano principalmente attraverso:
- Modifica di variabili Globali: La funzione modifica una variabile dichiarata all'esterno di essa.
int n = 5; // Variabile globale void incrementaGlobale() { ++n; // n passa da 5 a 6 } - Passaggio per Riferimento (&): La funzione riceve un riferimento alla variabile originale del chiamante, non una copia.
void incrementaRef(int &n) { ++n; // Modifica la variabile originale ('a' nel main) } - Passaggio tramite Puntatore (*): La funzione riceve l'indirizzo di memoria della variabile e ne modifica il contenuto tramite deferenziazione.
void incrementaPtr(int *p) { (*p)++; // Modifica il valore puntato }
Rischi e Ordine di Valutazione
L'uso di funzioni con effetti collaterali all'interno di espressioni complesse può portare a errori logici gravi e risultati imprevedibili. Questo accade perché il compilatore potrebbe non valutare gli operandi nell'ordine che ci si aspetta.
Considerando una funzione "pericolosa" che incrementa il maggiore tra due puntatori ma restituisce il valore NON ancora incrementato (post-incremento):
int incremMag(int *pa, int *pb) { if (*pa > *pb) return (*pa)++; // Restituisce *pa, poi lo incrementa else return (*pb)++; // Restituisce *pb, poi lo incrementa } Dati iniziali: i=10, j=100.
- Caso 1: Valutazione a sinistra (Legge i e j PRIMA di eseguire la funzione)
cout << i + j + incremMag(&i, &j); // Calcolo: 10 + 100 + 100 (valore restituito) = 210 // Risultato finale variabili: i=10, j=101 - Caso 2: Valutazione a destra (Esegue la funzione PRIMA di leggere i e j)
cout << incremMag(&i, &j) + i + j; // Funzione scatta subito: restituisce 100, MA incrementa j a 101. // Calcolo: 100 (valore restituito) + 10 + 101 (nuovo valore di j) = 211
Gli effetti collaterali sono potenti (permettono I/O e modifica di strutture dati), ma non dovrebbero mai essere usati all'interno di espressioni matematiche complesse, poiché l'ottimizzazione del compilatore può alterare il risultato finale.
Gestione della Memoria
Classi di Memorizzazione
La classe di memorizzazione risponde a due domande fondamentali sulla vita di una variabile:
- Durata (Lifetime): Quando viene creata e quando viene distrutta?
- Visibilità (Scope): Chi può vederla e usarla?
1. Classe Automatica (La "Variabile Usa e Getta")
È il comportamento di default per le variabili dichiarate dentro una funzione o un blocco { ... }.
- Dove vive: Nello Stack.
- Vita: Nasce quando l'esecuzione arriva alla riga della dichiarazione e muore immediatamente quando si chiude la parentesi graffa
}del blocco in cui si trova. - Comportamento: Non ha "memoria" tra una chiamata e l'altra della funzione. Ogni volta è una variabile nuova di zecca.
int contaChiamateErrata() {
int n = 0; // Classe AUTOMATICA
return ++n;
}
Risultato: Viene creato un nuovo spazio per n, impostato a 0, incrementato a 1 e restituito. La variabile viene distrutta. Restituirà sempre 1. Non sa quante volte è stata chiamata prima.
2. Classe Statica (La "Variabile con Memoria")
Una variabile statica ha una proprietà speciale: sopravvive per tutta la durata del programma, anche se la sua visibilità è limitata.
- Dove vive: In un'area di memoria dedicata (Data Segment).
- Vita: Nasce all'avvio del programma e muore solo quando il programma termina.
- Inizializzazione: Avviene una sola volta.
Possiamo avere due tipi di statica (variabile persistente):
A. Statica Locale (Dentro una funzione):
È visibile solo dentro la funzione, ma mantiene il suo valore tra una chiamata e l’altra.
int contaChiamate() {
static int n = 0; // Eseguita SOLO la prima volta
++m; // Modifica la variabile globale statica
return ++n; // Ricorda il valore precedente!
}
- Prima chiamata:
nviene creata e messa a 0. Viene incrementata a 1. - Seconda chiamata: L'istruzione
static int n = 0viene ignorata (perchénesiste già). Il programma riprende il vecchion(che era 1), lo incrementa e restituisce 2. - Terza chiamata: Riprende
n(che era 2), incrementa e restituisce 3.
Risultato: Conta effettivamente le chiamate (1, 2, 3...).
B. Statica Globale (Fuori dalle funzioni):
Essendo dichiarata static fuori dalle funzioni, questa variabile è visibile in tutto il file corrente, ma è nascosta agli altri file del progetto (collegamento interno). Vive per sempre.
3. Classe Dinamica (La "Variabile Manuale")
Qui il programmatore ha il controllo totale. La variabile non nasce e muore automaticamente; sei tu a decidere.
- Dove vive: Nello Heap (memoria libera).
- Vita: Nasce con
newe muore solo condelete. - Attenzione: Se dimentichi
delete, la memoria rimane occupata (Memory Leak).
int main() {
// 'p' è una variabile AUTOMATICA (sullo stack)
// che contiene l'indirizzo di un intero.
int *p;
// L'intero creato con 'new' è DINAMICO (sullo heap).
// Non ha nome, ci arriviamo solo tramite *p.
p = new int;
*p = 10; // Uso la variabile dinamica
delete p; // Distruggo la variabile dinamica
// Se non lo faccio, quell'int resta in memoria per sempre!
return 0;
}
Segmento RODATA (Read-Only Data)
Il segmento .rodata è un'area di memoria dedicata esclusivamente ai dati immutabili. Durante la fase di caricamento del programma, il Sistema Operativo marca queste pagine di memoria come "sola lettura". Qualsiasi tentativo di scrittura in quest'area da parte del software provocherà un'eccezione hardware immediata, solitamente un Access Violation o Segmentation Fault.
In questo spazio risiedono le costanti globali e, soprattutto, le stringhe letterali. Quando scrivi un testo tra virgolette nel codice, quel testo non "nasce" sullo stack, ma viene cablato in questa zona protetta per essere condiviso in tutto il programma senza rischio di corruzione.
// La stringa letterale risiede fisicamente in .RODATA
const char* etichetta = "SISTEMA_OPERATIVO";
int main() {
// Costante dichiarata a livello locale
const int SOGLIA_CRITICA = 1024;
// Errore Semantico: etichetta[0] = 'A';
// Crash a runtime perché si tenta di scrivere in .RODATA
return 0;
}
Segmento BSS (Block Started by Symbol)
Il segmento .bss rappresenta una sezione di memoria statica ottimizzata per l'efficienza. Qui vengono allocate le variabili globali e le variabili static che non sono state inizializzate esplicitamente dal programmatore o che sono state inizializzate a zero.
Una caratteristica fondamentale del BSS è che non occupa spazio fisico all'interno dell'eseguibile salvato su disco. Il file binario contiene solo un'istruzione che indica la dimensione totale richiesta. All'avvio del programma, il Sistema Operativo riserva questa memoria in RAM e garantisce la pulizia automatica, impostando ogni singolo bit a zero. Questo assicura che variabili non inizializzate abbiano un valore prevedibile (0, 0.0 o NULL).
// Allocata in .BSS (non occupa spazio sul disco, azzerata a runtime)
int contatoreGlobale;
static double bufferDati[5000];
int main() {
// Anche senza assegnazione, il valore è garantito essere 0
if (contatoreGlobale == 0) {
// Eseguito sempre grazie alle proprietà del segmento BSS
}
return 0;
}
.data, dove ogni valore deve essere memorizzato nel file eseguibile (es. int x = 123;), il segmento .bss permette di avere eseguibili molto piccoli anche se dichiarano array globali di enormi dimensioni.
Memoria Dinamica (Heap)
Le tecniche di programmazione standard (lavorando con lo Stack) richiedono che il valore di una variabile (come la dimensione di un array) sia conosciuto a tempo di compilazione.
Per superare questo limite e poter dimensionare un array in base al volere dell'utente (a tempo di esecuzione), si utilizza la Memoria Dinamica (o Memoria Libera).
Il meccanismo di memoria dinamica permette di allocare variabili nell'area Heap (una parte della RAM) e di accedervi tramite puntatori. Queste variabili sono chiamate variabili dinamiche.
Allocazione con new
Per allocare una variabile nell'Heap si utilizza la keyword new:
- Argomento: Il tipo di variabile che si vuole allocare.
- Restituisce: L’indirizzo di memoria su cui la variabile è stata allocata, che viene assegnato a un puntatore.
- Fallimento: Se non è possibile ottenere l’indirizzo (memoria esaurita), restituisce 0 (o lancia un'eccezione, a seconda della configurazione).
int main() { int* q; q = new int; // Alloca un singolo intero *q = 10; cout << *q << endl; // Stampa 10
int* p;
int n;
cin >> n;
p = new int[n]; // Alloca un array di n interi
// Utilizzo dell'array:
for (int i = 0; i < n; i++)
p[i] = i; // o *(p + i) = i;
}
Gestione Errori con set_new_handler()
Per verificare il buon esito dell’operatore new si può utilizzare la funzione set_new_handler() (dichiarata in <new>), che prende come argomento una funzione void (definita dall'utente) da lanciare nel caso in cui l'allocazione nella memoria dinamica fallisca.
void myhandler() { cerr << "Memoria libera non disponibile" << endl; exit(1); }
int main() { int n; set_new_handler(myhandler); // Imposta la funzione di gestione errori cout << "Inserisci la dimensione" << endl; cin >> n; // La chiamata a new qui può lanciare myhandler() in caso di esaurimento memoria. int** m = new int*[n]; }
Deallocazione con delete
Le variabili allocate nell'Heap esisteranno fino a quando non verranno deallocate con il comando delete o fino a quando il programma non terminerà.
- L’operatore delete prende come argomento un puntatore all’indirizzo di memoria della variabile allocata nell'Heap.
- Importante: Può essere usata solo con un puntatore che indirizza un oggetto allocato tramite l’operatore new (altrimenti si commette un errore).
Deallocazione e Errori Comuni:
int main() { int n = 12; int* p = new int(10);
delete p; // Deallocazione del singolo intero
// cout << *p << endl; // SBAGLIATO, ma non segnala errore (comportamento indefinito)
p = 0; // o nullptr, per prevenire il dangling pointer
// cout << *p << endl; // SEGNALA ERRORE A TEMPO DI ESECUZIONE (dereferenziazione di nullptr)
int* m = new int[n];
delete[] m; // Deallocazione corretta di un array (uso delle parentesi [])
// delete m; // SBAGLIATO per un array allocato con new int[n]
// delete &n; // ERRORE – n non è allocato dinamicamente con new
}
Gestione unità di codice
Unità di Compilazione
L’unità di compilazione è una parte del compilatore che si occupa di prendere il file di codice e trasformarlo nel file oggetto intermedio, interpretabile (o quasi) dal processore.
In C++ i file di compilazione costituiscono tutti i file sorgente ed i file inclusi tramite #include.
NOTA: Se il file non fa parte delle librerie deve essere incluso tramite l’utilizzo delle virgolette (e non con <..> come per i file facenti parte delle librerie).
#include <iostream>
using namespace std;
int i; // visibilità a livello di unità di compilazione
void leggi() // visibilità a livello di unità di compilazione
{
cout << "Inserisci un numero intero " << endl;
cin >> i;
}
void scrivi() // visibilità a livello di unità di compilazione
{
cout << i << endl;
}
int main()
{
leggi();
scrivi();
}
Output:
Inserisci un numero intero
2
2
In pratica gli elementi visibili a livello di unità di compilazione possono essere accessibili da altri file, ma soprattutto possono accedere ad ogni variabile definita a livello di file (quindi globale, visibile ad unità di compilazione), in questo caso la i è sempre accessibile per tutte le funzioni.
Ovviamente nel caso in cui una funzione sia definita ad unità di compilazione non potrà accedere a variabile non definite al suo stesso livello.
#include <iostream>
using namespace std;
int i = 1; // visibilità a livello di unità di compilazione
int main()
{
cout << i << endl; // 1
{
int i = 5; // visibilità locale
cout << ::i << '\t' << i << endl; // 1 5
{
int i = 10; // visibilità locale
cout << ::i << '\t' << i << endl; // 1 10
}
}
cout << ::i << endl; // 1
return 0;
}
Spazio di Nomi (Namespace)
I namespace in C++ servono a organizzare il codice e evitare conflitti di nomi. Sono come "cartelle logiche" in cui puoi mettere variabili, funzioni, classi, strutture, ecc.
Con i namespace posso dichiarare variabili, funzioni o strutture con lo stesso nome e distinguerli per namespace.
namespace LibreriaA {
void print() { /*...*/ }
}
namespace LibreriaB {
void print() { /*...*/ }
}
// < ---Main--- >
LibreriaA::print();
LibreriaB::print();
using namespace LibreriaA; // direttiva
print(); // usa la funzione della ‘LibreriaA’
L’uso della direttiva using namespace può generare delle ambiguità.
int main() {
uno::st ss1;
using namespace uno;
using namespace due;
// st ss2; // ERRORE: Ambiguità se st esiste in entrambi
due::st ss2; // using due::st; -> per ‘st’ uso il due
}
I namespace sono aperti: puoi dichiarare lo stesso namespace più volte in file diversi o nello stesso file, e aggiungere nuovi elementi ogni volta. Anche se appare in due blocchi separati, è lo stesso namespace; tutto si unisce.
namespace esempio {
int a = 10;
}
// più avanti nel codice...
namespace esempio {
void stampa() {
std::cout << a << std::endl;
}
}
Tutto ciò che viene dichiarato fuori da qualsiasi funzione, classe o namespace sta nel namespace globale. Puoi fare riferimento al namespace globale usando l’operatore :: senza nome davanti.
Collegamento (Linkage)
Partiamo dal presupposto che un programma può essere composto da più unità di compilazione, trattate prima singolarmente, e poi unite assieme per formare il file eseguibile. Per sviluppare un programma su più file abbiamo bisogno che siano collegati tra di loro; da qui possiamo definire due tipi di collegamento:
- Collegamento Interno: Usato da un identificatore se e solo se si riferisce ad un’entità propria della singola unità di compilazione.
Nota: Uno stesso identificatore che ha collegamento interno in più unità di compilazione, si riferirà in ognuna ad un’entità diversa. - Collegamento Esterno: Usato da un identificatore se si riferisce ad un'entità accessibile anche da altre unità di compilazione.
Nota: Tale entità deve essere unica in tutto il programma.
Regole di base:
- Gli identificatori con visibilità locale hanno collegamento interno.
- Gli identificatori con visibilità a livello di unità di compilazione hanno collegamento esterno (a meno che non siano dichiarati
const).
Gli oggetti con visibilità a livello di unità di compilazione possono essere utilizzati in più unità di compilazione.
Utilizzo di extern
Per definire e dichiarare un identificatore/variabile/funzione in un file esterno rispetto a quello in cui abbiamo dichiarato questi ultimi usiamo la keyword extern.
- Oggetto: Se lo dichiaro con
externnon posso assegnargli un valore (es.extern int i;).
Se provassi ad assegnargli un valore forzerei una definizione, con il rischio di duplicare la variabilei; una volta messi insieme i file, il linker darebbe l’errore di Ridefinizione Multipla (due celle di memoria per la stessa variabile). - Funzione:
- Viene solo dichiarata se si specifica solo l'intestazione (si può usare
externse la definizione è altrove). - Viene anche definita se si specifica anche il corpo.
- Viene solo dichiarata se si specifica solo l'intestazione (si può usare
Osservazione: Analogamente agli oggetti con visibilità a livello di unità di collegamento (oggetti condivisi), anche gli oggetti con collegamento esterno (oggetti globali) permettono la condivisione di informazioni fra funzioni.
/*------ FILE 1 ------*/
int a = 1; // collegamento esterno
const int N = 0;
static int b = 10; // static, collegamento interno
void f1(int a) {
int k; /* ... */
}
static void f2() { // static, collegamento interno
/* ... */
}
struct punto {
double x;
double y;
};
punto p1; // collegamento esterno
/*------ FILE 2 ------*/
#include <iostream>
using namespace std;
extern int a; // solo dichiarazione
void f1(int); // solo dichiarazione
void f2(); // solo dichiarazione
void f3(); // solo dichiarazione
double f4(double, double); // definizione mancante (OK se non utilizzata)
int main() {
cout << a << endl; // OK, 1
extern int b; // dichiarazione
// cout << b << endl; // ERRORE! b in FILE 1 ha collegamento interno
f1(a); // OK
// f2(); // ERRORE! f2 in FILE 1 è static
// f3(); // ERRORE! f3 non definita
// punto p2; // ERRORE! punto non dichiarato in FILE 2
// p1.x = 10; // ERRORE! p1 non dichiarato in FILE 2
return 0;
}
Stesso tipo in più unità di compilazione: Viene verificata solo l’uguaglianza tra gli identificatori del tipo; se l’organizzazione interna non è la stessa, si hanno errori logici a tempo di esecuzione.
Moduli e astrazioni
Regola del Cortocircuito
La regola del cortocircuito si basa sul valutare l’espressione di una condizione ed in base al funzionamento degli operatori utilizzati (OR o AND) determinare il risultato dell’espressione senza però valutarla tutta. In pratica, secondo la regola del cortocircuito, il compilatore può interrompere la valutazione dell’espressione logica non appena conosce il risultato finale.
Cortocircuito applicato all'AND (&&)
L’AND (&&) è un operatore logico dove se anche solo un’espressione è falsa, allora restituirà falso, mentre se entrambe le espressioni sono vere restituirà vero.
Supponendo di avere una condizione AND con due espressioni logiche del tipo: expr1 && expr2. Secondo il cortocircuito valuterò sicuramente la prima espressione a sinistra; se questa risulta false, tuttavia, per il comportamento dell’operatore AND, il risultato sarà sicuramente falso, perciò la valutazione dell’espressione si fermerà alla prima espressione.
Esempio 1: Valutazione ed effetti collaterali
int main() {
int a = -1, b = 2;
// La condizione è falsa perché expr1 (++a) diventa 0 (false)
if (++a && --b)
cout << b << endl; // NON viene eseguito
cout << a << ' ' << b << endl; // stampa: 0 2 (b non è stato decrementato)
// Questa volta expr1 (++a) diventa 1 (true), quindi valuta anche --b
if (++a && --b)
cout << a << ' ' << b << endl; // stampa: 1 1
return 0;
}
Esempio 2: Sicurezza (Puntatori e Divisioni)
int main() {
int num, den;
cin >> num >> den;
// AND logico: esempio 2 (evita divisione per zero)
if (den != 0 && num / den > 5)
cout << "Il rapporto è maggiore di cinque" << endl;
// AND logico: esempio 3 (utile con liste/array dinamici)
int* p = nullptr;
allocaVettoreMemDin(p, 10);
// Evita dereferenziazione di puntatore nullo
if (p != nullptr && *p < 0)
cout << "Il primo elemento è negativo" << endl;
return 0;
}
Cortocircuito applicato all'OR (||)
L’OR (||) è un operatore logico dove se anche solo una espressione è vera allora il risultato sarà vero; l’unico caso in cui ritornerà falso sarà nel caso in cui entrambe le espressioni logiche ritorneranno falso.
Supponendo di avere una condizione OR con due espressioni logiche del tipo: expr1 || expr2. Secondo il cortocircuito valuterò sicuramente la prima espressione a sinistra; se questa risulta true, tuttavia, per il comportamento dell’operatore OR, il risultato sarà sicuramente vero, perciò la valutazione dell’espressione si fermerà alla prima espressione.
Esempio 1: Valutazione ed effetti collaterali
int main() {
int a = -1, b = 2;
// OR logico: expr1 (++a) è 0 (false) -> valuta expr2 (--b)
if (++a || --b)
cout << b << endl; // stampa 1
cout << a << ' ' << b << endl; // stampa 0 1
// OR logico: expr1 (++a) è 1 (true) -> cortocircuito, expr2 non valutata
if (++a || --b)
cout << a << ' ' << b << endl; // stampa 1 1
return 0;
}
Esempio 2: Ottimizzazione e Controlli
int main() {
int* p = nullptr;
// Esegue l'allocazione SOLO se p non è già allocato (se vettoreAllocato ritorna false)
if (vettoreAllocato(p) || allocaVettoreMemDin(p, 10))
cout << "Il vettore risulta allocato" << endl;
// OR logico: Esempio 3
// Controlla se l'indice è invalido PRIMA di accedere all'array
if (indiceInvalido(indice) || vett[indice] <= 0)
cout << "Operazione di decremento fallita";
else
vett[indice]--; // decremento solo se l’indice è valido e vett[indice] > 0
return 0;
}
Moduli
Un programma complesso, per facilitare la manutenzione del codice, può essere diviso in moduli, cioè parti di programma adibite a determinati compiti; a sua volta un modulo può essere composto da uno o più file.
Esistono due categorie di moduli:
- Servitori (Server): Offre risorse di varia natura, funzioni, variabili globali, tipi eccetera. Sono formati la maggior parte delle volte da due file:
- Un file
.h(intestazione/interfaccia), dove si dichiarano i servizi offerti (quindi funzioni, variabili ecc). - Un file
.cpp(che contiene invece il corpo del modulo, la sua realizzazione).
NOTA: lo scopo della separazione tra interfaccia e realizzazione ha lo scopo dell’information hiding, grazie al quale si semplificano le dipendenze fra moduli e inoltre si permette di modificare la realizzazione di un modulo senza influenzare il funzionamento dei suoi clienti.
- Un file
- Clienti: Utilizza ed importa le risorse offerte dai moduli servitori, implementando la loro interfaccia (questi vengono sviluppati senza sapere il contenuto dei moduli servitori).
ESEMPIO (Modulo Pila):
// ESEMPIO PILA
// MODULO SERVER
// file pila.h
#ifndef PILA_H
#define PILA_H
typedef int T;
const int DIM = 5;
struct pila {
int top;
T stack[DIM];
};
void inip(pila& pp);
bool empty(const pila& pp);
bool full(const pila& pp);
bool push(pila& pp, T s);
bool pop(pila& pp, T& s);
void stampa(const pila& pp);
#endif
// ESEMPIO PILA
// MODULO CLIENT
// file pilaMain.cpp
#include <iostream>
#include "pila.h"
using namespace std;
int main()
{
pila st;
inip(st);
T num;
if (empty(st))
cout << "Pila vuota" << endl;
for (int i = 0; i < DIM; i++)
if (push(st, DIM - i))
cout << "Inserito " << DIM - i << ". Valore di top: " << st.top << endl;
else
cerr << "Inserimento di " << i << " fallito" << endl;
if (full(st))
cout << "Pila piena" << endl;
}
Comandi di Compilazione per Moduli
Per compilare programmi distribuiti su più file si usano comandi separati. Ecco una tabella riassuntiva per l'uso di g++:
| Scenario | Azione | Sistema | Comando g++ | File Generati | Esecuzione |
|---|---|---|---|---|---|
| File Singolo | Compilazione (solo oggetto) | Win/Linux | g++ -c main.cpp -o main.obj |
main.obj | N/A |
| File Singolo | Linking (genera eseguibile) | Windows | g++ main.obj -o main.exe |
main.exe | main |
| File Multipli | Compilazione Modulo 1 | Win/Linux | g++ -c main.cpp -o main.obj |
main.obj | N/A |
| File Multipli | Compilazione Modulo 2 | Win/Linux | g++ -c pila.cpp -o pila.obj |
pila.obj | N/A |
| File Multipli | Linking dei Moduli | Windows | g++ main.obj pila.obj -o main.exe |
main.exe | main |
| Completo | Compilazione e Linking | Windows | g++ main.cpp pila.cpp -o main.exe |
main.exe | main |
| Completo | Compilazione e Linking | Linux/macOS | g++ main.cpp pila.cpp -o main |
main | ./main |
Astrazioni Procedurali
L'astrazione procedurale è un concetto fondamentale della programmazione che serve a gestire la complessità del codice nascondendo i dettagli "tecnici" di come un compito viene svolto.
In breve, funziona secondo il principio della "Scatola Nera" (Black Box): sai cosa fa la scatola (input/output), ma non hai bisogno di sapere cosa succede al suo interno.
Ecco i punti chiave spiegati in modo semplice:
- Separazione dei Ruoli:
- Modulo Servitore (Server): È chi scrive le funzioni (la libreria). Mette a disposizione strumenti.
- Modulo Cliente (Client): È chi usa le funzioni (ad esempio il tuo
main).
- Organizzazione dei File:
- L'Interfaccia (File di intestazione
.h): Contiene solo le dichiarazioni (i nomi delle funzioni e i parametri). È come un "menu": ti dice cosa puoi ordinare. Il cliente include questo file. - La Realizzazione (File sorgente
.cpp): Contiene il codice vero e proprio (la logica interna). È la "cucina": dove il lavoro viene svolto. Il cliente non vede né include questo file.
- L'Interfaccia (File di intestazione
- Il Vantaggio: Puoi usare funzioni complesse (come quelle per le stringhe in
<cstring>) sapendo solo come chiamarle e cosa restituiscono, ignorando completamente la loro complessa programmazione interna.