第11章 字符串和字符串函数(2 字符串输入)

发布于 2021-09-08  624 次阅读


11.2 字符串输入

如果想把一个字符串读入程序,首先必须预留存储该字符串的空间,然后用输入函数获取该字符串

11.2.1 分配空间

要做的第1件事是分配,以存储稍后读入的字符串。前面提到过,这意味着必须要为字符串分配足够的空间。不要指望计算机在读取字符串时顺便计算它的长度,然后再分配空间(计算机不会这样做,除非编写一个处理这样事务的函数)。
假设编写了如下代码:
char *name;
scanf("%s",name);
虽然可能会通过编译(编译器很可能给出警告),但是在读入name时,name可能会擦写掉程序中的数据或代码,从而导致程序异常中止。因为scanf()要把信息拷贝至参数指定的地址上,此时该参数是个未初始化的指针,name可能会指向任何地方。
最简单的方法是,在声明时显示指明数组的大小:
char name[81];
现在name是一个已分配块(81字节)的地址。还有一种方法是C库韩式来分配内存。
为字符串分配内存后,便可读入字符串。C库提供了许多读取字符串的函数:scanf()、gets()和fgets().

11.2.2 不幸的gets()函数

在读取字符串时,scanf()和转换说明%s只能读取一个单词。可是在程序中经常要读取一整行输入,而不仅仅是一个单词。gets()函数就用于处理这种情况。gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空白字符使其称为一个C字符串。他经常和puts()函数配对使用,该函数用于显示字符串,并在末尾添加换行符。程序清单演示了这两个函数
//getsputs.c        --使用gets()和puts()
#include<stdio.h>
#define STLEN 81
int main(void)
{
    char words[STLEN];

    puts("Enter a string,please!");
    gets(words);            //典型用法
    printf("%s\n",words);
    puts(words);
    puts("Done.");

    return 0; 
 } 

file

整行输入(除了换行符)都被存储在words,puts(words)和printf("%s\n",words)的效果相同。

file

file

如果输入的字符串过长。会导致缓冲区溢出(buffer owerflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;但是它们擦写掉了程序中其他数据,会导致程序异常终止;或者还有其他情况。为了让输入的字符串容易溢出,把程序中的STLEN设置为5,该程序输出如下:

file

11.2.3 gets()的替代品

过去通常用fgets()来代替gets(),fgets()函数稍微复杂些,在处理输入方面于gets()略有不同。
C11标准新增加的gets_s()函数也可以代替gets().该函数与gets()函数更接近,而且可以替换现有代码中的gets().但是,它是stdio.h输入/输出函数系列中的可选扩展,所以支持C11的编译器也不一定支持它。

1.fgets()函数 (和fputs())

fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题,该函数专门涉及用于处理文件输入,所以一般情况下可能不太好用。
fgets()和fputs()的区别如下:
- fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
- 如果fgets()读到一个换行符,会把它存储在字符串中,这点与gets()不同,gets()会丢弃换行符。
- fgets()函数的第3个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符在stdio.h中。
因为fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与fputs()函数(和puts()类似)配对使用。除非该函数不在字符串末尾添加换行符。fputs()函数的第2个参数指明了它要写入的文件。如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。
//fgets1.c      fgets和fputs
#include<stdio.h>
#define STLEN 14
int main(void)
{
    char words[STLEN];

    puts("Enter a string,please");
    fgets(words,STLEN,stdin);
    printf("Your string twice(puts(),then fputs()):\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;

 } 

file

第1行输入,apple pie,比fgets()读入的整行输入端,因此apple pie\n\0被存储在数组中。所以当puts()显示该字符串时又在末尾添加了换行符,因此apple pie后面有一行空行。因为fputs()不在字符串末尾田间换行符,所以并未打印出空行。
第2行输入,strawberry shortcake,超过了大小的限制,所以fgets()只读入了13个字符,并把strawberry sh\0储存在数组中。puts()函数会在待输出字符串末尾添加一个换行符,而fputs()不会这样做。
fgets()函数返回指向char的指针。如果一切顺利,该函数返回的地址与传入的第1个参数相同。但是,如果函数读到文件结尾,他将返回一个特殊的指针:空指针(null pointer).该指针保证不会指向有效的数据,所以可以用于标识这种特殊情况。在代码中,可以用数字0来代替。不过在C语言中用宏NULL来代替更常见(如果在读入数据时出现某些错误,该函数也返回NULL)
Q:一个简单的循环,读入并显示该用户输入的内容,直到fgets()读到文件结尾或空行(即首字符是换行符)
//fgets2.c      --使用fgets()和fputs()
#include<stdio.h>
#define STLEN 10
int main(void)
{
    char words[STLEN];

    puts("Enter string (empty line to quit):");
    while(fgets(words,STLEN,stdin)!=NULL && words[0]!='\n')
    {
        fputs(words,stdout);
    }
    puts("Done.");

    return 0;
}

file

有意思。虽然STLEN被设置为10,但是该程序似乎处理过长的输入时完全没问题。程序中的fgets()一次读入STLEN-1个字符(该例中为9个字符),所以一开始它只读入了"By the wa",并存储为By the wa\0;接着fputs打印该字符串,并且未换行。然后while循环进入下一轮迭代,fgets()继续从剩余的输入中读入数据,即读入“y, the ge”并存储为y, the ge\0;接着fputs()在刚才打印字符串的这一行接着打印第2次读入的字符串。然后while进入下一轮迭代,fgets()将继续读取输入、fputs()打印字符串,这一过程循环进行,直到读入最后的“tion\n”.fgets()将其存储为tion\n\0,fputs()打印该字符串,由于字符串中的\n,光标被移至下一行开始处。
系统使用缓冲I/O。这意味着用户按下return键之前,输入都被存储在临时存储区(缓冲区)中。按下return键就在输出中增加了一个换行符,并且把整行输入发送给fgets()。对于输出,fputs()把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上。
fgets()存储换行符又好处也有坏处。坏处是你可能并不像把换行符存储在字符串中,这样的换行符会带来一些麻烦。好处是对于存储的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理一行中剩下的字符。
首先,如何处理掉换行符?一个方法是在已存储的字符串中查找换行符,并将其替换成空字符:
while(words[i]!='\n')     //假设\n在words中
    i++;
words[i] = '\0';
其次,如果仍有字符串留在输入行怎么办?一个可行的办法是,如果目标数组装不下一整行输入,就丢弃那些多处的字符:
while(getchar()!='\n')    //读取但不存储输入,包括 \n
    continue;
该程序读取输入行,删除存储在字符串中的换行符,如果没有换行符,则丢弃数组装不下的字符。
//fgets2.c      --使用fgets()和fputs()
#include<stdio.h>
#define STLEN 10
int main(void)
{
    char words[STLEN];
    int i;

    puts("Enter string (empty line to quit):");
    while(fgets(words,STLEN,stdin)!=NULL && words[0]!='\n')
    {
        i = 0;
        while(words[i]!='\n' && words[i] != '\0')
            i++;
        if(words[i]=='\n')
            words[i]='\0';
        else
            while(getchar()!='\n')
                continue;
        puts(words);
    }
    puts("Done.");

    return 0;
}

file

空字符和空指针
从概念上看,空字符('\0')是用于标记C字符串末尾的字符,其对应字符编码是0.由于其他字符的编码不可能是0,所以不可能是字符串的一部分。
空指针(NULL)有一个值,该值不会与任何数据的有效地址对应,通常,函数使用它返回一个有效地址标识某些特殊情况发生,例如遇到文件积为或未能按预期执行。
空字符是整数类型,而空指针是指针类型。两者有时容易混淆的原因是:它们都可以用数值0来标识,但是从概念上看,两者是不同类型的0.另外空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。

2.gets_s函数

C11新增的gets_s()函数和fgets()类似,用一个参数限制读入的字符数。若把上述程序清单中的fgets()换成gets_s(),其他内容不变,那么下面代码将把一行输入中的前9个字符读入words数组中,假设末尾有换行符:
gets_s(words,STLEN);
gets_s()和fgets()的区别如下.
- gets_s()只从标准输入中读取数据,所以不需要第三个参数。
- 如果gets_s()读到换行符,会丢弃它而不是存储它。
- 如果get_s()读到最大字符都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后输入直至督导换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
第2个特性说明,只要输入行未超过最大字符数,gets_s()和gets()几乎一样,完全可以用gets_s()代替gets()。第3个特性说明,要使用这个函数还需要进一步学习。
我们来比较一下gets(),fgets()和gets_s()的适用性。如果目标存储区装的下输入行,三个函数都没问题。但是fgets()会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符。
如果输入行太长会怎样?使用gets()不安全,他会擦写现有的数据,存在安全隐患。gets_s()函数很安全。但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。另外,如果打算让程序继续运行,gets_s会丢弃输入行的其余字符,无论你是否需要。由此可见,当输入太长,超过数组可容纳的字符数时,fget()函数最容易使用,而且可以选择不同的处理方式。如果让程序继续使用输入行中的超出字符,可以参考上上程序做法。如果像丢弃,写处理程序.
当输入与预期不符时,get_s()完全没有fgets()函数方便\灵活。也许这也是get_s()只能作为C的可选扩展库的原因之一。鉴于此,fgets()通常是处理类似情况的最佳选择。

s_gets()函数

fgets()函数的一种用法,如上所示:读取整行输入并用空白字符代替换行符,或者读取一部分输入,并丢弃其余部分。既然没有处理这种情况的标准函数,我们创建一个,在后面的程序中会用得上。
//s_gets()函数
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 ret_val;
}
如果fgets()返回NULL,说明读到文件结尾或出现读取错误,s_gets()函数跳过了这个过程。它模仿前面哪个程序的处理方法,如果字符串中出现换行符,那就用空字符代替它;如果字符串中出现空字符,就丢弃改输入行的其余字符,然后返回与fgets()相同的值。我们在后面的示例中将讨论fgets()函数。
为什么要丢弃过长输入行中的余下字符。这是因为,输入行中多出来的字符会被留在缓冲区中,称为下一次读取语句的输入。例如,如果下一条语句读入的是double类型的值,就可能导致程序崩溃。丢去输入行余下的字符保证了读取语句与键盘的输入同步。
设计的s_gets()函数并不完美,她最严重的缺陷是遇到不合适的输入时毫无反应。它丢弃多余的字符时,既不通知程序也不告诉用户。但是用它替换前面的gets()足够了.

11.2.4 scanf()函数

scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾:scanf()更像是“获取单词”函数,而不是“获取字符串”函数;如果预留的存储区装得下输入行,gets()和fgets()会读取第1个换行符之前的所有字符。scanf()函数有两种方法确认输入结束,无论哪种方法,都从第一个非空白字符作为字符串的开始。如果使用%s转换说明,以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10个字符或读到第一个空白字符停止。

file

前面介绍过,scanf()函数返回一个整数值,该值等于scanf()成功读取的项数或EOF(读到文件结尾时返回EOF)
//scan_str.c
#include<stdio.h>
int main(void)
{
    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;   
} 

file

file

file

第一个输出示例,两个名字的字符个数都为超过字符段的宽度。第2个输出示例,只读入了Applebottham的前10个字符(因为使用了%10s转换说明)。第3个输出示例,portensia的后4个字符nsia被写入了name2中,因为第2次调用scanf()时,从上一次调用结束的地方继续读取数据。在该例子中,读取的仍是portensia中的字母。
根据输入数据的性质,用fgets()读取的从键盘输入的数据更合适。例如,scanf()无法完整读取书名或者歌曲,除非这些名称是一个单词。scanf()的典型用法是读取并转换混合数据类型为某种标准形式。例如,如果输入行包括一种工具名、库存量和单价,就可以用scanf()。否则可能要自己拼凑一个函数处理一些输入检查。如果一次值输入一个单词,用scnaf()也没问题。
scanf()和gets()类似,也存在一些潜在的缺点。如果输入的内容过长,scanf()也会导致数据溢出,不过,在%s转换说明中使用字段宽度可以防止溢出

擦肩而过的概率