Где я должен освободить память в функциях?

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

Как пример, рассмотрите следующую функцию:

int foo(int param...) {
  // declare variables
  struct bar *a, *b, *c;

  // do some work
  a = bar_creator();
  b = bar_modifier(a);
  c = bar_modifier(b);

  // cleanup
  free(a);
  free(b);
  free(c);

  return 1;
}

Информация на заметку:

  • три фазы: объявление, инициирование/модификация, очистка

  • недавно выделенные структуры часто возвращаются из функций как измененные копии других объектов

  • огромное количество объектов не нужно, таким образом, использование памяти не является проблемой

В настоящий момент три раздела были относительно отличны. Это позволяет мне подходить первые и последние разделы и удостоверяться, что все составляется. Теперь интересно, мог ли лучший стиль быть должен освободить что-то, как только это не нужно. Мотивация для этого могла бы быть должна минимизировать контекст, в котором секция кода имеет смысл.

Что Ваш подход к освобождению ресурсов? Каковы преимущества данной стратегии?

править

Разрешать любой беспорядок относительно поведения функций:

/**
* returns a newly created bar
*/
struct bar *bar_creator();

/**
* takes a bar, and returns a _new_ copy of it that may have been modified.
* the original is not modified.
*/
struct bar *bar_modifier(struct bar *param);
6
задан Willi Ballenthin 28 December 2009 в 06:50
поделиться

11 ответов

Пусть компилятор почистит для вас стек?

int foo(int param...) {
  // declare variables
  struct bar a, b, c;

  // do some work
  bar_creator(/*retvalue*/&a);
  bar_modifier(a,/*retvalue*/&b);
  bar_modifier(b,/*retvalue*/&c);

  return 1;
}
0
ответ дан 8 December 2019 в 14:43
поделиться

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

.
4
ответ дан 8 December 2019 в 14:43
поделиться

необходимо учитывать две различные ситуации:

(1) объект создается в локальной области видимости, и он не нужен за пределами этой локальной области видимости. В этом случае можно выделить хранилище с помощью calloc alloca() или с помощью RAII подхода. Используя calloc alloca(). имеет большое преимущество, что вам не нужно заботиться о вызове free(), потому что выделенная память автоматически освобождается, когда остаётся локальная область видимости.

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

EDITED: используйте alloca() вместо calloc()

.
3
ответ дан 8 December 2019 в 14:43
поделиться

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

.
2
ответ дан 8 December 2019 в 14:43
поделиться

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

Я склонен иметь:

char * foo;
/* some work */
{
foo = create();
/* use foo */
destroy(foo);
}
/* some other work */
{
foo = create();
/* use foo */
destroy(foo);
}

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

Теперь, если у вас есть 2 объекта с одинаковой областью видимости (или 3 объекта, как в вашем примере), то это одно и то же:

{
foo1 = create();
foo2 = create();
foo3 = create();
/* do something */
destroy(foo1);
destroy(foo4);
destroy(foo3);
}

Но эта конкретная компоновка актуальна только тогда, когда три объекта имеют одинаковый диапазон видимости.

Я стараюсь избегать такой компоновки:

{
foo1 = create();
{
    foo2 = create();
    /* use foo2 */
}
destroy(foo1);
/* use foo2 again */
destroy(foo2);
}

Как мне кажется, она сломана.

Конечно, {} здесь только для примера, но вы также можете использовать их в реальном коде, или в vim складках, или во всем, что обозначает scope.

Когда мне нужна большая область видимости (например, глобальная или общая), я использую счетчик ссылок и механизм освобождения (замена create на retain и destroy на release), и это всегда обеспечивает мне хорошее и простое управление памятью.

.
2
ответ дан 8 December 2019 в 14:43
поделиться
  1. Обычно динамически выделяемая память имеет длительное время жизни (дольше, чем вызов функции), поэтому бессмысленно говорить о том, где внутри функции она разобрана.

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

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

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

  5. Для наглядности, если ваша функция просто модифицирует объект, то не используйте функцию вообще. Используйте процедуру. Это абсолютно ясно указывает на то, что новая память не выделяется. Другими словами, уберите свои указатели b и c - они лишние. Они могут изменять то, на что указывает a, не возвращая значения.

  6. Судя по внешнему виду вашего кода, либо вы освобождаете уже освобожденную память, либо bar_modifier вводит в заблуждение название в том смысле, что он не просто модифицирует память, на которую указывает a, а создает совершенно новую динамически выделяемую память. В этом случае их не следует называть bar_modifier, а create_SomethingElse.

2
ответ дан 8 December 2019 в 14:43
поделиться

Почему вы освобождаете его 3 раза?

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

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

Когда закончите!

Не позволяйте дешевым ценам на память способствовать ленивому программированию.

1
ответ дан 8 December 2019 в 14:43
поделиться

Нужно быть внимательным к тому, что произойдет, когда не удастся выделить память. Так как в C нет поддержки исключений, я использую goto для управления размоткой динамического состояния при ошибке. Вот тривиальная манипуляция с оригинальной функцией, демонстрирующая эту технику:

int foo(int param...) {
  // declare variables
  struct bar *a, *b, *c;

  // do some work
  a = bar_creator();
  if(a == (struct bar *) 0)
    goto err0;
  b = bar_modifier(a);
  if(b == (struct bar *) 0)
    goto err1;
  c = bar_modifier(b);
  if(c == (struct bar *) 0)
    goto err2;

  // cleanup
  free(a);
  free(b);
  free(c);

  return 1;

err2:
  free(b);
err1:
  free(a);
err0:
  return -1;
}

При использовании этой техники, я всегда хочу иметь оператор return, предшествующий метке ошибки, чтобы визуально отличить нормальный регистр возврата от случая ошибки. Теперь это предполагает, что вы используете парадигму ветра/развертывания для динамически выделяемой памяти.... То, что вы делаете, выглядит более последовательным, так что, вероятно, у меня есть кое-что более близкое к следующему:

  a = bar_creator();
  if(a == (struct bar *) 0)
    goto err0;
  /* work with a */
  b = bar_modifier(a);
  free(a);
  if(b == (struct bar *) 0)
    goto err0;
  /* work with b */
  c = bar_modifier(b);
  free(b);
  if(c == (struct bar *) 0)
    goto err0;
  /* work with c */
  free(c);

  return 1;
err0:
  return -1;
1
ответ дан 8 December 2019 в 14:43
поделиться

Для сложного кода я бы использовал структурные диаграммы, чтобы показать, как подпрограммы работают вместе, а затем для выделения/удаления я пытаюсь сделать так, чтобы они возникали примерно на одном и том же уровне на диаграммах для данного объекта.

В вашем случае у меня может возникнуть соблазн определить новую функцию, называемую bar_destroyer, вызвать ее 3 раза в конце функции foo, и сделать там free().

.
0
ответ дан 8 December 2019 в 14:43
поделиться

Рассмотрим возможность использования другого паттерна. Распределять переменные на стеке, если это разумно (используя декларации, а не аллока). Рассмотрим возможность сделать свой bar_creator инициализатором bar_initialiser, который принимает структурный бар *.

Тогда можно сделать так, чтобы ваш bar_modifier выглядел как

void bar_modifier(const struct bar * source, struct bar *dest);

Тогда не стоит так беспокоиться о выделении памяти.

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

.
0
ответ дан 8 December 2019 в 14:43
поделиться
Другие вопросы по тегам:

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