kebingungan stringstream, string, dan char *

141

Pertanyaan saya dapat didiskusikan, di mana string kembali dari stringstream.str().c_str()hidup dalam memori, dan mengapa tidak dapat ditugaskan ke const char*?

Contoh kode ini akan menjelaskannya lebih baik daripada yang saya bisa

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

Asumsi yang stringstream.str().c_str()dapat ditugaskan ke const char*menyebabkan bug yang saya butuh waktu untuk melacak.

Untuk poin bonus, adakah yang bisa menjelaskan mengapa mengganti coutpernyataan itu dengan

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

mencetak string dengan benar?

Saya sedang menyusun di Visual Studio 2008.

Grafik Noob
sumber

Jawaban:

201

stringstream.str()mengembalikan objek string sementara yang dihancurkan pada akhir ekspresi penuh. Jika Anda mendapatkan pointer ke string C dari itu ( stringstream.str().c_str()), itu akan menunjuk ke string yang dihapus di mana pernyataan berakhir. Itu sebabnya kode Anda mencetak sampah.

Anda bisa menyalin objek string sementara ke beberapa objek string lain dan mengambil string C dari yang itu:

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

Perhatikan bahwa saya membuat string sementara const, karena perubahan apa pun dapat menyebabkannya mengalokasikan kembali dan dengan demikian membuat cstrtidak valid. Karena itu lebih aman untuk tidak menyimpan hasil panggilan str()sama sekali dan cstrhanya menggunakan sampai akhir ekspresi penuh:

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

Tentu saja, yang terakhir mungkin tidak mudah dan menyalin mungkin terlalu mahal. Yang bisa Anda lakukan adalah mengikat sementara ke constreferensi. Ini akan memperpanjang masa berlaku seumur hidup referensi:

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

IMO itu solusi terbaik. Sayangnya itu tidak terlalu terkenal.

sbi
sumber
13
Perlu dicatat bahwa melakukan salinan (seperti dalam contoh pertama Anda) tidak perlu memperkenalkan overhead - jika str()diterapkan sedemikian rupa sehingga RVO dapat menendang (yang sangat mungkin), kompiler diizinkan untuk membangun hasilnya secara langsung ke dalam tmp, menghilangkan yang sementara; dan setiap kompiler C ++ modern akan melakukannya ketika optimisasi diaktifkan. Tentu saja, solusi bind-to-const-reference menjamin tidak ada salinan, jadi mungkin lebih disukai - tapi saya pikir itu masih layak diklarifikasi.
Pavel Minaev
1
"Tentu saja, solusi bind-to-const-reference menjamin tidak ada salinan" <- tidak. Di C ++ 03, konstruktor salin harus dapat diakses dan implementasinya diizinkan untuk menyalin inisialisasi dan mengikat referensi ke salinan.
Johannes Schaub - litb
1
Contoh pertama Anda salah. Nilai yang dikembalikan oleh c_str () bersifat sementara. Itu tidak bisa diandalkan setelah akhir pernyataan saat ini. Dengan demikian Anda dapat menggunakannya untuk meneruskan nilai ke suatu fungsi, tetapi Anda TIDAK PERNAH harus menetapkan hasil c_str () ke variabel lokal.
Martin York
2
@ litb: Secara teknis Anda benar. Pointer ini valid hingga panggilan metode non-biaya berikutnya pada string. Masalahnya adalah penggunaannya berbahaya. Mungkin tidak untuk pengembang asli (meskipun dalam hal ini) tetapi terutama untuk perbaikan pemeliharaan selanjutnya, kode semacam ini menjadi sangat rapuh. Jika Anda ingin melakukan ini, Anda harus membungkus lingkup pointer sehingga penggunaannya sesingkat mungkin (yang terbaik adalah panjang ekspresi).
Martin York
1
@sbi: Ok, terima kasih, itu lebih jelas. Tegasnya, karena 'string str' var tidak dimodifikasi dalam kode di atas, str.c_str () tetap benar-benar valid, tetapi saya menghargai potensi bahaya dalam kasus lain.
William Knight
13

Apa yang Anda lakukan adalah membuat sementara. Sementara itu ada dalam ruang lingkup yang ditentukan oleh kompiler, sehingga cukup lama untuk memenuhi persyaratan ke mana ia pergi.

Segera setelah pernyataan const char* cstr2 = ss.str().c_str();selesai, kompiler tidak melihat alasan untuk menjaga string sementara sekitar, dan itu hancur, dan dengan demikian Anda const char *menunjuk ke memori yang bebas.

Pernyataan Anda string str(ss.str());berarti bahwa sementara digunakan di konstruktor untuk stringvariabel stryang telah Anda tempatkan di tumpukan lokal, dan itu tetap ada selama yang Anda harapkan: sampai akhir blok, atau fungsi yang Anda tulis. Oleh karena itu, const char *ingatan di dalamnya masih bagus ketika Anda mencoba cout.

Jared Oberhaus
sumber
6

Di baris ini:

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

ss.str()akan membuat salinan dari isi stringstream. Saat Anda menelepon c_str()pada saluran yang sama, Anda akan mereferensikan data yang sah, tetapi setelah baris itu string tersebut akan dihancurkan, meninggalkan Anda char*untuk menunjuk ke memori yang tidak dimiliki.

untuk melakukannya
sumber
5

Objek std :: string yang dikembalikan oleh ss.str () adalah objek sementara yang akan memiliki masa hidup terbatas pada ekspresi. Jadi, Anda tidak dapat menetapkan pointer ke objek sementara tanpa mendapatkan sampah.

Sekarang, ada satu pengecualian: jika Anda menggunakan referensi const untuk mendapatkan objek sementara, adalah sah untuk menggunakannya untuk waktu hidup yang lebih luas. Misalnya yang harus Anda lakukan:

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

Dengan begitu Anda mendapatkan string untuk waktu yang lebih lama.

Sekarang, Anda harus tahu bahwa ada semacam optimasi yang disebut RVO yang mengatakan bahwa jika kompilator melihat inisialisasi melalui pemanggilan fungsi dan fungsi itu mengembalikan sementara, itu tidak akan melakukan salinan tetapi hanya membuat nilai yang diberikan menjadi sementara . Dengan begitu Anda tidak perlu menggunakan referensi, itu hanya jika Anda ingin memastikan bahwa itu tidak akan menyalin bahwa itu perlu. Demikian melakukan:

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

akan lebih baik dan lebih sederhana.

Klaim
sumber
5

The ss.str()sementara hancur setelah inisialisasi cstr2selesai. Jadi ketika Anda mencetaknya cout, c-string yang dikaitkan dengan std::stringtemporer itu telah lama rusak, dan dengan demikian Anda akan beruntung jika crash dan menegaskan, dan tidak beruntung jika mencetak sampah atau tampaknya berfungsi.

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

C-string di mana cstr1menunjuk ke, dikaitkan dengan string yang masih ada pada saat Anda melakukan cout- sehingga mencetak hasil dengan benar.

Dalam kode berikut, yang pertama cstrbenar (saya menganggap itu cstr1dalam kode asli?). Yang kedua mencetak c-string yang terkait dengan objek string sementara ss.str(). Objek dihancurkan pada akhir evaluasi ekspresi penuh yang muncul. Ekspresi penuh adalah seluruh cout << ...ekspresi - jadi ketika c-string adalah output, objek string yang terkait masih ada. Karena cstr2- itu adalah kejahatan murni yang berhasil. Kemungkinan besar secara internal memilih lokasi penyimpanan yang sama untuk sementara baru yang sudah dipilih untuk sementara digunakan untuk menginisialisasi cstr2. Itu juga bisa crash.

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

Kembalinya c_str()biasanya hanya akan menunjuk ke buffer string internal - tapi itu bukan keharusan. String dapat membuat buffer jika implementasi internalnya tidak berdekatan misalnya (itu sangat mungkin - tetapi dalam C ++ Standard berikutnya, string perlu disimpan secara bersebelahan).

Dalam GCC, string menggunakan penghitungan referensi dan copy-on-write. Dengan demikian, Anda akan menemukan bahwa yang berikut ini benar (ya, setidaknya pada versi GCC saya)

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

Dua string berbagi buffer yang sama di sini. Pada saat Anda mengubah salah satunya, buffer akan disalin dan masing-masing akan menyimpan salinannya yang terpisah. Implementasi string lain melakukan hal yang berbeda.

Johannes Schaub - litb
sumber