Basi di programmazione C++
Introduzione alla programmazione C/C++
Appunti di programmazione C e C++ su GNU/Linux
Parte seconda
Strutture di controllo
Un programma sarebbe del codice statico se non ci fosse la possibilità di compiere delle scelte, saltare alcune parti o eseguirne delle altre, ripetere delle operazioni, eccetera... per questo, il C ci mette a disposizione alcuni costrutti per far pensare il nostro programma e dargli una sua struttura. Dalla programmazione procedurale si passa quindi alla programmazione strutturata.
Costrutti condizionali
In C (e C++) abbiamo a disposizione vari costrutti condizionali, che consentono di eseguire blocchi di codice solo se una condizione è verificata.
Costrutto if
if
, dall'inglese se, è un costrutto che esegue
il blocco successivo soltanto se l'espressione contenuta nelle parentesi tonde
è vera.
... if (a == 4) /* se a è uguale a 4 */ { /* ... inserire qui le istruzioni da eseguire... */ } ...
A coppia con if
è possibile aggiungere else
, che sta a indicare in tutti
gli altri casi...; in questo modo viene eseguito il blocco di codice di if se la condizione è vera,
altrimenti viene eseguito il blocco di codice di else:
... if (a > 4) /* se a è maggiore di 4 */ { /* viene eseguito questo blocco, */ } else /* altrimenti, */ { /* viene eseguito quest'altro blocco */ } ...
Importante: if ed else non hanno necessariamente bisogno di un blocco di istruzioni da eseguire: quando si deve eseguire una sola istruzione, per semplicità, le parentesi graffe che delimitano il blocco possono essere omesse.
Osservazione: specialmente per chi è alle prime armi, è facile commettere l'errore di valutare un'uguaglianza con l'operatore = anziché con l'operatore ==.
Il seguente programma riassume quanto visto fin'ora. Si consiglia di compilarlo, verificarne il funzionamento, capirlo e estenderlo.
/* Programma che dice se un numero è pari */ #include <stdio.h> int main(int argc, char** argv) { int n; printf("Inserisci il numero: "); scanf("%d", &n); if (n % 2) printf("Dispari"); else printf("Pari"); printf("\n"); return 0; }
Costrutto switch
Spesso capita di dover effettuare delle scelte tra un numero limitato
di alternative, ad esempio, per svolgere un determinato compito in
dipendenza del giorno della settimana.
Per questo scopo esiste il costrutto switch
.
int giornoDellaSettimana; switch (giornoDellaSettimana) { case 1: /* Istruzioni del lunedì... */ break; case 2: /* Istruzioni del martedì... */ break; case 3: /* Istruzioni del mercoledì... */ break; case 4: /* Istruzioni del giovedì... */ break; case 5: /* Istruzioni del venerdì... */ break; case 6: /* Istruzioni del sabato... */ break; case 7: /* Istruzioni della domenica...*/ break; default: /* Che giorno sarà? */ break; }
- Tra parentesi tonde, deve essere specificata la variabile da controllare. Essa deve poter assumere valori discreti (non continui) e distinti: perciò, si può effettuare una scelta su int o su char, ma non su float e double.
- L'esecuzione delle istruzioni inizia alla prima occorrenza di case
che soddisfa la condizione, e prosegue fino ad incontrare un break. Tale comportamento:
- può essere sfruttato a proprio vantaggio, in quanto consente di eseguire una stessa operazione per più casi (una sorta di or inclusivo);
- può indurre in errori quando ci si dimentica di inserire il break.
case
, intero, reale o stringa che sia, deve essere una costante letterale. È quindi errato inserire nel confronto delcase
una variabile (anche se costante) o un range di valori (es.< 7
). - default è un caso speciale, che viene eseguito quando nessuno dei casi precedenti si è verificato. Per tale ragione, deve essere sempre posto alla fine dello switch.
Ecco qui un esempio da compilare, verificarne il funzionamento, capire e correggere e modificare (è segnalato un errore di distrazione comune).
/* Scegli la marcia da innestare, e mostra una situazione tipo in cui utilizzarla */ #include <stdio.h> int main() { printf("Quale marcia innesti? (1-5) "); int m; scanf("%d", &m); switch (m) { case -1: printf("In manovra."); break; case 1: printf("Stai partendo."); break; case 2: case 3: /* Esempio di OR inclusivo */ printf("Circolazione urbana."); break; case 4: printf("Sulla circonvallazione."); /* Dimenticato del break? */ case 5: printf("In autostrada."); break; default: printf("Questa marcia non esiste!"); break; } printf("\n"); return 0; }
Costrutti iterativi
I costrutti iterativi sono particolari costrutti del C che permettono di eseguire più volte le stesse operazioni. Per questa ragione, sono più comunemente conosciuti come cicli.
Costrutto while
e do...while
Il ciclo while
permette di eseguire del codice finché una condizione
è vera. La veridicità della condizione viene calcolata prima ancora di
entrare nel ciclo, così si può fare in modo che, in determinate circostanze,
il ciclo non venga eseguito nemmeno una volta.
Nel ciclo do...while
, invece, la condizione viene valutata
solamente alla fine del ciclo, il che implica che le istruzioni vengono
eseguite almeno una volta.
... while (condizione) { /* istruzioni da eseguire finché la condizione è vera */ } ...
... do { /* istruzioni da eseguire almeno una volta, e finché la condizione è vera */ } while (condizione); ...
Ecco un programma d'esempio, come sempre da compilare, verificarne il funzionamento, capire e modificare a proprio piacere per impratichirsi.
/* Cicla finché non si immette un determinato numero */ #include <stdio.h> int main() { int r; do { printf("Per uscire, scrivi 5: "); scanf("%d", &r); } while (r != 5); return 0; }
Costrutto for
Un altro ciclo utilizzato comunemente nella programmazione, è il ciclo for
.
for (avvio; condizione; istruzione) { /* ... altre istruzioni da eseguire ... */ }
Dove:
- Avvio rappresenta l'istruzione da eseguire prima di iniziare il ciclo;
- Condizione rappresenta un'espressione che, finché è vera, garantisce che il ciclo verrà eseguito; essa viene controllata prima di ogni iterazione.
- Istruzione rappresenta l'istruzione da eseguire automaticamente alla fine di ogni iterazione.
Il che implica, banalmente, che un ciclo for non è altro che un ciclo while costruito in questo modo:
avvio; while (condizione) { /* ... altre istruzioni da eseguire ... */ istruzione; }
Data la sua forma molto concisa, il ciclo for
è particolarmente utile per ripetere del codice un numero prestabilito
di volte, anche se può essere utilizzato in molti altri ambiti, ad esempio,
per scorrere agevolmente una lista a basso livello. Ecco un programma
d'esempio che conta da 1 fino al numero immesso, da compilare, capire
e modificare a piacere:
#include <stdio.h> int main() { int n; printf("Fino a quanto devo contare? "); scanf("%d", &n); for (int i = 1; i <= n; ++i) { printf("%d... ", i); } printf("\n"); }
Nota: i tre campi del for non sono obbligatori, quindi possono essere omessi a piacere. Se si omettono tutti i campi, si genera un ciclo infinito.
Alienazione dei cicli
Il linguaggio C mette a disposizione due parole chiave che consentono di modificare l'esecuzione dei cicli a piacere, saltando iterazioni o interrompendo del tutto il ciclo: continue e break.
- continue consente di interrompere l'esecuzione di una iterazione e saltare direttamente alla successiva, se presente;
- break invece interrompe l'esecuzione del ciclo ed esce.
Esempio dimostrativo:
#include <stdio.h> int main() { short c; while (1) { printf("Inizio ciclo\n"); printf("\t0 - continue\n\t1 - break\n\t2 - Normale\n\tScelta? "); scanf("%d", &c); if (c == 0) continue; if (c == 1) break; printf("Fine ciclo\n"); } return 0; }
Costanti
Un modo comodo per utilizzare dei dati che non devono essere modificati è utilizzare delle costanti. L'utilizzo delle costanti ha alcuni vantaggi:
- riduce la probabilità di commettere errori logici, poiché il compilatore si rifiuta di compilare un'istruzione che modifica il contenuto di una costante;
- consente di risparmiare memoria a tempo di esecuzione.
Le costanti possono assumere lo stesso tipo delle variabili e si istanziano anteponendo la parola chiave const alla loro definizione come variabile. Si noti che una costante deve necessariamente essere definita assegnandole il valore.
const double pi = 3.14159265359;
Array
Detti anche vettori, o matrici quando si sviluppano su più dimensioni, l'array è una struttura che consente di memorizzare sequenzialmente più valori dello stesso tipo. Ad esempio, si vogliano memorizzare le temperature medie dei mesi dell'anno in 12 interi:
int temperatura[12]; /* allocazione in memoria di 12 int */
Ogni elemento dell'array è accessibile attraverso l'operatore [ ] (parentesi quadre) attraverso il numero della sua posizione all'interno della struttura. Si osservi che il primo valore di un array è contenuto nella locazione 0, e l'n-esimo nella locazione n - 1.
temperatura[0] = -4; /* imposta la temperatura di Gennaio a -4 */
Osservazione: quando si definisce un vettore, vengono occupate tante locazioni di memoria consecutive quante ne sono necessarie a contenerlo completamente; tuttavia esse non vengono inizializzate con nessun valore, pertanto, se si interpreta il loro contenuto, in realtà si andrà a leggere ciò che era precedentemente contenuto in tali locazioni, il che significa che, nella maggior parte dei casi, si leggeranno valori fuori contesto, casuali e apparentemente privi di senso. È quindi buona norma inizializzare un array prima di leggerne i valori; questa operazione è facilmente programmabile con un ciclo for.
Osservazione: per poter stabilire la quantità di memoria necessaria per contenere l'array, il compilatore deve conoscere il numero di elementi che esso contiene al momento della compilazione, perciò tale valore non può che essere costante. Si vedrà in seguito come gestire dinamicamente la memoria per realizzare array e altre strutture dati estensibili a piacere a tempo di esecuzione.
int n = 12; int temperatura[n]; /* Esempio sbagliato: n non è costante */
In questo esempio, viene popolato (e successivamente mostrato) il vettore contenente le temperature medie dei vari mesi dell'anno (anche se, per semplicità, la distribuzione lineare dei valori non rappresenta il reale andamento della temperatura). Lo si compili, se ne verifichi il funzionamento, si capisca e lo si modifichi a piacere per esercizio.
#include <stdio.h> int main() { const int n = 12; int temperatura[n]; for (int i = 0; i < n; ++i) { if (i < 6) temperatura[i] = 6 + 3 * i; else temperatura[i] = 6 + (12 - i) * 3; } printf("Mese\tTemperatura\n"); for (int i = 0; i < n; ++i) printf("%d\t%d\n", i + 1, temperatura[i]); return 0; }
Un modo alternativo di inizializzare un array è l'inizializzazione esplicita, indicando gli elementi tra parentesi graffe e separati da virgola, esattamente come si fa con gli insiemi:
int temperatura[] = { 6, 9, 12, 15, 18, 21, 24, 21, 18, 15, 12, 9 };
In questo caso non è necessario specificare la dimensione del vettore, perché il compilatore può facilmente dedurla. È possibile anche assegnare un valore noto ad un sottoinsieme degli elementi dell'array; in questo caso, inoltre, alle locazioni rimanenti verrà convenzionalmente assegnato un valore nullo:
int temperatura[12] = { 6, 9, 12 };
Una forma speciale per inizializzare tutte le locazioni a 0, molto concisa, è:
int temperatura[12] = { 0 };
Matrici
Viene detta matrice un vettore di vettori (o un vettore a più dimensioni). Questi nomi derivano direttamente dalle corrispondenti strutture dell'algebra, perciò risulteranno familiari a chiunque abbia anche solo letto l'introduzione di un qualunque libro di suddetta disciplina. La dichiarazione di una matrice e l'accesso ai suoi elementi avviene per mezzo dell'applicazione ripetuta dell'operatore [ ] (parentesi quadre).
int scacchiera[8][8]; printf("%d", scacchiera[0][0]);
L'argomento degli array e delle matrici verrà approfondito nel capitolo dei puntatori, dopo l'introduzione all'aritmetica dei puntatori.
C++
Una volta presa confidenza con le basi della programmazione in C, innalziamo il nostro livello di astrazione. Un buon sottoinsieme degli argomenti affrontati da ora in avanti, sono comunque validi anche in C, ma risulteranno molto meno ostici se si aggiunge quel poco di zucchero sintattico che tuttavia semplifica enormemente la scrittura del codice. Con l'espressione zucchero sintattico si denotano quei costrutti non necessari alla programmazione in sé, in quanto le operazioni da essi eseguite sono comunque fattibili con gli strumenti che si hanno a disposizione; tuttavia, questi costrutti semplificano enormemente il compito del programmatore, consentono di realizzare progetti più complessi con un approccio lineare, e aumentano la vita utile e la leggibilità del codice.
Come abbiamo già fatto con il C, compiliamo il nostro primo programma in C++. Da ora in avanti, ogni programma di questa guida verrà compilato con il compilatore C++.
La mia "seconda" compilazione
Listato del programma di esempio, da copiare, incollare in un qualunque
editor, e salvare con estensione .cpp, ad esempio
hello.cpp
:
#include <iostream> using namespace std; int main() { cout << "Hello world!" << endl; return 0; }
Comando per compilare il programma, che invoca il compilatore g++
e che funziona in maniera del tutto analoga a gcc
:
g++ hello.cpp
Come sempre, avviamo il programma con:
./a.out
e verifichiamo che stampi l'output atteso:
Hello world!
iostream
Dopo aver rivisto il concetto di flusso già affrontato nei primi capitoli, introduciamo il codice che ci permetterà di gestire i flussi più semplicemente in C++, in modo da capire subito le enormi potenzialità di questo linguaggio. In un certo senso, dovremo disimparare alcune cose apprese fino ad ora, ma ci accorgeremo che reimpararle col nuovo metodo sarà molto più facile e tuttavia rimarremo consapevoli di ciò che è mascherato all'apparenza, ma che si cela dietro le quinte. Quanto affrontato per i flussi in C++, ovviamente, non vale per il C.
iostream è la libreria fornita dal C++ per svolgere le operazioni di input e output sui flussi (Input/Output Stream), l'analoga di stdio.h. Per utilizzarla, sarà sufficiente includerla all'inizio del programma; per completezza, ma non ancora per chiarezza, si deve inoltre sapere che, in C++, dopo essere state raggruppate in librerie, le funzioni e gli altri elementi, possono essere ulteriormente raggruppati in spazi di nomi. Questo verrà spiegato meglio più avanti, ma intanto adoperiamo questo zucchero sintattico a nostro favore.
Includiamo iostream e utilizziamo lo spazio di nomi std:
#include <iostream> using namespace std;
Output
cout è l'oggetto che rappresenta il flusso di uscita: agendo su di esso e su un altro operando per mezzo dell'operatore <<, è possibile stampare su schermo. L'operatore << è stato sovraccaricato dai programmatori della libreria iostream, cioè è in grado di accettare più tipi di dati, pertanto è possibile utilizzarlo per stampare qualunque tipo, senza che dobbiamo preoccuparci di specificarlo: sarà sufficiente separare i vari elementi ripetendo l'operatore.
int i; double d; char c; cout << i << d << c << "Ciao";
Il seguente codice in C esegue esattamente la stessa operazione, ma è molto meno conciso e leggibile:
int i; double d; char c; printf("%d%f%cCiao", i, d, c);
Inoltre, possono essere utilizzati tutte le sequenze di escape presenti anche nel C. Per andare a capo si può utilizzare anche la funzione alternativa endl (end line):
cout << "Vado" << endl << "a capo";
Input
A coppia con l'oggetto cout troviamo il cin, che consente di leggere dati dal flusso di ingresso e di immetterli nelle variabili. cin deve essere utilizzato con l'operatore >>, anch'esso sovraccaricato:
int i; cin >> i;
Anche cin può leggere più dati (in realtà il merito è dell'operatore >>): nel flusso di ingresso, i dati separati da spazi o caratteri di ritorno di carrello (CR, Invio) vengono considerati come informazioni distinte.
Per esercizio, può essere utile riscrivere un paio dei programmi utilizzati fin'ora facendo uso di questi nuovi strumenti. Qualunque cosa non riguardi i flussi, come i tipi di variabili e i vari costrutti, viene ripresa dal C senza alcuna modifica. Segue un esempio che riassume in un breve programma i concetti esposti: lo si compili, si verifichi e si capisca il suo funzionamento.
#include <iostream> using namespace std; int main() { cout << "Inserire espressione del tipo" << endl; cout << " X O Y" << endl; cout << " dove X, Y = due operandi numerici" << endl; cout << " O = un operatore a scelta tra + * - /" << endl; cout << "Input? "; double x, y, r; char o; cin >> x >> o >> y; switch(o) { case '+': r = x + y; break; case '-': r = x - y; break; case '*': case 'x': r = x * y; break; case '/': case ':': r = x / y; break; default: cout << "Operando sconosciuto!" << endl; break; } cout << "Risultato: " << r << endl; return 0; }