数组的新放置可以以可移植的方式使用吗?
-
08-06-2019 - |
题
在将其用于数组时,是否可以在可移植代码中实际使用放置 new ?
看来你从 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
big,数组中的最后一个元素被写入未分配的堆中。
所以问题是你能找出你的实现需要多少额外开销才能安全地使用放置 new[] 吗?理想情况下,我需要一种可在不同编译器之间移植的技术。请注意,至少在 VC 的情况下,不同类的开销似乎有所不同。例如,如果我删除示例中的虚拟析构函数,则从 new[] 返回的地址与我传入的地址相同。
解决方案
就我个人而言,我会选择不在数组上使用placement new,而是在数组中的每个项目上单独使用placement new。例如:
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++ 编译器的机器上)。它仍然表明了这一点:)希望它能以某种方式有所帮助!
编辑:
它需要跟踪元素数量的原因是,当您在数组上调用删除并确保在每个对象上调用析构函数时,它可以迭代它们。如果不知道有多少人,就无法做到这一点。
其他提示
@德里克
5.3.4,第 12 节讨论了数组分配开销,除非我误读了它,否则它似乎表明编译器将其添加到新的放置上也是有效的:
此开销可能会应用于所有数组 new 表达式,包括引用库函数运算符 new[](std::size_t, void*) 和其他布局分配函数的表达式。每次 new 调用与另一次调用之间的开销量可能会有所不同。
也就是说,我认为 VC 是唯一给我带来麻烦的编译器,除了 GCC、Codewarrior 和 ProDG。不过,我必须再次检查才能确定。
感谢您的回复。当我遇到这个问题时,我最终使用的解决方案是为数组中的每个项目使用新的放置(抱歉,应该在问题中提到这一点)。我只是觉得在放置 new[] 时我一定遗漏了一些东西。事实上,由于标准允许编译器向数组添加额外的未指定开销,因此放置 new[] 似乎基本上无法使用。我不明白你如何安全、便携地使用它。
我什至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用delete[],所以我不完全明白为什么它需要知道其中有多少项。
@詹姆士
我什至不太清楚为什么它需要额外的数据,因为无论如何你都不会在数组上调用delete[],所以我不完全明白为什么它需要知道其中有多少项。
经过一番思考后,我同意你的观点。没有理由说placement new 应该需要存储元素的数量,因为没有placement delete。由于没有放置删除,因此没有理由使用新放置来存储元素数量。
我还在 Mac 上使用带有析构函数的类对 gcc 进行了测试。在我的系统上,新的放置是 不是 改变指针。这让我想知道这是否是一个 VC++ 问题,以及这是否可能违反标准(据我所知,该标准没有具体解决这个问题)。
Placement new 本身是可移植的,但是您对它对指定内存块执行的操作所做的假设是不可移植的。就像之前所说的那样,如果你是一个编译器并获得了一块内存,如果你只有一个指针,你怎么知道如何分配一个数组并正确地析构每个元素?(参见操作符delete[]的接口。)
编辑:
实际上有一个放置删除,只是只有当构造函数在使用放置 new[] 分配数组时抛出异常时才会调用它。
new[] 是否实际上需要以某种方式跟踪元素的数量是由标准决定的,这将其留给编译器。不幸的是,在这种情况下。
我认为 gcc 与 MSVC 做同样的事情,但这当然并不意味着它“可移植”。
我认为当 NUMELMENTS 确实是一个编译时常量时,您可以解决这个问题,如下所示:
typedef A Arr[NUMELEMENTS];
A* p = new (buffer) Arr;
这应该使用新的标量放置。
与使用单个元素计算一个新放置的大小类似,使用这些元素的数组来计算数组所需的大小。
如果您需要进行其他计算(其中元素数量可能未知)的大小,则可以使用 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();
}