ctype.h

本文介绍了C语言中的ctype.h头文件,它包含了一系列用于字符处理的函数和宏,主要用于ASCII字符集。文章详细讲解了ctype.h中的转换表和宏定义,以及如何利用位运算进行字符类型的判断,同时探讨了如何避免宏展开导致的问题。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

字符处理从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,你开心就好,我也不知道加了多少个,但是编译器会忽略多余的括号。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值