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()
读到文件结尾或空行(首字符是换行符)虽然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()
储存换行符有好处也有坏处。坏处是你可能并不想把换行符储存在字符串中,这样的换行符会带来一些麻烦。好处是对于储存的字符串而言,检查末尾是否有换行符可以判断是否读取了一整行。如果不是一整行,要妥善处理一行中剩下的字符。如何处理掉换行符?一个方法是在已储存的字符串中查找换行符,并将其替换成空字符:
如果仍有字符串留在输入行怎么办?一个可行的办法是,如果目标数组装不下一整行输入,就丢弃那些多出的字符:
空字符和空指针
空字符(或'\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个空白字符停止(先满足的条件即是结束输入的条件)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()