2

我编写了一个简单的库文件,其中包含一个从任意大小的文件中读取行的函数。该函数通过传入堆栈分配的缓冲区和大小来调用,但如果行太大,则初始化一个特殊的堆分配缓冲区并用于传回更大的行。

这个堆分配的缓冲区是函数范围的并声明为静态的,当然在开始时初始化为 NULL。我在函数开头写了一些检查,检查堆缓冲区是否为非空;如果是这种情况,则上一行读取的内容太长。自然地,我释放了堆缓冲区并将其设置回 NULL,认为下一次读取可能只需要填充堆栈分配的缓冲区(即使在我们的应用程序中,应该很少看到超过 1MB 的行!)。

通过仔细阅读和运行一些测试,我已经仔细阅读了代码并对其进行了相当彻底的测试。我有理由相信保持以下不变量:

  • 如果只需要堆栈缓冲区,则函数返回时堆缓冲区将为空(并且不会泄漏任何内存)。
  • 如果堆缓冲区不为空,因为它是需要的,它将在下一个函数调用时被释放(如果需要,可能在下一行中重用)。

但是我想到了一个潜在的问题:如果文件中的最后一行太长,那么由于该函数可能不会再次调用,我不确定我有什么方法可以释放堆缓冲区——它是函数毕竟,范围很广。

所以我的问题是,如何在函数范围的静态指针中释放动态分配的内存,最好不要再次调用该函数?(理想情况下也不要让它成为全局变量!)

可根据要求提供代码。(抱歉,我现在无法访问。而且我希望这个问题足够笼统且解释清楚,因此不需要它,但无论如何,请随时消除我的这个想法!)


编辑:我觉得我应该添加一些关于函数用法的注释。

这个特定的函数以从文件中连续读取行的形式使用,然后立即复制到 POD 结构中,每个结构一行。这些是在读取文件时在堆上创建的,并且每个结构都有一个 char 指针,其中包含文件中的一行(清理后的版本)。为了使这些持续存在,必须进行复制。(这是许多答案中提出的主要反驳之一——哦,不,需要复制这条线,哦,亲爱的我)。

至于多线程,正如我所说,这是为了串行使用而设计的。不,它不是线程安全的,但我不在乎。

不过,感谢您的众多回复!当我有时间时,我会更彻底地阅读它们。目前,我倾向于传递一个额外的指针或重新设计函数,以便在fgets显示 EOF 时,我可能只是在那里构建释放逻辑,希望用户不必担心它。

4

8 回答 8

3

如果可以更改功能,我建议更改功能接口本身。我知道您已经花了很多时间调试和测试它,但是您当前的实现存在一些问题:

  • 它不是线程安全的,
  • 用户无法控制数据,因此如果他以后需要它,他必须复制它,很可能在将要被malloc()编辑的缓冲区中,从而抵消您malloc()在函数中选择性使用的任何优势,
  • 最重要的是,正如您所发现的,用户必须在最后一行执行特殊操作。

您的用户不应该担心您的功能的实现怪异,他们应该能够“只使用它”。

除非您出于教育目的这样做,否则我建议您查看此页面,该页面具有“从流中读取任意长行”的一种实现,并链接到其他此类实现(每个实现与其他实现略有不同,所以你应该能够找到你喜欢的)。

根据您的编辑,MT 安全不是必需的,并且总是会发生副本。因此,最明显的设计是以下两者之一:

  • 让用户提供 a char **,它指向你的函数将分配的缓冲区,使用malloc()and的组合realloc()(如果需要)。完成后是用户free()对它的责任。这样,用户不必再次复制数据,因为他可以将指针传递到数据的最终目的地。
  • 返回一个char *由你的函数分配的。同样,这是用户对它的责任free()

两者几乎是等价的。

对于您当前的实现,如果最后一行很长并且不以换行符结尾,您始终可以返回“not end of file”。然后,用户将再次调用您的函数,然后您可以释放缓冲区。就个人而言,我会更喜欢一个功能,它允许我读取任意多的行,而不是强迫我转到文件末尾。

于 2010-01-25T23:31:47.627 回答
1

除了释放动态分配的缓冲区的困难之外,还有另一个潜在的问题。它不是线程安全的。既然是库函数,那么以后总有可能在多线程环境中使用。

要求调用函数通过相关的库函数释放缓冲区可能会更好。

于 2010-01-25T23:19:48.300 回答
1

而不是函数范围,给它模块范围(即在文件范围内,但是是静态的,因此它在该文件之外不可见。添加一个释放缓冲区的小函数,并用于atexit()确保在程序退出之前调用它。替代方案,不要'不用担心——泄漏只发生一次,并在程序退出时自动释放并不是特别有害。

我觉得有必要说这个设计对我来说听起来像是灾难的秘诀。当您释放缓冲区时,几乎无法猜测它是否仍在使用中。用户(显然)必须跟踪数据的返回位置,并在(且仅当)您动态分配数据时将数据复制到新缓冲区。在多线程环境中,您需要使内部指针线程本地有任何机会正常工作。对用户来说,函数可能会做两种完全不同的事情之一——要么返回用户拥有的缓冲区,要么返回函数拥有的缓冲区,并且只能通过分配另一个缓冲区来安全使用,然后复制在再次调用函数之前将数据放入另一个缓冲区。

于 2010-01-25T23:25:15.433 回答
1

如果您使用标准技术来指示文件结束,那仍然可以。

在这种情况下发生的情况是,在读取最后一行之后,将需要再次调用您的 read-line 函数,以便它可以返回 NULL 以指示已到达文件末尾。在最后一次调用中,您可以释放缓冲区。

于 2010-01-25T23:26:16.367 回答
1

立即出现的两种选择:

  1. 将指向堆分配缓冲区的指针设为静态但文件范围。添加一个(静态)函数来检查它是否不为空,如果它不为空 free()s 它。在程序开始时调用 atexit(free_func),其中 free_func 是静态函数。您可以在其中完成一些全局设置例程(由 main() 调用)。

  2. 别担心;当您的进程退出时,操作系统会释放堆分配的内存,并且内存泄漏不是累积的,因此即使您的程序寿命很长,它也不会引发 OOM 异常(除非您有其他错误)。

我假设您的应用程序不是多线程的;在这种情况下,您根本不应该使用静态缓冲区,或者您应该使用线程本地数据。

于 2010-01-25T23:26:46.797 回答
1

您选择的界面使这是一个无法解决的问题:

  • 客户端必须不知道返回值是指向静态内存还是动态内存。

  • 返回值必须指向超过调用的内存。

  • 任何电话都可能是最后一次。

我不知道你为什么对这个泄漏感到困扰。毕竟,如果客户端读取了很长的一行,对该行做了一些处理,然后在读取下一行之前进行了大量的计算和分配,你仍然有一大块未使用的内存,阻塞了系统。如果这对您来说没问题(在回收内存之前进行任意计算),您可以承认您愿意无限期地保留死内存。

如果您无法忍受泄漏,最简单的做法是扩大接口,以便客户端可以在客户端完成内存时通知您的函数。(现在与客户端的合同说客户端拥有内存,直到它再次调用您的函数,此时所有权恢复到您的函数。)当然,更改接口意味着要么

  • 添加一个新函数,这将要求您将指针提升static为编译单元的本地,或者

  • 向现有函数添加一些参数(或重载参数),以便您进行调用,这意味着“我现在已经完成了您的记忆,但我不想要另一行”。

一个更彻底的改变是重写函数以在其整个生命周期内使用动态分配的内存,根据需要逐渐扩大块,直到它与曾经读取的最大块一样大(或者可能四舍五入到下一个二的幂)。根据实际情况,这种策略可能会比保持大的静态缓冲区消耗更少的地址空间。

In any case I'm not convinced you should be worrying about this corner case. If you think this case matters, please edit your question to show us the evidence.

于 2010-01-26T03:43:17.067 回答
0

我能想到一些技巧,尽管两者都需要将静态声明移出函数。我无法想象为什么会有问题。

使用GCC 扩展

static char *buffer;
void use_buffer(size_t n) {
    buffer = realloc(buffer, n);
}
void cleanup_buffer() __attribute__((destructor)) {
    free(buffer);
}

使用 C++,

static char *buffer;
static class buffer_guard {
    ~buffer_guard() { free(buffer); }
} my_buffer_guard;

无论如何,我真的不喜欢这个设计。在 C 中,通常调用者负责分配/释放它需要使用的内存,即使它是由被调用者填充的。

顺便说一句,与 Glibc 的非标准getline进行比较。它从不使用静态内存。

于 2010-01-25T23:24:28.900 回答
0

我只是想在马克的回答下面发表评论,但可能会感觉有点局促。尽管如此,这个答案本质上是对他的答案的评论,除了快速之外,我发现它非常好:)。

不仅您的函数不是 MT 安全的,而且即使没有线程,正确使用它的接口也很复杂。调用者必须在再次调用函数之前完成先前的结果。如果两年后这段代码还在使用,有人会挠头试图正确使用它……或者更糟的是,甚至想都没想就用错了。那个人甚至可能是你……

马克的建议(要求调用者释放缓冲区)恕我直言是最合理的。但也许您不信任malloc并且free从长远来看不会导致碎片,或者有其他原因更喜欢静态缓冲解决方案。在这种情况下,您可以为普通长度的行保留静态缓冲区,定义一个布尔标志,指示静态缓冲区当前是否繁忙,并记录以下函数(而不是free)应该使用缓冲区的地址调用,当调用者不再使用它:

char static_buffer[512];
int buffer_busy;

void free_buffer(char *p)
{
  if (p == static_buffer)
  {
     assert(buffer_busy);
     buffer_busy=0;
  }
  else free(p);
}

char *get_line(...)
{
  char *result;
  if (..short line..)
  {
     result = static_buffer;
     assert(!buffer_busy);
     buffer_busy=1;
  }
  else result = malloc(...);
  ...
  return result;
}

断言将触发的唯一情况是您之前的实现会默默地出错,并且与您现有的解决方案相比开销非常低(仅切换标志,并要求调用者free_buffer在他完成时调用,这更干净)。如果特别触发了断言get_line,则意味着您毕竟需要动态分配,因为调用者在请求另一个缓冲区时无法完成缓冲区。

注意:这仍然不是 MT 安全的。

于 2010-01-25T23:42:25.143 回答