Stringhe in C++
- Le stringhe in C++ sono rappresentate dall'oggetto
string
della libreria standard. - Le stringhe possono essere inizializzate in diversi modi, tra cui l'inizializzazione di default, per copia e diretta.
- Le operazioni comuni sulle stringhe includono la lettura, la scrittura, il controllo della lunghezza e l'accesso ai singoli caratteri.
- Le stringhe in C++ gestiscono automaticamente la memoria, rendendole più sicure e facili da usare rispetto alle stringhe in C.
Il tipo string
Una stringa è una sequenza di caratteri di lunghezza variabile.
In C++ esiste un oggetto string
che rappresenta una stringa. Questo tipo è definito nella libreria standard C++. La libreria standard C++ fornisce molti tipi e funzioni che estendono il linguaggio di base. Questi tipi e funzioni sono definiti in una serie di file header.
Per utilizzare il tipo string
, dobbiamo includere l'header string
. Poiché fa parte della libreria, string
è definita nel namespace std
. I nostri esempi presuppongono che abbiamo incluso il seguente codice:
#include <string>
using std::string;
Questa sezione descrive le operazioni più comuni sulle stringhe; le prossime lezioni tratteranno operazioni aggiuntive.
Il tipo string
del C++ non è la stessa cosa delle stringhe in C
Chi viene dal linguaggio C deve fare attenzione a non confondere il tipo string
del C++ con le stringhe in C.
Le stringhe in C sono rappresentate come array di caratteri terminati da un carattere null ('\0'
). Queste stringhe richiedono una gestione manuale della memoria e possono essere soggette a errori come il buffer overflow. Al contrario, il tipo string
del C++ gestisce automaticamente la memoria e offre molte funzionalità utili per lavorare con le stringhe in modo sicuro ed efficiente.
Dato che il C++ è un'estensione del linguaggio C, si possono comunque adoperare le stringhe C anche all'interno di programmi C++. Tuttavia, è consigliabile utilizzare il tipo string
del C++ per la maggior parte delle applicazioni moderne, a meno che non si abbia una ragione specifica per utilizzare le stringhe C.
Inoltre, spesso chi proviene dal linguaggio C è spesso restio ad adoperare oggetti definiti nella libreria standard C++ pensando, erroneamente, che si tratti di qualcosa di inefficiente. In realtà, gli oggetti della libreria standard C++ sono progettati per essere efficienti e facili da usare.
Infatti, oltre a specificare le operazioni fornite dai tipi della libreria, lo standard del C++ impone anche requisiti di efficienza agli implementatori. Di conseguenza, i tipi della libreria sono abbastanza efficienti per l'uso generale.
Definire e Inizializzare Stringhe
Ogni classe definisce come possono essere inizializzati gli oggetti del suo tipo.
Una classe può definire molti modi diversi per inizializzare oggetti del suo tipo. Ogni modo deve essere distinto dagli altri sia per il numero di inizializzatori che forniamo, sia per i tipi di tali inizializzatori. La Tabella 3.1 elenca i modi più comuni per inizializzare le string
.
Ecco alcuni esempi:
// Inizializzazione di default: s1 è una stringa vuota
string s1;
// Inizializzazione per copia: s2 è una copia di s1
string s2 = s1;
// Inizializzazione per copia: s3 è una copia del letterale stringa
string s3 = "ciao";
// Inizializzazione diretta: s4 contiene 10 copie del carattere 'c'
string s4(10, 'c'); // s4 è cccccccccc
Come si può osservare, possiamo inizializzare una string
in modo predefinito, ossia creando una stringa vuota senza caratteri.
Quando forniamo un letterale stringa come inizializzatore, i caratteri di quel letterale, fino al carattere nullo \0
alla fine del letterale ma escluso, vengono copiati nella stringa appena creata.
Quando forniamo un conteggio e un carattere, la stringa contiene quel numero di copie del carattere dato.
Inizializzazione Diretta e per Copia
Nella lezione sulle variabili abbiamo visto che il C++ ha diverse forme di inizializzazione.
Usando le string
come esempio, possiamo iniziare a capire come queste forme differiscono l'una dall'altra. Quando inizializziamo una variabile usando l'operatore di assegnamento =
, stiamo chiedendo al compilatore di inizializzare per copia l'oggetto copiando l'inizializzatore sul lato destro nell'oggetto che viene creato. Altrimenti, quando omettiamo =
, usiamo l'inizializzazione diretta.
Quando abbiamo un singolo inizializzatore, possiamo usare sia la forma diretta che quella di copia dell'inizializzazione. Quando inizializziamo una variabile da più di un valore, come nell'inizializzazione di s4
sopra, dobbiamo usare la forma diretta di inizializzazione:
// inizializzazione per copia
string s5 = "ciao";
// inizializzazione diretta
string s6("ciao");
// i
string s7(10, 'c'); // inizializzazione diretta; s7 è cccccccccc
Quando vogliamo usare diversi valori, possiamo usare indirettamente la forma di copia dell'inizializzazione creando esplicitamente un oggetto (temporaneo) da copiare:
// inizializzazione per copia; s8 è cccccccccc
string s8 = string(10, 'c');
L'inizializzatore di s8
, ossia string(10, 'c')
, crea una stringa della dimensione e del valore carattere dati e poi copia quel valore in s8
. È come se avessimo scritto:
string temp(10, 'c'); // temp è cccccccccc
string s8 = temp; // copia temp in s8
Sebbene il codice usato per inizializzare s8
sia legale, è meno leggibile e non offre alcun vantaggio compensativo rispetto al modo in cui abbiamo inizializzato s7
.
Ricapitolando, quindi, i modi per inizializzare una string
sono:
-
Inizializzazione di Default:
string s1;
In questo modo otteniamo una stringa vuota.
-
Inizializzazione per Copia:
string s2(s1); string s2 = s1;
Le due inizializzazioni di sopra sono equivalenti e copiano il valore di
s1
ins2
. -
Inizializzazione per Copia da un Letterale Stringa:
string s3("ciao"); string s3 = "ciao";
Le due inizializzazioni di sopra sono equivalenti e copiano i caratteri del letterale stringa in
s3
. -
Inizializzazione diretta:
string s4(10, 'c');
In questo modo otteniamo una stringa che contiene 10 copie del carattere
'c'
.
Operazioni sulle stringhe
Oltre a definire come gli oggetti vengono creati e inizializzati, una classe definisce anche le operazioni che gli oggetti del tipo classe possono eseguire.
Una classe può definire operazioni che vengono chiamate per nome. Una classe può anche definire cosa significano vari simboli operatore, come <<
o +
, quando applicati a oggetti del tipo della classe. La che segue elenca le operazioni più comuni sulle string:
Operazione | Descrizione |
---|---|
os << s |
Scrive s sul flusso di output os . Restituisce os . |
is >> s |
Legge una stringa separata da spazi bianchi da is in s . Restituisce is . |
getline(is, s) |
Legge una riga di input da is in s . Restituisce is . |
s.empty() |
Restituisce true se s è vuota; altrimenti restituisce false. |
s.size() |
Restituisce il numero di caratteri in s . |
s[n] |
Restituisce un riferimento al char alla posizione n in s ; le posizioni iniziano da 0. |
s1 + s2 |
Restituisce una string che è la concatenazione di s1 e s2 . |
s1 = s2 |
Sostituisce i caratteri in s1 con una copia di s2 . |
s1 == s2 |
Le stringhe s1 e s2 sono uguali se contengono gli stessi caratteri. |
s1 != s2 |
L'uguaglianza è sensibile alle maiuscole/minuscole. |
< , <= , > , >= |
I confronti sono sensibili alle maiuscole/minuscole e usano l'ordinamento del dizionario. |
Lettura e Scrittura di stringhe
Come abbiamo visto nelle lezioni precedenti, usiamo la libreria iostream
per leggere e scrivere valori di tipi built-in come int
, double
, e così via. Usiamo gli stessi operatori I/O per leggere e scrivere string
:
#include <iostream>
#include <string>
using namespace std;
int main()
{
// stringa vuota
string s;
// legge una stringa separata da spazi bianchi in s
cin >> s;
// scrive s sull'output
cout << s << endl;
return 0;
}
Questo programma inizia definendo una stringa vuota chiamata s
. La riga successiva legge l'input standard, memorizzando ciò che viene letto in s
. L'operatore di input per string legge e scarta tutti gli spazi bianchi iniziali (ad esempio, spazi, nuove righe, tabulazioni). Quindi legge i caratteri fino a quando non incontra il successivo carattere di spazio bianco.
Quindi, se l'input di questo programma è:
Ciao Mondo!
(nota gli spazi iniziali e finali), l'output sarà Ciao
senza spazi extra.
Come le operazioni di input e output sui tipi built-in, gli operatori string
restituiscono il loro operando sinistro come risultato. Pertanto, possiamo concatenare più letture o scritture:
string s1, s2;
// legge il primo input in s1, il secondo in s2
cin >> s1 >> s2;
// scrive entrambe le stringhe
cout << s1 << s2 << endl;
Se diamo a questa versione del programma lo stesso input:
Ciao Mondo!
il nostro output sarebbe "CiaoMondo!
"
Lettura di un Numero Sconosciuto di stringhe
Nella lezione sul flusso di controllo abbiamo scritto un programma che leggeva un numero sconosciuto di valori int
. Possiamo scrivere un programma simile che legge string
invece:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string parola;
// legge fino alla fine del file
while (cin >> parola)
// scrive ogni parola seguita da una nuova riga
cout << parola << endl;
return 0;
}
In questo programma, leggiamo in una string
, non in un int
. Altrimenti, la condizione while
si esegue in modo simile a quella del nostro programma precedente. La condizione testa il flusso dopo che la lettura è completa. Se il flusso è valido, ossia non ha raggiunto la fine del file o incontrato un input non valido, allora il corpo del while viene eseguito. Il corpo stampa il valore che abbiamo letto sull'output standard. Una volta raggiunta la fine del file (o un input non valido), usciamo dal while
.
Usare getline
per Leggere un'Intera Riga
A volte non vogliamo ignorare gli spazi bianchi nel nostro input. In tali casi, possiamo usare la funzione getline
invece dell'operatore >>
. La funzione getline
prende un flusso di input e una string
. Questa funzione legge il flusso dato fino alla prima nuova riga inclusa e memorizza ciò che ha letto, esclusa il carattere di nuova riga, nel suo argomento string
. Dopo che getline
vede una nuova riga, anche se è il primo carattere nell'input, smette di leggere e ritorna. Se il primo carattere nell'input è una nuova riga, allora la stringa risultante è la stringa vuota.
Come l'operatore di input, getline
restituisce il suo argomento istream
. Di conseguenza, possiamo usare getline
come condizione proprio come possiamo usare l'operatore di input come condizione. Ad esempio, possiamo riscrivere il programma precedente che scriveva una parola per riga per scrivere invece una riga alla volta:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string riga;
// legge l'input una riga alla volta fino alla fine del file
while (getline(cin, riga))
cout << riga << endl;
return 0;
}
Poiché riga
non contiene un carattere di nuova riga, dobbiamo aggiungerlo direttamente. Come al solito, usiamo endl
per terminare la riga corrente e svuotare il buffer.
Il carattere di nuova riga che causa il ritorno di getline
viene scartato; esso non viene memorizzato nella string
.
Le Operazioni empty
e size
di string
La funzione empty
fa ciò che ci si aspetterebbe: restituisce un bool
che indica se la stringa è vuota. empty
è una funzione membro di string
. Per chiamare questa funzione, usiamo l'operatore punto per specificare l'oggetto su cui vogliamo eseguire la funzione empty
.
Possiamo rivedere il programma precedente per stampare solo le righe che non sono vuote:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string riga;
// legge l'input una riga alla volta e scarta le righe vuote
while (getline(cin, riga))
if (!riga.empty())
cout << riga << endl;
return 0;
}
La condizione usa l'operatore NOT logico (l'operatore !
). Questo operatore restituisce l'inverso del valore bool
del suo operando. In questo caso, la condizione è vera se riga
non è vuota.
Il membro size
restituisce la lunghezza di una string
(cioè, il numero di caratteri in essa). Possiamo usare size
per stampare solo le righe più lunghe di 80 caratteri:
#include <iostream>
#include <string>
using namespace std;
int main()
{
string riga;
// legge l'input una riga alla volta e
// stampa le righe più lunghe di 80 caratteri
while (getline(cin, riga))
if (riga.size() > 80)
cout << riga << endl;
return 0;
}
Il Tipo string::size_type
Potrebbe sembrare logico aspettarsi che size
restituisca un int
o addirittura un unsigned
. Invece, size restituisce un valore string::size_type
. Questo tipo richiede un po' di spiegazione.
La classe string
, e la maggior parte degli altri tipi della libreria, definisce diversi tipi accessori. Questi tipi accessori rendono possibile usare i tipi della libreria in modo indipendente dalla macchina. Il tipo size_type
è uno di questi tipi accessori. Per usare il size_type
definito da string
, usiamo l'operatore di scope per dire che il nome size_type
è definito nella classe string
.
Sebbene non conosciamo il tipo preciso di string::size_type
, sappiamo che è un tipo unsigned
abbastanza grande da contenere la dimensione di qualsiasi string
. Qualsiasi variabile utilizzata per memorizzare il risultato dell'operazione size
di string
dovrebbe essere di tipo string::size_type
.
Vero è anche che può essere tedioso digitare string::size_type
ogni volta. Ma a partire dallo standard C++11 possiamo chiedere al compilatore di fornire il tipo appropriato usando auto
o decltype
. Ad esempio, possiamo scrivere:
// lunghezza ha tipo string::size_type
auto lunghezza = riga.size();
Poiché size
restituisce un tipo unsigned
, è essenziale ricordare che le espressioni che mescolano dati signed
e unsigned
possono avere risultati sorprendenti. Ad esempio, se n
è un int
che contiene un valore negativo, allora lunghezza < n
verrà quasi sicuramente valutato come true
. Produce true
perché il valore negativo in n
verrà convertito in un grande valore unsigned
.
Puoi evitare problemi dovuti alla conversione tra unsigned
e int
non usando int
in espressioni che usano size()
.
Confrontare stringhe
La classe string
definisce diversi operatori che confrontano le stringhe. Questi operatori funzionano confrontando i caratteri delle stringhe. I confronti sono sensibili alle maiuscole/minuscole, ossia le versioni maiuscole e minuscole di una lettera sono considerati caratteri diversi.
Gli operatori di uguaglianza (==
e !=
) verificano se due stringhe sono uguali o diverse, rispettivamente. Due stringhe sono uguali se hanno la stessa lunghezza e contengono gli stessi caratteri. Gli operatori relazionali <
, <=
, >
, >=
verificano se una stringa è minore di, minore o uguale a, maggiore di, o maggiore o uguale a un'altra. Questi operatori usano la stessa strategia di un dizionario (sensibile alle maiuscole/minuscole):
- Se due stringhe hanno lunghezze diverse e se ogni carattere nella stringa più corta è uguale al carattere corrispondente della stringa più lunga, allora la stringa più corta è minore di quella più lunga.
- Se qualsiasi carattere in posizioni corrispondenti nelle due stringhe differisce, allora il risultato del confronto di stringhe è il risultato del confronto del primo carattere in cui le stringhe differiscono.
Questo tipo di confronto è chiamato confronto lessicografico.
Come esempio, consideriamo le seguenti string
:
string str = "Ciao";
string frase = "Ciao Mondo";
string saluto = "Salve";
Usando la regola 1, vediamo che str
è minore di frase
. Applicando la regola 2, vediamo che saluto
è maggiore sia di str
che di frase
.
Assegnamento di stringhe
In generale, i tipi della libreria si sforzano di rendere l'uso di un tipo della libreria facile quanto l'uso di un tipo built-in. A tal fine, la maggior parte dei tipi della libreria supporta l'assegnamento. Nel caso delle string
, possiamo assegnare un oggetto string
a un altro:
// st1 è cccccccccc; st2 è una stringa vuota
string st1(10, 'c'), st2;
// assegnamento: sostituisce il contenuto di st1 con una copia di st2
// sia st1 che st2 sono ora la stringa vuota
st1 = st2;
Concatenare stringhe
La concatenazione di due string
produce una nuova string
che è la concatenazione dell'operando sinistro seguito dall'operando destro. Cioè, quando usiamo l'operatore più (+
) sulle string
, il risultato è una nuova string
i cui caratteri sono una copia di quelli nell'operando sinistro seguiti da quelli dell'operando destro. L'operatore di assegnamento composto (+=
) aggiunge l'operando destro alla string
sinistra:
string s1 = "ciao, ";
string s2 = "mondo\n";
// s3 è ciao, mondo\n
string s3 = s1 + s2;
// equivalente a s1 = s1 + s2
s1 += s2;
Concatenare stringhe e letterali
Come abbiamo visto nelle lezioni precedenti, possiamo usare un tipo dove è atteso un altro tipo se esiste una conversione dal tipo dato al tipo atteso.
La libreria string
ci permette di convertire sia i letterali carattere che i letterali stringa carattere in string
. Poiché possiamo usare questi letterali dove è attesa una string
, possiamo riscrivere il programma precedente come segue:
// nessuna punteggiatura in s1 o s2
string s1 = "ciao", s2 = "mondo";
// s3 è ciao, mondo\n
string s3 = s1 + ", " + s2 + '\n';
Quando mescoliamo string
e letterali string
o carattere, almeno un operando di ogni operatore +
deve essere di tipo string
:
// OK: aggiunta di una string e un letterale
string s4 = s1 + ", ";
// ERRORE: nessun operando string
string s5 = "ciao" + ", ";
// OK: ogni + ha un operando string
string s6 = s1 + ", " + "mondo";
// ERRORE: non si possono aggiungere letterali string
string s7 = "ciao" + ", " + s2;
Le inizializzazioni di s4
e s5
coinvolgono solo una singola operazione ciascuna, quindi è facile vedere se l'inizializzazione è legale. L'inizializzazione di s6
può sembrare sorprendente, ma funziona nello stesso modo di quando concateniamo espressioni di input o output. Questa inizializzazione raggruppa gli operandi come mostrato di seguito:
string s6 = (s1 + ", ") + "mondo";
La sottoespressione s1 + ", "
restituisce una string
, che forma l'operando sinistro del secondo operatore +
. È come se avessimo scritto
// OK: + ha un operando string
string tmp = s1 + ", ";
// OK: + ha un operando string
s6 = tmp + "mondo";
D'altra parte, l'inizializzazione di s7
è illegale, cosa che possiamo vedere se mettiamo tra parentesi l'espressione:
// ERRORE: non si possono aggiungere letterali string
string s7 = ("ciao" + ", ") + s2;
Ora dovrebbe essere facile vedere che la prima sottoespressione aggiunge due letterali string
. Non c'è modo di farlo, e quindi l'istruzione è errata.
Per ragioni storiche, e per compatibilità con il linguaggio C, i letterali string
non sono string
della libreria standard. È importante ricordare che questi tipi differiscono quando si usano letterali string
e string
della libreria.
Esercizi
-
Scrivi un programma per leggere l'input standard una riga alla volta. Modifica il tuo programma per leggere una parola alla volta.
Soluzione:
#include <iostream> #include <string> using namespace std; int main() { string riga; // legge l'input una riga alla volta fino alla fine del file while (getline(cin, riga)) // stampa la riga letta cout << riga << endl; return 0; }
Modifica per leggere una parola alla volta:
#include <iostream> #include <string> using namespace std; int main() { string parola; // legge l'input una parola alla volta fino alla fine del file while (cin >> parola) // stampa la parola letta cout << parola << endl; return 0; }
-
Scrivi un programma per leggere due
string
e riportare se lestring
sono uguali. In caso contrario, riporta quale delle due è più grande. Inoltre riporta se lestring
hanno la stessa lunghezza, e in caso contrario, riporta quale è più lunga.#include <iostream> #include <string> using namespace std; int main() { string s1, s2; // legge due string dall'input cout << "Inserisci la prima string: "; getline(cin, s1); cout << "Inserisci la seconda string: "; getline(cin, s2); // confronta le string if (s1 == s2) cout << "Le string sono uguali." << endl; else if (s1 > s2) cout << "La prima string è più grande." << endl; else cout << "La seconda string è più grande." << endl; // confronta la lunghezza delle string if (s1.length() == s2.length()) cout << "Le string hanno la stessa lunghezza." << endl; else if (s1.length() > s2.length()) cout << "La prima string è più lunga." << endl; else cout << "La seconda string è più lunga." << endl; return 0; }
-
Scrivi un programma per leggere
string
dall'input standard, concatenando ciò che viene letto in un'unica grandestring
. Stampa lastring
concatenata. Successivamente, modifica il programma per separare le string di input adiacenti con uno spazio.Soluzione:
#include <iostream> #include <string> using namespace std; int main() { string parola; string risultato; // legge l'input una parola alla volta fino alla fine del file while (cin >> parola) // concatena la parola letta al risultato risultato += parola; // stampa la string concatenata cout << risultato << endl; return 0; }
Modifica per separare le string di input adiacenti con uno spazio:
#include <iostream> #include <string> using namespace std; int main() { string parola; string risultato; // legge l'input una parola alla volta fino alla fine del file while (cin >> parola) { if (!risultato.empty()) risultato += " "; // aggiunge uno spazio se non è la prima parola risultato += parola; // concatena la parola letta al risultato } // stampa la string concatenata cout << risultato << endl; return 0; }
Gestire i Caratteri in una stringa
Spesso abbiamo bisogno di gestire i singoli caratteri in una string
. Potremmo voler verificare se una string
contiene spazi bianchi, o modificare i caratteri in minuscolo, o vedere se un dato carattere è presente, e così via.
Una parte di questo tipo di elaborazione implica come otteniamo accesso ai caratteri stessi. A volte dobbiamo elaborare ogni carattere. Altre volte dobbiamo elaborare solo un carattere specifico, oppure possiamo smettere di elaborare una volta soddisfatta una certa condizione. Si scopre che il modo migliore per gestire questi casi implica diverse funzionalità del linguaggio e della libreria.
L'altra parte dell'elaborazione dei caratteri consiste nel conoscere e/o modificare le caratteristiche di un carattere. Questa parte del lavoro è gestita da un insieme di funzioni della libreria, descritte nella tabella che segue. Queste funzioni sono definite nell'header cctype
.
Funzione | Descrizione |
---|---|
isalnum(c) |
true se c è una lettera o una cifra. |
isalpha(c) |
true se c è una lettera. |
iscntrl(c) |
true se c è un carattere di controllo. |
isdigit(c) |
true se c è una cifra. |
isgraph(c) |
true se c non è uno spazio ma è stampabile. |
islower(c) |
true se c è una lettera minuscola. |
isprint(c) |
true se c è un carattere stampabile (cioè, uno spazio o un carattere che ha una rappresentazione visibile). |
ispunct(c) |
true se c è un carattere di punteggiatura (cioè, un carattere che non è un controllo, una cifra, una lettera, o uno spazio bianco stampabile). |
isspace(c) |
true se c è uno spazio bianco (cioè, uno spazio, tab, tab verticale, ritorno, nuova riga, o avanzamento modulo). |
isupper(c) |
true se c è una lettera maiuscola. |
isxdigit(c) |
true se c è una cifra esadecimale. |
tolower(c) |
Se c è una lettera maiuscola, restituisce il suo equivalente minuscolo; altrimenti restituisce c invariato. |
toupper(c) |
Se c è una lettera minuscola, restituisce il suo equivalente maiuscolo; altrimenti restituisce c invariato. |
Utilizzare sempre gli Header C++ rispetto agli Header C
Oltre alle funzionalità definite specificamente per C++, la libreria C++ incorpora la libreria C.
Gli header in C hanno nomi della forma nome.h
. Le versioni C++ di questi header sono chiamate cnome
, ossia rimuovono il suffisso .h
e precedono il nome
con la lettera c
. La c
indica che l'header fa parte della libreria C.
Quindi, cctype
ha lo stesso contenuto di ctype.h
, ma in una forma appropriata per i programmi C++. In particolare, i nomi definiti negli header cnome
sono definiti all'interno del namespace std
, mentre quelli definiti nelle versioni .h
non lo sono.
Normalmente, i programmi C++ dovrebbero usare le versioni cnome
degli header e non le versioni nome.h
. In questo modo i nomi della libreria standard si trovano costantemente nel namespace std
. L'uso degli header .h
mette l'onere sul programmatore di ricordare quali nomi della libreria sono ereditati da C e quali sono unici per C++.
Elaborare Ogni Carattere usando un Range-Based for
Se vogliamo fare qualcosa a ogni carattere in una string
, di gran lunga l'approccio migliore è usare un'istruzione introdotta dallo standard C++11: l'istruzione range for
. Questa istruzione itera attraverso gli elementi in una data sequenza ed esegue qualche operazione su ogni valore in quella sequenza. La forma sintattica è
for (dichiarazione : espressione)
istruzione
dove espressione
è un oggetto di un tipo che rappresenta una sequenza, e dichiarazione
definisce la variabile che useremo per accedere agli elementi sottostanti nella sequenza. Su ogni iterazione, la variabile in dichiarazione
viene inizializzata dal valore dell'elemento successivo in espressione
.
Una string rappresenta una sequenza di caratteri, quindi possiamo usare una string come espressione
in un range for
. Come esempio semplice, possiamo usare un range for
per stampare ogni carattere da una string
sulla propria riga di output:
string str("Ciao, Mondo!");
// stampa i caratteri in str un carattere per riga
for (auto c : str) // per ogni char in str
// stampa il carattere corrente seguito da una nuova riga
cout << c << endl;
Il ciclo for
associa la variabile c
con str
. Definiamo la variabile di controllo del ciclo allo stesso modo in cui facciamo con qualsiasi altra variabile. In questo caso, usiamo auto
per lasciare che il compilatore determini il tipo di c
, che in questo caso sarà char
. Su ogni iterazione, il carattere successivo in str
verrà copiato in c
. Quindi, possiamo leggere questo ciclo come se dicesse, "Per ogni carattere c
nella string str
," fai qualcosa. Il "qualcosa" in questo caso è stampare il carattere seguito da una nuova riga.
Come esempio un po' più complicato, useremo un range for
e la funzione ispunct
per contare il numero di caratteri di punteggiatura in una string:
#include <iostream>
#include <string>
#include <cctype> // per ispunct
using namespace std;
int main()
{
string s("Ciao Mondo!!!");
// conteggio_punt ha lo stesso tipo restituito da s.size;
decltype(s.size()) conteggio_punt = 0;
// conta il numero di caratteri di punteggiatura in s
for (auto c : s) // per ogni char in s
if (ispunct(c)) // se il carattere è punteggiatura
++conteggio_punt; // incrementa il contatore di punteggiatura
// stampa il risultato
cout << conteggio_punt
<< " caratteri di punteggiatura in " << s << endl;
}
L'output di questo programma è
3 caratteri di punteggiatura in Ciao Mondo!!!
Qui usiamo decltype
per dichiarare il nostro contatore, conteggio_punt
. Il suo tipo è il tipo restituito dalla chiamata s.size()
, che è string::size_type
. Usiamo un range for
per elaborare ogni carattere nella string
. Questa volta verifichiamo se ogni carattere è punteggiatura. Se lo è, usiamo l'operatore di incremento per aggiungere 1 al contatore. Quando il range for
è completo, stampiamo il risultato.
Usare un Range for
per Modificare i Caratteri in una stringa
Se vogliamo modificare il valore dei caratteri in una string, dobbiamo definire la variabile del ciclo come un tipo di riferimento. Ricorda che un riferimento è solo un altro nome per un dato oggetto. Quando usiamo un riferimento come variabile di controllo, quella variabile è legata a ogni elemento nella sequenza a turno. Usando il riferimento, possiamo modificare il carattere a cui il riferimento è legato.
Supponiamo che invece di contare la punteggiatura, volessimo convertire una string
in tutte lettere maiuscole. Per farlo possiamo usare la funzione della libreria toupper
, che prende un carattere e restituisce la versione maiuscola di quel carattere. Per convertire l'intera string
dobbiamo chiamare toupper
su ogni carattere e rimettere il risultato in quel carattere:
#include <iostream>
#include <string>
#include <cctype> // per toupper
using namespace std;
int main()
{
string s("Ciao Mondo!!!");
// converte s in maiuscolo
for (auto &c : s) // per ogni char in s (nota: c è un riferimento)
// c è un riferimento, quindi l'assegnamento cambia il char in s
c = toupper(c);
cout << s << endl;
}
L'output di questo codice è
CIAO MONDO!!!
Su ogni iterazione, c
si riferisce al carattere successivo in s
. Quando assegniamo a c
, stiamo modificando il carattere sottostante in s
. Quindi, quando eseguiamo
// c è un riferimento, quindi l'assegnamento cambia il char in s
c = toupper(c);
stiamo modificando il valore del carattere a cui c
è legato. Quando questo ciclo è completo, tutti i caratteri in s
saranno maiuscoli.
Accedere ai Singoli Caratteri in una stringa
Un range for
funziona bene quando dobbiamo elaborare ogni carattere. Tuttavia, a volte dobbiamo accedere solo a un singolo carattere o accedere ai caratteri fino a quando non viene raggiunta una certa condizione. Ad esempio, potremmo voler mettere in maiuscolo solo il primo carattere o solo la prima parola in una string
.
Ci sono due modi per accedere ai singoli caratteri in una string
: possiamo usare un pedice o un iteratore. Avremo più da dire sugli iteratori nelle prossime lezioni.
L'operatore di pedice (l'operatore []
) prende un valore string::size_type
che indica la posizione del carattere a cui vogliamo accedere. L'operatore restituisce un riferimento al carattere nella posizione data.
I pedici per le string
iniziano da zero; se s
è una string
con almeno due caratteri, allora s[0]
è il primo carattere, s[1]
è il secondo, e l'ultimo carattere è in s[s.size() - 1]
.
I valori che usiamo per indicizzare una string
devono essere compresi tra 0 e il valore restituito da size() - 1
, ossia la lunghezza della stringa meno 1. Il risultato dell'uso di un indice al di fuori di questo intervallo è indefinito. Per implicazione, l'indicizzazione di una string
vuota è indefinita.
Il valore nel pedice è chiamato "pedice" o "indice". L'indice che forniamo può essere qualsiasi espressione che produce un valore integrale. Tuttavia, se il nostro indice ha un tipo signed
, il suo valore verrà convertito nel tipo unsigned
che string::size_type
rappresenta.
L'esempio seguente usa l'operatore di pedice per stampare il primo carattere in una string:
// Si assicura che la string non sia vuota
if (!s.empty())
// stampa il primo carattere in s
cout << s[0] << endl;
Prima di accedere al carattere, verifichiamo che s
non sia vuota. Ogni volta che usiamo un pedice, dobbiamo assicurarci che ci sia un valore nella posizione data. Se s
è vuota, allora s[0]
è indefinito.
Finché la string non è const
, possiamo assegnare un nuovo valore al carattere restituito dall'operatore di pedice. Ad esempio, possiamo mettere in maiuscolo la prima lettera come segue:
string s("una stringa");
// Si assicura che ci sia un carattere in s[0]
if (!s.empty())
// assegna un nuovo valore al primo carattere in s
s[0] = toupper(s[0]);
L'output di questo programma è
Una stringa
Iterare una stringa accedendo tramite indice
Oltre all'iterazione tramite il range for
, possiamo anche iterare una string
usando un ciclo for
tradizionale e un indice. Questo approccio è utile quando dobbiamo elaborare i caratteri in una string
fino a quando non viene soddisfatta una certa condizione.
Come esempio, scriviamo un programma che modifica solo la prima parola in s
convertendone le lettere in tutte maiuscole:
#include <iostream>
#include <string>
#include <cctype> // per toupper e isspace
using namespace std;
int main()
{
string s("una stringa");
// elabora i caratteri in s fino a quando
// non esauriamo i caratteri o incontriamo uno spazio bianco
for (decltype(s.size()) indice = 0;
indice != s.size() && !isspace(s[indice]); ++indice)
// metti in maiuscolo il carattere corrente
s[indice] = toupper(s[indice]);
cout << s << endl;
return 0;
}
Questo programma genera il seguente output:
UNA stringa
Il nostro ciclo for
usa indice per indicizzare s
. Usiamo decltype
per dare a indice il tipo appropriato. Inizializziamo indice
a 0 in modo che la prima iterazione inizi sul primo carattere in s
. Su ogni iterazione incrementiamo indice
per guardare il carattere successivo in s
. Nel corpo del ciclo mettiamo in maiuscolo la lettera corrente.
La parte nuova in questo ciclo è la condizione nel for
. Quella condizione usa l'operatore AND logico (l'operatore &&
). Questo operatore produce true
se entrambi gli operandi sono true
e false
altrimenti. La parte importante di questo operatore è che abbiamo la garanzia che valuta il suo operando destro solo se l'operando sinistro è true
. In questo caso, abbiamo la garanzia che non indicizzeremo s
a meno che non sappiamo che indice
è nell'intervallo. Cioè, s[indice]
viene eseguito solo se indice
non è uguale a s.size()
. Poiché indice
non viene mai incrementato oltre il valore di s.size()
, sappiamo che indice
sarà sempre minore di s.size()
.
Questo comportamento degli operatori logici prende il nome di corto circuito ed è ereditato dal linguaggio C. Gli operatori logici &&
e ||
sono gli unici operatori in C++ che garantiscono il corto circuito.
L'accesso con indice ad una stringa non è controllato
Quando usiamo un indice, dobbiamo assicurarci che il indice sia nell'intervallo. Cioè, l'indice deve essere compreso tra 0 e il valore di size() - 1
.
Un modo per semplificare il codice che usa gli indici è sempre usare una variabile di tipo string::size_type
come indice. Poiché quel tipo è unsigned
, assicuriamo che l'indice non possa essere minore di zero. Quando usiamo un valore size_type
come indice, dobbiamo solo verificare che il nostro indice sia minore del valore restituito da size()
.
Inoltre, la libreria standard del C++ non è obbligata a controllare il valore di un indice. Il risultato dell'uso di un indice fuori intervallo è indefinito.
Usare un indice per l'accesso casuale alle stringhe
Nell'esempio precedente abbiamo avanzato il nostro indice di una posizione alla volta per mettere in maiuscolo ogni carattere in sequenza. Possiamo anche calcolare un indice e recuperare direttamente il carattere indicato. Non c'è bisogno di accedere ai caratteri in sequenza.
Come esempio, supponiamo di avere un numero tra 0 e 15 e vogliamo generare la rappresentazione esadecimale di quel numero. Possiamo farlo usando una string
inizializzata per contenere le 16 "cifre" esadecimali:
#include <iostream>
#include <string>
using namespace std;
int main()
{
// possibili cifre esadecimali
const string cifre_esadecimali = "0123456789ABCDEF";
cout << "Inserisci una serie di numeri tra 0 e 15"
<< " separati da spazi. Premi INVIO quando hai finito: "
<< endl;
// conterrà la stringa esadecimale risultante
string risultato;
// contiene i numeri dall'input
string::size_type n;
while (cin >> n)
// ignora input non valido
if (n < cifre_esadecimali.size())
// recupera la cifra esadecimale indicata
risultato += cifre_esadecimali[n];
cout << "Il tuo numero esadecimale è: " << risultato << endl;
return 0;
}
Se diamo a questo programma l'input
12 0 5 15 8 15
l'output sarà
Il tuo numero esadecimale è: C05F8F
Iniziamo inizializzando cifre_esadecimali
per contenere le cifre esadecimali da 0 a F. Rendiamo quella string const
perché non vogliamo che questi valori cambino. All'interno del ciclo usiamo il valore di input n
per indicizzare cifre_esadecimali
. Il valore di cifre_esadecimali[n]
è il char
che appare alla posizione n
in cifre_esadecimali
. Ad esempio, se n
è 15, il risultato è F; se è 12, il risultato è C; e così via. Aggiungiamo quella cifra a risultato, che stampiamo una volta letto tutto l'input.
Ogni volta che usiamo un indice, dovremmo pensare a come sappiamo che è nell'intervallo. In questo programma, il nostro indice, n
, è un string::size_type
, che come sappiamo è un tipo unsigned
. Di conseguenza, sappiamo che n
è garantito essere maggiore o uguale a 0. Prima di usare n
per indicizzare cifre_esadecimali
, verifichiamo che sia minore della dimensione di cifre_esadecimali
.
Esercizi
-
Usa un range for per cambiare tutti i caratteri in una
string
nel caratterex
.Soluzione:
#include <iostream> #include <string> using namespace std; int main() { string s("Ciao Mondo!"); // cambia tutti i caratteri in 'x' for (auto &c : s) // per ogni char in s (nota: c è un riferimento) c = 'x'; // assegna 'x' al carattere corrente cout << s << endl; // stampa la string modificata return 0; }
-
Cosa succederebbe se definissi la variabile di controllo del ciclo nell'esercizio precedente come tipo char?
Risposta: Se definissi la variabile di controllo del ciclo come tipo
char
senza il riferimento (ossiafor (auto c : s)
), allorac
sarebbe una copia del carattere corrente in ogni iterazione. Modificarec
non influenzerebbe i caratteri nella stringa originales
, quindi la stringa rimarrebbe invariata. -
Riscrivi il programma nel primo esercizio, prima usando un while e di nuovo usando un ciclo for tradizionale. Qual'è la differenza tra i tre approcci?
Soluzione con
while
:#include <iostream> #include <string> using namespace std; int main() { string s("Ciao Mondo!"); auto it = s.begin(); // iteratore all'inizio della stringa // cambia tutti i caratteri in 'x' usando while while (it != s.end()) { *it = 'x'; // assegna 'x' al carattere corrente ++it; // avanza l'iteratore } cout << s << endl; // stampa la string modificata return 0; }
Soluzione con ciclo
for
tradizionale:#include <iostream> #include <string> using namespace std; int main() { string s("Ciao Mondo!"); // cambia tutti i caratteri in 'x' usando for tradizionale for (decltype(s.size()) i = 0; i < s.size(); ++i) s[i] = 'x'; // assegna 'x' al carattere corrente cout << s << endl; // stampa la string modificata return 0; }
Differenza tra i tre approcci: - Il range
for
è il più conciso e leggibile, ideale per iterare su ogni elemento di una collezione senza preoccuparsi degli indici o degli iteratori. - Ilwhile
con iteratori offre un controllo più esplicito sull'iterazione, utile quando si lavora con collezioni complesse o quando si desidera manipolare gli iteratori direttamente. - Il ciclofor
tradizionale con indici è più verboso e richiede la gestione manuale degli indici, ma può essere utile quando si ha bisogno di accedere agli elementi in modo non sequenziale o quando si lavora con array. -
Cosa fa il seguente programma? È valido? Se no, perché no?
string s; cout << s[0] << endl;
Risposta: Il programma tenta di accedere al primo carattere di una stringa vuota
s
. Poichés
è vuota, l'accesso as[0]
è indefinito e può causare un comportamento imprevisto. Quindi, il programma non è valido perché non verifica se la stringa contiene almeno un carattere prima di accedere as[0]
. -
Scrivi un programma che legge una
string
di caratteri inclusa la punteggiatura e scrive ciò che è stato letto ma con la punteggiatura rimossa.Soluzione:
#include <iostream> #include <string> #include <cctype> // per ispunct using namespace std; int main() { string input; string output; cout << "Inserisci una stringa (inclusa la punteggiatura): "; getline(cin, input); // legge una riga di input // rimuove la punteggiatura for (auto c : input) { if (!ispunct(c)) // se il carattere non è punteggiatura output += c; // aggiungi il carattere a output } cout << "Stringa senza punteggiatura: " << output << endl; return 0; }
-
Il seguente range
for
è legale? Se sì, qual è il tipo dic
?const string s = "Vietato entrare!"; for (auto &c : s) { /* ... */ }
Risposta: No, il range
for
non è legale. La variabiles
è dichiarata comeconst string
, il che significa che i suoi caratteri non possono essere modificati. Tuttavia, la variabile di controllo del cicloc
è dichiarata come un riferimento nonconst
(auto &c
), il che implica che si potrebbe tentare di modificare i caratteri dis
. Per rendere il ciclo legale,c
dovrebbe essere dichiarato come un riferimentoconst
, cioèconst auto &c
.