Programmazione in C++: stranezze con la classe string

Oggi un lettore (medeoTL, grazie!) mi ha fatto notare che nel programmino che avevo proposto ieri ho scritto una cosa inesatta. Ho scritto che l’istruzione v2[2] = 5; non compilava. E invece compila e funziona correttamente. Per capire di cosa stiamo parlando, ecco un programmino che mette in risalto il problema.

#include <string>
#include <iostream>

using namespace std;

int main()
{
    string nome = "Ok";
    string cognome;
    cognome = "Panico";
    string s;
    s = 65;
    cout << s << endl;
    string s1(65);
    cout << s1 << endl;
}

In questo programmino mi limito ad usare la classe string della libreria standard.
La prima istruzione crea un oggetto di tipo string e gli assegna come valore iniziale la stringa “Ok”.
La seconda istruzione invece crea l’oggetto cognome, che è una stringa vuota, e la terza istruzione assegna il valore “Panico”.

Sembra la stessa cosa, e invece sono due operazioni diverse. Nel caso dell’oggetto nome, sto creando l’oggetto con un valore iniziale. La funzione che si occupa di creare un oggetto e assegnare il valore iniziale si chiama “costruttore”. Il suo prototipo è qualcosa simile a questo:

class string {
    ...
    string(const char *s);
    ...
};

Si tratta di una funzione che ha lo stesso nome della classe, e prende in input un puntatore a carattere.

Quando viene costruito l’oggetto cognome, invece, non si da alcun valore iniziale. Si usa allora un altro costruttore che non prende alcun parametro (detto “costruttore di default”). Più o meno una cosa così:

class string {
    ...
    string();
    string(const char *s);
    ...
};

Avete quindi visto due costruttori, uno di default e uno che prende un puntatore a carattere.

Infine, assegnamo un valore all’oggetto cognome: in questo caso, viene invocata un’altra funzione che si chiama “operatore di assegnamento”, che ha questo proprotipo:

class string {
    ...
    string();
    string(const char *s);
    ...
    string &operator=(const char *s);
};

Come vedete, dunque, ci sono ben due modi di assegnare valori a una stringa. Durante la creazione, viene chiamato il costruttore, mentre se l’assegnamento avviene dopo la creazione, viene chiamato l’operatore di assegnamento.

Potete anche notare una particolarità del C++ (e non solo): ci possono essere più funzioni con lo stesso nome, ma con parameteri diversi. Per il C++ sono a tutti gli effetti funzioni completamente diverse. Come fa il C++ a sapere quale chiamare? beh, a seconda del numero e tipo degli argomenti. Ricordate? Il C++ applica lo strong typing, e quindi è possibile capire quale funzione chiamare a seconda del tipo del parametro.

E veniamo al punto. In realtà di operatori assegnamento ce ne sono 3 versioni:

class string {
    ...
    string();
    string(const char *s);
    ...
    string& operator= ( const string& str );
    string& operator= ( const char* s );
    string& operator= ( char c );
};

Il primo viene chiamato quando a destra dell’uguale c’è un’altro oggetto di tipo stringa; il secondo quando c’è un puntatore a carattere (ovvero una “stringa” del vecchio C), il terzo quando c’è un semplice carattere.

E ora torniamo al nostro programma di prova iniziale, e osserviamo le istruzioni seguenti:

    ...
    string s;
    s = 65;
    cout << s << endl;
    string s1(65);
    cout << s1 << endl;
    ...

L’istruzione s = 65 non da alcun problema, perché una costante può essere convertita implicitamente in un char, e quindi viene chiamata la terza versione dell’operatore assegnamento! Se eseguite il codice, vi stamperà una bella A maiuscola (corrispondente al codice ASCII 65).

L’istruzione string s1(65); invece da un errore di compilazione, esattamente questo:

lipari@lipari$ g++ prova.cpp 
prova.cpp: In function ‘int main()’:
prova.cpp:12:17: error: invalid conversion from ‘int’ to ‘const char*’ [-fpermissive]
/usr/include/c++/4.6/bits/basic_string.tcc:214:5: error:   initializing argument 1 of ‘std::basic_string<_CharT, _Traits, _Alloc>::basic_string(const _CharT*, const _Alloc&) [with _CharT = char, _Traits = std::char_traits<char>, _Alloc = std::allocator<char>]’ [-fpermissive]

Cioè, non esiste un costruttore di stringa che prenda in ingresso un carattere e basta; il compilatore cerca disperatamente di convertire l’intero 65 in un puntatore a char, ma questa conversione è vietata (a meno di usare il flag di compilazione -fpermissive), e quindi da errore.

A mio modesto parere, questa cosa qui è una cavolata. Secondo la logica e secondo tutti i guru e i libri di testo, il costruttore e l’operatore di assegnamento dovrebbero essere “simmetrici”, proprio per evitare strani comportamenti come quello descritto nell’esempio. Quindi, o tutti e due prendono per buono il carattere isolato; oppure nessuno dei due. Per la classe string, invece, questa regola d’oro è stata violata, perché?

Proabbilmente perché la classe è stata una delle prime ad essere progettata e realizzata nella libreria, quando ancora non era chiarissimo cosa era giusto e cosa non lo era; e poi cambiarla è diventato impossibile, o quantomeno molto difficile per problemi di compatibilità del codice.

Insomma, nessuno è perfetto (tantomeno la libreria standard c++, anche se ci si avvicina abbastanza!)

About these ads
Posta un commento o usa questo indirizzo per il trackback.

Commenti

  • Maurizio Cozzetto  On 30 gennaio 2014 at 19:04

    ciao

    ho scritto un programmino stupido

    ve lo riporto sotto (lasciate pure perdere il significato dei campi)

    struct DataNascita {
    int giorno;
    string mese;
    int anno;
    };

    // modella un regista
    struct Regista {
    string nomeRegista;
    DataNascita dataNascita;
    int numFilms;
    int posPrimoFilm;
    };

    // modella un film
    struct Film {
    string titoloFilm;
    int durata;
    };

    int main() {

    cout << "Dimensione tipo int: " << sizeof(int) << endl;
    cout << "Dimensione tipo string: " << sizeof(string) << endl;
    cout << "Dimensione struttura DataNascita: " << sizeof(DataNascita) << " byte" << endl;
    cout << "Dimensione struttura Regista: " << sizeof(Regista) << " byte" << endl;
    cout << "Dimensione struttura Film " << sizeof(Film) << " byte" << endl;

    return 0;
    }

    ma mi dà cose strane, mi aspetto che la struttura DataNascita occupi 16 byte ma mi restituisce 24 (8 byte in più), Regista sulla base del suo calcolo mi da 40 byte mentre Film è 16 invece che 12, 4 byte in più

    secondo voi perché mi da questi risultati?

    grazie

    mi sono iscritto vi seguo

    io insegno informatica e sistemi a brescia presso itis castelli

    http://www.mauriziocozzetto.it

    • glipari  On 30 gennaio 2014 at 20:11

      Salve, grazie per l’interesse!

      Perché si aspetta che DataNascita occupi solo 16 byte? Oltre ai due interi c’è un terzo campo, il campo mese che è di tipo string, e quest’ultimo è un oggetto. Nel mio PC (che in questo momento è un 32 bit) mi da sizeof(int) di 4 byte, e sizeof(string) ancora 4 byte. Essendoci 3 campi da 4 byte l’uno, mi stampa correttamente 12 byte per il tipo DataNascita. Dove sarebbe la stranezza? Anche per Regista mi da 24, e per Film mi da 8.
      Insomma, a me torna.
      Nel suo caso, su un sistema a 64 bit, dovrebbero essere esattamente il doppio: Sono 8 byte per ciascuno dei 3 campi in DataNascita, e arriviamo a 24. Per Regista dovrebbe dare 48 e per Film 16. Torna?

Rispondi

Inserisci i tuoi dati qui sotto o clicca su un'icona per effettuare l'accesso:

Logo WordPress.com

Stai commentando usando il tuo account WordPress.com. Chiudi sessione / Modifica )

Foto Twitter

Stai commentando usando il tuo account Twitter. Chiudi sessione / Modifica )

Foto di Facebook

Stai commentando usando il tuo account Facebook. Chiudi sessione / Modifica )

Google+ photo

Stai commentando usando il tuo account Google+. Chiudi sessione / Modifica )

Connessione a %s...

Iscriviti

Ricevi al tuo indirizzo email tutti i nuovi post del sito.

Unisciti agli altri 63 follower

%d blogger cliccano Mi Piace per questo: