【问题标题】:Array as template parameter: stack or heap?数组作为模板参数:堆栈还是堆?
【发布时间】:2014-09-08 19:31:30
【问题描述】:

与堆相比,我对堆栈的了解非常初级,但是当涉及到数组时,据我所知,这样的东西是在堆栈上创建的

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],在我的情况下是@ 987654333@ 字节。现在一切都说得通了。谢谢!

【问题讨论】:

  • 你违反了三法则。我知道这与重点无关,但是查看或键入析构函数定义会自动敲响警钟并让您考虑复制和分配是一个好习惯。 (如果您不想打扰,请在 C++03 中将它们声明为私有或在 C++11 中删除)。

标签: c++ arrays templates heap-memory stack-memory


【解决方案1】:
Array<float[n]>* array = new Array<float[n]>(m);

这里发生的是两个堆分配。 Array 对象将在堆上分配,因为您使用 new 创建它。 new-expression 调用Array 构造函数,该构造函数再次使用new 分配数组data;因此data 也分配在堆上。

最好这样做:

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

这会在堆栈上分配array(因此它将在块的末尾自动销毁)。然而,虽然array 对象本身在堆栈上,但数据仍然存储在堆上,因为它是在Array 构造函数中分配在堆上的。这类似于当您有一个 std::vectorstd::string 局部变量时发生的情况。

此外,如果没有模板的帮助,我似乎无法做到这一点。具体来说,这段代码无法编译:

这只是因为您的语法错误。正确的语法是:

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

左侧显示了声明指向数组的指针的正确方法。对于右侧,您需要mfloat[n]s 的数组。这表示为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 &lt; m; i++) data2[i] = new float[n];
    • @hunse 这是可能的,但与单个块相比效率低下。请注意,float**float (*)[N] 的类型不同。
    • @hunse 如果您正在考虑具有多个可变维度的数组,那么仍有一些库解决方案可以为您提供单一分配,并为您执行索引运算,主要是 Boost.MultiArray:boost.org/doc/libs/1_55_0/libs/multi_array/doc/user.html
    • 我刚刚尝试了boost::multi_array,对于基本的创建和数据写入任务,我尝试了它比在堆上创建自己的平面数组并自己进行索引要慢得多。无论我不使用优化还是-O3(使用g++)都是如此。编辑:我应该跟进-O3,写作部分只是稍微慢一点,但分配对象要慢得多。甚至比将数组分配为单独的块还要慢,这令人惊讶。
    • 谢谢。您的陈述“指针一次递增 n 个元素,按照数组类型的步幅”确实帮助我理解了发生了什么。
    【解决方案3】:

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

    顺便说一句,如果有人复制或分配您的 Array 类,您将泄漏内存和/或双重删除。

    【讨论】:

      【解决方案4】:

      排队

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

      Array&lt;T&gt; 的实例在堆上分配。鉴于您对一般分配的第一个陈述,您已经理解了这一点。

      也许令人困惑的部分是使用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 模式下使用时,实际分配很可能在堆栈上。


      (1) 请注意,C++ 接受它是因为n 被声明为常量,因此可以在编译时评估结果类型。删除n 定义的const trait,编译器会报错。

      【讨论】:

      • 另外,您的其他问题已在其他答案中得到很好的解决。
      猜你喜欢
      • 1970-01-01
      • 2021-06-19
      • 2019-09-16
      • 2015-05-03
      • 1970-01-01
      • 2018-12-23
      • 2011-11-08
      • 1970-01-01
      相关资源
      最近更新 更多