Simple Dynamic Strings(SDS)源码解析和使用说明二
? ? ?在《Simple Dynamic Strings(SDS)源碼解析和使用說明一》文中,我們分析了SDS庫中數據的基本結構和創建、釋放等方法。本文將介紹其一些其他方法及實現。(轉載請指明出于breaksoftware的csdn博客)
字符串連接
? ? ? ? SDS庫提供下面兩種方法進行字符串連接
sds sdscatlen(sds s, const void *t, size_t len);
sds sdscat(sds s, const char *t);
? ? ? ??sdscat函數在底層使用了sdscatlen去實現。sdscatlen方法第一個元素是需要被連接的SDS字符串,第二個參數是需要連接的內容起始地址,第三個是其內容的長度。和C語言中的連接函數不同,sdscatlen方法并不要求追加的內容要以NULL結尾,因為SDS字符串可以在內容中間承載NULL字符。但是sdscat則需要追加的字符串以NULL結尾,因為它沒有提供長度參數。我們看下它們的實現:
sds sdscat(sds s, const char *t) {return sdscatlen(s, t, strlen(t));
}sds sdscatlen(sds s, const void *t, size_t len) {size_t curlen = sdslen(s);s = sdsMakeRoomFor(s,len);if (s == NULL) return NULL;memcpy(s+curlen, t, len);sdssetlen(s, curlen+len);s[curlen+len] = '\0';return s;
}
? ? ? ? sdscatlen方法中通過sdsMakeRoomFor方法獲取需要被追加的sds對象,然后通過memcpy追加相關的內容到該對象的字符串末尾。最后修改SDS字符串中代表已經使用了空間長度的字段len,并把最后一位設置為NULL。我們需要關注下sdsMakeRoomFor方法的實現
sds sdsMakeRoomFor(sds s, size_t addlen) {void *sh, *newsh;size_t avail = sdsavail(s);size_t len, newlen;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;/* Return ASAP if there is enough space left. */if (avail >= addlen) return s;
? ? ? ? sdsMakeRoomFor方法首先需要知道被追加的SDS字符串還有多少空余的空間,這步計算通過sdsavail方法實現,其實現也很簡單,我們以SDS_TYPE_5和SDS_TYPE_8為例:
static inline size_t sdsavail(const sds s) {unsigned char flags = s[-1];switch(flags&SDS_TYPE_MASK) {case SDS_TYPE_5: {return 0;}case SDS_TYPE_8: {SDS_HDR_VAR(8,s);return sh->alloc - sh->len;}
? ? ? ? 從這個設計可以看出,作者認為如果調用sdsavail方法時,這個SDS字符串可能是需要擴展空間了。如果此時它的類型是SDS_TYPE_5,則不經過任何計算,直接認為可用空間不夠。如果不是空間最小的類型,則通過分配的了空間大小alloc減去已使用的空間大小len計算出還可用的空間大小。
? ? ? ? 再回到sdsMakeRoomFor方法中,如果判斷發現SDS字符串剩余空間的大小足以承載追加的內容,則直接返回入參字符串對象。如果不夠,則需要計算需要的長度
len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);newlen = (len+addlen);if (newlen < SDS_MAX_PREALLOC)newlen *= 2;elsenewlen += SDS_MAX_PREALLOC;
? ? ? ??SDS_MAX_PREALLOC是1M的空間,如果追加的長度和原始長度之和在1M以內,則新的空間是它們和的2倍大;如果大于1M,則在它們之和的基礎上增加1M。這就是預分配的邏輯,這樣設計可以預防頻繁的的內存分配操作,當然相應的也會增加一定的內存浪費。但是總的來說,在目前CPU資源比內存資源貴的場景下,空間換時間還是比較好的。
? ? ? ? 然后通過新空間的大小匹配SDS字符串類型,如果新老類型相同,則直接使用realloc操作擴展空間。如果類型不同,則需要重新分配一個空間,將老空間里內容復制過來,最后還要將老空間釋放掉。
type = sdsReqType(newlen);/* Don't use type 5: the user is appending to the string and type 5 is* not able to remember empty space, so sdsMakeRoomFor() must be called* at every appending operation. */if (type == SDS_TYPE_5) type = SDS_TYPE_8;hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+newlen+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {/* Since the header size changes, need to move the string forward,* and can't use realloc */newsh = s_malloc(hdrlen+newlen+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, newlen);return s;
}
? ? ? ??sdsMakeRoomFor方法在之后的代碼中我們會反復見到,但是見到它就要有個印象:通過它操作的SDS字符串可能是原始的,也可能是將原始空間釋放后重新分配的。
? ? ? ? 我們再看下它們的使用樣例:
sds s = sdsempty();
s = sdscat(s, "Hello ");
s = sdscat(s, "World!");
printf("%s\n", s);output> Hello World!
? ? ? ? SDS字符串庫還提供了連接兩個SDS字符串的方法
sds sdscatsds(sds s, const sds t);
? ? ? ? 其底層還是調用了sdscatlen方法
sds sdscatsds(sds s, const sds t) {return sdscatlen(s, t, sdslen(t));
}
? ? ? ? 它的使用方法是:
sds s1 = sdsnew("aaa");
sds s2 = sdsnew("bbb");
s1 = sdscatsds(s1,s2);
sdsfree(s2);
printf("%s\n", s1);output> aaabbb
? ? ? ? 還有一個特殊的方法,它是用于擴張SDS字符串的長度
sds sdsgrowzero(sds s, size_t len);
? ? ? ? 如果入參s的長度已經大于等于len了,則不作任何操作;否則增加s的長度到len,并使用NULL去填充多出來的空間。它的實現和sdscatlen很像,只是填充的長度是總長,而不是追加的長度;填充的字符是NULL而已。
sds sdsgrowzero(sds s, size_t len) {size_t curlen = sdslen(s);if (len <= curlen) return s;s = sdsMakeRoomFor(s,len-curlen);if (s == NULL) return NULL;/* Make sure added region doesn't contain garbage */memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */sdssetlen(s, len);return s;
}
? ? ? ? 我們看下使用例子:
sds s = sdsnew("Hello");
s = sdsgrowzero(s,6);
s[5] = '!'; /* We are sure this is safe because of sdsgrowzero() */
printf("%s\n', s);output> Hello!
字符串格式化
? ? ? ? 字符串格式化是非常有用的工具,它可以讓我們通過指定格式生成一個字符串。SDS字符串庫也提供了相應的方法:
sds sdscatprintf(sds s, const char *fmt, ...) {va_list ap;char *t;va_start(ap, fmt);t = sdscatvprintf(s,fmt,ap);va_end(ap);return t;
}
? ? ? ? 這個方法底層調用了sdscatvprintf方法:
sds sdscatvprintf(sds s, const char *fmt, va_list ap) {va_list cpy;char staticbuf[1024], *buf = staticbuf, *t;size_t buflen = strlen(fmt)*2;/* We try to start using a static buffer for speed.* If not possible we revert to heap allocation. */if (buflen > sizeof(staticbuf)) {buf = s_malloc(buflen);if (buf == NULL) return NULL;} else {buflen = sizeof(staticbuf);}/* Try with buffers two times bigger every time we fail to* fit the string in the current buffer size. */while(1) {buf[buflen-2] = '\0';va_copy(cpy,ap);vsnprintf(buf, buflen, fmt, cpy);va_end(cpy);if (buf[buflen-2] != '\0') {if (buf != staticbuf) s_free(buf);buflen *= 2;buf = s_malloc(buflen);if (buf == NULL) return NULL;continue;}break;}/* Finally concat the obtained string to the SDS string and return it. */t = sdscat(s, buf);if (buf != staticbuf) s_free(buf);return t;
}
? ? ? ? 這和我們之前使用C語言的格式化不同,這個方法將格式化后的字符串追加到入參第一個參數s的后面。怎么感覺第一個參數s非常雞肋,我們看看調用的例子就感覺到了:
char *name = "Anna";
int loc = 2500;
sds s;
s = sdscatprintf(sdsempty(), "%s wrote %d lines of LISP\n", name, loc);
? ? ? ? 我們再看一個使用特例:
int some_integer = 100;
sds num = sdscatprintf(sdsempty(),"%d\n", some_integer);
? ? ? ? 這個方法將一個整形轉換成一個字符串。但是這個方法在此次轉換場景中還是非常低效的,我們會在之后介紹專門針對整形數字轉換成字符串的高效方法。
整形數組轉字符串
? ? ? ? sds提供下面這種方法將整形數字數字轉換成字符串,當然這個轉換的效率要比sdscatprintf要高:
sds sdsfromlonglong(long long value) {char buf[SDS_LLSTR_SIZE];int len = sdsll2str(buf,value);return sdsnewlen(buf,len);
}
? ? ? ? 我們看下sdsll2str為什么比較高效:
int sdsll2str(char *s, long long value) {char *p, aux;unsigned long long v;size_t l;/* Generate the string representation, this method produces* an reversed string. */v = (value < 0) ? -value : value;p = s;do {*p++ = '0'+(v%10);v /= 10;} while(v);if (value < 0) *p++ = '-';/* Compute length and add null term. */l = p-s;*p = '\0';/* Reverse the string. */p--;while(s < p) {aux = *s;*s = *p;*p = aux;s++;p--;}return l;
}
? ? ? ? 這個方法底層沒有使用字符串格式化這種比較通用但是效率不高的方法,它將入參整形數字從后向前逐個分解出來轉換成字符,然后再逆序將這些字符寫回到字符串內存空間中。這個函數還可以處理負數,相應的SDS字符串庫還提供了針對無符號整形數的轉換函數sdsull2str,其實現和sdsll2str非常類似,這兒我就不詳細說明了。
字符串裁剪和截取
? ? ? ? 字符串裁剪操作在實際工作中也是常用的。SDS字符串庫通過下面這個方法實現裁剪
sds sdstrim(sds s, const char *cset) {char *start, *end, *sp, *ep;size_t len;sp = start = s;ep = end = s+sdslen(s)-1;while(sp <= end && strchr(cset, *sp)) sp++;while(ep > sp && strchr(cset, *ep)) ep--;len = (sp > ep) ? 0 : ((ep-sp)+1);if (s != sp) memmove(s, sp, len);s[len] = '\0';sdssetlen(s,len);return s;
}
? ? ? ? 該函數第二個參數是一個C語言的字符串,其以NULL結尾,包含了需要被裁剪掉的字符。這個裁剪操作通過兩個while循環,分別從待裁剪的字符串最前和最后兩個位置向另一個方向進行檢索,只要遇到需要被裁剪的就繼續探索下一個字符,如果是不需要裁剪的就終止當前方向的探測和檢索。最終確定剩下的字符串的起始地址后,將這段空間內容復制到SDS字符串內容起始處,并設置結尾符NULL和已使用的空間長度記錄變量len。我們來看個使用例子:
sds s = sdsnew(" my string\n\n ");
sdstrim(s," \n");
printf("-%s-\n",s);output> -my string-
? ? ? ? 可見my和string之間的空格沒有被裁剪,雖然它在要被裁剪的字符串列表中。這兒還有個地方需要說明下,該字符串裁剪操作沒有進行字符串空間的再分配,而是利用原來的字符串空間進行處理的。
? ? ? ? 字符串截取操作是通過指定字符串的前后下標方式,截取其區間的內容并返回。這塊操作通過下面方法實現的:
void sdsrange(sds s, int start, int end) {size_t newlen, len = sdslen(s);if (len == 0) return;if (start < 0) {start = len+start;if (start < 0) start = 0;}if (end < 0) {end = len+end;if (end < 0) end = 0;}newlen = (start > end) ? 0 : (end-start)+1;if (newlen != 0) {if (start >= (signed)len) {newlen = 0;} else if (end >= (signed)len) {end = len-1;newlen = (start > end) ? 0 : (end-start)+1;}} else {start = 0;}if (start && newlen) memmove(s, s+start, newlen);s[newlen] = 0;sdssetlen(s,newlen);
}
? ? ? ? 可見其實現就是簡單的下標計算,然后內容復制,最后是結尾符NULL設置和已使用空間長度len字段設置。從代碼中我們可以看出,用戶傳入的下標參數可以是正數,也可以是負數。正數代表下標從起始位置開始,負數代表下標從結束位置開始。這個操作也沒有進行內存的重新分配。其使用樣例見下:
sds s = sdsnew("Hello World!");
sdsrange(s,6,-1);
printf("-%s-\n");
sdsrange(s,0,-2);
printf("-%s-\n");output> -World!-
output> -World-
字符串復制
? ? ? ? 字符串復制的操作也非常簡單,SDS字符串庫提供了兩個方法,一個是供C語言字符串使用的
sds sdscpy(sds s, const char *t) {return sdscpylen(s, t, strlen(t));
}
? ? ? ? 另一個則是可以復制包含NULL字符的二進制數據的
sds sdscpylen(sds s, const char *t, size_t len) {if (sdsalloc(s) < len) {s = sdsMakeRoomFor(s,len-sdslen(s));if (s == NULL) return NULL;}memcpy(s, t, len);s[len] = '\0';sdssetlen(s, len);return s;
}
字符串轉可視化
? ? ? ? 因為SDS字符串中可以包含二進制字符,所以當我們試圖打印出這個字符串時,printf方法可能輸出不可見的字符。這在調試一一段數據的時候可能比較有用,SDS提供了下面這個方法將字符串中不可見字符和轉義字符都轉換成可見的內容:
sds sdscatrepr(sds s, const char *p, size_t len) {s = sdscatlen(s,"\"",1);while(len--) {switch(*p) {case '\\':case '"':s = sdscatprintf(s,"\\%c",*p);break;case '\n': s = sdscatlen(s,"\\n",2); break;case '\r': s = sdscatlen(s,"\\r",2); break;case '\t': s = sdscatlen(s,"\\t",2); break;case '\a': s = sdscatlen(s,"\\a",2); break;case '\b': s = sdscatlen(s,"\\b",2); break;default:if (isprint(*p))s = sdscatprintf(s,"%c",*p);elses = sdscatprintf(s,"\\x%02x",(unsigned char)*p);break;}p++;}return sdscatlen(s,"\"",1);
}
? ? ? ? 這個方法的實現原理也很簡單。對于可見字符,則直接顯示。對于被反斜杠轉義的字符,增加一個反斜杠使得反斜杠自身被轉義,從而顯示出可打印的內容。對于剩下的不可見的,則將其轉成8進制數字輸出。我們看下例子:
sds s1 = sdsnew("abcd");
sds s2 = sdsempty();
s[1] = 1;
s[2] = 2;
s[3] = '\n';
s2 = sdscatrepr(s2,s1,sdslen(s1));
printf("%s\n", s2);output> "a\x01\x02\n"
字符串拆分
? ? ? ? ?字符串拆分是指將一個一定規則的字符串,按照某種分隔符進行切分,從而得到一組切分后的字符串的操作。舉個例子,我們要對下面這串字符按照|-|為切割符進行切割
foo|-|bar|-|zap
? ? ? ? 最終得到的結果是一組字符串,它們分別為foo、bar和zap。如果以-為切割符,則切割后的字符串數組包含:foo|、|bar|、|zap。我們看看SDS字符串庫通過什么接口完成這個功能的
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count);
? ? ? ? 第一個參數是需要被切割的字符串的指針,第二個參數是該字符串的長度。第三個參數是分隔符字符串的起始地址,第四個則是分隔符字符串的長度。通過這種形式傳遞進去的字符串,在其內容中是可以包含NULL的,因為提供了長度信息就意味著不用以NULL來查找字符串結尾了。該函數的返回值是一個SDS字符串數組的起始地址,這個數組的長度通過第五個參數返回。我們看下其實現:
sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {int elements = 0, slots = 5, start = 0, j;sds *tokens;if (seplen < 1 || len < 0) return NULL;tokens = s_malloc(sizeof(sds)*slots);if (tokens == NULL) return NULL;if (len == 0) {*count = 0;return tokens;}
? ? ? ? 首先預分配了5個槽位用于存儲切割留下的數據,然后遍歷整個字符串空間,并使用分隔符進行對比,取出分割后的字符串
for (j = 0; j < (len-(seplen-1)); j++) {/* make sure there is room for the next element and the final one */if (slots < elements+2) {sds *newtokens;slots *= 2;newtokens = s_realloc(tokens,sizeof(sds)*slots);if (newtokens == NULL) goto cleanup;tokens = newtokens;}/* search the separator */if ((seplen == 1 && *(s+j) == sep[0]) || (memcmp(s+j,sep,seplen) == 0)) {tokens[elements] = sdsnewlen(s+start,j-start);if (tokens[elements] == NULL) goto cleanup;elements++;start = j+seplen;j = j+seplen-1; /* skip the separator */}}/* Add the final element. We are sure there is room in the tokens array. */tokens[elements] = sdsnewlen(s+start,len-start);
? ? ? ? 如果預分配的5個槽位不夠,則在填充即將滿了的時候,讓槽位數量增加一倍。這些操作如果都成功,則返回SDS字符串數組,否則清空整個申請的空間
if (tokens[elements] == NULL) goto cleanup;elements++;*count = elements;return tokens;cleanup:{int i;for (i = 0; i < elements; i++) sdsfree(tokens[i]);s_free(tokens);*count = 0;return NULL;}
}
? ? ? ? 在我們調用該方法獲取到切割后的字符串數組后,我們要釋放該數組的所有空間,以防止內存溢出問題。釋放的方法是:
void sdsfreesplitres(sds *tokens, int count) {if (!tokens) return;while(count--)sdsfree(tokens[count]);s_free(tokens);
}
? ? ? ? 最后看下這些方法的使用樣例:
sds *tokens;
int count, j;sds line = sdsnew("Hello World!");
tokens = sdssplitlen(line,sdslen(line)," ",1,&count);for (j = 0; j < count; j++)printf("%s\n", tokens[j]);
sdsfreesplitres(tokens,count);output> Hello
output> World!
命令行字符串拆解
? ? ? ? 之前介紹的字符串拆解方法要求入參字符串是嚴格按照一定方式布局的。然而用戶輸入形式的字符串,比如命令行,則非常可能不嚴格遵守格式。比如命令行中我們一般以空格分隔調用程序和其參數,但是并不嚴格要求使用幾個空格去分隔
call "Sabrina" and "Mark Smith\n"
? ? ? ? 上面這個命令行式的字符串則不能使用之前介紹的通過分隔符切割出各個字符串的方法,這時要準確切割需要使用:
sds *sdssplitargs(const char *line, int *argc)
? ? ? ? 這個方法第一個參數是待切割的字符串首地址,當然是以NULL結尾的。返回值是切割后的字符串數組首地址,第二個參數是用于傳出這個數組的長度。它的實現就是從頭向尾遍歷整個字符串空間,然后切分出各個字符串。由于過程比較簡單,但是代碼比較長,我就不在這兒貼出來了。唯一要說的,可能引號匹配的場景截取稍微復雜點,因為引號里的空格是不能當成分隔符的。我們看下通過上面函數切分本節例子的結果
"call"
"Sabrina"
"and"
"Mark Smith\n"
字符串數組轉字符串
? ? ? ? 有些場景下,我們會需要將“字符串切割成字符串數組”的行為逆過來,讓字符串數組中對象逐個連接變成一個字符串。SDS字符串庫提供了如下兩個方法:
sds sdsjoin(char **argv, int argc, char *sep, size_t seplen);
sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen);
? ? ? ? sdsjoin方法針對C語言的字符串數組。第一個參數是字符串數組首地址。第二個參數是該數組的長度。第三個參數是連接各個字符串元素的“分隔符”字符串首地址。第四個參數是“分隔符”字符串的長度。sdsjoinsds方法則是針對sds字符串數組的。從這種參數設計可以看出,分隔符是可以包含NULL的,因為它提供了其長度信息。它們的實現也很簡單:
sds sdsjoin(char **argv, int argc, char *sep) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscat(join, argv[j]);if (j != argc-1) join = sdscat(join,sep);}return join;
}sds sdsjoinsds(sds *argv, int argc, const char *sep, size_t seplen) {sds join = sdsempty();int j;for (j = 0; j < argc; j++) {join = sdscatsds(join, argv[j]);if (j != argc-1) join = sdscatlen(join,sep,seplen);}return join;
}
? ? ? ? 我們再看下其使用樣例:
char *tokens[3] = {"foo","bar","zap"};
sds s = sdsjoin(tokens,3,"|",1);
printf("%s\n", s);output> foo|bar|zap
縮減字符串空間
? ? ? ? SDS字符串在初次創建時,其分配空間大小就是使用了的空間大小。但是由于字符串連接等操作,會觸發sdsMakeRoomFor方法,從而產生預分配的現象。這個時候往往被使用的空間大小只占已分配空間的一半。在大部分場景下,這種設計沒有什么問題。但是對于內存特別緊張的時候,可能需要縮減這些字符串空間。SDS提供了如下方法實現空間縮減:
sds sdsRemoveFreeSpace(sds s) {void *sh, *newsh;char type, oldtype = s[-1] & SDS_TYPE_MASK;int hdrlen;size_t len = sdslen(s);sh = (char*)s-sdsHdrSize(oldtype);type = sdsReqType(len);hdrlen = sdsHdrSize(type);if (oldtype==type) {newsh = s_realloc(sh, hdrlen+len+1);if (newsh == NULL) return NULL;s = (char*)newsh+hdrlen;} else {newsh = s_malloc(hdrlen+len+1);if (newsh == NULL) return NULL;memcpy((char*)newsh+hdrlen, s, len+1);s_free(sh);s = (char*)newsh+hdrlen;s[-1] = type;sdssetlen(s, len);}sdssetalloc(s, len);return s;
}
? ? ? ? 這個操作一定會產生內存重分配的問題,所以它還是比較消耗效率的。好在需要它的場景不太多。
C語言字符串格式化SDS字符串
? ? ? ? C語言中的字符串是以NULL結尾的,而SDS字符串可以包含NULL。如果我們希望SDS字符串按照C語言字符串格式一樣,以NULL結尾,則可以調用如下方法:
void sdsupdatelen(sds s) {int reallen = strlen(s);sdssetlen(s, reallen);
}
? ? ? ? 這步操作非常簡單,它只是以C語言字符串方式重新計算長度,并設置長度信息。它并沒有進行字符串空間的重分配。我們看下例子:
sds s = sdsnew("foobar");
s[2] = '\0';
printf("%d\n", sdslen(s));
sdsupdatelen(s);
printf("%d\n", sdslen(s));output> 6
output> 2
? ? ? ? 基于上面的介紹,我們可以得知SDS字符串存在如下的特點:
- 便于使用。我們可以像使用C語言中字符串一樣接受和使用SDS字符串。比如我們可以
sds mystring = sdsnew("Hello World!");
mystring[0] = 'h';
printf("%s\n", mystring);
……
output>hello World!
- 二進制安全。SDS字符串在頭部使用len字段表示了buf中被使用了的空間長度,也就是說buf空間內容可以不用以NULL結尾。這種設計可以讓SDS字符串承載包括NULL在內的一些二進制數據。
- 執行高效。在字符串連接過程中,如果每次連接都要重新分配內存以承載更多的數據,會導致效率下降。而我們在SDS字符串頭結構中看到有用于保存已分配空間的長度和已使用的空間的長度。
? ? ? ? 當然它也有相應的缺陷:
- 可能要經常分配空間。一般一個字符串在第一次執行連接操作時,會發生原來字符串空間被釋放,新空間被申請的過程。雖然作者做了優化,但是這個操作還是在所難免的。
- 非線程安全的。我們在代碼中沒有看到任何線程安全性的輔助操作,所以它是線程非安全的。
? ? ? ? 本文的很多知識和樣例來源于GitHub上的SDS說明:https://github.com/antirez/sds。我本來是想翻譯這篇文章的,但是翻譯過后感覺如果是內容直譯可能不太符合大眾的閱讀口味,所以我就穿插的代碼將它重新寫了一遍。
總結
以上是生活随笔為你收集整理的Simple Dynamic Strings(SDS)源码解析和使用说明二的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Simple Dynamic Strin
- 下一篇: Redis源码解析——前言