9.5 什么是字符串
字符串就是字符并列的结果,但在不同的语言中,字符串列的表现方式各不相同。
本节我们来看一下 C、Pascal、Java、Ruby 和 Python 这几种语言中的字符串。这五种语言中,只有 C 语言中的字符串不知道自身的长度。其他语言中的字符串都携带有表现自身长度的整数。可以说 C 语言中的字符串是最为原始的字符串。
带有长度信息的 Pascal 语言字符串和不带这一信息的 C 语言字符串
C 语言和 Pascal 语言都规定 1 个字符为 8 个比特 39。同时 Pascal 语言采用了在字符串开头放置字符串长度的规则。而 C 语言中的字符串只是拥有字符串开始后的内存空间。它不携带长度的信息,因此不知道从开始到何处为止是这个字符串(图 9.18)。
39实际上,因为 C 语言中只是规定了 char 类型为最低 8 比特的类型,IBM7.01(1952 年) 和 UNIVAC 1103(1953 年)中采用了 9 比特。但是时至 21 世纪的今天,8 比特的机器占了绝大多数,这里简单化处理了。另外,char 是 character(字符)的前四个字母。

图 9.18 C 语言字符串和 Pascal 语言字符串
那么 C 语言字符串是如何表现字符串本身到何处为止呢?
用 NUL 字符表示字符串的终止
为达到这一目的使用了一种表现字符串终止的特殊字符,这就 NUL 字符 40。NUL 字符是一个与 0 对应的字符,在 C 语言代码中用 \0 表示。
40ASCII 规定将 null character 简称为 NUL,换行(line feed)简称为 LF。为了避免与 C 语言中的 NULL 指针相混淆,本书中用 NUL 字符来表述。
我们来试一下这段代码。在命名为 str 的变量中放入一个在 abc 和 def 中夹着 NUL 字符的字符串。再把 str 变量放到 printf 输出,并且转递给返回字符串长度的函数 strlen。这段代码的执行结果如何呢?
C语言
#include <stdio.h>
#include <string.h>
int main(){
char str[100] = "abc\0def";
printf("%s\n", str);
printf("%zu\n", strlen(str));
return 0;
}
结果如下所示,返回只有 abc,字符串的长度为 3.
输出
abc
3
尽管声明了 str[100],NUL 字符后面还有 def,但这些都无影响输出结果。归根结底,C 语言字符串是把“从头开始读取,直到第一个 NUL 字符出现”的位置当作一个字符串处理。
NUL 字符导致的不便
C 语言字符串是非常原始的,因此很容易发生一些奇怪的事情。执行下面的代码得到的结果是 defabc$$,字符串的长度是 8。这是怎么造成的呢 41 ?
41这段代码在 Mac OS X 10.7.5 上的 gcc 4.2.1 中确认过,但很可能因操作系统、编译器版本或选项的不同,执行结果也有所不同。
C语言
#include <stdio.h>
#include <string.h>
int main(){
int x = 9252;
char str[3] = "abc";
char str2[3] = "defg";
printf("%s\n", str2);
printf("%zu\n", strlen(str2));
return 0;
}
输出
defabc$$
8
原因是 str 和 str2 都声明为 char[3],只分配了 3 字节的空间。abc 这一 3 个字符的字符串要表达它在字符 c 的地方结束的话,需要 3 个字符再加 NUL 字符总共 4 个字符的空间,但是代码中只为其分配了 3 个字节的空间。因此,abc 后面的 NUL 字符以及 def 后面的 g 和 NUL 字符都没能放入而被舍弃了。故而在显示 str2 时,首先显示 def,然后是显示与之相邻的空间里保存的 abc。
那么最后的 $$ 又是怎么回事呢?这其实是函数开始部分的 int x = 9252;语句在内存中写入的整数 9252. 9252 用 16 进制表示就是 2424,在 ASCII 码中 24 是 $。因此这个整数被解释为有两个 $ 并列的字符串的一部分。与之相邻的内存中是 00,被当作是 NUL 字符,显示到此终止(图 9.19)。然而,在某些情况下可能显示出更多的内容,并且有可能会试图读取那些禁止读取的内容,从而造成程序的异常终止。

图 9.19 运行时的内存状况
1 个字符为 16 比特的 Java 语言字符串
至此,我们学习了携带有长度信息的 Pascal 语言字符串和不带有这一信息的 C 语言的字符串。C 语言风格的字符串处理起来还是比较困难的。实际上,大多数语言都采用了 Pascal 语言风格的字符串。
Java 语言字符串也是携带有长度信息的字符串。然而 Java 语言的最大区别在于它规定 char 类型是 16 比特 42。C 语言或是 Pascal 语言对字符为何物的定义都不一样。C 语言中字符被定义为是一个 8 比特、0~255 范围内可以表现的 ASCII 字符或者 EBCDIC 字符,而 Java 语言中字符被定义为是一个 16bit、0~65535 范围内可表现的 Unicode 字符。
42那么 16 比特无法表达的字符怎么办?这时可以使用通过两个 16bit 的值组合来表达 1 个字符的方法——代理对。它使用了 16bit 中没有用于字符表达的空间。这一原理和 EUC-JP 中通过组合 8bit 中没有用于表达字符的空间来表达 8bit 所不能表达的字符的原理是相似的。
Python 3 中引入的设计变更
Python 语言既支持 Java 语言的 16 比特的 Unicode 字符串,也支持 Pascal 语言那样的 8 比特的字节串列的字符串 43。
43严格来讲,Unicode 是 16bit 还是 32bit 是在编译时通过选项来指定的。
在 Python 2.x 版本中,源代码中有 " あ " 时,这是一个字节串列的字符串。如果源代码的编码方式为 UTF-8,这就变成一个有 ['0xe3', '0x81', '0x82'] 三个字节的串列。写成 u" あ " 时,表示这是一个 Unicode 的字符串,只有一个 Unicode 字符即 ['0x3042'].
因为同时存在两种类型的字符串,于是会有一个问题:两者混合使用的话会怎样? Python 2.x 版本规定,在 ASCII 码环境下时字节串列被当作 ASCII 码并且可以自动转换成 Unicode。
Python 2.7
>>> u"hello, " + "Alice"
u'hello, Alice'
>>> u"hello, " + "太郎"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
UnicodeDecodeError: 'ascii' codec can't decode byte 0xe5 in position 0:
ordinal not in range(128)
※ Python 2.7 中字节串列只在 ASCII 码时才能和 Unicode 字符串结合
然而,在字符串内容不同时,这一规定有时正常有时却会导致错误。在只使用了 ASCII 字符的测试案例中可以正常运行,而在使用了 ASCII 字符以外的字符的日本却会有问题。
因此,Python 3.x 版本舍弃了 Python 2.x 版本中的兼容性,围绕字符串展开了大的变革。首先,规则发生了变化。写成 " あ " 时直接是 Unicode 字符,写成 "b" 时是字节串列,这样 Unicode 字符串就很容易书写了。" あ " 就变成和在 Java 语言中一样的了。
其次,在 Unicode 字符串和字节串列结合的时候,不管其想结合的内容如何,都将抛出类型错误。在有需要混合字符串时,规定有必要显式地使用转换代码,这避免了在不知情的情况下进行了转换而导致问题发生的被动局面 44。
44这是我们在第 6 章中学习的“错误优先”的一个表现。
Python 3.0
>>> "hello, " + b"Alice"
Traceback (most recent call last):
File "<stdin>", line 1, in <module>
TypeError: Can't convert 'bytes' object to str implicitly
>>> "hello, " + b"Alice".decode("ASCII")
'hello, Alice'
※ Python 3.0 中将字节串列结合到 Unicode 字符串时常常发生错误。这时需要显示地将字节串列转换成 Unicode 字符串。
※ 在转换成 Unicode 字符串时为什么要写 ASCII 呢?也许有人这样问。这里的 decode("ASCII") 是指将使用 ASCII 编码方式编码了(encode)的内容做还原处理(decode)。
Ruby 1.9 的挑战
Python、Java 等众多语言都采用了以 Unicode 为基础的字符串,而 Ruby 语言却走出了独树一帜的路线。从 Ruby 1.9 开始,字符串就是 8 个比特,并且采用了追加编码方式信息的设计方法 45。这种方法的优点是可以直接书写那些不包含在 Unicode 字符集中的字符。比如,在提供面向移动电话使用的互联网服务时,有需要保证各移动电话厂商设置的表情符号之间的兼容性。
45《Rubyist Magazine - Ruby M17N の設計と実装》(中文译名:Rubyist Magazine - Ruby M17N 的设计与实现)http://jp.rubyist.net/magazine/?0025-Ruby19_m17n。
在这个表情符号的问题上,谷歌公司和苹果公司采取了不同的战略。它们在 Unicode 联盟上提议将移动电话上使用的表情符号也添加到 Unicode 字符集中来。于 2010 年发布的 Unicode 6.0 中追加了数百个字符的表情符号。亦即,从 Unicode 6.0 开始,诸如“握手表情”这样的都变成字符了 46。
46谷歌公司作为最大的检索引擎供应商,其提供的网络邮箱服务 Gmail 在 2013 年有同时超过 4 亿人的活跃用户。为了使得包含表情符号的邮件也能在其他终端正确显示,把表情符号添加到 Unicode 中去确实带来了很大的便利。顺便说一句,表示握手的编码是 1F359。
