在C中处理内存分配的最佳方法?

我认为我已经很好地掌握了如何在C ++中处理内存,但在C中进行操作是不同的我有点不对劲。

在C ++中我有构造函数和析构函数,我有非常简单的new和delete,我知道如何使用RAII封装它,使用智能指针和类。

但是在CI中无法以同样的方式处理malloc和free。 我不知道如何隐藏它们以及如何自动化。 我所能想到的只是使用函数来启动和销毁我的指针。 但是我应该如何构建我的内存处理?

在写这篇文章时,我意识到这更像是一个关于我理解C流程的问题,而不是其他任何问题,但一次只能提出一个问题。

编辑 :谢谢你的答案,但我需要改写自己。

当我说我使用RAII和C ++的智能指针时,我不希望C相同,我知道它不一样。 但是我如何处理C ++中的内存分配与这些技术有关。

例如,在我的类中,我动态地添加和销毁我的类使用的内存。 这样我可以实现一种封装,我不需要知道类何时/如何/为什么类处理它的内存,它就是这样。 这意味着我可以“隐藏”较低的内存处理,只关注一些“​​更大”的类。

我想知道的是在C中处理内存的最佳做法是什么? 没有带有构造函数/析构函数的类来为我处理这个问题。 在函数的开头分配内存或使用为我创建它的函数是否合适? 我应该如何再次释放他们?

这些是广泛的问题,它们因情况而异,但您更喜欢如何处理呢? 您可以提供哪些提示和课程?

令人困惑的部分原因是它在C. malloc本质上更加困难,而free类似于newdeletemalloc分配新内存,并返回指向该内存的指针。 free使内存再次可用, 只要它是使用malloc分配的内存 。 否则,它只会产生一些内存的哈希值。 它并不关心。

malloc / free的重要一点是决定并始终保持严格的使用。 以下是一些提示:

始终检查malloc返回的指针是否为NULL

 if((p = (char *) malloc(BUFSIZ)) == NULL { /* then malloc failed do some error processing. */ } 

对于安全带和吊带安全,请在释放后将指针设置为NULL。

 free(p); p = NULL ; 

尝试malloc并尽可能在同一范围内释放一块内存:

  { char * p ; if((p = malloc(BUFSIZ)) == NULL { /* then malloc failed do some error processing. */ } /* do your work. */ /* now you're done, free the memory */ free(p); p = NULL ; /* belt-and suspenders */ } 

如果你不能,请说清楚你返回的是malloc内存,这样调用者就可以释放它。

  /* foo: do something good, returning ptr to malloc memory */ char * foo(int bar) { return (char *) malloc(bar); } 

在写这篇文章时,我意识到这更像是一个关于我理解C流程的问题,而不是其他任何问题,但一次只能提出一个问题。

老实说,如果你没有,我应该读一读K&R 。

遗憾的是,在C中自动执行内存分配和释放的策略有限.C ++编译器为您生成了大量代码 – 它跟踪堆栈中的每个变量并确保在堆栈时调用相应的析构函数被清理干净了。 这实际上是一种相当复杂的代码生成类型,尤其是当您将exception抛入混合时。

另一方面,C更简单,这就是为什么它有时被称为“高级汇编语言”。 C没有任何机制来保证在函数退出或从堆栈弹出变量时调用特定的代码位,因此您可以跟踪分配的每个内存位以及每个文件或网络打开sockets并在适当的位置清理它们。 在C中构建自动智能指针没有实用的方法。

您应该关注的一个概念是“内存池”。 基本上,尝试跟踪您分配的每个单独的内存块,您创建一个池,执行一些工作,将您分配的每个内存块放入池中,然后在完成后释放整个池。 为了减轻程序员的认知负担,你可以在这里权衡一点性能和控制,但大多数时候它是值得的。

您应该看看Apache Portable Runtime项目。 他们有一个内存池库(文档位于http://apr.apache.org/docs/apr/1.3/group__apr__pools.html )。 如果APR对于您来说太过分了,您可以使用三个函数和一个链表数据结构来实现一个非常简单的内存池。 伪代码类似于:

 struct Pool { void* memoryBlock; struct Pool *next; } struct Pool *createPool(void) { /* allocate a Pool and return it */ } void addToPool(struct Pool *pool, void *memoryBlock) { /* create a new Pool node and push it onto the list */ } void destroyPool(struct Pool *pool) { /* walk the list, free each memory block then free its node */ } 

使用池是这样的:

 int main(void) { struct Pool *pool = createPool(); /* pool is empty */ doSomething(pool); /* pool full of crap, clean it up and make a new one */ destroyPool(pool); pool = createPool(); /* new pool is empty */ doMoreStuff(pool); destroyPool(pool); return 0; } 

可悲的事实是,C并不是真的旨在封装所有这些内存管理问题。

如果你看一下相当高质量的API,比如POSIX,你会看到常见的模式是你将指针传递给指向函数的指针,然后分配内存,然后你再次将它传递给一个破坏的函数它。

它不一定优雅,但我不认为有很多方法可以让它真正优雅而不用C模拟OOP。

在C语言中,您必须手动完成所有内存管理,就像您已经发现的那样。 这应该不足为奇。

我“隐藏”内存分配和解除分配的一种方法是将其传递给自定义容器。 将容器传递给非malloced对象。 让它担心malloc,当我删除对象让它担心免费。 当然,这仅在您将对象存储在一个容器中时才有效。 如果我在所有地方都有对象引用,我将使用c语法创建等效的构造函数和析构函数方法:

  glob* newGlob(); void freeGlob(glob* g); 

(按对象我的意思是你要指的任何东西 – 不是c ++对象)。

你可以做很多事情来让你的生活更轻松。 您似乎已经想到了为C对象创建工厂/构造函数的想法。 这是一个良好的开端跟进。

其他一些想法需要考虑。

  1. 不满足于标准的malloc / free。 去寻找一个已经开源的更好的,或者编写一个适合你正在创建的对象的内存使用的那个。 另外,我们在这里谈论C,你将更多地覆盖你的对象并且忘记释放一些,所以在你的malloc中构建一些调试支持。 如果找不到符合您需求的产品,那么写自己的并不难。

  2. 使用多个堆。 如果你知道你将拥有大量与之相关的瞬态对象,那么使用你创建的每个类对象使用一个临时堆,这会使内存碎片化,并允许你根据用途管理内存。

  3. 看看Objective-C池这样的策略

  4. 如果你认为你理解C ++是如何工作的,那么在对象工厂中将构造函数行为添加到内存分配并不是那么困难,并且使用自定义构建的free可以让你能够在被释放的对象上调用析构函数你支持你喜欢的一些C ++行为

我不知道如何隐藏它们以及如何自动化。

C和C ++是不同的语言。 现在,对自己说一百次。 大声说

躲藏是什么意思? 你是什​​么意思自动化? 你能发一些例子吗? 为什么需要隐藏和/或自动化。

在线启动C内存分配的好地方是:

  • C FAQ (特别是第7章)
  • C程序员的十诫

通常的方式是

 MyType *ptr = malloc(array_size * sizeof *ptr); 

但是如果你想与c ++兼容,那就做吧

 MyType *ptr = (MyType*) malloc(array_size * sizeof *ptr); 

你也可以制作一个宏

 #define MALLOC( NUMBER, TYPE ) ( TYPE * ) malloc( NUMBER * sizeof( TYPE ) ) MyType *ptr = MALLOC(10, MyType); 

当然,没有RAII,请确保以后有一段时间

 free(ptr); 

我不太确定你在问什么,但C很简单:

 struct Foo *f0 = malloc(sizeof(*f)); // alloc uninitialized Foo struct struct Foo *f1 = calloc(1,sizeof(*f)); // alloc Foo struct cleared to all zeroes //You usually either want to clear your structs using calloc on allocation, or memset. If // you need a constructor, just write a function: Foo *Foo_Create(int a, char *b) { Foo *r = calloc(1,sizeof(*r)); r->a = a; r->b = strdup(b); return r; } Here is a simple C workflow with arrays: struct Foo **foos = NULL; int n_foos = 0; ... for(i = 0; i < n_foos; ++i) { struct Foo *f = calloc(1,sizeof(*f)); foos = realloc(foos,sizeof(*foos)*++n_foos); // foos before and after may be different foos[n_foos-1] = f; } 

如果您喜欢,可以编写宏来帮助:

 #define MALLOCP(P) calloc(1,sizeof(*P)) // calloc inits alloc'd mem to zero 

几点:

  • malloc,calloc,realloc等都使用free(),因此管理这些东西很容易。 只是一致的。
  • mallocs的性能可能很慢。 有人在上面发布了一个链接。 目前,快速multithreading分配是关键,参见tcmalloc等。 你可能不必担心这个。
  • 在现代虚拟内存架构上,除非您没有虚拟地址空间,否则malloc几乎不会失败。 如果发生这种情况,请切换到64位;)
  • 确保你使用的系统有边界检查,擦除自由值,泄漏跟踪和所有好东西(参见valgrind,win32调试堆等)

我知道这是一篇很老的post,但是在风格方面对于最佳实践并没有太多全面的答案,我认为这是op真正想要的,所以这是我在C中对内存分配的看法。注意我我更多的是一个C ++人,所以我的想法来自于这种态度。

知道指针是否已分配通常很方便,因此在声明指针时始终为指针指定NULL。 你也可以创建一个安全的免费函数来释放内存,然后为它分配NULL,这样你就不用担心了。

如果在一个C文件中分配内存,则应将其释放到同一文件中。 这可能比需要的限制更多,但是如果你正在编写一个库,那么你绝对应该释放库中malloc中的任何内存。 这是因为在Windows上dll具有与exe不同的堆,因此在DLL中对内存进行mallocing并在exe中释放它会破坏堆。

通过扩展和为了对称,这意味着如果你有一个返回指向已分配内存的指针的函数,那么你应该有一个释放该内存的函数。 这就是为什么许多图书馆都有一个初始化函数,它返回一个指向某些数据的指针(通常被转换为void *),然后是一个清理函数,它将释放库的资源。 如果你可以在同一个函数中使用malloc和free,那么这很好,因为它可以让你轻松跟踪事物。

不要试图在函数开头分配所有内存,然后在结尾处释放它。 这只是意味着如果你想要通过函数返回部分,你必须释放所有内存,而如果你使用malloc和free memory,那么你将获得更少的免费指针。

如果你经常有分配许多指针的函数,那么考虑创建和数组,它保存指向函数开头所有指针的指针,然后有一个函数可以释放它们。 如果你想返回midfunction,这将为你节省不可避免的“我会回来并在以后排序我的内存泄漏”综合症。

工厂的概念很有用。 工厂将是一个函数,它为结构化mallocs内存,为结构分配函数指针,初始化其变量,然后返回指向它的指针。 如果第一个是析构函数或特定函数数组,那么你可以使用一个genericsdestroy函数来调用任何struct的析构函数,然后释放struct的内存。 您还可以通过对结构具有不同的向内和向外定义来隐藏类的一些内部细节。 COM建立在这些原则之上。

所以这些只是我在C中看待记忆的方式。它不像C ++那样优雅,但是当你依靠人类来处理它时,有类似上面的策略可以使事情变得尽可能简单他们。

另请注意,每个规则都有例外 – 这些只是我在使用C时所考虑的事情。我确信其他人也有其他想法。

菲尔

您可能认为我说的很奇怪,但C ++和C之间没有太大区别.C也有RAII。 RAII不仅属于C ++。

只有你应该有足够的纪律来做管理。

C ++类:

 class foo { public: ofstream ff; int x,y; foo(int _x) : x(_x),y(_x){ ff.open("log.txt"); } void bar() { ff< f(new foo); f->bar(); } 

C对象

 typedef struct FOO { FILE *ff; int x,y; } *foo_t; foo_t foo_init(int x) { foo_t p=NULL; p=malloc(sizeof(struct FOO)); // RAII if(!p) goto error_exit; p->x=x; p->y=x; p->ff=fopen("log.txt","w"); // RAII if(!p->ff) goto error_exit; return p; error_exit: // ON THROW if(p) free(p); return NULL; } void foo_close(foo_t p) { if(p) fclose(p->ff); free(p); } void foo_bar(foo_t p) { fprintf(p->ff,"%d\n",p->x+p->y); } int main() { foo_t f=foo_init(1); if(!f) return 1; foo_bar(f); foo_close(f); return 0; }