【问题标题】:Variable-length strings in structures and undefined behavior causing time travel结构中的可变长度字符串和导致时间旅行的未定义行为
【发布时间】:2014-07-11 13:36:33
【问题描述】:

this Stackoverflow question 或文章Undefined behavior can result in time travel (among other things, but time travel is the funkiest) 中,人们可能会了解到在大于其大小的索引处访问数据结构是未定义的行为,并且当编译器看到未定义的行为 em>,它会生成疯狂的代码,甚至不会报告遇到未定义的行为。这样做是为了使代码运行速度快几纳秒,并且因为标准允许这样做。出现所谓的“时间旅行”是因为编译器在控制流分支上操作,当它在一个分支中看到未定义的行为时,它只是删除该分支(基于任何行为都会在未定义行为的位置)。

尽管如此,这是一个古老的成语:

struct myString {
    int length;
    char text[1];
}

用作

    char* s = "hello, world";
    int len = strlen(s);
    myString* m = malloc(sizeof(myString) + len);
    m->length = len;
    strcpy(&m->text,s);

现在,如果我访问m->text[3] 会发生什么? (注意它是如何声明的。)编译器会将其视为未定义的行为吗?如果是,如何在结构的末尾添加一个静态未知数量的项目数组?

特别是,我对大小至少为 1,但可能更大的数组感兴趣。有点,

struct x {
    unsigned u[1];
};

and access like `struct x* p; ... p->x[3]`.

UPD 相关:Is the "struct hack" technically undefined behavior?(正如@mafso 在评论中指出的那样)。

【问题讨论】:

  • 这是一个flexible array member,您显示的版本使用old c90 style,但gcc 可能仍然支持它,
  • 如果你想要 C++ 中的可变长度字符串,你应该使用std::string
  • @ShafikYaghmour 他展示的内容在 C90 中是不合法的?它被广泛使用,并且 C99 添加了一个特殊功能,允许使用相同的功能而不会使边界检查通常是非法的。
  • 在某些未定义行为的情况下,至少有一个编译器会故意导致崩溃。
  • 我希望没有人会在 C++ 中这样做(因为你这样标记它)。与正确编写的容器相比,没有效率提升。想一想,也应该用 C 编写一个容器(伪造一个具有工厂函数作为 ctor 替换的类,它在幕后分配和适当的访问函数作为成员替换)。在 C 中,我们失去了自动释放(没有 dtor),但原始方法也需要手动释放。

标签: c++ c gcc


【解决方案1】:

这可能是一个古老的习语,但它在两者中都是未定义的行为 C 和 C++。从 C99 开始,您可以编写如下内容:

struct MyString
{
    int length;
    char text[];
};

并按照您的描述使用它(尽管您可能需要 malloc) 的长度加 1。在C++中,你需要跳转 通过更多的箍:

struct MyString
{
    int length
    char* text()
    {
        return reinterpret_cast<char*>( this + 1 );
    }
};

但是,对于char 以外的任何内容,您都需要观看 出于对齐限制,因为编译器不知道 结构的结尾必须正确对齐 跟随。 (G++ 使用或至少使用过类似的东西 其实现std::basic_string。和实例化 像std::basic_string&lt;double&gt; 会在机器上崩溃 size_t 只有 4 个字节,需要访问一个 double 8 字节对齐。)

【讨论】:

  • @Manu343726 this + 1 无论对象是什么都有效,而您的建议仅在 sizeof(*this) == sizeof(int) 时有效。
  • @Manu343726 首先,this + sizeof(int) 可能会过多地增加指针,因为它会将sizeof(int) * sizeof(*this) 添加到指针中。其次,正如其他人已经说过的,this + 1 的工作原理与MyString 的内容无关。
  • @MattMcNabb this + sizeof(int) 即使sizeof(*this) == sizeof(int) 也不起作用。 this 的类型是MyString*,所以你添加到它的任何东西都会乘以sizeof(MyString)
  • @Manu343726 您缺少对指针运算工作原理的理解。向指针添加 1 会将其前进到数组中的下一个元素。
  • 这正是问题所在,我想如果它是 char 指针算术。谢谢
【解决方案2】:

为了回答您的问题,编译器没有对数组索引进行边界检查,这就是灵活数组起作用的原因。您可以在任何数组中使用任何索引。

【讨论】:

  • 一些编译器对数组索引没有边界检查。 C 标准经过精心编写,因此边界检查是合法的,并且他的代码包含未定义的行为,即使它适用于大多数编译器。
  • @JamesKanze,在大多数 today 编译器上;我关心的是未来版本或使用不同命令行开关调用的同一版本。
  • @18446744073709551615 对于它的价值,我相信CenterLine曾经有一个编译器做了边界检查,并且会导致程序在这种情况下终止。它不适合生产使用,因为它运行速度要慢得多,但它很像大多数现代 C++ 编译器中的检查迭代器。
【解决方案3】:

您似乎对“未定义行为”的含义有一个奇怪的想法。编译器不需要识别导致未定义行为的代码,如果他们这样做了,他们不需要对它做任何特别的事情。事实上,一种非常合理的方法是根本不做任何特别的事情。不确定性与编译器可能生成的机器代码无关,而是与运行它的效果有关。

话虽如此,数组和指针之间有一个微妙但非常重要的区别。数组(例如char text[1])有关联的存储,而指针(char *textchar text[])没有。

如果我访问 m->text[3] 会发生什么? (注意它是如何声明的。)编译器会将其视为未定义的行为吗?

嗯,first 未定义的行为与之前的声明相关:

strcpy(&m->text,s);

m->text 指的是长度为 1 的 char 数组,strcpy() 将写入其末尾。就定义而言,您在为您的结构分配的块中保留额外空间的事实是无关紧要的。在实践中,它可能会按照您的意愿行事,但依赖它是一个糟糕的主意。

同样适用于访问m-&gt;text[3]。编译器更有可能识别越界访问,但即使识别,其效果也可能是您想要的。 “可能”,但不是确定。这就是为什么要避免依赖未定义行为的原因。

【讨论】:

  • char text[] 不是指针,function parameter list 除外。指针确实有相关的存储:存储指针值所需的内存量。 &amp;m-&gt;text 是类型不匹配,代码格式不正确。
猜你喜欢
  • 2014-08-23
  • 1970-01-01
  • 1970-01-01
  • 2015-12-30
  • 2021-10-06
  • 1970-01-01
  • 1970-01-01
  • 2011-11-12
  • 2019-10-07
相关资源
最近更新 更多