【问题标题】:Why are variable length arrays in C++ overlapping on some sizes?为什么 C++ 中的可变长度数组在某些大小上重叠?
【发布时间】:2021-08-03 02:14:51
【问题描述】:

在这里提供一个问题的答案后,我正在测试我编辑的this code 并注意到一些奇怪的行为:

#include <iostream>
#define MAX 100
using namespace std;

int main()
{
    int size = 0;
    int array[MAX];
    int i, j;
    int input;

    cout << "Array: ";
    for(i = 0; i < MAX; i++)
    {
        cin >> input;

        if(input == -1)
            break;
        else
        {
            array[i] = input;
            size++;
        }
    }

    cout << "Size: " << size << "\n\n";


    int left[size / 2];
    int right[size / 2];

    for(i = 0; i < size / 2; i++)
        left[i] = array[i];
    for(i = size / 2, j = 0; i < size; i++, j++)
        right[j] = array[i];

    cout << "Left: ";
    for(i = 0; i < size / 2; i++)
        cout << left[i] << ' ';
    cout << '\n';

    cout << "Right: ";
    for(i = 0; i < size - size / 2; i++)
        cout << right[i] << ' ';
    cout << '\n';

    return 0;
}

此代码应该将数组拆分为两个单独的数组。当这些是输入时,输出是错误的:

1 2 3 4 5 6 7 8 9 -1
Left: 9  2  3  4
Right: 5  6  7  8  9

调试后如果left的元素是这样打印的:

for(i = size / 2, j = 0; i < size; i++, j++)
{
    right[j] = array[i];
    cout << left[0] << ' ';
}
cout << '\n';

表示left[0]的值在第5次迭代后被修改:

1 1 1 1 9
Left: 9 2 3 4
Right: 5 6 7 8 9

这只发生在数组大小为 9 时。我还没有测试超过 16。我可以修复代码,使其具有正确的大小

int right[size - size / 2];

或使用malloc() 来遵守 C++ 标准,

int *left = (int *) malloc(sizeof(*left) * n / 2);
int *right = (int *) malloc(sizeof(*left) * n / 2);

这样left 就不会受到影响,但这不是我要问的。为什么只有在拆分大小为 9 的数组时才会发生这种情况?为什么left[0] 会被覆盖?这是应该报告的 g++ 中的错误还是其他问题?

【问题讨论】:

标签: c++ arrays g++ variable-length-array


【解决方案1】:

表示left[0]的值在第5次迭代后被修改:

这就是你的答案。问题出现在对具有 四个 元素的数组进行的第五次 迭代中。

size 为奇数时,size/2 的计算会向下取整。所以总和size/2 + size/2 严格小于size,但您的循环确保原始数组中的所有size 元素都分配到某处。必须将某些东西分配到意外的位置。我们将此称为“未定义行为”,根据 C++ 标准,编译器此时所做的任何事情都是正确的。 (无论发生什么,编译器都会为此责备你的代码。)碰巧当size9 时,编译器使用left[0] 作为right[4] 的位置。

在幕后,leftright 数组在内存中可能或多或少是相邻的。布局将有right[0]right[size/2],然后可能是一些未使用的空间(也称为“填充”),然后是left[0]left[size/2]。当您访问right 的最后一个元素时,您最终会进入未使用的空间或left[0]。当您覆盖未使用的空间时,您看不到任何症状,因为该空间未使用。但是,当您覆盖 left[0] 时,您肯定会看到症状。

您的编译器显然使用填充来确保数组与4*sizeof(int) 对齐。 (这样必须更快,因为编译器很少无缘无故地引入浪费。不过,我很惊讶它不是2*sizeof(int)。)也就是说,当size/24 的倍数时没有填充。如果这个猜测是准确的,当size 是奇数并且size/24 的倍数时,您应该会看到这种行为;即size 是 8 的倍数的 1,例如 9、17、25、33 等。

【讨论】:

  • 解释:for(i = size / 2, j = 0; i &lt; size; i++, j++)size == 9 的情况下解析为for(i = 4, j = 0; i &lt; 9; i++, j++)。这将在 4、5、6、7 和 8 上迭代 i,总共进行五次迭代。这意味着j的范围是0到4,没有right[4]
  • @user4581301 所以right[4] 可以在任何地方,而在left[0] 数组大小为9 时是巧合吗?
  • 没错。如果系统的自动存储是基于堆栈的,并且大多数是基于堆栈的,那么当您写入 right[4] 时,您会覆盖(也称为“粉碎”)堆栈中 right 周围的一些变量。
  • 很难说,但很可能是padding的结果。出于性能原因,变量通常是对齐的,以使它们更容易和更快地访问。如果数组有 5 个元素长并且编译器将数据对齐到 64 位,则rightleft 之间可能有一个空的 4 个字节。也许不是。专门针对未定义的行为进行推理没有太大价值,因为它是未定义的。无论您学到什么,都可能只适用于为该操作系统构建的那个 CPU 上的那个编译器。
  • 旁注:如果您正在为一个操作系统/编译器/硬件平台编写代码,您可能会滥用 UB 的生活地狱。您经常会在编译器附带的 C++ 标准库实现中看到一些非常狡猾的代码,因为该库是专门为该编译器编写的,并且编写者对编译器非常熟悉,并且知道他们什么时候可以摆脱一些 UB以获得巨大的性能收益。 UB 的另一个用途是插入调试代码。 Visual Studio 将范围检查添加到其调试库中,因此您有时可以及早发现不良行为。
猜你喜欢
  • 2013-03-31
  • 1970-01-01
  • 2018-11-07
  • 2011-02-20
  • 2013-06-23
  • 2018-03-26
  • 1970-01-01
  • 1970-01-01
  • 1970-01-01
相关资源
最近更新 更多