新闻组:comp.lang.c
发件人:scs@adam.mit.edu (Steve Summit)
主题:回复:关于风格的问题……
Message-ID: <1992Aug4.170926.2335@athena.mit.edu>
摘要:返回聚合数据
日期:1992年8月4日 星期二 17:09:26 GMT

在文章 <1992Aug3.070036.20027@cucs5.cs.cuhk.hk> 中,wong yick pong 写道:
> 我正在写一个库,其中一个函数应该返回一个字符串
> 但这个[例程倾向于返回]……(垃圾)。
> 这是因为 test 是一个局部变量吗?
> 我知道我可以使用 static char 或将 test 声明为全局变量,
> 但第一种方法会使代码不易理解,
> 而第二种方法在库中是不可行的。

我不确定使用 static 为什么会使代码更难理解;这是流行且推荐的解决方案之一。

我知道至少有九种不同的方法可以在 C 中从子例程返回聚合数据(即结构和数组,包括字符串),尽管其中一种不起作用,两种是组合,一种是开玩笑的。

  1. 局部:例程返回指向局部数组或结构的指针。这是不起作用的那种。

  2. 调用者提供:调用者传递一个指向返回缓冲区的指针及其大小。

  3. malloc:例程返回一个指针,该指针从malloc获得,指向一个动态分配的内存区域;调用者负责显式释放它(可能需要一个特殊的xxxfree()例程,如果你也提供的话,如果返回的结构包含其他必须释放的指针)。

  4. 静态:例程返回指向所谓的“每次调用都会覆盖内容的静态缓冲区”的指针。(一个额外的麻烦是,多个例程可以共享一个静态缓冲区,类似于ctimeasctime).

  5. 2 和 3 的组合:如果调用者传递一个空指针,则例程返回一个动态分配的指针。

  6. 2 和 4 的组合:如果调用者传递一个空指针,则例程返回指向静态数据的指针。

  7. 结构:只要返回的数据可以封装在一个单独的结构中,就可以返回整个结构(使用常规的按值return语句),编译器基本上会处理分配问题。(不过,这种技术对于字符串来说效果不佳,尤其是如果它们需要任意长度的话。)

  8. 多个静态:4 的推广;例程返回指向多个静态缓冲区之一的指针。这意味着调用者可以同时使用少量返回值,而无需担心释放它们。

  9. 临时文件:例程将数据写入调用者打开和读取的临时文件。(当然,如果例程选择临时文件名,那么问题就在于如何将该名称返回给调用者……)

我听说第 5 种是 GNU 项目的最爱。我非常喜欢第 8 种,尽管我并不经常使用它。

第 2 和第 3 种显然是最简单安全的选项,尽管它们对调用者来说需要更多工作。两者之间,第 2 种大多数时候都很好;第 3 种适用于调用者无法很好地估计所需返回缓冲区大小时(且所有数据都必须一次性返回),或者当调用者很可能调用 malloc 并存储多个返回值的副本时,并且动态内存分配的开销和其他轻微缺点不成问题时。

第 4 种,虽然可能令人烦恼且容易出错,但在某些情况下也很好,显然是指调用者不太可能同时需要使用多个返回值的情况。ctime()是一个很好的例子——它最常见的用途是打印当前时间,并且在任何给定时刻只有一个当前时间,调用者不太可能需要维护多个独立的返回值。

第 5 和第 6 种显然是想两全其美,有时是合适的,但可能导致不必要的复杂性和混淆(也就是说,不够成熟的程序员可能会对例程文档中关于其多种个性的讨论感到困惑)。这些技术可能应该谨慎使用,仅在真正需要极高的灵活性且主要由经验丰富的程序员使用该例程时使用。

第 7 种是一种完全足够的解决方案,它不像它本可以的那样被广泛了解和使用,因为许多人认为结构在 C 中是“二等公民”,因为根据 K&R1 它们是,尽管 Ritchie 在 K&R1 发布时的 C 编译器,以及之后的所有高质量 C 编译器,以及所有 ANSI C 编译器,都完全支持结构赋值、传递和返回。

请注意,如果我们有

	extern struct blort blortfunc();
	f()
	{
	struct blort blort1, blort2;

	blort1 = blortfunc(1);
	blort2 = blortfunc(2);

	...
	}

结构返回和赋值给两个局部结构变量,使我们能够同时操作两个聚合返回值,而不会发生冲突(即不会发生“每次调用数据都被覆盖”),并且无需手动分配或取消分配。

第 8 种是一种鲜为人知的技术,是我某天自己发明的,尽管我在网上见过一两次提及,所以其他人显然也想到了。这是一种稍微复杂的技术,在新手程序员在场的情况下使用可能会令人困惑,但在特定情况下可能非常有用。我只将其用于字符串格式化例程之类的操作,这些例程可以生成其他数据结构的打印字符串表示,并且在单次调用(例如)期间很可能被使用一次以上printf。例如,假设我有

	char *roman(int);
返回一个整数的罗马数字表示int。我可能想做这样的事情

	printf("%s + %s = %s\n", roman(12), roman(34), roman(12 + 34));
现在,如果我使用技术 2,它看起来会像

	char buf1[20], buf2[20], buf3[20];
	printf("%s + %s = %s\n", roman(12, buf1, 20), roman(34, buf2, 20),
						roman(12 + 34, buf3, 20));

,这是一团糟。如果我使用技术 3,它看起来会像

	char *p1, *p2, *p3;
	printf("%s + %s = %s\n", p1 = roman(12), p2 = roman(34),
						p3 = roman(12 + 34));
	free(p1);
	free(p2);
	free(p3);

,这也很糟糕。

这显然是技术 4 崩溃的那种情况,尽管你当然可以这样做

	printf("%s + ", roman(12));
	printf("%s = ", roman(34));
	printf("%s\n", roman(12 + 34));

但是如果我使用技术 8,并且如果roman()的实现保证至少有 3 个独立的静态返回缓冲区,那么我实际上可以编写显而易见的

	printf("%s + %s = %s\n", roman(12), roman(34), roman(12 + 34));
并且它会工作得很好。

技术 8 的实现看起来大致如下

	#define NRETBUFS 3
	#define RETBUFSIZE 20

	char *roman(n)
	int n;
	{
	static char retbufs[NRETBUFS][RETBUFSIZE];
	static int whichret = 0;
	char *ret;

	ret = retbufs[whichret];
	whichret = (whichret + 1) % NRETBUFS;

	now format answer into ret...

	return ret;
	}

我不再讨论技术 9。

Steve Summit
scs@adam.mit.edu