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

3

我正在使用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加上足够多的额外字节以容纳我所需的ArrayOfThese元素数量,并使用虚拟数组对它们进行索引。(当然,ArrayOfThese元素的数量必须在DataPoint的字段中定义。)

在C++中,使用放置new和相同的0长度数组技巧是否是正确的方法?如果是这样,放置new是否保证后续从同一内存池调用new将分配连续的内存?

11个回答

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));

他的 ArrayOfThese 是元素,不是一个数组,所以我认为你需要 _ArrayOfThese*_。 - NVRAM
..但仅限于最后一行。 - NVRAM
NVRAM发现了一个编码错误(我已经纠正了我的代码,所以他的评论现在看起来是错误的,但当他写下时是正确的)。 - R Samuel Klatchko

3

由于您的结构体是POD(Plain Old Data),因此您可以像在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
正如Adrian在他的回答中所说,内存中的操作不必与网络传输的内容相同。实际上,清晰地划分这两者可能是有好处的,因为如果通信协议依赖于数据以特定方式设计,则稍后需要重构数据将会带来巨大问题。
在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;
}

vector 不适用于大小可变的元素。请参见 https://dev59.com/TknSa4cB1Zd3GeqPSvy_#1626884。 - Jim Hunziker
@Jim:但是 ArrayOfThese 不是可变大小的,对吧?而且你链接的那个答案是关于多态类的,所以不相关。 - sbi
我认为你的意思是写类似于new DataPointWithArray<27>这样的代码。但是这会在内存中随机分配数据。除了需要通过网络流传输之外,我还需要在处理过程中按顺序访问大量这些数据,因此对于我来说,具有参考位置的局部性非常重要。 - Jim Hunziker

1
为什么不让DataPoint包含一个变长的ArrayOfThese数组呢?这在C或C++中都可行。但如果任一struct包含非原始类型,则会引发一些问题。
但是在结果上使用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]
}

顺便说一下,对于非原始类型的担忧在于使用构造函数和析构函数的缺乏,相比之下使用分配和释放。这些问题可以解决,但并不容易。 - NVRAM
这个答案是我在问题中提到的 C 语言实现方式。 - Jim Hunziker

1
在C++0X之前,该语言没有可言的内存模型。而且随着新标准的出现,我不记得有任何关于连续性保证的讨论。
关于这个特定问题,听起来你想要一个池分配器,有很多例子存在。例如,考虑Alexandrescu的《现代C++设计》。小对象分配器的讨论是你应该看的内容。

1

我认为boost::variant可能可以实现这一点。虽然我还没有使用过它,但我相信它是围绕联合体的一个包装器,所以它们的std::vector应该是连续的,但是每个项目将占用两个大小中较大的一个,你不能有不同大小元素的向量。

请查看boost::variant和boost::any的比较

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


+1 我本来想说,如果在结构体之外有一些数据没有问题的话,那么向量将是一个很好的选择。 - user34537

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

1

不要混淆程序内部的数据组织和序列化的数据组织:它们的目标不同。

在网络数据流传输时,你需要考虑通道的两端,即发送方和接收方:接收方如何区分 DataPoint 和 ArrayOfThese?接收方如何知道在 DataPoint 之后有多少个 ArrayOfThese?(还需考虑的问题包括:每一端的字节顺序是什么?数据类型在内存中大小是否相同?)

就我个人而言,我认为你需要针对数据流传输使用不同的结构,并在每个 DataPoint 后添加你所发送的 DataPoint 数量以及接下来的 ArrayOfThese 数量。此外,我也不会关心数据在程序中的已有组织方式,而是按照协议重新组织/格式化数据,而非按照程序要求。然后编写一个发送函数和另一个接收函数并不是什么大问题。


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;
}

0

你可以将它们转换为具有相同超类的类,然后使用你喜欢的 STL 容器作为模板,将超类作为参数吗?


3
除非您想了解切片,否则请勿这样做。您可以在STL容器中存储指向超类的指针,但是如果您尝试将子类存储在专为超类特化的容器中,则最终会丢失任何子类信息。 - Eclipse

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