一个参数free(void *)
(在Unix V7中引入)比以前的两个参数具有另一个主要优势mfree(void *, size_t)
没有提及的:这里没有提到:一个参数free
极大地简化了所有其他与堆内存一起使用的API。例如,如果free
需要存储块的大小,则将strdup
不得不以某种方式返回两个值(指针+大小)而不是一个值(指针),并且C使多值返回比单值返回更加麻烦。而不是char *strdup(char *)
我们不得不写char *strdup(char *, size_t *)
,否则struct CharPWithSize { char *val; size_t size}; CharPWithSize strdup(char *)
。(现在,第二种选择看起来很诱人,因为我们知道以NUL终止的字符串是“计算历史上最灾难性的设计错误”。,但这是事后看来。在70年代,C能够将字符串作为简单的字符串进行处理。char *
实际上,它被认为比Pascal和Algol等竞争者具有决定性的优势。)此外,不仅仅是strdup
受此问题困扰,它还会影响分配堆内存的每个系统或用户定义的函数。
早期的Unix设计师是非常聪明的人,并且有很多理由说明为什么free
要比mfree
基本上更好,我认为问题的答案是他们注意到了这一点并据此设计了系统。我怀疑您在做出决定时会发现他们脑海中发生的任何直接记录。但是我们可以想象。
假设您要使用C编写应用程序以在V6 Unix(带有两个参数)上运行mfree
。到目前为止,您的工作还不错,但是随着程序的发展,跟踪这些指针的大小变得越来越麻烦变得更加雄心勃勃,并且需要越来越多地使用堆分配的变量。但是,然后您有了一个绝妙的主意:您不必size_t
编写所有函数,而只需编写一些实用程序函数即可,这些函数直接将大小存储在分配的内存中:
void *my_alloc(size_t size) {
void *block = malloc(sizeof(size) + size);
*(size_t *)block = size;
return (void *) ((size_t *)block + 1);
}
void my_free(void *block) {
block = (size_t *)block - 1;
mfree(block, *(size_t *)block);
}
使用这些新功能编写的代码越多,它们看起来就越强大。他们不仅使你的代码更容易编写,他们也让你的代码更快-两件事情不经常一起去!在size_t
各处传递这些s之前,这增加了复制的CPU开销,这意味着您不得不更频繁地溢出寄存器(尤其是多余的函数参数),并浪费内存(因为经常会导致嵌套函数调用)以多个副本size_t
形式存储在不同的堆栈框架中)。在新系统中,您仍然必须花费内存来存储size_t
,但只能复制一次,并且永远不会复制到任何地方。这些效率似乎很小,但请记住,我们所谈论的是具有256 KiB RAM的高端机器。
这让你开心!因此,您与正在开发下一个Unix版本的有胡子的人分享了很酷的技巧,但这并不能使他们感到高兴,而会让他们感到悲伤。您会看到,它们只是在添加一堆新的实用程序功能,例如strdup
,他们意识到使用您的绝妙技巧的人们将无法使用其新功能,因为他们的新功能都使用了繁琐的指针+大小API。然后让你伤心过,因为你意识到你必须重写好strdup(char *)
自己功能每次你写的程序,而不是能够使用的系统版本。
可是等等!这是1977年,向后兼容不会再发明5年了!此外,没有人真正使用这种晦涩的“ Unix”名称及其变色名称。K&R的第一版现已面世,但这没问题-它在第一页上说:“ C没有提供直接处理诸如字符串之类的复合对象的操作……没有堆。 ...”。在这个历史时刻,string.h
并且malloc
是供应商扩展(!)。因此,建议有胡子的人#1,我们可以根据需要更改它们;我们为什么不只宣布您棘手的分配器为正式分配器?
几天后,Bearded Man#2看到了新的API并说,嘿,等等,这比以前要好,但是它仍然会为分配大小花费整个单词。他认为这是亵渎的下一件事。其他人都像疯了一样看着他,因为你还能做什么?那天晚上,他待到很晚,发明了一个根本不存储大小的新分配器,而是通过对指针值执行黑魔术位移位来动态推断出它,并在保持新API不变的情况下进行交换。新的API意味着没有人注意到此切换,但是他们确实注意到第二天早上编译器使用的RAM减少了10%。
现在所有人都高兴了:您得到了更易于编写和更快的代码,Bearded Man#1获得了一个strdup
可以供人们实际使用的简单方法,而Bearded Man#2获得了一点点保留的自信- -回到弄糟的藜。装运它!
或者至少,这是它可能发生。