🍍字符串输入输出
2021-1-23
| 2023-8-2
0  |  阅读时长 0 分钟
type
status
date
slug
summary
tags
category
icon
password
Property
 

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

分配空间

不要指望计算机在读取字符串时顺便计算它的长度,然后再分配空间(计算机不会这样做,除非你编写一个处理这些任务的函数)。假设编写了如下代码:
虽然可能会通过编译(编译器很可能给出警告),但是在读入name时,name可能会擦写掉程序中的数据或代码,从而导致程序异常中止。因为scanf()要把信息拷贝至参数指定的地址上,而此时该参数是个未初始化的指针,name可能会指向任何地方。
 
最简单的方法是,在声明时显式指明数组的大小:
还有一种方法是使用C库函数来分配内存
 
 

字符串输入

为字符串分配内存后,便可读入字符串。C 库提供了许多读取字符串的函数:scanf()gets()fgets()

gets()

在读取字符串时,scanf()和转换说明%s只能读取一个单词。可是在程序中经常要读取一整行输入,而不仅仅是一个单词。许多年前,gets()函数就用于处理这种情况。gets()函数简单易用,它读取整行输入,直至遇到换行符,然后丢弃换行符,储存其余字符,并在这些字符的末尾添加一个空字符使其成为一个 C 字符串。它经常和puts()函数配对使用,该函数用于显示字符串,并在末尾添加换行符。
编译器会插入了警告消息?问题出在gets()唯一的参数是 words,它无法检查数组是否装得下输入行。数组名会被转换成该数组首元素的地址,因此,gets()只知道数组的开始处,并不知道数组中有多少个元素。
 
如果输入的字符串过长,会导致缓冲区溢出,即多余的字符超出了指定的目标空间。如果这些多余的字符只是占用了尚未使用的内存,就不会立即出现问题;如果它们擦写掉程序中的其他数据,会导致程序异常中止;或者还有其他情况。
 

fgets()

过去通常用fgets()来代替gets()fgets()函数在处理输入方面与gets()略有不同。
C11标准新增的gets_s()函数也可代替gets()。该函数与gets()函数更接近,可以替换现有代码中的gets()。但是,它是stdio.h输入/输出函数系列中的可选扩展,所以支持C11的编译器也不一定支持它。
fgets()函数通过第2个参数限制读入的字符数来解决溢出的问题。该函数专门设计用于处理文件输入,所以一般情况下可能不太好用。
 
fgets()函数的第2个参数指明了读入字符的最大数量。如果该参数的值是n,那么fgets()将读入n-1个字符,或者读到遇到的第一个换行符为止。如果fgets()读到一个换行符,会把它储存在字符串中。这点与gets()不同,gets()会丢弃换行符。
fgets()函数的第3 个参数指明要读入的文件。如果读入从键盘输入的数据,则以stdin(标准输入)作为参数,该标识符定义在stdio.h中。 因为fgets()函数把换行符放在字符串的末尾(假设输入行不溢出),通常要与fputs()函数(和puts()类似)配对使用,除非该函数不在字符串末尾添加换行符。fputs()函数的第2个参数指明它要写入的文件。如果要显示在显示器上,应使用stdout(标准输出)作为该参数。
输入apple pie,比fgets()读入的整行输入短,因此,apple pie\n\0被储存在数组中。所以当puts()显示该字符串时又在末尾添加了换行符,因此apple pie后面有一行空行。因为fputs()不在字符串末尾添加换行符,所以并未打印出空行。 输入strawberry shortcake,超过了大小的限制,所以fgets()只读入了13个字符,并把strawberry sh\0 储存在数组中。puts()函数会在待输出字符串末尾添加一个换行符,而fputs()不会这样做。
 
fputs()函数返回指向 char的指针。如果一切进行顺利,该函数返回的地址与传入的第 1 个参数相同。但是,如果函数读到文件结尾,它将返回一个特殊的指针:空指针。该指针保证不会指向有效的数据,所以可用于标识这种特殊情况。在代码中,可以用数字0来代替,不过在C语言中用宏NULL来代替更常见(如果在读入数据时出现某些错误,该函数也返回NULL)。
下面演示一个简单的循环:读入并显示用户输入的内容,直到fgets()读到文件结尾或空行(首字符是换行符)
notion image
 
虽然STLEN被设置为10,但是该程序似乎在处理过长的输入时完全没问题。
程序中的fgets()一次读入 STLEN - 1 个字符。所以,一开始它只读入了“By the wa”,并储存为By the wa\0;接着fputs()打印该字符串,而且并未换行。然后while循环进入下一轮迭代,fgets()继续从剩余的输入中读入数据,即读入“y, the ge”并储存为y, the ge\0;接着fputs()在刚才打印字符串的这一行接着打印第 2 次读入的字符串。然后while 进入下一轮迭代,fgets()继续读取输入、fputs()打印字符串,这一过程循环进行,直到读入最后的“tion\n”。fgets()将其储存为tion\n\0, fputs()打印该字符串,由于字符串中的\n,光标被移至下一行开始处。
系统使用缓冲的I/O。这意味着用户在按下Return键之前,输入都被储存在临时存储区(缓冲区)中。按下Return键就在输入中增加了一个换行符,并把整行输入发送给fgets()。对于输出,fputs()把字符发送给另一个缓冲区,当发送换行符时,缓冲区中的内容被发送至屏幕上。 fgets()储存换行符有好处也有坏处。坏处是你可能并不想把换行符储存在字符串中,这样的换行符会带来一些麻烦。好处是对于储存的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理一行中剩下的字符。
如何处理掉换行符?一个方法是在已储存的字符串中查找换行符,并将其替换成空字符:
如果仍有字符串留在输入行怎么办?一个可行的办法是,如果目标数组装不下一整行输入,就丢弃那些多出的字符:
 
notion image
 
空字符和空指针
空字符(或'\0')是用于标记C字符串末尾的字符,其对应字符编码是0。由于其他字符的编码不可能是 0,所以不可能是字符串的一部分。
空指针(或NULL)有一个值,该值不会与任何数据的有效地址对应。通常,函数使用它返回一个有效地址表示某些特殊情况发生,例如遇到文件结尾或未能按预期执行。
空字符是整数类型,而空指针是指针类型。两者有时容易混淆的原因是:它们都可以用数值0来表示。但是,从概念上看,两者是不同类型的0。另外,空字符是一个字符,占1字节;而空指针是一个地址,通常占4字节。
 
 

gets_s()函数

C11新增的gets_s()函数(可选)和fgets()类似,用一个参数限制读入的字符数。假设把之前程序中的fgets()换成gets_s(),其他内容不变,那么下面的代码将把一行输入中的前9个字符读入words数组中,假设末尾有换行符:
 
gets_s()fgets()的区别如下:
gets_s()只从标准输入中读取数据,所以不需要第3个参数。
如果gets_s()读到换行符,会丢弃它而不是储存它。
如果gets_s()读到最大字符数都没有读到换行符,会执行以下几步。首先把目标数组中的首字符设置为空字符,读取并丢弃随后的输入直至读到换行符或文件结尾,然后返回空指针。接着,调用依赖实现的“处理函数”(或选择的其他函数),可能会中止或退出程序。
只要输入行未超过最大字符数,gets_s()gets()几乎一样,完全可以用gets_s()替换gets()
 
比较一下gets()fgets()gets_s()的适用性:
如果目标存储区装得下输入行,3个函数都没问题。但是fgets()会保留输入末尾的换行符作为字符串的一部分,要编写额外的代码将其替换成空字符。
如果输入行太长会怎样?使用gets()不安全,它会擦写现有数据,存在安全隐患。gets_s()函数很安全,但是,如果并不希望程序中止或退出,就要知道如何编写特殊的“处理函数”。另外,如果打算让程序继续运行,gets_s()会丢弃该输入行的其余字符,无论你是否需要。由此可见,当输入太长,超过数组可容纳的字符数时,fgets()函数最容易使用,而且可以选择不同的处理方式。 所以,当输入与预期不符时,gets_s()完全没有fgets()函数方便、灵活。也许这也是gets_s()只作为C库的可选扩展的原因之一。鉴于此,fgets()通常是处理类似情况的最佳选择。
 
 

拓展fgets()

读取整行输入并用空字符代替换行符,或者读取一部分输入,并丢弃其余部分
如果fgets()返回 NULL,说明读到文件结尾或出现读取错误,s_gets()函数跳过了这个过程。如果字符串中出现换行符,就用空字符替换它;如果字符串中出现空字符,就丢弃该输入行的其余字符,然后返回与fgets()相同的值。
为什么要丢弃过长输入行中的余下字符?这是因为,输入行中多出来的字符会被留在缓冲区中,成为下一次读取语句的输入。例如,如果下一条读取语句要读取的是 double 类型的值,就可能导致程序崩溃。丢弃输入行余下的字符保证了读取语句与键盘输入同步
 
 

scanf()函数

scanf()gets()fgets()的区别在于它们如何确定字符串的末尾:scanf()更像是“获取单词”函数,而不是“获取字符串”函数;如果预留的存储区装得下输入行,gets()fgets()会读取第1个换行符之前所有的字符。scanf()函数有两种方法确定输入结束。无论哪种方法,都从第1个非空白字符作为字符串的开始。如果使用%s转换说明,以下一个空白字符(空行、空格、制表符或换行符)作为字符串的结束(字符串不包括空白字符)。如果指定了字段宽度,如%10s,那么scanf()将读取10 个字符或读到第1个空白字符停止(先满足的条件即是结束输入的条件)
notion image
scanf()函数返回一个整数值,该值等于scanf()成功读取的项数或EOF(读到文件结尾时返回EOF)
 
 
 
 

字符串输出

C有3个标准库函数用于打印字符串:put()fputs()printf()

puts()函数

puts()函数很容易使用,只需把字符串的地址作为参数传递给它即可
每个字符串独占一行,因为puts()在显示字符串时会自动在其末尾添加一个换行符。
该程序示例再次说明,用双引号括起来的内容是字符串常量,且被视为该字符串的地址。另外,储存字符串的数组名也被看作是地址。在第5个puts()调用中,表达式&str1[5]是str1数组的第6个元素(r),puts()从该元素开始输出。与此类似,第6个puts()调用中,str2+4指向储存"pointer"中i的存储单元,puts()从这里开始输出。
puts()如何知道在何处停止?该函数在遇到空字符时就停止输出,所以必须确保有空字符。
 
 

fputs()函数

fputs()函数是puts()针对文件定制的版本。
fputs()函数的第 2 个参数指明要写入数据的文件。如果要打印在显示器上,可以用定义在stdio.h中的stdout(标准输出)作为该参数。
puts()不同,fputs()不会在输出的末尾添加换行符。 注意,gets()丢弃输入中的换行符,但是puts()在输出中添加换行符。另一方面,fgets()保留输入中的换行符,fputs()不在输出中添加换行符。
如果混合使用 fgets()输入和puts()输出,每个待显示的字符串末尾就会有两个换行符。这里关键要注意:puts()应与gets()配对使用,fputs()应与fgets()配对使用。
 
一个循环,读取一行输入,另起一行打印出该输入:
如果gets()读到文件结尾会返回空指针。对空指针求值为0(即为假),这样便可结束循环:
 

printf()函数

printf()也把字符串的地址作为参数。printf()函数用起来没有puts()函数那么方便,但是它可以格式化不同的数据类型。
puts()不同的是,printf()不会自动在每个字符串末尾加上一个换行符,必须在参数中指明应该在哪里使用换行符。
 

自定义输入输出函数

不一定非要使用C库中的标准函数,如果无法使用这些函数或者不想用它们,完全可以在getchar()putchar()的基础上自定义所需的函数。一个类似puts()但是不会自动添加换行符的函数:
指向char的指针string最初指向传入参数的首元素。因为该函数不会改变传入的字符串,所以形参使用了const限定符。打印了首元素的内容后,指针递增1,指向下一个元素。while循环重复这一过程,直到指针指向包含空字符的元素。++的优先级高于*,因此putchar(*string++)打印string指向的值,递增的是string本身,而不是递增它所指向的字符。
 
用数组表示法编写这个函数稍微复杂些:
要为数组索引创建一个额外的变量。
在while循环中使用下面的测试条件比上面两种方法简洁:
 
 
一个类似puts()的函数,同时给出待打印字符的个数:
 
使用一个简单的驱动程序测试put1()put2()
  • C
  • 字符串字符串函数
    目录