在读取字符串时,scanf()和转换说明%s只能读取一个单词,可是在程序中经常要读取一整行输入,而不仅仅是一个单词。

gets()函数:

     它读取整行输入,直到遇到换行符,然后丢弃换行符,储存其他字符,并在这些字符的末尾添加一个空字符使其成为一个C字符串。它经常和puts()配对使用,该函数用于显示字符,并在末尾添加一个换行符。

main()
{
  char words[10];
  puts("Enter a string,please!");
  gets(words);
  printf("%s\n",words);
  puts(words);
  return 0;
}

这段代码运行在一些编译器里会警告,不安全,为什么呢?

问题出在gets()唯一的参数是words,它无法检查数组是否装得下输入行。如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常终止;或者还有其他情况。

C11标准委员会直接废除了gets()函数。

gets()的替代品--->fgets()( 和fputs() )

fgets()函数通过第2个参数限制读入的字符来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。fgets()和gets()的区别如下。

  • fgets()函数的第二个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
  • 如果fgets()读到一个换行符,会把它存储在字符串中,这点与gets()不同,gets()会丢弃换行符。

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

#define STLEN 14
main()
{
  char words[STLEN];
  puts("Enter a string,please");
  fgets(words,STLEN,stdin);

  fputs(words,stdout);
  return 0;
}

输出示例: 

strawberry shortcake

strawberry sh

 

strawberry shortcake 超过了大小的限制,所以fgets()只读入了13个字符并把strawberry sh\0储存在数组中。再次提醒读者注意,puts()函数会在待输入字符串末尾添加一个换行符,而fputs()不会这样做。

  fputs()函数返回指向char 的指针。如果一切进行顺利,该函数返回的地址与传入的第1个参数相同。但是,如果函数读到文件末尾,它将返回一个特殊的指针:空指针(null pointer)。该指针保证会指向有效的数据,所以可用于标识这种特殊情况。在代码中,可以用数字0来代替,不过在C语言中用宏NULL来代替更常见(如果在读入数据时出现某些错误,该函数也返回NULL。

看下面一段代码:

18.8程序:
#define STLEN 10;
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

also returns a null pointer if it 

also returns a null pointer if it

ancounters end-of-file.

ancounters end-of-file.

有意思,虽然STLEN被设置为10,但是该程序似乎在处理过长的输入时完全没问题。程序中的fgets()一次读入STLEN-1个字符。所以,一开始它值读入了“By the wa”,并储存为By the wa\0;接着fputs()打印该字符串,而且并未换行。然后while 进入下一轮迭代fgets()从剩余的输入中读入数据,即读入“y,the ge”并存储为y,the ge\0;接着fputs()在刚才打印字符串的这一行接着打印第2次读入的字符串,这一过程循环进行,直到读入最后的"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;

程序18.9在18.8的基础上添加了一些测试代码,该程序读取输入行,删除存储在字符串中的换行符,如果没有换行符,则丢弃数组装不下的字符。

18.9程序:
#define STLEN 10;
main()
{
  char words[STLEN];
  int i;
  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 //如果words[i]=='0'则执行这部分代码
    while(getchar()!='\n')
        continue;
  puts(words);
  return 0;
}

while (words[i]!='\n' && words[i]!='\0')

   i++;

遍历字符串,直至遇到换行符或空字符。如果先遇到换行符,下面的if语句就将其替换成空字符,如果先遇到空字符,else 部分便丢弃输入行的剩余字符。下面是该程序的输出示例:

This

This

program seems

program s

unwilling to accept long lines

unwilling 

But it doesn't get stuck on long 

But it do 

lines either

lines eit

空字符和空指针

程序18.9中出现了空字符和空指针。从概念上看,两者完全不同。空字符(或'\0')是用于标记C字符串末尾的字符,其对应字符编码是0.由于其他字符的编码不可能是0,所以不可能是字符串中的一部分。

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

  空字符是整数类型,而空指针是指针类型,两者有时容易混淆的原因是:他们都可以用数值0来表示,但是,从概念上看,两者是不同类型的0.另外,空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。