问题描述
我曾与 K&R 合作,它广泛使用 getchar()
来输入基础知识。
但问题是我无法完全理解它的行为。
下面是一段代码:
#include <stdio.h>
int main() {
char c,i;
char line[10000];
i = 0;
while((c=getchar()) != EOF && c!= '\n') {
line[i++] = c;
}
printf("%s",line);
}
代码按预期工作。
我的问题是:为什么当我按回车键时它会终止?当我仍在编写输入且程序处于 c=getchar()
时,它如何知道换行符是终止条件?
我知道这不是像 getchar()
这样的默认 scanf()
行为,因为当我删除换行条件时,程序不会在换行处终止。 也许我的问题超出了getchar()
,是一个笼统的问题。
假设我的输入是 Hello
,然后按 Enter。
首先,c
变量变成 'H'
,它被成行存储,然后是 'e'
,然后是 'l'
,然后是 'l'
,然后是 {{1 }},之后它遇到换行符并循环终止。很好理解。
我想知道为什么在我按 Enter 后它开始读取字符。我希望换行并写更多字符。
解决方法
理解该代码有两个部分,还有一个错误,chqrlie 已经为修复提出了很好的论据。
第 0 部分:为什么应该使用 int
与 getchar
一起阅读
正如许多人评论的那样,如果您打算使用 char c
进行阅读,则使用 getchar
是危险的,因为 getchar()
返回有符号整数,最值得注意的是 EOF
-- 即通常 #define
d 为 -1
以表示文件结束。标准 char
may or may not have a sign - 这会使您的程序无法识别 -1
/ EOF
。所以让我们把第一行改成
int c,i;
第 1 部分:为什么 \n
很特别
根据 man,getchar()
等价于 getc(stdin)
,它等价于 fgetc() 不同之处在于它可以实现为一个评估其流的宏({{1} },在这种情况下)不止一次。
重要的是,每次调用它时,它都会从输入中消耗一个字符。每次调用 stdin
都会返回输入中的 next 字符,只要有要返回的字符即可。如果没有剩余,则返回 getchar
。
现在,标准输入 EOF
通常是行缓冲的,这意味着在行以 stdin
终止之前,程序将无法访问实际字符。您可以使用此程序对此进行测试:
\n
如果你运行它,它仍然不会做任何事情,直到按下 #include <stdio.h>
int main() {
int c,i;
char line[10000];
i = 0;
while((c=getchar()) != EOF && c!= 'a') { // <-- replaced `\n` with `a`
line[i++] = c;
}
printf("%s",line);
}
;但是当按下时,输入将在第一个 \n
(不包括)完成。请注意,之后的输出将是未定义的,因为不能保证之后会有 a
终止字符串。为了避免这个陷阱,请在最后查看重写的程序。
第 2 部分:为什么循环条件如此有效
您可以按如下方式重写循环条件。这样可以更轻松地查看正在发生的事情:
\0
尾声:改进的代码
// loop condition looks up next char,tests it against EOF and `\n`
while((c=getchar()) != EOF && c!= '\n') { line[i++] = c; }
// loop condition broken up for readability; fully equivalent to above code
while (true) {
c = getchar();
if (c == EOF || c == '\n') {
break; // exit loop
} else {
line [i++] = c;
}
}
,
程序不正确,可能会调用未定义的行为。
对于初学者,变量 c
应声明为
int c;
否则条件
(c=getchar()) != EOF
可以始终为真,即使用户试图中断输入。问题在于宏 EOF 是 int
类型的负整数值。另一方面,类型 char
可以表现为类型 unsigned char
。因此,提升为类型 c
的变量 int
将始终包含一个非负值。
其次,char
类型无论如何不能保存等于字符数组大小的 10000
值。因此,变量 i
至少应声明为具有 short int
类型。
while 循环将检查索引变量 i
的当前值是否已经大于或等于字符数组的大小。否则这个声明
line[i++] = c;
可以写超出字符数组。
最后结果字符数组 line
不包含字符串,因为终止零字符 '\0'
没有附加到输入的字符序列中。结果这个电话
printf("%s",line);
调用未定义的行为。
程序可以如下所示
#include <stdio.h>
int main( void )
{
enum { N = 10000 };
char line[N];
size_t i = 0;
for ( int c; i + 1 < N && ( c = getchar() ) != EOF && c != '\n'; i++ )
{
line[i] = c;
}
line[i] = '\0';
puts( line );
}
即循环继续填充字符数组,直到字符数组行中有足够的空间
i + 1 < N
用户不会中断输入
( c = getchar() ) != EOF
并且它没有按回车键完成输入字符串
c != '\n'
循环后附加终止零
line[i] = '\0';
现在数组 line
包含在语句中输出的字符串
puts( line );
例如,如果用户将键入此字符序列
Hello world!
然后按下 Enter 键(在输入缓冲区中发送换行符 '\n'
)然后循环将停止其迭代。换行符 '\n'
不会写入字符串。在循环之后,终止零字符 '\0'
将附加到数组 line
中存储的字符。
所以数组将包含以下字符串
{ 'H','e','l','o',' ','W','r','d','!','\0' }
输出。
,您的理解基本正确,但代码中存在一些问题,输入机制比您推断的要复杂:
-
c
应该具有类型int
以容纳getc()
返回的所有值,即unsigned char
类型的所有值(在大多数当前系统中0
到255
) 和特殊的负值EOF
(通常是 -1)。 -
i
还应该具有类型int
,或者可能是size_t
以正确索引到line
数组。如果您输入的行超过 127 个字符,则发布的char
类型代码可能具有未定义的行为。 - 您应该测试
i
是否保持在数组line
的边界内。这将需要很长的输入行,但通过从文件重定向可以轻松实现。 -
line
在将其作为printf
格式的参数传递给%s
之前必须以空字符结尾。
这是修改后的版本:
#include <stdio.h>
int main() {
int c,i;
char line[10000];
i = 0;
while (i < sizeof(line) - 1 && (c = getchar()) != EOF && c != '\n') {
line[i++] = c;
}
line[i] = '\0'; // null terminate the array.
printf("%s\n",line);
return 0;
}
关于控制台响应程序输入请求的行为,它是实现定义的,但通常涉及 2 层缓冲:
-
FILE
流包实现了一种缓冲方案,其中数据以块的形式从系统读取或写入系统。这种缓冲可以用setvbuf()
控制。有 3 种设置可用:无缓冲(stderr
的默认设置)、行缓冲(通常是连接到字符设备时stdin
和stdout
的默认设置)和完全缓冲可自定义的块大小(常见大小为 512 和 4096)。 - 当您调用
getchar()
或更一般的getc(stream)
时,如果流缓冲区中有可用字节,则返回该字节并增加流位置,否则向系统发出请求以填充缓冲区。 - 如果流附加到文件,填充缓冲区会执行
read
系统调用或等效的,除非在文件末尾或读取错误时成功。 - 如果流附加到字符设备,例如终端或图形显示上的终端窗口之类的虚拟 tty,则涉及另一层缓冲,其中设备驱动程序从输入设备读取输入并处理一些键以特殊方式,例如 Backspace 擦除前一个字符,光标移动键在输入行内移动,Ctrl-D (unix) 或 Ctrl-Z (windows) 表示文件结束。这层缓冲可以通过
tcsetattr()
系统调用或其他系统特定的 API 进行控制。文本编辑器等交互式应用程序通常会禁用此功能并直接从输入设备检索原始输入。 - 用户输入的键由终端处理形成输入行,当用户输入Enter时发送回C流API(翻译为系统特定的行尾)序列),流函数执行另一组转换(即:在遗留系统上将
CR
/LF
转换为'\n'
)并且字节行存储在流缓冲区中。当getc()
终于有机会返回第一个可用字节时,整行已被用户键入并输入,并在流或设备缓冲区中等待。
研究这感觉就像剥洋葱一样:当你穿过一层层皮肤时,你会发现更多的层可以刮掉,这让你哭了:)
,因为它是 K&R 的一个例子,而且它不是您问题的核心问题,所以让我们回顾一下应该是 char c
的 int c
(因为 getchar ()
返回一个 {{ 1}})。你会发现很多问题可以更好地解释它。
while 循环行为是
int
你的条件包含一个总是被执行的赋值:
while (condition_is_true)
Do_Something;
这是逻辑检查 (c=getchar()
) 的一部分,它在您的程序中始终为真(您正在从 c != EOF
读取)。因此,将执行 stdin
之外的条件(短路 确保在逻辑 and 操作数中从左到右求值,直到它们为真。 >
后一个条件是&&
。对于 c != '\n'
字符串中的所有字符,它将为 false,并且所有字符都将存储在您的 "Hello"
数组中。但是一旦插入换行符,由于之前的赋值将 line
放入 \n
,条件变为假,并且执行退出循环(因此,换行符不会存储在 { {1}} 数组)。
然后,然后,将打印字符串 c
。
这是因为 getchar()
的实现。此函数首先让您写入缓冲区,直到您按下 enter
键,然后才从缓冲区中获取一个字符。
如果您想直接从键盘获取一个字符,可以使用库 conio.h
。
享受学习 C 的乐趣,不要害怕提问!