confusion de conversion de chaînes de caractères, de chaînes et de caractères *

141

Ma question peut être résumée à, où la chaîne retournée par stringstream.str().c_str()Live in Memory, et pourquoi ne peut-elle pas être attribuée à un const char*?

Cet exemple de code l'expliquera mieux que moi

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const char* cstr2 = ss.str().c_str();

    cout << cstr1   // Prints correctly
        << cstr2;   // ERROR, prints out garbage

    system("PAUSE");

    return 0;
}

L'hypothèse qui stringstream.str().c_str()pourrait être attribuée à un a const char*conduit à un bug qui m'a pris du temps à dépister.

Pour les points bonus, quelqu'un peut-il expliquer pourquoi le remplacement de la coutdéclaration par

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)

imprime correctement les chaînes?

Je compile dans Visual Studio 2008.

Graphiques Noob
la source

Réponses:

201

stringstream.str()renvoie un objet chaîne temporaire qui est détruit à la fin de l'expression complète. Si vous obtenez un pointeur vers une chaîne C à partir de ce ( stringstream.str().c_str()), il pointera vers une chaîne qui est supprimée à la fin de l'instruction. C'est pourquoi votre code imprime des déchets.

Vous pouvez copier cet objet de chaîne temporaire dans un autre objet de chaîne et prendre la chaîne C de celui-ci:

const std::string tmp = stringstream.str();
const char* cstr = tmp.c_str();

Notez que j'ai créé la chaîne temporaire const, car toute modification y est susceptible de la réallouer et donc de la rendre cstrinvalide. Il est donc plus sûr de ne pas stocker du tout le résultat de l'appel à str()et de cstrne l' utiliser que jusqu'à la fin de l'expression complète:

use_c_str( stringstream.str().c_str() );

Bien sûr, ce dernier n'est peut-être pas facile et la copie peut être trop coûteuse. Ce que vous pouvez faire à la place, c'est lier le temporaire à une constréférence. Cela étendra sa durée de vie à la durée de vie de la référence:

{
  const std::string& tmp = stringstream.str();   
  const char* cstr = tmp.c_str();
}

IMO c'est la meilleure solution. Malheureusement, ce n'est pas très connu.

sbi
la source
13
Il convient de noter que faire une copie (comme dans votre premier exemple) n'introduira pas nécessairement de surcharge - si elle str()est implémentée de telle sorte que RVO puisse démarrer (ce qui est très probable), le compilateur est autorisé à construire le résultat directement en tmp, élimination du temporaire; et tout compilateur C ++ moderne le fera lorsque les optimisations sont activées. Bien sûr, la solution bind-to-const-reference garantit l'absence de copie, donc peut-être préférable - mais j'ai pensé que cela méritait encore d'être clarifié.
Pavel Minaev
1
"Bien sûr, la solution de liaison à const-reference garantit l'absence de copie" <- ce n'est pas le cas. En C ++ 03, le constructeur de copie doit être accessible et l'implémentation est autorisée à copier l'initialiseur et à lier la référence à la copie.
Johannes Schaub - litb le
1
Votre premier exemple est faux. La valeur renvoyée par c_str () est transitoire. Il ne peut pas être invoqué après la fin de la déclaration actuelle. Vous pouvez donc l'utiliser pour passer une valeur à une fonction mais vous ne devez JAMAIS affecter le résultat de c_str () à une variable locale.
Martin York
2
@litb: Vous avez techniquement raison. Le pointeur est valide jusqu'au prochain appel de méthode sans coût sur la chaîne. Le problème est que l'utilisation est intrinsèquement dangereuse. Peut-être pas pour le développeur d'origine (bien que dans ce cas c'était le cas) mais surtout pour les correctifs de maintenance ultérieurs, ce type de code devient extrêmement fragile. Si vous voulez faire cela, vous devez envelopper la portée des pointeurs afin que son utilisation soit aussi courte que possible (le meilleur étant la longueur de l'expression).
Martin York
1
@sbi: Ok, merci, c'est plus clair. Cependant, à proprement parler, puisque la variable 'string str' n'est pas modifiée dans le code ci-dessus, la str.c_str () reste parfaitement valide, mais j'apprécie le danger potentiel dans d'autres cas.
William Knight
13

Ce que vous faites, c'est créer un fichier temporaire. Ce temporaire existe dans une portée déterminée par le compilateur, de sorte qu'il est suffisamment long pour répondre aux exigences de la destination.

Dès que l'instruction const char* cstr2 = ss.str().c_str();est terminée, le compilateur ne voit aucune raison de conserver la chaîne temporaire, et elle est détruite, et ainsi vous const char *pointe vers la mémoire libre.

Votre déclaration string str(ss.str());signifie que le temporaire est utilisé dans le constructeur pour la stringvariable strque vous avez placée sur la pile locale, et qu'il reste aussi longtemps que vous le souhaitez: jusqu'à la fin du bloc ou de la fonction que vous avez écrite. Par conséquent, l' const char *intérieur est toujours une bonne mémoire lorsque vous essayez le cout.

Jared Oberhaus
la source
6

Dans cette ligne:

const char* cstr2 = ss.str().c_str();

ss.str()fera une copie du contenu du stringstream. Lorsque vous appelez c_str()sur la même ligne, vous référencerez des données légitimes, mais après cette ligne, la chaîne sera détruite, vous laissant char*pointer vers une mémoire sans propriétaire.

fbrereto
la source
5

L'objet std :: string retourné par ss.str () est un objet temporaire qui aura une durée de vie limitée à l'expression. Vous ne pouvez donc pas affecter un pointeur à un objet temporaire sans obtenir la corbeille.

Maintenant, il y a une exception: si vous utilisez une référence const pour obtenir l'objet temporaire, il est légal de l'utiliser pour une durée de vie plus longue. Par exemple, vous devriez faire:

#include <string>
#include <sstream>
#include <iostream>

using namespace std;

int main()
{
    stringstream ss("this is a string\n");

    string str(ss.str());

    const char* cstr1 = str.c_str();

    const std::string& resultstr = ss.str();
    const char* cstr2 = resultstr.c_str();

    cout << cstr1       // Prints correctly
        << cstr2;       // No more error : cstr2 points to resultstr memory that is still alive as we used the const reference to keep it for a time.

    system("PAUSE");

    return 0;
}

De cette façon, vous obtenez la chaîne plus longtemps.

Maintenant, vous devez savoir qu'il existe une sorte d'optimisation appelée RVO qui dit que si le compilateur voit une initialisation via un appel de fonction et que cette fonction retourne un temporaire, il ne fera pas la copie mais rendra simplement la valeur assignée temporaire . De cette façon, vous n'avez pas besoin d'utiliser réellement une référence, c'est seulement si vous voulez être sûr qu'elle ne copiera pas que c'est nécessaire. Alors faire:

 std::string resultstr = ss.str();
 const char* cstr2 = resultstr.c_str();

serait mieux et plus simple.

Klaim
la source
5

Le ss.str()temporaire est détruit une fois l'initialisation de cstr2est terminée. Ainsi, lorsque vous l'imprimez avec cout, la chaîne C qui était associée à ce std::stringtemporaire a longtemps été détruite, et vous aurez donc de la chance si elle se bloque et s'affirme, et pas de chance si elle imprime des déchets ou semble fonctionner.

const char* cstr2 = ss.str().c_str();

La chaîne C vers laquelle cstr1pointe, cependant, est associée à une chaîne qui existe toujours au moment où vous effectuez le cout- donc elle imprime correctement le résultat.

Dans le code suivant, le premier cstrest correct (je suppose que c'est cstr1dans le vrai code?). La seconde imprime la chaîne C associée à l'objet chaîne temporaire ss.str(). L'objet est détruit à la fin de l'évaluation de l'expression complète dans laquelle il apparaît. L'expression complète est l' cout << ...expression entière - ainsi, tant que la chaîne C est sortie, l'objet chaîne associé existe toujours. Car cstr2- c'est de la pure méchanceté qu'elle réussit. Il choisit très probablement en interne le même emplacement de stockage pour le nouveau temporaire qu'il a déjà choisi pour le temporaire utilisé pour l'initialisation cstr2. Cela pourrait aussi bien s'écraser.

cout << cstr            // Prints correctly
    << ss.str().c_str() // Prints correctly
    << cstr2;           // Prints correctly (???)

Le retour de c_str()pointera généralement simplement vers le tampon de chaîne interne - mais ce n'est pas obligatoire. La chaîne pourrait constituer un tampon si son implémentation interne n'est pas contiguë par exemple (c'est bien possible - mais dans le prochain standard C ++, les chaînes doivent être stockées de manière contiguë).

Dans GCC, les chaînes utilisent le comptage de références et la copie sur écriture. Ainsi, vous constaterez que ce qui suit est vrai (c'est le cas, du moins sur ma version GCC)

string a = "hello";
string b(a);
assert(a.c_str() == b.c_str());

Les deux chaînes partagent le même tampon ici. Au moment où vous changez l'un d'eux, le tampon sera copié et chacun conservera sa copie séparée. Cependant, d'autres implémentations de chaînes font les choses différemment.

Johannes Schaub - litb
la source