Укрощение 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*
, поэтому я передаю правильные типы.