字符串是一种重要的数据类型,但是C语言并没有显式的字符串数据类型,因为字符串以字符串常量的形式出现或者存储于字符数组中。字符串常量适用于那些程序不会对它们进行修改的字符串。所有其他字符串都必须存储于字符数组或动态分配的内存中。本文描述处理字符串和字符的库函数,以及一些相关的,具有类似能力的,既可以处理字符串也可以处理非字符串数据的函数。
1.1 字符串基础
字符串是一串零个或多个字符,并且以一个位模式全0的NUL字节(' ')结尾。NUL字节是字符串的终止符,但它本身并不是字符串的一部分,所以字符串长度并不包括NUL字节。
头文件string.h包含了字符串函数所需的圆形和声明。在程序中包含这个头文件确实是个好主意,因为有了它所包含的原型,编译器可以更好地为程序执行错误检查。
1.2字符串长度
字符串的长度是它所包含的字符个数,不包含NUL字节。我们很容易对字符进行计数来计算字符串的长度。需要说明的是,如果strlen函数的参数不是一个以NUL字节结尾的字符序列,它将继续进行查找,直到发现一个NUL字节为止,这样strlen函数的返回值将是不确定数!
库函数strlen的源码如下:
size_t strlen(char const * string) { int length; for (length = 0; *string++ != ' '; ;) { length += 1; } return length; }
注意:
strlen返回一个类型为size_t的值,它是一个无符号整数类型。在表达式中使用无符号可能导致不可预料的结果。
例如:
if ( strlen(x) >= strlen(y) ) { ... } if (strlen(x) - strlen(y) >= 0) { ... }
上面两个表达式看似相等,但事实上它们是不想等的。第一条语句是我们所预想的那样工作,但第2条语句的结果永远为真。因为strlen的结果是无符号,所以strlen(x)-strlen(y)的结果也是个无符号数,而无符号数都是大于等于“0”的。在表达式中如果同时包含了无符号和有符号数同样会产生奇怪的结果。
1.3 不受限制的字符串函数
最常用的字符串函数都是“不受限制”的,就是说它们只是通过寻找字符串参数结尾的NUL字节来判断它们的长度。这些函数一般都指定一块内存用于存放结果字符串。在使用这些函数时,程序员必须保证结果字符串不会溢出这块内存。
1.3.1 复制字符串
用于复制字符串的函数是strcpy,它的原型如下:
char *strcpy( char *dst, char const *src);
这个函数把参数src字符串复制到dst参数。如果参数src和dst在内存中出现重叠,其结果是未定义的。由于dst参数将进行修改,所以它必须是个字符数组或者是一个指向动态分配内存的数组指针,不能使用字符串常量。
目标参数以前的内容将被覆盖丢失。即使新的字符串比dst原先的内存更短,由于新的字符串是以NUL字节结尾,所以老字符串最后剩余的几个字符也会被有效地删除。需要注意的是字符结束标志也将被复制。
例如:
char message[] = "message"; ... if (...) { strcpy( message, "Dif"); }如果条件为真并且复制顺利执行,数组将包含下面的内容:
第一个NUL字节后面的几个字符再也无法被字符串函数访问,因此从任何角度实现看,它们都已经是丢失的了。
注意:
程序员必须保证目标字符数组的空间足以容纳需要复制的字符串。如果字符串比数组长,多余的字符仍被复制,它们将覆盖原先存储于数组后面的内存空间的值。strcpy无法解决这个问题,因为它无法判断目标字符数组的长度。
1.3.2 连接字符串
strcat函数可以实现一个字符串添加到另一个字符串的后面。函数原型如下:
strcat函数要求dst参数原先已经包含一个字符串(可以是空字符串)。它找到这个字符串的末尾,并把src字符串的一份拷贝添加到这个位置。如果src和dst的位置发生重叠,其结果是未定义的。char *strcat( char *dst, char const *src);
下面是这个函数的常见用法:
strcpy( message, "hello"); strcpy( message, customer_name);
注意:程序员必须保证目标字符数组剩余的空间足以保存整个src源字符串。
1.3.3 字符串比较
库函数strcmp的原型如下:
int strcmp( char const *s1, char const *s2);
字符串比较的规则:
对两个字符串自左向右逐个字符比较(按ASCII码值大小比较),直到出现不同的字符或遇到‘ ’为止,如果全部相同则认为相等。
1. s1小于s2,函数返回负整数;
2. s1大于s2,函数返回正整数;3. s1等于s2,函数返回0;
注意:比较两个字符串更好的方法是把返回值与零进行比较。
if ( 0 == strcmp(str1,str2)) { ... }
注意:
由于strcmp并不修改它的任一个参数,所以不存在溢出字符数组的危险。strcmp函数的字符串参数也必须以一个NUL字节结尾。如果不是,strcmp就可能对参数后面的字节进行比较,这个比较结果无意义!
1.4 长度受限的字符串函数
标准库函数还包含了一些函数,这些函数接受一个显式的长度参数,用于限定进行复制或比较的字符数。这些函数提供了一种方便的机制,可以防止难以预料的长字符串从它们的目标数组溢出。
这些函数的原型如下所示,和不受限制版本一样,如果源参数和目标参数发生重叠,strncpy和strncat的结果都是未定义。
char *strncpy(char *dst, char const *src, size_t len ); char *strncat(char *dst, char const *src, size_t len ); char *strncmp(char const *s1, char const *s2, size_t len );
1.4.1 strncpy
和strcpy一样,strncpy把源字符串的字符复制到目标数组。但它总是向dsr写入len个字符。如果strlen(src)的值小于len,dst数组就用额外的NUL字节填充到len长度。如果strlen(src)的值大于或等于len,那么只有len个字符被复制到dst中。此时,它的结果将可能不会以NUL字节结尾。
例如:
char dst[10]; char src[] = "abcdefghijklmn"; strncpy(dst, src,5); //dst字符数组不是以NUL结尾,len是23,是个随机数 int len = strlen(dst);
警告:
strncpy调用的结果可能不是一个字符串,因此字符串必须是以NUL字节结尾。如果在一个需要字符串的地方(例如:strlen函数参数)使用了没有以NUL字节结尾的字符序列,会发生什么情况呢?strlen函数不知道NUL字节是没有的,所以它将继续一个字符一个字符的查找,知道发现NUL字节为止。或者如果函数试图访问系统分配给这个程序以外的内存范围,程序就会崩溃。
这个问题只有当你使用strncpy函数创建字符串,然后或者对它们使用str开头的库函数,或者在printf中使用%s格式打印它们时才会发生。考虑如下代码:
如果strlen(name)小于BSIZE,之后的赋值语句不起作用。但是,name长度很长,这条赋值语句可以保证buffer中的字符串是以NUL字节结尾。以后对这个数组使用strlen等其它函数就会正常工作。char buffer[BSIZE]; ... strncpy(buffer,name,BSIZE); buffer[BSIZE-1] = ' ';
1.4.2 strncat
strncat也是一个长度受限的函数,但它和strncpy不同,strncat从src中最多复制len个字符到目标数组的后面。并且,strncat总是在结果字符串后面添加一个NUL字节。它不管目标参数除去原先存在的字符串之后留下的空间够不够。
1.4.3 strncmp
strncmp用于比较两个字符串,但它最多比较len个字节。如果两个字符串在第len个字符之前存在不等的字符,这个函数停止比较,返回结果。如果两个字符串的前len个字符相等,函数就返回零。
1.5 字符串查找基础
标准库中存在许多函数,它们用各种不同的方法查找字符串。
1.5.1 查找一个字符
在一个字符串中查找一个特定字符最容易的方法是使用strchr和strchr函数,它们的原型如下:
char *strchr( char const *str, int ch); char *strrchr( char const *str, int ch);
它们的第二个参数是一个整型值。但是,它包含了一个字符值(ASCII)。strchr在字符串中str查找字符ch第1次出现的位置,找到后函数返回一个指向该位置的指针。如果该字符并不存在于字符串中,函数就返回一个NUL指针。
strrchr的功能与strchr基本一致,只是它所返回的是一个指向字符串中该字符最后一次出现的位置(r:最右边)。
例如:
char src[] = "abc12def12"; printf("the first = %s ",strchr(src,49));//strchr(src,'1')
1.5.2 查找任何几个字符
strprk是个更为常见的函数,它是查找任何一组字符第一次在字符串中出现的位置,它的原型如下:
例如:char *strpbrk( char const *str, char const *group);
这个函数返回一个指向str中第1个匹配group中任何一个字符的字符位置。如果未找到函数返回一个NULL指针。
char src[] = "123qaz!@#"; char group[] = "aq!"; printf("location = %s ",strpbrk(src,group));//qaz!@#,group中的字符第1次出现的位置是q
1.5.3 查找一个子串
为了在字符串中查找一个子串,我们可以使用strstr函数,它的原型如下:
char *strstr( char const *s1, char const *s2 );
这个函数在s1中查找整个s2第1次出现的位置,并返回一个指向该位置的指针。如果s2没有完整的出现在s1中任何地方,函数返回NULL指针。如果s2是一个空字符串,函数就返回s2。
1.6 高级字符串查找
1.6.1 查找一个字符串前缀
strspn和strcspn函数用于在字符串的起始位置对字符计数。它们的函数原型如下:
size_t strspn( char const *str, char const *group ); size_t strcspn( char const *str, char const *group );
1.6.2 查找标记
一个字符串常常包含几个单独的部分,它们彼此分隔开来。每次为了处理这些部分,你首先必须把它们从字符串中抽取出来。这个任务正是strtok函数所实现的功能。它从字符串中隔离各个单独的称为标记(token)的部分,并丢弃分割符。它的原型如下:
char *strtok( char *str, char const *sep);
sep参数是个字符串,定义了用作分隔符的字符集。str指定一个字符串,它包含零个或多个有sep字符串中一个或多个分隔符分隔的标记。strtok找到str的下一个标记,并将其用NUL结尾,然后返回一个指向这个标记的指针。
高级字符串查找将另外详解描述!
1.7 字符操作
标准库包含了两组函数,用于操作单独的字符,它们的原型位于头文件ctype.h。第一组函数用于字符分类,第二组函数用于转换字符。
1.7.1 字符分类
每个分类函数接受一个包含字符值的整型参数。函数测试这个字符并返回一个整型值,表示真或假。
int isalnum( int ch ); int iscntrl( int ch ); int islower( int ch ); int isprint( int ch ); int isupper( int ch ); int isspace( int ch );
1.7.2 字符转换
转换函数把大写字母转换为小写字母或把小写字母转换为大写字母。它们的函数原型如下:
toupper函数返回其参数的对应大写形式,tolower函数返回其参数的对应的小写形式。如果函数参数并不是处于一个适当的大小写的字符,函数将不修改直接返回。int tolower( int ch ); int toupper( int ch );
提示:
直接测试或操纵字符将会降低程序的可移植性。例如,考虑下面这条语句,它试图测试ch是否是一个大写字符。
if( ch >= 'A' && ch <= 'z' )这条语句在使用ASCII字符集的机器上能够运行,但是在使用EBCDIC字符集的机器上将会失败。另一方面,下面这条语句
if ( isupper( ch ) )无论机器使用哪个字符集,它都能顺利进行,因此字符分类和转换函数可以提高函数的可移植性。
1.8 内存操作
在非字符串数据中包含零值的情况并不罕见,此时无法使用字符串处理函数来处理这种类型的数据,因为当它们遇到第1个NUL字节时就停止工作。我们应该使用另外一组相关的函数,它们的操作与字符串函数类似,但这些函数能够处理任意的字符序列。下面是它们的原型:
void *memcpy( void *dst, void const *src, size_t length ); void *memmvoe( void *dst, void const *src, size_t length ); void *memcmp( void const *a, void const *b, size_t length); void *memchr( void const *a, int ch, size_t length); void *memset( void *a, int ch, size_t length);每个原型都包含一个显示的参数说明需要处理的字节数,它们在遇到NUL字节时并不会停止工作。
1.8.1 memcpy
void *memcpy( void *dst, void const *src, size_t length );
char temp[SIZE],value[SIZE]; ... memcpy( temp, value, SIZE);//从数组value复制SIZE个字节到temp如果两个数组为整型数组该怎么办?下面语句完成可以完成这项任务:memcpy( temp, value, sizeof(value) );memcpy()前两个参数类型是void*型指针,而任何类型的指针都可以转化为void*型指针。
void *memmvoe( void *dst, void const *src, size_t length );
memmove函数的行为和memcpy差不多,只是它的源和目标操作数可以重叠。它的内部实现过程:把源操作数复制到一个临时位置,这个临时的位置不会与源或目标操作数重叠,然后再把它从这个临时位置复制到目标操作数。如果源和目标参数真的可能存在重叠,就应该使用memmove,如下所示:
//Shift the values int the x array left one position. memmove( x, x+1, ( count-1 ) * sizeof(x[0]) );
1.8.3 memcmp
void *memcmp( void const *a, void const *b, size_t length);
memcmp对两段内存的内容进行比较,这两段内存分别起始于a和b,共比较length个字节。这些值按照无符号字符逐字进行比较,函数的返回值与strcmp函数一样。由于这些值是根据一串无符号字节进行比较的,所以memcmp函数用于比较不是单字节的数据如整数或浮点数时可能出现不可预料的结果。
1.8.4 memchr
void *memchr( void const *a, int ch, size_t length);
memchr从a的起始位置开始查找字符ch第一次出现的位置,并返回一个指向该位置的指针,它共查找length个字节。如果在length个字节中未找到该字符,函数就返回NULL指针。
1.8.5 memset
void *memset( void *a, int ch, size_t length);
memset函数把从a开始的length字节都设置为字符值ch。例如:
memset( buffer, 0, SIZE);//把buffer前SIZE个字节都初始化为‘ ’