zamieszanie podczas konwersji stringstream, string i char *


141

Moje pytanie można sprowadzić do tego, gdzie stringstream.str().c_str()w pamięci znajduje się zwracany ciąg i dlaczego nie można go przypisać do const char*?

Ten przykład kodu wyjaśni to lepiej niż ja

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

Założenie, które stringstream.str().c_str()można przypisać do, const char*doprowadziło do błędu, którego wytropienie zajęło mi trochę czasu.

Jeśli chodzi o punkty bonusowe, czy ktoś może wyjaśnić, dlaczego zastąpić coutoświadczenie

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

drukuje napisy poprawnie?

Kompiluję w Visual Studio 2008.

Odpowiedzi:


201

stringstream.str()zwraca tymczasowy obiekt ciągu, który jest niszczony na końcu pełnego wyrażenia. Jeśli otrzymasz wskaźnik do łańcucha C z that ( stringstream.str().c_str()), wskaże on łańcuch, który zostanie usunięty w miejscu, w którym instrukcja się kończy. Dlatego twój kod wypisuje śmieci.

Możesz skopiować ten tymczasowy obiekt typu string do innego obiektu typu string i pobrać z niego łańcuch C:

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

Zauważ, że utworzyłem tymczasowy ciąg const, ponieważ wszelkie zmiany w nim mogą spowodować jego ponowne przydzielenie, a tym samym cstrunieważnienie. Dlatego bezpieczniej jest w ogóle nie przechowywać wyniku wywołania str()i używać cstrtylko do końca pełnego wyrażenia:

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

Oczywiście to drugie może nie być łatwe, a kopiowanie może być zbyt kosztowne. Zamiast tego możesz powiązać obiekt tymczasowy z constodwołaniem. Wydłuży to jego żywotność do czasu życia odniesienia:

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

IMO to najlepsze rozwiązanie. Niestety nie jest to zbyt dobrze znane.


13
Należy zauważyć, że wykonanie kopii (tak jak w pierwszym przykładzie) niekoniecznie spowoduje jakiekolwiek obciążenie - jeśli str()jest zaimplementowane w taki sposób, że RVO może się uruchomić (co jest bardzo prawdopodobne), kompilator może bezpośrednio skonstruować wynik do tmp, eliminując tymczasowe; a każdy nowoczesny kompilator C ++ zrobi to, gdy optymalizacje są włączone. Oczywiście rozwiązanie bind-to-const-reference gwarantuje brak kopiowania, więc może być lepsze - ale pomyślałem, że nadal warto to wyjaśnić.
Pavel Minaev

1
„Oczywiście, rozwiązanie typu bind-to-const-reference gwarantuje brak kopiowania” <- tak nie jest. W C ++ 03 konstruktor kopiujący musi być dostępny, a implementacja może skopiować inicjator i powiązać odwołanie z kopią.
Johannes Schaub - litb

1
Twój pierwszy przykład jest zły. Wartość zwracana przez c_str () jest przejściowa. Nie można na to polegać po zakończeniu obecnego oświadczenia. Dlatego możesz go użyć do przekazania wartości do funkcji, ale NIGDY nie powinieneś przypisywać wyniku c_str () do zmiennej lokalnej.
Martin York

2
@litb: Masz techniczną poprawność. Wskaźnik jest ważny do następnego wywołania metody bezkosztowej w ciągu. Problem polega na tym, że używanie jest z natury niebezpieczne. Może nie dla pierwotnego dewelopera (choć w tym przypadku tak było), ale zwłaszcza przy późniejszych poprawkach konserwacyjnych, ten rodzaj kodu staje się niezwykle delikatny. Jeśli chcesz to zrobić, powinieneś zawinąć zakres wskaźników tak, aby jego użycie było jak najkrótsze (najlepiej długość wyrażenia).
Martin York

1
@sbi: Ok, dzięki, to jest bardziej jasne. Ściśle mówiąc, ponieważ zmienna „string str” nie jest modyfikowana w powyższym kodzie, str.c_str () pozostaje całkowicie poprawna, ale doceniam potencjalne niebezpieczeństwo w innych przypadkach.
William Knight,

13

To, co robisz, to tworzenie tymczasowego. Ten tymczasowy istnieje w zakresie określonym przez kompilator, tak że jest wystarczająco długi, aby spełnić wymagania miejsca, w którym się znajduje.

Gdy tylko instrukcja const char* cstr2 = ss.str().c_str();jest kompletna, kompilator nie widzi powodu, aby trzymać tymczasowy ciąg w pobliżu i jest on niszczony, a zatem const char *wskazuje na zwolnioną pamięć.

Twoja instrukcja string str(ss.str());oznacza, że stringzmienna tymczasowa jest używana w konstruktorze dla zmiennej str, którą umieściłeś na lokalnym stosie i pozostaje tak długo, jak można się spodziewać: do końca bloku lub funkcji, którą napisałeś. Dlatego const char *wnętrze jest nadal dobrą pamięcią, gdy spróbujesz cout.


6

W tej linii:

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

ss.str()utworzy kopię zawartości łańcucha ciągów. Kiedy wywołujesz c_str()tę samą linię, będziesz odnosić się do prawdziwych danych, ale po tej linii ciąg zostanie zniszczony, pozostawiając char*wskazanie pamięci , której nie jesteś właścicielem.


5

Obiekt std :: string zwrócony przez ss.str () jest obiektem tymczasowym, którego czas życia będzie ograniczony do wyrażenia. Nie możesz więc przypisać wskaźnika do tymczasowego obiektu bez wyrzucania śmieci.

Teraz jest jeden wyjątek: jeśli używasz odwołania do const, aby uzyskać obiekt tymczasowy, legalne jest używanie go przez dłuższy czas. Na przykład powinieneś zrobić:

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

W ten sposób otrzymujesz sznurek na dłuższy czas.

Teraz musisz wiedzieć, że istnieje rodzaj optymalizacji zwany RVO, który mówi, że jeśli kompilator zobaczy inicjalizację za pośrednictwem wywołania funkcji, a ta funkcja zwróci wartość tymczasową, nie wykona kopii, ale po prostu sprawi, że przypisana wartość będzie tymczasowa . W ten sposób nie musisz faktycznie używać referencji, tylko wtedy, gdy chcesz mieć pewność, że nie skopiuje się, że jest to konieczne. Więc robiąc:

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

byłoby lepsze i prostsze.


5

Plik ss.str()tymczasowy jest niszczony po zakończeniu inicjalizacji cstr2. Więc kiedy drukujesz to za pomocą cout, ciąg c, który był powiązany z tym std::stringtymczasem, już dawno został zniszczony, a zatem będziesz miał szczęście, jeśli ulegnie awarii i potwierdzi, a nie będzie szczęścia, jeśli drukuje śmieci lub wydaje się działać.

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

C-string, w którym cstr1wskazuje, jest jednak powiązany z ciągiem, który nadal istnieje w momencie wykonywania cout- więc poprawnie drukuje wynik.

W poniższym kodzie pierwszy cstrjest poprawny (zakładam, że jest cstr1w prawdziwym kodzie?). Drugi drukuje łańcuch c powiązany z tymczasowym obiektem ciągu ss.str(). Obiekt jest niszczony pod koniec oceny pełnego wyrażenia, w którym się pojawia. Pełne wyrażenie jest całym cout << ...wyrażeniem - więc gdy wyprowadzany jest łańcuch c, powiązany obiekt ciągu nadal istnieje. Bo cstr2- to czyste zło, że się udaje. Najprawdopodobniej wewnętrznie wybiera tę samą lokalizację przechowywania dla nowego tymczasowego, który już wybrał dla tymczasowego używanego do inicjalizacji cstr2. Może również ulec awarii.

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

Zwrot c_str()will zwykle po prostu wskazuje na wewnętrzny bufor ciągów - ale nie jest to wymagane. Łańcuch mógłby stanowić bufor, jeśli na przykład jego wewnętrzna implementacja nie jest ciągła (jest to całkiem możliwe - ale w następnym standardzie C ++ ciągi muszą być przechowywane w sposób ciągły).

W GCC ciągi używają liczenia odwołań i kopiowania przy zapisie. W związku z tym przekonasz się, że spełnione są następujące warunki (tak jest, przynajmniej w mojej wersji GCC)

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

Te dwa ciągi mają tutaj ten sam bufor. W momencie zmiany jednego z nich bufor zostanie skopiowany, a każdy będzie przechowywać oddzielną kopię. Jednak inne implementacje stringów działają inaczej.

Korzystając z naszej strony potwierdzasz, że przeczytałeś(-aś) i rozumiesz nasze zasady używania plików cookie i zasady ochrony prywatności.
Licensed under cc by-sa 3.0 with attribution required.