В ответе в другом месте, я нашел следующий отрывок:
В целом более хорошо в C сделать, чтобы вызывающая сторона выделила память, не вызываемого - следовательно, почему 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)
Всякий раз, когда вам нужна непрозрачная структура и вы не хотите выставлять ее внутреннюю часть в заголовочном файле. Ваш пример foo_create()
иллюстрирует это.
Другой пример - Windows API. Например, CreateWindow
дает вам HWND
. Вы понятия не имеете, как выглядит реальная структура WND
и не можете прикасаться к ее полям.
То же самое с дескрипторами объектов кернела. Например, CreateEvent
дает HANDLE
. Вы можете манипулировать им только с помощью хорошо определенного API, и закрыть его с помощью CloseHandle()
.
Re:
struct foo *a = malloc(sizeof(foo));
Для этого необходимо определить struct foo
в заголовке и, следовательно, раскрыть его внутренние части. Если вы хотите изменить его вниз по дорожке, вы рискуете взломать существующий код, который (неправильно) полагался непосредственно на его членов.
Оба подхода идеально подходят. Рассмотрим все манипуляционные функции 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 имеет некоторые преимущества:
Недостатки
Nicer как в Friendlier . Пусть звонящий выделит память, а звонящий может решить КАК выделить память. Очевидно, что они могут выбирать между стеком и кучей. Но также и между несколькими кучами в некоторых случаях. Они могут упаковать многократное выделение памяти в один вызов malloc (полезно, когда нужно распределить данные в другое адресное пространство).
В Win32 есть GlobalAlloc()
, который является ТОЛЬКО способом выделения памяти, которая может быть передана в сообщениях DDE другим приложениям. (уже никого не волнует ;)
В Win32 также есть VirtualAlloc
, который используется не очень часто, но обладает некоторыми свойствами, делающими его неоценимым для некоторых особых случаев. (вы можете изменить память с чтения-записи на чтение после ее инициализации)
Также есть CreateFileMapping/MapViewOfFile
, который получает память, подкрепленную определенным файлом - записывает в память в конце записи в файл.
Я уверен, что в Unix есть эквивалентные специальные функции выделения памяти.
Если вы выделяете память самостоятельно, вы имеете контроль над тем, как вы это делаете. Либо в стеке, стандартном malloc, либо в одном из шестнадцати менеджеров памяти, которые вы используете в своем приложении.
Если память выделяется для вас, вы не только не имеете никакого контроля над тем, как это делается, но и должны знать, как освободить память. Ну, большинство библиотек предоставили бы Вам "свободную" функцию бесплатно.
Сказав это, я все же не думаю, что есть один "более приятный" подход. То, что лучше подходит для вашего использования.
Любой подход, который вы опубликуете - это хорошая форма; первый ближе к тому, как С++ справляется с вещами, второй больше похож на Объектив-С. Главное - сбалансировать создание и разрушение внутри блока кода. Эта практика подпадает под категорию уменьшения сцепления . Плохая практика - иметь функцию, которая создает что-то и выполняет дополнительные задачи, как это делает strdup
, что затрудняет решение проблемы, если вызывающему абоненту приходится что-либо утилизировать, не обращаясь за документацией.
Основным преимуществом выделения памяти вызывающему абоненту является то, что он упрощает интерфейс, и совершенно однозначно, что вызывающий абонент владеет памятью. Как показывает ваш пример создания/уничтожения, упрощение не очень велико.
Я предпочитаю конвенцию создания/уничтожения, созданную Дэйвом Хэнсоном в C Interfaces and Implementations:
struct foo *foo_new(...); // returns result of malloc()
void foo_free(struct foo **foop); // free *foop's resources and set *foop = NULL
Вы следуете этой конвенции таким образом:
struct foo *a = foo_new();
...
foo_free(&a);
// now `a` is guaranteed to be NULL
Эта конвенция делает немного менее вероятным то, что вы оставите висячий указатель.
.Выделяя и распределяя память в одной и той же функции (или в исходном файле), вы сможете проще выявлять потенциальные утечки памяти (или убеждать себя, что их нет) без необходимости прыгать по разным местам в вашей программе. Если callee выделяет память, то неоднозначно, где должно происходить перераспределение. Напротив, если вызывающий абонент делает это, то этот код берет на себя "полную ответственность" за память.
Но, прежде всего, ключевым моментом является согласованность. Выберите один подход и придерживайтесь его.
Мое мнение таково - с этим можно справиться двумя способами:
Если вы пишете функцию, которая выделяет память, то напишите комментарий над функцией, чтобы указать, что ответственность за управление памятью лежит на программисте, т.е. явно освобождая память, тем самым передавая бремя управления памятью программисту, который будет за нее отвечать.
Или напишите функцию-обертку, которая заканчивается на _alloc
и соответствующую функцию-обертку, заканчивающуюся на _free
, таким образом, вы определяете хорошо документированный набор процедур, который облегчает программисту чтение. Простой плюс в том, что если программист невольно ввёл утечку памяти, то предупреждение есть, так как на языке Си написано 'Для каждого malloc, если его нет, должен быть соответствующий free, тогда у тебя утечка '. Программист, в свою очередь, может подсказать и сказать: "Ага... Я вызвал эту функцию обертки something_alloc
, но не вызвал something_free
". Ты понял суть? И в любом случае, программист скажет за это спасибо!
На самом деле, все дело в том, насколько хорошо определено API кода. Если вы хотите написать код для управления памятью и тем самым освободить программиста от ответственности за управление памятью, то лучше обернуть его и придать ему особое значение, как я предлагал, например, использовать подчеркивание с последующим 'alloc' и 'free'.
Это заработает вам одобрение и уважение, так как программист, который будет читать и использовать ваш код, скажет - 'Спасибо, приятель', и в итоге все будут довольны.
Надеюсь, это поможет, С наилучшими пожеланиями, Том.
Все сводится к установлению права собственности на память.
Когда проекты становятся очень большими, бывает трудно понять, куда уходит вся память.
В C++ мы часто обходим это, используя фабрику, такую как foo_create() пример. Эта фабрика знает, как настраивать объекты foo, и может легко отследить, сколько памяти она выделяет и сколько освобождается.
Хотя что-то подобное может быть сделано в C, часто мы просто следим за тем, чтобы каждый слой вашей программы очищал используемую ею память. Таким образом, обозреватель может взглянуть на код, чтобы убедиться, что каждый злоумышленник имеет соответствующий свободный. Когда выделения слишком глубоко вложены, может быстро стать непонятно, где происходит утечка памяти.
Кстати, я склоняюсь к тому, чтобы иметь инициализатор, который отделен от выделения, ради возврата значения ошибки из инициализатора. Если просто вызвать foo_create() и вернуть нулевой указатель, то непонятно, не получилось ли создать ошибку из-за нехватки памяти или по какой-то другой причине. Привычка иметь возвращаемые значения на init-функциях может сэкономить много времени на отладку.
.Я предпочитаю стиль GLib (первый, о котором вы упомянули). Для меня выбор этого стиля делает его более объектно-ориентированным. Ваши методы заботятся о создании и уничтожении структуры, так что вам не придется бороться с внутренностями структуры. Этот подход приводит к тому, что в вашем коде ошибок тоже меньше.
Пример 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!");
Наличие выделенной вызывающим абонентом памяти лучше, так как можно сохранить выделение памяти, перерабатывая старые структуры данных вручную. Это полезно в математических приложениях, когда вокруг много массивов с размером N. Помните, что выделение памяти происходит довольно медленно.
С другой стороны, если размер массива может быть определен только функцией (т.е. размер результата неизвестен), то следует выделить вызывающий абонент.
Что бы вы ни делали, используйте условности, чтобы рассказать людям о том, что произошло. Большие глупые имена типа pre_allocated_N_array
или new_result_array
(извините, я не эксперт по Си, для этого должны быть конвенции по Си) очень удобны для людей, которые используют вашу функцию, не читая документацию. Все сводится к согласованности.