第16章 高级字符串

前一章描述了Str接口导出的函数,这些函数增强了C语言处理字符串的约定。按照惯例,字符串是字符的数组,其中最后一个字符是NULL。虽然这种表示适用于许多应用程序,它确实有两个重要的缺点。首先,获取字符串长度需要搜索字符串,查找标志字符串结束的0字符,因此计算长度花费的时间与字符串的长度成正比。其次,Str接口中的函数和标准库中的一些函数假定字符串是可以修改的,因此函数或其调用者必须为结果字符串分配空间,在不修改字符串的应用程序中,许多这种分配是不必要的。

本章描述的Text接口对字符串使用了一种稍有不同的表示,解决了这两个缺点。长度可以在常数时间内计算得到,因为相关信息保存在字符串中,而仅在有必要时才分配内存。Text提供的字符串是不可改变的,即它们无法直接修改,而且其中可能包含嵌入的0字符。Text提供了函数,可以在Text字符串和C风格字符串之间进行转换,这些转换函数是Text接口的改进带来的代价。

16.1 接口

Text接口通过一个两个元素的描述符 来表示字符串,描述符的两个元素分别是字符串的长度和指向第一个字符的指针:

exported types

 192〉≡   
  typedef struct T {  
      int len;  
      const char *str;  
  } T;  
 
〈text.h

〉≡  
  #ifndef TEXT_INCLUDED  
  #define TEXT_INCLUDED  
  #include <stdarg.h>  
 
  #define T Text_T  
 
 〈exported types

 192〉  
 〈exported data

 195〉  
 〈exported functions

 193〉  
 
  #undef T  
  #endif  

str字段指向的字符串 是以0字符结尾的。Text_T指向的字符串可能包含任意字符,包含0字符在内。Text接口给出了描述符的表示,使得客户程序可以直接访问其中的字段。给定一个Text_T实例,s.len给出了字符串的长度,而实际的字符通过s.str[0..s.len-1]访问。

客户程序可以读取Text_T实例中的字段和其指向的字符串中的字符,但不允许改变字段或字符,除非通过本接口中的函数,或者Text_T实例是由客户程序初始化的,或者Text_T实例是由Text_box返回的。改变Text_T描述的字符串,是一个未检查的运行时错误。向本接口中任何函数传递的Text_T实例,如果len字段为负值或str字段为NULL,都是已检查的运行时错误。

Text导出的函数按 传递和返回描述符,即传递给函数、函数返回的都是描述符本身,而不是指向描述符的指针。因而,Text函数都不分配描述符。

必要时,一些Text函数确实会为字符串本身分配空间。这种串空间 [1] 完全由Text管理,客户程序决不能释放字符串(除下文所述的例外情况)。通过外部手段(如调用free或Mem_free)释放字符串,是一个未检查的运行时错误。

以下函数

exported functions

 193〉≡  
  extern T     Text_put(const char *str);  
  extern char *Text_get(char *str, int size, T s);  
  extern T     Text_box(const char *str, int len);  

在描述符和C风格字符串之间进行转换。Text_put将0结尾字符串str复制到串空间中,并返回对应于新字符串的描述符。Text_put可能引发Mem_Failed异常。str是NULL,则造成已检查的运行时错误。

Text_get将s描述的字符串复制到str[0..size-2]中,附加一个0字符,并返回str。如果size小于s.len+1,则造成已检查的运行时错误。如果str是NULL,Text_get将忽略size,调用Mem_alloc来分配s.len+1个字节,将s.str复制到新分配的空间中,并返回指向分配空间起始处的指针。在str是NULL时,Text_get可能引发Mem_Failed异常。

客户程序调用Text_box为常数字符串或客户程序自行分配的字符串建立描述符。它将str和len“装箱”到一个描述符中并返回描述符。例如,

static char editmsg[] = "Last edited by: ";  
... 
Text_T msg = Text_box(editmsg, sizeof (editmsg) - 1);  

将对应于"Last edited by:"的Text_T实例赋值给msg。请注意,Text_box的第二个参数忽略了editmsg末尾的0字符。如果不略去该字符,它将被当做msg描述的字符串的一部分。str是NULL或len是负值,则造成已检查的运行时错误。

许多Text函数可以接受字符串位置,位置的定义可参见Str接口。位置标识了字符之间 的位置,包含第一个字符之前和最后一个字符之后。正数位置标识了从字符串第一个字符左侧开始(向右)的各个位置,而非正数位置标识了从字符串最后一个字符右侧开始(向左)的各个位置。例如图15-1,给出了字符串Interface中的各个位置。

下列函数

exported functions

 193〉+≡   
  extern T Text_sub(T s, int i, int j);  

返回一个描述符,对应于s中位置i和j之间的子串。位置i和j可以按任意顺序给出。例如,如果

Text_T s = Text_put("Interface");  

下述表达式

Text_sub(s,  6, 10)  
Text_sub(s,  0, -4)  
Text_sub(s, 10, -4)  
Text_sub(s,  6, 0)  

都返回对应于子串face的描述符。

因为客户程序并不修改字符串中的字符,字符串也不需要以0字符结束,Text_sub只需要返回一个Text_T实例,其中的str字段指向s的子串的第一个字符,而len字段设置为该子串的长度即可。因而s和返回值共享实际字符串中的字符,Text_sub没有为返回值分配空间。但客户程序不能依赖于s和返回值共享同一字符串的这一事实,因为Text可能对空串和单字符串给予特殊处理。Text导出的大部分函数都类似于Str导出的函数,但其中很多函数不接受位置参数,因为Text_sub用很少的代价提供了同样的功能。

以下函数

exported functions

 193〉+≡  
  extern int Text_pos(T s, int i);  

返回s中对应于任意位置i的正数位置。例如,如果s如上所述被赋值为Interface,

Text_pos(s, -4)  

将返回6。

如果Text_pos的参数i或者Text_sub中的参数i或j指定了s中一个不存在的位置,会造成已检查的运行时错误。

以下函数

exported functions

 193〉+≡  
  extern T Text_cat (T s1, T s2);  
  extern T Text_dup (T s, int n);  
  extern T Text_reverse(T s);  

分别连接、复制和反转字符串,所有这些函数都可能引发Mem_Failed异常。Text_cat返回一个描述符,对应于连接s1和s2得到的结果字符串,如果s1或s2是空串,则返回另一个参数。另外,Text_cat仅在必要时构造s1和s2的一个新副本。

Text_dup返回一个描述符,对应于连接s的n个副本得到的结果字符串,传递负的n值,是一个已检查的运行时错误。Text_reverse返回一个字符串,其中包含了s中所有的字符,但字符出现的顺序与s正好相反。

exported functions

 193〉+≡  
  extern T Text_map(T s, const T *from, const T *to);  

返回根据from和to指向的字符串映射s的结果,映射规则如下:对s中每个出现在from中的字符,使用to中的对应字符替换后输出到结果字符串,对于s中没有出现在from中的字符,字符本身直接输出到结果字符串。例如,

Text_map(s, &Text_ucase, &Text_lcase)  

返回s的一个副本,其中的大写字母转换为对应的小写字母。Text_ucase和Text_lcase是Text接口导出的预定义描述符中的两个例子。完整的预定义描述符列表如下:

exported data

 195〉≡  
  extern const T Text_cset;  
  extern const T Text_ascii;  
  extern const T Text_ucase;  
  extern const T Text_lcase;  
  extern const T Text_digits;  
  extern const T Text_null;  

Text_cset是一个字符串,由所有256个8比特的字符组成,Text_ascii包含128个ASCII字符,Text_ucase是字符串ABCDEFGHIJKLMNOPQRSTUVWXYZ,Text_lcase是字符串abcdefghijklmnopqrstuvwxyz,Text_digits是0123456789,Text_null是空串。客户程序通过获取这些字符串的子串,可以形成其他常见的字符串。

Text_map可以记录最新且不是NULL的from和to值,在from和to都是NULL时将使用记录的值。如果from和to中仅有一个为NULL,或二者均非NULL时from->len不等于to->len,则造成已检查的运行时错误。Text_map可能引发Mem_Failed异常。

字符串通过以下函数比较

exported functions

 193〉+≡  
  extern int Text_cmp(T s1, T s2);  

当s1小于、等于、大于s2时,该函数分别返回负值、0、正值。

Text接口导出了一组字符串分析函数,与Str接口导出的相关函数几乎是相同的。如下所述的这些函数,可以接受被检查字符串中的位置作为参数,因为这些位置中通常编码了分析的当前状态信息。在接下来的描述中,s[i:j]表示s中在位置i和j之间的子串,s[i]表示s中位置i右侧的字符。

下列函数在字符串中查找单个字符或一组字符,在所有情况下,如果i或j指定不存在的位置,均属已检查的运行时错误。

exported functions

 193〉+≡  
  extern int Text_chr(T s, int i, int j, int c);  
  extern int Text_rchr(T s, int i, int j, int c);  
  extern int Text_upto(T s, int i, int j, T set);  
  extern int Text_rupto(T s, int i, int j, T set);  
  extern int Text_any(T s, int i, T set);  
  extern int Text_many(T s, int i, int j, T set);  
  extern int Text_rmany(T s, int i, int j, T set);  

Text_chr和Text_rchr分别在s[i:j]查找最左侧和最右侧的字符c,并返回s中该字符左侧的正数位置。如果c没有在s[i:j]中,两个函数都返回0。Text_upto在s[i:j]中从左向右搜索set中的任意字符,Text_rupto在s[i:j]中从右向左搜索set中的任意字符,二者均返回找到的第一个字符左侧的正数位置。如果set中的所有字符都没有出现在s[i:j]中,两个函数都返回0。

如果s[i]等于c,Text_any返回Text_pos(s, i)+1,否则返回0。如果s[i:j]以set中的某个字符开始,Text_many返回完全由set中字符组成的(最长)子串之后的正数位置;否则,该函数返回0。如果s[i:j]以set中的某个字符结束,Text_rmany返回完全由set中字符组成的(最长)子串之前 的正数位置,否则Text_rmany返回0。

剩余的分析函数查找字符串。

exported functions

 193〉+≡  
  extern int Text_find(T s, int i, int j, T str);  
  extern int Text_rfind(T s, int i, int j, T str);  
  extern int Text_match(T s, int i, int j, T str);  
  extern int Text_rmatch(T s, int i, int j, T str);  

Text_find和Text_rfind分别在s[i:j]查找最左侧和最右侧的子串str,并返回s中该子串左侧的正数位置。如果str没有出现在s[i:j]中,两个函数都返回0。

如果s[i:j]以子串str开始,那么Text_match返回Text_pos(s, i)+str.len,否则返回0。如果s[i:j]以子串str结束,那么Text_rmatch返回Text_pos(s, j)-str.len,否则返回0。

以下函数

exported functions

 193〉+≡  
  extern void Text_fmt(int code, va_list *app,  
      int put(int c, void *cl), void *cl,  
      unsigned char flags[], int width, int precision);  

可以用于Fmt接口,作为转换函数。它消耗一个指向Text_T实例的指针,并按照可选的flags、width和precision参数来格式化字符串,格式化的方式与printf格式码%s相同。之所以使用指向Text_T实例的指针,是因为在标准C语言中,在可变长度参数列表的可变部分传递小的结构,这种做法可能是不可移植的。指向Text_T实例的指针为NULL、app或flags为NULL,均为已检查的运行时错误。

Text接口使得客户程序可以有限地控制对串空间的分配,即,对于上文描述的返回描述符的函数,可以控制结果字符串实际存储的位置。具体来说,可通过下列函数以栈的形式来管理相应的内存空间。

exported types

 192〉+≡  
  typedef struct Text_save_T *Text_save_T;  
 
〈exported functions

 193〉+≡  
  extern Text_save_T Text_save(void);  
  extern void        Text_restore(Text_save_T *save);  

Text_save返回一个类型Text_save_T的不透明指针值,其中编码了串空间的“顶部”位置。该值在以后传递给Text_restore,以释放Text_save_T值创建以来分配的那部分串空间。如果h是一个Text_save_T类型的值,调用Text_restore(h)将使在h之后创建的所有描述符和所有Text_save_T值变为无效。传递给Text_restore的Text_save_T值为NULL,是一个已检查的运行时错误。调用Text_restore之后,使用变为无效的描述符和Text_savt_T值,是未检查的运行时错误。Text_save可能引发Mem_Failed异常。

16.2 实现

Text接口的实现与Str接口的实现非常类似,但Text函数可以利用几个重要的特例,详述如下。

text.c

〉≡  
  #include <string.h>  
  #include <limits.h>  
  #include "assert.h"  
  #include "fmt.h"  
  #include "text.h"  
  #include "mem.h"  
 
  #define T Text_T  
 
 〈macros

 198〉  
 〈types

 205〉  
 〈data

 198〉  
 〈static functions

 204〉  
 〈functions

 198〉  

所有常数描述符都指向一个由所有256个字符组成的字符串:

data

 198〉≡  
  static char cset[] =  
      "\000\001\002\003\004\005\006\007\010\011\012\013\014\015\016\017"  
      "\020\021\022\023\024\025\026\027\030\031\032\033\034\035\036\037"  
      "\040\041\042\043\044\045\046\047\050\051\052\053\054\055\056\057"      
      "\060\061\062\063\064\065\066\067\070\071\072\073\074\075\076\077"  
      "\100\101\102\103\104\105\106\107\110\111\112\113\114\115\116\117"  
      "\120\121\122\123\124\125\126\127\130\131\132\133\134\135\136\137"  
      "\140\141\142\143\144\145\146\147\150\151\152\153\154\155\156\157"  
      "\160\161\162\163\164\165\166\167\170\171\172\173\174\175\176\177"  
      "\200\201\202\203\204\205\206\207\210\211\212\213\214\215\216\217"  
      "\220\221\222\223\224\225\226\227\230\231\232\233\234\235\236\237"  
      "\240\241\242\243\244\245\246\247\250\251\252\253\254\255\256\257"  
      "\260\261\262\263\264\265\266\267\270\271\272\273\274\275\276\277"  
      "\300\301\302\303\304\305\306\307\310\311\312\313\314\315\316\317"  
      "\320\321\322\323\324\325\326\327\330\331\332\333\334\335\336\337"  
      "\340\341\342\343\344\345\346\347\350\351\352\353\354\355\356\357"  
      "\360\361\362\363\364\365\366\367\370\371\372\373\374\375\376\377"  
      ;  
  const T Text_cset   = { 256, cset };  
  const T Text_ascii  = { 128, cset };  
  const T Text_ucase  = {  26, cset + 'A' };  
  const T Text_lcase  = {  26, cset + 'a' };  
  const T Text_digits = {  10, cset + '0' };  
  const T Text_null   = {   0, cset };  

Text函数都接受位置参数,但会将位置转换为位置右侧字符的索引,以便访问字符串中的字符。正数位置减去1即可转换为索引值,非正数位置需要加上字符串的长度才能转换为索引值:

macros

 198〉≡  
  #define idx(i, len

) ((i

) <= 0 ? (i

) + (len

) : (i

) - 1)  

索引值加1即可转换为正数位置,如Text_pos的实现所示,该函数将其位置参数转换为索引值,而后又将索引值转换为一个正数位置。

functions

 198〉≡  
  int Text_pos(T s, int i) {  
      assert(s.len >= 0 && s.str);  
      i = idx(i, s.len);  
      assert(i >= 0 && i <= s.len);  
      return i + 1;  
  }  

Text_pos中的第一个断言实现了下述已检查的运行时错误:所有Text_T实例的len字段必须为非负值、str字段不能为NULL。第二个断言实现的已检查的运行时错误是:位置i(已转换为索引)应该对应于s中一个有效位置。如果s有N个字符,有效索引值从0到N-1,而有效正数位置从1到N+1,这也是第二个断言可以接受i为N的原因。

Text_box和Text_sub都建立并返回新的描述符。

functions

 198〉+≡  
  T Text_box(const char *str, int len) {  
      T text;  
 
      assert(str);  
      assert(len >= 0);  
      text.str = str;  
      text.len = len;  
      return text;  
  }  

Text_sub类似,但它必须将位置参数转换为索引,以便计算结果字符串的长度:

functions

 198〉+≡  
  T Text_sub(T s, int i, int j) {  
      T text;  
 
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      text.len = j - i;  
      text.str = s.str + i;  
      return text;  
  }  

如代码所示,在i和j由位置转换为索引之后,在i和j之间有j - i个字符。转换代码还会在适当情况下交换i和j的值,使得i总是指定了最左侧字符的索引。

convert

 i and

 j to indices in

 0..s.len 199〉≡  
  assert(s.len >= 0 && s.str);  
  i = idx(i, s.len);  
  j = idx(j, s.len);  
  if (i > j) { int t = i; i = j; j = t; }  
  assert(i >= 0 && j <= s.len);  

最后一个字符右侧的位置转换为一个不存在字符的索引,断言可以接受这种位置。仅当转换得到的索引值不用于获取或存储字符时,才使用<convert i and j to indices in 0..s.len 279 >代码块。例如,Text_sub仅使用该代码块计算子串的起始位置和长度。其他的Text函数仅在检查过i和j为有效索引后才使用i和j的结果值。

Text_put将字符串复制到串空间中,Text_get从串空间中获取字符串。因为几个原因,Text实现了自身的分配函数*alloc(int len),它可以在串空间中分配len个字节。首先,alloc避免了通用分配器中使用的内存块首部(block header),这样它可以将字符串在内存中安排到相邻的位置。这使得可以对Text_dup和Text_cat进行几个重要的优化。其次,alloc可以忽略对齐约束,字符实际上是不需要对齐约束的。最后,alloc必须与Text_save和Text_restore协作。alloc在16.2.2节开始描述,此外还有Text_save和Text_restore。

在需要分配串空间的少数Text函数中,Text_put是比较典型的。它调用alloc分配所需的内存空间,将其参数字符串复制到该空间中,并返回适当的描述符:

functions

 198〉+≡  
  T Text_put(const char *str) {  
      T text;  
 
      assert(str);  
      text.len = strlen(str);  
      text.str = memcpy(alloc(text.len), str, text.len);  
      return text;  
  }  

Text_put调用memcpy而不是strcpy来复制字符串,因为它不能向text.str附加0字符。

Text_get所做的刚好相反:它将字符串从串空间复制到一个C风格的字符串。如果指向C风格字符串的指针为NULL,Text_get调用Mem的通用分配器来为字符串及其结束0字符分配内存空间:

functions

 198〉+≡  
  char *Text_get(char *str, int size, T s) {  
      assert(s.len >= 0 && s.str);  
      if (str == NULL)  
          str = ALLOC(s.len + 1);  
      else  
          assert(size >= s.len + 1);  
      memcpy(str, s.str, s.len);  
      str[s.len] = '\0';  
      return str;  
  }  

Text_get调用memcpy而不是strncpy来复制字符串,因为它必须复制s中可能出现的0字符。

16.2.1 字符串操作

Text_dup生成其Text_T参数s的n个副本,并将其连接起来。

functions

 198〉+≡  
  T Text_dup(T s, int n) {  
      assert(s.len >= 0 && s.str);  
      assert(n >= 0);  
     〈Text_dup

 200〉  
  }  

其中有几个重要的特例,可以避免分配s的n个副本。例如,如果s为空串或n为0,则Text_dup返回空串,如果n为1,Text_dup只返回s即可:

Text_dup

 200〉≡  
  if (n == 0 || s.len == 0)  
      return Text_null;  
  if (n == 1)  
      return s;  

如果s是最近创建的,那么s.str可能刚好位于串空间的末端,即,s.str + s.len可能等于下一个空闲字节的地址。倘若如此,只需要分配s的n - 1个副本,因为原来的s可以充当第一个副本。16.2.2节定义的宏isatend(s, n),可以检查s.str是否位于串空间的末端,以及串空间中是否还有空闲空间可容纳至少n个字符。

Text_dup

 200〉+≡  
  {  
      T text;  
      char *p;  
      text.len = n*s.len;  
      if (isatend(s, text.len - s.len)) {  
          text.str = s.str;  
          p = alloc(text.len - s.len);  
          n--;  
      } else  
          text.str = p = alloc(text.len);  
      for ( ; n-- > 0; p += s.len)  
          memcpy(p, s.str, s.len);  
      return text;  
  }  

Text_cat返回两个字符串s1和s2连接的结果。

functions

 180〉+≡  
  T Text_cat(T s1, T s2) {  
      assert(s1.len >= 0 && s1.str);  
      assert(s2.len >= 0 && s2.str);  
     〈Text_cat

 201〉  
  }  

类似于Text_dup,其中有几个重要的特例,可以避免分配内存。首先,如果s1或s2中有一个为空串,Text_cat可以只返回另一个描述符:

Text_cat

 201〉≡  
  if (s1.len == 0)  
      return s2;  
  if (s2.len == 0)  
      return s1; 

s1和s2可能已经是相邻的,在这种情况下Text_cat可以返回s1作为合并后的结果:

Text_cat

 201〉+≡  
  if (s1.str + s1.len == s2.str) {  
      s1.len += s2.len;  
      return s1;  
  }  

如果s1位于串空间的末端,那么只需要复制s2,否则,两个字符串都必须复制:

Text_cat

 201〉+≡  
  {  
      T text;  
      text.len = s1.len + s2.len;  
      if (isatend(s1, s2.len)) {  
          text.str = s1.str;  
          memcpy(alloc(s2.len), s2.str, s2.len);  
      } else {  
          char *p;  
          text.str = p = alloc(s1.len + s2.len);  
          memcpy(p,            s1.str, s1.len);  
          memcpy(p + s1.len,   s2.str, s2.len);  
      }  
      return text;  
  }  

Text_reverse返回其参数s的一个副本,但其中字符的顺序与s相反,该函数只有两个重要特例:即s为空串和s只有一个字符时:

functions

 198〉+≡  
  T Text_reverse(T s) {  
      assert(s.len >= 0 && s.str);  
      if (s.len == 0)  
          return Text_null;  
      else if (s.len == 1)  
          return s;  
      else {  
          T text;  
          char *p;  
          int i = s.len;  
          text.len = s.len;  
          text.str = p = alloc(s.len);  
          while (--i >= 0)  
              *p++ = s.str[i];  
          return text;  
      }  
  }  

Text_map的实现类似于Str_map的实现。首先,它使用from和to字符串建立一个数组来映射字符,给出一个输入字符c,map[c]即为输出字符串中对应于c的字符。map初始化时,对所有k都将map[k]设置为k,然后以from中的字符为索引,将map中的元素设置为to中对应的字符:

rebuild

 map 202〉≡  
  int k;  
  for (k = 0; k < (int)sizeof map; k++)  
      map[k] = k;  
  assert(from->len == to->len);  
  for (k = 0; k < from->len; k++)  
      map[(unsigned char)from->str[k]] = to->str[k];  
  inited = 1;  

在map初始化之后,inited标志设置为1,inited用于实现下述已检查的运行时错误:第一次调用Text_map时,指定的from和to字符串必须不是NULL:

functions

 198〉+≡  
  T Text_map(T s, const T *from, const T *to) {  
      static char map[256];  
      static int inited = 0;  
 
      assert(s.len >= 0 && s.str);  
      if (from && to) {  
         〈rebuild

 map 202〉  
      } else {  
          assert(from == NULL && to == NULL);  
          assert(inited);  
      }  
      if (s.len == 0)  
          return Text_null;  
      else {  
          T text;  
          int i;  
          char *p;  
          text.len = s.len;  
          text.str = p = alloc(s.len);  
          for (i = 0; i < s.len; i++)  
              *p++ = map[(unsigned char)s.str[i]];  
          return text;  
      }  
  }  

Str_map并不需要inited标志,因为Str_map不可能将一个字符映射到0字符,通过断言检查map['a']非零,即足以实现已检查的运行时错误(参见15.3.1节)。但Text_map允许所有可能的映射,因而不能使用map中的一个值来实现该检查。

Text_cmp比较两个字符串s1和s2,并根据s1小于、等于或大于s2,分别返回一个小于零、等于零或大于零的值。重要的特例是s1和s2指向同一字符串时,在这种情况下短的字符串小于长的。同样地,当一个字符串是另一个字符串的前缀时,较短的较小。

functions

 198〉+≡  
  int Text_cmp(T s1, T s2) {  
      assert(s1.len >= 0 && s1.str);  
      assert(s2.len >= 0 && s2.str);  
      if (s1.str == s2.str)  
          return s1.len - s2.len;  
      else if (s1.len < s2.len) {  
          int cond = memcmp(s1.str, s2.str, s1.len);  
          return cond == 0 ? -1 : cond;  
      } else if (s1.len > s2.len) {  
          int cond = memcmp(s1.str, s2.str, s2.len);  
          return cond == 0 ? +1 : cond;  
      } else  
          return memcmp(s1.str, s2.str, s1.len);  
  }  

16.2.2 内存管理

Text实现其自身的内存分配器,这样在Text_dup和Text_cat中它可以利用相邻的字符串。由于串空间只包含字符,Text的分配器还可以避免内存块首部结构和对齐问题,能够节省空间。该分配器是第6章描述的内存池分配器的一种简单变体。串空间就如同一个内存池,其中已分配的大内存块位于从head发出的链表上:

data

 198〉+≡  
  static struct chunk {  
      struct chunk *link;  
      char *avail;  
      char *limit;  
  } head = { NULL, NULL, NULL }, *current = &head;  

limit字段指向内存块末端的下一个字节,avail指向第一个空闲字节,link指向下一个内存块,该内存块是全部空闲的。current指向“当前”内存块,内存分配操作在该内存块中进行。上述的定义将current初始化为指向一个零长度内存块,第一次分配会向head附加一个新内存块。

alloc从当前内存块分配len个字节,或分配一个至少为10KB的新内存块:

static functions

 204〉≡  
  static char *alloc(int len) {  
      assert(len >= 0);  
      if (current->avail + len > current->limit) {  
          current = current->link =  
              ALLOC(sizeof (*current) + 10*1024 + len);  
          current->avail = (char *)(current + 1);  
          current->limit = current->avail + 10*1024 + len;  
          current->link = NULL;  
      }  
      current->avail += len;  
      return current->avail - len;  
  }  

current->avail是串空间末端第一个空闲字节的地址。对于一个Text_T实例s来说,如果s.str + s.len等于current->avail,那么s就位于串空间的末端。因而宏isatend定义如下:

macros

 198〉+≡  
  #define isatend(s, n) ((s).str+(s).len == current->avail\  
      && current->avail + (n) <= current->limit)  

Text_dup和Text_cat可以利用出现在串空间末端的字符串,只要当前内存块中还包含足够的空闲空间可满足要求即可,这解释了isatend第二个参数的用途。

Text_save和Text_restore向客户程序提供了一种方法,可以保存和恢复串空间末端的位置,该位置由current和current->avail的值给出。Text_save返回一个不透明指针,指向下述结构的实例。

types

 205〉≡  
  struct Text_save_T {  
      struct chunk *current;  
      char *avail;  
  };  

该结构可以给出current和current->avail的值。

functions

 198〉+≡  
  Text_save_T Text_save(void) {  
      Text_save_T save;  
 
      NEW(save);  
      save->current = current;  
      save->avail = current->avail;  
      alloc(1);  
      return save;  
  }  

Text_save调用alloc(1)在串空间中创建一个“洞”,使得对于在洞之前分配的任何字符串,调用isatend都会失败。因而,如果将返回给客户程序的串空间末端地址值作为边界,是不可能有某个字符串跨越这一边界的。

Text_restore恢复current和current->avail的值,释放Text_save_T结构并将*save清零,并释放当前内存块之后所有的其他内存块。

functions

 198〉+≡  
  void Text_restore(Text_save_T *save) {  
      struct chunk *p, *q;  
 
      assert(save && *save);  
      current = (*save)->current;  
      current->avail = (*save)->avail;  
      FREE(*save);  
      for (p = current->link; p; p = q) {  
          q = p->link;  
          FREE(p);  
      }  
      current->link = NULL;  
  }  

16.2.3 分析字符串

Text导出的其余函数都用于检查字符串,这些函数都不会分配新的字符串。

Text_chr在s[i:j]中查找最左侧的某个指定字符:

functions

 198〉+≡  
  int Text_chr(T s, int i, int j, int c) {  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      for ( ; i < j; i++)  
          if (s.str[i] == c)  
              return i + 1;  
      return 0;  
  }  

如果s.str[i]等于c,i+1即为s中该字符左侧的位置。Text_rchr的处理过程类似,但它查找子串中最右侧出现的字符c:

functions

 198〉+≡  
  int Text_rchr(T s, int i, int j, int c) {  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      while (j > i)  
          if (s.str[--j] == c)  
              return j + 1;  
      return 0;  
  }  

Text_upto和Text_rupto类似Text_chr和Text_rchr,但它们会在字符串中查找某个字符集合(通过一个Text_T实例指定)中的任意字符。

functions

 198〉+≡  
  int Text_upto(T s, int i, int j, T set) {  
      assert(set.len >= 0 && set.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      for ( ; i < j; i++)  
          if (memchr(set.str, s.str[i], set.len))  
              return i + 1;  
      return 0;  
  }  
  int Text_rupto(T s, int i, int j, T set) {  
      assert(set.len >= 0 && set.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      while (j > i)  
          if (memchr(set.str, s.str[--j], set.len))  
              return j + 1;  
      return 0;  
  }  

Str_upto和Str_rupto使用了C库函数strchr来检查s中的某个字符是否出现在set中。Text中的对应函数不能使用strchr,因为s和set都可能包含0字符,因此它们使用了memchr函数,该函数并不将0字符解释为字符串结束符。

Text_find和Text_rfind在s[i:j]中查找字符串,这两个函数也有类似的问题:这些函数在Str接口中对应的变体函数使用了strncmp来比较子串,但Text接口中的函数必须使用memcmp,以便处理0字符。Text_find在s[i:j]中搜索最左侧出现的子串str时,将使用memcmp函数。当str为空串或只有一个字符时,这两种特例值得特别注意。

functions

 198〉+≡  
  int Text_find(T s, int i, int j, T str) {  
      assert(str.len >= 0 && str.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (str.len == 0)  
          return i + 1;  
      else if (str.len == 1) {  
          for ( ; i < j; i++)  
              if (s.str[i] == *str.str)  
                  return i + 1;  
      } else  
          for ( ; i + str.len <= j; i++)  
              if (equal(s, i, str))  
                  return i + 1;  
      return 0;  
  }  
 
〈macros

 198〉+≡  
  #define equal(s, i, t

) \  
     (memcmp(&(s

).str[i], (t

).str, (t

).len) == 0)  

在一般情况下,Text_find不可以检查超出子串s[i:j]边界的字符,这也解释了for循环中的结束条件。

Text_rfind类似Text_find,但它搜索最右侧出现的str,它会避免检查s[i:j]之前的字符。

functions

 198〉+≡  
  int Text_rfind(T s, int i, int j, T str) {  
      assert(str.len >= 0 && str.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (str.len == 0)  
          return j + 1;  
      else if (str.len == 1) {  
          while (j > i)  
              if (s.str[--j] == *str.str)  
                  return j + 1;  
      } else  
          for ( ; j - str.len >= i; j--)  
              if (equal(s, j - str.len, str))  
                  return j - str.len + 1;  
      return 0;  
  }  

Text_any查看s中位置i右侧的字符,如果该字符出现在set中,则返回Text_pos(s, i)+1。

functions

 198〉+≡  
  int Text_any(T s, int i, T set) {  
      assert(s.len >= 0 && s.str);  
      assert(set.len >= 0 && set.str);  
      i = idx(i, s.len);  
      assert(i >= 0 && i <= s.len);  
      if (i < s.len && memchr(set.str, s.str[i], set.len))  
          return i + 2;  
      return 0;  
  }  

当s[i]在set中时,Text_any返回i + 2,因为i + 1是s[i]左侧的位置 [2] ,因此i + 2是s[i]右侧的位置。

Text_many和Text_rmany通常在Text_upto和Text_rupto之后调用。它们会跨过一连串属于某个给定集合的字符,并返回第一个不属于该集合的字符左侧的位置。Text_many在s[i:j]中从左向右进行处理:

functions

 198〉+≡  
  int Text_many(T s, int i, int j, T set) {  
      assert(set.len >= 0 && set.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (i < j && memchr(set.str, s.str[i], set.len)) {  
          do  
              i++;  
          while (i < j  
          && memchr(set.str, s.str[i], set.len));  
          return i + 1;  
      }  
      return 0;  
  }  

Text_rmany从s[i:j]末端开始工作,从右向左处理,跨越一连串属于set的字符:

functions

 198〉+≡  
  int Text_rmany(T s, int i, int j, T set) {  
      assert(set.len >= 0 && set.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (j > i && memchr(set.str, s.str[j-1], set.len)) {  
          do  
              --j;  
          while (j >= i  
          && memchr(set.str, s.str[j], set.len));  
          return j + 2;  
      }  
      return 0;  
  }  

当索引j对应的字符不属于set,或j等于i - 1时,do-while循环将结束。在前一种情况下,j + 2是“违例”字符右侧的位置,因而刚好在一连串属于set字符的左侧。在第二种情况下,s[i:j]完全由set中的字符构成,j + 2位于s[i:j]的左侧。

如果s[i:j]开始于字符串str,Text_match会跳过str。类似Text_find,Text_match的两个重要特例是,str为空串和str只有一个字符的情形。Text_match不能查看s[i:j]以外的字符,下述第三个if语句中的条件,确保了只检查s[i:j]中的字符。

functions

 198〉+≡  
  int Text_match(T s, int i, int j, T str) {  
      assert(str.len >= 0 && str.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (str.len == 0)  
          return i + 1;  
      else if (str.len == 1) {  
          if (i < j && s.str[i] == *str.str)  
              return i + 2;  
      } else if (i + str.len <= j && equal(s, i, str))  
          return i + str.len + 1;  
      return 0;  
  }  

Text_rmatch类似Text_match,如果s[i:j]以字符串str结束,那么该函数会返回str之前的位置,该函数不会检查s[i:j]之前的字符。

functions

 198〉+≡  
  int Text_rmatch(T s, int i, int j, T str) {  
      assert(str.len >= 0 && str.str);  
     〈convert

 i and

 j to indices in

 0..s.len 199〉  
      if (str.len == 0)  
          return j + 1;  
      else if (str.len == 1) {  
          if (j > i && s.str[j-1] == *str.str)  
              return j;  
      } else if (j - str.len >= i  
      && equal(s, j - str.len, str))  
          return j - str.len + 1;  
      return 0;  
  }  

16.2.4 转换函数

最后一个函数是Text_fmt,这是一个格式转换函数,供Fmt接口导出的函数使用。Text_fmt用于输出Text_T,其风格与printf的%s格式符相同。它只是调用Fmt_puts,像printf处理C字符串那样,来为Text_T解释flags、width和precision。

functions

 198〉+≡  
  void Text_fmt(int code, va_list *app,  
        int put(int c, void *cl), void *cl,  
        unsigned char flags[], int width, int precision) {  
        T *s;  
 
        assert(app && flags);  
        s = va_arg(*app, T*);  
        assert(s && s->len >= 0 && s->str);  
        Fmt_puts(s->str, s->len, put, cl, flags,  
            width, precision);  
  }  

不同于Text接口中的所有其他函数,Text_fmt会消耗指向Text_T实例的一个指针,而不是Text_T的一个实例。Text_T实例很小,通常是一个双字,但缺乏某种可移植的方法,使得我们能够在可变长度参数列表中将双字长度的结构实例与double区分开。因此,一些C语言实现无法在可变长度参数列表中按值可靠地传递双字结构实例。传递一个指向Text_T实例的指针,在所有的实现中都避免了这些问题。

16.3 扩展阅读

Text_T的语义和实现都类似于SNOBOL4[Griswold,1972]和Icon[Griswold and Griswold,1990]中的字符串。这两种语言都是通用字符串处理语言,其内建特性与Text接口导出的函数很相似。

类似的表示和操作字符串的技术,已经在编译器和其他分析字符串的应用程序中长期使用,XPL编译器生成器[Mckeeman, Horning and Wortman,1970]是一个早期的例子。在所有Text_T都已知的系统中,可使用垃圾收集技术来管理串空间。Icon使用XPL的垃圾收集算法,来回收不被任何已知的Text_T实例引用的串空间[Hanson,1980]。它将已知的Text_T实例包含的字符串复制到串空间的起始处,来使字符串的存储更为紧凑。

[Hansen,1992]描述了字符串的一种完全不同的表示方法,其中的子串描述符承载了足够的信息,可以检索到子串所处的较大字符串。其中需要说明的一点是,这种表示使得字符串可以向左右扩展。

Rope是另一种字符串表示方法,其中字符串由子串构成的树来表示[Boehm, Atkinson and Plass,1995]rope中的字符可以在线性时间内遍历,这几乎与Text_T或C字符串相同,但子串操作需要花费对数时间。但字符串连接要快得多:连接两个rope只需花费常数时间。rope的另一种有用特性是,rope可以通过一个生成第i个字符的函数来描述。

16.4 习题

16.1 重写15.2节中描述的ids.c,使用Text函数。

16.2 Text_save和Text_restore不是很健壮。例如,下列操作序列是错误的,但该错误未被发现。

Text_save_T x, y;  
x = Text_save();  
...  
y = Text_save();  
...  
Text_restore(&x);  
...  
Text_restore(&y);  

在调用Text_restore(&x)之后,y是无效的,因为它描述了x之后的一个串空间位置。修改Text的实现,使得该错误成为一个已检查的运行时错误。

16.3 Text_save和Text_restore只允许栈式分配。垃圾收集可能更好些,但要求所有可访问的Text_T实例都是已知的。设计Text接口的一个扩展版本,其中包含一个用来“注册”Text_T实例的函数,另一个函数Text_compact使用[Hanson,1980]中描述的方案,将所有已注册的Text_T实例引用的字符串“紧缩”到串空间的起始处,以回收被未注册的Text_T实例占据的空间。

16.4 扩展搜索字符串的函数,如Text_find和Text_match,使之能够接受Text_T参数来指定正则表达式 ,而不是只搜索普通的字符串。[Kernighan and Plauger,1976]描述了正则表达式,以及用于匹配正则表达式的自动机的实现。

16.5 基于[Hansen,1992]描述的子串模型,设计一个接口并实现。


[1] 指由Text接口的实现为字符串分配的内存空间,本章中多处引用,故重新命名一个名称。

[2] 原文中所谓的位置,是指字符之间的位置,不是指字符的位置,字符实际上没有位置的。——译者注