C 中的分配内存管理
处理动态分配的
引言
在 C 语言编程中,动态分配内存的处理是一大痛点。您必须为每次分配的内存调用 malloc()
。但更重要的是,当不再需要该变量时,您必须 free()
它。许多 bug 都源于对这些指令的不当处理。最理想的情况是您只是忘记了 free()
。但有时,bug 会隐藏起来。例如,您覆盖了一个包含指针的变量,导致指针丢失。在 C 语言中,这还会影响程序的架构。例如,在我上一篇文章 https://codeproject.org.cn/Tips/1091150/Fast-Graph-Traversal 中,我假设存在一个包含所有节点和一个包含所有边的数组。这种在其他语言中可能无用的架构解决方案在 C 语言中是强制性的。没有办法 free()
一个图,因为根据定义,图没有根。通过我的算法,也许您可以将其分解为一棵树,但同时,您需要编写另一个程序来 free()
这个图。
预处理器
C 语言提供了一个有用的工具,称为预处理器指令。如果您想知道预处理器是做什么的,我猜您是初学者。所以我不打算给出学术定义,而是给出一个易于理解的定义。
预处理器指令是帮助程序员编写代码的工具,其附带效应是提高程序的性能。因此,如果它有助于编写代码,意味着它作用于文本形式的人类可读代码。它正是这样做的,它用另一个文本替换某个文本。它只是愚蠢地比较 string
并替换它们。为什么会提高性能?因为您也可以声明一些“函数”,这些函数称为宏,而不是使用效率较低的函数调用,它只是用您的指令替换宏。传统上,getc()
和 putc()
函数是宏,因为它们不是在处理文本中的每个字符时调用函数,而是在本地进行评估。
所以我们说这些是编写代码的工具,是为了帮助程序员,而本文的重点正是如此。帮助程序员处理变量,并在发布前检测一些错误。最好在发布时,移除所有这些指令。
我们将使用哪些指令?
#define abs(foo) (foo)>=0?(foo):(-foo)
这只是一个例子。这个宏做什么?它只是将 abs()
的每个出现都替换为相应的指令 (foo)>=0?(foo):(-foo)
,请注意没有分号。因为它只是替换文本,如果您加上分号,它也会在您的代码中添加分号,但这可能是不需要的。
__LINE__
这是一个标准的 ANSI 宏,它将单词 __LINE__
替换为相应的行号。
__FILE__
也同样是标准的,它将单词 __FILE__
替换为相应的文件名(例如:FOO.h 或 main.h)。
#undef abs(foo)
停止对已定义的宏的定义。因此,从这一点开始,您的宏将不再被替换。
有时,我总觉得有一种反对预处理器指令的战争。C++ 的纯粹主义者自豪地说,这些指令将变得多余。每次听到这样的评论,我都会问自己:“你们怎么了?” 为什么要反对一个工具?为什么要反对一个旨在帮助您的工具?因为这就是宏的范围:帮助程序员。C++ 可能想要的是提供一种替代宏副作用的方法:提高性能。我们将要做的,无法通过内联函数或常量来实现。我们需要一种愚蠢地替换文本的工具。但 C++ 的内联函数并非多余。如果我有一个大函数,我不太喜欢用宏来写。例如:fast graph traversal 文章中的 next_row()
函数,我会将其声明为内联。它会被重复n 次,但为了提高可读性,我选择将其写在主函数之外,并且它太大了,无法用宏实现。
所以每个工具都有其工作范围。我真正不理解的是 const
- 这完全不必要。
Using the Code
我们假设您已经声明了哈希表例程。如果您没有……那就不太好了。您必须搜索代码并编写一个。
那么范围是什么:我们将所有 malloc
调用重定向到我们的自定义函数,在该函数中我们实际分配了内存,但我们也添加了新分配的指针的地址名称,以及相应的文件和行标签。这样,我们就可以随时知道哪些变量已被分配。并且如果在每次调用 free()
时,我们 pop()
相同的变量,当调用退出例程时,应该没有人被分配。但如果有人在那里,我们就知道是谁在哪里分配的。并将您的调试简化为局部研究。
首先,重定向 malloc()
(同样,您必须对处理内存的每个函数都这样做:strdup()
、calloc()
、realloc()
,在后一种情况下,您必须弹出当前指针并添加新的)。
#define malloc(a) smalloc((a),__FILE__,__LINE__)
这会将您所有对 malloc
的调用替换为我们的自定义函数 smalloc()
,它接受三个参数,而无需修改您的任何代码行。当您在发布前移除此宏时,一切将恢复正常。
我们也对 free()
做了同样的处理。
#define free(a) sfree((a),__FILE__,__LINE__)
好的,现在我们必须编写我们的函数 smalloc()
和 sfree()
,但在这样做之前,我们必须将指针的地址转换为人类可读的 string
。否则,就无法计算地址的哈希值。为此,我们使用了 union
的属性。它们在相同的内存“槽”中分配两个或多个变量。如果您放入一个变量,并用另一个变量访问它,您就知道当它具有另一个 datatype
时它会如何显示……(定义非常接近)。
union pointer_translator
{
void * pointer;
unsigned value; //if int or long depends from the architecture.
}
我们构建我们的 struct
来推入 hashtable:a
。
struct file_line
{
char * file;
unsigned line;
}
瞧!我们写了函数。
//first we undefine the macros locally, or we are going to have a infinite loop:
#undef malloc(a)
#undef free(a)
void *smalloc(size_t sz, char *fl, unsigned l)
{
char st[10];
void *res=malloc(sz);
struct pointer_translator pt;
pt.pointer=res;
atoi(st,pt.value,10);
struct file_line* f_l=(struct file_line*)malloc(sizeof(struct file_line));
f_l->file=strdup(fl);
f_l->line=l;
hadd(hshtbl, st,f_l);
return res;
}
//similarly for free()
void sfree(void * p, char *fl, unsigned l)
{
char st[10];
struct file_line *fl;
struct pointer_translator pt;
pt.pointer=p;
atoi(st,pt.value,10);
fl=(struct file_line *)hpop(hshtbl,st);
if (!fl)
printf("%s %d\n",fl,l);
destroy_fl(fl);
free(p);
}
//now we redifine the macros
#define malloc(a) smalloc((a),__FILE__,__LINE__)
#define free(a) sfree((a),__FILE__,__LINE__)
请记住,也要在哈希表代码内部取消宏的定义。
这就是全部了……祝大家 C 语言编程愉快。