以下是我摘录自《JOEL说软件》书的“深入底层”。

C语言中字符串的工作方式:

  1. 如果不遍历字符串并查找末尾的空字符,就没有办法知道字符串在何处结束(即字符串长度)。
  2. 字符串中不能包含任何零。因此,C字符串中不能存放诸如JPEG图片之类的任何二进制数据块。


  对于重要程序、API、操作系统与类库,用户应该像躲避瘟疫一样地避开使用ASCIZ字符串。为什么呢?先从编写函数strcat的一个代码版本入手进行讨论。该函数的功能是将一个字符串附加在另一个字符串之后。
void strcat( char* dest, char* src )
{
while (*dest) dest++;
while (*dest++ = *src++);
}

很简洁,很好用!但你考虑下面的使用情况:

char bigString[1000]; /* 永远也不知道需要分配多大的存储空间... */
bigString[0] = '';
strcat(bigString,"John, ");
strcat(bigString,"Paul, ");
strcat(bigString,"George, ");
strcat(bigString,"Joel ");这样处理还算得上很好的方式吗?

  不,该代码使用的是蹩脚的Shlemiel喷涂算法。Shlemiel是谁?他是下面这则笑话中的人物:
Shlemiel是谁?他是下面这则笑话中的人物:
  Shlemiel得到一份当街道油漆匠的工作,工作内容是在马路中间喷涂点画线。第一天,他拿出一罐漆来到他负责的路段,喷涂了300码长的线。“干得不错!”他的老板称赞道,“真是一位麻利的工匠”,然后赏给他一个戈比(一种俄罗斯辅币,译者注)。
第二天,Shlemiel只喷涂了150码。“喏,虽然不如昨天那样好,但你仍然算得上一位麻利的工匠!150码还是值得肯定的一个长度,”老板说完又赏给他一戈比。
接下来的一天,Shlemiel只喷涂了30码长的马路。“才30码!”他的老板吼道。“这太令人难以接受了!第一天你干的工作量是今天的10倍!接下来是怎么回事?”
“我尽力了,”Shlemiel说道。“一天一天下去,我离油漆罐越来越远!”

  如果像前面给出的代码那样使用strcat函数将会出现什么结果。由于strcat的第一部分代码必须每次都扫描整个目的字符串,以反复寻找那个捉摸不定的空终止字符,因此该函数将比所希望的速度慢得多,并且它根本谈不上存在伸缩性。你每天使用的许多代码都有这个问题。
如何修正strcat函数呢?有几个聪明的C语言程序员是这样来实现他们自己的mystrcat函数的:

char* mystrcat( char* dest, char* src )
{
while (*dest) dest++;
while (*dest++ = *src++);
return --dest;
}
char bigString[1000]; /* 永远不知道要分配多少存储空间……*/
char *p = bigString;
bigString[0] = '';
p = mystrcat(p,"John, ");
p = mystrcat(p,"Paul, ");
p = mystrcat(p,"George, ");
p = mystrcat(p,"Joel ");
这个性能当然是线性而不是n的。

  Pascal语言的设计人员意识到了这个问题,并通过将字符串的首字节用于存放字节个数,对该问题加以解决。由此得到的字符串称为Pascal字符串。这样的字符串可以包含取值为0的字节,它不会被当做空终止字符。由于一个字节可以表示的最大数值为255,因此Pascal字符串只能限于255个字节的长度。这是非常快的。


  如果想在C代码中放一个Pascal字符串文本,就不得不写一条如下形式的代码:
char* str = "*Hello!";
str[0] = strlen(str) - 1;

在这种情况下得到的字符串,同时也是以空字符作为终止符的(由编译器来做这件事)。我习惯于将它们称为杂交串

前面忽略了一个重要的问题。还记得这样的一个代码行吗?

char bigString[1000]; /* 从不知道要分配多少存储空间…… */

既然现在谈到了位,就不应该忽略掉这个问题。我本来应该正确地处理该问题:弄清楚到底需要多少个字节,并分配合适的内存量。

  大家知道,如果不这样做,那么聪明的***会读取该代码,并注意到用户只分配了1000个字节而觉得它够用了。***们还会找到一些比较聪明的方式,诱骗用户将1100个字节的字符串用strcat放到1000个字节的内存中,从而重写堆栈页面并改变返回地址。结果造成在该函数返回时,它执行了***自己写的一些代码。这就是他们在说某特殊程序具有缓冲区溢出易感性(buffer overflow susceptibility时所谈论的内容。

应该分配多少内存?不妨试着以合理的方式来做这件事。

char* bigString;
  int i = 0;
  i = strlen("John, ")
+ strlen("Paul, ")
+ strlen("George, ")
+ strlen("Joel ");
  bigString = (char*) malloc (i + 1);

/* 别忘了存放空终止字符也要空间!*/

仅仅为了弄清字符串有多大,就必须一次扫描完所有的字符串,然后在串接时还要再次对它们进行扫描。如果使用Pascal字符串,至少strlen操作是很快的。也许,我们可以编写一个能够为我们重新分配内存的strcat函数版本。

它将另外一整罐蠕虫(内存分配器)打开。你知道malloc函数是如何工作的吗?malloc函数的实质体现在,它有一个将可用的内存块连接为一个长长的列表的所谓空闲链表。调用malloc函数时,它沿连接表寻找一个大到足以满足用户请求所需要的内存块。然后,将该内存块一分为二(一块的大小与用户请求的大小相等,另一块的大小就是剩下的字节)。接下来,将分配给用户的那块内存传给用户,并将剩下的那块(如果有的话)返回到连接表上。调用free函数时,它将用户释放的内存块连接到空闲链上。到最后,空闲链会被切成很多的小内存片段,如果这时用户申请一个大的内存片段,那么空闲链上可能没有可以满足用户要求的片段了。于是,malloc函数请求延时,并开始在空闲链上翻箱倒柜地检查各内存片段,对它们进行整理,将相邻的小空闲块合并成较大的内存块。

malloc的性能表现为永远也快不起来(总是要遍历空闲链),有时候甚至显得不可预测,在清理内存时慢得使人害怕。(顺便说一下,这与垃圾收集系统的性能特征类似,不令人吃惊才怪呢。可见,人们做出的关于垃圾收集行为如何导致性能损失的所有断言都不完全成立。因为典型的malloc实现形式具有同样的性能损失,尽管略显温和。)

聪明的程序员通过总是分配大小为2的幂的内存块,而最大限度地降低潜在的malloc性能丧失。建议,在调用realloc函数时,应分配出两倍于以前所分配的内存。这意味着,用户决不会调用realloc函数达lgn次以上。该性能特征即使对于大型字符串也可以忍受,并且决不会浪费多于50%的内存。

XML形式存储的数据不能用SELECT author FROM books实现性能很快的SQL语句的评论受到了不友善***。正是因为大家没有理解在说些什么

关系数据库是如何实现SELECT author FROM books?在关系数据库里,关系表(例如books表)的每一行具有相同的字节长度,而每个字段距离各行开头的偏移量是大小固定的。因此,如果books表的每条记录是100个字节长,并且author字段处在偏移量为23的位置,那么作者们的名字就存放在第23123223323等位置的字节处。移到该查询结果的下一条记录的代码是什么?大体上讲,这条代码为:

pointer += 100;

只用了一条CPU指令!快,快,实在是快!

现在我们来看XML中的books表。

<?xml blah blah>

<books>

<book>

<title>UI Design for Programmers</title>

<author>Joel Spolsky</author>

</book>

<book>

<title>The Chop Suey Club</title>

<author>Bruce Weber</author>

</book>

</books>

马上就想到的另外一个问题是,移到下一条记录的代码是什么?

唔……

在这一点上,好的程序员会说,喏,将XML解析成内存中的树结构吧,以便能够相当快速地处理它。在这里,针对SELECT author FROM books语句,CPU必须完成的工作量绝对会将人折磨至疯。正如每个编译器设计人员都知道的那样,语法分析与解释是在编译处理过程中最慢的部分。只要谈我们在解释、分析与建立抽象的内存语法树时,发现它涉及许多处理起来很慢的字符串素材,以及许多执行起来很慢的内存分配内容就够了。

况且,这还假定了有足够的内存用于一次性加载整个内容。对于关系数据库来说,在记录之间执行移动操作的性能是固定不变的,实际上它不过是一条指令的事。这是费了好大的力气有意为之的。同时,因为得益于内存映像文件,用户只需加载将要实际使用的那些磁盘页面。

对于XML来说,如果要做预分析的话,那么在记录之间执行移动操作的性能是固定不变的,只是启动时间极大;如果不做预分析,那么在记录之间执行移动操作的性能根据它前面的记录长度而变化,并且CPU指令长达好几百条。

这意味着,如果用户讲究性能,并且数据量很大,那么就不能使用XML。如果只有一点儿数据,或者事情做得不必很快,那么XML不失为一个好的形式。并且,如果用户确实希望鱼与熊掌兼得,就必须找出一种途径用于紧靠XML存放元数据。元数据跟Pascal字符串中用于计数的字节的作用差不多,它向用户给出提示信息,以便不用去分析与扫描字符串就能确定它们在文件的什么地方。当然,这样一来用户就不能使用文本编辑器去编辑文件了,因为那会搞乱元数据,从而它不再是真正的XML

所有这些事情都要求用户去思考字节,字节影响着用户在各种体系与策略方面做出决定。这就是我为什么坚持一种教学观点—大学一年级学生需要从基础学起,即用C语言以及从CPU开始向上逐步构建自己程序设计技能—的原因。我真的从心底里厌烦在多得出奇的计算机课程计划中,将Java看做是一门好的入门语言的做法。这些计划的理由是:Java语言虽然很“简易”且不会陷入所有那些令人厌烦的字符串与malloc素材之中,但是可以学习特别棒的OOP知识,从而使大型程序具有如此之多的模块化特性。

这是一场即将发生的教学灾难。一届又一届的毕业生正在侵袭我们,他们正在忽左忽右地创建着Shlemiel油漆匠算法,而他们甚至还没有意识到这一点。原因在于,他们根本就没有那种字符串在深层次上处理起来很困难的理念,即使在Perl脚本上也不能很好地看到那一点。如果想在某个方面把别人教好,自己首先得从最底层开始研究。这就像空手道功夫小子要做的事情一样。打蜡,剥蜡。打蜡,剥蜡。这样的过程要进行三个星期。然后,他就可以轻松击败其他小孩了。