## 介绍 字符串实际是一串字符的集合,在C语言中,字符串是为char类型的数组,在结尾加上结束符`\0`则为字符串,没有结束符则为字符数组。 在C语言中,没有专门的字符串变量,而varch中的str则专门针对数组字符串进行了封装,封装成不需要关注底层存储结构的字符串变量。str实际还是通过连续地址的空间进行存储字符集,但是这些存储的工作不需要额外去关注,str提供的方法已经实现存储的工作,而且str也提供符合字符串操作的方法,方便字符串的灵活操作。方便说明,后面的传统的C语言字符串称为数组字符串,varch提供的成为str字符串。 ## 接口 ### 创建和删除str字符串对象 ```c 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对象。创建方法和删除应该成对使用,创建出来在结束使用应该删除掉。 ```c void test(void) { str_t ss = str("Hello str!"); // 用数组字符串进行赋值构造 str_t copy = str(ss); // 用str字符串进行拷贝构造 _str(ss); // 删除str _str(copy); // 删除str } ``` ### str字符串的数组字符串 ```c #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()`则是简短版的。 ```c 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赋值 ```c str_t str_assign(str_t str, void *string); ``` str抽象字符串为一个类了,给str进行赋值操作,类似于=号的赋值操作。如同前面所说的`void *string`可以支持数组字符串和str字符串。 ```c 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拼接 ```c #define str_append(str, ...) ``` str除了有类似`=`号的赋值操作,还有类似`+=`号的拼接操作。 形参后面不定参数,**可以拼接至少一个任意数量的字符串**。同样,可以是数组字符串也可以是str字符串。 `str_append`方法的原型是`str_t str_append_series(str_t str, ...);`,原型函数不定参数需要接`NULL`结尾,不建议使用原型方法。 ```c 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插入 ```c str_t str_insert(str_t str, int pos, void *string); ``` 在str中插入一个字符串,并返回自己。`pos`是插入的位置,`string`是插入的源字符串。 ```c 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移除 ```c str_t str_erase(str_t str, int pos, int len); ``` 从str中移除一段字符,并返回自己。`pos`是移除的位置,`len`是移除的长度。 ```c 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尾部推入和弹出 ```c int str_push_back(str_t str, char c); char str_pop_back(str_t str); ``` 这两个方法分别从str字符串尾部推入和弹出一个字符,推入方法的返回值1成功0失败,弹出方法返回弹出的字符。 `str`存储结构和`vector`类似,在尾部操作具有比较高的效率。 ```c 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字符串比较 ```c int str_compare(str_t str, void *string); ``` 比较str与另外一个字符串,0为相等,-1为str小于string,1为str大于string。 ```c 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创建子串 ```c str_t str_substr(str_t str, int pos, int len); ``` 这个方法是从str中创建一个新的子串,**切记,在结束使用后,调用`_str(str)`方法来删除**。。 `pos`传入起始位置,`len`传入创建的长度。 ```c 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字符串长度 ```c int str_length(str_t str); ``` str标准的获取长度的方法是这个,当然也可以用`strlen`方法去量,但是对于`str`对象没必要,因为这个标准的方法是不用耗费时间去量的,返回记录好了的长度。 ```c void test(void) { str_t ss = str("0123456789"); printf("length: %d\r\n", str_length(ss)); _str(ss); } ``` 结果: ``` length: 10 ``` ### str字符读写 ```c #define str_at(str, i) ``` str提供了针对其中的字符进行读写操作,当作数组的元素使用即可。 但切记,不要把字符修改成`\0`了。 ```c 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字符串查找 ```c int str_find(str_t str, void *string, int pos); int str_rfind(str_t str, void *string, int pos); ``` 这两个方法分别从正向和反向查找字符子串,`string`传入字符串,`pos`传入开始查找的起始索引,返回则是返回子串所在的位置索引,没找到就返回`str_npos`。 ```c 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查找匹配字符集 ```c 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_find`、`str_rfind`方法不一样,find和rfind需要匹配全部字符,而这四个方法只要匹配上字符集的任意之一就匹配成功。 同样,`string`传入字符串(匹配字符集),`pos`传入开始查找的起始索引,返回则是返回子串所在的位置索引,没找到就返回`str_npos`。这四个函数分别找到**“第一个包含”**、**“第一个不包含”**、**“最后一个包含”**、**“最后一个不包含”** 例子:实际应用获取一个文件路径的文件和所在盘符 ```c 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字符串翻转 ```c str_t str_reverse(str_t str, int begin, int end); ``` 这个方法将str指定区间的字符串整体翻转。`begin`为区间起始位置,`end`为区间结束位置。 ```c 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字符替换 ```c str_t str_replace(str_t str, int pos, int len, void *string); ``` 这个方法把指定位置的字符按长度替换成指定的字符串。 ```c 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字符串交换 ```c void str_swap(str_t str, str_t swap); ``` 将两个字符串进行交换。 ```c 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字符串复制到数组 ```c int str_copy(str_t str, int pos, int len, char *buf); ``` 将str字符串指定位置的字符复制到指定的数组。其中,`pos`复制的起始位置。`len`复制的最大长度。`buf`就是接收复制数据的数组。函数返回实际复制的长度。 ```c 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字符串格式化 ```c str_t str_format(str_t str, char *format, ...); ``` str字符串格式化操作,套用`sprintf`函数进行了重写,与其用法基本一致。**增加了%s对str类型的支持**,`str_format`对格式化进行分段的动态扩容,在使用上不需要像`sprintf`那样关注buf长度是否超出的问题,使用起来更加的简便。 ```c 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 输入一个字符串,判断是否为回文字符串,即从左到右和从右到左完全一致。 ```c 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 输入一个字符串,删除字符串中重复出现的字符。 ```c 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类型声明 ```c typedef struct STR *str_t; ``` 使用时候,只是用`str_t`即可。 ```c /* 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(真正存储字符串的基地址),length(str的大小,也就是str的长度),capacity(分配的空间的容量,用来实际存储字符串,一般情况下不用去关注容量的事情)。 ### 区分数组字符串和str字符串 在`STR`结构体当中,`ident`成员就是用于区分数组字符串和str字符串的标志,那是怎么区分的呢? 看下区分的代码: ```c /* 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()`到底是什么?为什么其可以区分。 ```c #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 10xxxxxx`,4字节`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`赋值函数为例: ```c 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`函数。 ```c 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获取实际存储数据 ```c 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_at`、`str_c_str`、`_S`都是基于`str_data`实现的,`str_data`实现的内容其实也很简单,就是将结构体中`base`指定索引的地址进行返回。在其中加入了`error`机制,也就是当传入的添加不满足的时候,会返回`error`的地址,`error`为模块定义的一个静态变量。防止返回NULL,对空地址进行操作时候导致的程序崩溃。 ### str字符串替换原理 ```c 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; } ``` 不管是插入还是移除的方法,都是基于替换的方法进行实现的。 ```c 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拼接原理 ```c #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格式化源码 ```c 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`分成一段段进行动态扩容、赋值。