数组作为模板参数:堆栈还是堆?

6

就我对堆栈与堆的了解,很基础,但当涉及到数组时,从我的了解来看,类似下面这样的东西是在堆栈上创建的

float x[100];

而像这样的东西是在堆上创建的

float* x = new float[100];

但是如果我创建一个模板数组类,并将其传递到“堆栈”数组类型中(如float[100]),会发生什么?例如:

#include <iostream>

using namespace std;

template <class T>
class Array {
public:
    int size;
    T* data;

    Array(int size_) : size(size_) {
        data = new T[size];
    }

    ~Array() {
        delete [] data;
    }
};

int main() {
    int m = 1000000;
    const int n = 100;
    Array<float[n]>* array = new Array<float[n]>(m);

    for (int i = 0; i < m; i++)
        for (int j = 0; j < n; j++)
            array->data[i][j] = i * j;

    cout << array->data[10][9] << endl;
    delete array;
}

这里到底是什么情况?这个内存是在堆栈上创建的还是在堆上创建的?我猜测应该是在堆上,但是它是如何工作的呢?编译器会分配一个大的内存块,然后将指向每个 n 元素的指针存储进去吗?还是它会分配许多较小的内存块(不一定是连续的),并将指针存储到每个块中?
此外,我似乎不能在没有模板的帮助下完成这个操作。具体来说,这段代码无法编译:
int m = 1000;
const int n = 100;
(float[n])* array = new (float[n])[m];

这里发生了什么事情?

编辑:

感谢大家提供的语法提示。我真正感兴趣的是块内发生了什么。

int m = 1000;
const int n = 100;
float (*array)[n] = new float[m][n];

但是我不知道如何在没有使用模板的情况下编写它。我真正感兴趣的一件事是,如果编译器将其分配为堆上的一个大块,如何使用语法array[i][j]访问特定元素而不存储每个第n个元素的指针?然后我意识到,由于n是常量,sizeof(float[n])是固定的,因此当您创建数组时,编译器正在分配一个包含m个元素的数组,其中每个元素都是一个float[n],在我的情况下是100 * 4 = 400字节。现在一切都有意义了。谢谢!


2
你违反了三大法则。我知道这与重点无关,但养成一个好习惯是很重要的,当你看到或输入析构函数定义时自动触发警报并让你考虑复制和赋值。 (如果你不想麻烦,可以在C++03中将它们声明为私有或在C++11中删除它们)。 - aschepler
4个回答

3
Array<float[n]>* array = new Array<float[n]>(m);

这里发生了两个堆分配的情况。由于使用了new来创建Array对象,因此Array对象将在堆上分配。new-expression调用Array构造函数,该构造函数再次使用new来分配数组data,因此data也在堆上分配。
最好这样做:
Array<float[n]> array(m);

这将在堆栈上分配数组(因此它将在块的末尾自动销毁)。但是,虽然数组对象本身在堆栈上,但数据仍存储在堆上,因为在Array构造函数中分配了堆。当您有一个std :: vectorstd :: string局部变量时,类似的情况也会发生。
此外,我似乎无法在没有模板的帮助下完成此操作。具体来说,此代码无法编译:
这只是因为您的语法错误。正确的语法是:
float (*array)[n] = new float[m][n];

左侧展示了声明一个指向数组的正确方法。右侧需要一个由 mfloat[n] 数组组成。这可以用 float[m][n] 表示;注意,[m] 不应该放在末尾。


2

您的数组范围写反了。正确的写法是:

  int m = 1000;
  const int n = 100;
  float (*array)[n] = new float[m][n];
  delete[] array;

如果您想保持数组的范围顺序,可以使用类型别名或适当的模板:

  using A = float[n];
  A* array = new A[m];

或者

// at file scope
template<typename T, unsigned N> using add_extent = T[N];
// ...
  add_extent<float, n>* array = new add_extent<float, n>[m];

无论是在堆栈上还是在堆上分配的多维数组都被分配为一个大小为 m*n 的单个块。当你索引数组类型的指针(例如float (*array)[n])时,指针按照数组类型的步幅逐次增加 n 元素。

我曾经看到过将多维数组作为单独的块来处理:float** data2 = new float*[m]; for (int i = 0; i < m; i++) data2[i] = new float[n]; - hunse
@hunse 这是可能的,但与单个块相比效率低下。请注意,float ** 是与 float (*)[N] 不同的类型。 - ecatmur
@hunse 如果你正在考虑具有多个变量维度的数组,那么仍然有库解决方案可以为你提供单一分配,并为你执行索引算术运算,主要是Boost.MultiArray:http://www.boost.org/doc/libs/1_55_0/libs/multi_array/doc/user.html - ecatmur
我刚刚尝试了boost::multi_array,对于我所尝试的基本创建和数据写入任务,它比我自己在堆上制作平坦数组并自行索引要慢得多。无论我是不使用优化还是使用-O3(使用g ++)都是如此。编辑:我应该补充说,使用-O3时,编写部分仅略慢,但分配对象的速度要慢得多,这甚至比分配为单独块的数组更慢,这很令人惊讶。 - hunse
谢谢。你的陈述“指针每次增加n个元素,根据数组类型的步幅”真的帮助我理解了正在发生的事情。 - hunse

1

在这行中

Array<float[n]>* array = new Array<float[n]>(m);

堆上分配了一个Array<T>的实例。鉴于你关于一般分配的第一条声明,你已经理解了这一点。

也许令人困惑的是将float[n]用作模板参数?

模板参数T,如你对Array的定义中所示,由关键字class表示类型。它本身与任何形式的分配都没有关系。

作为演示,让我们编写一个简单的模板,不使用其参数:

#include <cassert>

using namespace std;

template <typename T>
class A {
};

int main(){

    A<float[100]> a1;
    A<float[1000]> a2;
    float f[100];

    assert(sizeof(a1) == sizeof(a2));
    cout << "a1 : " << sizeof(a1) << endl;
    cout.<< "f : " << sizeof(f) << endl;
}

输出:

a1 : 1
f : 400

因此,这里的float[n]确实是一种类型(1)

另一方面,当您使用关键字new时,您知道正在分配堆上的某些内容。所以正如我所说,array变量将指向堆中的内存块。此外,模板本身包含一个堆分配(同样是关键字new)。

最后,我想澄清基本前提,即new表示堆分配。虽然默认情况下是这种情况,但在placement模式下使用时,实际分配很可能在栈上。


请注意,C++接受这种方式是因为n被声明为常量,因此结果类型可以在编译时计算。如果移除nconst特性,编译器将报错。

此外,您的另一个问题已经在其他答案中得到了很好的解答。 - didierc

1

所有的内存都放在堆上。编译器为数组分配了一个巨大的内存块,并设置索引以使其可访问。

另外,如果有人复制或分配你的Array类,你会泄漏内存和/或重复删除。


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