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

15

如何在C语言中处理内存是最佳实践?

没有类可以为我处理构造函数/析构函数。

  • 在函数开头分配内存还是使用一个为我创建它的函数好?如何释放它们?
  • 这些是广泛的问题,不同情况下可能有所不同,但您如何处理它们?
  • 您能给出什么提示和经验教训吗?

我认为我掌握了如何在C++中处理内存,但在C中处理内存是不同的,我有点脱节。

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

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

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

编辑:感谢您的回答,但我需要重新表达自己。

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

例如,在我的类中,我动态添加和销毁类使用的内存。这样,我可以实现一种封装,我不需要知道类如何处理其内存,它只是处理它。这意味着我可以“隐藏”更低级别的内存处理,并只专注于几个“较大”的类。

12个回答

23

造成困惑的部分在于,使用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和free来分配和释放内存块:

 {  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
除非直接将一个malloc()调用作为varargs函数参数传递或使用早期版本的编译器,否则您不需要强制转换类型。这实际上会导致一些不好的习惯,因为它隐藏了缺失的#include <stdlib.h>。 - dirkgently
那是另一种风格问题,Dirk。我更喜欢显式转换,因为这是我在编写C时进行类型推理的一部分。我将"(char *) malloc"视为返回一个malloc'ed char内存块的抽象操作。 - Charlie Martin
@Charlie:那是在ANSI之前的日子。这正是为什么要引入void *的确切原因。 - dirkgently
不,这正是我的观点。void 在类型格中是⊥;您可以将其分配给任何指针。通过显式地进行转换,我正在告诉*我认为它是什么类型。 - Charlie Martin
1
我倾向于定义一个宏FREE,用于释放某个东西并将其设置为NULL。这样做会让事情变得更容易一些。 - TLW
显示剩余3条评论

12

在写这篇文章的时候,我意识到这更多是关于我理解C语言流程,而不是其他方面的问题,但一次只问一个问题。

如果你还没有阅读过K&R ,我真诚地认为你应该阅读一下。


2
为什么人们仍然建议阅读那本古老的书?当然,它在C语言历史上是非常出色的,由其作者编写,但它的最新版本已经20年了,没有涵盖ISO C99,更不用说像OO和由诸如GLib(来自GNOME)这样的库提供的智能内存管理等当前实践了。 - Juliano
7
@Juliano:我认为推荐一本老书来学习一门老语言并没有问题。《C程序设计语言》写得非常出色,人们不断地推荐它是因为它做到了很好地教授想要学习的内容。流行的实践和特定的第三方库超出了关于语言本身的书籍的范围。C99没有破坏任何重要的东西,而且它的新特性也没有被许多编译器很好地支持。如果你正在尝试学习C语言(而不是GLib或OO实践等),确实没有比这本教材更好的了。 - Steve S

10
很遗憾,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;
}

8

可悲的事实是,C语言并非旨在封装所有内存管理问题。

如果您查看像POSIX这样的高质量API,您会发现常见模式是将指针传递给函数的指针,该函数然后分配内存,然后您稍后再将其传递给销毁它的函数。

这并不一定优雅,但我认为在不模拟C中的面向对象编程的情况下,很难使其真正优雅。


3
我知道这是一个老帖子,但实际上并没有什么全面的最佳实践答案,特别是在风格方面,我认为这才是原帖作者真正想要的。因此,以下是我对C语言中内存分配的看法。请注意,我更倾向于C++。
当你声明指针时,将NULL赋值给指针通常很方便,这样你就可以知道指针是否已被分配。你也可以创建一个安全的free函数,它会释放内存并将其赋值为NULL,这样你就不必担心了。
如果你在一个C文件中分配了内存,那么你应该在同一个文件中释放它。这可能比实际需要的更加严格,但是如果你正在编写一个库,那么你绝对应该释放库中任何malloc'd的内存。这是因为在Windows上,dll与exe有不同的堆,因此在dll中malloc内存并在exe中释放它会破坏你的堆。
顺理成章,出于对称性考虑,如果你有一个返回指向已分配内存的指针的函数,那么你应该有一个函数来释放该内存。这就是为什么许多库都有一个初始化函数,它返回指向一些数据(通常转换为void *)的指针,然后有一个清除函数来释放库的资源。如果你可以在同一个函数中malloc和free,那么这是很好的,因为它使你更容易跟踪内存。
不要尝试在函数开始时分配所有内存,然后在函数结束时释放它们。这意味着如果你想在函数中途返回,你必须释放所有内存,而如果你在需要时malloc和free内存,你将有较少的指针需要释放。
如果你经常有许多函数分配指针,那么考虑创建一个数组,在函数开始时保存指向所有指针的指针,然后有一个函数释放它们。如果你想在函数中途返回,这将避免不可避免的“我会回来解决我的内存泄漏”的症候群。
工厂模式是有用的。一个工厂是一个函数,它malloc一个结构体的内存,将函数指针分配给该结构体,初始化其变量,然后返回指向它的指针。如果第一个是析构函数或特定函数的数组,那么你可以有一个通用的destroy函数,它可以调用任何结构体的析构函数,然后释放结构体的内存。你还可以通过具有不同内部和外部面向的结构体定义来隐藏类的一些内部细节。COM就是建立在这些原则之上的。
所以这些只是我在C语言中处理内存的方式。它不像C++那样优雅,但由于你要依靠人来处理它,所以有像上面那样的策略可以使事情尽可能简单。
请注意,每个规则总会有例外——这些只是我在使用C时考虑的事情。我相信其他人也有其他想法。
Phil

3

在C语言中,你需要手动进行所有的内存管理,正如你已经发现的那样。这应该不会让你感到意外。


2

您可以采取很多措施来让生活更轻松。您已经想到了为C对象创建工厂/构造函数的想法。这是一个好的开始,请继续努力。

其他一些值得考虑的想法:

  1. 不要满足于标准的malloc/free。寻找一个更好的已开源的或编写一个适合所创建对象内存使用的,此外,我们在谈论C语言,你会覆盖你的对象free超过一次,并忘记释放一些,因此,在你的malloc中建立一些调试支持。如果找不到符合需求的,编写自己的也不难。

  2. 使用多个堆。每个创建的对象类使用一个堆,如果您知道将拥有大量相关的短暂对象,则使用临时堆,这可以降低内存碎片化并根据使用情况管理内存。

  3. 查看类似Objective-C池的策略

  4. 如果您认为自己理解C++的工作原理,那么在对象工厂中添加构造函数行为并使用自定义的free就不难做到,并且在free对象时调用析构函数可以为您提供一些喜欢的C++行为


1

我“隐藏”内存分配和释放的一种方法是将其传递给自定义容器。将未分配内存的对象传递给容器。让它担心malloc,当我删除对象时,让它担心free。当然,这仅适用于在一个容器中存储一个对象的情况。如果我在各个地方都有对象引用,我将使用C语法创建构造函数和析构函数的等效方法:

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

(所谓对象,是指您指向的任何东西 - 而不是C++对象)。


1
我不知道如何隐藏它们和如何自动化事物。
C和C ++是不同的语言。现在,对自己说一百遍。大声点。
你所说的隐藏是什么意思?你所说的自动化是什么意思?你能举些例子吗?为什么需要隐藏和/或自动化?
在线开始C内存分配的好地方包括:

由于某种奇怪的原因,您的第二个链接无法使用:http://doc.cat-v.org/henry_spencer/ten-commandments - Klelky

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

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接