练习 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
$ vim src/lcthw/bstrlib.c
$ make clean all
...
$
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 下运行测试,确保内存使用正确。