C

C 知识量:16 - 74 - 317

11.2 字符串输入><

分配空间- 11.2.1 -

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

在输入字符串时,计算机不会自动计算输入字符串的大小,以下方法是错误的:

char *name;
scanf("%s", name);

此时,name是一个未初始化的指针,它可能指向任何地方,如果把输入储存到指针指向的地址,可能会擦写掉其他数据或代码。应当通过指定一个明确大小的数组来储存输入的字符串:

char name[100];
scanf("%s", name);

gets()函数- 11.2.2 -

C语言提供了许多用于读取字符串的函数,例如:scanf()、gets()、fgets()函数等。

gets()函数简单易用,调用时,它会读取整行输入,直至遇到换行符,这时,丢掉换行符,储存其余字符,并在这些字符的末尾添加一个空字符(\0),使其成为一个字符串。

gets()函数经常与puts()函数配对使用,puts()函数用于显示字符串,并在末尾添加换行符,以下是一个示例:

#include <stdio.h>

int main(void) {
    char name[100];
    puts("Please enter your name.");
    gets(name);
    puts("Your name is:");
    puts(name);
    system("pause");
    return 0;
}

运行结果为:

Please enter your name.
Jeff
Your name is:
Jeff

以上代码在编译或显示时可能会给出警告,说明gets()函数不安全,因为它不能检查数组是否装得下输入的字符串。gets()函数只知道数组的开始处,并不知道数组中有多少个元素。如果我们输入的字符串超过了数组的声明长度(100),就会导致缓冲区溢出。因此,在C11标准中废除了gets()函数,出于兼容性的考虑,一些编译器还是支持的。但是,不应该继续使用gets()函数了。

fgets()函数- 11.2.3 -

目前,可选择的gets()函数的替代品有fgets()函数和gets_s()函数。

fgets()函数通过第2个参数限制读入的字符数来解决溢出问题。其与gets()的区别如下:

  • fgets()的第2个参数指明了读入的字符最大数量。如果设为n,那么fgets()将能够读入n-1个字符,或者在读到第一个换行符时终止。

  • 如果fgets()读到一个换行符,会将换行符储存在字符串中,而gets()会直接丢弃换行符。

  • fgets()函数的第3个参数指明要读入的文件。如果要读入从键盘输入的数据,则以stdin(标准输入)作为参数,该表示法定义在stdio.h中。

fgets()函数通常与fputs()函数配对使用,fputs()函数的第2个参数指明要写入的文件。如果要显示在显示器上,应当使用stdout(标准输出)作为参数。以下是一个示例:

#include <stdio.h>

int main(void) {
    char name[6];
    //第一次输入
    puts("Please enter your name.");
    fgets(name, 6, stdin);
    puts("Your name is:");
    puts(name);
    fputs(name, stdout);
    //第二次输入
    puts("Please enter another name.");
    fgets(name, 6, stdin);
    puts("Another name is:");
    puts(name);
    fputs(name, stdout);
    system("pause");
    return 0;
}

运行结果为:

Please enter your name.
Jeff
Your name is:
Jeff

Jeff
Please enter another name.
Abraham
Another name is:
Abrah
Abrah

在以上代码运行时:

  • 第1次输入Jeff,没有超过fgets()函数设定的长度,所以“Jeff\n\0”会被储存在数组name中。使用puts()函数打印时,会自动在打印完毕后添加一个换行符,而此时name的值中含有一个换行符,因此第一次打印输入值时,Jeff下面会有一个空行。而使用fputs()函数第二次打印输入值时,因为fputs()函数不在字符串末尾添加换行符,只是如实打印字符串内容,因此,第二个Jeff下面没有空行。

  • 第2次输入Abraham,因为超过了fgets()函数设定的长度,所以只有“Abrah\0”被存储到数组name中。puts()函数打印后会添加换行,而fputs()函数打印后就不换行了。

fputs()函数返回指向char的指针。通常,该函数返回的地址与传入的第1个参数相同。但是,如果函数读到了文件结尾,它将返回一个特殊的指针:空指针,而这个空指针保证不会指向有效的数据。以下是一个在循环体中使用fgets()和fputs()函数的示例:

#include <stdio.h>

int main(void) {
    char name[10];
    puts("Please enter strings(empty line to quit):");
    while (fgets(name, 10, stdin) != NULL && name[0] != '\n')
        fputs(name, stdout);
    puts("Done.");
    system("pause");
    return 0;
}

运行结果为:

Please enter strings(empty line to quit):
Hello!I am Jeff.
Hello!I am Jeff.
I like programming.
I like programming.

Done.

虽然fgets()函数的第2个参数被设置为10,但是从结果看,超长的输入显示并没有问题。原因是:fgets()函数和fputs()函数都在while循环语句中,fgets()函数一次可以接受9个字符,第一次输入后,实际读取的是“Hello!I a”,并将“Hello!I a\0”存储到数组name中,接着使用fputs()打印出来,而且没有换行。在第2次循环中,fgets()继续从剩余的输入中读取了“m Jeff.\n”,然后将“m Jeff.\n\0”存储并打印,由于字符串含有“\n”,光标移到下一行。后面的处理方法一样。

之所以会如此,是因为系统使用缓冲的I/O,在按下回车键前,输入都被储存在临时缓冲区中。按下回车键后,输入中增加一个换行符,同时将整行发送给fgets()函数。而在输出时,fputs()函数将字符发送给另一个缓冲区,遇到换行符时,缓冲区中的内容会被发送至屏幕。

因为fgets()函数有存储换行符的特性,如果要去掉换行符,一种方法是在字符串中查找并替换成空字符:

while (name[i] != '\n')
    i++;
name[i] = '\0';

如果要避免缓冲区中的剩余输入字符串的影响,可以使用循环丢弃它们:

while (getchar() != '\n')
    continue;

下面的示例用于读取输入字符串,删除储存在字符串中的换行符,如果没有换行符,则丢弃数组装不下的字符(输入缓冲区中剩余的字符):

#include <stdio.h>

int main(void) {
    char name[10];
    int i;
    puts("Please enter strings(empty line to quit):");
    while (fgets(name, 10, stdin) != NULL && name[0] != '\n') {
        i = 0;
        while (name[i] != '\n' && name[i] != '\0')
            i++;
        if (name[i] == '\n')
            name[i] = '\0';
        else
            while (getchar() != '\n')
                continue; //丢弃缓冲区剩余字符
        puts(name);
    }
    puts("Done.");
    system("pause");
    return 0;
}

运行结果为:

Please enter strings(empty line to quit):
Hello
Hello
My name is Jeff
My name i
I like programming
I like pr
Welcome to pnotes.cn
Welcome t

Done.

以上代码遍历字符串,直至遇到换行符或空字符。如果先遇到换行符,就将其替换成空字符;如果先遇到空字符,便丢弃输入行剩余字符。

需要注意的是:

  • 空指针(NULL)有一个值,但不会与任何数据的有效地址对应。函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到了文件结尾或未能按预期执行等。

  • 空字符是整数类型,而空指针是指针类型。它们都可以用数值0来表示。但是,空字符是一个字符,占1个字节;而空指针是一个地址,一般占4个字节。

gets_s()函数- 11.2.4 -

在C11标准中新增了gets_s()函数,但是,它是stdio.h中可选扩展,因此,支持C11的编译器也不一定支持它。

gets_s()函数也用一个参数限制输入的字符数。例如:

gets_s(name, 10);

gets_s()与fgets()的区别如下:

  • gets_s()只从标准输入中读取数据,所以不需要第3个参数。

  • 如果gets_s()读到换行符,会丢弃它而不是储存它。

  • 如果gets_s()读到最大字符数都没有读到换行符,会执行以下操作:首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”或用户选择的其他函数,可能会中止或退出程序。

由以上区别可以看出,当输入太长时,gets_s()需要编写特殊的处理函数,而且会丢弃剩余的输入字符,即使剩余的字符也是有用的,完全没有fgets()函数方便、灵活。

scanf()函数- 11.2.5 -

scanf()函数使用%s转换说明来读取字符串。在读取时,从第1个非空白字符开始,以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束,因此,读取的字符串中不会包含空白字符。此外,还可以指定字段宽度,例如对于%10s,scanf()函数将读取10个字符或读到第1个空白字符停止,这取决于先满足哪个条件。

scanf()函数返回一个整数值,该值等于scanf()成功读取的项数或EOF(当读到文件结尾时会返回EOF)。以下是一个示例:

#include <stdio.h>

int main(void) {
    char str1[10];
    char str2[10];
    int count;
    puts("Please enter 2 strings");
    count = scanf("%5s %10s", str1, str2);
    printf("Get %d strings, str1 is %s and str2 is %s\n", count, str1, str2);
    puts("Please enter 2 strings again");
    count = scanf("%5s %10s", str1, str2);
    printf("Get %d strings, str1 is %s and str2 is %s\n", count, str1, str2);
    system("pause");
    return 0;
}

运行结果为:

Please enter 2 strings
Jeff Abraham
Get 2 strings, str1 is Jeff and str2 is Abraham
Please enter 2 strings again
University name
Get 2 strings, str1 is Unive and str2 is rsity

以上代码中,第1次输入的两个字符串都没有超出设定的字段长度,均正常显示。第2次输入时,第一个字符串超出了长度,因此,str1只读取了“Unive”,剩下的部分写入了str2,因为遇到了空格,只读取了“rsity”。

从以上结果可以看出,scanf()无法读取内含空格的字符串,这种情况下使用fgets()更合适。同时,为了防止输入溢出,使用scanf()时最好设定字段宽度。