從cTags的vString學習動態字符串

在ctags中,vString分佈在`vString.h`和`vString.c`兩個文件中,代碼很是簡潔,加起來僅有300餘行,是初學c語言與動態字符串一個很好研究的範例。
sql

結構定義

在vString.h中,vString定義以下:
安全

struct sVString {
        size_t  length;  /* size of buffer used */
        size_t  size;    /* allocated size of buffer */
        char   *buffer;  /* location of buffer */
};

這也差很少是絕大多數動態字符串的實現方式。函數

buffer存儲內容,size存儲實際內容長度,按理說這樣兩個成員已經能夠起到vString的做用了,可是爲了速度起見,仍是增長了length變量,使得每次增長新內容時不是分配一個單位大小,而是到達上限後一次分配多個單位。測試

爲方便起見,在定義下面的函數的時候,若是一個參數是vString,另外一個參數能夠是vString也能夠是char *的,統一隻定義了vString版本,由於vString.buffer自己就是一個傳統的char* 字符串。在這裏經過宏,構造出char* 版本:spa

#define vStringValue(vs)      ((vs)->buffer)
#define vStringCat(vs,s)      vStringCatS((vs), vStringValue((s)))

exxx

凡是帶exxx的函數(如eFree),其實是在原函數(如Free)上加上了相應的檢查,若是有錯誤就報錯,這種技術在sqlite中也有使用。指針

xXXX

帶x前綴的函數是本身實現的方便書寫的宏。
調試

#define xMalloc(n,Type)    (Type *)eMalloc((size_t)(n) * sizeof (Type)) //分配n個Type類型並初始化
#define xCalloc(n,Type)    (Type *)eCalloc((size_t)(n), sizeof (Type))
#define xRealloc(p,n,Type) (Type *)eRealloc((p), (n) * sizeof (Type))

DebugStatement(x)

#ifdef DEBUG
# define DebugStatement(x) x
#else
# define DebugStatement(x)
#endif

括號內的語句只有在定義了DEBUG宏時纔會被編譯,這樣對於調試單條語句減小了代碼量。
code

函數定義

vString *vStringNew (void);

新建一個vString並返回指針。sqlite

vString *vStringNew (void)
{
    vString *const string = xMalloc (1, vString);
    string->length = 0;
    string->size   = vStringInitialSize;
    string->buffer = xMalloc (string->size, char);
    vStringClear (string);
    return string;
}

vStringInitialSize是32,就像標準庫sort的那個常數同樣是實驗出來的。太大佔用空間,過小開始時刻須要頻繁分配內存影響速度。內存

vString *const,後置const不容許修改指針自己,涉及到指針操做在定義時就應該給予其最小的權限,這樣即便誤操做也有可能被編譯器檢查出來。

void vStringClear (vString *const string)

清空vString。

void vStringClear (vString *const string)
{
    string->length = 0;
    string->buffer [0] = '\0';
    DebugStatement ( memset (string->buffer, 0, string->size); )
}

以char*的一般規範,首位填\0,長度清空便可。注意這裏clear的是buffer的大小,對字符串自己的size沒有變更。默認狀況下不用buffer清零,由於以後使用該部分時必然是先賦值的。

void vStringSetLength (vString *const string)

自動根據buffer中實際內容大小設置size。

void vStringSetLength (vString *const string)
{
    string->length = strlen (string->buffer);
}

使用strlen,避免重複造輪子。

void vStringDelete (vString *const string)

完全清空vString空間。

void vStringDelete (vString *const string)
{
    if (string != NULL)
    {
        if (string->buffer != NULL)
            eFree (string->buffer);
        eFree (string);
    }
}

預先檢查,防止重複清空。

void vStringResize (vString *const string, const size_t newSize)

給buffer分配新的大小。

void vStringResize (vString *const string, const size_t newSize)
{
    char *const newBuffer = xRealloc (string->buffer, newSize, char);

    string->size = newSize;
    string->buffer = newBuffer;
}

不然可能會發生實際給buffer賦值時賦值到了未定義區段,形成程序崩潰或內存被篡改等問題。

realloc有可能新的指針和舊指針同樣,也有可能不一樣。

boolean vStringAutoResize (vString *const string)

精華。決定了一次分配多大空間。

 boolean vStringAutoResize (vString *const string)
{
    boolean ok = TRUE;
    if (string->size <= INT_MAX / 2)
    {
        const size_t newSize = string->size * 2;
        vStringResize (string, newSize);
    }
    return ok;
}

若是有空間則分配當前空間的兩倍。

這個也屬於一些經驗之道,由於通常來講,一個字符串自己的大小越大,它須要更大空間可能性越大,這個方法也在這裏使用。

void vStringPut (vString *const string, const int c)

新增長一個字符。

注意到因爲這個函數用得很是頻繁,爲了減小調用開支,定義了一個宏版本。

void vStringPut (vString *const string, const int c)
{
    if (string->length + 1 == string->size)  /*  check for buffer overflow */
        vStringAutoResize (string);

    string->buffer [string->length] = c;
    if (c != '\0')
        string->buffer [++string->length] = '\0';
}
#define vStringPut(s,c) \
    (void)(((s)->length + 1 == (s)->size ? vStringAutoResize (s) : 0), \
    ((s)->buffer [(s)->length] = (c)), \
    ((c) == '\0' ? 0 : ((s)->buffer [++(s)->length] = '\0')))
#endif

在string->length+1==string->size時,因爲有末尾\0實際上存儲已滿,故增大buffer大小。

注意不能添加'\0',不然結果會錯誤。

宏版本用,運算符精煉地鏈接了多個語句,用?來進行if的做用。

void vStringCatS (vString *const string, const char *const s)

將s的內容增長到string的後面。

void vStringCatS (vString *const string, const char *const s)
{
#if 1
    const size_t len = strlen (s);
    while (string->length + len + 1 >= string->size)/*  check for buffer overflow */
        vStringAutoResize (string);
    strcpy (string->buffer + string->length, s);
    string->length += len;
#else
    const char *p = s;
    do
        vStringPut (string, *p);
    while (*p++ != '\0');
#endif
}

後面那一段是本來註釋掉的內容,仍是一句話:strcpy這些標準庫內置的東西,在實現時都是通過反覆測試的,在絕大多數狀況下比本身寫的要快要安全,不要重複造輪子。

爲何不用strcat?

由於在這裏, string的結束地址是已知的(咱們已經記錄了length),strcat會從頭掃一遍string,效率過低。

void vStringNCatS (vString *const string, const char *const s, const size_t length)

將s的前length個字符拷貝到string中。

void vStringNCatS (
        vString *const string, const char *const s, const size_t length)
{
    const char *p = s;
    size_t remain = length;

    while (*p != '\0'  &&  remain > 0)
    {
        vStringPut (string, *p);
        --remain;
        ++p;
    }
    vStringTerminate (string); //在buffer末尾添加'\0'
}

這裏爲何不使用strncpy?動態擴容當然是一方面,但若是咱們一開始就擴容好呢?咱們看一下strncpy的狀況:

  • If count is reached before the entire string src was copied, the resulting character array is not null-terminated.

  • If, after copying the terminating null character from src, count is not reached, additional null characters are written to dest until the total of count characters have been written.

簡而言之:

  1. 在length<s長度時,strncpy不會添加'\0'

  2. 在length==s長度時,strncpy會添加'\0'

  3. 在length>s長度時,strncpy會添加多個'\0'

而咱們是要始終末尾有'\0'。雖然咱們能夠在末尾強制加一個'\0'快速解決問題,但在狀況3時,效率低。

相關文章
相關標籤/搜索