stringstream, string y char * conversion confusion

141

Mi pregunta se puede resumir en: ¿dónde se devuelve la cadena de la stringstream.str().c_str()memoria en vivo y por qué no se puede asignar a una const char*?

Este ejemplo de código lo explicará mejor de lo que puedo

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

La suposición que stringstream.str().c_str()podría asignarse a un const char*condujo a un error que me llevó un tiempo rastrear.

Para obtener puntos de bonificación, ¿alguien puede explicar por qué reemplazar la coutdeclaración con

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

imprime las cadenas correctamente?

Estoy compilando en Visual Studio 2008.

Noob de gráficos
fuente

Respuestas:

201

stringstream.str()devuelve un objeto de cadena temporal que se destruye al final de la expresión completa. Si obtiene un puntero a una cadena C desde ese ( stringstream.str().c_str()), apuntará a una cadena que se elimina donde termina la instrucción. Es por eso que su código imprime basura.

Puede copiar ese objeto de cadena temporal en otro objeto de cadena y tomar la cadena C de ese:

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

Tenga en cuenta que hice la cadena temporal const, porque cualquier cambio en ella podría hacer que se reasigne y, por lo tanto, se vuelva cstrinválida. Por lo tanto, es más seguro no almacenar el resultado de la llamada str()y usarlo cstrsolo hasta el final de la expresión completa:

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

Por supuesto, esto último puede no ser fácil y copiar puede ser demasiado costoso. En cambio, lo que puede hacer es vincular lo temporal a una constreferencia. Esto extenderá su vida útil a la vida útil de la referencia:

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

OMI, esa es la mejor solución. Lamentablemente no es muy conocido.

sbi
fuente
13
Debe tenerse en cuenta que hacer una copia (como en su primer ejemplo) no necesariamente introducirá ninguna sobrecarga; si str()se implementa de tal manera que RVO pueda entrar en acción (lo cual es muy probable), el compilador puede construir el resultado directamente dentro tmp, eludiendo lo temporal; y cualquier compilador de C ++ moderno lo hará cuando las optimizaciones estén habilitadas. Por supuesto, la solución bind-to-const-reference garantiza la no copia, por lo que puede ser preferible, pero pensé que aún vale la pena aclararlo.
Pavel Minaev
1
"Por supuesto, la solución bind-to-const-reference garantiza la no copia" <- no lo hace. En C ++ 03, el constructor de la copia debe ser accesible y la implementación puede copiar el inicializador y vincular la referencia a la copia.
Johannes Schaub - litb
1
Tu primer ejemplo está mal. El valor devuelto por c_str () es transitorio. No se puede confiar después del final de la declaración actual. Por lo tanto, puede usarlo para pasar un valor a una función, pero NUNCA debe asignar el resultado de c_str () a una variable local.
Martin York
2
@litb: Eres técnicamente correcto. El puntero es válido hasta la próxima llamada al método sin costo en la cadena. El problema es que el uso es inherentemente peligroso. Tal vez no para el desarrollador original (aunque en este caso lo fue), pero especialmente para las correcciones de mantenimiento posteriores, este tipo de código se vuelve extremadamente frágil. Si desea hacer esto, debe ajustar el alcance de los punteros para que su uso sea lo más breve posible (lo mejor es la longitud de la expresión).
Martin York
1
@sbi: Ok, gracias, eso está más claro. Sin embargo, estrictamente hablando, dado que la 'cadena str' var no está modificada en el código anterior, str.c_str () sigue siendo perfectamente válido, pero aprecio el peligro potencial en otros casos.
William Knight
13

Lo que estás haciendo es crear un temporal. Ese temporal existe en un ámbito determinado por el compilador, de modo que es lo suficientemente largo para satisfacer los requisitos de hacia dónde se dirige.

Tan pronto como const char* cstr2 = ss.str().c_str();se completa la declaración , el compilador no ve ninguna razón para mantener la cadena temporal, y se destruye, y por lo tanto, const char *está apuntando a la memoria liberada.

Su declaración string str(ss.str());significa que el temporal se usa en el constructor para la stringvariable strque ha puesto en la pila local, y que permanece durante todo el tiempo que esperaría: hasta el final del bloque o la función que ha escrito. Por lo tanto, el const char *interior sigue siendo buena memoria cuando prueba el cout.

Jared Oberhaus
fuente
6

En esta linea:

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

ss.str()hará una copia del contenido de la secuencia de cadenas. Cuando llame c_str()a la misma línea, hará referencia a datos legítimos, pero después de esa línea, la cadena se destruirá, dejando char*que apunte a la memoria no deseada.

fbrereto
fuente
5

El objeto std :: string devuelto por ss.str () es un objeto temporal que tendrá un tiempo de vida limitado a la expresión. Por lo tanto, no puede asignar un puntero a un objeto temporal sin obtener basura.

Ahora, hay una excepción: si usa una referencia constante para obtener el objeto temporal, es legal usarlo durante un tiempo de vida más amplio. Por ejemplo, debes hacer:

#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 esa manera obtienes la cuerda por más tiempo.

Ahora, debe saber que hay un tipo de optimización llamada RVO que dice que si el compilador ve una inicialización a través de una llamada de función y esa función devuelve un temporal, no hará la copia sino que hará que el valor asignado sea temporal . De esa manera, no necesita utilizar una referencia, solo si desea asegurarse de que no copiará que es necesario. Al hacer eso:

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

Sería mejor y más simple.

Klaim
fuente
5

El ss.str()temporal se destruye después de cstr2completar la inicialización . Entonces, cuando lo imprime cout, la cadena en C que estaba asociada con ese std::stringtemporal ha sido destruida por mucho tiempo, y por lo tanto tendrá suerte si se bloquea y afirma, y ​​no tendrá suerte si imprime basura o parece funcionar.

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

cstr1Sin embargo, la cadena C a la que apunta está asociada con una cadena que todavía existe en el momento en que lo hace cout, por lo que imprime correctamente el resultado.

En el siguiente código, el primero cstres correcto (¿supongo que está cstr1en el código real?). El segundo imprime la cadena c asociada con el objeto de cadena temporal ss.str(). El objeto se destruye al final de evaluar la expresión completa en la que aparece. La expresión completa es la cout << ...expresión completa , por lo tanto, mientras se emite la cadena c, el objeto de cadena asociado todavía existe. Porque cstr2es pura maldad lo que tiene éxito. Posiblemente, internamente elige la misma ubicación de almacenamiento para el nuevo temporal que ya eligió para el temporal utilizado para inicializar cstr2. También podría estrellarse.

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

El retorno de c_str()generalmente solo apuntará al búfer de cadena interno, pero eso no es un requisito. La cadena podría formar un búfer si su implementación interna no es contigua, por ejemplo (eso es muy posible, pero en el próximo estándar de C ++, las cadenas deben almacenarse contiguamente).

En GCC, las cadenas usan recuento de referencias y copia en escritura. Por lo tanto, encontrará que lo siguiente es cierto (lo hace, al menos en mi versión de GCC)

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

Las dos cadenas comparten el mismo búfer aquí. En el momento en que cambie uno de ellos, el búfer se copiará y cada uno tendrá su copia separada. Sin embargo, otras implementaciones de cadenas hacen cosas diferentes.

Johannes Schaub - litb
fuente