为何char引发的死循环如此顽固,难以破解?

摘要:原来的程序要读的文件内容只有一个字符。(ch = fgetc(fp)) != EOF 却一直是 true。读文件的那段代码是这样的: while ((ch = fgetc(fp)) != EOF) { } 产生了一个死循环 (故意隐藏 ch
原来的程序要读的文件内容只有一个字符。(ch = fgetc(fp)) != EOF 却一直是 true。读文件的那段代码是这样的: while ((ch = fgetc(fp)) != EOF) { } 产生了一个死循环 (故意隐藏 ch 的定义)。 TLDR: 点这里折叠了一个简单的解释。 TLDR: 应该用 int 类型的变量接收 fgetc() 的返回值 熟悉 C 语言 getchar() 系列函数的肯定已经猜到,ch 的类型是 char 导致了这个问题。 fgetc(3): fgetc() reads the next character from stream and returns it as an unsigned char cast to an int, or EOF on end of file or error. 最小复现示例 下面是一个名为 read-null 的程序的代码,这个程序在 x86_64 不会死循环,但是在 aarch64 上会死循环。 #include <stdio.h> #include <assert.h> int main() { FILE *fp = fopen("/dev/null", "r"); assert(fp != NULL); char ch; // Reads from /dev/null always return end of file while ((ch = fgetc(fp)) != EOF) { printf("Infinite loop...\n"); } fclose(fp); return 0; } 下面是这个示例在 x86_64 的运行效果,程序直接读到 EOF 退出了: aarch64 gcc 把比较指令都优化掉了 在 aarch64 机器上,b .L2,无条件跳转,编译器知道 char 和 int 的 -1 比较,(char)-1 != (int)-1 永远都是 true,直接给优化成死循环了,没有对比 (char)fgetc() 返回值是否不等于 EOF这一条指令。 判断 EOF 的操作被编译器优化了。 为什么 x86_64 上是正常的? C 语言的 EOF一般是常量 -1,来看看如果使用 char 类型的变量来接收 fgetc()的返回值,char 的 -1 和 int 的 -1 到底相等不相等。 #include <stdio.h> #include <assert.h> int main() { #if defined(__x86_64__) || defined(_M_X64) printf("=====On x86_64=====\n"); #elif defined(__aarch64__) printf("=====On ARM64=====\n"); #endif assert((unsigned char)-1 != -1); printf("(unsigned char)-1 != -1 is true\n"); assert((char)-1 != -1); printf("(char)-1 != -1 is true\n"); return 0; } x86_64 上,(char)-1 != -1 是 false。aarch64 上,(char)-1 != -1 是 true。 char 和 int 做比较,char 会被隐式转换为 int。unsigned char展开到 int 的时候符号位是不会扩展的,换句话说,(unsigned char)-1 转为 int是 255。(char)-1 转 int 会是多少呢? x86_64 上是 -1,arm64 上是 255. char 的负数转 int ,或者说 signed char 转 int 这是 UB。 K&R p.41 On some machines a char whose leftmost bit is 1 will be converted to a negative integer (``sign extension''). On others, a char is promoted to an int by adding zeros at the left end, and thus is always positive. 下面是 read-null.c 在 x86_64 平台的汇编,x86_64 平台上 gcc char 转 int 会做 sign extension,而且,是和 char 类型比较,直接使用的是 %al,就算没有做符号扩展,%al是返回值的低字节,fgetc()返回EOF 就必定是 0xff。 x86_64 机器上 signed char 转 signed int 符号位会扩展,简单说 -1 还是 -1,而且和 char做比较指令上可能就只用了低字节。 TLDR: 应该用 int 类型的变量接收 fgetc() 的返回值 getchar(), fgetc() 系列函数的返回值是 int。ch的类型应该为int。 K&R p.18 The problem is distinguishing the end of input from valid data. The solution is that getchar returns a distinctive value when there is no more input, a value that cannot be confused with any real character. This value is called EOF, for ``end of file''. We must declare c to be a type big enough to hold any value that getchar returns. We can't use char since c must be big enough to hold EOF in addition to any possible char. Therefore we use int. 没有想到,这快死去的 C 知识居然有用上的一天。 我之前很喜欢用 C,不是嵌入式,只是 Linux 的 C 程序,但是毕业后发现自己只想用 C 找不到工作,因此转头去学 JavaScript 之类的前端技术,可恰逢此时,我又找到了一份 C 开发的工作,以为自己终于能用 C 去做开发了,没想到在工作中我的 C 知识几乎用不上,大多数的工作只是维护而已。尽管用不上,没有那种使劲浑身解数般的酣畅淋漓,但是之前走的每一步,学会的每一个技能,看的每一本书也都有用。我不知道我之后会去哪里,我也不知道以后我的工作会不会和 C 密不可分。 我喜欢 C 语言,喜欢 Linux,但是我没有找到我理想中的工作,或者给不到我想要的薪水。我常常怀疑,自己是不是走错了路,选错了方向…… 借用新华社视频号的一段话,自勉: 不要后悔自己做过的任何决定,人这一生,最怕的不是做错选择,而是永远活在如果的阴影里。走错了路又怎样,那些意外的风景,或许恰恰是命运埋下的伏笔,所以别再回头苛责那个站在风雨里做决定的自己。