Stringstream-, String- und Char * -Konvertierungsverwirrung

140

Meine Frage kann auf Folgendes reduziert werden: Woher kommt die Zeichenfolge, die von stringstream.str().c_str()live im Speicher zurückgegeben wurde, und warum kann sie nicht einer zugewiesen werden const char*?

Dieses Codebeispiel erklärt es besser als ich

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

Die Annahme, stringstream.str().c_str()die einem zugeordnet werden konnte, const char*führte zu einem Fehler, bei dem ich eine Weile brauchte, um ihn aufzuspüren.

Kann jemand für Bonuspunkte erklären, warum er die coutErklärung durch ersetzt

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

druckt die Zeichenfolgen richtig?

Ich kompiliere in Visual Studio 2008.

Grafik Noob
quelle

Antworten:

200

stringstream.str()Gibt ein temporäres Zeichenfolgenobjekt zurück, das am Ende des vollständigen Ausdrucks zerstört wird. Wenn Sie von diesem ( stringstream.str().c_str()) einen Zeiger auf eine C-Zeichenfolge erhalten , zeigt er auf eine Zeichenfolge, die dort gelöscht wird, wo die Anweisung endet. Deshalb druckt Ihr Code Müll.

Sie können dieses temporäre Zeichenfolgenobjekt in ein anderes Zeichenfolgenobjekt kopieren und die C-Zeichenfolge von diesem übernehmen:

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

Beachten Sie, dass ich die temporäre Zeichenfolge erstellt habe const, da Änderungen daran dazu führen können, dass sie neu zugewiesen und somit cstrungültig wird. Es ist daher sicherer, das Ergebnis des Aufrufs str()überhaupt nicht zu speichern und cstrnur bis zum Ende des vollständigen Ausdrucks zu verwenden:

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

Letzteres ist natürlich möglicherweise nicht einfach und das Kopieren ist möglicherweise zu teuer. Sie können stattdessen das Temporäre an eine constReferenz binden . Dies verlängert seine Lebensdauer auf die Lebensdauer der Referenz:

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

IMO, das ist die beste Lösung. Leider ist es nicht sehr bekannt.

sbi
quelle
13
Es sollte beachtet werden, dass das Erstellen einer Kopie (wie in Ihrem ersten Beispiel) nicht unbedingt einen Overhead verursacht. Wenn dies str()so implementiert ist, dass RVO einschalten kann (was sehr wahrscheinlich ist), kann der Compiler das Ergebnis direkt erstellen in tmp, die vorübergehende eliding; und jeder moderne C ++ - Compiler wird dies tun, wenn Optimierungen aktiviert sind. Natürlich garantiert die Bind-to-Const-Referenzlösung keine Kopie, daher ist sie möglicherweise vorzuziehen - aber ich dachte, es lohnt sich immer noch zu klären.
Pavel Minaev
1
"Natürlich garantiert die Bind-to-Const-Referenzlösung keine Kopie" <- das tut es nicht. In C ++ 03 muss auf den Kopierkonstruktor zugegriffen werden können, und die Implementierung darf den Initialisierer kopieren und den Verweis an die Kopie binden.
Johannes Schaub - litb
1
Ihr erstes Beispiel ist falsch. Der von c_str () zurückgegebene Wert ist vorübergehend. Nach dem Ende der aktuellen Anweisung kann nicht mehr darauf vertraut werden. Sie können es also verwenden, um einen Wert an eine Funktion zu übergeben, aber Sie sollten das Ergebnis von c_str () NIEMALS einer lokalen Variablen zuweisen.
Martin York
2
@litb: Du bist technisch korrekt. Der Zeiger ist bis zum nächsten kostenfreien Methodenaufruf für die Zeichenfolge gültig. Das Problem ist, dass die Verwendung von Natur aus gefährlich ist. Möglicherweise nicht für den ursprünglichen Entwickler (obwohl dies in diesem Fall der Fall war), sondern insbesondere für nachfolgende Wartungskorrekturen, wird diese Art von Code äußerst fragil. Wenn Sie dies tun möchten, sollten Sie den Zeigerbereich so umbrechen, dass seine Verwendung so kurz wie möglich ist (am besten die Länge des Ausdrucks).
Martin York
1
@sbi: Ok, danke, das ist klarer. Genau genommen bleibt str.c_str () vollkommen gültig, da die Variable 'string str' im obigen Code nicht geändert wird, aber ich schätze die potenzielle Gefahr in anderen Fällen.
William Knight
13

Was Sie tun, ist eine temporäre zu erstellen. Diese temporäre Datei ist in einem vom Compiler festgelegten Bereich vorhanden, sodass sie lang genug ist, um die Anforderungen zu erfüllen.

Sobald die Anweisung const char* cstr2 = ss.str().c_str();vollständig ist, sieht der Compiler keinen Grund, die temporäre Zeichenfolge const char *beizubehalten , und sie wird zerstört, sodass Sie auf freien Speicher verweisen.

Ihre Anweisung string str(ss.str());bedeutet, dass die temporäre Datei im Konstruktor für die stringVariable verwendet wird str, die Sie auf den lokalen Stapel gelegt haben, und die so lange erhalten bleibt, wie Sie es erwarten: bis zum Ende des Blocks oder der von Ihnen geschriebenen Funktion. Daher ist das const char *Innere immer noch ein gutes Gedächtnis, wenn Sie das versuchen cout.

Jared Oberhaus
quelle
6

In dieser Zeile:

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

ss.str()erstellt eine Kopie des Inhalts des Stringstreams. Wenn Sie c_str()in derselben Zeile anrufen , verweisen Sie auf legitime Daten. Nach dieser Zeile wird die Zeichenfolge jedoch zerstört, sodass Sie char*auf nicht besessenen Speicher verweisen können.

fbrereto
quelle
5

Das von ss.str () zurückgegebene std :: string-Objekt ist ein temporäres Objekt, dessen Lebensdauer auf den Ausdruck beschränkt ist. Sie können also keinem temporären Objekt einen Zeiger zuweisen, ohne Papierkorb zu erhalten.

Nun gibt es eine Ausnahme: Wenn Sie eine konstante Referenz verwenden, um das temporäre Objekt abzurufen, ist es legal, es für eine längere Lebensdauer zu verwenden. Zum Beispiel sollten Sie tun:

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

Auf diese Weise erhalten Sie die Zeichenfolge für eine längere Zeit.

Jetzt müssen Sie wissen, dass es eine Art Optimierung namens RVO gibt, die besagt, dass wenn der Compiler eine Initialisierung über einen Funktionsaufruf sieht und diese Funktion eine temporäre zurückgibt, er nicht kopiert, sondern nur den zugewiesenen Wert als temporär festlegt . Auf diese Weise müssen Sie keine Referenz verwenden. Nur wenn Sie sicher sein möchten, dass sie nicht kopiert wird, ist dies erforderlich. Also:

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

wäre besser und einfacher.

Klaim
quelle
5

Das ss.str()temporäre wird zerstört, nachdem die Initialisierung cstr2abgeschlossen ist. Wenn Sie es also mit drucken cout, ist die C-Zeichenfolge, die mit diesem std::stringtemporären Zeichen verknüpft war, schon lange nicht mehr vorhanden. Sie haben also Glück, wenn es abstürzt und behauptet, und nicht Glück, wenn es Müll druckt oder scheinbar funktioniert.

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

Die C-Zeichenfolge, auf die cstr1verweist, ist jedoch einer Zeichenfolge zugeordnet, die zum Zeitpunkt der Ausführung noch vorhanden ist, coutsodass das Ergebnis korrekt gedruckt wird.

Im folgenden Code ist der erste cstrkorrekt (ich nehme an, er befindet sich cstr1im realen Code?). Die zweite druckt die C-Zeichenfolge, die dem temporären Zeichenfolgenobjekt zugeordnet ist ss.str(). Das Objekt wird am Ende der Auswertung des vollständigen Ausdrucks, in dem es erscheint, zerstört. Der vollständige Ausdruck ist der gesamte cout << ...Ausdruck. Während die Ausgabe des C-Strings erfolgt, ist das zugehörige String-Objekt weiterhin vorhanden. Denn cstr2- es ist reine Schlechtigkeit, dass es gelingt. Möglicherweise wählt es intern denselben Speicherort für das neue temporäre Element aus, das es bereits für das zum Initialisieren verwendete temporäre Element ausgewählt hat cstr2. Es könnte auch abstürzen.

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

Die Rückgabe von c_str()wird normalerweise nur auf den internen Zeichenfolgenpuffer verweisen - dies ist jedoch keine Voraussetzung. Die Zeichenfolge könnte einen Puffer bilden, wenn ihre interne Implementierung beispielsweise nicht zusammenhängend ist (das ist gut möglich - aber im nächsten C ++ - Standard müssen Zeichenfolgen zusammenhängend gespeichert werden).

In GCC verwenden Zeichenfolgen Referenzzählung und Copy-on-Write. Sie werden also feststellen, dass das Folgende zutrifft (zumindest bei meiner GCC-Version).

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

Die beiden Zeichenfolgen teilen sich hier den gleichen Puffer. Wenn Sie einen von ihnen ändern, wird der Puffer kopiert und jeder hat seine eigene Kopie. Andere String-Implementierungen machen die Dinge jedoch anders.

Johannes Schaub - litb
quelle