Когда функция C возвращает вновь выделенную память?

В ответ в другом месте я нашел следующий фрагмент:

В целом, на C лучше вызывающий абонент выделяет память, а не callee - поэтому почему strcpy является "более приятным", функции, на мой взгляд, чем strdup.

Я вижу, как это действительный шаблон, но почему его можно считать более приятным? Имеются ли преимущества для следования этой схеме? Или нет?

Пример

Недавно я написал довольно много кода, который выглядит примерно так:

struct foo *a = foo_create();
// do something with a
foo_destroy(a);

Если foo - это нечто большее, чем плоская структура, то я решил, что смогу выполнить всю свою инициализацию за один шаг. Кроме того, предположим, что структура должна находиться в куче. Почему лучше было бы сделать что-то вроде:

struct foo *a = malloc(sizeof(foo));
foo_init(a);
// do something with a
foo_destroy(a)

Ответы

Ответ 1

Всякий раз, когда вы хотите создать непрозрачную структуру и не хотите раскрывать ее внутренности в файле заголовка. Ваш пример foo_create() иллюстрирует это.

Другим примером является API Windows. Например. CreateWindow дает вам HWND. Вы не представляете, как выглядит фактическая структура WND и не может коснуться ее полей.

То же самое с обработкой объектов ядра. Например. CreateEvent дает HANDLE. Вы можете манипулировать им только с помощью четко определенного API и закрыть его с помощью CloseHandle().

Re:

struct foo *a = malloc(sizeof(foo));

Это требует, чтобы вы определили struct foo в заголовке и, следовательно, разоблачили его внутренние элементы. Если вы хотите изменить его по треку, вы рискуете нарушить существующий код, который (неправильно) полагался на его членов напрямую.

Ответ 2

Основное преимущество, заключающееся в том, что вызывающий абонент выделяет память, заключается в том, что он упрощает интерфейс, и он совершенно однозначно указывает, что вызывающий абонент владеет памятью. Как показывает пример create/destroy, упрощение не очень велико.

Я предпочитаю соглашение о создании/уничтожении, установленное Дейвом Хэнсоном в C Интерфейсы и реализации:

struct foo *foo_new(...);   // returns result of malloc()
void foo_free(struct foo **foop); // free *foop resources and set *foop = NULL

Вы следуете за соглашением, таким образом:

struct foo *a = foo_new();
...
foo_free(&a);
// now `a` is guaranteed to be NULL

Это соглашение немного снижает вероятность того, что вы оставите свисающий указатель.

Ответ 3

Любой подход к публикации - хорошая форма; первый ближе к тому, как С++ обрабатывает вещи, а последний больше похож на Objective-C. Главное - сбалансировать создание и уничтожение в кодовом блоке. Эта практика относится к категории уменьшения coupling. Какая плохая практика состоит в том, чтобы иметь функцию, которая создает что-то и выполняет дополнительные задачи, как это делает strdup, что затрудняет определение того, должен ли вызывающий абонент распоряжаться чем-либо, не обращаясь к документации.

Ответ 4

Оба подхода совершенно прекрасны. Рассмотрите все функции манипуляции FILE *, они не позволяют вам самостоятельно распределять FILE.

Если вы программируете на C, вам часто нужен полный контроль над всем. Это означает, что дать вызывающему лицу контроль над тем, где и как распределить структуру, - это хорошо. Лично я обычно создаю 2 функции для инициализации, если мне не нужна непрозрачная структура

int foo_init(struct foo *f); // allows the caller to allocate 'f' 
                             //however is suitable
struct foo * new_foo(void);  // mallocs and calls foo_init, for convenience.

И при необходимости соответствующий

 void foo_free(struct foo *f );   //frees and destroys 'f'
 void foo_destroy(struct foo *f); //cleans up whatever internal stuff 'f' has,
                                  // but does not free 'f' itself

В тех случаях, когда вы хотите, чтобы вызывающий объект обрабатывал структуру как непрозрачную, вы будете предоставлять только структуру foo * new_foo (void); Невыражение реализации структуры foo имеет некоторые преимущества:

  • Caller не разрешается совать или выполнять потенциально опасные ярлыки, обратившись непосредственно к членам.
  • Вы можете изменить struct foo, не нарушая существующие двоичные файлы (вы не нарушаете ABI), может быть большой проблемой, если вы реализуете библиотеку.
  • В вашем общедоступном файле заголовка не нужно раскрывать реализацию и другие необходимые заголовки для struct foo

Недостатки

  • Вызывающий не имеет контроля над распределением структуры foo
  • У вас будут накладные расходы, связанные с необходимостью манипулирования функциями struct foo через функции

Ответ 5

Выделяя и освобождая память в одной и той же функции (или исходном файле), вам будет легче определить потенциальные утечки памяти (или убедить себя, что их нет), не перескакивая в разные места в вашей программе. Если вызывающий абонент выделяет память, это неоднозначно, где должно происходить освобождение. В отличие от этого, когда вызывающий абонент делает это, этот код берет "полную ответственность" за память.

Прежде всего, однако, согласованность - это ключ. Выберите один подход и придерживайтесь его.

Ответ 6

Чем больше, тем лучше. Позвольте вызывающему абоненту выделить память, и вызывающий может решить КАК выделить память. Они могут выбирать между стеком и кучей. Но и между несколькими кучами в некоторых случаях. Они могут упаковывать множественные распределения в один вызов malloc (полезно, когда вам нужно маршалировать данные в другое адресное пространство).

В Win32 существует GlobalAlloc(), который является ТОЛЬКО способом выделения памяти, которая может передаваться в сообщениях DDE другим приложениям. (не то, чтобы кто-то больше заботился;)

в Win32 мы также имеем VirtualAlloc, который не используется очень часто, но имеет некоторые свойства, которые делают его неоценимым для некоторых особых случаев. (вы можете изменить память с чтения-записи на чтение только после ее инициализации).

Там также CreateFileMapping/MapViewOfFile, который получает вам память, которая поддерживается конкретным файлом - записывает в конец памяти запись в файл.

Я уверен, что Unix имеет эквивалентные специализированные функции выделения памяти.

Ответ 7

Мое мнение об этом - есть два способа справиться с этим:

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

В качестве альтернативы, напишите функцию-оболочку примерно так, заканчивая на _alloc и соответствующую функцию-оболочку, заканчивающуюся на _free, таким образом, вы определяете хорошо документированный набор процедур, который упрощает программирование читать.

Простое преимущество заключается в следующем: если программист непреднамеренно ввел утечку памяти, предупреждение появляется там, где пословица на C такова: "Для каждого malloc должен быть соответствующий бесплатный, если у вас его нет, то у вас есть утечка". Программист, в свою очередь, может понять и сказать "Ага.. Я назвал эту функцию-обертку something_alloc, но не назвал something_free". Вы получаете суть? И вообще, программист поблагодарит вас за это!

Действительно, дело не в том, насколько хорошо определен код API. Если вы хотите написать код для управления памятью и, следовательно, освободить программиста от ответственности за управление памятью, лучше всего обернуть его и придавать ему особое значение, поскольку я предложил использовать подчеркивание, за которым следуют "alloc" и "free.

Это принесет вам высокую оценку и уважение, поскольку программист, который будет читать и используя ваш код, скажет: "Спасибо, бутон", и конечный результат будет всем счастливым.

Надеюсь, это поможет, С наилучшими пожеланиями, Том.

Ответ 8

Все сводится к установлению права собственности на память.

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

В С++ мы часто обходим это, используя factory, например, пример foo_create(). Этот factory знает, как настроить объекты foo, и может легко отслеживать, сколько памяти он выделяет и сколько он освобождает.

Хотя что-то подобное можно сделать на C, часто мы просто убеждаемся, что каждый слой вашей программы очищает память, которую он использует. Таким образом, рецензент может заглянуть в код, чтобы убедиться, что каждый malloc имеет свободный доступ. Когда распределения слишком глубоко вложены, он может быстро стать неясным, где происходит утечка памяти.

Кстати, я склоняюсь к тому, что инициализатор, который отделен от выделения, возвращает значение ошибки из инициализатора. Если вы просто вызываете foo_create() и возвращаете нулевой указатель, неясно, произошло ли создание из-за нехватки памяти или по какой-либо другой причине. Получение привычки иметь возвращаемые значения в функциях init может сэкономить вам много времени отладки.

Ответ 9

Я предпочитаю стиль GLib (первый вы упомянули). Для меня выбор этого стиля делает его более ориентированным на объект. Ваши методы заботятся о создании и уничтожении структуры, поэтому вам не придется сражаться с внутренними структурами. Этот aproach приводит к тому, что ваш код также имеет меньше ошибок.

Пример GString:

GString *s;
s = g_string_new();
// Not in this case, but sometimes you can
// find this:
if (s == NULL)
    printf("Error creating the object!");

Ответ 10

Лучше выделять память вызывающего абонента, потому что вы можете экономить выделение памяти, вручную перерабатывая старые структуры данных. Это полезно в приложениях с математикой, когда у вас много массивных размеров N. Помните, что распределение памяти происходит довольно медленно.

С другой стороны, если размер массива может быть определен только функцией (т.е. размер результата неизвестен), то вызывающий должен выделить.

Что бы вы ни делали, используйте условные обозначения, чтобы рассказать людям, что произошло. Большие глупые имена, такие как pre_allocated_N_array или new_result_array (извините, я не эксперт C, для этого должны быть C-соглашения) очень удобны для людей, которые используют вашу функцию, не читая документы. Все сводится к последовательности.

Ответ 11

Если вы сами распределяете свою память, у вас есть контроль над тем, как вы это делаете. Либо в стеке, стандартный malloc, либо один из шестнадцати менеджеров памяти, которые вы используете в своем приложении.

Если для вас выделена память, вы не только не можете контролировать, как это делается, но вы должны знать, как освободить память. Ну, большинство библиотек предоставят вам бесплатную функцию.

Сказав, что я до сих пор не думаю, что есть один "более приятный" подход. Что лучше подходит для вас.