плутанина перетворення рядків, рядків та знаків char *


141

Моє запитання можна звести до того, куди повертається рядок із stringstream.str().c_str()живої пам’яті і чому його не можна призначити a 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();
}

ІМО - це найкраще рішення. На жаль, це не дуже добре відомо.


13
Слід зазначити, що виконання копії (як у вашому першому прикладі) не обов'язково вводить накладні витрати - якщо str()вона реалізована таким чином, що RVO може вдаритись (що дуже ймовірно), компілятору дозволено безпосередньо сконструювати результат. в tmp, усуваючи тимчасове; і будь-який сучасний компілятор C ++ зробить це, коли ввімкнено оптимізацію. Звичайно, рішення прив’язки до контесту посилань гарантує відсутність копії, тому може бути кращим - але я вважав, що все-таки варто уточнити.
Павло Мінаєв

1
"Звичайно, рішення прив'язки до const-посилання гарантує відсутність копії" <- це не так. У C ++ 03 конструктору копій потрібно бути доступним, а реалізації дозволяється копіювати ініціалізатор і прив’язувати посилання на копію.
Йоханнес Шауб - litb

1
Ваш перший приклад неправильний. Значення, повернене c_str (), є тимчасовим. На неї не можна покластися після закінчення поточного твердження. Таким чином, ви можете використовувати його для передачі значення функції, але НІКОЛИ не слід призначати результат c_str () локальній змінній.
Мартін Йорк

2
@litb: Ви технічно правильні. Вказівник дійсний до наступного виклику методу не витрат у рядку. Проблема полягає в тому, що використання по суті небезпечно. Можливо, не для оригінального розробника (хоча в цьому випадку це було), але особливо для подальших виправлень технічного обслуговування цей код стає надзвичайно крихким. Якщо ви хочете це зробити, слід обернути область покажчиків так, щоб його використання було максимально коротким (найкраще - це довжина виразу).
Мартін Йорк

1
@sbi: Добре, дякую, це зрозуміліше. Однак суворо кажучи, оскільки var 'string str' не змінено у наведеному вище коді, str.c_str () залишається абсолютно дійсним, але я оцінюю потенційну небезпеку в інших випадках.
Вільям Найт

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 :: string, повернутий 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завершена. Отже, коли ви друкуєте його cout, c-рядок, який був пов'язаний з std::stringтимчасовим, вже давно призначений, і, таким чином, вам пощастить, якщо він вийде з ладу і стверджує, а не пощастить, якщо він друкує сміття або здається, що він працює.

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

Однак C-рядок, на який cstr1вказує, пов'язаний із рядком, який існує ще в той момент, коли ви це робите cout- тому він правильно друкує результат.

У наступному коді перший cstrправильний (я припускаю, що він знаходиться cstr1в реальному коді?). Друга друкує c-рядок, пов'язаний з тимчасовим об'єктом рядка ss.str(). Об'єкт руйнується в кінці оцінки повного вираження, в якому він з'являється. Повний вираз - це весь cout << ...вираз - тому, поки виводиться рядок c, пов'язаний об'єкт рядка все ще існує. Бо 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());

Дві струни мають один і той же буфер. Під час зміни однієї з них буфер буде скопійовано, і кожен вмістить свою окрему копію. В інших реалізаціях рядків все ж інакше.

Використовуючи наш веб-сайт, ви визнаєте, що прочитали та зрозуміли наші Політику щодо файлів cookie та Політику конфіденційності.
Licensed under cc by-sa 3.0 with attribution required.