使用C语言进行文本处理
字符集(Charset)、代码页(Code page)、编码方式(Encoding)这三个术语常常描述一件事情——如何把字符存储为二进制形式(字节)。
严格的讲,字符集是字符的集合,编码方式则用于确定某个字符集中的字符如何编码(为字节),但是对于ASCII、GB 2312、Big5、GBK、GB 18030之类的遗留方案来说一种字符集只有一种编码方式,这导致某些时候术语字符集、编码方式被混用。而Unicode是严格区分字符集、编码的,Unicode字符集有 UTF-8、UTF-16、UTF-32等多种编码方式。
微软称当前Locale对应的字符集为ANSI,和ASCII没有关系,对于简体中文的Windows操作系统,ANSI通常就是GBK。
进行文本处理时,编码方式常常是令人头疼的问题。相比起其它语言,C/C++的编码方式问题比较复杂。造成这种复杂性的原因包括:
- 对于编码方式缺乏统一规范,依赖于编译器、操作系统
- 构建出的二进制可执行文件丢失字符串(char*)编码方式信息
使用C/C++处理字符串时,要注意四个层面的字符集(表格出现的字符集可以理解为编码方式):
字符集 | 说明 | ||
源代码字符集 |
即作为编译输入的C/C++源代码文件使用的字符集。编译器必须能够正确的识别源文件的编码方式才能读取并处理之 对于GCC编译器,也称为输入字符集(Input charset)。源文件默认编码方式取决于编译器被调用时 LC_ALL 、 LC_* 、 LANG 等Locale相关环境变量,你也可以用编译器选项-finput-charset覆盖Locale 很多使用GCC或者其衍生工具条链(MinGW)的IDE,例如CLion、Eclipse,默认源代码使用UTF-8字符集 |
||
编译器内部字符集 | 对于GCC编译器,也称为源字符集(Source charset)。GCC内部使用UTF-8编码方式 | ||
执行字符集 |
即Execution charset,二进制可执行文件中的字符串、字符的编码方式。这个编码影响二进制文件的尺寸,例如多字节字符串 char* str = "你好" 使用GBK时 strlen() 返回值4,使用UTF-8时返回6 对于GCC编译器,有两个选项控制执行字符集: |
||
控制台字符集 |
程序使用 printf() 等函数打印字符到控制台上,用户才能看到。控制台也需要知道自己打印的内容是什么编码方式,否则会显示为乱码
对于字符串, printf() 仅仅是简单的读取可执行文件中的字节流,并打印到输出流中,这要求你保证执行字符集和控制台字符集一致或兼容,否则乱码
对于宽字符串, wprintf() 需要读取宽字符串的编码(UTF-16/UTF-32),并将其转化为Locale指定的编码方式,然后打印到输出流。那么C程序如何得到Locale呢? 在所有C程序main函数执行前,它会调用 setlocale(LC_ALL,"C"); ,这个所谓的C是所有C程序使用的最小化的Locale。这个C仅支持少量字符,肯定会导致中文乱码。要解决此问题,可以在程序开始处手工调用 setlocale(LC_CTYPE, "") ,该调用后程序使用系统默认Locale来处理C字符串(CTYPE),其编码方式和控制台一般是一致的,因而避免了乱码
控制台使用的编码方式取决于软件或者OS,例如简体中文Windows操作系统中cmd.exe使用的代码页936类似于字符集GBK。你可以使用命令调整代码页:
Windows全局的Locale设置在控制面板中进行 Linux下可以设置Locale相关环境变量,来改变Terminal使用的字符集 |
该头文件主要提供两类重要的函数:
- 字符类别测试
- 字符大小转换
该库提供的函数中都以int类型为参数,并返回一个int类型的值。实参类型应该隐式/显式转换为int类型
函数列表如下:
函数 | 说明 |
int isalnum(int c) | 判断是否是字母或数字 |
int isalpha(int c) | 判断是否是字母 |
int iscntrl(int c) | 判断是否是控制字符 |
int isdigit(int c) | 判断是否是数字 |
int isgraph(int c) | 判断是否是可显示字符 |
int islower(int c) | 判断是否是小写字母 |
int isupper(int c) | 判断是否是大写字母 |
int isprint(int c) | 判断是否是可显示字符 |
int ispunct(int c) | 判断是否是标点字符 |
int isspace(int c) | 判断是否是空白字符 |
int isxdigit(int c) | 判断字符是否为16进制 |
int tolower(int c) | 转换为小写字母 |
int toupper(int c) | 转换为大写字母 |
函数 | 说明 | ||||
memchr() |
在某一内存范围中查找一特定字符:
举例:
|
||||
memcmp() |
比较内存内容:
举例:
|
||||
memcpy() |
拷贝内存内容,两个内存区域必须不重叠:
|
||||
memmove() |
移动内存内容,两个内存区域可以重叠:
|
||||
memset() |
将一段内存空间填入某值,常用于内存清零:
|
||||
strcat() |
连接两字符串:
举例:
|
||||
strchr() |
在字符串中定位单个字符:
|
||||
strcmp() |
比较字符串:
|
||||
strcoll() |
使用当前Locale比较两个字符串,结果受 LC_COLLATE 影响:
|
||||
strcpy() |
拷贝字符串:
|
||||
strspn() |
依据一系列字节来搜索字符串:
举例:
|
||||
strerror() |
返回错误原因的描述字符串,举例:
|
||||
strlen() |
计算字符串长度,得到的是字节数量:
|
||||
strpbrk() |
查找字符串中第一个出现的指定字节:
|
||||
strstr() |
在一字符串中查找指定的子串:
|
||||
strtok() |
将字符串分割为0个或多个非空字符串:
举例:
注意,该函数修改了str的内容:它把分隔字符替换为\0 |
||||
strxfrm() |
拷贝字符串:
举例:
|
所谓宽字符,是指使用多个字节表示的字符。宽字符类型具有固定宽度,但是宽度取决于平台(编译器),这意味着使用宽字符会导致可移植性问题。宽字符在Linux系统中使用的不多。
在2011年的C和C++标准中固定宽度的字符类型 char16_t 、 char32_t 被引入,用来表示无歧义的16位、32位的Unicode转换格式(UTF)。
函数 | 说明 | ||||||
btowc() |
把单个字节转换为宽字符:
避免使用该函数,因为其无法处理带有状态的编码方式。使用 mbtowc() 或者线程安全的 mbrtowc() 代替之:
举例:
|
||||||
wctob() |
执行宽字符到单字节字符的转换,避免使用该函数,使用 wctomb() 代替之:
举例:
|
||||||
wprintf() |
|
||||||
vwprintf() |
|
||||||
wscanf() |
|
||||||
iswalnum() |
|
||||||
iswalpha() |
|
||||||
iswxdigit() |
|
||||||
iswcntrl() |
|
||||||
iswgraph() |
|
||||||
iswprint() |
|
||||||
iswspace() |
|
||||||
iswupper() iswlower() |
|
||||||
towupper() towlower() |
|
||||||
fgetwc() |
|
||||||
getwchar() |
|
||||||
fputwc() |
|
||||||
putwchar() |
|
||||||
fgetws() |
|
||||||
fputws() |
|
||||||
fwide() |
|
||||||
wcscat() |
|
||||||
wcsncat() |
|
||||||
wcschr() |
|
||||||
wcspbrk() |
|
||||||
wcscmp() |
|
||||||
wcscpy() |
|
||||||
wcsftime() |
|
||||||
wcslen() |
|
||||||
wcsstr() |
|
||||||
wcstok() |
|
||||||
wmemchr() |
|
||||||
wmemcmp() |
|
||||||
wmemcpy() |
|
||||||
wmemset() |
|
stdio.h中定义了一系列用于格式化输出的函数,包括:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 |
#include <stdio.h> /** * 将format指定的格式使用后续参数填充后,打印到标准输出 */ int printf( const char *format, ... ); /** * 将format指定的格式使用后续参数填充后,打印到流stream */ int fprintf( FILE *stream, const char *format, ... ); /** * 将format指定的格式使用后续参数填充后,打印到str指定的缓冲区 */ int sprintf( char *str, const char *format, ... ); //类似上面,但是最多打印size字节 int snprintf( char *str, size_t size, const char *format, ... ); #include <stdarg.h> // 类似上面,但是使用使用va_list而不是变长参数列表 int vprintf( const char *format, va_list ap ); int vfprintf( FILE *stream, const char *format, va_list ap ); int vsprintf( char *str, const char *format, va_list ap ); int vsnprintf( char *str, size_t size, const char *format, va_list ap ); |
此外,对应的还有格式化输入的函数:
1 2 3 4 5 6 7 8 9 10 11 |
#include <stdio.h> int scanf(const char *format, ...); int fscanf(FILE *stream, const char *format, ...); int sscanf(const char *str, const char *format, ...); #include <stdarg.h> int vscanf(const char *format, va_list ap); int vsscanf(const char *str, const char *format, va_list ap); int vfscanf(FILE *stream, const char *format, va_list ap); |
这些函数的宽字符版本在wchar.h中声明。
上述所有函数中的format遵守一致的规范。format由普通字符和若干转换规则(conversion specifications)组成。后者导致列表中下一个参数被转换并打印。转换规则的语法为:
1 |
%[#0- +'][宽度][.精度][长度限定符][转换符] |
其中:
标记 | 说明 |
% | 表示转换规则的开始 |
# 0 - + ' |
这些字符紧随着%,可以出现一个或者多个: |
宽度 | 表示字段的最小宽度,如果目标参数长度不够,会自动补白 |
.精度 | 精度:字符串的最大的字符数;浮点数小数部分的位数;整数的最小数字个数 |
长度限定符 | hh 如果后续整数转换符则输出signed/unsigned char,后续n转换符则输出signed char* h 如果后续整数转换符则输出signed/unsigned short,后续n转换符则输出signed short* l 如果后续整数转换符则输出signed/unsigned long,后续n转换符则输出signed long* 后续c转换符则输出wint_t,后续s则输出wchar_t* ll 如果后续整数转换符则输出signed/unsigned long long,后续n转换符则输出signed long long* L 如果后续a, A, e, E, f, F, g或G则输出long double格式 |
可用的转换符如下表:
转换符 | 说明 |
d,i | int,格式化为十进制整数 可以附加前缀l,用于格式化long类型,例如li、ld |
o | int,格式化为八进制整数(无符号),默认前缀0省略 |
x,X | int,格式化为十六进制整数(无符号),默认前缀0x省略。X表示字母大写打印 |
u | unsigned int,打印为无符号整数 可以附加前缀l,用于格式化long类型,例如lu |
c | int,打印单个字符 |
s | char*,打印字符串,直到遇到\0,或者到达精度限制 |
f | double,打印为:[-]m.dddddd,小数部分默认6 |
e,E | double,打印为:[-]m.dddddde+/-xx或者[-]m.ddddddE+/-xx,其中d为精度 |
g,G | double,如果指数小于-4或者大于等于精度,使用%e、%E输出;否则使用%f输出,尾部的0和小数点不打印 |
p | void*,打印指针 |
% | 原样打印字符% |
C标准库的功能非常有限,并且比较难用或存在性能问题,很多情况下需要使用第三方库。
SDS是一个简单的动态字符串库,原本是Redis内部的组件。它在内部维护一个基于堆的缓冲区,避免受到C标准库的限制,SDS兼容普通的C字符串函数。
典型的C动态字符串库是基于下面的结构实现的:
1 2 3 4 5 |
struct DynamicString { char *buf; size_t len; //其它字段 }; |
SDS没有遵循这一模式,它由头部+二进制安全的C风格字符串+NULL字符构成。SDS的结构导致它具有一些缺点和优点。
SDS的很多函数会可能返回一个新字符串,而不是修改原有字符串,所以很多SDS API必须这样使用:
1 |
s = sdscat(s,"more data"); |
如果忘记把返回值赋值给原先的变量可能导致BUG。
进一步讲,如果你在多个地方引用同一个SDS,调用上述函数后必须赋值所有引用。
你可以直接对SDS变量使用C标准库函数,例如:
1 2 3 4 5 |
printf("%s\n", sds); //其它库一般是这样: printf("%s\n", str->buf); //或者这样: printf("%s\n", getStringPointer(str)); |
索引方式访问单个字符也是支持的:
1 |
printf("%c %c\n", sds[0], sds[1]); |
函数 | 说明 | ||||
sdsnew() sdsnewlen() sdsempty() sdsdup() |
这些函数用于创建新的动态字符串:
|
||||
sdslen() |
获得动态字符串的长度:
size_t sdslen(const sds s);
|
||||
sdsfree() | 销毁动态字符串,即使空串也必须销毁,否则内存泄漏:
|
||||
sdscat() sdscatlen() sdscatsds() |
连接字符串:
举例:
|
||||
sdsgrowzero() | 确保动态字符串的长度:
|
||||
sdscatprintf() | 格式化字符串并连接到动态字符串:
举例: sdscatprintf(sdsempty(), "%s %s", "Hello", "Alex"); |
||||
sdsfromlonglong() | 从数字创建字符串:
举例:
|
||||
sdstrim() | 修剪字符串:
举例:
|
||||
sdsrange() | 修剪为子串:
|
||||
sdscpy() sdscpylen() |
strcpy() 是C标准库中最危险和恶名的操作之一。SDS提供的相应的操作则可用于性能关键的领域:
|
||||
scscatrepr() | 连接字符串,并将其中的不可打印字符使用转义字符的形式显示出来:
|
||||
sdssplitlen() sdsfreesplitres() |
标记化(Tokenization):将一个大字符串分隔(split)为多个小字符串:
举例:
|
||||
sdsjoin() sdsjoinsds() |
使用分隔符连接一组字符串:
举例:
|
||||
sdsRemoveFreeSpace() | 收缩可变字符串,移除空闲的空间,在内存受限环境下可以使用:
|
||||
sdsAllocSize() | 获得一个可变字符串实际分配的空间:
|
||||
sdsupdatelen() | 改变逻辑长度,反映出C字符串的长度:
|
上面这些函数,但凡返回sds的,在内存溢出的情况下一律会返回空指针。
SDS头部由下面的数据结构表示:
1 2 3 4 5 |
struct sdshdr { int len; // 保存动态字符串的长度 int free; // 保存缓冲区空闲字节数,这些空闲字节可以容纳更多的字符 char buf[]; // 未声明长度的数组,因此它实际上指向free后面的那个字节,这很重要 }; |
结构中的buf字段是一个flexible array member,它是C99引入的特性——位于结构体最后的无长度的数组。该数组的指针指向紧跟着结构体的内存,下面的示例代码可以验证这一点:
1 2 3 4 5 6 7 8 9 10 11 12 13 |
typedef struct { int len; char buf[]; } head; int main() { void *mem = malloc( 1024 ); head *h = mem; char *str = mem + sizeof( head ); str[0] = 'A'; str[1] = 'B'; printf( h->buf ); // 打印AB return 0; } |
要创建一个SDS,只需要在堆上分配不小于sdshdr长度+字符串长度的内存即可。但是为了避免每次操控都导致新的内存分配,SDS总是预分配一些额外的内存。SDS的预分配算法是:当每次进行内存分配时,实际分配的内存是最小需求量的2倍。内存分配的最大量由宏 SDS_MAX_PREALLOC 控制。
如果你需要在多个数据结构中共享SDS动态字符串,应当将SDS封装在具有引用计数的结构体中,避免编程错误导致的内存泄漏:
1 2 3 4 |
struct ds { int refcount; sds str; } |
你应当提供增加、减少引用计数的函数:
- 每当其它数据结构引用ds或者将ds赋值给变量时,都应当增加引用计数
- 每当引用移除时,减少计数,计数为0时,自动销毁SDS字符串
在Redis中,为了增强性能使用了SDS提供的一些低级API。使用 sdsIncrLen() 和 sdsMakeRoomFor() 可以将来自内核的字节直接连接到SDS尾部,而不需要中介的缓冲区:
1 2 3 4 |
oldlen = sdslen(s); s = sdsMakeRoomFor(s, BUFFER_SIZE); nread = read(fd, s+oldlen, BUFFER_SIZE); // 系统调用 sdsIncrLen(s, nread); |
Leave a Reply