2月8日 C Primer Plus学习
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()函数
gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。它经常和 puts()函数配对使用,该函数用于显示字符串,并在末尾添加换行符。程序清单11.6中演示了这两个函数的用法。
程序清单11.6
/* getsputs.c -- 使用 gets() 和 puts() */
#include <stdio.h>
#define STLEN 81
int main(void)
{
char words[STLEN];
puts("Enter a string, please.");
gets(words); // 典型用法
printf("Your string twice:\n");
printf("%s\n", words);
puts(words);
puts("Done.");
return 0;
}
如果输入的字符串过长,会导致缓冲区溢出,即多余的字符超出了指定的目标空间。
如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;
如果它们擦写掉程序中的其他数据,会导致程序异常中止;或者还有其他情况。
gets()函数的不安全行为造成了安全隐患。过去,有些人通过系统编程,利用gets()插入和运行一些破坏系统安全的代码。
不久,C 编程社区的许多人都建议在编程时摒弃 gets()。
制定 C99 标准的委员会把这些建议放入了标准,承认了gets()的问题并建议不要再使用它。
尽管如此,在标准中保留gets()也合情合理,因为现有程序中含有大量使用该函数的代码。而且,只要使用得当,它的确是一个很方便的函数。
好景不长,C11标准委员会采取了更强硬的态度,直接从标准中废除了 gets()函数。
既然标准已经发布,那么编译器就必须根据标准来调整支持什么,不支持什么。
然而在实际应用中,编译器为了能兼容以前的代码,大部分都继续支持gets()函数。
11.2.3 gets()的替代品
过去通常用fgets()来代替gets()。C11标准新增的gets_s()函数也可代替gets()。该函数与 gets()函数更接近,而且可以替换现有代码中的gets()。
但是,它是stdio.h输入/输出函数系列中的可选扩展,所以支持C11的编译器也不一定支持它。
1.fgets()函数(和fputs())
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。fgets()和 gets()的区别如下。
fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
fgets()函数的第3 个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中。
因为 fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与 fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第2个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。
再次提醒读者注意, puts()函数会在待输出字符串末尾添加一个换行符,而fputs()不会这样做。
fputs()函数返回指向 char的指针。如果一切进行顺利,该函数返回的地址与传入的第 1 个参数相同。但是,如果函数读到文件结尾,它将返回一个特殊的指针:空指针。该指针保证不会指向有效的数据,所以可用于标识这种特殊情况。在代码中,可以用数字0来代替,不过在C语言中用宏NULL来代替更常见(如果在读入数据时出现某些错误,该函数也返回NULL)。
系统使用缓冲的I/O。这意味着用户在按下Return键之前,输入都被储存在临时存储区(即,缓冲区)中。按下Return键就在输入中增加了一个换行符,并把整行输入发送给fgets()。对于输出,fputs()把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上。
fgets()储存换行符有好处也有坏处。坏处是你可能并不想把换行符储存在字符串中,这样的换行符会带来一些麻烦。好处是对于储存的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理一行中剩下的字符。
首先,如何处理掉换行符?一个方法是在已储存的字符串中查找换行符,并将其替换成空字符:
while (words[i] != '\n') // 假设\n在words中
i++;
words[i] = '\0';
其次,如果仍有字符串留在输入行怎么办?一个可行的办法是,如果目标数组装不下一整行输入,就丢弃那些多出的字符:
while (getchar() != '\n') // 读取但不储存输入,包括\n
continue;
从概念上看,空字符和空指针两者完全不同。空字符(或'\0')是用于标记C字符串末尾的字符,其对应字符编码是 0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分。
空指针(或NULL)有一个值,该值不会与任何数据的有效地址对应。
通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行。
空字符是整数类型,而空指针是指针类型。两者有时容易混淆的原因是:它们都可以用数值0来表示。但是,从概念上看,两者是不同类型的0。另外,空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。
2.gets_s()函数
C11新增的gets_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。
gets_s()只从标准输入中读取数据,所以不需要第3个参数。
如果gets_s()读到换行符,会丢弃它而不是储存它。
如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
只要输入行未超过最大字符数,gets_s()和gets()几乎一样,完全可以用gets_s()替换gets()。
如果目标存储区装得下输入行,3 个函数都没问题。但是fgets()会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符。
如果输入行太长会怎样?使用gets()不安全,它会擦写现有数据,存在安全隐患。gets_s()函数很安全,但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。
另外,如果打算让程序继续运行,gets_s()会丢弃该输入行的其余字符,无论你是否需要。由此可见,当输入太长,超过数组可容纳的字符数时,fgets()函数最容易使用,而且可以选择不同的处理方式。
所以,当输入与预期不符时,gets_s()完全没有fgets()函数方便、灵活。也许这也是gets_s()只作为C库的可选扩展的原因之一。鉴于此,fgets()通常是处理类似情况的最佳选择。
3.s_gets()函数
如果 fgets()返回 NULL,说明读到文件结尾或出现读取错误,s_gets()函数跳过了这个过程。
如果字符串中出现换行符,就用空字符替换它;
如果字符串中出现空字符,就丢弃该输入行的其余字符,然后返回与fgets()相同的值。
输入行中多出来的字符会被留在缓冲区中,成为下一次读取语句的输入,所以要丢弃过长输入行中的余下字符。
例如,如果下一条读取语句要读取的是 double 类型的值,就可能导致程序崩溃。丢弃输入行余下的字符保证了读取语句与键盘输入同步。
我们设计的 s_gets()函数并不完美,它最严重的缺陷是遇到不合适的输入时毫无反应。它丢弃多余的字符时,既不通知程序也不告知用户。但是,用来替换前面程序示例中的gets()足够了。
11.2.4 scanf()函数
scanf()和gets()或fgets()的区别在于它们如何确定字符串的末尾:
scanf()更像是“获取单词”函数,而不是“获取字符串”函数;如果预留的存储
区装得下输入行,gets()和fgets()会读取第1个换行符之前所有的字符。
scanf()函数有两种方法确定输入结束。无论哪种方法,都从第1个非空白字符作为字符串的开始。如果使用%s转换说明,以下一个空白字符(空行、
空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10 个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)。

scanf()函数返回一个整数值,该值等于scanf()成功读取的项数或EOF(读到文件结尾时返回EOF)。
根据输入数据的性质,用fgets()读取从键盘输入的数据更合适。
例如, scanf()无法完整读取书名或歌曲名,除非这些名称是一个单词。
scanf()的典型用法是读取并转换混合数据类型为某种标准形式。
例如,如果输入行包含一种工具名、库存量和单价,就可以使用scanf()。否则可能要自己拼凑一个函数处理一些输入检查。
如果一次只输入一个单词,用scanf()也没问题。
scanf()和gets()类似,也存在一些潜在的缺点。
如果输入行的内容过长, scanf()也会导致数据溢出。不过,在%s转换说明中使用字段宽度可防止溢出。
浙公网安备 33010602011771号