1. Introduzione
A volte un ciclo si scrive facilmente: «finché i <= n — fai». Ma nella vita reale le condizioni di arresto possono essere nascoste nel corpo: stiamo cercando qualcosa, verifichiamo l'input, aspettiamo una «parola di stop», abbiamo trovato un errore o la risposta prima del previsto. In questi casi è comodo avere comandi espliciti di controllo del flusso: break per uscire e continue per saltare parte dell'iterazione. L'importante è usarli consapevolmente: sono come un coltello affilato in cucina — utili, ma è bene non farselo male.
Una piccola tabella-navigazione per il significato:
| Comando | Cosa fa | Dove agisce |
|---|---|---|
|
termina immediatamente il ciclo più vicino | solo uno (il più interno) ciclo |
|
salta il resto del corpo e passa all'iterazione successiva | anche qui solo nel ciclo corrente (più vicino) |
2. break: “trovato — usciamo”
break serve quando non vogliamo «far continuare» il ciclo fino alla fine, perché la risposta è già stata trovata o proseguire è inutile. Questo accade spesso nelle ricerche: il primo divisore di un numero, il primo valore adatto, il primo errore nell'input, il comando «esci» in un menu. È importante ricordare una regola semplice: break non salta dove vuole, termina solo il ciclo più vicino in cui si trova.
Esempio minimo: cerchiamo il primo divisore
Immaginiamo di cercare il più piccolo divisore di un numero n (diverso da 1). Appena lo troviamo — non ha senso controllare oltre.
#include <iostream>
int main() {
int n = 0;
std::cin >> n;
int firstDiv = 0;
for (int d = 2; d <= n; d = d + 1) {
if (n % d == 0) {
firstDiv = d;
break; // trovato — usciamo
}
}
std::cout << firstDiv << '\n'; // ad esempio: 15 -> 3
}
Qui break rende il codice più onesto: vogliamo davvero il «primo adatto» e non è necessario iterare fino a n.
break in while: leggiamo finché non diciamo stop
A volte è comodo tenere la condizione «dentro» e non nell'intestazione. Per esempio, leggiamo comandi finché non incontriamo 0.
#include <iostream>
int main() {
while (true) {
int x = 0;
std::cin >> x;
if (x == 0) {
break; // 0 è il comando di uscita
}
std::cout << "Read: " << x << '\n'; // Read: 5
}
}
Sì, qui while (true) sembra una «infinita», ma in realtà il ciclo è finito, perché al suo interno c'è un'uscita chiara tramite break. Questo è accettabile se l'uscita è davvero unica e la logica è trasparente.
3. continue: “saltiamo il superfluo, lavoriamo con il necessario”
continue è utile quando nel ciclo ci sono casi «spazzatura» che vogliamo scartare. Scenario tipico: filtraggio — stampare solo i dispari, sommare solo i positivi, ignorare numeri fuori intervallo. Ma continue ha una caratteristica: tutto ciò che è scritto sotto continue; nell'iterazione corrente non verrà eseguito. Questo è una causa comune del «perché il programma non fa nulla?» tra i principianti.
Esempio minimo: stampiamo solo i dispari
#include <iostream>
int main() {
for (int i = 1; i <= 10; i = i + 1) {
if (i % 2 == 0) {
continue; // saltiamo i pari
}
std::cout << i << '\n'; // 1 3 5 7 9
}
}
Il senso si legge quasi «in lingua»: «se è pari — salta; altrimenti stampa».
continue come protezione dall'input non adeguato
Finché non affrontiamo il complesso ripristino di std::cin dopo errori, considereremo l'input corretto ma potenzialmente «non adatto» (per esempio numeri negativi).
#include <iostream>
int main() {
int sum = 0;
for (int k = 0; k < 5; k = k + 1) {
int x = 0;
std::cin >> x;
if (x < 0) {
continue; // ignoriamo i negativi
}
sum = sum + x;
}
std::cout << sum << '\n';
}
Qui continue aiuta a non gonfiare il codice con grandi if/else: tagliamo subito il caso inutile.
Sfumatore: continue in for e while si comporta diversamente
A prima vista sembra che continue semplicemente «passi all'iterazione successiva». È vero, ma c'è una sottile differenza su cosa succede tra continue e la successiva verifica della condizione. Nel for dopo continue viene comunque eseguito il passo (i = i + 1), mentre nel while non c'è un passo separato, si passa subito a valutare la condizione. Questa sottigliezza a volte salva dai cicli infiniti… e a volte li crea.
Vediamo lo schema (semplificato, senza dettagli):
flowchart TD
A["for: corpo"] --> B{"continue?"}
B -- "sì" --> C["passo (i = i + 1)"]
B -- "no" --> D["fine corpo"]
D --> C
C --> E{"condizione?"}
E -- "sì" --> A
E -- "no" --> F["uscita"]
W["while: corpo"] --> X{"continue?"}
X -- "sì" --> Y{"condizione?"}
X -- "no" --> Z["fine corpo"]
Z --> Y
Y -- "sì" --> W
Y -- "no" --> Q["uscita"]
E ora la conclusione pratica: se il vostro «passo verso il termine» si trova sotto continue, quel passo potrebbe non essere eseguito e il ciclo potrebbe cominciare a somigliare a una macchina eterna.
4. Cicli annidati: “un ciclo dentro un altro” e perché servono
Un ciclo annidato è quando dentro il corpo di un ciclo vive un altro ciclo. Sembra una matrioska, ed è proprio così: il ciclo esterno governa i «passi grandi», quello interno le «ripetizioni piccole» per ogni passo grande. Serve per tabelle, griglie, enumerazione di coppie di valori, disegnare rettangoli con caratteri, lavoro con coordinate (riga/colonna). È importante abituarsi al fatto che il ciclo interno parte di nuovo ad ogni iterazione dell'esterno, quindi il numero di operazioni cresce velocemente — ed è normale, finché si capisce perché lo si fa.
Tabella di moltiplicazione 1..5
#include <iostream>
int main() {
for (int a = 1; a <= 5; a = a + 1) {
for (int b = 1; b <= 5; b = b + 1) {
std::cout << (a * b) << ' ';
}
std::cout << '\n';
}
}
Il ciclo esterno sono le righe (a), quello interno le colonne (b). È un modello comodo di «2D», anche se non abbiamo ancora studiato array 2D.
Disegniamo un rettangolo di #
Anche qui cicli annidati: esterno — righe, interno — quanti caratteri in una riga.
#include <iostream>
int main() {
int h = 3;
int w = 6;
for (int row = 0; row < h; row = row + 1) {
for (int col = 0; col < w; col = col + 1) {
std::cout << '#';
}
std::cout << '\n';
}
}
Esempi come questi allenano bene la comprensione dei confini: row < h significa esattamente h righe, col < w — esattamente w caratteri.
5. break nei cicli annidati: perché non esce “da tutto”
Qui comincia la sorpresa più comune: break esce solo dal ciclo più vicino. Se siete dentro il ciclo interno — break terminerà l'interno, mentre l'esterno continuerà a funzionare come se nulla fosse successo (non è cattivo, è semplicemente ignaro della vostra tragedia).
Cerchiamo la coppia a * b == target
#include <iostream>
int main() {
int target = 12;
bool found = false;
for (int a = 1; a <= 10; a = a + 1) {
for (int b = 1; b <= 10; b = b + 1) {
if (a * b == target) {
std::cout << a << " * " << b << " = " << target << '\n'; // 2 * 6 = 12
found = true;
break; // usciamo solo dal ciclo interno
}
}
if (found) {
break; // ora usciamo anche da quello esterno
}
}
}
Qui il flag found è un modo semplice per «comunicare all'esterno» che il risultato è stato trovato. Non usiamo costrutti complessi: solo bool, if e due break.
6. Quando break/continue aiutano e quando ostacolano
È molto facile cominciare a «fuggire» tramite break e continue da ogni complessità, e finire con codice che si legge come una missione: «se qui salta, poi salta, poi esci, ma solo se non sei già uscito prima». Buono stile — usare questi operatori come spezie: un poco rende il piatto più gustoso, ma se si versa tutto il vasetto — non è più cibo, è un esperimento di sopravvivenza.
Parliamo di due casi tipici.
continue come filtro pulito — è positivo
Se all'inizio dell'iterazione avete una «guardia» che scarta i casi inutili, allora continue rende il codice più corto e pulito. Abbiamo già visto l'esempio con i numeri dispari — si legge facilmente.
Molti continue e break mescolati — di solito è male
Confrontiamo due stili. Il primo — «salti multipli»:
#include <iostream>
int main() {
for (int i = 1; i <= 10; i = i + 1) {
if (i == 7) break;
if (i % 2 == 0) continue;
if (i == 3) continue;
std::cout << i << '\n';
}
}
Non è ancora terribile, ma bisogna tenere in testa troppe eccezioni.
Secondo stile — «una condizione per stampare» (spesso più leggibile):
#include <iostream>
int main() {
for (int i = 1; i <= 10; i = i + 1) {
bool shouldPrint = (i % 2 != 0) && (i != 3) && (i != 7);
if (shouldPrint) {
std::cout << i << '\n'; // 1 5 9
}
if (i == 7) {
break; // se è davvero necessario uscire prima
}
}
}
Lo svantaggio è che l'espressione diventa più lunga. Ma il vantaggio è che la logica «quando stampare» è raccolta in un punto e non nascosta dietro salti.
7. Menu da console: break/continue e cicli annidati
Supportiamo l'idea di una «piccola applicazione» iniziata con il do-while e un menu semplice. Non trasformeremo main in un mostro enorme (anche se ci prova), ma faremo aggiunte con cura: aggiungeremo il comando per stampare la tabella e il comando per cercare il divisore. Sono proprio quei posti dove i cicli annidati e break stanno bene.
Struttura del menu e comando “tabella 1..N”
#include <iostream>
int main() {
int cmd = -1;
do {
std::cout << "1 - Multiplication table\n";
std::cout << "2 - First divisor\n";
std::cout << "0 - Exit\n";
std::cin >> cmd;
if (cmd == 1) {
int n = 0;
std::cin >> n;
for (int a = 1; a <= n; a = a + 1) {
for (int b = 1; b <= n; b = b + 1) {
std::cout << (a * b) << ' ';
}
std::cout << '\n';
}
}
} while (cmd != 0);
}
Nota: non usiamo switch (non l'abbiamo ancora visto), quindi il menu è costruito con if. Sì, è un po' più lungo, ma è onesto rispetto alle conoscenze attuali.
Comando “primo divisore”: break + controllo con continue
Ora aggiungiamo il secondo comando e mostriamo come continue possa «filtrare» uno scenario non valido. Per esempio, ha senso cercare un divisore per n >= 2.
#include <iostream>
int main() {
int cmd = -1;
do {
std::cout << "1 - Multiplication table\n";
std::cout << "2 - First divisor\n";
std::cout << "0 - Exit\n";
std::cin >> cmd;
if (cmd == 2) {
int n = 0;
std::cin >> n;
if (n < 2) {
std::cout << "Need n >= 2\n";
continue; // torniamo al menu, non facciamo lavoro inutile
}
int firstDiv = 0;
for (int d = 2; d <= n; d = d + 1) {
if (n % d == 0) {
firstDiv = d;
break;
}
}
std::cout << firstDiv << '\n';
}
} while (cmd != 0);
}
Notate una cosa importante: qui continue si riferisce al ciclo do-while, cioè «salta il resto del corpo del menu e mostralo di nuovo». Ha senso: con un n non valido vogliamo tornare al menu.
8. Errori tipici: break/continue e cicli annidati
Errore №1: aspettarsi che break “esca da tutti i cicli in una volta”.
I principianti spesso pensano che break sia un grande pulsante rosso per fermare tutto. In realtà termina solo il ciclo più vicino. Se siete dentro annidamenti, il ciclo esterno continuerà. Si risolve con un flag (per esempio, bool found) o ristrutturando la logica in modo che il ciclo esterno veda la condizione di termine.
Errore №2: continue è posizionato sopra il “passo verso la fine”, e il ciclo diventa infinito.
È particolarmente facile farlo in un while, dove il passo spesso è scritto alla fine del corpo. Se in una certa condizione fate continue; mentre la modifica della variabile è sotto, quella modifica non verrà eseguita, la condizione non cambierà e il ciclo si ripeterà all'infinito. Prevenzione migliore: posizionare il «passo» prima dei possibili continue, oppure assicurarsi che continue non eviti cambiamenti di stato importanti.
Errore №3: il codice dopo continue “all'improvviso non viene eseguito”, anche se sembra che dovrebbe.
Non è un bug del compilatore né «C++ rotto», è la semantica letterale di continue: saltare il resto del corpo. Perciò è pericoloso mettere logica importante sotto continue e poi aspettarsi che funzioni. Se in un'iterazione dovete fare sia filtraggio sia azioni obbligatorie, è meglio separarle: le azioni obbligatorie prima del continue, il resto dopo.
Errore №4: i cicli annidati sono scritti correttamente, ma i limiti sono scambiati.
Classico: volevate row < h e col < w, ma per errore avete messo row < w e col < h. Il codice compila, ma il risultato è «storto», e il debug è spiacevole perché formalmente tutto sembra ragionevole. Aiuta l'abitudine di nominare le variabili per significato (row, col) e provare un piccolo esempio (per esempio, h = 2, w = 3) e aspettarsi una forma concreta.
Errore №5: troppe break/continue trasformano il ciclo in uno scenario “saltellante”.
break e continue sono comodi, ma ognuno aggiunge un punto di transizione non lineare. Quando questi punti diventano molti, leggere il codice diventa difficile: bisogna simulare mentalmente l'esecuzione. Se sentite che in un ciclo ci sono già tre «buchi nella realtà» tramite continue, spesso è meglio riscrivere la condizione in modo più lineare: fare una singola if chiara per l'azione, o estrarre parte della verifica in una variabile bool con un nome esplicativo.
GO TO FULL VERSION