45

将其用于数组时,是否可以实际使用可移植代码中的新位置?

看来您从 new[] 返回的指针并不总是与您传入的地址相同(5.3.4,标准中的注释 12 似乎确认这是正确的),但我不明白您是如何如果是这种情况,可以为数组分配一个缓冲区。

以下示例显示了该问题。使用 Visual Studio 编译,此示例导致内存损坏:

#include <new>
#include <stdio.h>

class A
{
    public:

    A() : data(0) {}
    virtual ~A() {}
    int data;
};

int main()
{
    const int NUMELEMENTS=20;

    char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
    A *pA = new(pBuffer) A[NUMELEMENTS];

    // With VC++, pA will be four bytes higher than pBuffer
    printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);

    // Debug runtime will assert here due to heap corruption
    delete[] pBuffer;

    return 0;
}

查看内存,编译器似乎正在使用缓冲区的前四个字节来存储其中项目数的计数。这意味着因为缓冲区只是sizeof(A)*NUMELEMENTS很大,所以数组中的最后一个元素被写入未分配的堆中。

所以问题是你能找出你的实现需要多少额外的开销才能安全地使用placement new[]?理想情况下,我需要一种在不同编译器之间可移植的技术。请注意,至少在 VC 的情况下,不同类的开销似乎不同。例如,如果我在示例中删除虚拟析构函数,则 new[] 返回的地址与我传入的地址相同。

4

7 回答 7

33

就我个人而言,我会选择不在数组上使用新位置,而是在数组中的每个项目上单独使用新位置。例如:

int main(int argc, char* argv[])
{
  const int NUMELEMENTS=20;

  char *pBuffer = new char[NUMELEMENTS*sizeof(A)];
  A *pA = (A*)pBuffer;

  for(int i = 0; i < NUMELEMENTS; ++i)
  {
    pA[i] = new (pA + i) A();
  }

  printf("Buffer address: %x, Array address: %x\n", pBuffer, pA);

  // dont forget to destroy!
  for(int i = 0; i < NUMELEMENTS; ++i)
  {
    pA[i].~A();
  }    

  delete[] pBuffer;

  return 0;
}

无论您使用哪种方法,请确保在删除 pBuffer 之前手动销毁数组中的每个项目,因为您最终可能会出现泄漏;)

注意:我还没有编译这个,但我认为它应该可以工作(我在一台没有安装 C++ 编译器的机器上)。它仍然表明了这一点:) 希望它在某种程度上有所帮助!


编辑:

它需要跟踪元素数量的原因是,当您在数组上调用 delete 并确保在每个对象上调用析构函数时,它可以遍历它们。如果它不知道有多少,它就无法做到这一点。

于 2008-08-18T22:53:05.003 回答
5

@德里克

5.3.4,第 12 节讨论了数组分配开销,除非我误读它,否则它似乎向我建议编译器将其添加到放置 new 也是有效的:

这种开销可以应用于所有数组 new 表达式,包括那些引用库函数 operator new[](std::size_t, void*) 和其他放置分配函数的表达式。开销的数量可能因一次调用 new 到另一次而异。

也就是说,我认为 VC 是唯一给我带来麻烦的编译器,其中包括 GCC、Codewarrior 和 ProDG。不过,我必须再次检查才能确定。

于 2008-08-19T10:16:44.577 回答
4

@詹姆士

我什至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用 delete[] ,所以我不完全明白为什么它需要知道其中有多少项目。

在考虑了这一点之后,我同意你的看法。没有放置 new 需要存储元素数量的理由,因为没有放置删除。由于没有放置删除,因此没有理由放置新来存储元素的数量。

我还在我的 Mac 上使用带有析构函数的类使用 gcc 对此进行了测试。在我的系统上,放置新并没有改变指针。这让我想知道这是否是一个 VC++ 问题,以及这是否可能违反标准(据我所知,该标准并没有专门解决这个问题)。

于 2008-08-19T02:14:14.067 回答
3

感谢您的回复。为数组中的每个项目使用新位置是我遇到这个问题时最终使用的解决方案(对不起,应该在问题中提到这一点)。我只是觉得在放置 new[] 时一定有一些我错过的东西。实际上,由于标准允许编译器向数组添加额外的未指定开销,因此放置 new[] 似乎本质上是不可用的。我看不出你怎么能安全便携地使用它。

我什至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用 delete[] ,所以我不完全明白为什么它需要知道其中有多少项目。

于 2008-08-19T00:03:57.933 回答
3

Placement new 本身是可移植的,但是您对它对指定内存块所做的假设是不可移植的。就像之前所说的那样,如果你是一个编译器并且有一块内存,如果你只有一个指针,你怎么知道如何分配一个数组并正确地破坏每个元素?(见 operator delete[] 的接口。)

编辑:

实际上有一个放置删除,只有当构造函数在使用放置 new[] 分配数组时抛出异常时才调用它。

new[] 是否真的需要以某种方式跟踪元素的数量是由标准决定的,这由编译器决定。不幸的是,在这种情况下。

于 2008-08-19T21:36:16.077 回答
2

类似于使用单个元素来计算一个放置新的大小,使用这些元素的数组来计算数组所需的大小。

如果您需要元素数量可能不知道的其他计算的大小,您可以使用 sizeof(A[1]) 并乘以所需的元素数。

例如

char *pBuffer = new char[ sizeof(A[NUMELEMENTS]) ];
A *pA = (A*)pBuffer;

for(int i = 0; i < NUMELEMENTS; ++i)
{
    pA[i] = new (pA + i) A();
}
于 2008-08-18T23:26:37.463 回答
1

我认为 gcc 与 MSVC 做同样的事情,但这当然不会使它“便携”。

我认为当 NUMELEMENTS 确实是编译时间常数时,您可以解决该问题,如下所示:

typedef A Arr[NUMELEMENTS];

A* p = new (buffer) Arr;

这应该使用新的标量放置。

于 2008-08-18T21:45:14.640 回答