Una classe C++ per le stringhe

 


 

La classe string

 

La Libreria Standard del C++ mette a disposizione una classe per la gestione delle stringhe, non come array di caratteri (come le stringhe del C), ma come normali oggetti (e quindi, per esempio, trasferibili per copia, a differenza delle stringhe del C, nelle chiamate delle  funzioni). Questa classe si chiama string  ed è definita nell'header file <string>.

Per la verità, il nome string non è altro che un sinonimo (definito con typedef) di:

basic_string<char>

dove basic_string è una classe template con tipo di carattere generico, e quindi string è una specializzazione di basic_string con argomento char. Ma poiché, come abbiamo già detto nel capitolo di introduzione alla Libreria, a noi interessano solo i caratteri di tipo char, ignoreremo la classe template da cui string proviene e tratteremo string come una classe specifica (non template).

Da un altro punto di vista, più vicino agli interessi dell'utente, string può essere considerata come un "contenitore specializzato", e in particolare "somiglia" molto a vector<char>. Possiede quasi tutte le funzionalità di vector, con alcune (poche) caratteristiche in meno e altre (molte) caratteristiche in più; quest'ultime servono soprattutto per eseguire le operazioni specifiche di manipolazione delle stringhe (come per esempio la concatenazione).

In particolare, come gli elementi di vector, anche i caratteri di string possono essere considerati come facenti parte di una sequenza, e quindi string definisce gli stessi iteratori di vector e della stessa categoria (ad accesso casuale). Ciò rende possibile l'applicazione di tutti gli algoritmi generici della STL anche a string, tramite i suoi iteratori. Questo fatto è indubbiamente un vantaggio, ma non così grande come potrebbe sembrare. Infatti gli algoritmi generici sono pensati principalmente per strutture i cui elementi sono significativi anche se presi singolarmente, il che non è generalmente vero per le stringhe. Per esempio, ordinare una stringa non ha senso (e quindi gli algoritmi di ordinamento o di manipolazione di sequenze ordinate sono poco utili se applicati alle stringhe). L'attenzione maggiore va invece concentrata sui metodi di string, alcuni dei quali sono implementati in modo da ottenere un'ottimizzazione più spinta di quanto non sia possibile nel caso generale.

 


 

Confronto fra string e vector<char>

 

In questa sezione elencheremo le funzionalità comuni a string e vector, e, separatamente, i metodi di vector non presenti in string. Nelle sezioni successive tratteremo esclusivamente delle funzioni-membro e delle funzioni esterne specifiche di string. Per il significato dei nomi, e per la descrizione dei tipi e delle funzioni, vedere il capitolo: La Standard Template Library, sezione: Contenitori Standard.

Tipi definiti in string

Nell'ambito della classe string sono definiti gli stessi tipi definiti in vector e in particolare (citiamo i più importanti): iterator, const_iterator, reverse_iterator, const_reverse_iterator, difference_type, value_type, size_type, reference, const_reference

Funzioni-membro comuni

I seguenti metodi, già descritti nella trattazione dei contenitori, sono definiti sia in vector che in string, hanno la stessa sintassi di chiamata e svolgono le medesime operazioni (se vector è specializzato con con argomento char):

dereferenziazione di un iteratore
begin    end    rbegin    rend
resize
size    empty
max_size
reserve    capacity
costruttore di default
costruttore di copia    operator=   assign
costruttore e assign tramite iteratori    
swap
operator[] (accesso non controllato)
at (accesso controllato)
insert    erase

Note:

Funzioni esterne comuni

Tutte le funzioni esterne di "appoggio" definite nell'header file <vector> sono anche definite nell'header file <string>; ricordiamo che queste funzioni sono: swap, operator==, operator!=, operator<, operator<=, operator>, operator>=. Ognuna di esse ha due argomenti, che nelle funzioni definite nell'header file <string> sono ovviamente di tipo string.

Funzioni-membro di vector non presenti in string

Un numero molto ridotto di metodi di vector non è ridefinito in string:

 [p86]

 


 

Il membro statico npos

 

La classe string dichiara il seguente dato-membro "atipico":

static   const   size_type   npos;

che è inizializzato con il valore -1. Poiché d'altra parte il tipo size_type è sempre unsigned, la costante string::npos contiene in realtà il massimo numero positivo possibile. Viene usato come argomento di default di alcune funzioni-membro o come valore di ritorno "speciale" (per esempio per indicare che un certo elemento non è stato trovato). In pratica npos rappresenta un indice che "non può esistere", in quanto è maggiore di tutti gli indici possibili. In un certo senso svolge le stesse funzioni del terminatore nelle stringhe del C, che non esiste negli oggetti di string (il carattere '\0' può essere un elemento di string come tutti gli altri).

Come vedremo, i metodi di string che utilizzano gli indici come argomenti spesso fanno uso di npos per indicare la fine della stringa.

 


 

Costruttori e operazioni di copia

 

Oltre ai 4 costruttori già visti (default, copia da oggetto string, copia tramite iteratori e inizializzazione con carattere di "riempimento"), string definisce i seguenti costruttori specifici:

string::string(const string& str, size_type ind, size_type n=npos)
copia da str, a partire dall'elemento con indice ind, per n elementi o fino al termine di str (quello che "arriva prima")

string::string(const char* s)
copia caratteri, a partire da quello puntato da s e fino a quando incontra il carattere '\0' (escluso); in pratica copia una stringa del C (che può anche essere una costante literal)

string::string(const char* s, size_type n)
come sopra, salvo che copia solo n caratteri (se prima non incontra '\0')

Per quello che riguarda le copie in oggetti di string già esistenti, oltre all'operatore di assegnazione standard e alle due versioni del metodo assign (copia tramite iteratori e copia con carattere di "riempimento") presenti anche in vector, string definisce ulteriori overloads (in tutte le seguenti operazioni l'oggetto esistente viene cancellato e sostituito da quello ottenuto per copia):
string& string::operator=(const char* s)
copia una stringa del C

string& string::operator=(char c)
copia un singolo carattere; nota: l'assegnazione di un singolo carattere è ammessa, mentre l'inizializzazione non lo è

string& string::assign(const string& str)
esegue le stesse operazioni dell'operatore di assegnazione standard

string& string::assign(const string& str, size_type ind, size_type n)
esegue le stesse operazioni del costruttore con uguali argomenti

string& string::assign(const char* s)
esegue le stesse operazioni dell'operatore di assegnazione con uguale argomento

string& string::assign(const char* s, size_type n)
esegue le stesse operazioni del costruttore con uguali argomenti

 


 

Gestione degli errori

 

Abbiamo detto che, come in vector, operator[] non controlla che l'argomento indice sia compreso nel range [0,size()), mentre il metodo at effettua il controllo e genera un'eccezione di tipo out_of_range in caso di errore.

Molti altri metodi di string hanno, fra gli  argomenti, due tipi size_type consecutivi, di cui il primo rappresenta un  indice (che ha il significato di "posizione iniziale"), mentre il secondo rappresenta il numero di caratteri "da quel punto in poi" (abbiamo già visto così fatti un costruttore e un metodo assign). In tutti i casi il primo argomento è sempre controllato (generando la solita eccezione se l'indice non è nel range), mentre il secondo non lo è mai e quindi un numero di caratteri troppo alto viene semplicemente interpretato come "il resto della stringa" (che in particolare è l'unica interpretazione possibile se il valore del secondo argomento è npos). Notare che, se la "posizione iniziale" e/o il numero di caratteri  sono dati come numeri negativi, questi vengono convertiti in valori positivi molto grandi (essendo size_type un tipo unsigned), e quindi, per esempio:
    string(str,-2,3);   genera out_of_range
    string(str,3,-2);  

va bene: costruisce un oggetto string per copia da str, a partire dal quarto carattere fino al  termine

I metodi per la ricerca di sotto-stringhe (che vedremo più avanti) restituiscono npos in caso di insuccesso, ma non generano eccezioni; se però il programma dell'utente non controlla il valore di ritorno e lo usa direttamente come argomento di "posizione" nella chiamata di un'altra funzione, allora sì che, in caso di insuccesso nella ricerca, si genera un'eccezione out_of_range.

I metodi che usano una coppia di iteratori al posto della coppia "posizione-numero" non effettuano nessun controllo (e lo stesso discorso vale per gli algoritmi, come sappiamo) e quindi spetta al programma dell'utente assicurare che i limiti del range non vengano oltrepassati.

La stessa cosa dicasi quando la coppia di argomenti "posizione-numero" si riferisce a una stringa del C: anche qui non viene eseguito nessun controllo  (a parte il controllo sul terminator che viene riconosciuto come fine della stringa) e quindi bisogna porre la massima attenzione sull'argomento che rappresenta la "posizione iniziale" (che in questo caso è un puntatore a char): anzitutto deve essere diverso da NULL (altrimenti il programma abortisce) e in secondo luogo deve realmente puntare a un carattere interno alla stringa.

Un altro tipo di errore (comune anche a vector), molto raro, che genera un'eccezione di tipo length_error, avviene quando si tenta di costruire una stringa più lunga del massimo consentito (dato da max_size). Lo stesso errore è generato se si tenta di superare max_size chiamando un metodo che modifica la dimensione direttamente (resize) o implicitamente (insert, append, replace, operator+=), oppure che modifica la capacità (reserve).

 


 

Conversioni fra oggetti string e stringhe del C

 

La conversione da una stringa del C (che indichiamo con s) a un oggetto string (che indichiamo con str) si ottiene semplicemente assegnando s a str (con operator= o con il metodo assign), oppure costruendo str per copia da s.

Ovviamente, se si vuole eseguire la conversione inversa, da oggetto string a stringa del C, non si può semplicemente invertire gli operandi nell'assegnazione, in quanto il tipo nativo char* non consente assegnazioni da oggetti string. Bisogna invece  ricorrere ad alcuni metodi definiti nella stessa classe string. Questi metodi sono 3 e precisamente:

  1. const char* string::data() const
    scrive i caratteri di
    *this in un array di cui restituisce il puntatore. L'array  è gestito internamente a string e perciò non va preallocato nè cancellato. L'oggetto *this non può essere modificato, nel senso che una sua successiva modifica invalida l'array, nè possono essere modificati i caratteri dello stesso array (in pratica il metodo data può operare solo su oggetti costanti). Non viene aggiunto il terminator alla fine dell'array e quindi non è possibile utilizzare l'array come argomento nelle funzioni che operano sulle stringhe (in sostanza è proprio un array di caratteri , non una stringa!)

  2. const char* string::c_str() const
    è identico a
    data, salvo il fatto che aggiunge il terminator alla fine, creando così un array di caratteri null terminated, cioè una "vera" stringa del C

  3. size_type copy(char* s, size_type n, size_type pos = 0) const
    copia
    n caratteri di *this, a partire dal carattere con indice pos, nell'array s, preallocato dal chiamante. Restituisce il numero di caratteri effettivamente copiati. Non aggiunge il terminator alla fine dell'array. Per copiare tutti i caratteri di *this si può usare string::npos come secondo argomento e omettere il terzo.

Da un esame critico dei tre metodi sopracitati, si può osservare che:

  1. data è "quasi" inutilizzabile (può servire solo quando si trattano array di caratteri e non stringhe)

  2. c_str è invece molto utile, perchè permette di inserire il suo valore di ritorno come argomento nelle funzioni di Libreria del C che operano sulle stringhe. Per esempio:
                   
    int m = atoi(str.c_str());
    (nota: non esistono funzioni C++ che convertono stringhe di caratteri decimali in numeri).
    Tuttavia può operare solo su oggetti costanti

  3. copy ha il vantaggio di permettere la modifica dell'array copiato. Bisogna però ricordarsi di aggiungere un carattere '\0' in fondo (e bisogna anche evitare che lo stesso carattere sia presente all'interno della stringa da copiare)

 


 

Confronti fra stringhe

 

Per confrontare due oggetti string, o un oggetto string e una stringa del C, la classe string fornisce il metodo compare, con vari overloads. Il valore di ritorno è sempre di tipo int ed ha il seguente significato:

Rispetto agli operatori relazionali, il metodo compare ha quindi il vantaggio di restituire il risultato di <, == o > con una sola chiamata. I suoi overloads sono:

L'utente non può fornire un criterio di confronto specifico; se lo vuol fare, non deve usare compare, ma l'algoritmo lexicographical_compare con un predicato. Per esempio:
   lexicographical_compare
(s1.begin(),s1.end(),s2.begin(),s2.end(),nocase);
restituisce true se la stringa s1 precede la stringa s2 in base al criterio di confronto dato dalla funzione
nocase (fornita dall'utente).

Nell'header-file <string> si trovano varie funzioni esterne di "appoggio" che implementano diversi overloads degli operatori relazionali: <, <=, ==, !=, >, >=; per ognuno di essi esistono tre versioni: quella presente anche in <vector> e negli header-files degli altri contenitori,  in cui entrambi gli operandi sono della stessa classe contenitore (in questo caso string) e quelle in cui rispettivamente il primo o il secondo operando è di tipo const char* (cioè una stringa del C). Questo permette di confrontare indifferentemente due oggetti string, o un oggetto string e una stringa del C, o una stringa del C e un oggetto string. In particolare la stringa del C può essere una costante literal. Esempio:
                if (
str == "Hello") .....

 


 

Concatenazioni e inserimenti

 

Concatenare due stringhe significa scrivere le due stringhe l'una di seguito all'altra in una terza stringa.

Nell'header-file <string> si trovano varie funzioni esterne di "appoggio" che implementano diversi overloads dell'operatore +, il quale fornisce la stringa concatenata, date due stringhe come operandi; di queste, una è sempre di tipo const string&, mentre l'altra può essere ancora di tipo const string&, oppure di tipo const char* (cioè una stringa del C), oppure di tipo char (cioè un singolo carattere). Mantenendo la convenzione simbolica che abbiamo usato finora, riteniamo a questo punto che la descrizione delle funzioni possa essere omessa (quando è autoesplicativa già in base ai tipi e ai nomi convenzionali degli argomenti):

Per l'operazione di somma e assegnazione in notazione compatta, sono disponibili tre metodi che implementano altrettanti overloads dell'operatore +=. In questo caso la stringa concatenata è la stessa di partenza (*this) a cui viene aggiunta in coda la stringa-argomento:

Il metodo append esegue la stessa operazione di operator+=, con il vantaggio che gli argomenti possono essere più di uno. Ne sono forniti vari overloads:

Per quello che riguarda l'inserimento di caratteri "in mezzo" a una stringa (operazione di bassa efficienza, come in vector), sono disponibili ulteriori overloads del metodo insert (oltre a quelli comuni con vector); tutti inseriscono caratteri prima dell'elemento di *this con indice pos e restituiscono by reference lo stesso *this:

 


 

Ricerca di sotto-stringhe

 

Nella classe string sono definiti molti metodi che ricercano la stringa-argomento come sotto-stringa di *this. Tutti restituiscono un valore di tipo size_type, che, se la sotto-stringa è trovata, rappresenta l'indice del suo primo carattere; se invece la ricerca fallisce il valore restituito è npos. Tutti i metodi sono definiti const in quanto eseguono la ricerca senza modificare l'oggetto.

Nell'elenco che segue, suddiviso in vari gruppi, l'argomento di nome pos rappresenta l'indice dell'elemento di *this da cui iniziare la ricerca, mentre l'argomento di nome n rappresenta il numero di caratteri della stringa-argomento da utilizzare per la ricerca.

Cerca una sotto-stringa:

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

Cerca il primo carattere di *this che si trova nella stringa-argomento:

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

Cerca il primo carattere di *this che non si trova nella stringa-argomento:

Come sopra, ma partendo dalla fine di *this e scorrendo all'indietro:

 


 

Estrazione e sostituzione di sotto-stringhe

 

Il metodo substr crea una stringa estraendola da *this e la restituisce per copia:

string string::substr(size_type pos=0, size_type n=npos) const

la stringa originaria non è modificata; la nuova stringa coincide con la sotto-stringa di *this che parte dall'elemento con indice pos e contiene n caratteri.

Il metodo replace, definito con vari overloads, sotituisce una sotto-stringa di *this con la stringa-argomento (o una sua sotto-stringa) e restituisce by reference lo stesso *this. Il numero dei nuovi caratteri non deve necessariamente coincidere con quello preesistente (la nuova sotto-stringa può essere più lunga o più corta di quella sostituita) e quindi il metodo replace, oltre a modificare l'oggetto, può anche modificarne la dimensione.

Nell'elenco che segue, i nomi degli argomenti hanno il seguente significato:

Metodi che definiscono la sotto-stringa da sostituire mediante la coppia "posizione-numero":

Metodi che definiscono la sotto-stringa da sostituire mediante una coppia di iteratori:

   
Per cancellare una sotto-stringa è disponibile un ulteriore overload del metodo erase (oltre a quelli comuni con vector):
         string& string::erase(
size_type pos=0, size_type n=npos)
notare che la chiamata di erase senza argomenti equivale alla chiamata di clear in vector.

 


 

Operazioni di input-output

 

Nell'header-file <string> si trovano due funzioni esterne di "appoggio" che implementano due ulteriori overloads degli operatori di flusso "<<" (inserimento) e ">>" (estrazione), con right-operand di tipo string.

Pertanto, la lettura e la scrittura di un oggetto string si possono eseguire semplicemente utilizzando gli operatori di flusso come per le stringhe del C.

In particolare la lettura "salta" (cioè non inserisce nella stringa) i caratteri bianchi e i caratteri speciali (che anzi usa come separatori fra una stringa e l'altra). I caratteri "buoni" vengono invece immessi nella stringa l'uno dopo l'altro a partire dalla "posizione" 0 e fino all'incontro di un separatore; la stringa letta sostituisce quella memorizzata precedentemente, assumendo (in più o in meno) anche una nuova dimensione.

Per la lettura di una stringa che includa anche i caratteri bianchi e i caratteri speciali, in <string> è definita anche la funzione getline:

istream& getline(istream&, string& str, char eol='\n')

che  estrae caratteri dal flusso di input e li memorizza in str; l'estrazione termina quando è incontrato il carattere eol, che viene rimosso dal flusso di input  ma non inserito in str. Omettendo il terzo argomento si ottiene effettivamente la lettura di una intera "linea" di testo.
Il valore di ritono, di tipo riferimento a
istream, permette di utilizzare la chiamata di getline come left-operand di una o più operazioni di estrazione. Esempio:
   
     getline(cin,str1,'\t') >> str2 >> str3 ;
legge tutti i caratteri fino al primo tabulatore (escluso), memorizzandoli in
str1, e poi legge due sequenze di caratteri delimitate da separatori e li memorizza in str2 e str3 .

 [p87]

 


 

Torna all'Indice