C語言做爲一門古老的高級語言,對於字符串的支持十分的薄弱。redis
入門時咱們就知道咱們使用數組來包含一串的ASCII字符來做爲字符串的實現,如數組
char arr[] = "hello world!";
這樣基於長度固定的數組的實現方式就致使了C的字符串的長度是不可變的,可是arr[]
的內容倒是可變的。數據結構
這樣的設計致使不少時候咱們對字符串的處理十分的麻煩與危險,像我以前寫的哈夫曼編碼解碼的時候,爲了盛放解碼後的結果,我不得不建立一個很是大的靜態數組或者動態分配內存來放置函數產生的長度不定的字符串。函數
相較於其後輩(如Python/Java,C++基本兼容C的語法,儘管C++實現了本身的string類),C在不少方面也是比較異類的,好比C使用'\0'
來標誌字符串的結束,於是len(arr)
這樣的操做的複雜度就達到了O(n),這是一個比較大的開銷,而Pascal/Python等的實現均可以作到O(1),同時,因爲char
類型自己就是最短的整型再加上C語言的弱類型的類型系統,'a'- 32
也是徹底有效的語法,而在Python中這會引起*TypeError*
. 這些問題在C語言誕生的年代不是大問題,畢竟當時沒有那麼多字符串的處理需求,並且C主要的應用場景也比較偏底層。學習
而如今,一些選擇C實現的程序須要頻繁的處理字符串(如 Redis
,須要頻繁的處理鍵值對),爲了應對這種場景,不少頗有意思的本身的String實現都被提了出來。flex
在這裏我主要是介紹ccan的xstring和sds的一些實現的思路。優化
/** * struct xstring - string metadata * @str: pointer to buf * @len: current length of buf contents * @cap: maximum capacity of buf * @truncated: -1 indicates truncation */ typedef struct xstring { char *str; size_t len; size_t cap; int truncated; } xstring; xstring *xstrNew(const size_t size) { char *str; xstring *x; if (size < 1) { errno = EINVAL; return NULL; } str = malloc(size);//mark 1 if (!str) return NULL; x = malloc(sizeof(struct xstring));//mark 2 if (!x) { free(str); return NULL; } xstrInit(x, str, size, 0); return x; }
透過xstring
結構體與*xstrNew(const size_t size)
這個建立新的xstring
的函數,ccan
的這個實現的思路就比較清晰了,xstring
結構體自己佔據內存,可是並不存儲字符串,字符串在mark 1被分配存儲空間,而結構體在mark 2被分配內存。ui
PS:google
在剛剛學習使用C來實現數據結構的時候,我很疑惑爲什麼不能直接編碼
struct xstring* newStruct(){ struct xstring s; return &s; }
直到後來才逐漸明白了棧上的變量與動態分配的變量的微妙的區別,s在這個函數返回後就已經被銷燬了,傳出的這個地址是無效的,而對他的引用極可能會致使段錯誤(segment fault),操做系統,編譯原理等課真的會讓本身對於程序設計語言得到更深的理解。
並且這種寫法當時頗有吸引力,畢竟不用malloc,不用強制類型轉換。
這種野指針是不少很難修正的錯誤的來源,有興趣的同窗能夠去學習一下Rust語言的全部權系統,不少的概念頗有意思。
| xstring
| -> | str
|
能夠看出xstring
的實現中內存是分爲兩個部分的。
Note: xstring只須要編譯器支持C89/90。
redis sds(simple dynamic string)是Redis對於str的實現,在這裏有官方對於sds實現的一些技巧的介紹,
在這裏我會將SDS實現的主要的細節介紹如下。
// sds 類型 typedef char *sds; // sdshdr 結構 struct sdshdr { // buf 已佔用長度 int len; // buf 剩餘可用長度 int free; // 實際保存字符串數據的地方 // 利用c99(C99 specification 6.7.2.1.16)中引入的 flexible array member,經過buf來引用sdshdr後面的地址, // 詳情google "flexible array member" char buf[]; };
和上面的實現不太同樣的是sds只存儲存儲的字符串長度以及剩餘長度,可是最引人矚目的無疑是最後的那一個數組聲明:
char buf[];
結構體中居然沒有聲明數組的大小,這樣好像與咱們對於數組一向的印象不符,可是這是合法的特性,叫作柔性數組。
具體的語法細節我再也不介紹,可是注意如下幾點
sizeof(struct sdshdr) == sizeof(len) + sizeof(buf)
,在x86_64上典型值應該爲8個字節(4 + 4),這說明buf[]
沒有實際佔據空間,一個64位系統下的指針就要8個字節。上面的寫法是C99 only的,這個特性應該來自於如下這種寫法,
struct header { size_t len; unsigned char data[1]; };
這種寫法下data
就是一個 unsigned char*
型的指針,能夠經過它用來訪問存儲的字符串。
//在分配內存的時候,結構體中存儲了一個字符,其餘的(n-1)個空間在 //緊隨結構體結束地址的地方 // | struct (char) | (n - 1) char | ptr = malloc(sizeof(struct header) + (n-1));
對比sds
中的實現,sds
中不存儲任何一個數據,只有一個不佔據內存空間的標記表明,全部的數據都存儲在結構體所佔空間後面
| struct | str
|
咱們來看這有什麼用:
/* * 返回 sds buf 的已佔用長度 */ static inline size_t sdslen(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->len; } /* * 返回 sds buf 的可用長度 */ static inline size_t sdsavail(const sds s) { struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr))); return sh->free; } /* * 建立一個指定長度的 sds * 若是給定了初始化值 init 的話,那麼將 init 複製到 sds 的 buf 當中 * * T = O(N) */ sds sdsnewlen(const void *init, size_t initlen) { struct sdshdr *sh; // 有 init ? // O(N) if (init) { sh = zmalloc(sizeof(struct sdshdr)+initlen+1); } else { sh = zcalloc(sizeof(struct sdshdr)+initlen+1); } // 內存不足,分配失敗 if (sh == NULL) return NULL; sh->len = initlen; sh->free = 0; // 若是給定了 init 且 initlen 不爲 0 的話 // 那麼將 init 的內容複製至 sds buf // O(N) if (initlen && init) memcpy(sh->buf, init, initlen); // 加上終結符 sh->buf[initlen] = '\0'; // 返回 buf 而不是整個 sdshdr return (char*)sh->buf; }
咱們建立一個新的sds的時候,分配sizeof(struct sdshdr) + len + 1
大小的空間,len表明不包含結束符號在內的容量,最後咱們返回的是字符串開始的地址,這個返回的地址能夠直接做爲通常的字符串被其餘庫函數等使用,即Redis所說的二進制兼容的(由於其內部也使用'0'結尾)。
同時結構體的地址能夠經過用字符串的地址減去結構體的大小獲得
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
這樣一來sds能夠在常數時間內得到字符串的長度。
#include <stdio.h> #include "./src/simple_dynamic_str.h" int main() { sds s = sdsnew("Hello World! K&R"); printf("%s\n", s); printf("%zu %zu\n", sdslen(s), sdsavail(s)); printf("%c",s[0]); return 0; }
結果: Hello World! K&R 16 0 H
這種經過指針的計算得到結構體的地址的方式仍是比較少見的技巧,我也只是在Linux內核的task_struct
結構體中見識過相似的技巧,固然那個更復雜。
這種操做是很危險的,可是C數組在這方面其實也沒有好多少(並無多出數組越界檢查等),不是嗎?
在字符串較短時,結構體佔據放入空間是比較可觀的,更新版本的Redis
優化了不一樣長度的字符串結構體的定義。
/* Note: sdshdr5 is never used, we just access the flags byte directly. * However is here to document the layout of type 5 SDS strings. */ struct __attribute__ ((__packed__)) sdshdr5 { unsigned char flags; /* 3 lsb of type, and 5 msb of string length */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr8 { uint8_t len; /* used */ uint8_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr16 { uint16_t len; /* used */ uint16_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr32 { uint32_t len; /* used */ uint32_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; }; struct __attribute__ ((__packed__)) sdshdr64 { uint64_t len; /* used */ uint64_t alloc; /* excluding the header and null terminator */ unsigned char flags; /* 3 lsb of type, 5 unused bits */ char buf[]; };
這篇文章中有些技巧仍是有些難度的,像sds
我也是花了一些時間才弄明白其原理,這裏的兩種實現我我的更偏心第二種,可是這畢竟是二等公民,沒有語言級別的支持是硬傷。
因此若是真的須要大量處理字符串,特別是非純ASCII碼,左轉Java/Python etc.
reference:
[redis sds(simple dynamic string)]()
[ccan xstring]()
Redis設計與實現