如何在内存中连续分配可变大小的结构?

时间:2009-10-26 19:28:56

标签: c++ memory-management placement-new

我正在使用C ++,我有以下结构:

struct ArrayOfThese {
  int a;
  int b;
};

struct DataPoint {
  int a;
  int b;
  int c;
};

在内存中,我想在每个DataPoint的末尾有一个或多个ArrayOfThese元素。每个DataPoint并不总是具有相同数量的ArrayOfThese元素。

因为我有一些荒谬的DataPoints要汇编然后通过网络传输,我希望我的所有DataPoints和它们的ArrayOfThese元素都是连续的。为固定数量的ArrayOfThese元素浪费空间是不可接受的。

在C中,我会在DataPoint的末尾创建一个声明为ArrayOfThese d[0];的元素,分配一个DataPoint加上足够的额外字节,无论我有多少个ArrayOf这些元素,并使用虚拟数组索引到他们。 (当然,ArrayOfThese元素的数量必须在DataPoint的字段中。)

在C ++中,正在使用placement new和相同的0-length数组破解正确的方法?如果是这样,放置新保证后续从同一内存池调用new会连续分配吗?

11 个答案:

答案 0 :(得分:5)

由于您正在处理没有构造函数的普通结构,因此可以恢复为C内存管理:

void *ptr = malloc(sizeof(DataPoint) + n * sizeof(ArrayOfThese));
DataPoint *dp = reinterpret_cast<DataPoint *>(ptr));
ArrayOfThese *aotp = reinterpet_cast<ArrayOfThese *>(reintepret_cast<char *>(ptr) + sizeof(DataPoint));

答案 1 :(得分:3)

由于你的结构是POD,你可以像在C中那样做。你唯一需要的就是施法。假设n是要分配的内容的数量:

DataPoint *p=static_cast<DataPoint *>(malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese)));
如果您的对象具有非平凡的构造函数,那么

Placement new确实会出现这种情况。它不保证任何分配,因为它没有分配自己,并且要求内存已经以某种方式分配。相反,它将作为空间传入的内存块视为尚未构造的对象,然后调用正确的构造函数来构造它。如果您要使用它,代码可能会像这样。假设DataPoint拥有您建议的ArrayOfThese arr[0]成员:

void *p=malloc(sizeof(DataPoint)+n*sizeof(ArrayOfThese));
DataPoint *dp=new(p) DataPoint;
for(size_t i=0;i<n;++i)
    new(&dp->arr[i]) ArrayOfThese;

构造的内容必须被破坏,所以如果你这样做,你也应该理清析构函数的调用。

(我个人建议在这种情况下使用POD,因为它不需要调用构造函数和析构函数,但如果你小心的话,这种事情可以合理安全地完成。)

答案 2 :(得分:2)

正如Adrian在his answer中所说,你在内存中所做的事情不一定与你在网络上传输的内容相同。事实上,明确区分这一点可能会更好,因为如果您以后需要重构数据,那么依赖于您的数据以特定方式设计的通信协议会产生巨大的问题。

连续存储任意数量元素的C ++方法当然是std::vector。既然你甚至没有考虑过这个问题,我认为有些东西会让人觉得不合时宜。 (你只有少量的ArrayOfThese,并担心与std::vector相关的空间开销吗?)

虽然过度分配零长度数组的技巧可能无法保证工作,但从技术上讲,可能会调用可怕的未定义行为,但这是一个广泛传播的行为。你在什么平台上?在Windows上,这是在Windows API中完成的,因此很难想象供应商会提供不支持此功能的C ++编译器。

如果可能的ArrayOfThese元素数量有限,您还可以使用fnieto's trick指定这几个数字,然后new生成一个模板实例,具体取决于运行时间号码:

struct DataPoint {
  int a;
  int b;
  int c;
};

template <std::size_t sz>
struct DataPointWithArray : DataPoint {
  ArrayOfThese array[sz];
};

DataPoint* create(std::size_t n)
{
  switch(n) {
    case 1: return new DataPointWithArray[1];
    case 2: return new DataPointWithArray[2];
    case 5: return new DataPointWithArray[5];
    case 7: return new DataPointWithArray[7];
    case 27: return new DataPointWithArray[27];
    default: assert(false);
  }
  return NULL;
}

答案 3 :(得分:1)

在C ++ 0X之前,该语言有 no 内存模型可供使用。而且,根据新标准,我不记得任何有关邻接保证的言论。

关于这个特定的问题,听起来好像你想要的是一个池分配器,其中有很多例子。例如,考虑一下Alexandrescu的Modern C++ Design。小对象分配器讨论是你应该看到的。

答案 4 :(得分:1)

我认为boost::variant可能会实现这一目标。我没有机会使用它,但我相信它是工会的包装,因此std::vector它们应该是连续的,但当然每个项目将占用两种尺寸中较大的一种,你不能有一个具有不同大小元素的向量。

查看comparison of boost::variant and boost::any

如果希望每个元素的偏移量取决于前面元素的组成,则必须编写自己的分配器和访问器。

答案 5 :(得分:1)

似乎分配一个指针数组并使用它更简单,而不是使用placement new。这样你就可以将整个数组重新分配到新的大小而运行成本很低。此外,如果您使用placement new,则必须明确调用析构函数,这意味着在单个数组中混合非放置和放置是危险的。在做任何事情之前,请先阅读http://www.parashift.com/c++-faq-lite/dtors.html

答案 6 :(得分:1)

不要混淆程序和数据组织中的数据组织以进行序列化:它们没有相同的目标。

要通过网络进行流式传输,您必须考虑通道的两端,发送方和接收方:接收方如何区分DataPoint和ArrayOfThese?接收方如何知道在DataPoint之后附加了多少个ArrayOfThese? (还要考虑:每一方的字节顺序是什么?数据类型在内存中的大小是否相同?)

就个人而言,我认为您需要一种不同的数据流结构,您可以在其中添加您发送的DataPoint数量以及每个DataPoint之后的ArrayOfThese数量。我也不关心我的程序中数据的组织方式,并重新组织/重新格式化以适应我的协议,而不是我的程序。之后写一个发送功能和另一个接收功能并不是什么大问题。

答案 7 :(得分:1)

为什么 DataPoint 包含 ArrayOfThese 项目的可变长度数组?这将适用于C或C ++。如果任一结构包含非基本类型

,则存在一些问题

但是在结果上使用 free()而不是 delete

struct ArrayOfThese {
  int a;
  int b;
};


struct DataPoint {
  int a;
  int b;
  int c;
  int length;
  ArrayOfThese those[0];
};

DataPoint* allocDP(int a, int b, int c, size_t length)
{
    // There might be alignment issues, but not for most compilers:
    size_t sz = sizeof(DataPoint) + length * sizeof(ArrayOfThese);
    DataPoint dp = (DataPoint*)calloc( sz );
    // (Check for out of memory)
    dp->a = a; dp->b = b; tp->c = c; dp->length = length;
}

然后你可以在DataPoint知道其长度的循环中“正常”使用它:

DataPoint *dp = allocDP( 5, 8, 3, 20 );

for(int i=0; i < dp->length; ++i)
{
    // Initialize or access: dp->those[i]
}

答案 8 :(得分:0)

你能用相同的超类将它们变成类,然后使用你喜欢的stl容器,使用超类作为模板吗?

答案 9 :(得分:0)

两个问题:

  1. ArrayOfThese和DataPoint之间的相似性是真的,还是发布的简化?即是真正的区别只是一个int(或相同类型的项目的任意数量)?
  2. 在编译时是否知道与特定DataPoint关联的ArrayOfThese的数量?

如果第一个是真的,我会认真考虑简单地为一个DataPoint + N ArrayOfThese分配一个尽可能多的项目数组。然后我构建一个快速的代码来重载operator []以返回项目N + 3,并重载a(),b()和c()以返回前三项。

如果第二个是真的,我基本上会建议fnieto刚刚发布的内容,所以我不会详细介绍。

就新位置而言,它并不真正保证分配的任何内容 - 实际上,关于新位置的整个想法是它与内存分配完全无关。相反,它允许您在已经分配的内存块中的任意地址(受到对齐限制)创建对象。

答案 10 :(得分:0)

这是我最后编写的代码:

#include <iostream>
#include <cstdlib>
#include <cassert>
using namespace std;

struct ArrayOfThese {
  int e;
  int f;
};

struct DataPoint {
  int a;
  int b;
  int c;
  int numDPars;
  ArrayOfThese d[0];

  DataPoint(int numDPars) : numDPars(numDPars) {}

  DataPoint* next() {
    return reinterpret_cast<DataPoint*>(reinterpret_cast<char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }

  const DataPoint* next() const {
    return reinterpret_cast<const DataPoint*>(reinterpret_cast<const char*>(this) + sizeof(DataPoint) + numDPars * sizeof(ArrayOfThese));
  }
};

int main() {
  const size_t BUF_SIZE = 1024*1024*200;

  char* const buffer = new char[BUF_SIZE];
  char* bufPtr = buffer;

  const int numDataPoints = 1024*1024*2;
  for (int i = 0; i < numDataPoints; ++i) {
    // This wouldn't really be random.
    const int numArrayOfTheses = random() % 10 + 1;

    DataPoint* dp = new(bufPtr) DataPoint(numArrayOfTheses);

    // Here, do some stuff to fill in the fields.
    dp->a = i;

    bufPtr += sizeof(DataPoint) + numArrayOfTheses * sizeof(ArrayOfThese);
  }

  DataPoint* dp = reinterpret_cast<DataPoint*>(buffer);
  for (int i = 0; i < numDataPoints; ++i) {
    assert(dp->a == i);
    dp = dp->next();
  }

  // Here, send it out.

  delete[] buffer;

  return 0;
}