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

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

В целом более хорошо в 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)
14
задан Norman Ramsey 29 December 2009 в 04:07
поделиться

11 ответов

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

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

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

Re:

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

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

.
12
ответ дан 1 December 2019 в 06:53
поделиться

Оба подхода идеально подходят. Рассмотрим все манипуляционные функции 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 arn не имеет права рыскать или выполнять потенциально опасные ярлыки путем прямого доступа к членам структуры.
  • Вы можете изменить struct foo, не ломая существующие двоичные файлы (вы не ломаете ABI), это может быть большой проблемой, если вы реализуете библиотеку.
  • Ваш публичный заголовочный файл не должен раскрывать реализацию и другие необходимые заголовки для struct foo

Недостатки

  • Звонящий не имеет никакого контроля над выделением структуры foo
  • Вам придется всегда манипулировать структурой foo через вызовы функций
4
ответ дан 1 December 2019 в 06:53
поделиться

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

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

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

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

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

3
ответ дан 1 December 2019 в 06:53
поделиться

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

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

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

0
ответ дан 1 December 2019 в 06:53
поделиться

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

.
4
ответ дан 1 December 2019 в 06:53
поделиться

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

Я предпочитаю конвенцию создания/уничтожения, созданную Дэйвом Хэнсоном в 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

Эта конвенция делает немного менее вероятным то, что вы оставите висячий указатель.

.
8
ответ дан 1 December 2019 в 06:53
поделиться

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

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

3
ответ дан 1 December 2019 в 06:53
поделиться

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

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

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

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

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

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

3
ответ дан 1 December 2019 в 06:53
поделиться

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

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

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

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

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

.
2
ответ дан 1 December 2019 в 06:53
поделиться

Я предпочитаю стиль 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!");
1
ответ дан 1 December 2019 в 06:53
поделиться

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

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

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

1
ответ дан 1 December 2019 в 06:53
поделиться
Другие вопросы по тегам:

Похожие вопросы: