字符串输入
字符串输入首先要考虑的是存储位置和存储空间大小。
例子:char *name; scanf("%s", name); 指针未初始化,可能指向内存中的任意地方,字符串读入的时候有可能覆盖内存中的关键数据,造成程序或机器崩溃。
char name[81]; 这种方式显式声明空间并进行初始化。
gets()函数
scanf("%s", str)函数读入单个word。而gets()可以读取一整行的字符串。它通过换行符读取整行,丢弃换行符,存储剩余的字符,添加空字符('\0')以创建C字符串。一般和puts()搭配使用。
示例:
#include<stdio.h>
#define STLEM 81
int main(void)
{
char words[STLEM];
puts("Enter a string, please.");
gets(words);
printf("Your string twice:\n");
printf("%s\n", words);
puts(words);
puts("Done.");
return 0;
}
⚠️:gets函数的问题(int puts(const char *);):虽然在定义字符串的时候声明了大小为81,但gets函数无法阻止输入超过大小的字符串长度,编译运行时将报错"warning: this program uses gets(), which is unsafe."
gets函数只接收一个参数,char类型的指针。没有其他参数限定用户输入的字符串长度。gets()只知道数组的开始位置,却不知道数组的长度。
如果输入字符串太长,则会出现缓冲区溢出,这意味着多余的字符溢出了指定的目标。 额外的字符可能只是进入未使用的内存并且没有立即出现问题,或者它们可能会覆盖程序中的其他数据,但这些肯定不是唯一的可能性。
C99已经不建议使用该函数,并建议将gets()从标准库中移除,C11已经将gets()从标准中移除。但C的标准只是要求编译器厂商必须符合所要求的标准,而不是限制编译器厂商不能做什么。所以,主流的编译器依然支持gets()函数已确保向后兼容。
gets()的替代品fgets()
为了解决gets()函数容易引起的缓冲区溢出问题,出现了fgets()函数。char *fgets(char * __restrict, int, FILE *);
fgets()与gets的主要不同点为:
- fgets()有第二个参数,是一个int类型,限定输入的字符串最大长度。
- fgets()读取换行符('\n'),并保存在字符串中,而不是像gets(),丢弃换行符。
- fgets()有第三个参数,默认为文件输入。也可以读取键盘输入,参数为stdin。
示例:
#include<stdio.h>
#define STLEN 14
int main(void)
{
char words[STLEN];
puts("Enter a string, please.");
fgets(words, STLEN, stdin);
printf("Your string twice (puts(), then fputs()):\n");
puts(words);
fputs(words, stdout);
puts("Enter another string, please.");
fgets(words, STLEN, stdin);
printf("Your string twice (puts(), then fputs()):\n");
puts(words);
fputs(words, stdout);
puts("Done.");
return 0;
}
输出:
Enter a string, please.
apple pie
Your string twice (puts(), then fputs()):
apple pie
apple pie
Enter another string, please.
strawberry shortcake
Your string twice (puts(), then fputs()):
strawberry sh
strawberry shDone.
解读:
- 首先输入"apple pie",字符串长度小于14,使用fgets()时会保留换行符。使用puts()输出时,会额外添加一个换行符(因为gets()丢弃了换行符),则puts()会输入"\n\0\n"。fputs()会按words实际保存的进行输出。因此,两个"apple pie"之间的空行就是puts()函数额外添加的。
- 其次输入"strawberry shortcake",字符串长度超过14,则仅保留13个有效当前输入(额外一个留给'\0')。故使用puts()时输出"strawberry sh\0\n"('\n'为额外添加)。而使用fputs()输出时,不会添加'\n',故直接输出"strawberry sh",紧接着是"Done."。
fgets()函数的返回值为指向char类型的指针。指针指向输入的字符串地址。但是,如果函数遇到文件结尾,则返回一个称为空指针的特殊指针。这是一个确保不指向有效数据的指针,因此它可用于指示特殊情况。在代码中,该指针可以表示为数字0,或者在C中更通常的表示方法为宏NULL。
fgets()的输入问题
由于fgets()会保存换行符,这就带来了一些问题。有时,你不想保存该换行符,而仅仅将用户输入的换行符作为buffered I/O的一个指令,通知临时存储区的数据可以被fgets()函数使用。
如何丢弃换行符?
while (words[i] != '\n')
i++;
words[i] = '\0';
words为通过fgets()得到的输入字符串,将换行符替换为'\0'。
如果想将超过数组定义大小的剩余字符串也丢弃掉,以免作为下次的输入,如何做到呢?
while (getchar() != '\n')
continue;
Null字符和NULL指针
NULL字符或'\0',用来标记字符串的结束。代码为0的字符,ASCII 为0的字符。
空指针或NULL具有与有效数据地址不对应的值。 它通常由函数使用,否则返回有效地址以指示某些特殊情况,例如遇到文件结束或未能按预期执行。
另外,空字符是int类型,而NULL指针是指针类型。
gets_s()函数
C11引入了一个新函数gets_s(),类似fgets(),但和fgets也有不同之处。
- gets_s()只读取标准输入,因此不需要第三个参数如stdin。
- 如果get_s()读取到换行符,则丢弃换行符,而不是和fgets()一样,保留它。
- 如果gets_s()读取最大字符数并且无法读取换行符,则需要几个步骤。 它将目标数组的第一个字符设置为空字符。 它会读取并丢弃后续输入,直到遇到换行符或文件结尾。 它返回空指针。 它调用依赖于实现的“处理程序”函数(或者选择的函数),这可能导致程序退出或中止。
轮子
char * s_gets(char *st, int n)
{
char *ret_val;
int i = 0;
ret_val = fgets(st, n, stdin);
if (ret_val)
{
while (st[i] != '\n' && st[i] != '\0')
i++;
if (st[i] == '\n')
st[i] = '\0';
else
{
while (getchar() != '\n')
continue;
}
}
return ret_val;
}
s_gets()函数为自定义函数,可以丢弃fgets()函数保留的换行符,同时丢弃超过最大字符数的剩余输入。
scanf()函数
一般我们使用scanf()函数和%s格式读取字符串。scanf()更像是读取一个单词,而不是一个字符串。
示例:
Input Statement | Original Input Queue* | Name Contents | Remaining Quenue |
---|---|---|---|
scanf("%s', name); | Fleebert#Hup | Fleebert | #Hup |
scanf("%5s", name); | Fleebert#Hup | Fleeb | ert#Hup |
scanf("%5s", name); | Ann#Ular | Ann | #Ular |
‘#’代表space character(/t, /b等)。
可以到,scanf会在遇到空白字符或最大字符时读取结束,其他字符将再下次输入时读取。scanf的返回值为成功读取到的字符串个数。
字符串输出
puts()函数
puts()函数用来输出字符串,函数原型为int puts(const char *); 参数为一个char类型的指针。那puts()是如何知道字符串的结尾位置呢?因为字符串结尾为'\0',puts()函数读到'\0'时截止。
puts()函数会在字符串末尾添加'\n'换行符,因为puts()函数读取字符串时会丢弃'\n'换行符。
fputs()函数
fputs()是puts()函数的面向文件的版本,原型为:int fputs(const char * __restrict, FILE * __restrict); 和puts()的主要区别如下:
- fputs()函数接收第二个参数,说明要输出的字符串写入的哪个文件,可以使用stdout作为屏幕输出。
- 和puts()不同,fputs()不会自动在字符串输出的时候添加换行符'\n'。
printf()函数
和puts()函数类似,也是将字符串地址作为函数参数。printf()函数使用起来不如puts(),但它更通用,因为它格式化各种数据类型。printf()也不会自动添加换行符。
总结
输入类型 | 参数及返回值 | 说明 | 输出类型 | 参数及返回值 | 说明 |
---|---|---|---|---|---|
int scanf(const char \* __restrict, ...) | 参数:字符串地址 返回值:返回成功匹配和分配的输入项的数量,可以少于提供的数量,或者在早期匹配失败时甚至为零 | scanf函数不会默认添加换行符,且一般用其读入单个单词(读到空格字符停止) | int printf(const char \* __restrict, ...) | 参数:字符串地址,各种格式化输出 返回值:如果成功,则返回写入的字符总数。 失败时,返回负数 | 一般用来格式化输出 |
char \*gets(char \*); | 参数:字符串地址 返回值:在成功时返回str,在出错时或文件结束时返回NULL,而没有读取任何字符 | 默认会丢弃输入的换行符。已在C11标准中移除,无法控制用户输入的字符串长度,可能造成缓冲区溢出 | int puts(const char \*); | 参数:字符串地址 返回值:如果成功,则返回非负值。 出错时,该函数返回EOF | 该函数会默认添加换行符 |
char \*fgets(char \* __restrict, int, FILE \*); | 参数:字符串地址;字符串最大长度;指向FILE对象的指针,该对象标识从中读取字符的流(可为stdin) 返回值:成功时,该函数返回相同的str参数。 如果遇到文件结尾且未读取任何字符,则str的内容保持不变,并返回空指针。如果发生错误,则返回空指针 | gets()函数的替代品,防止缓冲区溢出。fgets()会默认保留输入的换行符 | int fputs(const char \* __restrict, FILE \* __restrict) | 参数:包含要写入的以null结尾的字符序列的数组;指向FILE对象的指针,该对象标识要写入字符串的流(可为stdout) 返回值:返回非负值,否则在返回EOF时返回错误。 | 该函数不会默认添加换行符 |