confusão de conversão stringstream, string e char *

141

Minha pergunta pode ser resumida em: para onde a string retornada fica stringstream.str().c_str()na memória e por que não pode ser atribuída a const char*?

Este exemplo de código explicará melhor do que eu posso

#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;
}

A suposição que stringstream.str().c_str()poderia ser atribuída a um const char*levou a um bug que demorou um pouco para ser rastreado.

Para pontos de bônus, alguém pode explicar por que substituir a coutdeclaração por

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

imprime as cordas corretamente?

Estou compilando no Visual Studio 2008.

Graphics Noob
fonte

Respostas:

201

stringstream.str()retorna um objeto de string temporário que é destruído no final da expressão completa. Se você obter um ponteiro para uma string C a partir de ( stringstream.str().c_str()), ele apontará para uma string que será excluída onde a instrução termina. É por isso que seu código imprime lixo.

Você pode copiar esse objeto de string temporário para outro objeto de string e tirar a string C daquele:

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

Observe que eu criei a string temporária const, porque qualquer alteração nela pode fazer com que ela seja re-alocada e, assim, cstrinvalidada. Portanto, é mais seguro não armazenar o resultado da chamada str()e usá-lo cstrapenas até o final da expressão completa:

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

Obviamente, o último pode não ser fácil e a cópia pode ser muito cara. O que você pode fazer é vincular o temporário a uma constreferência. Isso prolongará sua vida útil para a vida útil da referência:

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

IMO é a melhor solução. Infelizmente, não é muito conhecido.

sbi
fonte
13
Note-se que fazer uma cópia (como no seu primeiro exemplo) não introduzirá necessariamente nenhuma sobrecarga - se str()for implementado de forma que o RVO possa entrar em ação (o que é muito provável), o compilador poderá construir o resultado diretamente entrando tmp, elegendo o temporário; e qualquer compilador C ++ moderno fará isso quando as otimizações estiverem ativadas. Obviamente, a solução de ligação a const-reference garante nenhuma cópia, portanto pode ser preferível - mas achei que ainda vale a pena esclarecer.
Pavel Minaev 03/09/09
1
"Obviamente, a solução de ligação a const-reference garante nenhuma cópia" <- não. No C ++ 03, o construtor de cópia precisa estar acessível e a implementação tem permissão para copiar o inicializador e vincular a referência à cópia.
Johannes Schaub - litb
1
Seu primeiro exemplo está errado. O valor retornado por c_str () é transitório. Não pode ser invocado após o final da declaração atual. Portanto, você pode usá-lo para passar um valor para uma função, mas NUNCA deve atribuir o resultado de c_str () a uma variável local.
Martin York
2
@ Litb: Você está tecnicamente correto. O ponteiro é válido até a próxima chamada de método sem custo na sequência. O problema é que o uso é inerentemente perigoso. Talvez não para o desenvolvedor original (embora nesse caso tenha sido), mas principalmente para as correções de manutenção subsequentes, esse tipo de código se torna extremamente frágil. Se você quiser fazer isso, você deve agrupar o escopo dos ponteiros para que seu uso seja o mais curto possível (o melhor é o comprimento da expressão).
Martin York
1
@bi: Ok, obrigado, isso é mais claro. Estritamente falando, porém, como a 'string str' var não é modificada no código acima, o str.c_str () permanece perfeitamente válido, mas eu aprecio o perigo potencial em outros casos.
22710 William Knight
13

O que você está fazendo é criar um temporário. Esse temporário existe em um escopo determinado pelo compilador, de modo que seja longo o suficiente para atender aos requisitos de onde está indo.

Assim que a instrução const char* cstr2 = ss.str().c_str();é concluída, o compilador não vê razão para manter a cadeia temporária por aí, e ela é destruída, e assim você const char *está apontando para a memória liberada.

Sua declaração string str(ss.str());significa que o temporário é usado no construtor para a stringvariável strque você colocou na pilha local e que permanece em torno do tempo esperado: até o final do bloco, ou função que você escreveu. Portanto, o const char *interior ainda é uma boa memória quando você tenta o cout.

Jared Oberhaus
fonte
6

Nesta linha:

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

ss.str()fará uma cópia do conteúdo do stringstream. Ao ligar c_str()na mesma linha, você fará referência a dados legítimos, mas após essa linha a string será destruída, deixando você char*apontar para a memória não proprietária.

fbrereto
fonte
5

O objeto std :: string retornado por ss.str () é um objeto temporário que terá um tempo de vida limitado à expressão. Portanto, você não pode atribuir um ponteiro a um objeto temporário sem obter lixo.

Agora, há uma exceção: se você usar uma referência const para obter o objeto temporário, é legal usá-lo por uma vida útil mais longa. Por exemplo, você deve fazer:

#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;
}

Dessa forma, você obtém a corda por mais tempo.

Agora, você precisa saber que existe um tipo de otimização chamado RVO que diz que, se o compilador vir uma inicialização por meio de uma chamada de função e essa função retornar temporariamente, ele não fará a cópia, mas fará com que o valor atribuído seja temporário. . Dessa forma, você não precisa realmente usar uma referência, é apenas se você quiser ter certeza de que não copiará que é necessário. Assim fazendo:

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

seria melhor e mais simples.

Klaim
fonte
5

O ss.str()temporário é destruído após a inicialização de cstr2ser concluída. Portanto, quando você o imprime cout, a string c que foi associada a esse std::stringtemporário há muito tempo é destruída e, portanto, você terá sorte se ele travar e afirmar, e não terá sorte se imprimir lixo ou parecer funcionar.

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

A string C para onde cstr1aponta, no entanto, está associada a uma string que ainda existe no momento em que você faz o item cout- portanto, ele imprime o resultado corretamente.

No código a seguir, o primeiro cstrestá correto (presumo que esteja cstr1no código real?). O segundo imprime a cadeia c associada ao objeto de cadeia temporária ss.str(). O objeto é destruído ao final da avaliação da expressão completa em que aparece. A expressão completa é a cout << ...expressão inteira - portanto, enquanto a c-string é emitida, o objeto de string associado ainda existe. Pois cstr2- é pura maldade que ela consegue. Possivelmente, internamente, escolhe o mesmo local de armazenamento para o novo temporário que já escolheu para o temporário usado para inicializar cstr2. Também poderia falhar.

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

O retorno de c_str()geralmente aponta apenas para o buffer interno da string - mas isso não é um requisito. A string pode formar um buffer se sua implementação interna não for contínua, por exemplo (isso é bem possível - mas no próximo padrão C ++, as strings precisam ser armazenadas contiguamente).

No GCC, as seqüências usam contagem de referência e cópia na gravação. Assim, você descobrirá que o seguinte é verdadeiro (pelo menos na minha versão do GCC)

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

As duas seqüências compartilham o mesmo buffer aqui. No momento em que você altera um deles, o buffer é copiado e cada um mantém sua cópia separada. Outras implementações de string fazem coisas diferentes, no entanto.

Johannes Schaub - litb
fonte