字符串
在C语言中,字符串通常有以下两种方式来表示:
char *buf1="redis";
char buf2[]="redis";
buf1是通过一个char指针指向一个字符串字面量,其内容是不能改变的,即不能使用buf1[3]=’c’;这种方式来改变字符串中的某个字符,要改变字符串内容只能通过给buf1指针重新赋值,因此不能重用buf1指向的内存空间。
buf2是一个char数组,末尾会自动有一个字节的’\0’表示结束,其长度为6个字节。其内容虽然可以改变,但是由于buf2本身不携带长度等信息,因此调用一些字符串操作如strcat时,可能会导致缓冲区溢出,不安全。
redis中没有使用C语言的这两种方式来表示字符串,而是通过一种名为简单动态字符串(simple dynamic string,SDS)的类型来表示的。
C字符串只会作为字符串字面量在一些无需修改的地方用到,如打印日志:
redisLogFromHandler(REDIS_WARNING, "You insist... exiting now.");
在redis中,sds除了可以保存数据库的字符串键值对外,还可以用作AOF缓冲区、客户端状态中的缓冲区。
SDS
1、sds的定义
struct sdshdr {
unsigned int len;//buf中已用长度
unsigned int free;//buf中剩余可用长度
char buf[];//保存字符串的数组
};
// sizeof(sdshdr) = 8
示例如下:
buf总长度为11个字节,其中5个字节已用(len),5个字节未用(free),buf末尾总会有1个字节的’\0’表示字符串的结束,这一个字节不会计入SDS的长度属性中。
2、sds的创建、复制、清除与释放
创建:
sds sdsnewlen(const void *init, size_t initlen) {
struct sdshdr *sh;
if (init) {
sh = zmalloc(sizeof(struct sdshdr)+initlen+1);//+1用于存储'\0'
} else {
sh = zcalloc(sizeof(struct sdshdr)+initlen+1);
}
if (sh == NULL) return NULL;
sh->len = initlen;//设置len和free的值
sh->free = 0;
if (initlen && init) //将init中的内容copy到sds中
memcpy(sh->buf, init, initlen);
sh->buf[initlen] = '\0';//字符串结束符
return (char*)sh->buf;
}
// 调用sdsnew来创建一个sds字符串,其中字符串的内容为init
sds sdsnew(const char *init) {
size_t initlen = (init == NULL) ? 0 : strlen(init);
return sdsnewlen(init, initlen);
}
这样就完成了一个字符串的创建了,返回的是保存字符串的buf的起始地址。
复制:
sds sdsdup(const sds s) {
return sdsnewlen(s, sdslen(s));
}
清除:
void sdsclear(sds s) {
struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
sh->free += sh->len;
sh->len = 0;
sh->buf[0] = '\0';
}
释放:
void sdsfree(sds s) {
if (s == NULL) return;
zfree(s-sizeof(struct sdshdr));
}
3、sds的长度获取
static inline size_t sdslen(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->len;
}
static inline size_t sdsavail(const sds s) {
struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
return sh->free;
}
由于sds在函数间传递时,使用的都是字符串的起始地址buf,因此sds结构体的起始地址为s-(sizeof(struct sdshdr))。
找到sds结构体的起始地址后,就可以直接获取相关字段的值了。
因此获取len和free的操作的时间复杂度都是O(1)。
4、sds空间扩展
sds sdsMakeRoomFor(sds s, size_t addlen) {
struct sdshdr *sh, *newsh;
size_t free = sdsavail(s);
size_t len, newlen;
if (free >= addlen) return s;//当剩余长度大于所需长度时,不需要扩展内存
len = sdslen(s);
sh = (void*) (s-(sizeof(struct sdshdr)));
newlen = (len+addlen);
if (newlen < SDS_MAX_PREALLOC)//长度扩展方法
newlen *= 2;
else
newlen += SDS_MAX_PREALLOC;
newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);//重新分配内存
if (newsh == NULL) return NULL;
newsh->free = newlen - len;
return newsh->buf;
}
通过上面的代码可以发现,sds空间扩展不是直接将空间扩展为newlen = sds->len+addlen的,而是在此基础上,再将newlen扩大2倍。如sds->len = 10, addlen=20,则扩展后的新sds字符串的所有可用空间的长度为(10+20)*2=60字节。
SDS的优势:
1. sds获取字符串长度的时间复杂度为O(1),sds->len。而C字符串为O(N)
2、sds能防止缓冲区溢出。C字符串在调用strcat等操作时,并不会进行安全性检查,因此可能会导致缓冲区溢出。而sds在操作前都会对缓冲区进行检查
3、减少修改字符串时带来的内存重分配的次数
4、二进制安全,C字符串遇到空格即认为字符串结束,因此C字符串只能保存不含空格的字符串。而sds字符串有一个长度字段指示字符串的总长度,因此不会有这个限制
本文所引用的源码全部来自Redis3.0.7版本
redis学习参考资料:
https://github.com/huangz1990/redis-3.0-annotated
Redis 设计与实现(第二版)