CodeGym /Corsi /C++ SELF /Escape nelle stringhe e virgolette nell'input

Escape nelle stringhe e virgolette nell'input

C++ SELF
Livello 9, Lezione 5
Disponibile

1. Introduzione

Quando si inizia a lavorare con le stringhe sembra che una stringa sia sempre la stessa: «una sequenza di caratteri». Ma in C++ c'è una differenza fondamentale tra ciò che è scritto nel sorgente del programma e ciò che l'utente inserisce durante l'esecuzione. Se questa differenza non la senti, puoi passare ore a discutere con std::cin, e poi incolpare il compilatore (non si offende, ne ha viste tante).

Literal di stringa è ciò che scrivi direttamente nel codice: "Hello\n", "C:\\temp\\file.txt", "He said: \"Hi\"".

Input dell'utente è ciò che arriva dalla console: per esempio, l'utente ha digitato He said: "Hi" o C:\temp\file.txt. Per il programma sono solo caratteri da leggere.

Punto chiave: l'escape (\n, \", \\) funziona solo all'interno dei literal nel sorgente, perché è il compilatore che decide come trasformare il tuo testo in byte della stringa. Nell'input dell'utente la barra inversa è semplicemente il carattere \, non ha proprietà «magiche».

2. Escape nei literal di stringa

L'idea dell'escape suona da film di spionaggio: «escapiamo le virgolette altrimenti ci scoprono». In realtà è più semplice: alcuni caratteri hanno difficoltà a «vivere» dentro un literal perché hanno un significato speciale per la sintassi del linguaggio. L'esempio più ovvio è la virgoletta doppia ": essa chiude il literal, quindi se vuoi stampare una virgoletta come carattere devi spiegare al compilatore: «non è la fine della stringa, è solo una virgoletta».

Nello standard C++ le regole sui literal di stringa e carattere sono descritte a livello lessicale (sezioni come lex.string, lex.ccon ecc.). Quindi non è una «feature di iostream», non è una «peculiarità di Windows» e non è un «capriccio dell'IDE». È parte del linguaggio.

Le escape-sequenze più utili

Per non trasformare la vita in una serie «indovina quanti slash servono», è utile avere una piccola tabella mentale. Diciamo subito: parliamo delle escape-sequenze comuni che usi ogni giorno.

Cosa vogliamo avere nella stringa Come scriverlo nel literal C++ Perché serve
A capo
"\n"
Per output multilinea, formattare testo
Tabulazione
"\t"
Allineamento semplice di colonne
Virgolette "
"\""
Stampare citazioni, testo simile a JSON, suggerimenti
Barra inversa \
"\\"
Percorsi Windows, regex (più avanti), formati speciali
Apostrofo '
'\''
Meno comune, ma serve nei testi
Byte nullo
"\0"
In esercizi didattici quasi inutile, ma utile da conoscere

Esistono anche varianti più esotiche (esadecimali, escape Unicode ecc.), ma oggi non le tocchiamo: prima concentriamoci a non avere paura della semplice \\.

Mini-esempio: perché "\n" funziona, mentre l'input dell'utente con \n no

Ecco un esempio molto istruttivo. È come una cartina al tornasole: capisci se distingui la «magia del compilatore» dai «semplici caratteri».

#include <iostream>
#include <string>

int main() {
    std::string a = "Hello\nWorld";
    std::string b = "Hello\\nWorld";

    std::cout << a << '\n';
    // Hello
    // World

    std::cout << b << '\n';
    // Hello\nWorld
}

La prima stringa viene stampata su due righe perché \n dentro il literal diventa il carattere di nuova linea.

La seconda viene stampata come \n, perché \\ significa «una singola barra inversa» e poi c'è la lettera normale n.

Differenza tra '\n' e "\n"

Qui nasce spesso confusione: abbiamo già mostrato che si può usare '\n' — allora perché esiste anche "\n"?

Differenza di tipi:

  • '\n' — è un carattere (char)
  • "\n" — è una stringa (literal di stringa)

Dal punto di vista pratico è importante quando concateni stringhe. Un carattere si può aggiungere come singolo simbolo, una stringa come sequenza.

#include <iostream>
#include <string>

int main() {
    std::string s = "A";
    s += '\n';
    s += "B\n";

    std::cout << s;
    // A
    // B
}

Qui prima abbiamo aggiunto un solo carattere newline, poi abbiamo aggiunto la stringa "B\n".

Virgolette dentro un literal di stringa

Vogliamo mostrare un suggerimento all'utente tipo:

Inserisci comando: "add milk 2"

Se lo scrivi ingenuamente, il compilatore vedrà la fine della stringa troppo presto:

// non è corretto:
std::cout << "Inserisci comando: "add milk 2"\n";

La versione corretta è escapare le virgolette interne:

#include <iostream>

int main() {
    std::cout << "Inserisci comando: \"add milk 2\"\n";
    // Inserisci comando: "add milk 2"
}

Nota: le virgolette interne sono caratteri di testo, le virgolette esterne sono la sintassi del C++.

Barra inversa nella stringa: perché il percorso Windows «si rompe»

Nei percorsi Windows la barra inversa è ovunque. Il principiante scrive:

std::string path = "C:\temp\file.txt";

E iniziano le sorprese (non quelle desiderate). Perché \t è una tabulazione, \f è un altro escape, e alla fine la stringa non è più un percorso ma una sequenza di caratteri speciali.

La soluzione corretta è raddoppiare la slash:

#include <iostream>
#include <string>

int main() {
    std::string path = "C:\\temp\\file.txt";
    std::cout << path << '\n';
    // C:\temp\file.txt
}

3. Perché si «rompe» std::cin >>

Siamo arrivati alla seconda metà dell'argomento, dove il dolore non viene dalle slash ma dall'input.

L'espressione std::cin >> s legge una parola (token): una sequenza di caratteri fino al primo spazio (o tab/nuova linea — grosso modo fino a whitespace). Perciò per lui lo spazio è un separatore.

Le virgolette " in questo modo non hanno alcuna magia. Sono semplicemente un carattere.

Se l'utente digita "green tea" e fai:

#include <iostream>
#include <string>

int main() {
    std::string x;
    std::cin >> x;
    std::cout << "x=[" << x << "]\n";
    // input: "green tea"
    // x=["green]
}

Il risultato sarà "green, perché la lettura si è fermata allo spazio. Il resto tea" resterà nel buffer e il successivo >> lo leggerà.

Si può immaginare così:

flowchart LR
    A[Input: 'green tea'] --> B[operator>> legge fino al whitespace]
    B --> C[x = 'green']
    A --> D[nel buffer resta: 'tea']

Logica molto umana: «nel terminale ho scritto "hello world", quindi è una frase unica». Questa logica funziona in alcune shell dove le virgolette fanno parte della sintassi della riga di comando. Ma std::cin >> non è una shell. Non conosce le regole delle virgolette, conosce solo «leggi fino allo spazio».

Importante da ricordare come principio: il formato di input è deciso dal programma, non dall'utente. Se vuoi supportare le virgolette come sintassi, devi implementarlo tu.

4. Introduzione alle funzioni

Voglio anticipare un po' e mostrare qualche esempio con funzioni. Come scrivere le tue funzioni lo vedremo nel dettaglio al livello 14, qui spiego l'essenza in modo che i prossimi esempi siano chiari.

Finora tutto il codice stava dentro main(). Va bene per i primi passi, ma i programmi reali quasi mai sono una sola funzione grande. Per rendere il codice più leggibile, comprensibile e manutenibile lo si divide in funzioni.

Una funzione è un blocco di codice nominato che puoi richiamare per nome. Quando il programma incontra la chiamata di funzione, entra temporaneamente in essa, esegue le istruzioni e poi torna al punto di chiamata.

Vediamo l'esempio più semplice:

#include <iostream>

void say_hello() {
    std::cout << "Hello!\n";
}

int main() {
    say_hello();
}

Quando l'esecuzione arriva a say_hello();, il programma entra nella funzione say_hello, stampa il testo e ritorna a main. Così possiamo dare nomi significativi alle azioni e non ripetere codice.

Ora analizziamo la parte importante della dichiarazione: la parola void.

Nel caso:

void say_hello()

la parola void significa che la funzione non restituisce nulla. Esegue un'azione ma non ritorna un valore.

Se la funzione deve restituire un risultato, al posto di void si mette il tipo del risultato. Per esempio:

int add(int a, int b) {
    return a + b;
}

Qui la funzione restituisce un valore di tipo int, quindi all'interno si usa return con un'espressione. Al contrario, in una funzione con void si può non scrivere return oppure scriverlo senza valore:

void print_line() {
    std::cout << "-----\n";
}

Tale funzione esegue semplicemente un'azione e termina. Niente di complicato.

5. Come leggere una frase intera e gestire le virgolette

Quando vuoi ottenere una riga intera (con spazi e virgolette come caratteri), usa std::getline.

#include <iostream>
#include <string>

int main() {
    std::string line;
    std::getline(std::cin, line);

    std::cout << "line=[" << line << "]\n";
    // input: "green tea"
    // line=["green tea"]
}

Ora virgolette e spazi sono parte della stringa.

Se prima hai letto qualcosa con std::cin >> (per esempio un numero), non dimenticare il passaggio robusto via std::ws:

#include <iostream>
#include <string>

int main() {
    int n = 0;
    std::cin >> n;

    std::string line;
    std::getline(std::cin >> std::ws, line);

    std::cout << "n=" << n << ", line=[" << line << "]\n";
}

Rimuovere le virgolette ai bordi, se presenti

std::getline non è tenuto a togliere le virgolette. Legge onestamente la riga. Se vuoi che "green tea" diventi green tea, devi fare un passo in più.

Non costruiremo un «parser completo di virgolette». Oggi facciamo il minimo sensato e sicuro: se la stringa inizia con " e finisce con ", togliamo quelle due virgolette.

#include <string>

std::string strip_quotes(std::string s) {
    if (s.size() >= 2 && s.front() == '"' && s.back() == '"') {
        return s.substr(1, s.size() - 2);
    }
    return s;
}

Nota il controllo su s.size() >= 2: altrimenti una stringa con una sola virgoletta è già un formato «rotto» e non ha senso tagliare.

Esempio pratico: mini-console di comandi

Per non restare «in astratto», continuiamo con una semplice applicazione console “MiniCLI”: legge il comando come riga intera e poi esegue alcune azioni semplici. Ancora senza contenitori, senza liste e senza una vera shell, ma assomiglia già a una piccola utility.

L'idea: abbiamo un "titolo" (title). L'utente può impostarlo con il comando:

  • title milk
  • title "green tea"

La seconda forma permette gli spazi.

Stampiamo l'help con le virgolette

Per far capire il formato stampiamo un suggerimento. Qui ci sono «due mondi»: nel codice le virgolette vanno escape-ate, sullo schermo devono apparire come virgolette normali.

#include <iostream>

void print_help() {
    std::cout << "Comandi:\n";
    std::cout << "  help                      - mostrare aiuto\n";
    std::cout << "  title TEXT                - impostare il titolo\n";
    std::cout << "  title \"TEXT WITH SPACES\"  - titolo con spazi\n";
}

Qui \"TEXT WITH SPACES\" è un literal, quindi le virgolette sono escape-ate.

Leggiamo la riga del comando intera

Lo scheletro di main() che legge il comando come riga:

#include <iostream>
#include <string>

int main() {
    std::string line;
    while (std::getline(std::cin, line)) {
        std::cout << "got=[" << line << "]\n";
    }
}

Sì, per ora è una «eco-mode», ma mostra il punto principale: ottieni la riga intera, senza la sorpresa che «operator>> abbia mangiato la prima parola».

Estrarre l'argomento dopo title e rimuovere le virgolette

Supponiamo che la normalizzazione degli spazi (trim) la sappiate già fare. Qui c'è un trim minimo trim_spaces così l'esempio è autonomo.

#include <string>

std::string trim_spaces(std::string s) {
    std::size_t b = 0;
    while (b < s.size() && s[b] == ' ') ++b;

    std::size_t e = s.size();
    while (e > b && s[e - 1] == ' ') --e;

    return s.substr(b, e - b);
}

Ed ora il comando title:

#include <iostream>
#include <string>

int main() {
    std::string title = "untitled";
    std::string line;

    while (std::getline(std::cin, line)) {
        if (line.rfind("title ", 0) == 0) {        // rfind - ricerca dall'inizio
            std::string arg = trim_spaces(line.substr(6));
            arg = strip_quotes(arg);
            title = arg;
            std::cout << "title=[" << title << "]\n";
        }
    }
}

line.rfind("title ", 0) == 0 — tecnica comoda per «la stringa comincia con…», se non vuoi ancora usare starts_with. Prendiamo tutto dopo title con substr(6), trimmiamo i bordi, e se ci sono virgolette ai bordi le togliamo.

6. Errori tipici

Errore №1: provare a stampare virgolette in un literal senza escape.
Quando scrivi una stringa come "He said: "Hi"", il compilatore pensa che la stringa termini alla seconda virgoletta e poi trova spazzatura. La forma corretta è "He said: \"Hi\"".

Errore №2: dimenticare che la barra inversa nel literal è l'inizio di un'escape-sequenza.
La stringa "C:\temp\file.txt" quasi certamente non è quello che ti aspetti, perché \t e altre combinazioni hanno significato speciale. Per i percorsi Windows usa "C:\\temp\\file.txt".

Errore №3: aspettarsi che le virgolette nell'input «uniscano le parole» per std::cin >>.
Se l'utente ha digitato "green tea", allora std::cin >> s leggerà solo "green. Le virgolette qui sono un normale carattere, lo spazio rimane separatore.

Errore №4: leggere «una frase con spazi» con std::cin >>.
È un errore fondamentale nella scelta dello strumento. Se ti serve la riga intera usa std::getline. Se prima hai usato >>, usa std::getline(std::cin >> std::ws, line) per non catturare una riga vuota.

Errore №5: «togliere le virgolette» senza controllare lunghezza e limiti.
Tentare di fare s.substr(1, s.size() - 2) su una stringa di lunghezza 0 o 1 porta a logica scorretta e possibili errori. La protezione minima è controllare s.size() >= 2 e verificare front()/back().

1
Compito
C++ SELF, livello 9, lezione 5
Bloccato
Vetrina degli escape
Vetrina degli escape
1
Compito
C++ SELF, livello 9, lezione 5
Bloccato
Citazione sicura
Citazione sicura
1
Compito
C++ SELF, livello 9, lezione 5
Bloccato
Input tra virgolette
Input tra virgolette
1
Compito
C++ SELF, livello 9, lezione 5
Bloccato
Mini JSON
Mini JSON
1
Sondaggio/quiz
Stringhe: approfondimenti e analisi del testo, livello 9, lezione 5
Non disponibile
Stringhe: approfondimenti e analisi del testo
Stringhe: approfondimenti e analisi del testo
Commenti
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION