背景
我正在编写使用
ctype.h
中的函数来识别字符串中的内容的代码。我不小心将字符串 (char*
) 传递给了采用 和 int
类型的函数,导致程序出现段错误。很容易看出我忘记取消引用字符串指针,但即使使用以下参数进行编译,GCC 也没有给我任何警告:
gcc -o main main.c -Wall -Wextra -Werror -pedantic -pedantic-errors -std=c99 -Wconversion
我正在使用
Debian GNU/Linux bookworm 12.5 x86_64
和 gcc (Debian 12.2.0-14) 12.2.0
,它们都是最新的。这是问题的示例:
/* main.c */
#include <ctype.h>
#include <stdio.h>
int main(void)
{
char msg[] = "hello";
int res = isspace(msg); // char* gets cast to int without warning
// It should be `isspace(*msg)`
// This also segfaults
printf("%i\n", res);
return 0;
}
问题
您传入的值超出了函数期望的值范围。这样做会触发未定义的行为,根据 C 标准第 7.4p1 节有关 ctype.h 中定义的函数:
标头
声明了几个对分类有用的函数 和映射字符。在所有情况下,参数都是 , 其值应表示为int
或应 等于宏unsigned char
的值。如果该参数有任何其他值, 行为未定义EOF
由于这是未定义的行为,因此崩溃是一种可能的结果。
至于为什么编译器没有产生警告,我们需要查看预处理器的输出。对
isspace
的调用在预处理器之后转换为以下内容:
int res = ((*__ctype_b_loc ())[(int) ((msg))] & (unsigned short int) _ISspace);
从中,我们可以看到
isspace
被实现为一个宏,它使用以给定参数作为索引的查找表,并且我们可以看到该参数被显式转换为 int
。这个明确的转换解释了为什么没有警告。
上面还解释了崩溃,因为指针值可能会远远超出此查找表的范围,因此会尝试访问它无法访问的内存。
作为宏实现的库函数实际上符合 C 标准。此外,此类定义为宏的函数也必须定义为实际函数。这是由 C 标准第 7.1.4p1 节规定的:
标头中声明的任何函数都可以另外实现为 头文件中定义的类似函数的宏,因此如果库函数是 当包含其标头时显式声明,其中一种技术 如下所示可用于确保声明不受 这样的宏。函数的任何宏定义都可以被抑制 通过将函数名称括在括号中来本地实现,因为 该名称后面不跟左括号,表示 宏函数名称的扩展。出于同样的语法原因,它 允许获取库函数的地址,即使它是 也定义为宏。 185)
- 这意味着实施应提供实际的 function 对于每个库函数,即使它还提供了宏 对于该功能。
上面还提到可以通过在函数名两边加上括号来抑制宏版本函数的使用:
int res = (isspace)(msg);
在这种情况下,编译器将产生指针到整数转换的警告。
很可能,在您的编译器中,
isspace()
被实现为一个宏,其中包含对 char
或 int
的任何参数的类型转换。
显然,当编译器看到强制转换时,它只会假设“好吧,他是这么说的”。宏根本不进行类型检查(好吧,你不能指定类型,那么编译器应该如何检查它)。