超轻量字符缓冲区库

在写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 *类型,可以把它当作正常的字符串,提供给诸如strlenstrchr这类标准库函数。

只要预留了空间,那么也可以把缓冲区作为输出参数,提供给诸如strcpysprintf这类函数使用也是完全没有问题的。这类函数会在第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释放。


分享