练习36:更安全的字符串
我已经在练习26中,构建devpkg
的时候介绍了Better String库。这个练习让你从现在开始熟悉bstring
库,并且明白C风格字符串为什么十分糟糕。之后你需要修改liblcthw
的代码来使用bstring
。
为什么C风格字符串十分糟糕
当人们谈论C的问题时,“字符串”的概念永远是首要缺陷之一。你已经用过它们,并且我也谈论过它们的种种缺陷,但是对为什么C字符串拥有缺陷,以及为什么一直是这样没有明确的解释。我会试着现在做出解释,部分原因是C风格字符串经过数十年的使用,有足够的证据表明它们是个非常糟糕的东西。
对于给定的任何C风格字符串,都不可能验证它是否有效。
- 以
'\0'
结尾的C字符串是有效的。 - 任何处理无效C字符串的循环都是无限的(或者造成缓冲区溢出)。
- C字符串没有确定的长度,所以检查它们的唯一方法就是遍历它来观察循环是否正确终止。
- 所以,不通过有限的循环就不可能验证C字符串。
这个逻辑非常简单。你不能编写一个循环来验证C字符串是否有效,因为无效的字符串导致循环永远不会停止。就是这样,唯一的解决方案就是包含大小。一旦你知道了大小,你可以避免无限循环问题。如果你观察练习27中我向你展示的两个函数:
译者注:检验C风格字符串是否有效等价于“停机问题”,这是一个非常著名的不可解问题。
void copy(char to[], char from[])
{
int i = 0;
// while loop will not end if from isn't '\0' terminated
while((to[i] = from[i]) != '\0') {
++i;
}
}
int safercopy(int from_len, char *from, int to_len, char *to)
{
int i = 0;
int max = from_len > to_len - 1 ? to_len - 1 : from_len;
// to_len must have at least 1 byte
if(from_len < 0 || to_len <= 0) return -1;
for(i = 0; i < max; i++) {
to[i] = from[i];
}
to[to_len - 1] = '\0';
return i;
}
想象你想要向copy
函数添加检查来确保from
字符串有效。你该怎么做呢?你编写了一个循环来检查字符串是否已'\0'
结尾。哦,等一下,如果字符串不以'\0'
结尾,那它怎么让循环停下?不可能停下,所以无解。
无论你怎么做,你都不能在不知道字符串长度的情况下检查C字符串的有效性,这里safercopy
包含了程度。这个函数没有相同的问题,因为他的循环一定会中止,即使你传入了错误的大小,大小也是有限的。
译者注:但是问题来了,对于一个C字符串,你怎么获取其大小?你需要在这个函数之前调用
strlen
,又是一个无限循环问题。
于是,bstring
库所做的事情就是创建一个结构体,它总是包含字符串长度。由于这个长度对于bstring
来说总是可访问的,它上面的所有操作都会更安全。循环是有限的,内容也是有效的,并且这个主要的缺陷也不存在了。BString库也带有大量所需的字串操作,比如分割、格式化、搜索,并且大多数都会正确并安全地执行。
bstring
中也可能有缺陷,但是经过这么长时间,可能性已经很低了。glibc
中也有缺陷,所以你让程序员怎么做才好呢?
使用 bstrlib
有很多改进后的字符串库,但是我最喜欢bstrlib
,因为它只有一个程序集,并且具有大多数所需的字符串功能。你已经在使用它了,所以这个练习中你需要从Better String获取两个文件,bstrlib.c
和bstrlib.h
。
下面是我在liblcthw
项目目录里所做的事情:
$ mkdir bstrlib
$ cd bstrlib/
$ unzip ~/Downloads/bstrlib-05122010.zip
Archive: /Users/zedshaw/Downloads/bstrlib-05122010.zip
...
$ ls
bsafe.c bstraux.c bstrlib.h bstrwrap.h license.txt test.cpp
bsafe.h bstraux.h bstrlib.txt cpptest.cpp porting.txt testaux.c
bstest.c bstrlib.c bstrwrap.cpp gpl.txt security.txt
$ mv bstrlib.h bstrlib.c ../src/lcthw/
$ cd ../
$ rm -rf bstrlib
# make the edits
$ vim src/lcthw/bstrlib.c
$ make clean all
...
$
在第14行你可以看到,我编辑了bstrlib.c
文件,来将它移动到新的位置,并且修复OSX上的bug。下面是差异:
25c25
< #include "bstrlib.h"
---
> #include <lcthw/bstrlib.h>
2759c2759
< #ifdef __GNUC__
---
> #if defined(__GNUC__) && !defined(__APPLE__)
我把包含修改为<lcthw/bstrlib.h>
,然后修复2759行ifdef
的问题。
学习使用该库
这个练习很短,只是让你准备好剩余的练习,它们会用到这个库。接下来两个联系中,我会使用bstrlib.c
来创建Hashmap`数据结构。
你现在应该阅读头文件和实现,之后编写tests/bstr_tests.c
来测试下列函数,来熟悉这个库:
bfromcstr
从C风格字符串中创建一个bstring
。
blk2bstr
与上面相同,但是可以提供缓冲区长度。
bstrcpy
复制bstring
。
bassign
将一个bstring
赋值为另一个。
bassigncstr
将bsting
的内容设置为C字符串的内容。
bassignblk
将bsting
的内容设置为C字符串的内容,但是可以提供长度。
bdestroy
销毁bstring
。
bconcat
在一个bstring
末尾连接另一个。
bstricmp
比较两个bstring
,返回值与strcmp
相同。
biseq
检查两个bstring
是否相等。
binstr
判断一个bstring
是否被包含于另一个。
bfindreplace
在一个bstring
中寻找另一个,并且将其替换为别的。
bsplit
将bstring
分割为bstrList
。
bformat
执行字符串格式化,十分便利。
blength
获取bstring
的长度。
bdata
获取bstring
的数据。
bchar
获得bstring
中的字符。
你的测试应该覆盖到所有这些操作,以及你从头文件中发现的更多有趣的东西。在valgrind
下运行测试,确保内存使用正确。