# 練習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” 已死