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

875 lines
26 KiB
Markdown
Raw Permalink Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

## 介绍
字符串实际是一串字符的集合在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小于string1为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真正存储字符串的基地址lengthstr的大小也就是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`分成一段段进行动态扩容、赋值。