11.5 字符串函数
C库提供了多个处理字符串的函数,ANSI C把这些函数的原型放在string.h头文件中。其中最常用的函数有strlen()、strcat()、strcmp()、strncmp()、strcpy()和strncpy()。另外,还有sprintf()函数,其原型在stdio.h头文件中。
11.5.1 strlen()函数
strlen()函数用于统计字符串的长度。下面的语句缩短了字符串的长度,其中用到了strlen():
void fit( char *string, unsigned int size ){
if( strlen( string ) > size ){
string[size] = '\0';
}
}
该函数改变字符串,所以函数头在声明形式参数string时没有使用const限定符。
/* test_fit.c -- try the string-shrinking function */
#include
#include
void fit(char *, unsigned int);
int main(void)
{
char mesg[] = "Things should be as simple as possible,"
" but not simpler.";
puts(mesg);
fit(mesg,38);
puts(mesg);
puts("Let's look at some more of the string.");
puts(mesg + 39);
return 0;
}
void fit(char *string, unsigned int size)
{
if (strlen(string) > size)
string[size] = '\0';
}
/* 输出:
*/
fit()函数把第39个元素的逗号替换成'\0'字符。
注意
一些ANSI之前的系统使用strings.h头文件,而有些系统可能根本没有字符串头文件。
string.h头文件包含了C字符串函数系列的原型。
11.5.2 strcat()函数
strcat()(用于拼接字符串)函数接受两个字符串作为参数。该函数把第2个字符串的备份附加在第1个字符串末尾,并把拼接后形成的新字符作为第1个字符串,第2个字符串不变。strcat()函数的类型是char *(即,指向char的指针)。strcat()函数返回第1个参数,即拼接第2个字符串后的第1个字符串的地址。
演示strcat()的用法
/* str_cat.c -- joins two strings */
#include
#include
#define SIZE 80
char * s_gets(char * st, int n);
int main(void)
{
char flower[SIZE];
char addon[] = "s smell like old shoes.";
puts("What is your favorite flower?");
if (s_gets(flower, SIZE))
{
strcat(flower, addon);
puts(flower);
puts(addon);
}
else
puts("End of file encountered!");
puts("bye");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
11.5.3 strncat()函数
strcat()函数无法检查第1个数组是否能容纳第2个字符串。如果分配给第1个数组的空间不够大,多出来的字符溢出到相邻存储单元就会出问题。当然,用strlen()查看第1个数组的长度。注意,要给拼接后的字符串长度加1才够空间存放末尾的空字符。或者,用strncat(),该函数的第3个参数指定了最大添加字符数。在加到最大字符数或遇到空字符时停止。因此,算上空字符(无论哪种情况都要添加空字符),目标数组应该足够大,以容纳原始字符串(不包含空字符)、添加原始字符串在后面的最大添加字符数和末尾的空符号。
计算avaiable变量的值,用于表示允许添加的最大字符数。
/* join_chk.c -- joins two strings, check size first */
#include
#include
#define SIZE 30
#define BUGSIZE 13
char * s_gets(char * st, int n);
int main(void)
{
char flower[SIZE];
char addon[] = "s smell like old shoes.";
char bug[BUGSIZE];
int available;
puts("What is your favorite flower?");
s_gets(flower, SIZE);
if ((strlen(addon) + strlen(flower) + 1) <= SIZE)
strcat(flower, addon);
puts(flower);
puts("What is your favorite bug?");
s_gets(bug, BUGSIZE);
available = BUGSIZE - strlen(bug) - 1;
strncat(bug, addon, available);
puts(bug);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
strcat()和gets()类似,也会导致缓冲区溢出。为什么C11标准不废弃strcat(),只留下strncat()?为何对gets()那么残忍?这也许是因为gets()造成的安全隐患来自于使用该程序的人,而strcat()暴露的问题是那些粗心的程序员造成的。无法控制用户进行什么操作,但是,可以控制你的程序做什么。C语言相信程序员,因此程序员有责任确保strcat()的使用安全。
11.5.4 strcmp()函数
用户的响应与已存储的字符串作比较。
/* nogo.c -- will this work? */
#include
#define ANSWER "Grant"
#define SIZE 40
char * s_gets(char * st, int n);
int main(void)
{
char try[SIZE];
puts("Who is buried in Grant's tomb?");
s_gets(try, SIZE);
while (try != ANSWER)
{
puts("No, that's wrong. Try again.");
s_gets(try, SIZE);
}
puts("That's right!");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
这个程序看上去没问题,但是运行后却不对劲。ANSWER和try都是指针,所以try != ANSWER检查的不是两个字符串相等,而是这两个字符串的地址相等。因为ANSWER和try存储在不同的位置,所以这两个地址不可能相同。因此,不论用户输入什么,程序都是提示不正确。这真让人
沮丧。
该函数要比较的是字符串的内容,不是字符串的地址。读者可以自己设计一个程序,也可以使用C标准库中的strcmp()函数(用于字符串比较)。该函数通过比较运算符来比较字符串,就像比较数字一样。如果两个字符串参数相同,该函数就返回0,否则返回非零值。修改后的版本如下:
/* compare.c -- this will work */
#include
#include
#define ANSWER "Grant"
#define SIZE 40
char * s_gets(char * st, int n);
int main(void)
{
char try[SIZE];
puts("Who is buried in Grant's tomb?");
s_gets(try, SIZE);
while (strcmp(try,ANSWER) != 0)
{
puts("No, that's wrong. Try again.");
s_gets(try, SIZE);
}
puts("That's right!");
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
注意
由于非零值都为“真”,所以许多经验丰富的C程序员会把该例main()中的while循环头写成:while( strcmp( try, ANSWER ) )
strcmp()函数比较的是字符串,不是整个数组,这是非常好的功能。"Grant"占用了6个字节(还有一个用来放空字符),strcmp()函数只会比较try中第1个空字符前面的部分。所以,可以用strcmp()比较存储在不同大小数组中的字符串。
如果希望程序更友好,必须把所有正确答案的可能性包含其中。这里可以使用一些小技巧。例如,可以使用#define定义类似GRANT这样的答案,并编写一个函数把输入的内容都转换成大写,就解决了大小写的问题。但是,还要考虑一些其他错误的形式。
1.strcmp()的返回值
使用strcmp()比较字符串。
/* compback.c -- strcmp returns */
#include
#include
int main(void)
{
printf("strcmp(\"A\", \"A\") is ");
printf("%d\n", strcmp("A", "A"));
printf("strcmp(\"A\", \"B\") is ");
printf("%d\n", strcmp("A", "B"));
printf("strcmp(\"B\", \"A\") is ");
printf("%d\n", strcmp("B", "A"));
printf("strcmp(\"C\", \"A\") is ");
printf("%d\n", strcmp("C", "A"));
printf("strcmp(\"Z\", \"a\") is ");
printf("%d\n", strcmp("Z", "a"));
printf("strcmp(\"apples\", \"apple\") is ");
printf("%d\n", strcmp("apples", "apple"));
return 0;
}
/* 输出:
*/
ANSI标准规定,在字母表中,如果第1个字符串在第2个字符串前面,strcmp()返回一个负数;如果两个字符串相同,strcmp()返回0;如果第1个字符串在第2个字符串后面,strcmp()返回正数。然而,返回的具体值取决于实现。在其他实现中输出可能为:
strcmp( "C", "A" ) is 2
strcmp( "Z", "a" ) is -7
strcmp( "apples", "apple" ) is 115
如果两个字符串开始的几个字符都相同会怎样?一般而言,strcmp()会依次比较每个字符,直到发现第1对不同的字符为止。然后,返回相应的值。
最后一个例子说明strcmp会比较所有的字符,不只是字母。所以,与其说该函数按字母顺序进行比较,还不如说是按照机器排序序列(machine collating sequence)进行比较,即根据字符的数值进行比较(通常都使用ASCII值)。在ASCII中,大写字母在小写字母前面。
大多数情况下,strcmp()返回的具体值并不重要,我们只在意该值是0还是非0(即,比较的两个字符串是否相等)。或者按字母排序字符串,在这种情况下,需要知道比较的结果是为正、为负还是为0。
注意
strcmp()函数比较的是字符串,不是字符,所以其参数应该是字符串而不是字符。但是,char类型实际上整数类型,所以可以使用关系运算符来比较字符。假设word是存储在char类型数组中的字符串,ch是char类型的变量,下面的语句都有效:
if( strcmp( word, "quit" ) == 0 ) //使用strcmp()比较字符串
{
puts( "Bye!" );
}
if( ch == 'q' ) //使用==比较字符
puts( "Bye!");
尽管如此,不要使用ch或'q'作为strcmp()的参数。
用strcmp()函数检查程序是否要停止读取输入。
/* quit_chk.c -- beginning of some program */
#include
#include
#define SIZE 80
#define LIM 10
#define STOP "quit"
char * s_gets(char * st, int n);
int main(void)
{
char input[LIM][SIZE];
int ct = 0;
printf("Enter up to %d lines (type quit to quit):\n", LIM);
while (ct < LIM && s_gets(input[ct], SIZE) != NULL &&
strcmp(input[ct],STOP) != 0)
{
ct++;
}
printf("%d strings entered\n", ct);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
该程序在读到EOF字符(这种情况下s_gets()返回NULL)、用户输入quit或输入项达到LIM时退出。
顺带一提,有时输入空行(即,只按下Enter键或Return键)表示结束输入更方便。为实现这一功能,只需修改一下while循环的条件即可:
while (ct < LIM && s_gets(input[ct], SIZE) != NULL && input[ct][0] != '\0' )
这里,input[ct]是刚输入的字符串,input[ct][0]是该字符串的第1个字符。如果用户输入空行,s_gets()便会把该行第一个字符(换行符)替换为空字符。所以,下面的表达式用于检测空行:
input[ct][0] != '\0'
2.strncmp()函数
strcmp()函数比较字符串中的字符,直到发现不同的字符为止,这一过程可能会持续到字符串的末尾。而strncmp()函数在比较两个字符串时,可以比较到字符不同的地方,也可以只比较第3个参数指定的字符数。
strncmp()函数的用法
/* starsrch.c -- use strncmp() */
#include
#include
#define LISTSIZE 6
int main()
{
const char * list[LISTSIZE] =
{
"astronomy", "astounding",
"astrophysics", "ostracize",
"asterism", "astrophobia"
};
int count = 0;
int i;
for (i = 0; i < LISTSIZE; i++)
if (strncmp(list[i],"astro", 5) == 0)
{
printf("Found: %s\n", list[i]);
count++;
}
printf("The list contained %d words beginning"
" with astro.\n", count);
return 0;
}
/* 输出:
*/
11.5.5 strcpy()和strncpy()函数
如果希望拷贝整个字符串,要使用strcpy()函数。程序要用用户输入以q开头的单词。该程序把输入拷贝至一个临时数组中,如果第1个字母是q,程序调用strcpy()把整个字符串从临时数组拷贝至目标数组中。strcpy()函数相当于字符串赋值运算符。
/* copy1.c -- strcpy() demo */
#include
#include
#define SIZE 40
#define LIM 5
char * s_gets(char * st, int n);
int main(void)
{
char qwords[LIM][SIZE];
char temp[SIZE];
int i = 0;
printf("Enter %d words beginning with q:\n", LIM);
while (i < LIM && s_gets(temp, SIZE))
{
if (temp[0] != 'q')
printf("%s doesn't begin with q!\n", temp);
else
{
strcpy(qwords[i], temp);
i++;
}
}
puts("Here are the words accepted:");
for (i = 0; i < LIM; i++)
puts(qwords[i]);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
拷贝出来的字符串被称为目标字符串,最初的字符串被称为源字符串。参考赋值表达式语句,很容易记住strcpy()参数的顺序,即第1个是目标字符串,第2个是源字符串。
char target[20];
int x;
x = 50; /*数字赋值*/
strcpy( target, "Hi ho!" ); /*字符串赋值*/
target = "So long"; /*语法错误*/
程序员有责任确保目标数组有足够的空间容纳源字符串的副本。下面的代码有点问题:
char *str;
strcpy( str, "The C of Tranquility" ); //有问题
由于str未被初始化,所以该字符串可能被拷贝到任意的地方!
总之,strcpy()接受两个字字符串指针作为参数,可以把指向源字符串的第2个指针声明为指针、数组名或字符串常量;而指向目标字符串的第1个指针应指向一个数据对象(如数组),且该对象有足够的空间存储字符串的版本。记住,声明数组将分配存储数据的空间,而声明指针只分配一个地址的空间。
1.strcpy()的其他属性
strcpy()函数还有两个有用的属性。第一,strcpy()的返回类型是char *,该函数返回的是第1个参数的值,即一个字符的地址。第二,第1个参数不必指向数组的开始。这个属性可用于拷贝数组的一部分。
/* copy2.c -- strcpy() demo */
#include
#include
#define WORDS "beast"
#define SIZE 40
int main(void)
{
const char * orig = WORDS;
char copy[SIZE] = "Be the best that you can be.";
char * ps;
puts(orig);
puts(copy);
ps = strcpy(copy + 7, orig);
puts(copy);
puts(ps);
return 0;
}
/* 输出:
*/
注意,strcpy()把源字符串中的空字符也拷贝在内。
2.更谨慎的选择:strncpy()
strcpy()和strncpy()都有同样的问题,它们都不能检查目标空间是否能容纳源字符串的副本。拷贝字符串用strncpy()更安全,该函数的第3个参数指明可拷贝的最大字符数。
演示目标空间装不下源字符串的副本会发生什么情况。
/* copy3.c -- strncpy() demo */
#include
#include
#define SIZE 40
#define TARGSIZE 7
#define LIM 5
char * s_gets(char * st, int n);
int main(void)
{
char qwords[LIM][TARGSIZE];
char temp[SIZE];
int i = 0;
printf("Enter %d words beginning with q:\n", LIM);
while (i < LIM && s_gets(temp, SIZE))
{
if (temp[0] != 'q')
printf("%s doesn't begin with q!\n", temp);
else
{
strncpy(qwords[i], temp, TARGSIZE - 1);
qwords[i][TARGSIZE - 1] = '\0';
i++;
}
}
puts("Here are the words accepted:");
for (i = 0; i < LIM; i++)
puts(qwords[i]);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
strncpy( target, source, n)把source中的n个字符或空字符之前的字符(先满足哪个条件就拷贝到何处)拷贝至target中。因此,如果source中的字符数小于n,则拷贝整个字符串,包括空字符。但是,strncpy()拷贝字符串的长度不会超过n,如果拷贝到第n个字符时还未拷贝完整个源字符串,就不会拷贝空字符。所以,拷贝的副本中不一定有空字符。鉴于此,该程序把n设置为比目标数组大小少1(TARGSIZE-1),然后把数组最后一个元素设置为空字符:
strncpy( qwords[i], temp, TARGSIZE - 1 );
qwords[i][TARGSIZE - 1] = '\0';
这样的确保存的是一个字符串。如果目标空间能容纳源字符串的副本,那么从源字符串拷贝的空字符便是该副本的末尾;如果目标空间装不下副本,则把副本最后一个元素设置为空字符。
11.5.6 sprintf()函数
sprintf()函数声明在stdio.h中。它是把数据写入字符串,而不是打印在显示器上。因此,该函数可以把多个元素合成一个字符串。sprintf()的第1个参数是目标字符串的地址。其余参数和printf()相同,即格式字符串和待写入项的列表。
程序用sprintf()把3个项组合成一个字符串。
/* format.c -- format a string */
#include
#define MAX 20
char * s_gets(char * st, int n);
int main(void)
{
char first[MAX];
char last[MAX];
char formal[2 * MAX + 10];
double prize;
puts("Enter your first name:");
s_gets(first, MAX);
puts("Enter your last name:");
s_gets(last, MAX);
puts("Enter your prize money:");
scanf("%lf", &prize);
sprintf(formal, "%s, %-19s: $%6.2f\n", last, first, prize);
puts(formal);
return 0;
}
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}
/* 输出:
*/
11.5.7 其他字符串函数
ANSI C库有20多个用于处理字符串的函数。下面总结了一些常用的函数。
*char *strcpy( char *restrict s1, const char *restrict s2 );
*char *strcpy( char *restrict s1, const char *restrict s2, size_t n );
*char *strcat( char *restrict s1, const char *restrict s2 );
*char *strncat( char *restrict s1, const char *restrict s2, size_t n );
*int strcmp( const char *s1, const char *s2 );
*int strncmp( const char *s1, const char *s2, size_t n );
*char *strchr( const char *s, int c );
如果s字符串中包含c字符,该函数返回指向s字符串首次出现的c字符的指针(末尾的空字符也是字符串的一部分,所以在查找范围内);如果在字符串s中未找到c字符,该函数则返回空指针。
*char *strpbrk( const char *s1, const char *s2 );
如果s1字符中包含s2字符串中的任意字符,该函数返回指向s1字符串s1字符串首位置的指针;如果在s1字符串未找到任何s2字符串中的字符,则返回空指针。(注意,是空指针,不是空字符)
*char *strrchr( const char *s, char c );
该函数返回s字符串中c字符最后一次出现的位置(末尾的空字符也是字符串的一部分,所以在查找范围内)。如果未找到c字符,则返回空指针。
*char *strstr( const char *s1, const char *s2 );
该函数返回指向s1字符串中s2字符串出现的首位置。如果在s1中没有找到s2,则返回空指针。
*size_t strlen( const char *s );
请注意,那些使用const关键字的函数原型表明,函数不会更改字符串。至少不能在该函数中更改。
关键字restrict关键字限制了函数参数的用法。例如,不能把字符串拷贝给本身。string.h头文件针对特定系统定义了size_t,或者参考其他由size_t定义的头文件。
fgets()读入一行输入时,在目标字符串的末尾添加换行符。我们自定义的s_gets()函数通过while循环检测换行符。其实,这里可以用strchr()代替s_gets()。首先,使用strchr()查找换行符(如果有的话)。如果该函数发现了换行符,将返回该换行符的地址,然后便可用空字符串替换该位置上的换行符:
char line[80];
char *find;
fgets( line, 80, stdin );
find = strchr( line, '\n' ); //查找换汉服
if( find ) //如果没找到换行符,返回NULL
*find = '\0'; //把该处的字符替换为空字符
如果strchr()未找到换行符,fgets()在达到行末尾之前就达到了它能读取的最大字符数。可以像在s_gets()中那样,给if添加一个else来处理这种情况。
char * s_gets(char * st, int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
char *find = strchr( st, '\n' );
if ( find )
*find = '\0';
else // must have words[i] == '\0'
while (getchar() != '\n')
continue;
}
return ret_val;
}