为什么在堆和栈上分配的数组会表现不同?

4

我正在检查在C++中两个二维数组的行为,一个是从堆栈分配,另一个是从堆分配。

我创建了两个相同形状的二维数组,并将这些数组填充了一些数据。然后我尝试用两种不同的方法读取数组,第一种方法是使用简单的数组索引格式“Arr [ROW] [COLUMN]”。然后我使用指针解引用来读取数组,并且我得到了堆分配数组的两个不同结果,但对于堆栈分配数组,结果相同。我正在尝试理解为什么结果不同。如有任何澄清,我将不胜感激。提前谢谢。

我运行的代码如下:

#include <iostream>

using namespace std;

int main(){

    int rows = 6;
    int columns = 3;

    // allocate from the stack.
    double q[rows][columns];

    // allocate from the heap.
    double ** a;
    a = new double*[rows];

    for(int i = 0; i < rows; ++i){
        a[i] = new double[columns];
    }

    // populate the arrays.
    for(int i = 0; i < rows; ++i){
        for(int j = 0; j < columns; ++j){
            a[i][j] = columns*i+j;
            q[i][j] = columns*i+j;
        }
    }

    cout << "*****************" << endl;
    cout << "Array indexing method." << endl;
    cout << "*****************" << endl;

    // print the heap allocated array using array indexing.
    for(int i = 0; i < rows; ++i){
        for(int j = 0; j < columns; ++j){
            cout << a[i][j] << '\t';
        }
        cout << endl;
    }

    cout << "*****************" << endl;

    // print the stack allocated array using array indexing.
    for(int i = 0; i < rows; ++i){
        for(int j = 0; j < columns; ++j){
            cout << q[i][j] << '\t';
        }
        cout << endl;
    }

    cout << "*****************" << endl;
    cout << "Pointer dereferencing method." << endl;
    cout << "*****************" << endl;

    // print the heap allocated array.
    for(int i = 0; i < rows; ++i){
        for(int j = 0; j < columns; ++j){
            cout << *(&a[0][0] + columns*i + j) << '\t';
        }
        cout << endl;
    }

    cout << "*****************" << endl;

    // print the stack allocated array.
    for(int i = 0; i < rows; ++i){
        for(int j = 0; j < columns; ++j){
            cout << *(&q[0][0] + columns*i + j) << '\t';
        }
        cout << endl;
    }
    cout << "*****************" << endl;

    // release the memory allocated to the heap.
    for(int i = 0; i < rows; ++i){
        delete[] a[i];
    }

    delete a;

    return 0;
}

我获得的结果如下:

*****************
Array indexing method.
*****************
0       1       2
3       4       5
6       7       8
9       10      11
12      13      14
15      16      17
*****************
0       1       2
3       4       5
6       7       8
9       10      11
12      13      14
15      16      17
*****************
Pointer dereferencing method.
*****************
0       1       2
0       3       4
5       0       6
7       8       0
9       10      11
0       12      13
*****************
0       1       2
3       4       5
6       7       8
9       10      11
12      13      14
15      16      17
*****************

在第三个输出块中,我可以看到堆分配的数组没有被正确地读取,但是栈分配的数组被正确读取了。
再次感谢。

3
double q[rows][columns]; 这是使用非标准的 C++ 变长数组(VLA)。 - user0042
这不是问题,但你真的需要 std::endl 额外的功能吗?'\n' 就可以换行。 - Pete Becker
@JoshM. -- 相关信息,如果你想要一个连续的二维数组并保持 [ ][ ] 语法。 - PaulMcKenzie
@JoshM。-- 另外,请注意使用我提供的SO链接中的代码时,创建的double **数组显示与您发布的(非标准)VLA版本相同,在此处查看 - PaulMcKenzie
@JoshM。真正的问题在于您如何从堆中分配内存。您可以从堆中分配内存,拥有连续的数据,并仍然使用double **。您的尝试以一种天真的方式分配数组——每行一个分配。 - PaulMcKenzie
显示剩余3条评论
3个回答

11

&q[0][0] 会给你一个指向包含 rowscolumns 列的双精度块中第一个双精度数的指针。而 &a[0][0] 则会给你一个指向包含 columns 列双精度数的块中第一个双精度数的指针(你使用 a[0] = new double[columns]; 分配了它,记得吗?)。因此,访问它时 columns*i + j 将会越界并导致未定义行为。


3
我的问题在于我假设从堆中分配的内存是连续的,但实际上并非如此吗?每行列应该都很接近,因为我的输出结果中得到了一些正确的值,只是它们的位置不对。 - Josh M.
2
@JoshM。你的堆分配完全不是连续的。你分配了一个包含“行”指针到双精度的块,然后是“列”数量的双精度块,每个块都有“行”计数。虽然在这种特定情况下它们是依次分配的(之间有一些填充),因为堆尚未混乱。但是,人们不应该依赖于任何关于分配器行为的假设。 - user7860670
你可以在回答中加入这个:即使在堆栈分配的数组上,"指针解引用方法"也会调用UB,因为它超出了第一个子数组的边界。即使内存是连续的,这仍然是非法的。 - Todd Fleming

3

数组通常是单一的分配,比如在这个例子中是在堆栈上。数组值q[1][0]被放置在q[0][columns-1]之后。指针实现从堆上为每一行分配了一个不连续的内存块。如果超出第一行的末尾进行引用,则处于未定义行为状态。


2
堆栈数组是完全连续的内存。而基于堆的数组则是每行一个分配,再加上一个分配来容纳所有这些分配。每个分配的内存可以散布在堆中的任意位置,彼此之间有不同的间距。如果超过第一行分配末尾并试图通过增加偏移量来访问下一行分配,那么你不能假定这是有效的,并且这肯定是一个错误,会导致程序出现 bug。
如果你想要实现你的目标,请把 new[] 表达式改为平铺成单个数组:
double * q = new [rows * columns];

那么您可以像访问2D数组一样对其进行索引。

此外,在您的原始帖子中,您使用了错误类型的删除调用a,它也是一个数组,并且需要作为一个数组进行删除(在其后面加上[])。


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