调用isalpha导致分段错误

10

我有以下程序,导致分段错误。

#include <stdio.h>
#include <string.h>
#include <ctype.h>

int main(int argc, char *argv[])
{
    printf("TEST");

    for (int k=0; k<(strlen(argv[1])); k++)
    {
        if (!isalpha(argv[1])) {
            printf("Enter only alphabets!");
            return 1;
        }
    }

    return 0;
}

我发现问题出在这一行

if (!isalpha(argv[1])) {

通过将 argv[1] 替换为 argv[1][k] 可以解决问题。

然而,我发现即使没有打印出 TEST,程序也会导致分段错误,这让我感到相当好奇。 我还期望 isalpha 函数在检查 char* 指向的 argv [1] 的低字节时出现错误,但事实似乎并非如此。 为了简洁起见,我这里没有展示用于检查参数数量的代码。

这里发生了什么?


3
向函数传递错误类型的参数会导致未定义行为。不要这样做。好的编译器应该会警告你,听从编译器的指示,它知道这些事情。 - Some programmer dude
4
关于你的“TEST”输出,stdout(即printf写入的位置)默认情况下是行缓冲的。如果你不打印换行符,那么缓冲区将不会被清空。这就是为什么你应该总是用一个换行符来结束你的输出。 - Some programmer dude
1
!isalpha(argv[1] --> !isalpha(argv[1][k],因为您传递的是指针强制转换为整数而不是字符。 - 0___________
1
代码存在约束违规。编译器必须发出诊断,任何可执行文件的行为都是无意义的。 - M.M
显示剩余11条评论
4个回答

19

一般来说,讨论未定义行为导致这种或那种结果是相当无意义的。

但是,即使超出规范,尝试理解为什么会发生某些事情可能也没有坏处。

有一些实现使用简单的数组查找所有可能的unsigned char值来实现isalpha。在这种情况下,传递的值被用作索引到该数组中。虽然真正的字符只限于8位,但整数却不是。该函数采用int作为参数。这是为了允许输入EOF,因为它不能适应unsigned char

如果您将诸如0x7239482342之类的地址传递给函数,则远远超出了所述数组的末端。当CPU尝试读取具有该索引的条目时,它就会掉进世界的边缘。 ;)

调用带有这样一个地址的isalpha是编译器应该发出一些警告的地方,提示转换指针为整数。您可能忽略了这个警告......

可能包含检查有效参数的代码,但也可能只依赖于用户不传递不应传递的内容。


核心问题是代码不是有效的 C 代码,编译器应该通知程序员此事。因此,这只是“未定义行为”,因为生成的可执行文件不是 C 代码,而是其他内容。 - Lundin

6
  1. printf was not flushed
  2. the implicit conversion from pointer to integer that ought to have generated at least compile-time diagnostics for constraint violation produced a number that was out of range for isalpha. isalpha being implemented as a look-up table means that your code accessed the table out of bounds, therefore undefined behaviour.
  3. Why you didn't get diagnostics might be in one part because of how isalpha is implemented as a macro. On my computer with Glibc 2.27-3ubuntu1, isalpha is defined as

    # define isalpha(c)     __isctype((c), _ISalpha)
    # define __isctype(c, type) \
        ((*__ctype_b_loc ())[(int) (c)] & (unsigned short int) type)
    

    the macro contains an unfortunate cast to int in it, which will silence your error!


我为什么选择在众多回答后发布这篇文章呢?因为你没有修复代码,如果使用扩展字符并且char被标记为有符号(通常是x86-32和x86-64的情况),它仍然会产生未定义行为。

正确的参数应该是(unsigned char)argv[1][k]C11 7.4:

在所有情况下,参数都是一个int类型的值,其值应该可以表示为unsigned char或等于宏EOF的值。 如果参数具有其他值,则行为将是未定义的。


1
char 中存储的所有字符都可以表示为 unsigned char,因此该部分对我来说似乎过于迂腐。当然,如果您向 isalpha(不是 EOF)传递负值,则可能会出现故障和错误。但如果您传递 5.2.1 中指定的最小字符集中的有效字符,则不会出现问题。 - Lundin
@Lundin 你是什么意思??所以你的意思是没有人会在命令行上写 äö 吗? - Antti Haapala -- Слава Україні
将命令行(或其他)输入转换为安全格式是另一回事。基本上与 printf("Enter a number: "); int i; scanf("%d", &i); 相同的问题,之后用户键入 A - Lundin
@Lundin 你错了。如果 char 是有符号的,那么它的一半值就不能被表示为 unsigned char。我在使用 MSVC 编译器时大约每年会发现一到两次。 - Joker_vD
宏中的int转换似乎是无效的;如果出于某种原因他们要使用转换,那么他们也可以转换为unsigned char - M.M
@Joker_vD 我的观点是:不存在负索引的符号表。因此,如果您仅使用“char”来存储符号表中的字符,则在这里永远不会遇到问题。 - Lundin

3
我觉得很奇怪的是,程序在没有打印TEST的情况下就导致了分段错误 printf并不会立即打印输出结果,而是被写入到临时缓存中。如果你希望将其刷新到实际输出中,请在字符串末尾添加 \n
用argv[1][k]代替argv[1]可以解决这个问题。 isalpha旨在处理单个字符。

1
首先,符合规范的编译器必须在此处为您提供诊断消息。不允许将指针隐式转换为isalpha期望的int参数。(这违反了简单赋值的规则6.5.16.1。)
至于为什么没有打印“TEST”,可能只是因为stdout没有被刷新。您可以尝试在printf之后添加fflush(stdout);,看看是否解决了问题。或者在字符串末尾添加一个换行符\n
否则,只要没有副作用,编译器就可以自由地重新排列代码的执行顺序。也就是说,在潜在地打印"Enter only alphabets!"之前,它可以在printf("TEST");之前执行整个循环,只要它在打印TEST之前打印出来。这样的优化在这里可能不太可能发生,但在其他情况下可能会发生。

我的回答包含了为什么“我”没有收到诊断消息的原因。 - Antti Haapala -- Слава Україні
@AnttiHaapala 因为你的编译器库不符合规范。7.4 “头文件<ctype.h>声明了几个有用的函数,用于分类和映射字符。”然后是6.5.2.2/7“如果表示所调用函数的表达式具有包括原型的类型,则参数会被隐式转换,就像通过赋值一样,转换为相应参数的类型”,然后是6.5.16.1,不存在左参数为算术类型且右参数为指针类型的情况。 - Lundin
那么,你应该回答另一个问题 - Antti Haapala -- Слава Україні

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接