处理C字符串时应该注意到的

字符串是一系列字符的有序排列,一个挨着一个排成一串。若要对一个存放在内存中的字符串进行处理,需要知道其首个字符所在的内存地址和其边界。

对于边界的描述,容易想到的有两种方式:一是在字符串中给出该字符串的长度,知道了长度就可得出边界,像这样:

二是在字符串尾部加一个特殊的字符作为标记,只要看到这个字符,就可认定已经到达了字符串的边界,像这样:

这个字符应是不可打印的,以免与真正需要处理的那部分字符想混淆。

C语言使用的是第二种方法,它用ASCII码为0的字符(’\0’)来标识一个字符串的结束。

基于这个大前提,像strcpystrcatstrlen之类的字符串处理函数的工作均要依赖于这个结束标记,也就是说,如果待处理的字符串不带结束标记的话,这些函数就不能正确地完成工作。

如果我们本来就是打算定义一个字符串,依据C语言的语法去做,得到的就一定是带结束标记的,这个没问题。

1
char buf[5] = "1234";

这个buf中存放的这组字节是符合规范的C字符串,末尾的’\0’不用我们显示地给出,C语言会做处理。

而,如果有一个buf,其中存放的字节我们并不确定是什么(不知是否符合C字符串的规则),尤其是从网络上收到的字节流。这时,如果贸然对其使用字符串处理函数的话,就会有问题,这种问题关乎内存数据,是严重的问题,但却不总是立即暴露出来。

例如:

1
2
3
char buf[5];
memset(buf, '1', sizeof(buf));
strlen(buf);

strlen会从首个字节开始记录,直到遇见第一个’\0’字符结束,得出一个length值返回给调用者。此种,情况下运行结果是未定义的,因为这块buf中没有提供’\0’字符。运气好的话,strlen会在不知道什么地方遇到一个’\0’字符,然后停下,给出一个length,这个length显然是要超过5的。

那么,运气不好的话,当strlen访问到某个虚拟页面的时候,这个虚拟页面恰好没有被分配物理页面,此时,就会出现内存非法访问之类的错误,进程就崩溃掉了。

另外,需要strcpy的时候,要使用带n的版本,即:strncpy

原因,跟上面的例子类似:

1
2
3
4
5
char buf[5];
memset(buf, '1', sizeof(buf));
char buf1[5];
memset(buf1, 0, sizeof(buf1));
strcpy(buf1, buf);

把buf的内容copy到buf1中,此时的but没有结束标记,也就是说它根本就不是规范的C语言字符串。在此,使用这个字符串处理函数strcpy必将会出现与上面strlen类似的问题,就是它会不停地找’\0’,要么偶然就找到了,多copy了数据给buf1,要么找不到,且访问到了一个没有分配物理页的虚拟页面导致进程崩溃。

改用strncpy就安全了,指定了长度,就等于告知了边界,它就不会做出格的事情了。

总之,对一组字节使用字符串处理函数的时候,要留意是不是末尾带’\0’的。不带’\0’,就意味着这些函数没法知道边界,你要想办法告诉它们,使用带n的版本,或者不使用针对字符串的函数。