超轻量字符缓冲区库
在写yasf项目的过程中遇到这样的需求:为了构造SQL语句,我必须将若干个字符串拼接起来。一个凑合的办法是,定义一个足够大的静态缓冲区,然后使用诸如sprintf
这样的库函数来构造字符串。但是事实上,构造出来的字符串的总长度是没有上限的。因此,若使用固定长度的缓冲区则会带来缓冲区溢出的隐患(C语言最著名的缺陷之一)。
大部分现代的语言的字符串功能都远强于C,字符串拼接更是非常基本的功能。选择C作为开发语言,那就不得不重复造轮子。我总抱着自己能造出更好的轮子的虚幻的指望。经过若干次试验,我得到了一种比较满意的实现方式。于是我把它从yasf项目中抽了出来,做成一个独立的模块,放在GitHub上,命名为ulbuf(__u__ltra __l__ightweight __buf__fer)。
整个实现的代码不足百行,功能也是少之又少。我发现,只有这样才能保证最大的灵活性。
整个库的特点:
简单
API非常简单,共4个函数。其中,2个用于缓冲区的创建和销毁,1个是为了便于使用而提供的便捷函数。因此,最关键的函数只有1个。
API的参数和返回值的形式是经过仔细设计的。
灵活
在C语言中,用户常常通过增/减指针来表示向字符串中增添/删除字符。只要预留了空间,ulbuf仍允许他们这样做。
在最初的实现中,ulbuf还保存了“缓冲区中已含有的字节数”这一个信息。并且只能向缓冲区中追加内容。对于yasf项目而言,已经足够。
现在的实现中,ulbuf只负责管理缓冲区的容量(最多能容纳的字节数),不储存已含有的字节数信息。既能简化实现,又能保证使用的灵活性。这可能是体现“少即是多”的一个例子。
鲁棒
当用户提示ulbuf需要向缓冲区中写入n个字符时,ulbuf会在第n+1个字符的位置写入字符'\0'
。因此,缓冲区的内容永远是合法的C字符串。这样,使用C标准库操作缓冲区永远不会发生内存越界的错误。
协调
ulbuf创建的缓冲区是char *
类型,可以把它当作正常的字符串,提供给诸如strlen
、strchr
这类标准库函数。
只要预留了空间,那么也可以把缓冲区作为输出参数,提供给诸如strcpy
、sprintf
这类函数使用也是完全没有问题的。这类函数会在第n+1个字符的位置添上一个字符'\0'
,仅仅是把ulbuf已经写入的'\0'
覆写了一次,而没有发生内存越界访问。
高效
在我的平台上测试重复追加字符串的操作,ulbuf的运行速度略快于C++ STL的string
类。
因为STL通常是仔细编写的、深度优化的运行库,可见ulbuf的运行效率是很高的。(欲知具体情况,可以编译并运行代码仓库中的example.cc
程序。)
因为代码中不含文档(仅提供了一个使用范例),所以我在此写一下它的用法。
创建
char *bufnew(size_t size);
创建一个初始容量是size
的缓冲区。ulbuf库的唯一作用就是提供一个不限容量的缓冲区,除了这里需要提供一个初始容量以外,以后用户再也无需理会容量的问题。
如果使用者预感到缓冲区中可能会被填充大量字符,那么可以提供一个较大的初始容量,以便减少realloc
的调用次数;反之,提供一个较小的初始容量,可以节省内存用量。这相当于是在时间和空间之间做一个权衡,我把这个权利留给了使用者。
扩展
char *bufext(char **buf, char *tail, size_t n)
这是整个实现里最核心的函数。其作用是,确保缓冲区里至少还有n个字节的可写空间。可写空间是从tail
指针的位置开始向后算的。该函数返回的是可写空间的指针。
如果这个函数未调用realloc
,则返回值等于tail
,并且第一个参数指向的值(即整个缓冲区的地址)保持不变。常用的用法是:
if (p = bufext(&buf, p, n)) {
/* 向p写入n个字节 */
p += n;
} else {
/* 处理内存不足问题 */
}
如果realloc
失败(内存不足),则返回空指针,而原有的缓冲区保持不变。即使发生了内存不足,继续调用bufext
也是可以的,这时候传入的tail
为空指针,bufext
相当于一个no-op。
追加
char *bufcat(char **buf, char *tail, const char *s);
该函数向缓冲区末尾追加字符串s
的内容。适用于预先不知道s
的长度的场合。例如
p = bufcat(&buf, p, "some text...");
该函数先调用strlen
再调用bufext
,最后调用memcpy
。所以它只是一个便捷函数。
销毁
void bufdel(char *buf);
该函数销毁使用bufnew
创建的缓冲区。必须使用该函数,而不能使用free
来释放缓冲区内存,这是ulbuf的一个限制。
关键的原因是,我必须在使用malloc
分配得到的内存的开头保存缓冲区的长度信息。所以,bufnew
返回的缓冲区的地址不是分配到的内存块的首地址,所以不能用free
释放。