详见CPrimerPlus P329
分析常用的处理字符串输入和输出的函数,以及如何结合这几个函数进行优化和设计一些新的处理字符串输入输出的函数。
要做的第一件事是分配空间,以存储稍后要读入的字符串。
这意味这要未字符串分配足够的空间,注意计算机在读取字符串时不会计算它的长度。
常见的一个错误写法:scanf要把信息拷贝到参数指定的地址上,而此时的参数是一个未初始话的指针,那么可以指向程中的任意的地址,可能会擦写掉程序中的数据和到吗,导致程序中断。
char *name;
scanf("%s",name);
分配内存的方法:
1.声明时显式指明数组的大小。
char name[80];
2.使用C语言的库函数来分配内存。动态内存分配(malloc等)
int name = (int*)malloc(sizeof(int)*n)
简介
在读取字符串时,scanf()函数和转换符%s只能读取一个单词,但是在程序中经常要读取一整行输入,因此gets()可以处理这种情况。
使用
gets函数读取整行输入,直到遇到换行符,然后丢弃换行符(与fgets函数区分),存储其他的字符,并在这些字符后面加上一个空字符使其成为一个C字符串。
问题:
问题在于gets的唯一参数就是数组名,它无法检查数组是否装得下输入行,数组名会被转换成该数组的首元素地址,因此gets函数只知道数组的开始处,如果输入的字符串过长,会导致缓冲区溢出,及多余的字符超出了指定的内存空间,如果这些多余的字符只是占据了尚未使用的内存时,并不会立刻出现问题,但是如果它们占擦写掉了程序中的其他数据,会导致程序中止。
因此C11委员会在标准中废除了gets()函数。
过去常使用fgets函数来代替gets函数,在输入处理方面与gets函数略有不同。
C11标准中新增的gets_s函数也可以代替gets函数,该函数与gets函数很接近,而且可以替换现有代码中的gets函数。
fgets函数和gets函数的区别:
案例一:
#include<stdio.h>
#define STLEN 14
int main()
{
char words[STLEN];
puts("Enter a string,please.");//自动添加换行符,无需添加’\n‘
fgets(words,STLEN,stdin);
printf("Your string twice (puts(),then fputs()):\n");//手动添加’\n‘
puts(words);
fputs(words,stdout);
puts("Enter another string,please.");
fgets(words,STLEN,stdin);
printf("Your string twice (puts(),then fputs()):\n");
puts(words);
fputs(words,stdout);
puts("Done.");
return 0;
}
输出:
Enter a string,please.
apple pie
Your string twice (puts(),then fputs()):
apple pie
apple pie
Enter another string,please.
strawberry shortcake
Your string twice (puts(),then fputs()):
strawberry sh
strawberry shDone.
分析:
gets函数抛弃\n,puts函数添加\n,fgets函数保留\n,fputs函数不添加\n
第一行输入中:apple pie
比fgets函数读入的整行输入短,因此apple pie\n\0
被存储在了数组中,不过puts函数在显示改字符串时又在末尾添加了换行符,因此apple pie后面又一行空行。fputs函数不在字符串,末尾添加换行符所以未打印空行。
第二行输入中:strawberry shortcake
超过了大小限制,所以fgets函数只读取了13个字符,把strawberry sh\0
存储到了数组中。
案例二:简单的循环,输入并显示用户输入的内容,直到fgets读取到文件结尾或空行。
fgets函数返回指向char的指针,如果一切顺利,该函数返回的地址与传入的第一个参数的相同,但是如果函数读到文件结尾,它将返回一个特殊的指针:空指针。
#include<stdio.h>
#define STLEN 10
int main ()
{
char words[STLEN];
puts("Enter strings (empty line to quit):");
while(fgets(words,STLEN,stdin) != NULL && words[0] != '\n')//当读取到文件结尾或出现了换行符时停止循环
fputs(words,stdout);
puts("Done.");
return 0;
}
输出:
Enter strings (empty line to quit):
by the way,the gets function
by the way,the gets function
分析:利用fputs函数不在字符串末尾不添加\n这一特性处理超过了规定的数组限制的输入情况,并且输出完整的字符串。
程序中的fgets函数一次读入STLEN-1
个字符(本案例中为9个字符),所以它第一次只读取了by the wa
,并存储为by the wa\0
,接着fputs函数打印该字符串,并且为换行。然后while循环进入了下一轮迭代,fgets函数继续从输入的剩余中读入数据,一直循环,直到读到tion\n
,fgets函数将其存储为tion\n\0
,fputs函数打印该字符,并且由于\n进行换行。
fgets()函数存储’\n’的好处与坏处:
好处:是对于存储的字符串而言,检查末尾的换行符可以判断是否成功读取了一整行,如果不是一整行则妥善处理一行中剩下的字符。(如上面的案例)
坏处:是有时候我们并不想把换行符存储在字符串中,这样的换行符会带来一些麻烦。
如何处理掉换行符?
while(words[i] != '\n')
i++;
words[i] = '\0';
//将\0替换为\n
如何丢弃掉仍存在输入行中的字符串?
如果目标数组装不下一整行输入,就丢弃多出的字符:
while(getchar() != '\n')//读取但是不存储输入,包括\n
continue;
案例三:
程序读取输入行,删除储存在字符串中的换行符,如果没有换行符,则丢弃数组装不下的字符。
#include<stdio.h>
#define STLEN 10
int main ()
{
char words[STRLEN];
int i;
puts("Enter strings(empty line to quit):");
while(fgets(words,STRLEN,stdin) != NULL && word[0] != '\0'){
i = 0;
while(words[i] != '\n' && wprds[i] != '\0')
i++;
if(words[i] == '\n')//将存储在字符串中的换行符替换成’\0‘
word[i] = '\0';
else //如果没有换行符则丢弃字符串剩余的部分
while(getchar() != '\n')
continue;
puts(words);
}
puts("done");
return 0;
}
分析:遍历字符串,直至遇到换行符或则和空字符。如果先遇到换行符,下面的if语句就将其替换成,空字符;如果先遇到空字符,else部分便丢弃输入行的剩余字符。
PS:注意区分空字符和空指针(详见CPrimerPlus P335)
空字符是整数类型,但是空指针是指针类型。 两者容易混淆的原因是:它们都可以用数值0表示,但是,从概念上看,两者是不同类型的0。 空字符是一个字符,占1个字节;而空指针是一个地址,通常占4字节。
C11新增的gets_s函数和fgets函数类型,用一个参数限制读入的字符数。
gets_s函数和fgets函数的区别是:
gets_s(),fgets(),gets()之间的选择与对比分析:
如果目标存储区装得下输入行,三个函数都没有问题。注意fgets函数会保留出入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符,但是只要输入行未超过最大字符数,gets_s和gets函数几乎一样,完全可以用gets_s函数替换gets。 如果输入太长,使用gets()不安全,它会擦写现有的程序,存在安全隐患,但是使用gets_s()函数很安全,但是由上面可知超出限定后gets_s()函数可能会中止或者退出程序,所以要知道如何编写特殊的“处理函数”。 由此可见如果输入太长,超过数组可容纳的字符数时,fgets函数最容易使用,而且可以选择不同的处理方法,如果想让程序急促使用输入行中超出的字符可以参考案例二中的处理方法,如果想丢弃初入行的超出字符,可以参考案例三中的处理方法。
鉴于此,fgets()通常是处理特殊情况的最佳选择。
上面案例演示了fgets()函数的一种使用方法:读取整行输入并用空字符替换换行符,或者读取一部分输入,并丢弃其余部分。
既然没有处理这种情况的函数,我们可以创建一个。
char * s_gets(char * st,int n)
{
char * ret_val;
int i = 0;
ret_val = fgets(st,n,stdin);
if(ret_val)//即ret_val != NULL
{
while(st[i] != '\n' && st[i] != '\0')
I++;
if(st[i] == '\n')
st[i] = '\0';
else
while(getchar() != '\n')
continue;
}
return 0;
}
分析:如果fgets函数返回了NULL说明读到文件结尾或出现了错误,s_gets()函数跳过了这个过程。
如果字符串中出现了换行符,那就用空字符替换它,如果字符串中出现了空字符,那就丢弃该输入行中的其余字符,然后返回与fgets()相同的值。
为什么要丢弃过长输入中的余下字符?这是因为输入行中多出来的字符会被留在缓冲区中,成为下一次读取语句的输入,
我们设计的s_gets()函数并不完美,缺陷:遇到不合适的输入时毫无反应,它丢弃多余的字符时既不通知程序也不告诉用户。
使用scanf()和%s转换说明读取字符串。scanf()和gets()或者fgets()的区别在于它们如何缺点字符串的末尾。
案例:在scanf函数中指定字符宽度的用法
#include<stdio.h>
int main()
{
char name1[11],name2[11];
int count;
printf("Please enter 2 names.\n");
count = scanf("%5s %10s",name1,name2);
printf("I read the %d names %s and %s.\n",count.name1,name2);
return 0;
}
结果:
Please enter 2 names.
Portensia Callowit
I read the 2 names Porte and nsia.
分析:scanf第二次读取数据时从上一次调用结束的地方继续读取数据。
PS:
区分空白字符(空格、空行、制表符、换行符)和空字符(’\0‘)
使用方法:只需把字符串的地址作为参数传递给它即可。
该函数在遇到空字符时就停止输出,所以必须确保字符串中有空字符(’\0’),注意不是空白字符,区分两者。
案例:
#include<stdio.h>
int main()
{
char side_a[] = "Side A";
char dont[] = {'W','O','W','!'};
char side_b[] = "Side B";
puts(dont);
return 0;
}
输出:
WOW!Side A
分析:由于dont缺少了一个表示结束的空字符,所以它不是一个字符串,因此puts()不知道在哪里停止,它会一直打印dont后面内存中的内容,知道发现一个空字符为止。