11.2 字符串输入

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

11.2.1 创建存储空间

要做的第一件事是建立一个空间以存放读入的字符串。正如前面提过的,这意味着需要分配足够大的存储区来存放希望读入的字符串。不要指望计算机读的时候会先计算字符串的长度,然后为字符串分配空间。计算机是不会这么做的(除非您写了一个函数命令它这么做)。例如,假定您尝试写了下面的语句:

广告:个人专属 VPN,独立 IP,无限流量,多机房切换,还可以屏蔽广告和恶意软件,每月最低仅 5 美元

阅读 ‧ 电子书库

这可能会通过编译器,但是在读入name的时候,name会覆盖程序中的数据和代码,并可能导致程序异常终止。这是因为scanf()把信息复制到由参数给定的地址中,而在这种情况下,参数是个未初始化的指针;name可能指向任何地方。绝大多数程序员认为这很搞笑,但仅限于这出现在别人的程序中时。

最简单的方法就是在声明中明确指出数组大小:

阅读 ‧ 电子书库

现在name是一个已分配的81字节存储块的地址。另外一个方法就是使用C库里分配存储空间的函数,这一点会在第12章讨论。

为字符串预留空间后,就可以读取字符串了。C库提供了三个读取字符串的函数:scanf() 、gets()和fgets()。我们先讨论最常用的gets()。

11.2.2 gets()函数

gets() (代表get string)函数对于交互式程序非常方便。它从系统的标准输入设备(通常是键盘)获得一个字符串。因为字符串没有预定的长度,所以gets()需要知道输入何时结束。解决办法是读字符串直到遇到一个换行字符(\n) ,按回车键可以产生这个字符。它读取换行符之前(不包括换行符)的所有字符,在这些字符后添加一个空字符(\0) ,然后把这个字符串交给调用它的程序。它将读取换行符并将其丢弃,这样下一次读取就会在新的一行开始。程序清单11.4给出了一个使用gets()的简单例子。

程序清单11.4 name1.c程序

阅读 ‧ 电子书库

阅读 ‧ 电子书库

下面是一个运行示例:

阅读 ‧ 电子书库

程序清单11. 4接受并存储最多80个字符(包括空格)的任何名字(记住为数组里的\0预留空间)。注意到希望gets()改变调用函数中的某个变量(name) ,也就是说应当使用一个地址作为参数;当然,数组名正是一个地址。

gets()函数的使用可以比前面的例子更为复杂,请参见程序清单11.5。

程序清单11.5 name2.c程序

阅读 ‧ 电子书库

下面是一个交互的例子:

阅读 ‧ 电子书库

gets()函数通过两种方式获得输入:

● 它使用一个地址把字符串赋予name。
● gets()的代码使用return关键字返回字符串的地址,程序把这个地址分配给ptr。注意到ptr是一个char指针,这意味着gets()必须返回一个指向char的指针值。

ANSI C要求stdio. h头文件包括gets()的函数原型。您不需要亲自声明这个函数,只须记住包含这个头文件即可。但是一些C的旧版本要求您提供gets()的函数声明。

gets()函数的构造如下:

阅读 ‧ 电子书库

这个函数头说明gets()返回一个指向char的指针。请注意gets()返回的指针与传递给它的是同一个指针。输入字符串只有一个备份,它放在作为函数参数传递过来的地址中,因此程序清单11. 5中的ptr最后也指向name数组。gets()函数实际的构造更复杂一点,因为它有两个可能的返回值。如果一切都顺利,它返回的是读入字符串的地址,正如我们上面所说。如果出错或如果gets()遇到文件结尾,它就返回一个空(或0)地址。这个空地址被称为空指针,并用stdio. h里定义的常量NULL来表示。因此gets()中还加入了一些错误检测,这使它可以很方便地以如下形式使用:

阅读 ‧ 电子书库

这样的指令使您既可以检查是否到了文件结尾,又可以读取一个值。如果遇到了文件结尾,name中什么也不会读入。这种一举两得的方法就比getchar()函数所采用的方法简洁得多,getchar()只返回一个值而没有参数:

阅读 ‧ 电子书库

附带提一下,不要混淆空指针和空字符。空指针是一个地址,而空字符是一个char类型的数据对象,其值为0。数值上两者都可以用0表示,但是它们的概念不同:NULL是一个指针,而0是一个char类型的常量。

11.2.3 fgets()函数

gets()的一个不足是它不检查预留存储区是否能够容纳实际输入的数据。多出来的字符简单地溢出到相邻的内存区。fgets()函数改进了这个问题,它让您指定最大读入字符数。由于fgets()是为文件I/O而设计的,在处理键盘输入时就不如gets()那么方便。fgets()和gets()有三方面不同:

● 它需要第二个参数来说明最大读入字符数。如果这个参数值为n, fgets()就会读取最多n-1个字符或者读完一个换行符为止,由这二者中最先满足的那个来结束输入。
● 如果fgets()读取到换行符,就会把它存到字符串里,而不是像gets()那样丢弃它。
● 它还需要第三个参数来说明读哪一个文件。从键盘上读数据时,可以使用stdin(代表standard input)作为该参数,这个标识符在stdio.h中定义。

程序清单11.6使用fgets()代替了程序清单11.5里的gets()。

程序清单11.6 name3.c程序

阅读 ‧ 电子书库

下面是一个输出示例,它显示了fgets()的一个不足之处:

阅读 ‧ 电子书库

问题在于fgets()把换行符存储到字符串里,这样每次显示字符串时就会显示换行符。本章后面“其他字符串函数”小节的结尾将会介绍如何用strchr()来定位和删除换行符。

由于gets()不检查目标数组是否能够容纳输入,所以很不安全。的确,几年前就有人注意到一些UNIX操作系统代码使用gets() ,于是他们利用这个弱点,用很长的输入覆盖操作系统的代码,从而发明了在UNIX网络上传播的“蠕虫(worm) ”病毒。那些系统代码后来被不使用gets()的代码所代替。因此对于重要的编程,应该使用fgets()而不是gets(),但本书使用了更随便的做法。

11.2.4 scanf()函数

前面您已经使用了带有%s格式的scanf()函数来读入一个字符串。scanf()和gets()主要的差别在于它们如何决定字符串何时结束。scanf()更基于获取单词(get word)而不是获取字符串(get string) ;而gets()函数,正如您所看到的,会读取所有的字符,直到遇到第一个换行符为止。scanf()使用两种方法决定输入结束。无论哪种方法,字符串都是以遇到的第一个非空白字符开始。如果使用%s格式,字符串读到(但不包括)下一个空白字符(比如空格、制表符或换行符)。如果指定了字段宽度,比如%10s, scanf()就会读入10个字符或直到遇到第一个空白字符,由二者中最先满足的那一个终止输入(请参见图11.3)。

阅读 ‧ 电子书库

图11.3 字段宽度和scanf()

回忆一下,scanf()函数返回一个整数值,这个值是成功读取的项目数;或者当遇到文件结束时返回一个EOF。

程序清单11.7举例说明了指定字段宽度时scanf()的工作情况。

程序清单11.7 scan_str.c程序

阅读 ‧ 电子书库

下面是三次运行的结果:

阅读 ‧ 电子书库

在第一个例子中,两个名字都小于允许的大小。在第二个例子中,由于使用了%10s格式,Applebottham只有前10个字符被读取。在第三个例子中,Portensia的后4个字母被读到name2中,这是因为第二次调用scanf()时,它在第一个调用结束的地方继续开始读输入数据。在这个例子中,仍从单词Portensia中间开始读取。

根据所需输入的特点,用gets()从键盘读取文本可能要更好,因为它更容易被使用、更快,而且更简洁。scanf()主要用于以某种标准形式输入的混合类型数据的读取和转换。例如,如果每一个输入行都包括一种工具的名称、库存数量和单价,您就可以使用scanf() ;否则您必须在函数中自己处理输入错误的检测。如果希望一次只输入一个单词,最好使用scanf()。

下面我们讨论字符串的显示方法。