Я использовал C на некоторых проектах для степени магистра, но никогда не создавал производственное программное обеспечение с ним. (.NET и JavaScript являются моим хлебом с маслом.), Очевидно, потребность к free()
память, что Вы malloc()
очень важно в C. Это прекрасно, хорошо и хорошо, если можно сделать обоих в одной стандартной программе. Но поскольку программы растут, и структуры углубляются, отслеживание того, что было malloc
'd то, где и что соответствует свободному, становится более твердым и более твердым.
Я озирался в межсетях и только нашел несколько универсальных рекомендаций для этого. То, что я подозреваю, - то, что некоторые из Вас давние кодеры C придумали Ваши собственные шаблоны и методы, чтобы упростить этот процесс и сохранить зло перед Вами.
Так: как Вы рекомендуете структурировать свои программы C, чтобы помешать динамическим выделениям становиться утечками памяти?
Проектирование по контракту. Удостоверьтесь, что каждый комментарий функции явно описывает гигиену ее памяти - то есть, блокируется ли она и чья ответственность заключается в освобождении того, что было выделено, и принимает ли она права собственности на что-либо переданное. И БУДЬТЕ СООТВЕТСТВУЮТ своим функциям.
Например, ваш заголовочный файл может содержать что-то вроде:
/* 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 - аналогичный инструмент, хотя и менее функциональный.
В крупных проектах часто используется метод «пула»: в этом случае каждое выделение связано с пулом и автоматически освобождается, когда пул освобождается. Это действительно удобно, если вы можете выполнить сложную обработку с помощью одного временного пула, который затем можно будет освободить одним махом, когда вы закончите. Подпулы обычно возможны; часто можно встретить такой шаблон:
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 */
}
}
Это значительно упрощает работу со строками. Строковые функции будут принимать аргумент пула, в котором они будут выделять свое возвращаемое значение, которое также будет возвращаемым значением.
Недостатки:
Это не будет надежным (но этого, вероятно, следует ожидать от C), и это может быть сложно сделать с большим количеством существующего кода, но это помогает, если вы четко документируете свой код и всегда точно указываете, кому принадлежит выделенная память и кто отвечает за ее освобождение (и с каким распределителем / освободителем). Кроме того, не бойтесь использовать goto
, чтобы обеспечить соблюдение идиомы с одним входом / одним выходом для нетривиальных функций распределения ресурсов.
I Я обнаружил, что Valgrind очень помогает в поддержании здорового управления моей памятью. Он сообщит вам, где вы получаете доступ к памяти, которая не была выделена, и где вы забываете освободить память (и многое другое).
Существуют также более высокоуровневые способы управления памятью в C, например, использование пулов памяти (см., Например, Apache APR ).
Абстрагируйте аллокаторы и деаллокаторы для каждого типа. Учитывая определение типа
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% - это обеспечение того, чтобы каждый вызов аллокатора уравновешивался деаллокатором, что является действительно трудной частью.
edit
Изменены вызовы функций delete*
, чтобы я передавал правильные типы.