多维数组是否会在 C and/or C++ 中引起任何问题?

Do multi-dimensional arrays cause any problems in C and/or C++?

我知道这个问题乍一看有点好笑。但是当我遇到这个 question I´ve found a comment, of @BasileStarynkevitch 时,一位 C 和 C++ 高级用户在其中声称多维数组不应该被优先使用,无论是在 C 中还是在 C++ 中:

Don't use multi-dimensional arrays in C++ (or in C).

为什么?为什么我不应该在 C++ 或 C 中使用多维数组?

他这句话是什么意思?


此后,另一位用户回复了这条评论:

Basile is right. It's possible to declare a 3D array in C/C++ but causes too many problems.

哪些问题?

我经常使用多维数组,并没有发现使用它们的缺点。相反,我认为它只有优点。

好吧,提到的 "problems" 没有正确使用结构,离开了数组的一个或另一个维度的末尾。如果您知道自己在做什么并仔细编码,它将完美运行。

我经常使用多维数组在 C 和 C++ 中进行复杂的矩阵操作。它经常出现在信号分析和信号检测以及用于分析模拟中的几何形状的高性能库中。我什至没有将动态数组分配视为问题的一部分。即使这样,对于具有重置功能的某些边界问题,通常大小的数组也可以节省内存并提高复杂分析的性能。可以将缓存用于库中较小的矩阵操作,将更复杂的 C++ OO 处理用于基于每个问题的较大动态分配。

与大多数数据结构一样,有 "right" 时间使用它们,还有 "wrong" 时间。这在很大程度上是主观的,但出于这个问题的目的,我们假设您在一个没有意义的地方使用二维数组。

即是说,我认为有两个值得注意的原因可以避免 在 C++ 中使用多维数组,它们主要基于数组的用例。即:

1.慢(呃)内存遍历

可以连续访问诸如 i[j][k] 的二维数组,但计算机必须花费额外的时间来计算每个元素的地址 - 比在一维数组上花费的时间更多。更重要的是,迭代器在多维数组中失去了可用性,迫使您使用速度较慢的 [j][k] 表示法。简单数组的一个主要优点是它们能够按顺序访问所有成员。这部分丢失了 2+D 数组。

2。大小不灵活

这只是一般数组的问题,但调整多维数组的大小对于 2、3 或更多维度会变得更加复杂。如果一个维度需要改变大小,则必须复制整个结构。如果您的应用程序需要调整大小,最好使用多维数组以外的结构。

同样,这些都是基于用例的,但这些都是使用多维数组可能出现的重要问题。在上述两种情况下,还有其他解决方案比多维数组更好。

您不能同时回答 C 和 C++ 的这个问题,因为这两种语言及其对多维数组的处理之间存在根本差异。所以这个答案包含两部分:


C++

多维数组在 C++ 中非常无用,因为您不能为它们分配动态大小。除了最外层的所有维度的大小必须是编译时常量。在我遇到的几乎所有多维数组用例中,大小参数在编译时根本就不知道。因为它们来自图像文件的尺寸,或者一些模拟参数等

可能存在一些特殊情况,其中维度实际上在编译时已知,在这些情况下,在 C++ 中使用多维数组没有问题。在所有其他情况下,您将需要使用指针数组(设置起来很乏味)、嵌套 std::vector<std::vector<std::vector<...>>>,或者使用手动索引计算的一维数组(容易出错)。


C

自 C99 以来,C 允许具有动态大小的真正多维数组。这称为 VLA,它允许您在堆栈和堆上创建完全动态大小的多维数组。

但是,有两个问题:

  • 您可以将多维 VLA 传递给函数,但不能 return。如果要将多维数据传递出函数,必须 return 通过引用传递。

    void foo(int width, int height, int (*data)[width]);  //works
    //int (*bar(int width, int height))[width];  //does not work
    
  • 你可以在变量中有指向多维数组的指针,你可以将它们传递给函数,但你不能将它们存储在结构中。

    struct foo {
        int width, height;
        //int (*data)[width];  //does not work
    };
    

这两个问题都可以解决(通过引用 return 多维数组,并将指针存储为结构中的 void*),但这并非微不足道。由于它不是一个经常使用的功能,因此只有极少数人知道如何正确使用它。


编译时数组大小

C 和 C++ 都允许您使用编译时维度已知的多维数组。这些没有上面列出的缺点。

但是它们的用处大大降低了:在很多情况下您想要使用多维数组,而您没有机会在编译时知道所涉及的大小。一个例子是图像处理:在打开图像文件之前,您不知道图像的尺寸。与任何物理模拟类似:在您的程序加载其配置文件之前,您不知道您的工作域有多大。等等

因此,为了有用,多维数组必须支持动态大小恕我直言。

这是一个相当广泛(且有趣)的性能相关主题。我们可以讨论缓存未命中、多维数组的初始化成本、向量化、堆栈上多维 std::array 的分配、堆上多维 std::vector 的分配、后两者的访问等等....

就是说,如果您的程序可以很好地处理多维数组,请保持原样,尤其是如果您的多维数组具有更高的可读性。

性能相关示例:

考虑一个 std::vector,其中包含许多 std::vector<double>:

std::vector<std::vector<double>> v;

我们知道 v 中的每个 std::vector 对象都是连续分配的。此外,std::vector<double> in v 中的所有元素都是连续分配的。然而,并非 v 中的所有 double 都在连续内存中。因此,根据您访问这些元素的方式(多少次,以什么顺序,......),与单个 std::vector<double> 相比,std::vector of std::vector 可能非常慢包含连续内存中的所有 double

矩阵库通常将 5x5 矩阵存储在大小为 25 的普通数组中。

这些陈述广泛适用,但不普遍。如果你有静态边界,那很好。

在 C++ 中,如果你想要动态边界,你不能有一个连续的分配,因为维度是类型的一部分。即使您不关心连续分配,也必须格外小心,尤其是在您希望调整维度大小时。

更简单的是在一些容器中有一个单一的维度来管理分配,以及一个多维的视图

鉴于:

std::size_t N, M, L;
std::cin >> N >> M >> L;

比较:

int *** arr = new int**[N];
std::generate_n(arr, N, [M, L]()
{ 
    int ** sub = new int*[M];
    std::generate_n(sub, M, [L](){ return new int[L]; });
    return sub;
});

// use arr

std::for_each_n(arr, N, [M](int** sub)
{ 
    std::for_each_n(sub, M, [](int* subsub){ delete[] subsub; });
    delete[] sub;
});
delete[] arr;

与:

std::vector<int> vec(N * M * L);
gsl::multi_span arr(vec.data(), gsl::strided_bounds<3>({ N, M, L }));

// use arr