在读取字符串时,scanf()和转换说明%s只能读取一个单词。可是在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。它经常和 puts()函数配对使用,该函数用于显示字符串,并在末尾添加换行符。
/*程序1.1-- 使用 gets() 和 puts() */
#include
#define LEN 30
int main(void)
{
char words[LEN];
gets(words); // 典型用法
printf("Your string twice:\n");
printf("%s\n", words);
puts(words);
return 0;
}
整行输入(除了换行符)都被储存在 words 中,gets()会丢弃换行符’\n’,但是puts()会在字符串末尾添加‘\n’,puts(words)和printf("%s\n,words")的效果相同。
每次运行这个程序,编译器在输出中插入了一行警告消息。这是怎么回事?问题出在 gets()唯一的参数是 words,它无法检查数组是否装得下输入行。在gets()的参数中,数组名会被转换成该数组首元素的地址,因此,gets()函数只知道数组的开始处,并不知道数组中有多少个元素。
如果输入的字符串过长,会导致缓冲区溢出(buffer overflow),即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常中止;或者还有其他情况(比如程序试图访问未分配的内存),有些人通过系统编程,利用gets()插入和运行一些破坏系统安全的代码。
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入。
fgets()和gets()的区别如下:
1、fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。
2、如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
3、fgets()函数的第3 个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中。
因为 fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与 fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第2个参数指明它要写入的文件。如果要显示在计算机显示器上,应使用stdout(标准输出)作为该参数。
/*程序2.1--- 使用 fgets() 和 fputs() */
#include
#define LEN 10
int main(void)
{
char words[LEN];
fgets(words, LEN, stdin);
printf("Your string twice (puts(), then fputs()):\n");
puts(words);
fputs(words, stdout);
puts("Enter another string, please.");
fgets(words, LEN, stdin);
printf("Your string twice (puts(), then fputs()):\n");
puts(words);
fputs(words, stdout);
return 0;
}
fputs()函数返回指向char的指针。如果一切进行顺利,该函数返回的地址与传入的第个参数相同。但是,如果函数读到文件结尾,它将返回一个(null pointer)特殊的指针:空指针,该指针保证不会指向有效的数据,所以可用于标识这种特殊情况。
下面的程序2.2演示了一个简单的循环,读入并显示用户输入的内容,直到fgets()读到文件结尾或空行(即,首字符是换行符)。
/*程序2.2-- 使用 fgets() 和 fputs() */
#include
#define LEN 10
int main(void)
{
char words[LEN];
while (fgets(words, LEN, stdin) != NULL && words[0] != '\n')
fputs(words, stdout);
return 0;
}
下面是该程序的输出示例:
123456789asdfghjklzxc
123456789asdfghjklzxc
有意思,虽然LEN被设置为10,但是该程序似乎在处理过长的输入时完全没问题。程序中的fgets()一次读入 LEN - 1 个字符(该例中为 9 个字符)。所以,一开始它只读入了“123456789”,并储存为123456789\0;接着fputs()打印该字符串,而且并未换行。然后while循环进入下一轮迭代,fgets()继续从剩余的输入中读入数据,即读入“asdfghjkl”并储存为asdfghjkl\0;接着fputs()在刚才打印字符串的这一行接着打印第 2 次读入的字符串。然后while 进入下一轮迭代,fgets()继续读取输入、fputs()打印字符串,这一过程循环进行,直到读入最后的“zxc\n”。fgets()将其储存为zxc\n\0, fputs()打印该字符串,由于字符串中的\n,光标被移至下一行开始处。
程序2.3在程序2.2的基础上添加了一部分测试代码。该程序读取输入行,删除储存在字符串中的换行符,如果没有换行符,则丢弃数组装不下的字符。
/* 程序2.3 -- 使用 fgets() */
#include
#define LEN 10
int main(void)
{
char words[LEN];
int i;
while (fgets(words, LEN, stdin) != NULL && words[0] != '\n')
{
i = 0;
while (words[i] != '\n' && words[i] != '\0')
i++;
if (words[i] == '\n')
words[i] = '\0';
else // 如果word[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
程序中出现了空字符和空指针。从概念上看,两者完全不同。空字符(或’\0’)是用于标记C字符串末尾的字符,其对应字符编码是0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分。空指针(或NULL)有一个值,该值不会与任何数据的有效地址对应。通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行。空字符是整数类型,而空指针是指针类型。两者有时容易混淆的原因是:它们都可以用数值0来表示。但是,从概念上看,两者是不同类型的0。
另外,空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。
gets_s()函数和fgets()类似,用一个参数限制读入的字符数。假设把下面的代码将把一行输入中的前9个字符读入words数组中,假设末尾有换行符:
gets_s(words, LEN);
gets_s()与fgets()的区别如下:
1、gets_s()只从标准输入中读取数据,所以不需要第3个参数。
2、如果gets_s()读到换行符,会丢弃它而不是储存它(和gets()函数一样)。
3、如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或你选择的其他函数),可能会中止或退出程序。
第2个特性说明,只要输入行未超过最大字符数,gets_s()和gets()几乎一样,完全可以用gets_s()替换gets()。第3个特性说明,要使用这个函数还需要进一步学习。
**
**
我们来比较一下 gets()、fgets()和 gets_s()的适用性。如果目标存储区装得下输入行,3 个函数都没问题。但是fgets()会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符。
如果输入行太长会怎样?使用gets()不安全,它会擦写现有数据,存在安全隐患。gets_s()函数很安全,但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。另外,如果打算让程序继续运行,gets_s()会丢弃该输入行的其余字符,无论你是否需要。由此可见,当输入太长,超过数组可容纳的字符数时,fgets()函数最容易使用,而且可以选择不同的处理方式。如果要让程序继续使用输入行中超出的字符,可以参考程序2.2中的处理方法。如果想丢弃输入行的超出字符,可以参考程序2.3中的处理方法。
所以,当输入与预期不符时,gets_s()完全没有fgets()函数方便、灵活。也许这也是gets_s()只作为C库的可选扩展的原因之一。鉴于此,fgets()通常是处理类似情况的最佳选择。