首先看一下redis字符串结构体
/*
* 类型别名,用于指向 sdshdr 的 buf 属性
*/
typedef char *sds;
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间 实际上不占用内存空间sizeof(struct sdshdr) = 8
char buf[];
};
这个都很好理解,使用结构体struct sdshdr来保存字符串;
注意:struct sdshdr中的char buf[],是针对可变长结构体的编程技巧,必须强调这点,否则后面没法理解。sizeof(struct sdshdr) === 8(柔性数组成员不占用结构体的空间,只作为一个符号地址存在,而且必须是结构体的最后一个成员。柔性数组成员不仅可以用于字符数组,还可以是元素为其它类型的数组。C99中,结构中的最后一个元素允许是未知大小的数组,这就叫做柔性数组成员,但结构中的柔性数组成员前面必须至少一个其他成员。柔性数组成员允许结构中包含一个大小可变的数组。sizeof返回的这种结构大小不包括**柔性数组的内存。包含柔性数组成员的结构用malloc()函数进行内存的动态分配,并且分配的内存应该大于结构的大小,以适应柔性数组的预期大小。)
/*
* 根据给定的初始化字符串 init 和字符串长度 initlen
* 创建一个新的 sds
*
* 参数
* init :初始化字符串指针
* initlen :初始化字符串的长度
*
* 返回值
* sds :创建成功返回 sdshdr 相对应的 sds
* 创建失败返回 NULL
*
* 复杂度
* T = O(N)
*/
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
// 根据是否有初始化内容,选择适当的内存分配方式
// T = O(N)
if (init) {
// zmalloc 不初始化所分配的内存
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);
} else {
// zcalloc 将分配的内存全部初始化为 0
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
// 内存分配失败,返回
if (sh == NULL) return NULL;
// 设置初始化长度
sh->len = initlen;
// 新 sds 不预留任何空间
sh->free = 0;
// 如果有指定初始化内容,将它们复制到 sdshdr 的 buf 中
// T = O(N)
if (initlen && init)
memcpy(sh->buf, init, initlen);
// 以 \0 结尾
sh->buf[initlen] = '\0';
// 返回 buf 部分,而不是整个 sdshdr
return (char*)sh->buf;
}
这个函数调用例如:sdsnewlen(“redis”,5)
会根据init判断内存是否初始化,所以有两种初始化方式,分配内存大小都是
sizeof(struct sdshdr)+initlen+1,1是为了给\0;
注意: return (char)sh->buf;*
返回的是buf的指针,而不是sh,如果后面我们想改变结构体中的len和free怎么办呢。
这里有内存的知识需要注意,我们的结构体在内存中存放是这样的:
|5|0|“redis”|现在返回的指针指向"redis",当我们使用sh->buf的内存减去两个int就能得到sh的指针。所以就有后面的
/*
* 返回 sds 实际保存的字符串的长度
*
* T = O(1)
*/
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
/*
* 返回 sds 可用空间的长度
*
* T = O(1)
*/
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
这里真是利用了内存的关系,通过sh->buf====>sh
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
// 获取 s 目前的空余空间长度
size_t free = sdsavail(s);
size_t len, newlen;
// s 目前的空余空间已经足够,无须再进行扩展,直接返回
if (free >= addlen) return s;
// 获取 s 目前已占用空间的长度
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
// s 最少需要的长度
newlen = (len+addlen);
// 根据新长度,为 s 分配新空间所需的大小
if (newlen < SDS_MAX_PREALLOC)
// 如果新长度小于 SDS_MAX_PREALLOC
// 那么为它分配两倍于所需长度的空间
newlen *= 2;
else
// 否则,分配长度为目前长度加上 SDS_MAX_PREALLOC
newlen += SDS_MAX_PREALLOC;
// T = O(N)
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);
// 内存不足,分配失败,返回
if (newsh == NULL) return NULL;
// 更新 sds 的空余长度
newsh->free = newlen - len;
// 返回 sds
return newsh->buf;
}
这个函数是用来为sds字符串的free部分扩容的。仅仅增加free的长度,不会改变sds字符串的长度。 这个函数比较难理解的地方是:扩容的幅度这里,为什么扩新长度的2倍
sds sdscatlen(sds s, const void *t, size_t len) {
struct sdshdr *sh;
// 原有字符串长度
size_t curlen = sdslen(s);
// 扩展 sds 空间
// T = O(N)
s = sdsMakeRoomFor(s,len);
// 内存不足?直接返回
if (s == NULL) return NULL;
// 复制 t 中的内容到字符串后部
// T = O(N)
sh = (void*) (s-(sizeof(struct sdshdr)));
memcpy(s+curlen, t, len);
// 更新属性
sh->len = curlen+len;
sh->free = sh->free-len;
// 添加新结尾符号
s[curlen+len] = '\0';
// 返回新 sds
return s;
}
1.首先使用扩容函数sdsMakeRoomFor扩容,注意,每次使用扩容函数后,需要判断返回值是否为空
2.然后使用memcpy函数从s+curlen处开始拷贝。
3.最后一定要记住改变字符串的状态(free和len)。
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. */
//将ll类型的数值转换为ull
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. */
// 开始反转字符串,让个位数到最后,这里采用交换的方式,两个指针s和p分别向中间靠,用一个中介aux指针做交换
p--;
while(s < p) {
aux = *s;
*s = *p;
*p = aux;
s++;
p--;
}
return l;
}
源码还提供了sdsull2str函数,将ull转换为str,方法一样;
这两个函数是为了给输入用的,redis提供了其它接口调用它
// 根据输入的 long long 值 value ,创建一个 SDS
sds sdsfromlonglong(long long value) {
char buf[SDS_LLSTR_SIZE];
\\
int len = sdsll2str(buf,value);
return sdsnewlen(buf,len);
}
字符串格式化是非常有用的工具,它可以让我们通过指定格式生成一个字符串。SDS字符串库也提供了相应的方法:
为了编写可变参数函数,我们通常需要用到
https://www.cnblogs.com/ThatsMyTiger/p/6924462.html 然后再看这段源码,会简单很多。
\\看过printf源码的对这个函数应该很熟悉
\\va_list变参输入
sds sdscatprintf(sds s, const char *fmt, ...) {
va_list ap;
char *t;
va_start(ap, fmt);
// T = O(N^2)
t = sdscatvprintf(s,fmt,ap);
va_end(ap);
return t;
}
打印任意数量个字符串,并将这些字符串追加到给定 sds 的末尾
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. */
/*
这里先用栈区的staticbuf,如果fmt的2倍长度超过这个staticbuf在从堆去分配
这样做如果是小于1024的,直接用栈区的内存,非常快
这是预分配冗余空间的惯用手段,减小对内存的频繁分配
*/
if (buflen > sizeof(staticbuf)) {
buf = zmalloc(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占满
buf[buflen-2] = '\0';
va_copy(cpy,ap);
// T = O(N)
// vsnprintf函数可以参考此链接https://blog.csdn.net/qq_37824129/article/details/78763286
vsnprintf(buf, buflen, fmt, cpy);
// 如果buf占满,释放buf,将buf*2
if (buf[buflen-2] != '\0') {
if (buf != staticbuf) zfree(buf);
buflen *= 2;
buf = zmalloc(buflen);
if (buf == NULL) return NULL;
continue;
}
break;
}
/* Finally concat the obtained string to the SDS string and return it. */
t = sdscat(s, buf);//这里底层调用的sdscatlen,是安全的
//如果一次存完,调用栈内存,操作系统会帮忙回收,但是如果调用了堆内存,则需要手动释放
if (buf != staticbuf) zfree(buf);
return t;
}
这段代码厉害在,对内存使用的细节和va使用,可以好好研读
上面分析的关键函数是redis实现SDS的核心函数,像外部接口sdsnew底层调用的是sdsnewlen,sdscpy、sdscat等底层调用的是sdscatlen。
其实c语言的字符串已经能够满足基本全部需求,为什么redis还要自己实现字符串sds呢?
要回答这个问题,还是回到开头说的c语言对于字符串的一般定义。其通常如下:
那么我们对比redis的实现,可以看出redis具有以下优点: