【问题标题】:C - Last element of char* arrayC - char* 数组的最后一个元素
【发布时间】:2020-12-03 07:18:07
【问题描述】:

以下示例代码;

char* g_commands[]={
    "abcd",
    NULL
};

int main()
{
    
    g_commands[1] = "efg";
    
    char ** tmp = &g_commands[0];
    
    for(; *tmp != NULL; tmp++){
        printf("%s", *tmp);
    }

    return 0;
}

由于 tmp 在循环中指向 g_commands 数组中的指针,在我将“efg”分配给 g_commands[1] 之后,我预计循环会创建分段错误,因为 g_commands 的最后一个元素不再为空。但程序无异常结束,打印abcdefg成功。

为什么会这样?编译器是否也将 NULL 添加到 char* 数组的末尾?

【问题讨论】:

  • 不,它没有。在这种情况下,你碰巧走运了。这是未定义的行为。
  • 您注意到在您的环境中碰巧允许“abcdefg”的未定义行为,但不会跨平台发生。什么是未定义的行为?这基本上意味着您正在做的事情超出了 C 标准,因此不同的编译器实现可能会以不同的方式处理它。它不是由 C 本身定义的。

标签: arrays c pointers char-pointer


【解决方案1】:

我预计循环会产生分段错误,因为 g_commands 的最后一个元素不再为空。但程序无异常结束,打印abcdefg成功。

为什么会这样?编译器是否也将 NULL 添加到 char* 数组的末尾?

当您取消引用指向指向数组末尾的指针tmp 的指针时调用undefined behavior 并尝试使用printf("%s", *tmp) 打印不确定的字符串。

未定义的行为不需要提供错误的结果。如果事情看起来是对的,就认为事情是对的,这是一种误解。

你不能期待任何事情。解释未定义行为的原因和方式也没有多大意义,因为它与您编写生产代码完全无关。

我知道有些人喜欢调查这些并查看实现的行为,但如果您对编写不易受感染、可移植和可靠的代码感兴趣,通常认为这些不是需要更深入地关注的事情。

【讨论】:

    【解决方案2】:

    程序具有未定义的行为。特别是它意味着程序可以产生预期或意外的结果。

    我预计循环会在最后一个元素之后产生分段错误 的 g_commands 不再为空

    程序没有分段错误,因为数组g_commands

    char* g_commands[]={
        "abcd",
        NULL
    };
    

    是在全局命名空间中定义的,并且在数组之后没有其他对象的定义。这样的声明具有静态存储持续时间,编译器通常将此内存设置为零。

    如果你要移动 main 中的定义,比如

    #include <stdio.h>
    /*
    char* g_commands[]={
        "abcd",
        NULL
    };
    */
    int main()
    {
        char* g_commands[]={
            "abcd",
            NULL
        };
        
        g_commands[1] = "efg";
        
        char ** tmp = &g_commands[0];
        
        for(; *tmp != NULL; tmp++){
            printf("%s", *tmp);
        }
    
        return 0;
    }
    

    那么发生分段错误的概率非常高。

    【讨论】:

    • @h0r53 段可以相互跟随。此外,例如可以与段落的大小对齐
    • 这是有道理的。所以原则上,在这个例子中声明额外的(不是 NULL)全局变量会增加段错误的可能性,对吧?
    • @h0r53 我想是的。我们需要查看生成的对象模块。
    • @VladfromMoscow 添加了一堆全局变量,瞧,核心转储。
    • 我更喜欢这个答案而不是其他答案,因为它深入探讨了“未定义”的含义以及为什么代码可能在某些情况下仍然有效的奥秘。很容易暗示某些东西是未定义的,不要相信输出,就是这样。但深入挖掘真正发生的事情提供了一个有用的视角,而不仅仅是简单的未定义。
    【解决方案3】:

    让我们一步一步来。

    char* g_commands[]={
        "abcd",
        NULL
    };
    
    int main()
    {
        
        g_commands[1] = "efg";
    

    此时g_commands 已被更改,就好像您已通过以下方式对其进行了初始化:

    // char* g_commands[]={
    //    "abcd",
    //    "efg"
    // };
    

    请注意,从此时起,g_commands 中不再有终止空指针。

    以下

        char ** tmp = &g_commands[0];
    

    也可以写成

    // char ** tmp = g_commands;
    

    现在,当您遍历 g_commands 的元素时,您正在测试 tmp 是否取消对空指针的引用。不幸的是,您之前确实用非空指针覆盖了g_commands 的最后一个元素,所以这个

        for(; *tmp != NULL; tmp++){
            printf("%s", *tmp);
        }
    

    正在超出数组的边界并调用未定义的行为。

        return 0;
    }
    

    【讨论】:

      猜你喜欢
      • 2014-10-10
      • 2017-06-28
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 1970-01-01
      • 2017-12-17
      • 2020-01-28
      • 1970-01-01
      相关资源
      最近更新 更多