前一章描述了Str接口导出的函数,这些函数增强了C语言处理字符串的约定。按照惯例,字符串是字符的数组,其中最后一个字符是NULL。虽然这种表示适用于许多应用程序,它确实有两个重要的缺点。首先,获取字符串长度需要搜索字符串,查找标志字符串结束的0字符,因此计算长度花费的时间与字符串的长度成正比。其次,Str接口中的函数和标准库中的一些函数假定字符串是可以修改的,因此函数或其调用者必须为结果字符串分配空间,在不修改字符串的应用程序中,许多这种分配是不必要的。
本章描述的Text接口对字符串使用了一种稍有不同的表示,解决了这两个缺点。长度可以在常数时间内计算得到,因为相关信息保存在字符串中,而仅在有必要时才分配内存。Text提供的字符串是不可改变的,即它们无法直接修改,而且其中可能包含嵌入的0字符。Text提供了函数,可以在Text字符串和C风格字符串之间进行转换,这些转换函数是Text接口的改进带来的代价。
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异常。
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字符。
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);
}
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;
}
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; }
最后一个函数是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实例的指针,在所有的实现中都避免了这些问题。
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.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] 原文中所谓的位置,是指字符之间的位置,不是指字符的位置,字符实际上没有位置的。——译者注