我一直在某些项目中使用C获得硕士学位,但从未使用它构建生产软件。 (.NET和Javascript是我的面包和黄油。)显然,free()
需要malloc()
记忆你malloc
在C中是至关重要的。如果你能同时做到这一点,这很好,很好也很好常规。但是随着程序的发展和结构的深化,追踪{{1}}'哪里和什么适合自由变得越来越难。
我查看了互联网,并且只为此找到了一些通用建议。我怀疑的是,你们中的一些长期C编码员已经提出了自己的模式和实践来简化这个过程并将邪恶置于你面前。
那么:您如何建议构建C程序以防止动态分配成为内存泄漏?
答案 0 :(得分:8)
按合同设计。确保每个函数注释都明确表示它的内存卫生 - 也就是说,它是否是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是一个类似的工具,虽然功能较差。
答案 1 :(得分: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 */
}
}
使用字符串操作可以使事情变得更容易。字符串函数将采用池参数,在该参数中,它们将分配其返回值,该返回值也将是返回值。
缺点是:
答案 2 :(得分:4)
它不会万无一失(但可能是C预期的那样),并且可能很难对很多现有代码进行操作,但如果您清楚地记录代码并始终准确说明谁拥有已分配的内存,它会有所帮助谁负责释放它(以及使用什么分配器/解除分配器)。此外,不要害怕使用goto
强制执行单一条目/单一退出习惯用法来处理非平凡的资源分配函数。
答案 3 :(得分:3)
我发现Valgrind对保持记忆管理健康有很大帮助。它将告诉您在哪里访问尚未分配的内存以及您忘记释放内存的位置(以及一大堆内容)。
在C中还有更高级别的内存管理方式,例如我使用内存池(例如,请参阅Apache APR)。
答案 4 :(得分: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。
这里的关键是如果成员元素也需要内存管理,则allocator和deallocator将责任委托给其他函数。因此,当您的类型变得更复杂时,您可以重复使用这些分配器:
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*
函数的调用,以便传递正确的类型。