Укрощение маллока / свободного зверя - советы и рекомендации - PullRequest
11 голосов
/ 12 мая 2010

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

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

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

Ответы [ 5 ]

8 голосов
/ 12 мая 2010

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

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

/* 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 - аналогичный инструмент, хотя и менее функциональный.

5 голосов
/ 12 мая 2010

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

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 */
    }
}

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

Недостатки:

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

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

3 голосов
/ 12 мая 2010

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

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

2 голосов
/ 12 мая 2010

Абстрагируйте распределители и деаллокаторы для каждого типа. Дано определение типа

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*, так что я передаю нужные типы.

...