笨辦法學C 練習36:更安全的字串

飛龍發表於2019-05-12

練習36:更安全的字串

原文:Exercise 36: Safer Strings

譯者:飛龍

我已經在練習26中,構建devpkg的時候介紹了Better String庫。這個練習讓你從現在開始熟悉bstring庫,並且明白C風格字串為什麼十分糟糕。之後你需要修改liblcthw的程式碼來使用bstring

為什麼C風格字串十分糟糕

當人們談論C的問題時,“字串”的概念永遠是首要缺陷之一。你已經用過它們,並且我也談論過它們的種種缺陷,但是對為什麼C字串擁有缺陷,以及為什麼一直是這樣沒有明確的解釋。我會試著現在做出解釋,部分原因是C風格字串經過數十年的使用,有足夠的證據表明它們是個非常糟糕的東西。

對於給定的任何C風格字串,都不可能驗證它是否有效。

  • ` `結尾的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 ` ` terminated
    while((to[i] = from[i]) != ` `) {
        ++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] = ` `;

    return i;
}

想象你想要向copy函式新增檢查來確保from字串有效。你該怎麼做呢?你編寫了一個迴圈來檢查字串是否已` `結尾。哦,等一下,如果字串不以` `結尾,那它怎麼讓迴圈停下?不可能停下,所以無解。

無論你怎麼做,你都不能在不知道字串長度的情況下檢查C字串的有效性,這裡safercopy包含了程度。這個函式沒有相同的問題,因為他的迴圈一定會中止,即使你傳入了錯誤的大小,大小也是有限的。

譯者注:但是問題來了,對於一個C字串,你怎麼獲取其大小?你需要在這個函式之前呼叫strlen,又是一個無限迴圈問題。

於是,bstring庫所做的事情就是建立一個結構體,它總是包含字串長度。由於這個長度對於bstring來說總是可訪問的,它上面的所有操作都會更安全。迴圈是有限的,內容也是有效的,並且這個主要的缺陷也不存在了。BString庫也帶有大量所需的字串操作,比如分割、格式化、搜尋,並且大多數都會正確並安全地執行。

bstring中也可能有缺陷,但是經過這麼長時間,可能性已經很低了。glibc中也有缺陷,所以你讓程式設計師怎麼做才好呢?

使用 bstrlib

有很多改進後的字串庫,但是我最喜歡bstrlib,因為它只有一個程式集,並且具有大多數所需的字串功能。你已經在使用它了,所以這個練習中你需要從Better String獲取兩個檔案,bstrlib.cbstrlib.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下執行測試,確保記憶體使用正確。

相關文章