Почему вызов std::string.c_str() для функции, которая возвращает строку, не работает?

У меня есть следующий код:

std::string getString() {
    std::string str("hello");
    return str;
}

int main() {
    const char* cStr = getString().c_str();
    std::cout << cStr << std::endl; // this prints garbage
}

Я думал, что это произойдет getString() вернет копию str (getString() возвращает по значению); Таким образом, копия str останется "живым" в main() до тех пор main() возвращается. Это сделало бы cStr указать на правильное место в памяти: основной char[] или же char* (или что-то еще) копии str вернулся getString() который остается в main(),

Однако это, очевидно, не тот случай, поскольку программа выводит мусор. Итак, вопрос в том, когда str разрушен, а почему?

3 ответа

Решение

getString() вернет копию str (getString() возвращает по значению);

Это верно.

Таким образом, копия str останется "живым" в main() до тех пор main() возвращается.

Нет, возвращенная копия является временной std::string, который будет уничтожен в конце оператора, в котором он был создан, т.е. перед std::cout << cStr << std::endl;, затем cStr повисает, разыменование на нем приводит к UB, все возможно.

Вы можете скопировать возвращенный временный объект в именованную переменную или связать его с const lvalue-reference или rvalue-reference (срок действия временного будет продлен до тех пор, пока ссылка не выйдет из области видимости). Такие как:

std::string s1 = getString();    // s1 will be copy initialized from the temporary
const char* cStr1 = s1.c_str();
std::cout << cStr1 << std::endl; // safe

const std::string& s2 = getString(); // lifetime of temporary will be extended when bound to a const lvalue-reference
const char* cStr2 = s2.c_str();
std::cout << cStr2 << std::endl; // safe

std::string&& s3 = getString();  // similar with above
const char* cStr3 = s3.c_str();
std::cout << cStr3 << std::endl; // safe

Вот объяснение из [The.C++.Programming.Language.Special.Edition] 10.4.10 Временные объекты [class.temp]]:

Если временный объект не связан со ссылкой или не используется для инициализации именованного объекта, он уничтожается в конце полного выражения, в котором он был создан. Полное выражение - это выражение, которое не является подвыражением какого-либо другого выражения.

Стандартный строковый класс имеет функцию-член c_str(), которая возвращает массив символов с нулевым символом в конце в стиле C (§3.5.1, §20.4.1). Кроме того, оператор + определен для обозначения конкатенации строк. Это очень полезные средства для строк. Однако в сочетании они могут вызвать неясные проблемы. Например:

void f(string& s1, string& s2, string& s3)
{

    const char* cs = (s1 + s2).c_str();
    cout << cs ;
    if (strlen(cs=(s2+s3).c_str())<8 && cs[0]==´a´) {
        // cs used here
    }

}

Возможно, ваша первая реакция - "но не делайте этого", и я согласен. Однако такой код написан, поэтому стоит знать, как он интерпретируется.

Временный объект класса string создается для хранения s1 + s2 . Затем указатель на строку в стиле C извлекается из этого объекта. Затем - в конце выражения - временный объект удаляется. Теперь, где была выделена строка в стиле C? Вероятно, как часть временного объекта, содержащего s1 + s2, и это хранилище не гарантированно существует после уничтожения этого временного объекта. Следовательно, cs указывает на освобожденное хранилище. Операция вывода cout << cs может работать как положено, но это будет просто удача. Компилятор может обнаружить и предупредить многие варианты этой проблемы.

Проблема здесь в том, что вы возвращаете временную переменную, а поверх этой временной переменной вы выполняете функцию c_str.

Функция c_str() Возвращает указатель на массив, который содержит завершенную нулем последовательность символов (т. е. C-строку), представляющую текущее значение строкового объекта ([ http://www.cplusplus.com/reference/string/string/c_str/][1]).

В этом случае ваш указатель указывает на область памяти, которой сейчас нет.

std::string getString() {
        std::string str("hello");
        return str; // Will create Temporary object as it's return by value}

    int main() {
         const char* cStr = getString().c_str(); // Temporary object is destroyed
        std::cout << cStr << std::endl; // this prints garbage }

Решение состоит в том, чтобы правильно скопировать ваш временный объект в область памяти (путем создания локальной копии), а затем использовать c_str поверх этого объекта.

Как уже упоминалось другими, вы используете указатель на временный после того, как он уже был удален - это классический пример кучи после свободного использования.

Что я могу добавить к ответам других, так это то, что вы легко можете обнаружить такое использование с помощью дезинфицирующих средств для адресов gcc или clang.

Пример:

#include <string>
#include <iostream>

std::string get()
{
  return "hello";
}

int main()
{
  const char* c = get().c_str();
  std::cout << c << std::endl;
}

выход дезинфицирующего средства:

=================================================================
==2951==ERROR: AddressSanitizer: heap-use-after-free on address 0x60300000eff8 at pc 0x7f78e27869bb bp 0x7fffc483e670 sp 0x7fffc483de20
READ of size 6 at 0x60300000eff8 thread T0
    #0 0x7f78e27869ba in strlen (/usr/lib64/libasan.so.2+0x6d9ba)
    #1 0x39b4892ba0 in std::basic_ostream<char, std::char_traits<char> >& std::operator<< <std::char_traits<char> >(std::basic_ostream<char, std::char_traits<char> >&, char const*) (/usr/lib64/libstdc++.so.6+0x39b4892ba0)
    #2 0x400dd8 in main /tmp/tmep_string/main.cpp:12
    #3 0x39aa41ed5c in __libc_start_main (/lib64/libc.so.6+0x39aa41ed5c)
    #4 0x400c48  (/tmp/tmep_string/a.out+0x400c48)

0x60300000eff8 is located 24 bytes inside of 30-byte region [0x60300000efe0,0x60300000effe)
freed by thread T0 here:
    #0 0x7f78e27ae6ea in operator delete(void*) (/usr/lib64/libasan.so.2+0x956ea)
    #1 0x39b489d4c8 in std::basic_string<char, std::char_traits<char>, std::allocator<char> >::~basic_string() (/usr/lib64/libstdc++.so.6+0x39b489d4c8)
    #2 0x39aa41ed5c in __libc_start_main (/lib64/libc.so.6+0x39aa41ed5c)

previously allocated by thread T0 here:
    #0 0x7f78e27ae1aa in operator new(unsigned long) (/usr/lib64/libasan.so.2+0x951aa)
    #1 0x39b489c3c8 in std::string::_Rep::_S_create(unsigned long, unsigned long, std::allocator<char> const&) (/usr/lib64/libstdc++.so.6+0x39b489c3c8)
    #2 0x400c1f  (/tmp/tmep_string/a.out+0x400c1f)

SUMMARY: AddressSanitizer: heap-use-after-free ??:0 strlen
Shadow bytes around the buggy address:
  0x0c067fff9da0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9db0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dc0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9dd0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9de0: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
=>0x0c067fff9df0: fa fa fa fa fa fa fa fa fa fa fa fa fd fd fd[fd]
  0x0c067fff9e00: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e10: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e20: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e30: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
  0x0c067fff9e40: fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa fa
Shadow byte legend (one shadow byte represents 8 application bytes):
  Addressable:           00
  Partially addressable: 01 02 03 04 05 06 07 
  Heap left redzone:       fa
  Heap right redzone:      fb
  Freed heap region:       fd
  Stack left redzone:      f1
  Stack mid redzone:       f2
  Stack right redzone:     f3
  Stack partial redzone:   f4
  Stack after return:      f5
  Stack use after scope:   f8
  Global redzone:          f9
  Global init order:       f6
  Poisoned by user:        f7
  Container overflow:      fc
  Array cookie:            ac
  Intra object redzone:    bb
  ASan internal:           fe
==2951==ABORTING
Другие вопросы по тегам