Is char Nice ? : 在 C 语言中,有问题的 char 类型不会递增的代码?

7 min

language: ja bn en es hi pt ru zh-cn zh-tw

你好,我是无能。

因为好奇,我尝试了一下。

https://blog.httrack.com/blog/2014/05/30/c-corner-cases-and-funny-things

首先,我尝试在上述状态下编译并运行。

#include <stdio.h>

/** Return the next character within a \0-terminated string, or EOF. **/
int my_read_char(const char *buffer, size_t *offs) {
  if (buffer[*offs] != '\0') {
    return buffer[*offs++];  /* here's the trap */
  } else {
    return EOF;
  }

int main() {
  const char *buffer = "Hello, World!";
  size_t offs = 0;

  printf("Reading characters from buffer:\n");
  while (1) {
    int ch = my_read_char(buffer, &offs);
    if (ch == EOF) {
      break;
    }
    printf("Character read: %c, offs: %zu\n", ch, offs);
  }

  return 0;
}

然后编译并运行

gcc -o bugcode bugcode.c
./bugcode
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Charact^C
alleycat:[haturatu]:~/clang$ 

啊啊啊!!!

因此很危险,但是我将日志输出到名为 log 的文件中,查看初始执行情况。
话说回来,尽管我急忙执行了 `ps -ef | grep bugcode` 来确认进程 ID 并将其杀死...

alleycat:[haturatu]:~/clang$ ls -la
合计 1256824
drwxr-xr-x  2 haturatu haturatu       4096  6月 14 23:38 .
drwx------ 39 haturatu haturatu       4096  6月 14 23:38 ..
~
-rw-r--r-- 1 haturatu haturatu 1286922240  6月 14 23:39 log

文件大小变得非常大,达到了大约1.2GB...这简直是足以使系统崩溃的危险代码...无法用 view 查看,但可以用 vim 打开。

alleycat:[haturatu]:~/clang$ head log
Reading characters from buffer:
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0
Character read: H, offs: 0

从一开始就没有更新(递增)。

我想在 ChatGPT 的帮助下理解这是怎么回事。

EOF 和 \0 的区别
EOF(End Of File)是表示文件结束的特殊值,通常为 -1。而 '\0' 是 C 字符串的终止符,即空字符,其值为 0。

我似乎在这里明白了什么,但我的头脑又开始混乱了。
为了冷静下来,我将询问这段代码在此处发生的流程。它已更改为 my_read_char_with_cast,但请忽略。

流程详情
初始状态:

offs 为 0。
缓冲区没有空终止符(没有 '\0')。
循环开始:

调用 my_read_char_with_cast 函数。
buffer[*offs] 是 'H'(第一个字符),所以不是 '\0'。
返回 'H',offs 变为 1。
循环继续:

下次调用时读取 buffer[1]('e'),并返回。
offs 变为 2。
重复:

此过程继续。
最终 offs 变为 11,读取 buffer[11]('!'),并返回。
offs 变为 12。
无限循环:

buffer[12] 的值未定义(引用缓冲区外的内存区域),且不是 '\0'。
由于 buffer[*offs] 始终不是 '\0',因此会发生无限循环,而不会达到 EOF。

我反而更不明白了。实际测试结果显示从一开始就发生了无限循环,所以 offs 根本没有达到 12。调试时显示

Returning character: H
Character read: H, offs: 0
my_read_char called with offs: 0
Returning character: H
Character read: H, offs: 0
my_read_char called with offs: 0
Returning character: H
Character read: H, o^C
alleycat:[haturatu]:~/clang$ 

果然,offs 仍然是 0。

那么,让我们回到 HTTrack 的博客。

此函数对于 ASCII > 127 的字符返回负值。特别是,对于 ASCII 255 (0xFF),它返回 -1。这也是 EOF 的值。

原来如此?那么为什么会返回 -1 呢?我问问 AI。

char 类型的特殊性
有符号 8 位整数: C 语言的 char 类型通常被视为有符号的 8 位整数。它可以表示 -128 到 127 的范围。
回绕: 当 char 类型的值达到最大值(127)时,下一个递增操作会使其回绕到最小值(-128)。这是由于 2 的补码表示,127 递增后会变成 -128。

关于 ASCII 255(0xFF)的考察
超出 ASCII 范围: 在 ASCII 表中,值 255(0xFF)超出范围,通常被视为控制字符。
与 EOF 的关系: 在 C 语言中,EOF 是表示文件结束的特殊值,通常被视为 -1。EOF 用于指示文件结束,但在这里它被用作 ASCII 值 255(0xFF)的返回值。
函数行为的含义
此函数对于 ASCII 255(0xFF)返回 -1 的原因如下:

超出范围的值: ASCII 255 超出 char 类型的范围,通常被视为无效值。
EOF 的利用: 此函数使用 EOF(-1)来指示缓冲区的结束。对于 ASCII 255 返回 -1,这是一种隐式约定,表示 EOF。
这种设计使得函数能够检测到缓冲区结束,并通过返回 EOF 来作为调用者停止读取数据的信号。但是,在此函数的上下文中,如果需要对 ASCII 255 进行特定处理,或者希望正确处理 ASCII 255,则需要注意。

原来如此,原来 char 类型本身就无法处理。这源于 ASCII 127 和 ASCII 255 的区别,正因为如此,EOF 才作为 -1 返回,从而导致无限循环。

HTTrack 网站上的修正代码如下。但是,我用 gcc 编译了下面的代码,但仍然发生了无限循环,所以这个也不行。

/** Return the next character within a \0-terminated string, or EOF. **/
int my_read_char(const char *buffer, size_t *offs) {
  if (buffer[*offs] != '\0') {
    return (unsigned char) buffer[*offs++];
  } else {
    return EOF;
  }

AI 给出了这个。

int my_read_char(const char *buffer, size_t *offs) {
  if (buffer[*offs] != '\0') {
    return buffer[(*offs)++];
  } else {
    return EOF;
  }
}

结果,即使进行类型转换,似乎也必须明确地将其作为 char 类型进行递增。

我又问了关于它们区别的问题。

区别详情
1. 包含 unsigned char 类型转换的函数
类型转换的原因:
buffer 是 char 数组,buffer[*offs] 返回 char 类型的值。
char 类型通常是有符号的 8 位整数,但这取决于实现(根据编译器或平台,也可能是无符号的)。
如果字符串中的值取负值,在不进行类型转换的情况下,负值在扩展为 int 类型时可能会保留符号。
例如,如果 buffer[*offs] 的值为 -1,不进行类型转换会返回 -1,但进行类型转换会返回 255(0xFF)。
2. 不进行类型转换的函数
处理有符号字符:
如果 buffer[*offs] 可能具有负值,不进行类型转换会直接返回负值。
这在某些情况下可能会导致意外行为。

那么,结果如何呢?

应该使用哪一个?
包含类型转换的函数:

如果将字符数据作为二进制处理,或者希望将其作为无符号值处理,通常会将其转换为 unsigned char。
这在处理非 ASCII 字符(例如,UTF-8 编码的数据等)时尤为重要。
不进行类型转换的函数:

如果只处理 ASCII 字符,或者有意将其作为有符号字符处理,则不进行类型转换可能没有问题。

这确实是一个特殊的例子呢。
对于后者,不进行类型转换时,将其作为 char 类型处理,通过明确地写成 buffer[(*offs)++],使其作为 char 类型的默认无符号处理,这样就能正确地作为 char 类型进行递增了吗?

字符编码的差异是很难可视化的东西,我们身边有很多这样的陷阱呢...哎呀,真有趣!

Related Posts