使用正确的方法在堆上声明静态连续多维数组(C++)

4
如何在堆上声明一个多维数组,在运行时不改变大小?(最好使用C++11,如果有一些只有在C++14中可用的功能(而不是C++17),我也很乐意听到,但可能对我没有用)。
我已经查看了数十个关于这个主题的问题和答案,但没有一个真正回答它/有些答案与其他答案冲突。
我找到的以下解决方案及其似乎具有的问题使它们不可行(其中大部分来自SO答案及其评论,以下示例都假定3D数组为目标):
普通[] [] []数组声明为new /声明指针数组 问题:内存不连续,每个单独的数组在内存中具有独立位置
多个嵌套在一起的std :: arrays / boost :: arrays 问题:内存不连续,每个单独的数组在内存中具有独立位置
矩阵 问题:仅为std :: array的容器,基本上适用于相同的问题
多个嵌套在一起的std :: vectors 问题:动态,几乎所有先前提到的问题
将其声明为指向普通[]数组的单个块,然后通过运行时计算索引的函数(如GetIndex(array,x,y,z))浏览索引 问题:这似乎适用于所有要点,但是当您需要经常访问/更改元素时,此解决方案似乎不理想,因为它引入了显着的CPU开销
与此有一点无关,如果它们在类中,并且我必须使用.运算符从外部访问它们的值,则我也遇到了一些问题,因此如果有人能告诉正确的解决方案,并提供正确声明和正确访问堆分配的多维数组作为类成员的示例,我将非常感激。

1
编译时大小已知还是在运行时不变?您是否需要能够在整个数组中进行一维指针算术运算? - Davis Herring
1
但是这个解决方案似乎不太理想,因为它会带来显著的CPU开销。您有测量过这种开销吗?从组件计算索引是一个简单的算术操作。也许它的性能很适合,您不需要在这样平凡的任务上增加程序复杂度。 - Ari0nhh
2
"static"和"在堆上"是互相矛盾的。你指的是哪一个? - user207421
1
你需要一个二维数组还是三维数组?你可以通过声明和分配一个指向int [width]数组的指针来简单地声明一个在内存中连续的二维数组。例如,一个5x10的连续数组可以这样声明:int (*arr)[10] = new int [5][10]; 这提供了直接的二维数组寻址方式,例如在连续块中使用 arr[i][j] - David C. Rankin
1
是的,本质上你正在为5个5x10数组分配存储空间,作为一个单独的内存块。您可以转储每个元素的指针以进行确认。这是一个指向5x10数组的单指针,您可以使用第三维度为所需数量分配存储空间。 - David C. Rankin
显示剩余15条评论
4个回答

1
正确的方法是编写/使用多维数组类。多维数组是计算机科学中的基本对象,但令人不可思议的是,STL从未包含对多维数组的一流支持。在内部,该类应在堆上分配一个1d数组(用于运行时大小的数组),并执行算术运算以将多维索引转换为1d索引。
如果您正在进行数值工作,则Eigen是一个不错的选择;不确定它对非数值类型的多维数组有多大用处。

谢谢您的回答。您能说一下为什么这个解决方案比David Rankin在评论中提供的解决方案更好吗? - uncanny
David的解决方案要求在编译时知道数组除第一个维度外的所有维度,因为它们是arr类型声明的一部分。 - japreiss

1

使用new声明普通的[][][]数组/声明指针数组 问题:在内存中不连续,每个独立的数组都有自己独立的位置

...

是的,大小始终是通过 #define 声明的。- uncanny

是的,C++ 多维数组很棘手,而且很容易让 C++ 代码难以阅读。实际上,如果在编译时知道大小,您可以创建静态多维数组,因此您将获得一个连续的内存块。

int main()
{
    int arr[100][200][100]; // allocate on the stack

    return 0;
}

问题是如何分配到堆上...没问题,只需将其包装为结构体,并在堆上分配此结构体。
#include <memory>

struct Foo
{
    int arr[100][200][100];
};

int main()
{
    auto foo = std::make_unique<Foo>(); // allocate on the heap
    auto& arr = foo->arr;

    arr[1][2][3] = 42;

    return 0;
}
< p > std::make_unique 调用在堆上分配 Foo,并保证内存将被释放。此外,您可以很少使用样板代码访问 Foo 内部和外部的数组。 很好!< /p >

int (*arr3)[5][10] = new int [5][5][10]; int (*arr3)[5][10] = 新的 int [5][5][10]; - uncanny
@uncanny 哦,自动内存管理+更少的丑陋语法,当然。 - Stas
1
@uncanny int (*arr3)[5][10] = new int [5][5][10] 需要在适当的位置调用 delete[],会丢失最外层维度的大小,并且将变量名隐藏在类型中间。 - Caleth
@uncanny 你是指语法吗?两者都有助于消除“指向数组”的丑陋语法或“引用数组”的语法。 - Stas
请注意,在许多实现中,将8 MB用于堆栈变量太多了。 - Davis Herring
显示剩余5条评论

0
如果除了第一维以外的所有维度都是编译时常量(无论第一维是否为常量),只需编写 new T[x][Y][Z] 或(更安全地)std::make_unique<T[][Y][Z]>(x)。结果是连续的,编译器可以尝试应用诸如移位而不是乘法之类的技巧,以适合维度。您不能使用这样的实体将其作为指针和大小传递给期望一维数组的函数:
f(&a3[0][0][0],x*Y*Z);  // undefined behavior

因为指针算术仅在一个 T[] 数组内定义(这里是数组 a3[0][0],它是一个 T[Z])。

如果第一维也是常量,则可以使用嵌套的 std::array(在实践中没有额外的内存开销)或者只需

struct A3 {
  T a[X][Y][Z];
};

无论是传递和返回,还是用作标准容器元素,都有其优势。这样的对象当然也可以通过引用传递,或者您可以使用“数组参数”:

T f(A3 &a3) {
  return a3.a[0][0][1]+a3.a[0][1][0]+a3.a[1][0][0];
}
T g(T a[][Y][Z]) {
  return a[0][0][1]+a[0][1][0]+a[1][0][0];
}

请注意,g的参数类型实际上是T (*)[Y][Z],这就是为什么第一个限制可以省略。如果您有...
A3 *a3;

你会称之为

f(*a3);
g(a3->a);

但这只是标准指针用法,与数组类型或堆分配无关。


那么,如果我选择结构体选项,如果我声明了 MyClass.A3 数组*; ,将它作为指针与 MyClassInstance.array 一起传递到函数(A3 *array); 中,在内部,我只需要对其进行 *array.a[X][Y][Z] = value; 就可以了,这就是你的意思吗?如果不是,请提供一个正确使用的示例。 - uncanny
@uncanny:你评论中的语法有几个问题,我加了一个例子,但不要指望它可以替代一本专业书籍。 - Davis Herring
好的,这并不完全是我所要求的,因为我已经意识到了所有这些,我只是快速地写了一些东西,指向我原来问题中的最后一段,但还是感谢您编辑答案的努力。 - uncanny

-1

内存几乎总是一维的。我们在编程语言中看到的多维数组实际上是编译器创建的幻觉。

让我们来举个例子。

int numbers_1[10];
int numbers_2[2][5];

这两个都为10个整数分配足够的存储空间,而且不考虑类型安全 - 就像在C/C++中一样 - 从处理器的角度来看是等效的。实际上,您可以通过类型转换指针将一个转换为另一个 - 将任意维度视为所需 -。

numbers_1[0] == ((int**)numbers_1)[0][0];

这个表达式是正确的,对于所有10个元素都适用。

数组的存储类别并不重要,它们实际上都只是一维的。

此外,我认为如果你了解图灵机的工作原理会有所帮助。即使在通用图灵机中,纸带也是一维的,而这是我们拥有的最强大的理论机器。


这取决于您所考虑的用例。我只是在解释多维数组是如何产生幻觉的。您可以使用“new”分配足够大的内存块,然后将其视为具有k个维度。但这会丢失编译器和库可能提供的所有安全性,因此,如果您成功地将自己射在脚或面部,那么除了自己之外,没有其他人可以责怪。 - Tanveer Badar
请参见最后一段。 - Tanveer Badar
1
我认为这更像是一条评论,因为它并没有直接回答问题,而我确实意识到了这一点,问题只是关于最好的管理方式。与std::vector等相比,数组确实保证了内存是一个单一的块,即使是多维的情况下也是如此。你的解决方案可能比我在问题中提到的第五点更好,或者更像是对其的改进,但正如你正确地解释的那样,它可能仍然不是传统意义上的“完美”,因为你几乎放弃了语言提供给你的每一个工具。 - uncanny
3
((int**)numbers_1)[0]是错误的(并且是未定义行为)。你可能想表达的是(reinterpret_cast<int (&)[2][5]>(numbers_1))[0][0](但这也是未定义行为)。 - Jarod42
2
@TanveerBadar:强制转换本身没问题;通常的TBAA规则禁止使用结果(除了将其转换回来)。如果我们忽略UB,你的版本(带有int **)将查看数组的开头,将前4/8个字节解释为int *,然后(尝试)从那个完全虚构的位置读取一个int。Jarod42的版本在没有这个UB的世界中可以正常工作;它重新塑造了数组,而不是假设指针存在于内存中。 - Davis Herring
显示剩余7条评论

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