分类 技术志 下的文章

P.J.Plauger版本C标准库实现分析之'ctype.h'

如果在你的源代码中经常见到如下代码:
/* To Identify a letter */
if ((i >= 'a' && i = 'A' && i <= 'Z'))

/* To Identify a digit */
if ( i >= '0' && i <= '9')

这说明你对头文件理解的不是很好,而也恰恰是为了减少代码中重复出现的各种'字符分类'代码而设置的。

中的接口常用来进行数据的校验和分类,如在我们的项目中它常被用来校验原始数据的'符合性'。比如说一个11位的手机号码就必须是一个全数字的字符串,我们可以选择'isdigit'来进行测试,如果返回失败,则说明原始数据不符合要求,校验失败。

首先这里有两件事不会提及,首先是对中各个接口的说明,你可以参见'ANSI C标准'文档,也可以参考各种C手册来找到你的答案;另外一点就是暂不考虑locale对中各种接口行为的影响问题,我们仅仅在'C' locale的范围内考虑问题。

Ok,有了上面两个前提,我们就可以考虑如何实现了。传统的方案,同时也是P.J.Plauger实现方案之一,那就是使用'Translation Table'和宏。宏的好处大家都很明了,不外乎可读性好+性能优越,它也是C程序员一直偏爱的工具,尽管现在很多人对之嗤之以鼻,我们依然在很多的源代码中大量的见到它的身影。

P.J.Plauger的实现方案有三个值得注意的地方:
首先我们来看看他声明的(摘录其中一部分)

/* ctype.h */
#ifndef _CTYPE
#define _CTYPE

/* _Ctype code bits */
#define _XA 0×200 /* extra alphabetic */
#define _XS 0×100 /* extra space */
#define _BB 0×80 /* BEL, BS, etc */
#define _CN 0×40 /* CR, FF, HT, NL, VT */
#define _DI 0×20 /* '0' ~ '9' */
#define _LO 0×10 /* 'a' ~ 'z' */
#define _PU 0×08 /* punctuation */
#define _SP 0×04 /* space */
#define _UP 0×02 /* 'A' ~ 'Z' */
#define _XD 0×01 /* '0' ~ '9', 'a' ~ 'z', 'A' ~ 'Z' */

int isdigit(int);
extern const short *_Ctype;
….

#define isdigit(c) (_Ctype[(int)(c)] & _DI)

#endif

/* isdigit.c */
#include

#define XDI (_DI|_XD)
… …

int (isdigit)(int c)
{
 return (_Ctype[c] & _DI);
}

中有一处奇怪的地方,那就是每个接口函数都有一个同名的宏与之对应。再看看isdigit.c中isdigit接口的实现是int (isdigit)(int c),而不是int isdigit(int c),如果是后者,编译都会有问题。不是很了解P.J.Plauger为什么要这么做,Maybe是为了提供多种字符处理的方案,你可以这样来使用宏:
int a = 0;
a = isdigit(5);

同样你也可以这样来选择使用函数接口:
int  b = 0;
b = (isdigit)(5);

第二个值得注意的地方就是'Translation Table'转换的原理了,以检查digit为例,先看看ctype_tab表是什么样子的:
static const short ctype_tab[257] = { 0, /* EOF */
…, …, …,
…, …, …,



…, XDI, …,

};
const short * _Ctype = &ctype_tab[1];

注意这个表支持另外一个额外的值'EOF'宏,这样表的大小就是257,而非256了。

当我们调用isdigit的时候,如:
c = '5';
if (isdigit(c)) {
 printf("c is a digit\n");
} else {
 printf("c is not a digit\n");
}

如上面isdigit实现,它把参数作为index在转换表中找到相应表项,然后与_DI宏做'与'操作。如c = '5',其在ASCII码表中的index为53,我们在转换表中找出index为53的那个表项是XDI,然后XDI & _DI,结果为真。当然转换表中的表项都是事先按照ASCII码标安排好的。

第三个值得注意的地方就是ctype_tab数组类型为short。按照P.J.Plauger的说法他之所以选择short而非unsigned char类型是因为他觉得这样的实现拥有最大的portability,易于以后支持其他各种locale。当然如果你能完全排除支持其他locale的念头,你大可使用unsigned char,而且这样可以更好的节省空间。

中还提供toupper和tolower两个接口,这两个接口的实现也分别各需要一个转换表。这里就不详细叙述了。

附录
P.J.Plauger版本C标准库实现分析之'assert.h'

P.J.Plauger版本C标准库实现分析之'assert.h'

I believe that seeing a realistic implementation of the Standard C library can help you better understand how to use it.
                                                                                  — P.J.Plauger

按照字母序首先我们来看看<assert.h>,这个文件提供的接口功能很简单,但却是我们极其常用的功能-断言机制(如果条件为False,则输出Diagnostics信息,然后Abort)。当然在最终产品中使用断言并不是一种好的方法,不过断言是一种很有用的帮助我们调试程序的好工具。

我们一般在程序的调试版本中使用断言机制,一般用来对Input进行Validation,输出一些Diagnostics信息。如:
assert((idx > 10) && (idx < 100));

<assert.h>中提供一个宏assert,这个宏的功能由另一个宏NDEBUG(标志是否是DEBUG版本)决定。如果NDEBUG宏在你include <assert.h>时没有被定义,这时断言功能开启;否则断言功能关闭。如:

#define NDEBUG
#include <assert.h> /* 此时断言功能关闭 */

你也大可不必在你的各个源文件中控制断言功能的开关,在编译器选项中同样可以定义NDEBUG宏,如gcc -DNDEBUG test.c,当然对于大的project,这些是应该放在Makefile中的,这样的结果就相当于在你所有#include <assert.h>的地方之前定义了NDEBUG宏,也就是说在每个编译单元中,断言功能都是关闭的。

assert宏看起来很简单,但是由于其是C标准库提供的接口,所以在实现的时候需要考虑的更加细致和全面一些。从上面的叙述上来看assert.h文件的结构应该大致如下:
#undef assert
#ifdef NDEBUG
#define assert(test) ((void)0)
#else
#define assert(test) …
#endif

我们可以很轻松的就拿出一个assert的实现版本:
/* NDEBUG not defined */
#define assert(test) if (!(test)) \
 fprintf(stderr, "Assertion Failed: %s, file %s, line %d\n", \
 #test, __FILE__, __LINE__); \

那么这个版本的实现可以接受不,答案是不能。原因有以下几点:
1) 这个实现中直接用到了stderr和fprintf,这两个符号都是在<stdio.h>中声明的,但是C标准库头文件基本上都是各自独立的,在<assert.h>中是不会再包含其他头文件的,那么这就要求使用assert的程序自己包含<stdio.h>,这显然不符合一个C标准库的基本要求;
2) assert宏应该最终展开为一个void expression,因为用户很可能在他们的程序中写出像(assert(0 < x), x < y)这样的代码来,而在上面的实现版本中,显然assert展开后不是一个void expression。

我们再来看看P.J.Plauger的实现版本:
/* NDEBUG not defined */
void _Assert(char *);
#define _STR(x) _VAL(x)
#define _VAL(x) #x
 
#define assert(test) (test) ? (void)0 \
 : _Assert(__FILE__ ":" _STR(__LINE__) " " #test)

/* in xassert.c */
#include <assert.h>
#include <stdio.h>

void _Assert(char *msg) {
 fprintf(stderr, "%s — assertion failed\n", msg);
 abort();
}
 
分析一下这一版本的实现,首先assert宏并没有直接调用任何库输出函数,而是调用了一个自己实现的函数_Assert,把向stderr输出诊断信息的活都交给了_Assert。_STR和_VAL是两个辅助宏,用来将__LINE__字符串化。这里比较难懂的地方就是_Assert(__FILE__ ":" _STR(__LINE__) " " #test)这一句,其实这个也很好理解。看看下面语句的执行结果:
printf("%s\n", "Hello" " " "Tony!");
执行上面语句你会看到Hello Tony!,这样一来实际上_Assert(__FILE__ ":" _STR(__LINE__) " " #test)就可以被理解为:
_Assert("THE_FILENAME_STRING" ":" "THE_LINE_STRING" " " "THE_TEST_STRING")

如发现本站页面被黑,比如:挂载广告、挖矿等恶意代码,请朋友们及时联系我。十分感谢! Go语言第一课 Go语言精进之路1 Go语言精进之路2 Go语言编程指南
商务合作请联系bigwhite.cn AT aliyun.com

欢迎使用邮件订阅我的博客

输入邮箱订阅本站,只要有新文章发布,就会第一时间发送邮件通知你哦!

这里是 Tony Bai的个人Blog,欢迎访问、订阅和留言! 订阅Feed请点击上面图片

如果您觉得这里的文章对您有帮助,请扫描上方二维码进行捐赠 ,加油后的Tony Bai将会为您呈现更多精彩的文章,谢谢!

如果您希望通过微信捐赠,请用微信客户端扫描下方赞赏码:

如果您希望通过比特币或以太币捐赠,可以扫描下方二维码:

比特币:

以太币:

如果您喜欢通过微信浏览本站内容,可以扫描下方二维码,订阅本站官方微信订阅号“iamtonybai”;点击二维码,可直达本人官方微博主页^_^:
本站Powered by Digital Ocean VPS。
选择Digital Ocean VPS主机,即可获得10美元现金充值,可 免费使用两个月哟! 著名主机提供商Linode 10$优惠码:linode10,在 这里注册即可免费获 得。阿里云推荐码: 1WFZ0V立享9折!


View Tony Bai's profile on LinkedIn
DigitalOcean Referral Badge

文章

评论

  • 正在加载...

分类

标签

归档



View My Stats