高度注意并警惕 placement new [] 的陷阱

本文探讨了在C++中,类是否有析构函数对于new操作符分配内存的影响。通过具体的代码示例,分析了不同情况下内存分配的区别,并给出了在placement new中避免问题的解决方案。

前几天在检一个服务器容器的时候,发现一个严重的问题,问题大概如下:

 

假设我们有这样一个类:

 

和假设如下的代码:

 

按照MSDN上面说明,我们可以认为p和pk所处的地址是相同的,仍而,
如果我们把类中的tk成员注掉,执行上面两句结果是p和pk地址相等。
但我们把类中的tk成员打开,再执行,p和pk地址却不相等。
经调试发现,中间差了4字节,里面存放的刚好就是数组的数量,即4。

 

我问了两个群的编程兄弟,他们都很热情,我很感谢,不过问题说得不是很清楚,但我更喜欢查根问底。
虽然我知道怎么解决这个BUG,让程序正常起来,但我还是硬着查清楚原因。

 

在解释原因之前我们先将结构改一下(去掉tk,增加析构函数):

 

好了,这个时候再来以有析构和没有析构函数的方法测试:
当有析构的时候p和pk地址不同,当没有析构的时候p和pk相同。

 

是什么原因造成这一现象的呢?
有没有析构又怎么会造成这一结果的呢?
类析构和std::vector< int > tk有什么联呢?
下面依次解释:

 

虽然FFntTexXX是一个类,但其成员没有一个成员有析构函数,所以编译器认为此类可以退化到结构。
(当类中有任意一个成员需要析构,如std::vector tk成员,编译器为自动为类FFntTexXX生成析构)
或许你又要问了,为什么生成析构与不生成析构和p、pk有什么关系呢?
确实是的,起初我也一直认识这个不存在任何关系的,但编过汇编发现,原因正是因为析构。

 

先看 ::new [] 函数代码(无析构):

 

再看 ::new [] 函数代码(有析构):

 

这里可能你也已经发现了,当有析构的时候new[]会要求多传入一个机器字长数据,当然目的就是为了保存数组的个数,
而没有析构的时候却不需要保存数组的个数。

 

接下来再看 constructor iterator 函数(无析构):

 

然后看 constructor iterator 函数(有析构):

 

初看这好像没有什么特别的,但细看才发现,两个数组构造器函数不一样,无析构的只有4个参数,而有析构的有5个参数。

“vector constructor iterator”和“eh vector constructor iterator”。
这时才恍然大悟,原因编译器做的事情不少。

 

总结:
因为没有析构函数的对象,在delete[]的时候,它可以直接从内存拿掉而不用调析构函数。
所以这里不用记录这个数组有多少个元素,也就不出现偏4个字节。
当有析构函数的时候,编译器为了记住有多少个对象,以便于在delete[]的时候要对其调用析构函数,
所以在数组首地址前面一个机器字长来保存数组长度,数组总尺寸增加一个机器的字长。
因此调用new[]的时候,有析构和无析构的时候,所需要的内存不一样。

 

解决方案:
使用 placement new[] 对给定地址进行数组构造的时候(placement new不存在此问题),
一定记住要多分配一个机器字长,用sizeof(int)即可得到所需字节数。
真正的数组地址应该取 placement new[] 返回的地址,而不是取 placement new(p) [] 中p的地址。

### `operator new` 与 `placement new` 的区别及使用场景 在 C++ 中,`operator new` 和 `placement new` 是两个不同的概念,尽管它们都与内存分配和对象构造相关。理解它们的区别有助于更高效地管理内存和控制对象生命周期。 #### `operator new` `operator new` 是一个标准库函数,其主要职责是分配原始内存空间。它不涉及对象的构造过程。基本形式如下: ```cpp void* operator new(std::size_t size) throw(std::bad_alloc); ``` 当调用 `new` 表达式时,例如: ```cpp MyClass* obj = new MyClass(); ``` 编译器会隐式调用 `operator new(sizeof(MyClass))` 来分配内存,然后在该内存上调用构造函数来初始化对象[^4]。如果内存分配失败,`operator new` 会抛出 `std::bad_alloc` 异常,除非使用了 `nothrow` 版本。 用户可以重载 `operator new` 以实现自定义的内存分配策略。例如,在实现自定义内存池或容器类时,这种机制非常有用。通过重载,开发者可以控制内存分配行为,而不影响 `new` 表达式的语义。 #### `placement new` `placement new` 是 `operator new` 的一种特殊形式,用于在已分配的内存中构造对象。它的典型形式如下: ```cpp void* operator new(std::size_t, void* location); ``` 这个版本的 `operator new` 不会分配新的内存,而是返回传入的指针 `location`,即直接在指定的内存地址上构造对象[^1]。因此,它通常与手动内存管理结合使用,比如在预分配的缓冲区中构造对象。 使用 `placement new` 的典型方式如下: ```cpp char buffer[sizeof(MyClass)]; // 预分配内存 MyClass* obj = new(buffer) MyClass(); // 在 buffer 上构造对象 ``` 这种方式允许开发者精确控制对象的内存位置,适用于嵌入式系统、性能敏感的库(如 STL 容器)或需要严格内存对齐的场景。 需要注意的是,`placement new` 不可重载,因为它只是简单地返回传入的指针,不涉及内存分配逻辑。 #### 总结对比 | 特性 | `operator new` | `placement new` | |-----------------------|------------------------------------------|------------------------------------------| | 是否分配新内存 | 是 | 否(使用已有内存) | | 是否调用构造函数 | 否(仅分配内存) | 是(在已有内存上构造对象) | | 可否被重载 | 是 | 否 | | 典型用途 | 自定义内存分配策略、容器内部实现 | 在特定内存位置构造对象 | #### 示例代码 ```cpp #include <iostream> #include <new> // for placement new struct MyClass { int value; MyClass() : value(42) {} }; int main() { // 使用 operator new 分配内存 void* rawMemory = ::operator new(sizeof(MyClass)); // 使用 placement new 构造对象 MyClass* obj = new(rawMemory) MyClass(); std::cout << "Value: " << obj->value << std::endl; // 显式调用析构函数 obj->~MyClass(); // 释放原始内存 ::operator delete(rawMemory); } ``` 此示例展示了如何结合 `operator new` 和 `placement new` 来手动管理内存生命周期,这在高性能库开发中是常见做法。 ---
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值