# 练习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`下运行测试,确保内存使用正确。
- 笨办法学C 中文版
- 前言
- 导言:C的笛卡尔之梦
- 练习0:准备
- 练习1:启用编译器
- 练习2:用Make来代替Python
- 练习3:格式化输出
- 练习4:Valgrind 介绍
- 练习5:一个C程序的结构
- 练习6:变量类型
- 练习7:更多变量和一些算术
- 练习8:大小和数组
- 练习9:数组和字符串
- 练习10:字符串数组和循环
- 练习11:While循环和布尔表达式
- 练习12:If,Else If,Else
- 练习13:Switch语句
- 练习14:编写并使用函数
- 练习15:指针,可怕的指针
- 练习16:结构体和指向它们的指针
- 练习17:堆和栈的内存分配
- 练习18:函数指针
- 练习19:一个简单的对象系统
- 练习20:Zed的强大的调试宏
- 练习21:高级数据类型和控制结构
- 练习22:栈、作用域和全局
- 练习23:认识达夫设备
- 练习24:输入输出和文件
- 练习25:变参函数
- 练习26:编写第一个真正的程序
- 练习27:创造性和防御性编程
- 练习28:Makefile 进阶
- 练习29:库和链接
- 练习30:自动化测试
- 练习31:代码调试
- 练习32:双向链表
- 练习33:链表算法
- 练习34:动态数组
- 练习35:排序和搜索
- 练习36:更安全的字符串
- 练习37:哈希表
- 练习38:哈希算法
- 练习39:字符串算法
- 练习40:二叉搜索树
- 练习41:将 Cachegrind 和 Callgrind 用于性能调优
- 练习42:栈和队列
- 练习43:一个简单的统计引擎
- 练习44:环形缓冲区
- 练习45:一个简单的TCP/IP客户端
- 练习46:三叉搜索树
- 练习47:一个快速的URL路由
- 后记:“解构 K&R C” 已死