ThinkChat2.0新版上线,更智能更精彩,支持会话、画图、阅读、搜索等,送10W Token,即刻开启你的AI之旅 广告
# 练习36:更安全的字符串 > 原文:[Exercise 36: Safer Strings](http://c.learncodethehardway.org/book/ex36.html) > 译者:[飞龙](https://github.com/wizardforcel) 我已经在练习26中,构建`devpkg`的时候介绍了[Better String](http://bstring.sourceforge.net/)库。这个练习让你从现在开始熟悉`bstring`库,并且明白C风格字符串为什么十分糟糕。之后你需要修改`liblcthw`的代码来使用`bstring`。 ## 为什么C风格字符串十分糟糕 当人们谈论C的问题时,“字符串”的概念永远是首要缺陷之一。你已经用过它们,并且我也谈论过它们的种种缺陷,但是对为什么C字符串拥有缺陷,以及为什么一直是这样没有明确的解释。我会试着现在做出解释,部分原因是C风格字符串经过数十年的使用,有足够的证据表明它们是个非常糟糕的东西。 对于给定的任何C风格字符串,都不可能验证它是否有效。 + 以`'\0'`结尾的C字符串是有效的。 + 任何处理无效C字符串的循环都是无限的(或者造成缓冲区溢出)。 + C字符串没有确定的长度,所以检查它们的唯一方法就是遍历它来观察循环是否正确终止。 + 所以,不通过有限的循环就不可能验证C字符串。 这个逻辑非常简单。你不能编写一个循环来验证C字符串是否有效,因为无效的字符串导致循环永远不会停止。就是这样,唯一的解决方案就是包含大小。一旦你知道了大小,你可以避免无限循环问题。如果你观察练习27中我向你展示的两个函数: > 译者注:检验C风格字符串是否有效等价于“停机问题”,这是一个非常著名的不可解问题。 ```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](http://bstring.sourceforge.net/)获取两个文件,`bstrlib.c`和`bstrlib.h`。 下面是我在`liblcthw`项目目录里所做的事情: ```sh $ 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。下面是差异: ```diff 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`下运行测试,确保内存使用正确。