[本文经过轻微编辑后发布。]
新闻组:comp.lang.c
发件人:scs@eskimo.com (Steve Summit)
主题:回复:新手:函数应该使用 malloc 还是固定字符串?
Message-ID: <CrHu2G.D3B@eskimo.com> [已修订]
References: <2tmtoi$1q4@hk.super.net>
日期:1994 年 6 月 16 日星期四 14:11:01 GMT
在文章 <2tmtoi$1q4@hk.super.net> 中,Rodney Haywood 写道:
> 我正在编写一个将数字转换为单词字符串的函数,
> 通常用于支票打印程序。字符串的长度
> 直到函数解析后才知道,并且可能
> 非常长,123 个字符。我不确定是传递预定义的
> 字符串给函数让它将结果放入其中,还是让
> 函数返回一个指向它用 malloc() 分配的字符串的指针malloc().
>
> 我可以看到两种方式的优缺点。使用malloc会给
> 调用者增加更多工作,因为他们需要free()> 内存,每次使用完毕后都要释放,否则如果他们调用 1000
> 次,它将继续使用不再需要的内存。另一
> 种方法是调用者可以创建一个缓冲区来接收字符串,并在
> 需要保留它时自己进行内存分配。
> 如果所有都是固定长度字符串,可能会浪费很多
> 空间。
这是一个很好的问题,您已经确定了两个主要的权衡。其他人也提到了其他方面;我将提几个对我来说很重要的方面。
在设计一个必须返回字符串的例程时,我最担心的是调用者的便利性,特别是如果该例程将被大量使用。需要问的问题是:
调用者传递缓冲区会有多麻烦?
上述片段为
extern char dollarformat(); printf("Pay to: %-40s $%.2f\n", payee, amount); printf("%-50s\n", dollarformat(amount));
dollarformat()的返回缓冲区分配设定了场景,并忽略了它(它也以美元为中心;抱歉)。让我们看看使用各种分配策略调用它的外观:如果调用者必须传递缓冲区,我们有(称之为“方法 A”)
如果例程返回一个
char amountbuf[51]; printf("%-50s\n", dollarformat(amount, amountbuf, 51));
malloc'edmalloc指针,我们有(“方法 B”)
char *amountret; amountret = dollarformat(amount); printf("%-50s\n", amountret); free(amountret);
或者,如果我们不介意一些简化,
printf("%-50s\n", amountret = dollarformat(amount)); free(amountret);
最后,如果例程返回臭名昭著的“指向静态缓冲区的指针,该缓冲区会在每次调用时被覆盖”,我们可以简单地使用(“方法 C”)
printf("%-50s\n", dollarformat(amount));(对于第三种技术,如果您不熟悉它,dollarformat 的实现看起来类似于
char * dollarformat(double amount) { static char retbuf[RETBUFSIZE]; /* ... format amount into retbuf ... */ return retbuf; }
,其中 retbuf 的静态声明至关重要,但有时会被忽略。)
返回指向静态缓冲区的指针的函数(即使用方法 C)有一个令人讨厌的特性,那就是您不能连续调用两次并同时使用两个返回值,这可能导致难以发现的错误。例如,使用第三种实现,您不能这样做:
char *p1 = dollarformat(amount1); char *p2 = dollarformat(amount2); printf("%s %s\n", p1, p2);或
printf("%s %s\n", dollarformat(amount1), dollarformat(amount2));然而,如果调用者不太可能需要同时处理多个值,并且调用者不介意在确实需要时进行显式复制,那么静态返回值(方法 C)会非常方便,因为调用者永远(嗯,几乎永远)不必担心缓冲区分配。这就是为什么这项技术在传统的 C 和 Unix 运行时库中很受欢迎(例如ctime(), getpwuid()),尽管我确定这是那些不喜欢 C 和 Unix 的人非常头疼的事情之一。
请注意,虽然静态返回缓冲区(方法 C)通常在知道返回缓冲区大小(或上限)时使用,但函数还可以保留一个指向动态分配内存区域的静态指针,该区域会(使用realloc)根据任何单次调用的需要进行增长。(它可能会使用第二个静态变量来跟踪缓冲区的当前大小。)
如果调用者希望同时处理多个返回值,并且函数要求调用者传递缓冲区(方法 A),调用者只需记住传递不同的缓冲区。
char amountbuf1[51], amountbuf2[51]; printf("%s %s", dollarformat(amount1, amountbuf1, 51), dollarformat(amount2, amountbuf2, 51));或
(void)dollarformat(amount1, amountbuf1, 51); (void)dollarformat(amount2, amountbuf2, 51); printf("%s %s", amountbuf1, amountbuf2);
请注意,当调用者传递缓冲区时,它可以是静态分配的,也可以是动态分配的;函数不在乎。
另请注意,接受调用者提供的缓冲区的例程必须始终允许指定缓冲区的大小,以便例程能够保证不溢出。请记住gets()与fgets()!
最后,请注意,接受调用者提供缓冲区的例程通常使用诸如“例程返回其第一个参数”(或在本例中是第二个参数)之类的词语来描述,乍一看这听起来很奇怪(如果调用者知道,为什么要返回它?),但这样可以将字符串传递给其他例程(在本例中是 printf)变得非常容易。
最后,当函数返回指向动态分配内存(方法 B)的指针时,调用者可以非常轻松地同时处理多个值。但是,要保留返回值的句柄以便释放它可能很麻烦。明显的调用,例如
printf("%s\n", dollarprint(amount));是赤裸裸的(尽管不一定明显)内存泄漏。
其他人提到了效率,包括缓冲区空间和分配开销。在字符串长度任意的情况下,固定长度数组明显逊色:要么太小而溢出,要么太大而浪费空间。
然而,如果能确定字符串大小的上限且该上限为几百个字符或更少,则使用固定大小的数组不会浪费太多(除非同时分配了数百个数组),并且它们的便利性是一个明显的优势。
为了避免固定大小数组的危险,有经验的人通常会推荐动态分配,尽管必须承认,这会强制您显式管理分配,并且内存泄漏和悬空指针(最近有人指出它们是相反的——谁观察到的,真不错!)的问题可能非常令人生畏;它们可能是学习 C 时最大的障碍,并且它们会导致持续的错误背景,其中一些错误非常微妙,即使在经验最丰富的程序员编写的最复杂的程序中也从未被发现。
然而,在大多数情况下,malloc/free开销不成问题。(当然也有例外,但这并不意味着每个程序都必须避免malloc,或编写自己的,或使用复杂的存储管理包装器。)
最终,如果之前的讨论没有说清楚,在 C 中,没有一种最佳的方法可以从函数返回字符串(或其他聚合体),也没有一个绝对的理由总是优先选择固定大小或动态分配的数组。通常,对于出现的任何特定情况,您都可以找到一个好的解决方案,但最好稍微考虑一下,以确保您做出一个好的选择。
上面三种返回值方法(A、B 和 C)并非穷尽所有可能性。一些例程(getcwd、许多 GNU 例程)使用方法 A 和 B 的组合,当调用者传递 null 指针而不是缓冲区时,它们会返回指向malloc'ed内存的指针。对概念不清楚的人偶尔会尝试通过临时文件将字符串从函数返回给调用者,但我们不必多谈这种方法。最后,方法 C 有一个巧妙的扩展,显然已被多人独立发现,它返回指向静态缓冲区的指针,并允许调用者同时使用最多 N(但不是 N+1)个返回值,但这可能是另一篇文章的主题。
Steve Summit