Укрощение malloc/free beast - советы и рекомендации

Я использую C для некоторых проектов для получения степени магистра, но с ним никогда не создавал программное обеспечение для производства. (.NET и Javascript - мой хлеб и масло). Очевидно, что потребность в free() памяти, которую вы malloc() критична в C. Это прекрасно, хорошо и хорошо, если вы можете делать как в одной рутине. Но по мере того, как программы растут, а структуры углубляются, отслеживание того, что было malloc 'd, где и что подходит для свободы, становится все труднее и труднее.

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

Итак: как вы рекомендуете структурировать свои C-программы, чтобы сохранить динамические распределения от утечек памяти?

Ответ 1

Дизайн по контракту. Удостоверьтесь, что в каждом комментарии функции явно указано о ее гигиене памяти - то есть, является ли она mallocs и чья ответственность заключается в том, чтобы освободить то, что было выделено, и принимает ли она участие в чем-либо, переданном. И БУДЬТЕ СООТВЕТСТВУЮТ с вашими функциями.

Например, ваш файл заголовка может содержать что-то вроде:

/* Sets up a new FooBar context with the given frobnication level.
 * The new context will be allocated and stored in *rv; 
 * call destroy_foobar to clean it up. 
 * Returns 0 for success, or a negative errno value if something went wrong. */
int create_foobar(struct foobar** rv, int frobnication_level);

/* Tidies up and tears down a FooBar context. ctx will be zeroed and freed. */
void destroy_foobar(struct foobar* ctx);

Я сердечно одобряю советы использовать Valgrind, это действительно фантастический инструмент для отслеживания утечек памяти и неправильного доступа к памяти. Если вы не работаете в Linux, то Electric Fence - это аналогичный инструмент, хотя и менее функциональный.

Ответ 2

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

void process_all_items(void *items, int num_items, pool *p)
{
    pool *sp = allocate_subpool(p);
    int i;

    for (i = 0; i < num_items; i++)
    {
        // perform lots of work using sp

        clear_pool(sp);  /* Clear the subpool for each iteration */
    }
}

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

Недостатки:

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

Ответ 3

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

Ответ 4

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

Существуют также способы более высокого уровня управления памятью на C, например, я использую пулы памяти (например, Apache APR).

Ответ 5

Извлеките распределители и освободители для каждого типа. Учитывая определение типа

typedef struct foo
{
  int x;
  double y;
  char *z;
} Foo;

создать функцию распределения

Foo *createFoo(int x, double y, char *z)
{
  Foo *newFoo = NULL;
  char *zcpy = copyStr(z);

  if (zcpy)
  {
    newFoo = malloc(sizeof *newFoo);
    if (newFoo)
    {
      newFoo->x = x;
      newFoo->y = y;
      newFoo->z = zcpy;
    }
  }
  return newFoo;
}

функция копирования

Foo *copyFoo(Foo f)
{
  Foo *newFoo = createFoo(f.x, f.y, f.z);
  return newFoo;
}

и функция деллалокатора

void destroyFoo(Foo **f)
{
  deleteStr(&((*f)->z));
  free(*f);
  *f = NULL;
}

Обратите внимание, что createFoo() в свою очередь вызывает функцию copyStr(), которая отвечает за выделение памяти и копирование содержимого строки. Заметим также, что если copyStr() не работает и возвращает NULL, то newFoo не будет пытаться выделить память и вернуть NULL. Аналогично, destroyFoo() вызовет функцию для удаления памяти для z перед тем, как освободить остальную часть структуры. Наконец, destroyFoo() устанавливает значение f в NULL.

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

typedef struct bar
{
  Foo *f;
  Bletch *b;
} Bar;

Bar *createBar(Foo f, Bletch b)
{
  Bar *newBar = NULL;
  Foo *fcpy = copyFoo(f);
  Bletch *bcpy = copyBar(b);

  if (fcpy && bcpy)
  {
    newBar = malloc(sizeof *newBar);
    if (newBar)
    {
      newBar->f = fcpy;
      newBar->b = bcpy;
    }
  }
  else
  {
    free(fcpy);
    free(bcpy);
  }

  return newBar;
}

Bar *copyBar(Bar b)
{
  Bar *newBar = createBar(b.f, b.b);
  return newBar;
}

void destroyBar(Bar **b)
{
  destroyFoo(&((*b)->f));
  destroyBletch(&((*b)->b));
  free(*b);
  *b = NULL;
}

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

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

изменить

Изменены вызовы функций delete*, поэтому я передаю правильные типы.