ความสับสนในการแปลง stringstream, string และ char *


141

คำถามของฉันสามารถต้มลงไปที่ใดสตริงกลับมาจากการstringstream.str().c_str()มีชีวิตอยู่ในความทรงจำและทำไมไม่สามารถที่จะได้รับมอบหมายให้const char*?

ตัวอย่างโค้ดนี้จะอธิบายได้ดีกว่าที่ฉันสามารถทำได้

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

การสันนิษฐานที่stringstream.str().c_str()สามารถมอบหมายให้const char*นำไปสู่ข้อผิดพลาดที่ใช้เวลาสักครู่ในการติดตาม

สำหรับคะแนนโบนัสทุกคนสามารถอธิบายได้ว่าทำไมแทนที่coutคำสั่งด้วย

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

พิมพ์สตริงอย่างถูกต้องหรือไม่

ฉันกำลังรวบรวมใน Visual Studio 2008

คำตอบ:


201

stringstream.str()ส่งคืนวัตถุสตริงชั่วคราวที่ถูกทำลายในตอนท้ายของการแสดงออกเต็มรูปแบบ หากคุณได้รับตัวชี้ไปยังสตริง C จากนั้น ( stringstream.str().c_str()) มันจะชี้ไปที่สตริงซึ่งจะถูกลบโดยที่คำสั่งสิ้นสุด นั่นเป็นสาเหตุที่รหัสของคุณพิมพ์ขยะ

คุณสามารถคัดลอกวัตถุสตริงชั่วคราวนั้นไปยังวัตถุสตริงอื่นและนำสตริง C จากอันนั้น:

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

โปรดทราบว่าฉันทำสตริงชั่วคราวconstเนื่องจากการเปลี่ยนแปลงใด ๆ กับมันอาจทำให้เกิดการจัดสรรอีกครั้งและทำให้การแสดงผลcstrไม่ถูกต้อง มันปลอดภัยกว่าที่จะไม่เก็บผลลัพธ์ของการโทรไปstr()เลยและใช้cstrเฉพาะเมื่อสิ้นสุดการแสดงออกเต็มรูปแบบ:

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

แน่นอนว่าหลังอาจไม่ง่ายและการทำสำเนาอาจแพงเกินไป คุณสามารถทำอะไรแทนการผูกชั่วคราวกับการconstอ้างอิง สิ่งนี้จะยืดอายุการใช้งานของมันไปตลอดอายุการอ้างอิง:

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

IMO นั่นคือทางออกที่ดีที่สุด น่าเสียดายที่มันไม่ค่อยเป็นที่รู้จัก


13
ควรสังเกตว่าการทำสำเนา (เช่นในตัวอย่างแรกของคุณ) จะไม่แนะนำค่าใช้จ่ายใด ๆ - หากstr()มีการนำไปใช้ในลักษณะที่ RVO สามารถเตะได้ (ซึ่งมีโอกาสมาก) ผู้แปลจะได้รับอนุญาตให้สร้างผลลัพธ์โดยตรง เข้าtmpหลบชั่วคราว และคอมไพเลอร์ C ++ ที่ทันสมัยจะทำเช่นนั้นเมื่อเปิดใช้งานการปรับให้เหมาะสม แน่นอนโซลูชันการอ้างอิงแบบ bind-to-const รับประกันไม่คัดลอกดังนั้นอาจจะดีกว่า - แต่ฉันคิดว่ามันยังคงคุ้มค่าชัดเจน
Pavel Minaev

1
"แน่นอนโซลูชันการอ้างอิงแบบ bind-to-const รับประกันการไม่คัดลอก" <- ไม่ได้ ใน C ++ 03 ตัวสร้างการคัดลอกจะต้องสามารถเข้าถึงได้และการใช้งานจะได้รับอนุญาตให้คัดลอก initializer และผูกการอ้างอิงกับสำเนา
Johannes Schaub - litb

1
ตัวอย่างแรกของคุณผิด ค่าที่ส่งคืนโดย c_str () เป็นค่าชั่วคราว ไม่สามารถเชื่อถือได้หลังจากสิ้นสุดคำสั่งปัจจุบัน ดังนั้นคุณสามารถใช้มันเพื่อส่งค่าไปยังฟังก์ชัน แต่คุณไม่ควรกำหนดผลลัพธ์ของ c_str () ให้กับตัวแปรโลคอล
Martin York

2
@litb: คุณถูกต้องทางเทคนิค ตัวชี้จะใช้ได้จนกระทั่งการเรียกใช้เมธอดไม่ใช่ค่าถัดไปบนสตริง ปัญหาคือว่าการใช้งานเป็นอันตรายโดยเนื้อแท้ อาจไม่ใช่นักพัฒนาดั้งเดิม (แต่ในกรณีนี้คือ) แต่โดยเฉพาะอย่างยิ่งการแก้ไขการบำรุงรักษาที่ตามมารหัสประเภทนี้จะบอบบางมาก หากคุณต้องการทำเช่นนี้คุณควรจะ จำกัด ขอบเขตพอยน์เตอร์เพื่อให้การใช้งานสั้นที่สุดเท่าที่จะทำได้ (ความยาวของนิพจน์ที่ดีที่สุด)
Martin York

1
@sbi: โอเคขอบคุณนั่นชัดเจนกว่า แม้ว่าจะพูดอย่างเคร่งครัดเนื่องจาก var 'string str' ไม่ได้ถูกแก้ไขในโค้ดข้างต้น str.c_str () ยังคงใช้ได้อย่างสมบูรณ์ แต่ฉันขอขอบคุณความเสี่ยงที่อาจเกิดขึ้นในกรณีอื่น ๆ
William Knight

13

สิ่งที่คุณกำลังทำคือการสร้างชั่วคราว ชั่วคราวนั้นมีอยู่ในขอบเขตที่คอมไพเลอร์กำหนดซึ่งยาวพอที่จะสนองความต้องการของที่ที่จะไปได้

ทันทีที่คำสั่งconst char* cstr2 = ss.str().c_str();เสร็จสมบูรณ์คอมไพเลอร์ไม่เห็นเหตุผลที่จะเก็บสตริงชั่วคราวไว้รอบ ๆ และมันจะถูกทำลายและดังนั้นคุณจึงconst char *ชี้ไปยังหน่วยความจำที่ว่าง

คำสั่งของคุณstring str(ss.str());หมายความว่ามีการใช้ชั่วคราวในตัวสร้างสำหรับstringตัวแปรstrที่คุณใส่ในสแต็กท้องถิ่นและที่อยู่รอบตราบเท่าที่คุณคาดหวัง: จนกว่าจะสิ้นสุดของบล็อกหรือฟังก์ชั่นที่คุณเขียน ดังนั้นภายในยังคงเป็นหน่วยความจำที่ดีเมื่อคุณลองconst char *cout


6

ในบรรทัดนี้:

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

ss.str()จะทำสำเนาเนื้อหาของสตริงสตรีม เมื่อคุณโทรc_str()ในบรรทัดเดียวกันคุณจะอ้างอิงข้อมูลที่ถูกต้อง แต่หลังจากนั้นบรรทัดนั้นสตริงจะถูกทำลายทำให้คุณchar*ชี้ไปยังหน่วยความจำที่ไม่ได้เป็นเจ้าของ


5

std :: วัตถุสตริงที่ส่งกลับโดย ss.str () เป็นวัตถุชั่วคราวที่จะมีเวลาชีวิต จำกัด การแสดงออก ดังนั้นคุณไม่สามารถกำหนดตัวชี้ไปยังวัตถุชั่วคราวโดยไม่ทิ้งขยะ

ตอนนี้มีข้อยกเว้นหนึ่งประการคือถ้าคุณใช้การอ้างอิง const เพื่อรับวัตถุชั่วคราวมันถูกต้องตามกฎหมายที่จะใช้มันเป็นเวลาชีวิตที่กว้างขึ้น ตัวอย่างเช่นคุณควรทำ:

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

ด้วยวิธีนี้คุณจะได้รับสตริงเป็นเวลานานขึ้น

ตอนนี้คุณต้องรู้ว่ามีการเพิ่มประสิทธิภาพที่เรียกว่า RVO ที่บอกว่าถ้าคอมไพเลอร์เห็นการเริ่มต้นผ่านการเรียกใช้ฟังก์ชั่นและฟังก์ชั่นที่ส่งกลับชั่วคราวมันจะไม่ทำสำเนา แต่เพียงทำให้ค่าที่กำหนดเป็นชั่วคราว . ด้วยวิธีนี้คุณไม่จำเป็นต้องใช้การอ้างอิงจริง ๆ ก็ต่อเมื่อคุณต้องการให้แน่ใจว่ามันจะไม่คัดลอกที่จำเป็น ดังนั้นการทำ:

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

จะดีขึ้นและง่ายขึ้น


5

ss.str()ชั่วคราวถูกทำลายหลังจากการเริ่มต้นของcstr2เสร็จสมบูรณ์ ดังนั้นเมื่อคุณพิมพ์ด้วยcoutc-string ที่เชื่อมโยงกับstd::stringชั่วคราวนั้นได้ถูกทำลายไปนานแล้วดังนั้นคุณจะโชคดีถ้ามันขัดข้องและยืนยันและไม่โชคดีถ้ามันพิมพ์ขยะหรือไม่ทำงาน

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

C-string ที่cstr1ชี้ไปยังเกี่ยวข้องกับสตริงที่ยังคงอยู่ในเวลาที่คุณทำcoutดังนั้นมันจะพิมพ์ผลลัพธ์ได้อย่างถูกต้อง

ในโค้ดต่อไปนี้อันแรกcstrถูกต้อง (ฉันถือว่ามันอยู่cstr1ในรหัสจริง?) ที่สองพิมพ์ ss.str()C-สตริงเกี่ยวข้องกับวัตถุสตริงชั่วคราว วัตถุถูกทำลายในตอนท้ายของการประเมินการแสดงออกที่ปรากฏ การแสดงออกแบบเต็มคือการcout << ...แสดงออกทั้งหมด- ดังนั้นในขณะที่ c-string เป็นเอาท์พุทวัตถุสตริงที่เกี่ยวข้องยังคงมีอยู่ สำหรับcstr2- มันเป็นความเลวร้ายที่มันประสบความสำเร็จ cstr2ส่วนใหญ่แล้วมันอาจจะเป็นภายในเลือกสถานที่เก็บเหมือนกันสำหรับใหม่ชั่วคราวซึ่งมันแล้วเลือกสำหรับชั่วคราวที่ใช้ในการเริ่มต้น มันอาจจะผิดพลาดได้เช่นกัน

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

โดยc_str()ปกติแล้วการคืนค่าจะชี้ไปที่บัฟเฟอร์สตริงภายใน แต่นั่นไม่ใช่ข้อกำหนด สตริงสามารถสร้างบัฟเฟอร์ได้หากการใช้งานภายในไม่ได้ต่อเนื่องกัน (เป็นไปได้ดี - แต่ในมาตรฐาน C ++ ถัดไปสตริงต้องถูกจัดเก็บไว้อย่างต่อเนื่อง)

ใน GCC สตริงใช้การนับการอ้างอิงและการคัดลอกเมื่อเขียน ดังนั้นคุณจะพบว่าสิ่งต่อไปนี้ถือเป็นจริง (อย่างน้อยก็ในเวอร์ชัน GCC ของฉัน)

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

สตริงสองสายใช้บัฟเฟอร์ร่วมกันที่นี่ ในเวลาที่คุณเปลี่ยนหนึ่งในนั้นบัฟเฟอร์จะถูกคัดลอกและแต่ละคนจะถือสำเนาที่แยกต่างหาก การใช้งานสตริงอื่น ๆ ทำสิ่งที่แตกต่างกัน

โดยการใช้ไซต์ของเรา หมายความว่าคุณได้อ่านและทำความเข้าใจนโยบายคุกกี้และนโยบายความเป็นส่วนตัวของเราแล้ว
Licensed under cc by-sa 3.0 with attribution required.