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

时间:2009-04-06 19:54:30

标签: c memory-management

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

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

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

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

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

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

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

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

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

12 个答案:

答案 0 :(得分:18)

令人困惑的部分原因是它本身在C中更加困难。mallocfree类似于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);
 }

答案 1 :(得分:10)

  

写这篇文章的时候,我意识到了这一点   对我来说更像一个问题   了解C的流量比   别的什么,但是一个问题   时间。

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

答案 2 :(得分:7)

不幸的是,在C语言中自动执行内存分配和释放的策略有限.C ++编译器会在幕后为您生成大量代码 - 它会跟踪堆栈中的每个变量并确保调用相应的析构函数清理堆栈时这实际上是一种相当复杂的代码生成类型,特别是当你将异常抛入混合时。

另一方面,C更简单,这就是为什么它有时被称为“高级汇编语言”。 C没有任何机制来保证在函数退出或从堆栈弹出变量时调用特定的代码位,因此您可以跟踪分配的每个内存位以及每个文件或网络打开插座并在适当的位置清理它们。在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;
}

答案 3 :(得分:6)

可悲的事实是,C并非真正用于封装所有内存管理问题。

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

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

答案 4 :(得分:2)

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

答案 5 :(得分:1)

  

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

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

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

开始使用C内存分配的好地方是:

答案 6 :(得分:1)

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

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

(按对象我的意思是你要指的是什么 - 不是c ++对象)。

答案 7 :(得分:1)

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

要考虑的其他一些想法。

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

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

  3. 查看Objective-C池等策略

  4. 如果你认为你理解C ++是如何工作的,那么在对象工厂中将构造函数行为添加到内存分配并不是那么困难,并且使用自定义构建的free可以为你提供调用析构函数的能力。对象是免费的,为您提供一些您喜欢的C ++行为

答案 8 :(得分:0)

通常的方法是

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);

答案 9 :(得分:0)

我不太确定你在问什么,但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 的性能可能变慢。有人在上面发布了一个链接。目前,快速多线程分配是关键,参见tcmalloc等。你可能不必担心这个。
  • 在现代虚拟内存架构上,除非您没有虚拟地址空间,否则malloc几乎永远不会失败。如果发生这种情况,请切换到64位;)
  • 确保您使用的系统具有边界检查,擦除自由值,泄漏跟踪和所有好东西(请参阅valgrind,win32调试堆等)。

答案 10 :(得分:0)

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

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

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

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

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

如果你经常有分配许多指针的函数,那么考虑创建和数组,它在函数的开头保存指向所有指针的指针,然后有一个函数释放它们。这将为您节省不可避免的时间&#34;我会回来并在以后对内存泄漏进行排序&#34;综合症如果你想恢复中期功能。

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

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

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

菲尔

答案 11 :(得分:-2)

你可能认为我说的很奇怪,但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<<x+y<<endl; }
};

int main()
{
   auto_ptr<foo> 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;
}