11

我一直在一些项目中使用 C 来获得硕士学位,但从未使用它构建生产软件。(.NET 和 Javascript 是我的生计。)显然,在 C 语言中需要free()记忆malloc()是至关重要的。如果您可以在一个例程中同时完成这两项工作,那就太好了。但是随着程序的增长和结构的加深,跟踪什么在malloc哪里被释放以及什么是适合释放的变得越来越难。

我在互联网上环顾四周,只发现了一些通用的建议。我怀疑你们中的一些长期 C 编码员已经提出了自己的模式和实践来简化这个过程并将邪恶放在你面前。

那么:您建议如何构建您的 C 程序以防止动态分配成为内存泄漏?

4

5 回答 5

8

按合同设计。确保每个函数注释都明确说明了它的内存卫生——也就是说,它是否是 malloc 以及它是否负责释放分配的内容,以及它是否拥有传入的任何内容的所有权。并且与你的函数保持一致。

例如,您的头文件可能包含以下内容:

/* 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是一个类似的工具,但功能较少。

于 2010-05-12T09:43:09.963 回答
5

大型项目经常使用“池”技术:在这种情况下,每个分配都与一个池相关联,并在该池时自动释放。如果您可以使用单个临时池进行一些复杂的处理,这真的很方便,然后可以在完成后一举释放。子池通常是可能的;你经常会看到这样的模式:

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

字符串操作使事情变得容易得多。字符串函数将接受一个池参数,它们将在其中分配它们的返回值,这也将是返回值。

缺点是:

  • 对象的分配生命周期可能会更长一些,因为您必须等待池被清除或释放。
  • 您最终将一个额外的池参数传递给函数(在某个地方让它们进行所需的任何分配)。
于 2010-05-12T10:59:50.607 回答
4

这不会是万无一失的(但这可能是 C 语言所期望的),并且可能很难处理大量现有代码,但如果您清楚地记录您的代码并始终准确说明谁拥有分配的内存以及谁负责,这会有所帮助释放它(以及使用什么分配器/释放器)。此外,不要害怕使用goto对非平凡的资源分配功能强制执行单入口/单出口习语。

于 2010-05-12T09:35:56.750 回答
3

我发现Valgrind对保持我的内存管理健康有很大帮助。它会告诉您在哪里访问尚未分配的内存,以及您忘记在哪里释放内存(以及一大堆事情)。

在 C 中还有更高级别的内存管理方法,例如我使用内存池(例如,参见Apache APR)。

于 2010-05-12T08:10:59.927 回答
2

抽象出每种类型的分配器和释放器。给定类型定义

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*函数的调用,以便我传递正确的类型。

于 2010-05-12T15:12:29.923 回答