Menu

  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay
  • Home
  • Work
    • Cloud
      • Virtualization
      • IaaS
      • PaaS
    • Java
    • Go
    • C
    • C++
    • JavaScript
    • PHP
    • Python
    • Architecture
    • Others
      • Assembly
      • Ruby
      • Perl
      • Lua
      • Rust
      • XML
      • Network
      • IoT
      • GIS
      • Algorithm
      • AI
      • Math
      • RE
      • Graphic
    • OS
      • Linux
      • Windows
      • Mac OS X
    • BigData
    • Database
      • MySQL
      • Oracle
    • Mobile
      • Android
      • IOS
    • Web
      • HTML
      • CSS
  • Life
    • Cooking
    • Travel
    • Gardening
  • Gallery
  • Video
  • Music
  • Essay

使用C语言进行文本处理

14
Jun
2011

使用C语言进行文本处理

By Alex
/ in C
/ tags 文本处理
0 Comments
字符集问题

字符集(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++的编码方式问题比较复杂。造成这种复杂性的原因包括:

  1. 对于编码方式缺乏统一规范,依赖于编译器、操作系统
  2. 构建出的二进制可执行文件丢失字符串(char*)编码方式信息

使用C/C++处理字符串时,要注意四个层面的字符集(表格出现的字符集可以理解为编码方式):

字符集 说明
源代码字符集

即作为编译输入的C/C++源代码文件使用的字符集。编译器必须能够正确的识别源文件的编码方式才能读取并处理之

对于GCC编译器,也称为输入字符集(Input charset)。源文件默认编码方式取决于编译器被调用时 LC_ALL 、 LC_* 、 LANG 等Locale相关环境变量,你也可以用编译器选项-finput-charset覆盖Locale

很多使用GCC或者其衍生工具条链(MinGW)的IDE,例如CLion、Eclipse,默认源代码使用UTF-8字符集
Visual Studio在简体中文的Windows下使用GBK字符集

编译器内部字符集 对于GCC编译器,也称为源字符集(Source charset)。GCC内部使用UTF-8编码方式
执行字符集

即Execution charset,二进制可执行文件中的字符串、字符的编码方式。这个编码影响二进制文件的尺寸,例如多字节字符串 char* str = "你好" 使用GBK时 strlen() 返回值4,使用UTF-8时返回6

对于GCC编译器,有两个选项控制执行字符集:
-fexec-charset,决定字符串(包括多字节字符)、字符的编码方式,默认UTF-8
-fwide-exec-charset,决定宽字符串、宽字符的编码方式,默认UTF-16或者UTF-32,一般和 wchar_t 宽度一致

控制台字符集

程序使用 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。你可以使用命令调整代码页:

MS DOS
1
2
3
4
5
6
rem 显示当前代码页(和字符集是类似的概念)
CHCP
rem 936是简体中文代码页,最初和GB2312一样,后来包含大部分的GBK字符
Active code page: 936
rem 切换为UTF-8代码页
CHCP 65001

Windows全局的Locale设置在控制面板中进行

Linux下可以设置Locale相关环境变量,来改变Terminal使用的字符集

C标准库
字符测试ctype.h

该头文件主要提供两类重要的函数:

  1. 字符类别测试
  2. 字符大小转换

该库提供的函数中都以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)  转换为大写字母
字符串函数string.h
函数 说明
memchr()

在某一内存范围中查找一特定字符:

C
1
2
3
4
5
6
7
8
9
/**
* 扫描s所指内存的前n个字节,来寻找c出现的第一个位置
* c和s所指内存的字节,均被解释为unsigned char
* 返回指向匹配字节的指针,如果找不到返回NULL,rawmemchr()在找不到的情况下返回值未定义
*/
void *memchr(const void *s, int c, size_t n);  // 正向搜索
// 下面两个是GNU扩展
void *memrchr(const void *s, int c, size_t n); // 反向搜索
void *rawmemchr(const void *s, int c); // 正向搜索,不限制字节数

举例:

C
1
2
3
const char *mem = "0123456789";
char *p3 = (char *) memchr( mem, '3', strlen( mem ));
assert( p3 - mem == 3 );
memcmp()

比较内存内容:

C
1
2
3
4
5
/**
* 比较s1、s2两块内存区域的前n个字节
* 当s1小于、等于、大于s2时分别返回负数、0、正数
*/
int memcmp(const void *s1, const void *s2, size_t n);

举例:

C
1
2
3
4
char *s1 = "abcdew";
char *s2 = "abcdez";
assert( memcmp( s1, s2, strlen( s1 ) - 1 ) == 0 );
assert( memcmp( s1, s2, strlen( s1 )) < 0 );
memcpy() 

拷贝内存内容,两个内存区域必须不重叠:

C
1
2
3
4
5
/**
* 从src拷贝n个字节到dest
* 返回指向dest的指针
*/
void *memcpy( void *dest, const void *src, size_t n );
memmove() 

移动内存内容,两个内存区域可以重叠:

C
1
2
3
4
/**
* 类似于memcpy,效果上相当于把src先拷贝到临时内存中,然后覆盖到dest
*/
void *memmove(void *dest, const void *src, size_t n);
memset() 

将一段内存空间填入某值,常用于内存清零:

C
1
void *memset(void *s, int c, size_t n); 
strcat() 

连接两字符串:

C
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 将src附加到dest后面,覆盖dest结尾的\0,并在连接后再次添加一个\0
* 两个字符串不得重叠,并且dest必须由足够的空间来存放结果,如果dest空间不足则程序的行为无法预测
*/
char *strcat( char *dest, const char *src );
 
/**
* 连接两字符串,与strcat类似,但是:
*     最多使用src的n个字节
*     如果src大于n字节,则它不需要以\0结束
*/
char *strncat( char *dest, const char *src, size_t n );

举例:

C
1
2
3
4
char *dest = malloc( 100 );
strcat( dest, "Hello" );
strcat( dest, " World" );
assert( strcmp( dest, "Hello World" ) == 0 ); 
strchr() 

在字符串中定位单个字符:

C
1
2
3
4
5
6
// 返回s中第一个c的指针,如果找不到返回NULL
char *strchr(const char *s, int c);
// 返回s中最后一个c的指针
char *strrchr(const char *s, int c);
// 与strchr()类似,但是在找不到的时候,返回s结尾的\n的指针而不是NULL
char *strchrnul(const char *s, int c);
strcmp() 

比较字符串:

C
1
2
3
4
//比较s1、s2,在s1小于、等于、大于s2时分别返回负数、0、正数
int strcmp( const char *s1, const char *s2 );
//比较s1、s2的前n个字节
int strncmp( const char *s1, const char *s2, size_t n );
strcoll() 

使用当前Locale比较两个字符串,结果受 LC_COLLATE 影响:

C
1
int strcoll(const char *s1, const char *s2);
strcpy() 

拷贝字符串:

C
1
2
3
4
5
6
7
/**
* 拷贝src,包含结尾的\0,到dest。dest必须足够大,两个字符串不得重叠
*
*/
char *strcpy( char *dest, const char *src );
// 类似,但是最多拷贝n字节,注意,该函数可能导致结尾的\0丢失
char *strncpy( char *dest, const char *src, size_t n );
strspn() 

依据一系列字节来搜索字符串:

C
1
2
3
4
5
6
7
8
9
10
/**
* 搜索s,直到出现不在accept中的字节
* 返回从头开始,一直处于accept中的字符的个数
*/
size_t strspn( const char *s, const char *accept );
/**
* 搜索s,知道出现在reject中的字节
* 返回从头开始,第一个在reject中的字节之前的字节总数
*/
size_t strcspn( const char *s, const char *reject );

 举例:

C
1
assert( strspn( "54213zyx", "1234567890" ) == 5 );
strerror() 

返回错误原因的描述字符串,举例:

C
1
printf( strerror( 2 ));//No such file or directory
strlen() 

计算字符串长度,得到的是字节数量:

C
1
2
assert( strlen( "nh" ) == 2 );
assert( strlen( "你好" ) == 6 ); 
strpbrk() 

查找字符串中第一个出现的指定字节:

C
1
2
3
4
5
/**
* 搜索s,直到出现accept中的任何一个字节
* 返回第一个accept中的字节,如果找不到返回NULL
*/
char *strpbrk( const char *s, const char *accept );
strstr() 

在一字符串中查找指定的子串:

C
1
2
3
4
5
6
7
/**
* 返回haystack中第一次出现needle的、needle的起始字节的指针,结尾的\0不参与比较
* 如果子串找不到返回NULL
*/
char *strstr(const char *haystack, const char *needle);
// GNU扩展,与上面类似,但是不区分大小写
char *strcasestr(const char *haystack, const char *needle);
strtok() 

将字符串分割为0个或多个非空字符串:

C
1
2
3
4
5
6
7
8
9
10
11
/**
* 第一次调用时,传递待分割的字符串到str,后续分割同一字符串的操作,必须传递NULL给str
* delim是一系列作为分隔符的字节,后续调用可以改变delim
* 每次调用的返回值是指向分割得到的子串的指针,没有更多的记号时返回NULL
*/
char *strtok( char *str, const char *delim );
/**
* strtok()的可重入版本
* saveptr是供函数内部使用的一个指针,保存分隔上下文
*/
char *strtok_r( char *str, const char *delim, char **saveptr );

 举例:

C
1
2
3
4
5
6
7
char str[] = "123:456,789....0";
char *token;
char *ctx = str;
while ( token = strtok_r( ctx, ":,.", &ctx )) {
    printf( "%s|", token );
}
//打印123|456|789|0| 可以看到....中间的不作为子串

 注意,该函数修改了str的内容:它把分隔字符替换为\0

strxfrm() 

拷贝字符串:

C
1
2
3
4
5
/**
* 拷贝src的n个字节到数组dest中,返回拷贝后的字符串的长度
* 如果返回值大于等于n则dest数组的内容是不确定的
*/
size_t strxfrm( char *dest, const char *src, size_t n );

 举例:

C
1
2
3
4
5
6
7
char *source = "1234567890";
char des[100];
size_t len = strxfrm( des, source, 50 );
assert( len == 10 && strcmp( des, source ) == 0 );
memset( des, 0, 100 );
len = strxfrm( des, source, 5 );
assert( len == 10 && strcmp( des, "12345" ) == 0 );
宽字符串函数wchar.h

所谓宽字符,是指使用多个字节表示的字符。宽字符类型具有固定宽度,但是宽度取决于平台(编译器),这意味着使用宽字符会导致可移植性问题。宽字符在Linux系统中使用的不多。

在2011年的C和C++标准中固定宽度的字符类型 char16_t 、 char32_t 被引入,用来表示无歧义的16位、32位的Unicode转换格式(UTF)。

函数 说明
btowc() 

把单个字节转换为宽字符:

C
1
2
3
4
5
6
7
/**
* 执行单字节字符到宽字符的转换
* @param c 单字节字符
* @return 转换c所代表的字符的宽字符表示
*         如果c为EOF或者不是有效单字节字符,返回WEOF
*/
wint_t btowc( int c ); 

避免使用该函数,因为其无法处理带有状态的编码方式。使用 mbtowc() 或者线程安全的 mbrtowc() 代替之:

C
1
2
3
4
5
6
7
8
/**
* 从多字节序列s中抽取出一个宽字符,该函数最多检查s的n个字节,并把转换后得到的宽
* 字符存放在*pwc中,返回从s中消费掉的字节数。该函数在内部维护一个偏移状态(Shift state)导致其线程不安全
*
* 该函数需要知道s的编码方式,这是由当前Locale的LC_CTYPE目录决定的,因此调用该函
* 数前你可能需要调用setlocale来设置多字节使用的编码方式
*/
int mbtowc( wchar_t *pwc, const char *s, size_t n );

举例: 

C
1
2
3
4
5
6
7
8
9
setlocale( LC_ALL, "" );
char *str = "你好,世界";
wchar_t wc = 0;
// MB_CUR_MAX 当前Locale下多字节字符占据的最大字节数
int len = mblen( str, MB_CUR_MAX);
str += mbtowc( &wc, str, len * strlen( str ));
wprintf( L"%lc \n", wc ); //你
str += mbtowc( &wc, str, len * strlen( str ));
wprintf( L"%lc \n", wc ); //好
wctob() 

执行宽字符到单字节字符的转换,避免使用该函数,使用 wctomb() 代替之:

C
1
2
3
4
5
6
7
/**
* 转换宽字符wc为多字节序列,存放到s中,程序员必须保证s至少由MB_CUR_MAX字节
* 如果s非NULL,返回写入到s中的字节数
* 如果s为NULL,该函数重置内部的Shift state为初始状态,并返回
* 零(如果多字节编码方式是无状态的)或非零
*/
int wctomb(char *s, wchar_t wc);

举例:

C
1
2
3
4
5
6
setlocale( LC_ALL, "" );
char buf[64];
char *str = buf;
str += wctomb( str, L'你' );
str += wctomb( str, L'好' );
printf( "%s", buf ); //你好
wprintf() 
C
1
2
3
4
//下面三个函数和对应单字节字符的版本功能类似
int fwprintf( FILE *stream, const wchar_t *format, ... );
int wprintf( const wchar_t *format, ... );
int swprintf( wchar_t *s, size_t n, const wchar_t *format, ... ); 
vwprintf() 
C
1
2
3
4
//下面三个函数与上面类似,但是使用列表而不是变长参数
int vwprintf( const wchar_t *format, va_list arg );
int vfwprintf( FILE *stream, const wchar_t *format, va_list arg );
int vswprintf( wchar_t *s, size_t n, const wchar_t *format, va_list arg );
wscanf() 
C
1
2
3
4
//下面三个函数和对应单字节字符的版本功能类似
int fwscanf( FILE *stream, const wchar_t *format, ... );
int wscanf( const wchar_t *format, ... );
int swscanf( const wchar_t *s, const wchar_t *format, ... );
iswalnum() 
C
1
2
//测试在当前Locale下,字符是否字母或者数字
int iswalnum( wint_t wc );
iswalpha() 
C
1
2
//测试在当前Locale下,字符是否字母
int iswalpha( wint_t wc ); 
iswxdigit() 
C
1
2
//测试在当前Locale下,字符是否属于十六进制字符
int iswxdigit( wint_t wc ); 
iswcntrl() 
C
1
2
//测试在当前Locale下,字符是否为控制字符
int iswcntrl( wint_t wc );
iswgraph()  
C
1
2
//测试在当前Locale下,字符是否为可见
int iswgraph( wint_t wc );
iswprint() 
C
1
2
//测试在当前Locale下,字符是否为可打印字符
int iswprint( wint_t wc ); 
iswspace() 
C
1
2
//测试在当前Locale下,字符是否为空白字符
int iswspace( wint_t wc );
iswupper() 
iswlower()
C
1
2
3
//是否大小写判断
int iswupper( wint_t wc );
int iswlower( wint_t wc );
towupper() 
towlower() 
C
1
2
3
//转换为大小写
wint_t towupper( wint_t wc );
wint_t towlower( wint_t wc );
  fgetwc() 
C
1
2
3
4
5
//从文件流中读取下一个宽字符
wint_t fgetwc( FILE *stream );
 
//与上面类似,但是作为宏实现
wint_t getwc(FILE *stream);
getwchar() 
C
1
2
//从标准输入读取一个宽字符
wint_t getwchar(void);
fputwc() 
C
1
2
3
4
5
//写入一个宽字符到文件流
wint_t fputwc( wchar_t wc, FILE *stream );
 
//与上面类似,但是作为宏实现
wint_t putwc(wchar_t wc, FILE *stream);
putwchar() 
C
1
2
//写入一个宽字符到标准输出
wint_t putwchar( wchar_t wc );
fgetws() 
C
1
2
//从文件流中读取宽字符串
wchar_t *fgetws( wchar_t *ws, int n, FILE *stream );
fputws() 
C
1
2
//写入宽字符串到文件流
int fputws( const wchar_t *ws, FILE *stream ); 
fwide()  
C
1
2
3
4
5
6
/**
* 修改流为面向字节/面向宽字符
* @param stream 目标流
* @param mode 1尝试修改为面向宽字符;-1尝试修改为面向字节;0不变
*/
int fwide( FILE *stream, int mode );
wcscat() 
C
1
2
//连接两个宽字符串
wchar_t *wcscat( wchar_t *ws1, const wchar_t *ws2 );
wcsncat() 
C
1
2
//连接ws2的最多n个字符到ws1,不包括\0字符
wchar_t *wcsncat( wchar_t *ws1, const wchar_t *ws2, size_t n );
wcschr() 
C
1
2
3
4
5
6
7
8
9
10
/**
* 搜索字符串,返回字符在串中第一次出现
* @param ws 字符串
* @param wc 搜索的字符
* @return 第一次出现的字符的指针,或者NULL
*/
wchar_t *wcschr( const wchar_t *ws, wchar_t wc );
 
//返回字符在串中的最后一次出现
wchar_t *wcsrchr( const wchar_t *ws, wchar_t wc );
wcspbrk() 
C
1
2
//得到第一个出现在ws1中的任何ws2中的字符
wchar_t *wcspbrk( const wchar_t *ws1, const wchar_t *ws2 );
wcscmp() 
C
1
2
3
4
5
/**
* 比较两个宽字符串,如果ws1大于ws2,返回正数;等于则返回0;小于返回负数
* 非零返回值说明了两者的差异
*/
int wcscmp( const wchar_t *ws1, const wchar_t *ws2 );
wcscpy() 
C
1
2
3
4
5
//将字符串ws2拷贝到ws1,如果两个串存在字符重叠,则行为未定义
wchar_t *wcscpy( wchar_t *ws1, const wchar_t *ws2 );
 
//将字符串ws2的最多n个字符拷贝到ws1,如果出现字符重叠,则行为未定义
wchar_t *wcsncpy( wchar_t *ws1, const wchar_t *ws2, size_t n );
wcsftime() 
C
1
2
//将日期时间转换为宽字符串
size_t wcsftime( wchar_t *wcs, size_t maxsize, const wchar_t *format, const struct tm *timptr );
wcslen() 
C
1
2
//得到宽字符串的长度,不包括结尾的0字符,结果是字符的个数,而不是字节数
size_t wcslen( const wchar_t *ws );
wcsstr() 
C
1
2
3
4
5
//搜索子串的第一次出现,如果找不到返回NULL,如果ws2是空串,那么直接返回ws1
wchar_t *wcsstr( const wchar_t *ws1, const wchar_t *ws2 );
 
//类似上面的宏版本
wchar_t *wcswcs( const wchar_t *ws1, const wchar_t *ws2 );
wcstok() 
C
1
2
//根据分隔符,分隔宽字符串
wchar_t *wcstok( wchar_t *ws1, const wchar_t *ws2, wchar_t **ptr );
wmemchr() 
C
1
2
//在长度为n字符的ws中寻找第一次出现的wc
wchar_t *wmemchr( const wchar_t *ws, wchar_t wc, size_t n );
wmemcmp() 
C
1
2
//比较两个字符串的前n个字符
int wmemcmp( const wchar_t *ws1, const wchar_t *ws2, size_t n );
wmemcpy() 
C
1
2
//把ws2的前n个字符拷贝到ws1,返回ws1
wchar_t *wmemcpy( wchar_t *ws1, const wchar_t *ws2, size_t n ); 
wmemset() 
C
1
2
//设置ws的前n个字符为wc
wchar_t *wmemset( wchar_t *ws, wchar_t wc, size_t n ); 
格式化输出

stdio.h中定义了一系列用于格式化输出的函数,包括:

C
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 );

此外,对应的还有格式化输入的函数:

C
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 - + '

这些字符紧随着%,可以出现一个或者多个:
# 表示输出为备选格式:对于o转换符需要输出为0开头,对于x或X需要输出为0x/0X开头。对于a, A, e, E, f, F, g会总是输出小数点
0 基于0来补白,对于d, i, o, u, x, X, a, A, e, E, f, F, g,G转换后的值在左边使用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

SDS是一个简单的动态字符串库,原本是Redis内部的组件。它在内部维护一个基于堆的缓冲区,避免受到C标准库的限制,SDS兼容普通的C字符串函数。

典型的C动态字符串库是基于下面的结构实现的:

C
1
2
3
4
5
struct DynamicString {
    char *buf;
    size_t len;
    //其它字段
};

SDS没有遵循这一模式,它由头部+二进制安全的C风格字符串+NULL字符构成。SDS的结构导致它具有一些缺点和优点。

SDS的缺点

SDS的很多函数会可能返回一个新字符串,而不是修改原有字符串,所以很多SDS API必须这样使用:

C
1
s = sdscat(s,"more data");

如果忘记把返回值赋值给原先的变量可能导致BUG。

进一步讲,如果你在多个地方引用同一个SDS,调用上述函数后必须赋值所有引用。

SDS的优势

你可以直接对SDS变量使用C标准库函数,例如:

C
1
2
3
4
5
printf("%s\n", sds);
//其它库一般是这样:
printf("%s\n", str->buf);
//或者这样:
printf("%s\n", getStringPointer(str));

索引方式访问单个字符也是支持的:

C
1
printf("%c %c\n", sds[0], sds[1]);
SDS的API
函数 说明
sdsnew() 
sdsnewlen() 
sdsempty() 
sdsdup() 
这些函数用于创建新的动态字符串:
C
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
/**
* 创建一个动态字符串,初始值从init的前initlen中取得
*/
sds sdsnewlen( const void *init, size_t initlen );
/**
* 创建一个动态字符串,初始值由init指定,init必须由\0结束
*/
sds sdsnew( const char *init );
/**
* 创建一个空白的动态字符串
*/
sds sdsempty( void );
/**
* 从s复制一个新的动态字符串
*/
sds sdsdup( const sds s );
sdslen() 

获得动态字符串的长度: size_t sdslen(const sds s);
类似于标准库的strlen()函数,但是:

  1. 该函数消耗时间是固定的,原因是SDS的长度存放在头部字段中
  2. 与其它SDS函数一样,该函数也是二进制安全的。其返回的长度是字符串的真实长度,即是中间包含\0字符:
    C
    1
    sdslen(sdsnewlen("A\0\0B",4)) == 4 
sdsfree()  销毁动态字符串,即使空串也必须销毁,否则内存泄漏:
C
1
void sdsfree(sds s);
sdscat() 
sdscatlen() 
sdscatsds() 
连接字符串:
C
1
2
3
4
5
6
7
8
9
10
11
12
/**
* 将缓冲区t的前len个字节连接到动态字符串中,返回(可能是)新的动态字符串
*/
sds sdscatlen( sds s, const void *t, size_t len );
/**
* 将字符串连接到s,返回(可能是)新的动态字符串
*/
sds sdscat( sds s, const char *t );
/**
* 将动态字符串t连接到s,返回(可能是)新的动态字符串
*/
sds sdscatsds(sds s, const sds t);

举例: 

C
1
2
3
4
sds hello = sdsnew( "Hello " );
sds world = sdsnew( "World" );
hello = sdscatsds( hello, world );
sdsfree( world );
sdsgrowzero()  确保动态字符串的长度:
C
1
2
// 如果s的长度大于等于len什么都不做,否则扩充到len长并使用0填充
sds sdsgrowzero(sds s, size_t len);
sdscatprintf()  格式化字符串并连接到动态字符串:
C
1
2
3
4
/**
* 根据fmt和后续参数进行字符串格式化,然后连接到动态字符串s中
*/
sds sdscatprintf(sds s, const char *fmt, ...);

举例: sdscatprintf(sdsempty(), "%s %s", "Hello", "Alex"); 

sdsfromlonglong()  从数字创建字符串:
C
1
sds sdsfromlonglong(long long value);

举例:

C
1
2
sds num = sdsfromlonglong( 9460500000000 );
assert( 0 == strcmp( "9460500000000", num ));
sdstrim()  修剪字符串:
C
1
2
3
4
/**
* 修剪动态字符串s,清除左侧或者右侧的、存在于cset中的字符
*/
void sdstrim( sds s, const char *cset );

举例:

C
1
2
3
sds str = sdsnew( "\n\nHello World   " );
sdstrim( str, "\n " );
assert( strcmp( "Hello World", str ) == 0 );
sdsrange()  修剪为子串:
C
1
2
3
4
/**
* 修改动态字符串s,保留从start到end的部分,end包含在内
*/
void sdsrange( sds s, int start, int end );
sdscpy() 
sdscpylen() 
strcpy() 是C标准库中最危险和恶名的操作之一。SDS提供的相应的操作则可用于性能关键的领域:
C
1
2
3
4
5
6
7
8
/**
* 拷贝t到动态字符串s中
*/
sds sdscpy( sds s, const char *t );
/**
* 拷贝t的前len个字节到动态字符串s中
*/
sds sdscpylen(sds s, const char *t, size_t len);
scscatrepr()  连接字符串,并将其中的不可打印字符使用转义字符的形式显示出来:
C
1
2
3
4
5
6
7
8
sds sdscatrepr(sds s, const char *p, size_t len);
 
//示例:
sds s = sdsempty();
char q[10];
q[0] = 'A';q[1] = 1;q[2] = 20;q[4] = '\t';q[3] = '\n';
s = sdscatrepr(s,q,10);
printf( s ); //输出:"A\x01\x14\n\t\x7f\x00\x00\x00\x00"
sdssplitlen() 
sdsfreesplitres() 
标记化(Tokenization):将一个大字符串分隔(split)为多个小字符串:
C
1
2
3
4
5
6
/**
* s的前len个字节用来被分割,sep的前len字节用作分隔符,count用于返回子串的数量
*/
sds *sdssplitlen( const char *s, int len, const char *sep, int seplen, int *count );
// 销毁子串资源
void sdsfreesplitres( sds *tokens, int count );

举例:

C
1
2
3
4
5
6
7
8
9
10
11
char *str = "1986,.1989.,2014";
int count;
sds *substrs = sdssplitlen( str, strlen( str ), ",.", 2, &count );
for ( int i = 0; i < count; ++i ) {
    sds substr = *( substrs + i );
    printf( "%s\n", substr );
    //打印:
    //1986
    //1989.,2014
}
sdsfreesplitres( substrs, count );
sdsjoin() 
sdsjoinsds() 
使用分隔符连接一组字符串:
C
1
2
3
4
5
6
/**
* 使用sep中的前seplen个字符作为分隔符,连接长度为argc的字符串数组
*/
sds sdsjoin( char **argv, int argc, char *sep, size_t seplen );
//类似上面
sds sdsjoinsds( sds *argv, int argc, const char *sep, size_t seplen );

 举例:

C
1
2
char *strs[3] = { "foo", "bar", "zap" };
printf( sdsjoin( strs, 3, "-" )); //打印:foo-bar-zap
sdsRemoveFreeSpace() 收缩可变字符串,移除空闲的空间,在内存受限环境下可以使用:
C
1
sds sdsRemoveFreeSpace(sds s);
sdsAllocSize() 获得一个可变字符串实际分配的空间:
C
1
size_t sdsAllocSize(sds s); 
sdsupdatelen() 改变逻辑长度,反映出C字符串的长度:
C
1
2
3
4
5
6
7
8
9
void sdsupdatelen( sds s );
 
// 举例
sds s = sdsnew( "foobar" );
s[2] = '\0';
assert( sdslen( s ) == 6 );
sdsupdatelen(s);
// 更新逻辑长度后,sds的长度与C字符串长度一致
assert( sdslen( s ) == 2 );

上面这些函数,但凡返回sds的,在内存溢出的情况下一律会返回空指针。 

SDS的技术细节

SDS头部由下面的数据结构表示:

C
1
2
3
4
5
struct sdshdr {
    int len;    // 保存动态字符串的长度
    int free;   // 保存缓冲区空闲字节数,这些空闲字节可以容纳更多的字符
    char buf[]; // 未声明长度的数组,因此它实际上指向free后面的那个字节,这很重要
};

结构中的buf字段是一个flexible array member,它是C99引入的特性——位于结构体最后的无长度的数组。该数组的指针指向紧跟着结构体的内存,下面的示例代码可以验证这一点:

C
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动态字符串,应当将SDS封装在具有引用计数的结构体中,避免编程错误导致的内存泄漏: 

C
1
2
3
4
struct ds {
    int refcount;
    sds str;
}

你应当提供增加、减少引用计数的函数:

  1. 每当其它数据结构引用ds或者将ds赋值给变量时,都应当增加引用计数
  2. 每当引用移除时,减少计数,计数为0时,自动销毁SDS字符串
零拷贝(Zero copy)连接

在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);  

  

← Howard Blake.Walking In The Air
MinGW知识集锦 →

Leave a Reply Cancel reply

Your email address will not be published. Required fields are marked *

You may use these HTML tags and attributes: <a href="" title=""> <abbr title=""> <acronym title=""> <b> <blockquote cite=""> <cite> <code class="" title="" data-url=""> <del datetime=""> <em> <i> <q cite=""> <strike> <strong> <pre class="" title="" data-url=""> <span class="" title="" data-url="">

Related Posts

  • 使用Python进行文本处理
  • Go语言IO编程
  • Cygwin知识集锦
  • Linux内核学习笔记(五)
  • libevent学习笔记

Recent Posts

  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
  • A Comprehensive Study of Kotlin for Java Developers
  • 背诵营笔记
  • 利用LangChain和语言模型交互
  • 享学营笔记
ABOUT ME

汪震 | Alex Wong

江苏淮安人,现居北京。目前供职于腾讯云,专注容器方向。

GitHub:gmemcc

Git:git.gmem.cc

Email:gmemjunk@gmem.cc@me.com

ABOUT GMEM

绿色记忆是我的个人网站,域名gmem.cc中G是Green的简写,MEM是Memory的简写,CC则是我的小天使彩彩名字的简写。

我在这里记录自己的工作与生活,同时和大家分享一些编程方面的知识。

GMEM HISTORY
v2.00:微风
v1.03:单车旅行
v1.02:夏日版
v1.01:未完成
v0.10:彩虹天堂
v0.01:阳光海岸
MIRROR INFO
Meta
  • Log in
  • Entries RSS
  • Comments RSS
  • WordPress.org
Recent Posts
  • Investigating and Solving the Issue of Failed Certificate Request with ZeroSSL and Cert-Manager
    In this blog post, I will walk ...
  • A Comprehensive Study of Kotlin for Java Developers
    Introduction Purpose of the Study Understanding the Mo ...
  • 背诵营笔记
    Day 1 Find Your Greatness 原文 Greatness. It’s just ...
  • 利用LangChain和语言模型交互
    LangChain是什么 从名字上可以看出来,LangChain可以用来构建自然语言处理能力的链条。它是一个库 ...
  • 享学营笔记
    Unit 1 At home Lesson 1 In the ...
  • K8S集群跨云迁移
    要将K8S集群从一个云服务商迁移到另外一个,需要解决以下问题: 各种K8S资源的迁移 工作负载所挂载的数 ...
  • Terraform快速参考
    简介 Terraform用于实现基础设施即代码(infrastructure as code)—— 通过代码( ...
  • 草缸2021
    经过四个多月的努力,我的小小荷兰景到达极致了状态。

  • 编写Kubernetes风格的APIServer
    背景 前段时间接到一个需求做一个工具,工具将在K8S中运行。需求很适合用控制器模式实现,很自然的就基于kube ...
  • 记录一次KeyDB缓慢的定位过程
    环境说明 运行环境 这个问题出现在一套搭建在虚拟机上的Kubernetes 1.18集群上。集群有三个节点: ...
  • eBPF学习笔记
    简介 BPF,即Berkeley Packet Filter,是一个古老的网络封包过滤机制。它允许从用户空间注 ...
  • IPVS模式下ClusterIP泄露宿主机端口的问题
    问题 在一个启用了IPVS模式kube-proxy的K8S集群中,运行着一个Docker Registry服务 ...
  • 念爷爷
      今天是爷爷的头七,十二月七日、阴历十月廿三中午,老人家与世长辞。   九月初,回家看望刚动完手术的爸爸,发

  • 6 杨梅坑

  • liuhuashan
    深圳人才公园的网红景点 —— 流花山

  • 1 2020年10月拈花湾

  • 内核缺陷触发的NodePort服务63秒延迟问题
    现象 我们有一个新创建的TKE 1.3.0集群,使用基于Galaxy + Flannel(VXLAN模式)的容 ...
  • Galaxy学习笔记
    简介 Galaxy是TKEStack的一个网络组件,支持为TKE集群提供Overlay/Underlay容器网 ...
TOPLINKS
  • Zitahli's blue 91 people like this
  • 梦中的婚礼 64 people like this
  • 汪静好 61 people like this
  • 那年我一岁 36 people like this
  • 为了爱 28 people like this
  • 小绿彩 26 people like this
  • 彩虹姐姐的笑脸 24 people like this
  • 杨梅坑 6 people like this
  • 亚龙湾之旅 1 people like this
  • 汪昌博 people like this
  • 2013年11月香山 10 people like this
  • 2013年7月秦皇岛 6 people like this
  • 2013年6月蓟县盘山 5 people like this
  • 2013年2月梅花山 2 people like this
  • 2013年淮阴自贡迎春灯会 3 people like this
  • 2012年镇江金山游 1 people like this
  • 2012年徽杭古道 9 people like this
  • 2011年清明节后扬州行 1 people like this
  • 2008年十一云龙公园 5 people like this
  • 2008年之秋忆 7 people like this
  • 老照片 13 people like this
  • 火一样的六月 16 people like this
  • 发黄的相片 3 people like this
  • Cesium学习笔记 90 people like this
  • IntelliJ IDEA知识集锦 59 people like this
  • 基于Kurento搭建WebRTC服务器 38 people like this
  • Bazel学习笔记 37 people like this
  • PhoneGap学习笔记 32 people like this
  • NaCl学习笔记 32 people like this
  • 使用Oracle Java Mission Control监控JVM运行状态 29 people like this
  • Ceph学习笔记 27 people like this
  • 基于Calico的CNI 27 people like this
Tag Cloud
ActiveMQ AspectJ CDT Ceph Chrome CNI Command Cordova Coroutine CXF Cygwin DNS Docker eBPF Eclipse ExtJS F7 FAQ Groovy Hibernate HTTP IntelliJ IO编程 IPVS JacksonJSON JMS JSON JVM K8S kernel LB libvirt Linux知识 Linux编程 LOG Maven MinGW Mock Monitoring Multimedia MVC MySQL netfs Netty Nginx NIO Node.js NoSQL Oracle PDT PHP Redis RPC Scheduler ServiceMesh SNMP Spring SSL svn Tomcat TSDB Ubuntu WebGL WebRTC WebService WebSocket wxWidgets XDebug XML XPath XRM ZooKeeper 亚龙湾 单元测试 学习笔记 实时处理 并发编程 彩姐 性能剖析 性能调优 文本处理 新特性 架构模式 系统编程 网络编程 视频监控 设计模式 远程调试 配置文件 齐塔莉
Recent Comments
  • qg on Istio中的透明代理问题
  • heao on 基于本地gRPC的Go插件系统
  • 黄豆豆 on Ginkgo学习笔记
  • cloud on OpenStack学习笔记
  • 5dragoncon on Cilium学习笔记
  • Archeb on 重温iptables
  • C/C++编程:WebSocketpp(Linux + Clion + boostAsio) – 源码巴士 on 基于C/C++的WebSocket库
  • jerbin on eBPF学习笔记
  • point on Istio中的透明代理问题
  • G on Istio中的透明代理问题
  • 绿色记忆:Go语言单元测试和仿冒 on Ginkgo学习笔记
  • point on Istio中的透明代理问题
  • 【Maven】maven插件开发实战 – IT汇 on Maven插件开发
  • chenlx on eBPF学习笔记
  • Alex on eBPF学习笔记
  • CFC4N on eBPF学习笔记
  • 李运田 on 念爷爷
  • yongman on 记录一次KeyDB缓慢的定位过程
  • Alex on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • will on Istio中的透明代理问题
  • haolipeng on 基于本地gRPC的Go插件系统
  • 吴杰 on 基于C/C++的WebSocket库
©2005-2025 Gmem.cc | Powered by WordPress | 京ICP备18007345号-2