varch/doc/str.md
2024-07-21 19:02:13 +08:00

26 KiB
Raw Permalink Blame History

介绍

字符串实际是一串字符的集合在C语言中字符串是为char类型的数组在结尾加上结束符\0则为字符串,没有结束符则为字符数组。
在C语言中没有专门的字符串变量而varch中的str则专门针对数组字符串进行了封装封装成不需要关注底层存储结构的字符串变量。str实际还是通过连续地址的空间进行存储字符集但是这些存储的工作不需要额外去关注str提供的方法已经实现存储的工作而且str也提供符合字符串操作的方法方便字符串的灵活操作。方便说明后面的传统的C语言字符串称为数组字符串varch提供的成为str字符串。

接口

创建和删除str字符串对象

str_t str_create(void *string);
void str_delete(str_t str);
#define str(str) // 为了更简便的使用对str_create套一层宏定义
#define _str(str) // 对str_delete套一层宏定义并在str删除后置为空

其中str_t为str的结构体创建方法则会返回一个str对象创建失败则返回NULL其中string传入初始化的字符串,这个string定义为了void *的类型所以可以支持数组字符串和str字符串的两种传参初始化后续介绍中的void *string均是可以支持这两种字符串。删除方法则是删除传入的str对象。创建方法和删除应该成对使用创建出来在结束使用应该删除掉。

void test(void)
{
	str_t ss = str("Hello str!"); // 用数组字符串进行赋值构造
	str_t copy = str(ss); // 用str字符串进行拷贝构造
	_str(ss); // 删除str
	_str(copy); // 删除str
}

str字符串的数组字符串

#define str_c_str(str)
#define _S(str)

str存储字符串也是通过连续地址空间进行存储获取此地址即可访问到实际存储的字符串。但在使用中建议不要通过此方法把字符串当成数组字符串来修改。有同样作用的方法还有char* str_data(str_t str, int pos);str_c_str()也是套用str_data实现的,此方法不建议替代str_c_str()来用。而_S()则是简短版的。

void test(void)
{
	str_t ss = str("Hello str!"); // 用数组字符串进行赋值构造
	str_t copy = str(ss); // 用str字符串进行拷贝构造
	printf("ss: %s\r\n", _S(ss));
	printf("copy: %s\r\n", _S(copy));
	_str(ss); // 删除str
	_str(copy); // 删除str
}

结果:

ss: Hello str!
copy: Hello str!

str赋值

str_t str_assign(str_t str, void *string);

str抽象字符串为一个类了给str进行赋值操作类似于=号的赋值操作。如同前面所说的void *string可以支持数组字符串和str字符串。

void test(void)
{
	str_t ss = str(""); // 初始化为空字符串
	printf("ss: %s\r\n", _S(ss));
	str_assign(ss, "Hello str!"); // 赋值为 Hello str!
	printf("ss: %s\r\n", _S(ss));
	str_assign(ss, "This is the assignment method!"); // 再赋值为 This is the assignment method!
	printf("ss: %s\r\n", _S(ss));
	_str(ss);
}

结果:

ss: 
ss: Hello str!
ss: This is the assignment method!

从中可以看到str不需要像数组字符串那样去关注定义的空间的大小问题而由str内部去进行适应。

str拼接

#define str_append(str, ...)

str除了有类似=号的赋值操作,还有类似+=号的拼接操作。
形参后面不定参数,可以拼接至少一个任意数量的字符串。同样可以是数组字符串也可以是str字符串。
str_append方法的原型是str_t str_append_series(str_t str, ...);,原型函数不定参数需要接NULL结尾,不建议使用原型方法。

void test(void)
{
	str_t name = str("123456789");
	str_t ident = str("qq");
	str_t email = str("");

	str_append(email, name, "@", ident, ".com"); // 拼接若干个字符串
	printf("%s\r\n", str_c_str(email));

	_str(name);
	_str(ident);
	_str(email);
}

结果:

123456789@qq.com

str插入

str_t str_insert(str_t str, int pos, void *string);

在str中插入一个字符串并返回自己。pos是插入的位置,string是插入的源字符串。

void test(void)
{
	str_t ss = str("0123456");
	str_insert(ss, 3, "|insert|"); // 在位置3插入字符串
	printf("ss: %s\r\n", str_c_str(ss));
	_str(ss);
}

结果:

ss: 012|insert|3456

str移除

str_t str_erase(str_t str, int pos, int len);

从str中移除一段字符并返回自己。pos是移除的位置,len是移除的长度。

void test(void)
{
	str_t ss = str("0123456789");
	str_erase(ss, 3, 3); // 移除位置3后面三个字符也就是移除 345
	printf("ss: %s\r\n", str_c_str(ss));
	_str(ss);
}

结果:

ss: 0126789

str尾部推入和弹出

int str_push_back(str_t str, char c);
char str_pop_back(str_t str);

这两个方法分别从str字符串尾部推入和弹出一个字符推入方法的返回值1成功0失败弹出方法返回弹出的字符。
str存储结构和vector类似,在尾部操作具有比较高的效率。

void test(void)
{
	str_t ss = str("0123456789");
	str_push_back(ss, 'A');
	printf("ss: %s\r\n", str_c_str(ss));
	printf("pop %c\r\n", str_pop_back(ss));
	printf("ss: %s\r\n", str_c_str(ss));
	_str(ss);
}

结果:

ss: 0123456789A
pop A
ss: 0123456789

str字符串比较

int str_compare(str_t str, void *string);

比较str与另外一个字符串0为相等-1为str小于string1为str大于string。

void test(void)
{
	str_t ss = str("01234");
	str_t copy = str(ss);
	printf("compare: %d\r\n", str_compare(ss, copy));
	printf("compare: %d\r\n", str_compare(ss, "01233"));
	printf("compare: %d\r\n", str_compare(ss, "012345"));
	_str(ss);
	_str(copy);
}

结果:

compare: 0
compare: 1
compare: -1

str创建子串

str_t str_substr(str_t str, int pos, int len);

这个方法是从str中创建一个新的子串切记,在结束使用后,调用_str(str)方法来删除。。
pos传入起始位置,len传入创建的长度。

void test(void)
{
	str_t ss = str("0123456789");
	str_t sub = str_substr(ss, 4, 2);
	printf("ss: %s\r\n", str_c_str(ss));
	printf("sub: %s\r\n", str_c_str(sub));
	_str(ss);
	_str(sub);
}

结果:

ss: 0123456789
sub: 45

str字符串长度

int str_length(str_t str);

str标准的获取长度的方法是这个当然也可以用strlen方法去量,但是对于str对象没必要,因为这个标准的方法是不用耗费时间去量的,返回记录好了的长度。

void test(void)
{
	str_t ss = str("0123456789");
	printf("length: %d\r\n", str_length(ss));
	_str(ss);
}

结果:

length: 10

str字符读写

#define str_at(str, i)

str提供了针对其中的字符进行读写操作当作数组的元素使用即可。 但切记,不要把字符修改成\0了。

void test(void)
{
	str_t ss = str("0123456789");
	printf("ss[3] = %c\r\n", str_at(ss, 3));
	str_at(ss, 3) = 'A';
	printf("ss: %s\r\n", _S(ss));
	_str(ss);
}

结果:

ss[3] = 3
ss: 012A456789

str字符串查找

int str_find(str_t str, void *string, int pos);
int str_rfind(str_t str, void *string, int pos);

这两个方法分别从正向和反向查找字符子串,string传入字符串,pos传入开始查找的起始索引,返回则是返回子串所在的位置索引,没找到就返回str_npos

void test(void)
{
	str_t ss = str("0123456789");
	printf("find = %d\r\n", str_find(ss, "3456", 0)); // 从0位置开始找
	printf("rfind = %d\r\n", str_rfind(ss, "23", str_length(ss) - 1)); // 从尾端开始往回找
	printf("find = %d\r\n", str_find(ss, "0000", 0)); // 找一个不存在的字符串
	_str(ss);
}

结果:

find = 3
rfind = 2
find = 2147483647

str查找匹配字符集

int str_find_first_of(str_t str, void *string, int pos);
int str_find_first_not_of(str_t str, void *string, int pos);
int str_find_last_of(str_t str, void *string, int pos);
int str_find_last_not_of(str_t str, void *string, int pos);

这四个方法和str_findstr_rfind方法不一样find和rfind需要匹配全部字符而这四个方法只要匹配上字符集的任意之一就匹配成功。
同样,string传入字符串(匹配字符集),pos传入开始查找的起始索引,返回则是返回子串所在的位置索引,没找到就返回str_npos。这四个函数分别找到**“第一个包含”“第一个不包含”“最后一个包含”“最后一个不包含”**
例子:实际应用获取一个文件路径的文件和所在盘符

void test(void)
{
	str_t ss = str("C:/workspace/project/C/00 - vlib/Project/main.c"); // 先给定一个文件路径
	str_t pan = NULL; // 初始化为NULL不构建
	str_t filename = NULL; // 初始化为NULL不构建
	pan = str_substr(ss, 0, str_find_first_of(ss, ":", 0)); // 先找到第一个 :
	filename = str_substr(ss, str_find_last_of(ss, "\\/", str_length(ss)-1) + 1, str_length(ss)); // 找到最后一个 \ 或者 / 路径分隔符
	printf("pan: %s\r\n", str_c_str(pan));
	printf("filename: %s\r\n", str_c_str(filename));
	_str(ss);
	_str(pan);
	_str(filename);
}

结果:

pan: C
filename: main.c

str字符串翻转

str_t str_reverse(str_t str, int begin, int end);

这个方法将str指定区间的字符串整体翻转。begin为区间起始位置,end为区间结束位置。

void test(void)
{
	str_t ss = str("0123456789");
	str_reverse(ss, 2, 8);
	printf("ss = %s\r\n", str_c_str(ss));
	_str(ss);
}

结果:

ss = 0187654329

str字符替换

str_t str_replace(str_t str, int pos, int len, void *string);

这个方法把指定位置的字符按长度替换成指定的字符串。

void test(void)
{
	str_t ss = str("My name is ZhangSan!");
	printf("ss = %s\r\n", str_c_str(ss));
	str_replace(ss, 11, 8, "Lisi"); // 把ZhangSan替换成Lisi
	printf("ss = %s\r\n", str_c_str(ss));
	_str(ss);
}

结果:

ss = My name is ZhangSan!
ss = My name is Lisi!

str字符串交换

void str_swap(str_t str, str_t swap);

将两个字符串进行交换。

void test(void)
{
	str_t s1 = str("This s1!");
	str_t s2 = str("This s2!");
	str_swap(s1, s2);
	printf("s1 = %s\r\n", str_c_str(s1));
	printf("s2 = %s\r\n", str_c_str(s2));
	_str(s1);
	_str(s2);
}

结果:

s1 = This s2!
s2 = This s1!

str字符串复制到数组

int str_copy(str_t str, int pos, int len, char *buf);

将str字符串指定位置的字符复制到指定的数组。其中pos复制的起始位置。len复制的最大长度。buf就是接收复制数据的数组。函数返回实际复制的长度。

void test(void)
{
	str_t ss = str("0123456789");
	char array[32] = { 0 };
	int length = 0;
	length = str_copy(ss, 5, sizeof(array), array); // 从位置开始复制,复制最大长度为数组的长度
	array[length] = '\0'; // 在后面添加结束符,方便打印
	printf("length = %d, copy: %s\r\n", length, array);
	_str(ss);
}

结果:

length = 5, copy: 56789

str字符串格式化

str_t str_format(str_t str, char *format, ...);

str字符串格式化操作套用sprintf函数进行了重写,与其用法基本一致。增加了%s对str类型的支持str_format对格式化进行分段的动态扩容,在使用上不需要像sprintf那样关注buf长度是否超出的问题使用起来更加的简便。

void test(void)
{
	str_t ss = str("");
	str_t pp = str("format");
	str_format(ss, "Hello str! %s %s! int:%d, float:%.2f, char:%c", pp, "function", 18, 175.5, 'A');
	printf("ss: %s\r\n", str_c_str(ss));
	_str(ss);
	_str(pp);
}

结果:

ss: Hello str! format function! int:18, float:175.50, char:A

从这个例子看,str_format不用去关注空间的问题,%s除了对char*支持也可以对str_t对象支持,做到了两种字符串的兼容。

参考例子

案例1

输入一个字符串,判断是否为回文字符串,即从左到右和从右到左完全一致。

void test(void)
{
	str_t s = str("");
	str_t r = str("");
	char buf[64] = {0};
	while (1)
	{
		scanf("%s", buf); // 输入一个字符串
		str_assign(s, buf); // 将s赋值为输入的字符串
		str_assign(r, s); // 将r赋值为s
		str_reverse(r, 0, str_length(r) - 1); // 将r进行翻转
		if (str_compare(s, r) == 0) // 比较s和r是否一致
		{
			printf("这是一个回文数字符串!\r\n");
		}
		else  
		{
			printf("这不是一个回文数字符串!\r\n");
		}
	}
	_str(s);
	_str(r);
}

结果:

qwewq
这是一个回文数字符串!
qwerrewq
这是一个回文数字符串!
zxcvfd
这不是一个回文数字符串!

案例2

输入一个字符串,删除字符串中重复出现的字符。

void test(void)
{
	str_t s = str("");
	str_t r = str("");
	char buf[64] = {0};

	while (1)
	{
		scanf("%s", buf); // 输入一个字符串
		str_assign(s, buf); // 将s赋值为输入的字符串
		str_assign(r, ""); // 将r设为空
		for (int i = 0; i < str_length(s); i++) // 遍历s
		{
			char t[2] = {str_at(s, i),0}; // 取出s的字符组成一个短的数组字符串
			if (str_find(r, t, 0) == str_npos) // 判断这个数组字符串是否在r当中了也就是判断s的字符是否在r里面
			{
				str_append(r, t); // 不在里面则追加到里面
			}
		}
		printf("%s\r\n", _S(r));
	}

	_str(s);
	_str(r);
}

结果:

16783679816488742135468794
167839425
       
asdfgsaf
asdfg

hijief145ahya21
hijef145ay2

源码解析

str结构体

str容器的所有结构体都是隐式的也就是不能直接访问到结构体成员的这样子的方式保证了模块的独立与安全防止外部调用修改结构体的成员导致str存储结构的破坏。所以str解析器只留了唯一一个str的声明在头文件然后结构体的定义都在源文件。只能使用str容器提供的方法对str对象进行操作。
str类型声明

typedef struct STR *str_t;

使用时候,只是用str_t即可。

/* str define */
typedef struct STR
{
	char ident;								/* ident of str */
	char* base;								/* base address of string */
	int length;								/* length of str */
	int capacity;							/* capacity of str */
} STR;

STR结构体中包含了4个成员ident识别标志区分是数组字符串还是str字符串的依据base真正存储字符串的基地址lengthstr的大小也就是str的长度capacity分配的空间的容量用来实际存储字符串一般情况下不用去关注容量的事情

区分数组字符串和str字符串

STR结构体当中,ident成员就是用于区分数组字符串和str字符串的标志那是怎么区分的呢
看下区分的代码:

/* str info define */
typedef struct 
{
	char *base;
	int length;
} str_info_t;

static str_info_t string_info(void *string)
{
	str_info_t info;

	if (((char *)string)[0] == 0) /* empty array string */
	{
		info.base = (char *)string;
		info.length = 0;
	}
	else if (((str_t)string)->ident == ident()) /* str type */
	{
		info.base = ((str_t)string)->base;
		info.length = ((str_t)string)->length;
	}
	else /* array string type */
	{
		info.base = (char *)string;
		info.length = strlen((char *)string);
	}

	return info;
}

首先,ident定义在结构体的首位,也就是结构体地址首位存储的内容就是ident的内容,所以当传入void *string,先判断一下这个地址首个字节的内容是什么。当为\0时候,就表示是一个空的数组字符串,当为ident()的时候,就代表是str字符串了,其他的值则为数组字符串了。 那这个ident()到底是什么?为什么其可以区分。

#define ident()								(-1)

其实就是-1那就是只要传入的地址的首个字节内容为-1就是str字符串否则就是数组字符串。为什么-1可以用来区分。
首先先看ASCII编码的规则字符只在0 ~ 127之间,用不上-1,那-1是可以用来区分的在ASCII编码中区分没问题。
再看Unicode编码的Unicode编码范围在0x0000 ~ 0x10FFFF-1对应的无符号值是0xFF在Unicode编码范围内没有以0xFF开头的,所以-1是可以用来区分的在Unicode编码中区分没问题。
然后在UTF-8编码中UTF-8编码是将Unicode编码调整为可变长的在1~4个字节不等。在为ASCII字符的时候采用一个字节编码保持和ASCII一致超过了ASCII编码范围就变成了多字节多字节是以第一个字节作为起始标志这个字符需要多少字节然后后面的字节归属这个字符。比如2字节的为110xxxxx 10xxxxxx,第一个字节前面多少个1就代表多少字节编码,以0分隔,后面的字节以10开头,其余x部分就是编码数据部分如此3字节的为1110xxxx 10xxxxxx 10xxxxxx4字节11110xxx 10xxxxxx 10xxxxxx 10xxxxxx。说明UTF-8的编码规则也就是说明在UTF-8编码中也是没有以0xFF开头的,所以-1是可以用来区分的在UTF-8编码中区分没问题。
选择-1作为区分,是因为在常用的编码里面,都不会用到,所以用-1来作为区分。
回到str_info_t string_info(void *string)函数,在str的api中涉及到需要区分字符串类型的都调用了这个函数进行区分返回的str_info_t就包含字符串的基地址个长度。

str自适应长度

在str的所有api当中使用时候都没有涉及需要自己去分配或者定义多少空间因为在str内部会自适应字符串长度来动态调整存储空间的大小。
str_assign赋值函数为例:

str_t str_assign(str_t str, void *string)
{
	str_info_t info;
	if (!str) return NULL;
	if (!string) return NULL;;
	info = string_info(string); // 获取字符串的信息,基地址和长度
	if (!str_alter_capacity(str, info.length)) return NULL; // 根据长度进行容量调整
	strcpy(str->base, info.base); // 将字符串复制到str空间
	str->length = info.length; // 更新长度
	return str;
}

其代码长度也就是这几行。其中调整容量str_alter_capacity函数。

static int str_alter_capacity(str_t str, int length)
{
	int capacity = 0;
	char *base = NULL;

	/* check whether the capacity changes and need to adjust the capacity space */
	capacity = gradient_capacity(length);
	if (str->capacity != 0 && str->capacity == capacity) return 1;
	if (!str->base) /* allocate new space */
	{
		str->base = (char *)malloc(capacity + 1);
		if (!str->base) return 0;
	}
	else /* reallocate space */
	{
		base = (char *)realloc(str->base, capacity + 1);
		if (!base) return 0;
		str->base = base;
	}
	str->capacity = capacity;
	return 1;
}

str获取实际存储数据

char* str_data(str_t str, int pos)
{
	if (!str || pos < 0 || pos >= str->length) 
	{
		error = 0; /* reset error area */
		return &error;
	}
	return &str->base[pos];
}

str_atstr_c_str_S都是基于str_data实现的,str_data实现的内容其实也很简单,就是将结构体中base指定索引的地址进行返回。在其中加入了error机制,也就是当传入的添加不满足的时候,会返回error的地址,error为模块定义的一个静态变量。防止返回NULL对空地址进行操作时候导致的程序崩溃。

str字符串替换原理

str_t str_replace(str_t str, int pos, int len, void *string)
{
	str_info_t info;
	char *overlap = NULL;
	if (!str) return NULL;
	if (!string) return NULL;
	if (pos < 0 || pos > str->length) return NULL;
	if (len > str->length - pos) len = str->length - pos;
	info = string_info(string);
	
	/* check if addresses overlap
	这一步是检查地址有没有重叠就是string信息的地址有没有和原来的str的地址重叠
	重叠的处理机制是,分配一块空间出来,把要替换的内容复制一份
	到后面再把复制出来的替换进去
	 */
	if (str->base <= info.base && info.base <= str->base + str->length && pos < str->length)
	{
		overlap = (char *)malloc(info.length + 1);
		if (!overlap) return NULL;
		strcpy(overlap, info.base);
		info.base = overlap;
	}

	/*
	这一步,判断被替换的字符串与替换字符串的长度关系,也就是判断需不需要调整容量大小
	*/
	if (info.length > len) // 替换的字符串长了,需要扩容
	{
		/* 先扩容,扩大的容量为这个长度差值 */ 
		if (str_alter_capacity(str, str->length + (info.length - len)) == 0)
		{
			if (overlap) free(overlap);
			return NULL;
		}
		/* 把后部分数据往后移动,腾出点空间来存放替换进来较长的字符串 */
		memmove(&str->base[pos + info.length], &str->base[pos + len], str->length - (pos + len));
		/* 腾出空间后,把替换的字符串复制进来 */
		memcpy(&str->base[pos], info.base, info.length);
	}
	else if (info.length < len) /* 长度变小 */ 
	{
		/* 把后面的数据往前面移留info的长度给替换进来的字符串即可 */
		memmove(&str->base[pos + info.length], &str->base[pos + len], str->length - (pos + len));
		/* 把替换的字符串复制进来 */
		memcpy(&str->base[pos], info.base, info.length);
		/* 缩容的,后面调整容量 */
		str_alter_capacity(str, str->length + (info.length - len));
	}
	else /* 长度没发生变化 */
	{
		/* 直接覆盖即可 */
		memcpy(&str->base[pos], info.base, info.length);
	}
	str->length += (info.length - len);
	str->base[str->length] = 0;
	if (overlap) free(overlap);

	return str;
}

不管是插入还是移除的方法,都是基于替换的方法进行实现的。

str_t str_insert(str_t str, int pos, void *string)
{
	return str_replace(str, pos, 0, string);
}

str_t str_erase(str_t str, int pos, int len)
{
	return str_replace(str, pos, len, "");
}

str拼接原理

#define str_append(str, ...) str_append_series((str), ##__VA_ARGS__, NULL)
str_t str_append_series(str_t str, ...)
{
	va_list args;
	void* s = NULL;
	if (!str) return NULL;
	va_start(args, str);
	s = va_arg(args, void*);
	while (s)
	{
		/* insert at the end */
		if (!str_insert(str, str->length, s))
		{
			va_end(args);
			return NULL;
		}
		s = va_arg(args, void*);
	}
	va_end(args);
	return str;
}

实际就是调用str_append_series函数进行传参,在不定参数后面传入个NULL作为结尾。
而在str_append_series函数内部,处理不定参数,依次将一段段字符串通过str_insert插到尾端,完成拼接。

str格式化源码

str_t str_format(str_t str, char *format, ...)
{
	va_list args;
	int i = 0;
	int len = 0;						/* length of sub string */
	char *s = NULL;						/* temp string */
	str_info_t info;					/* str info */
	double dbl = 0.0;					/* double precision floating point */
	char *begin = NULL;					/* begin of format */
	char qualifier = 0;					/* conversion qualifier for integer as 'h','l','L' */
	int width = 0;						/* transition field width */
	int precision = 0;					/* minimum digits of integers and maximum digits of strings */
	char tfmt[16] = "%";				/* temporary format */

	if (!str) return NULL;
	str_assign(str, "");
	va_start(args, format);
	for (; *format; format++)
	{
		/* 为普通字符时 */
		if (*format != '%')
		{
			if (!begin) begin = format; // 记录下普通字符段的起始位置
			continue; // 不再执行这个循环跳到下一个
		}

		/* 记录了普通字符的起始位置,也就表示当前位置的前段部分为普通的字符 */
		if (begin) 
		{
			len = format - begin; // 记录下长度给后面扩容
			if (!str_alter_capacity(str, str->length + len)) goto FAIL_CAPACITY;
			while (len--) str->base[str->length++] = *begin++; // 复制普通的字符进去
			begin = NULL;
		}

		/* 从这里开始处理格式化的数据段 */
		begin = format;
		while (1) // 记录前置符号标志
		{
			/* skips the first '%' also */
			format++;
			if ((*format != '0') &&
				(*format != ' ') && 
				(*format != '+') && 
				(*format != '-') && 
				(*format != '#')) break;
		}

		/* get field width */
		width = -1;
		if (is_digit(*format)) 
		{
			format += get_digit(format, &width);
		}
		else if (*format == '*')
		{
			format++;
			width = va_arg(args, int);
			if (width < 0) width = -width;
		}

		/* get the precision */
		precision = -1;
		if (*format == '.')
		{
			format++;
			if (is_digit(*format)) format += get_digit(format, &precision);
			else if (*format == '*')
			{
				format++;
				precision = va_arg(args, int);
			}
			if (precision < 0) precision = 0;
		}

		/* get the conversion qualifier */
		qualifier = 0;
		if (*format == 'h' || *format == 'l' || *format == 'L')
		{
			qualifier = *format;
			format++;
			if (qualifier == 'l' && *format == 'l')
			{
				qualifier = 'L';
				format++;
			}
		}

		/* format distribution */
		switch (*format)
		{
		case 'c': 
			// 预计需要多长空间
			// 调用sprintf尾插一个字符
			...
		case 's': 
			// 预计需要多长空间
			// 调用sprintf尾插一段字符串
			...
		case 'p': 
			// 预计需要多长空间
			// 调用sprintf尾插一个指针
			...
			
		case 'a':
		case 'A':
		case 'e':
		case 'E':
		case 'g':
		case 'G':
		case 'f':
			// 预计需要多长空间
			// 调用sprintf尾插一个浮点数
			...
		case 'o':
		case 'X':
		case 'x':
		case 'd':
		case 'i':
		case 'u':
			// 预计需要多长空间
			// 调用sprintf尾插一个整形数
			...
		case '%':
			// 推入 %
			...
		default:
			// 推入 %
			...
		}
	}

	/* copy tail string to str */
	if (begin)
	{
		len = format - begin;
		if (!str_alter_capacity(str, str->length + len)) goto FAIL_CAPACITY;
		while (len--) str->base[str->length++] = *begin++;
	}

	if (!str_alter_capacity(str, str->length)) goto FAIL_CAPACITY;
	str->base[str->length] = '\0';
	va_end(args);
	return str;

FAIL_CAPACITY:
	str->base[str->length] = '\0';
	va_end(args);
	return NULL;
}

源码比较长,大体的流程是,遍历format,在遇到%符号时进行具体的格式操作,以%为分隔符,将整段format分成一段段进行动态扩容、赋值。