redis源码2--字符串SDS

Redis是用C语言实现的,但是并没有使用 C 语言传统的字符串表示(以空字符结尾的字符数组,以下简称 C 字符串), 而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型, 并将 SDS 用作 Redis 的默认字符串表示

比如客户端执行命令

redis> SET hello "world"
OK

那么键 hello 和 值 wrold 内部都是保存了 "hello" 和 "wrold" 的SDS对象

SDS与C字符串的区别

1.常数复杂度获取字符串长度。
2.杜绝缓冲区溢出。
3.减少修改字符串长度时所需的内存重分配次数。
4.二进制安全。
5.兼容部分 C 字符串函数。

关于SDS的更多基础知识,可以详见 http://redisbook.com/ ,本文主要介绍sds源码部分。源码文件在sds.h和sds.c中。关于zmalloc和zfree的部分,请看上一个博客 https://www.jianshu.com/p/da5dae89c979

SDS定义

在 sds.h中定义

struct sdshdr {

    // buf 中已使用空间的长度
    //等于SDS中所保存字符串的长度
    int len;

    // buf 中剩余可用空间的长度
    int free;

    // 数据空间
    char buf[];
};

结构定义非常简单,在内存中简单表示为


SDS示例

全局变量及其他定义

//最大预分配长度(1MB)
#define SDS_MAX_PREALLOC (1024*1024)
//类型别名,用于指向 sdshdr 的 buf 属性,所以下文的sds,指向的是sdshdr 的buf字段
typedef char *sds;

静态函数

//O(1)时间复杂度返回 sds 实际保存的字符串的长度
static inline size_t sdslen(const sds s) {
  struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
  return sh->len;
}

//O(1)时间复杂度返回 sds 可用空间的长度
static inline size_t sdsavail(const sds s) {
  struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
  return sh->free;
}

主要函数

//根据给定的初始化字符串 init 和字符串长度 initlen 创建一个新的 sds
//并且该sds buf属性多申请了一个字节,结尾的最后一个字节被置为'\0'
sds sdsnewlen(const void *init, size_t initlen)
//创建并返回一个只保存了空字符串 "" 的 sds
//实际上是调用 sdsnewlen
sds sdsempty(void)
//给定字符串 init ,创建一个包含同样字符串的 sds,输入为 NULL ,创建一个空白 sds
//实际上是调用 sdsnewlen
sds sdsnew(const char *init)
//复制给定 sds 的副本,创建成功返回输入 sds 的副本,失败返回 NULL
//实际上是调用 sdsnewlen
sds sdsdup(const sds s)
//释放给定的 SDS
void sdsfree(sds s)
//重置sds所保存的字段串为空字符串,但不回收内存,第一个字节为'\0'
void sdsclear(sds s)
//对 sds 中 buf 的长度进行扩展,会进行空间预分配
//底层是调用了realloc,所以数据不会丢失,最后一个字节也置为'\0'
sds sdsMakeRoomFor(sds s, size_t addlen)
//回收 sds 中的空闲空间,也就是将free属性置0
//底层调用了realloc
sds sdsRemoveFreeSpace(sds s)
//返回给定 sds 分配的内存字节数,包括struct sdshdr所占的字节数和最后一个'\0'
size_t sdsAllocSize(sds s) 
//将 sds 扩充至指定长度,未使用的空间以 0 字节填充。
sds sdsgrowzero(sds s, size_t len)
//将长度为 len 的字符串 t 追加到 sds 的字符串末尾
//不去判断free够不够用,直接将sds的buf扩展len长度存放传入的数据,最后会将最后一个字节置为'\0'
sds sdscatlen(sds s, const void *t, size_t len)
//将给定字符串 t 追加到 sds 的末尾
//实际是调用 sdscatlen
sds sdscat(sds s, const char *t)
//将另一个 sds 追加到一个 sds 的末尾
//实际是调用 sdscatlen
sds sdscatsds(sds s, const sds t) 
//将字符串 t 的前 len 个字符复制到 sds s 当中,并在字符串的最后添加终结符。
//会覆盖sds原来的字符串内容
sds sdscpylen(sds s, const char *t, size_t len)
//将字符串复制到 sds 当中,覆盖原有的字符。
//实际是调用 sdscpylen
sds sdscpy(sds s, const char *t)
//对 sds 左右两端进行修剪,清除其中 cset 指定的所有字符
sds sdstrim(sds s, const char *cset)
//按索引对截取 sds 字符串的其中一段,start 和 end 都是闭区间(包含在内)
void sdsrange(sds s, int start, int end)
//将 sds 字符串中的所有字符转换为小写
void sdstolower(sds s)
//将 sds 字符串中的所有字符转换为大写
void sdstoupper(sds s)
//对比两个 sds , strcmp 的 sds 版本,相等返回 0 ,s1 较大返回正数, s2 较大返回负数
int sdscmp(const sds s1, const sds s2)

其他函数

//用户主动调用,增加len属性,减少free属性
void sdsIncrLen(sds s, int incr)
//传入一个char * 和long long 的数值,将数值转为字符串放入传入的char *中,char* 指向的地址要事先分配好内存
int sdsll2str(char *s, long long value) 
//跟sdsll2str原理一样,只不过传入的是unsigned long long
int sdsull2str(char *s, unsigned long long v)
// 根据输入的 long long 值 value ,转为字符串创建一个 SDS
////实际是调用 sdsll2str 和 sdsnewlen
sds sdsfromlonglong(long long value)
//输出函数,懒得看了
sds sdscatvprintf(sds s, const char *fmt, va_list ap)
//打印任意数量个字符串,并将这些字符串追加到给定 sds 的末尾,调用了 sdscatvprintf
sds sdscatprintf(sds s, const char *fmt, ...)
//对 sds 左右两端进行修剪,清除其中 cset 指定的所有字符
sds sdstrim(sds s, const char *cset)

sdsnewlen

根据给定的初始化字符串 init 和字符串长度 initlen 创建一个新的 sds

//申请后,bug数组默认含有一个 '\0',但是不计入len属性中
sds sdsnewlen(const void *init, size_t initlen) {

  struct sdshdr *sh;

  // 根据是否有初始化内容,选择适当的内存分配方式
  //多申请一个字节,用于存放 '\0'
  // 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;
}

sdsempty

//创建并返回一个只保存了空字符串 "" 的 sds
sds sdsempty(void) {
  return sdsnewlen("",0);
}

sdsnew

根据给定字符串 init ,创建一个包含同样字符串的 sds,如果输入为 NULL ,那么创建一个空白 sds

sds sdsnew(const char *init) {
  size_t initlen = (init == NULL) ? 0 : strlen(init);
  return sdsnewlen(init, initlen);
}

sdsdup

复制给定 sds 的副本,创建成功返回输入 sds 的副本,失败返回 NULL

sds sdsdup(const sds s) {
  //传入的s,就是sdshdr 的 buf 属性,所以和传入一个char* 一样
  return sdsnewlen(s, sdslen(s));
}

sdsfree

void sdsfree(sds s) {
  if (s == NULL) return;
  zfree(s-sizeof(struct sdshdr));
}

sdsclear

重置 SDS 所保存的字符串为空字符串,但不释放SDS 的字符串空间。相当于惰性空间释放(缩短 SDS 保存的字符串,但并不立即使用内存重分配来回收缩短后多出来的字节)

void sdsclear(sds s) {

  // 取出 sdshdr
  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

  // 重新计算属性
  sh->free += sh->len;
  sh->len = 0;

  // 将结束符放到最前面(相当于惰性地删除 buf 中的内容)
  sh->buf[0] = '\0';
}  

sdsMakeRoomFor

对 sds 中 buf 的长度进行扩展,SDS对buf进行扩展时,会进行空间预分配,不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间,以减少后续可能会出现的空间扩展操作
其中, 额外分配的未使用空间数量由以下公式决定:

  • 如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。 举个例子, 如果进行修改之后, SDS 的 len 将变成 13 字节, 那么程序也会分配 13 字节的未使用空间, SDS 的 buf 数组的实际长度将变成 13 + 13 + 1 = 27 字节(额外的一字节用于保存空字符)。
  • 如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。 举个例子, 如果进行修改之后, SDS 的 len 将变成 30 MB , 那么程序会分配 1 MB 的未使用空间, SDS 的 buf 数组的实际长度将为 30 MB + 1 MB + 1 byte 。
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)
   //zrealloc,指向新的内存,释放原来的内存
   newsh = zrealloc(sh, sizeof(struct sdshdr)+newlen+1);

   // 内存不足,分配失败,返回
   if (newsh == NULL) return NULL;
   //已用长度不变,还是原来的len 属性
   //更新 sds 的空余长度
   newsh->free = newlen - len;

    // 返回 sds
   return newsh->buf;
}

从代码可以看出,扩展以后,free属性,并不是严格的 2 * len,比如,zrealloc的时候,长度为 newlen = 2,那么最终free的长度为,len + 2addlen,并且zrealloc内部是调用了realloc,不仅重新分配新的内存,还会把原来内存中的数据拷贝到新内存中,所以数据不会被影响,len属性还是原来的长度,并且最后一个字节存储的'\0' 也被拷贝过去

sdsRemoveFreeSpace

回收 sds 中的空闲空间,不会对 sds 中保存的字符串内容做任何修改,返回内存调整后的 sds

sds sdsRemoveFreeSpace(sds s) {
  struct sdshdr *sh;

  sh = (void*) (s-(sizeof(struct sdshdr)));

  // 进行内存重分配,让 buf 的长度仅仅足够保存字符串内容
  // T = O(N)
  sh = zrealloc(sh, sizeof(struct sdshdr)+sh->len+1);

  // 空余空间为 0
  sh->free = 0;

  return sh->buf;
}

sdsAllocSize

返回给定 sds 分配的内存字节数

size_t sdsAllocSize(sds s) {
  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
  //buf的字节数,为已使用的加上未使用的,再加上一个'\0'
  return sizeof(*sh)+sh->len+sh->free+1;
}

sdsIncrLen

传入 incr 参数,增加sds已使用长度,减少可用空余长度,用户主动调用,调用前要保证空余长度够用,这个函数是在调用 sdsMakeRoomFor() 对字符串进行扩展,然后用户在字符串尾部写入了某些内容之后,用来正确更新 free 和 len 属性的。
以下是 sdsIncrLen 的用例:

oldlen = sdslen(s);
s = sdsMakeRoomFor(s, BUFFER_SIZE);
nread = read(fd, s+oldlen, BUFFER_SIZE);
... check for nread <= 0 and handle it ...
sdsIncrLen(s, nread);

如果 incr 参数为负数,那么对字符串进行右截断操作

void sdsIncrLen(sds s, int incr) {
  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

  // 确保 sds 空间足够
  assert(sh->free >= incr);

  // 更新属性
  sh->len += incr;
  sh->free -= incr;

  // 这个 assert 其实可以忽略
  // 因为前一个 assert 已经确保 sh->free - incr >= 0 了
  assert(sh->free >= 0);

  // 放置新的结尾符号
  s[sh->len] = '\0';
}

sdsgrowzero

将 sds 扩充至指定长度,未使用的空间以 0 字节填充。

sds sdsgrowzero(sds s, size_t len) {
  struct sdshdr *sh = (void*)(s-(sizeof(struct sdshdr)));
  size_t totlen, curlen = sh->len;

  // 如果 len 比字符串的现有长度小,
  // 那么直接返回,不做动作
  if (len <= curlen) return s;

  // 扩展 sds
  // 改变的是free的长度,没有改变len的长度
  s = sdsMakeRoomFor(s,len-curlen);
  // 如果内存不足,直接返回
  if (s == NULL) return NULL;

  // 将新分配的空间用 0 填充,防止出现垃圾内容
  // 包括最后一个字节
  sh = (void*)(s-(sizeof(struct sdshdr)));
  memset(s+curlen,0,(len-curlen+1)); /* also set trailing \0 byte */

  // 更新属性
  totlen = sh->len+sh->free;
  //将len字段,扩展到所传入的len参数的长度,并减少可用空余长度
  sh->len = len;
  sh->free = totlen-sh->len;

  // 返回新的 sds
  return s;
}

sdscatlen

将长度为 len 的字符串 t 追加到 sds 的字符串末尾

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;
}

sdscat

将给定字符串 t 追加到 sds 的末尾

sds sdscat(sds s, const char *t) {
  return sdscatlen(s, t, strlen(t));
}

sdscatsds

将另一个 sds 追加到一个 sds 的末尾

sds sdscatsds(sds s, const sds t) {
  return sdscatlen(s, t, sdslen(t));
}

sdscpylen

将字符串 t 的前 len 个字符复制到 sds s 当中,并在字符串的最后添加终结符。会覆盖原来sds字符串的数据

sds sdscpylen(sds s, const char *t, size_t len) {

  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));

  // sds 现有 buf 的长度
  size_t totlen = sh->free+sh->len;

  // 如果 s 的 buf 长度不满足 len ,那么扩展它
  if (totlen < len) {
      //这里扩展的长度之所以不用 len - sh->len - sh->free
      //是因为 sdsMakeRoomFor 内部会对 free 进行判断和处理
      s = sdsMakeRoomFor(s,len-sh->len);
      if (s == NULL) return NULL;
      sh = (void*) (s-(sizeof(struct sdshdr)));
      //用于更新最后的 free属性
      totlen = sh->free+sh->len;
  }

  // 复制内容
  // T = O(N)
  memcpy(s, t, len);

  // 添加终结符号
  s[len] = '\0';

  // 更新属性
  sh->len = len;
  sh->free = totlen-len;

  // 返回新的 sds
  return s;
}

sdscpy

将字符串复制到 sds 当中,覆盖原有的字符。如果 sds 的长度少于字符串的长度,那么扩展 sds 。

sds sdscpy(sds s, const char *t) {
  return sdscpylen(s, t, strlen(t));
}

sdsll2str

传入一个char * 和long long 的数值,将数值转为字符串放入传入的char 中,char 指向的地址要事先分配好内存

#define SDS_LLSTR_SIZE 21
int sdsll2str(char *s, long long value) {
  char *p, aux;
  unsigned long long v;
  size_t l;
  //转为无符号数
  v = (value < 0) ? -value : value;
  //指向字符串开头
  p = s;
  //不占用额外的空间,直接在原来的字符串内存上改
  do {
      //按照个十百千万的顺序
      *p++ = '0'+(v%10);
      v /= 10;
  } while(v);
  //这样运行完,是倒序的,比如 -123456,运行完是 654321-
  if (value < 0) *p++ = '-';

  l = p-s;
  *p = '\0';
   //p指向的是 '\0',所以要 -- 以后重新指向有效的数据位
   //如果value是 123456,运行完是654321,p指向 1,如果value是 -123456,p指向 -(负号)
  p--;
  //前后交换,s还是指向开头
  while(s < p) {
      aux = *s;
      *s = *p;
      *p = aux;
      s++;
      p--;
   }
  return l;
}

sdsfromlonglong

输入一个 long long 值 value ,转为字符串,创建一个 SDS

sds sdsfromlonglong(long long value) {
  char buf[SDS_LLSTR_SIZE];
  int len = sdsll2str(buf,value);

  return sdsnewlen(buf,len);
}

sdstrim

对 sds 左右两端进行修剪,清除其中 cset 指定的所有字符
比如 sdstrim(AA...AA.a.aa.aHello World:::Aa,"A.:"),返回 Hello World,不区分大小写。但是,只会清除两端的,比如sdstrim(xyyxHelloxxxyxWorldxyxxy","xy"),返回HelloxxxyxWorld

sds sdstrim(sds s, const char *cset) {
  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
  char *start, *end, *sp, *ep;
  size_t len;

  // sp 和 start 指向字符串开头
  sp = start = s;
  // ep 和 end 指向字符串的结尾
  ep = end = s+sdslen(s)-1;

  //从头开始遍历,只要当前sp指向的字符,在cset中,指针就继续后移
  while(sp <= end && strchr(cset, *sp)) sp++;
  //从尾部朝前遍历,只要当前ep指向的字符,在cset中,指针就继续前移
  while(ep > start && strchr(cset, *ep)) ep--;

  //到这的时候,sp和ep所指向的字符,在cset中不存在,但是sp和ep之间的不管

  // 计算 trim 完毕之后剩余的字符串长度
  len = (sp > ep) ? 0 : ((ep-sp)+1);

  // 如果有需要,前移字符串内容
  if (sh->buf != sp) memmove(sh->buf, sp, len);

  // 添加终结符
  sh->buf[len] = '\0';

  // 更新属性
  sh->free = sh->free+(sh->len-len);
  sh->len = len;

  // 返回修剪后的 sds
  return s;
}

sdsrange

按索引截取 sds 字符串的其中一段,start 和 end 都是闭区间(包含在内),索引从 0 开始,最大为 sdslen(s) - 1,索引可以是负数, sdslen(s) - 1 == -1,例如:sdsrange("Hello World",1,-1); => "ello World"

void sdsrange(sds s, int start, int end) {
  struct sdshdr *sh = (void*) (s-(sizeof(struct sdshdr)));
  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;
  //newlen大于0,截取才有意义
  if (newlen != 0) {
     if (start >= (signed)len) {
        newlen = 0;
        //当结束的下标大于字符串长度时,做一个处理,截取到尾部,但是start没有这种处理
     } else if (end >= (signed)len) {
        end = len-1;
        newlen = (start > end) ? 0 : (end-start)+1;
     }
  } else {
      start = 0;
  }

  if (start && newlen) memmove(sh->buf, sh->buf+start, newlen);

  // 添加终结符
  sh->buf[newlen] = 0;

  // 更新属性
  sh->free = sh->free+(sh->len-newlen);
  sh->len = newlen;
}

sdstolower

将 sds 字符串中的所有字符转换为小写

void sdstolower(sds s) {
  int len = sdslen(s), j;

  for (j = 0; j < len; j++) s[j] = tolower(s[j]);
}

sdstoupper

将 sds 字符串中的所有字符转换为大写

void sdstoupper(sds s) {
  int len = sdslen(s), j;

  for (j = 0; j < len; j++) s[j] = toupper(s[j]);
}

sdscmp

对比两个 sds , strcmp 的 sds 版本,相等返回 0 ,s1 较大返回正数, s2 较大返回负数

int sdscmp(const sds s1, const sds s2) {
  size_t l1, l2, minlen;
  int cmp;

  l1 = sdslen(s1);
  l2 = sdslen(s2);
  minlen = (l1 < l2) ? l1 : l2;
  //memcmp按字节比较,返回值的意义和sdscmp一样
  cmp = memcmp(s1,s2,minlen);

  if (cmp == 0) return l1-l2;

  return cmp;
}

sdssplitlen

使用分隔符 sep 对 s 进行分割,返回一个 sds 字符串的数组。*count 会被设置为返回数组元素的数量。
如果出现内存不足、字符串长度为 0 或分隔符长度为 0的情况,返回 NULL
分隔符可以的是包含多个字符的字符串
这个函数接受 len 参数,因此它是二进制安全的

SDS的二进制安全

C 字符串中的字符必须符合某种编码(比如 ASCII), 并且除了字符串的末尾之外, 字符串里面不能包含空字符, 否则最先被程序读入的空字符将被误认为是字符串结尾 —— 这些限制使得 C 字符串只能保存文本数据, 而不能保存像图片、音频、视频、压缩文件这样的二进制数据。所有 SDS API 都会以处理二进制的方式来处理 SDS 存放在 buf 数组里的数据, 程序不会对其中的数据做任何限制、过滤、或者假设 —— 数据在写入时是什么样的, 它被读取时就是什么样。这也是我们将 SDS 的 buf 属性称为字节数组的原因 —— Redis 不是用这个数组来保存字符, 而是用它来保存一系列二进制数据

sds *sdssplitlen(const char *s, int len, const char *sep, int seplen, int *count) {
  //返回的字符串数组的元素个数
  int elements = 0;
  //用于申请返回字符串数组的容量
  int slots = 5;
  //用于标记从哪开始分割
  int start = 0, j;
  sds *tokens;

  if (seplen < 1 || len < 0) return NULL;
  //先申请slots 个 sds内存
  tokens = zmalloc(sizeof(sds)*slots);
  if (tokens == NULL) return NULL;

  if (len == 0) {
    *count = 0;
    return tokens;
  }

  // T = O(N^2)
  for (j = 0; j < (len-(seplen-1)); j++) {
    /* make sure there is room for the next element and the final one */
    //当数组内存不太够用的时候,直接重新分配2倍
    if (slots < elements+2) {
        sds *newtokens;

        slots *= 2;
        newtokens = zrealloc(tokens,sizeof(sds)*slots);
        if (newtokens == NULL) goto cleanup;
        tokens = newtokens;
    }
    /* search the separator */
    // T = O(N)
    //判断一下是否相等,如果相等,增加到返回数组里
    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);
  if (tokens[elements] == NULL) goto cleanup;
  elements++;
  *count = elements;
  return tokens;

cleanup:
 {
    int i;
    for (i = 0; i < elements; i++) sdsfree(tokens[i]);
    zfree(tokens);
    *count = 0;
    return NULL;
  }
 }
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 205,033评论 6 478
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 87,725评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 151,473评论 0 338
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,846评论 1 277
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,848评论 5 368
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,691评论 1 282
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 38,053评论 3 399
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,700评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 42,856评论 1 300
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,676评论 2 323
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,787评论 1 333
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,430评论 4 321
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 39,034评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,990评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,218评论 1 260
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 45,174评论 2 352
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,526评论 2 343