字符处理从C语言发展的早期开始就非常重要。要知道,C程序趋向于短小而且单一功能的编写。我们用的最多的执行字符集当然是ASCII(American Standard Code for Information Interchange),这是使用非常广泛的字符编码,但不是所有计算机体系结构都是用它。比如IMB就用EBCDIC而不是ASCII。
在我们日常编写代码的时候可能会非常频繁地使用字符判断函数,如果自己写的话,又比较麻烦,而且说实话效率和安全性都不太高。经过多年的积累,C标准库纳入了关于字符处理、判断的头文件ctype.h。一个典型的文本处理程序对输入流中的每个字符会平均调用ctype.h里的函数3次。如果用函数来实现的话,对于如此频繁的调用,开销是非常巨大的。所以一些值得称赞的程序员开发了宏来替代这些函数。说道ctype.h里的宏就不得不说到几个典型的转换表的宏集合。转换表和字符类型建立起对应的映射关系。这是典型的用空间换时间的做法,把标准的字符集合先行存入一个静态转换表中,节省了时间。但是要注意,这只适用于字符集合元素不是太多的情况。比如ASCII用7位或者8位表示一个字符类型。这样的话,字符集合元素最多也才256个。但是有的甚至用32位表示字符类型,这样的话,字符集合元素最多可以有65536个。这个要是用转换表的就有点难受了。
关于ctype.h头文件:
#ifndef MY_CTYPE_H /**保护宏*/
#define MY_CTYPE_H
#define _XA 0x200 /**额外的字母, 0x200 == 2^9*/
#define _XS 0x100 /**额外的空格, 0x100 == 2^8*/
#define _BB 0x80 /**警报符BEL, 退格符BS, etc., 0x80 == 2^7*/
#define _CN 0x40 /**5个运动控制字符CR,FF,HT,NL,NT, x040 == 2^6*/
#define _DI 0x20 /**十进制字符'0'-'9', 0x20 == 2^5*/
#define _LO 0x10 /**小写字母字符'a'-'z', 0x10 == 2^4*/
#define _PU 0x08 /**标点字符, 0x08 == 2^3*/
#define _SP 0x04 /**空格, 0x04 == 2^2*/
#define _UP 0x02 /**大写字母'A'-'Z', 0x02 == 2^1*/
#define _XD 0x01 /**十六进制字符, 0x02 == 2^0*/
/**函数声明*/
int isalnum(int), isalpha(int), iscntrl(int), isdigit(int);
int isgraph(int), islower(int), isprint(int), ispunct(int);
int isspace(int), isupper(int), isxdigit(int);
int tolower(int), toupper(int);
/**变量声明*/
extern const short *_Ctype, *_Tolower, *_Toupper;
/**宏定义*/
#define isalnum(c) (_Ctype[(int)(c)] & (_DI | _LO | _UP | _XA))
#define isalpha(c) (_Ctype[(int)(c)] & (_LO | _UP | _XA))
#define iscntrl(c) (_Ctype[(int)(c)] & (_BB | _CN))
#define isdigit(c) (_Ctype[(int)(c)] & _DI)
#define isgraph(c) (_Ctype[(int)(c)] & (_DI | _LO | _PU | _UP | _XA))
#define islower(c) (_Ctype[(int)(c)] & _LO)
#define isprint(c) \
(_Ctype[(int)(c)] & (_DI | _LO | _PU | _SP | _UP | _XA))
#define ispunct(c) (_Ctype[(int)(c)] & _PU)
#define isspace(c) (_Ctype[(int)(c)] & (_CN | _SP | _XS))
#define isupper(c) (_Ctype[(int)(c)] & _UP)
#define isxdigit(c) (_Ctype[(int)(c)] & _XD)
#define tolower(c) _Tolower[(int)(c)]
#define toupper(c) _Toupper[(int)(c)]
#endif
首先当然是要编写保护宏。然后是一些宏定义:
#define _XA 0x200 /**额外的字母, 0x200 == 2^9*/
#define _XS 0x100 /**额外的空格, 0x100 == 2^8*/
#define _BB 0x80 /**警报符BEL, 退格符BS, etc., 0x80 == 2^7*/
#define _CN 0x40 /**5个运动控制字符CR,FF,HT,NL,NT, x040 == 2^6*/
#define _DI 0x20 /**十进制字符'0'-'9', 0x20 == 2^5*/
#define _LO 0x10 /**小写字母字符'a'-'z', 0x10 == 2^4*/
#define _PU 0x08 /**标点字符, 0x08 == 2^3*/
#define _SP 0x04 /**空格, 0x04 == 2^2*/
#define _UP 0x02 /**大写字母'A'-'Z', 0x02 == 2^1*/
#define _XD 0x01 /**十六进制字符, 0x02 == 2^0*/
为什么要定义_XS == 0x100,_BB == 0x80等等。这个就要涉及到位运算了,按位与’&’和按位或’|’。注意将这些宏定义替换的文本转换成二进制:
1,000,000,000 /**_XA*/
100,000,000 /**_XS*/
10,000,000 /**_BB*/
1,000,000 /**_CN*/
100,000 /**_DI*/
10,000 /**_LO*/
1,000 /**_PU*/
100 /**_SP*/
10 /**UP*/
1 /**_XD*/
现在明白为什么是0x200、0x100、0x80…0x01了吧。这里有10种不同的字符类型,转换表中有128个字符。我们通过按位与’&’来判断一个字符是不是属于某一类型的字符。那就要求这十种类型在二进制形式下按位与之后是唯一的,所以10种类型,就分别设置二进制某一位为1其他全为0。也就是说二进制的每一位都保存有一个信息,这就充分利用了资源。看看头文件中还有按位或’|’,这个就好理解了,比如(_BB | _CN)
我们知道_BB类型二进制第8位为1其他为0,_CN类型第7位为0,其他为0.按位或之后二进制第8、7位都为1,其他为0。前面我们说过每一位都保存一个属性(是不是属于某一类型),这里就表示,这个字符即属于_BB又属于_CN,但不输入其他类型。这里的类型顺序可以按照你自己的喜好更改,比如你可以设置_XD为0x200,而设置_XA为0x01,等等。但是这样的话,就要更改你对应映射关系的转换表了。
其中一个转换表如下:
#include <ctype.h>
#include <limits.h>
#include <stdio.h>
#if -1 != EOF || 255 != UCHAR_MAX
#error WRONG CTYPE TABLE
#endif
/**宏定义*/
#define XDI (_DI | _XD)
#define XLO (_LO | _XD)
#define XUP (_UP | _XD)
static const short ctyp_tab[257] = {0,
_BB, _BB, _BB, _BB, _BB, _BB, _BB, _BB,
_BB, _CN, _CN, _CN, _CN, _CN, _BB, _BB,
_BB, _BB, _BB, _BB, _BB, _BB, _BB, _BB,
_BB, _BB, _BB, _BB, _BB, _BB, _BB, _BB,
_SP, _PU, _PU, _PU, _PU, _PU, _PU, _PU,
_PU, _PU, _PU, _PU, _PU, _PU, _PU, _PU,
XDI, XDI, XDI, XDI, XDI, XDI, XDI, XDI,
XDI, XDI, _PU, _PU, _PU, _PU, _PU, _PU,
_PU, XUP, XUP, XUP, XUP, XUP, XUP, _UP,
_UP, _UP, _UP, _UP, _UP, _UP, _UP, _UP,
_UP, _UP, _UP, _UP, _UP, _UP, _UP, _UP,
_UP, _UP, _UP, _PU, _PU, _PU, _PU, _PU,
_PU, XLO, XLO, XLO, XLO, XLO, XLO, _LO,
_LO, _LO, _LO, _LO, _LO, _LO, _LO, _LO,
_LO, _LO, _LO, _LO, _LO, _LO, _LO, _LO,
_LO, _LO, _LO, _PU, _PU, _PU, _PU, _BB,
}; /**这里只写了129个*/
const short *_Ctype = &ctyp_tab[1];
这个转换表对应的就是ASCII表。0表示NUL,然后依次填写字符的类型。比如从ASCII第65个开始,到第90个。我们知道这是大写字母’A’~’Z’,我们看转换表中填写的类型,XUP到_PU之前的最后一个_UP。如果你现在调用的是int isupper(int)
,如果你输入的参数’A’,那么通过转换知到’A’的值是65,所以在数组中找到ctyp_tab[66],也就是XUP(_UP | _XD的结果).然后XUP & _UP,看结果是不是_UP,如果是那么’A’就是_UP类型。反之,则不然。判断结果,’A’确实是_UP类型。如果你输入的是’a’,也就是97,在数组中找到ctyp_tab[98],也就是XLO,然后XLO & _UP,看结果是不是_UP,计算得知不是_UP,那么你输入的就不是_UP类型。等等,值是65、97,为什么是[66]、[98]。注意代码,const short *_Ctype = &ctyp_tab[1];
,我们用的指针指向的是表中的第二个元素也就是ctyp_tab[1];所以,_Ctype[i] == ctyp_tab[i + 1]。
现在我们说说一个有趣的事儿,注意如下代码:
#include <stdio.h>
#define test_f2(x) ((x) < 0 ? -1 : 1)
int ((((((((((test_f1))))))))))(void)
{
return 1;
}
int (test_f2)(int x)
{
return x;
}
int main()
{
int x = 12;
printf("%d\n", (((test_f1)))());
printf("%d\n", test_f2(x));
printf("%d\n", (((test_f2)))(x));
return 0;
}
有的人可能以为这是段错误代码,但是我要告诉你这是正确的,你加上-wall编译还是一样的正确。由这段代码引出两个问题。第一个,关于屏蔽宏,也就是禁止宏展开。我们注意到有宏定义test_f2和函数test_f2,如果不写括号,我们调用函数test_f2就会被宏替代,有时替代过后会出现语法错误,比如:
#define f(x) 10
int f(int x){return (x = 123);}
如果我们调用函数f(x),f(x)会被宏替代成10,这不是我们想要的,显然会出现问题。如何避免呢?可以用括号。在同名的函数名字外加上括号,至于数量是不设限制的,随你喜好,你可以加上10个也没问题。调用宏就用f(x),调用函数就用(f)(x)参看(test_f2)。至于test_f1,你开心就好,我也不知道加了多少个,但是编译器会忽略多余的括号。