15

超轻量字符缓冲区库

 3 years ago
source link: https://z-rui.github.io/post/2015/08/ulbuf/
Go to the source link to view the article. You can view the picture content, updated content and better typesetting reading experience. If the link is broken, please click the button below to view the snapshot at that time.
neoserver,ios ssh client

超轻量字符缓冲区库

Sun Aug 2, 2015

在写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释放。



About Joyk


Aggregate valuable and interesting links.
Joyk means Joy of geeK