Как в C ++ происходит высвобождение памяти для больших возвращаемых значений (например, строки)?

Скажем, у меня есть функция A () в C ++, и она вызывает другую функцию B (). B () открывает файл и читает длинную строку, а затем возвращает эту строку в A (). Затем A () использует эту строку как входной параметр для C (). C () ожидает ссылку на строку.

Для этого я вижу два решения:

  1. B () получает ссылку на строку как входной параметр (назовите его str). B () редактирует str, поэтому A () получает строку. Освобождение выделенной памяти является обязанностью A (). Когда A () выполняется с использованием строки, она возвращает память системе. Мне это ясно.

  2. B () возвращает строку. Меня смущает этот вариант. Кто тогда освобождает выделенную память? B () создает локальную строку, поэтому не может вернуть ее адрес. B () должен возвращать параметр по значению. Насколько мне известно, благодаря оптимизации возвращаемого значения не создается фактическая копия строки. В фоновом режиме параметр по-прежнему передается по ссылке. Когда я пишу псевдо что-то вроде этого:

A() {
  C(B(filename))
}

Что будет, чем с выделенной памятью? B () получает имя файла, открывает файл, выделяет блок памяти для строки, которую он читает из файла, а затем возвращает эту строку. Это конец жизненного цикла B (). A () не имеет переменной, определенной для адресации этой строки. Кто отдает обратно этот блок памяти? И можно ли рассчитывать на RVO? Нормально ли, чтобы производительность возвращалась по значению, чем? Есть ли это в каждом компиляторе?


person lulijeta    schedule 29.01.2015    source источник
comment
Возврат по значению: прочтите об оптимизации возвращаемого значения (копирование) и rvalue-ссылках.   -  person    schedule 29.01.2015
comment
@ DieterLücking RVO неоднократно упоминается в вопросе, поэтому очевидно, что OP об этом знает.   -  person    schedule 29.01.2015
comment
Вам не нужно беспокоиться о том, поддерживает ли компилятор RVO. Начиная с C ++ 11, семантика перемещения (предоставляемая ссылками rvalue) гарантирует, что возврат строк (или любого объекта с конструктором перемещения) очень эффективен.   -  person Ferruccio    schedule 29.01.2015
comment
@Ferrucio Tnahk вы, RVO часть теперь ясна. Как насчет освобождения памяти? В Java я бы зависел от сборщика мусора и говорил, что если никто не использует эти данные, сборщик мусора позаботится об этом. Но что происходит в C ++?   -  person lulijeta    schedule 29.01.2015
comment
@lulijeta: это ответственность рассматриваемого объекта.   -  person greyfade    schedule 29.01.2015
comment
@lulijeta: В C ++ преобладающей парадигмой является RAII. std::string следует за этим, поэтому память освобождается, когда std::string объект, который ее использовал, выходит за пределы области видимости (или иным образом уничтожается).   -  person Jason R    schedule 29.01.2015
comment
Объекты с автоматической продолжительностью хранения (т.е. объекты, не созданные с помощью new) автоматически уничтожаются, а память, необходимая для их хранения, управляется компилятором.   -  person Benjamin Lindley    schedule 29.01.2015
comment
@greyfade, что, если я не занимаюсь программированием в объектно-ориентированном смысле? C ++ не является чисто объектно-ориентированным, как Java. @JasonR Итак, вы имеете в виду, что в этом примере, когда A () выходит за пределы области видимости, выделенный блок памяти освобождается. А если A () - основная функция, то это просто плохая практика программирования. @BenjaminLindley Что, если я создам char* и выделю для него память в B () с помощью new? Потом возвращаю в виде строки? Извините за вопрос о краях, но я не собираюсь решать что-то на данный момент, я пытаюсь понять принцип   -  person lulijeta    schedule 29.01.2015
comment
Это не крайний случай. ваш string B(char *) на самом деле будет void B(string &, char *). string выделяется в стеке вызывающего в соответствии с моим ответом, функция конструирует его с помощью string::string(char *) и возвращает. Тогда у вас, вероятно, будет утечка памяти, если вы не очистите исходный char *.   -  person Blindy    schedule 29.01.2015
comment
@lulijeta: (вернуть его как строку, я полагаю, вы имеете в виду std::string?) Сам char* обрабатывается компилятором. И строка, созданная из char* в операторе возврата, также обрабатывается компилятором. Но память, которую вы выделили с помощью new, не обрабатывается компилятором. У вас будет утечка памяти, если вы сделаете что-то вроде этого: std::string B() { char* p = new char[10]; return p; }   -  person Benjamin Lindley    schedule 29.01.2015
comment
@lulijeta: Это не имеет значения - значения в C ++ фактически являются объектами, у которых есть время жизни. Это помогает думать об этом именно так. Тот факт, что C ++ не является чистым объектно-ориентированным программированием, не имеет смысла. Тем не менее, значения, которые не являются фундаментальными типами (например, экземпляры класса std::string), имеют конструкторы и деструкторы копирования / перемещения, которые заботятся об очистке памяти, которую они используют.   -  person greyfade    schedule 29.01.2015
comment
@BenjaminLindley и @Blindy Я имею в виду std :: string, да. Насколько я понимаю, второй вариант, как я думал, вызовет утечку памяти. Потому что, если я читаю из файла, мне нужно выделить память для данных типа char* buffer = new char[size]. Тогда, поскольку мое возвращаемое значение является строкой, я верну свой буфер. Когда я возвращаю его, объем функции заканчивается, и я не могу вернуть память. Следовательно утечка памяти. Можно ли читать из файла внутри string B(string filename), а затем возвращать его как строку и при этом не вызывать утечку памяти?   -  person lulijeta    schedule 29.01.2015
comment
@lulijeta: Да, в этом случае у вас есть утечка памяти. Но это не имеет ничего общего с возвратом строки по значению. Это только потому, что вы использовали new без связанного delete. Что касается вопроса в конце вашего комментария, да. Конечно, это возможно. Но этот вопрос, вероятно, относится к отдельному посту, а не к разделу комментариев этого поста.   -  person Benjamin Lindley    schedule 29.01.2015
comment
Используйте getline(), чтобы напрямую читать из файлового потока в string и обходить глупые промежуточные объекты.   -  person Blindy    schedule 29.01.2015
comment
@BenjaminLindley Я думаю, что тогда я должен думать как C. Я мог бы, например, malloc (или выделить с помощью new) некоторую память для буфера в B () и вернуть указатель как возвращаемое значение B (), а затем освободить (или удалить) в A (). Пока я не потеряю указатель на память, которую я выделил с помощью new, я могу (и должен) освободить память. Но действительно ли это чистый код, когда я освобождаю память в другой функции?   -  person lulijeta    schedule 30.01.2015
comment
@lulijeta: Нет, не надо этого делать. Фактически, вы можете и, вероятно, должны полностью избегать ручного выделения памяти. Разумеется, в описываемой вами задаче нет необходимости в new или malloc. Как я уже сказал, задайте еще один вопрос, если хотите знать, как это сделать.   -  person Benjamin Lindley    schedule 30.01.2015


Ответы (2)


Независимо от того, используется ли RVO, - это оптимизация, причем прозрачная. Я расскажу об этом в конце, потому что это не имеет значения для механизма возврата объектов по значению.

Теперь, когда вы возвращаете объект по значению, компилятор фактически добавляет невидимый параметр в качестве указателя. Эта память выделяется в стеке вызывающего. Ваша функция делает свое дело с написанным кодом (помните, без RVO), а затем возвращает объект, который вызывает конструктор копирования для объекта вызывающего, используя локальный. Потом разрушается местный.

Кто очищает вызывающий объект? Как и любой другой объект, он следует правилам RAII: его деструктор вызывается при завершении области видимости, а указатель стека перематывается назад, когда функция завершается.

Теперь о РВО. Все, что делает RVO, - это избегает внутренних вызовов create + copy + desctructor в функции - он работает напрямую с объектом вне функции. Он вызовет на нем соответствующий конструктор (подумайте о размещении new), затем он будет использовать свои поля и функции для выполнения работы, и в конце концов ничего не останется. Он не будет уничтожен или освобожден, потому что это результат - и он уже в руках вызывающего.

Изменить: Что касается того, насколько распространен RVO, любой нормальный компилятор ПК поддерживает его. Он так же поддерживается, как и #pragma once, несмотря на чудаковатого пуриста, который всегда приходит и говорит, что это не «стандарт».

person Blindy    schedule 29.01.2015
comment
Странный пурист здесь: стандарт определяет RVO (компиляторы могут исключать копирование / перемещение возвращаемого значения) в 14882: 1999 §12.8.15 и расширяет его в 14882: 2011 §12.8.31-32, чтобы включить исключение перемещения. - person greyfade; 29.01.2015
comment
может подразумевает разрешение, а не принуждение. Может, я неправильно понимаю вашу точку зрения, в чем дело? - person Blindy; 29.01.2015
comment
Я хочу сказать, что разрешение на реализацию RVO было в стандарте с самого начала, и через 16 лет можно предположить, что большинство, если не все, компиляторы реализуют его за редкими исключениями. - person greyfade; 29.01.2015
comment
Это именно то, что я сказал. Я не понимаю, почему вы подняли этот вопрос, но мы согласны, так что ура! - person Blindy; 29.01.2015
comment
Я думаю, что заблуждение состоит в том, что ваше первоначальное замечание чудаковатого пуриста было, если я правильно понимаю, адресовано тем, кто может сказать, что #pragma once нестандартен. Но Greyfade интерпретировал это как направленное на людей, которые могли (ошибочно) сказать, что RVO не является стандартом. - person Benjamin Lindley; 29.01.2015

Предполагая, что для вашего кода используется RVO:

A() {
  C(B(filename))
}

Компилятор создает временный объект типа std::string в контексте A. По сути, он передает скрытую ссылку на этот временный объект, когда вызывает B. B записывает в указанную строку, а затем возвращает.

Затем этот временный объект передается C. Итак, окончательный код оказывается примерно эквивалентным чему-то в этом порядке:

// string B(); is turned into:
void B(string &ret) { 
    ret = "a really long string";
}

void A() { 
    {
        std::string temporary_object;

        B(temporary_object);
        C(temporary_object);
    }
}

Я добавил дополнительный блок вокруг расширения выражения, чтобы подчеркнуть тот факт, что временный объект создается, когда выражение C(B(filename)) начинает выполнение, и снова уничтожается в конце выражения.

То, что я показал выше, отличается от реального в нескольких отношениях. Например, если у вас есть класс, который не поддерживает построение по умолчанию, он все равно будет работать, даже если будет задействован RVO (где приведенный выше код требует, чтобы тип был конструктивным по умолчанию, что подходит для std::string, но может не работать так хорошо для всего остального). Если вы хотите быть немного точнее, вы, вероятно, передадите указатель на необработанную память, и B будет использовать новое размещение для создания возвращаемого значения на месте (но ничто из того, что вы можете показать в C ++, точно не дублирует то, что делает компилятор) .

person Jerry Coffin    schedule 29.01.2015