使用放置 new 覆盖内存中的对象
Overriding an object in memory with placement new
我有一个对象,我想把它'转换'成另一个对象。为此,我在第一个对象上使用placement new
,该对象在其自己的地址之上创建一个其他类型的新对象。
请考虑以下代码:
#include <string>
#include <iostream>
class Animal {
public:
virtual void voice() = 0;
virtual void transform(void *animal) = 0;
virtual ~Animal() = default;;
};
class Cat : public Animal {
public:
std::string name = "CAT";
void voice() override {
std::cout << "MEOW I am a " << name << std::endl;
}
void transform(void *animal) override {
}
};
class Dog : public Animal {
public:
std::string name = "DOG";
void voice() override {
std::cout << "WOOF I am a " << name << std::endl;
}
void transform(void *animal) override {
new(animal) Cat();
}
};
您可以看到,当使用transform
调用Dog
时,它会在给定地址之上创建一个新Cat
。
接下来,我将用自己的地址调用Dog::transform
:
#include <iostream>
#include "Animals.h"
int main() {
Cat cat{};
Dog dog{};
std::cout << "Cat says: ";
cat.voice() ;
std::cout << "Dog says: ";
dog.voice();
dog.transform(&dog);
std::cout << "Dog says: ";
dog.voice();
std::cout << "Dog address says: ";
(&dog)->voice();
return 0;
}
其结果是:
Cat says: MEOW I am a CAT
Dog says: WOOF I am a DOG
Dog says: WOOF I am a CAT
Dog address says: MEOW I am a CAT
我的问题是:
- 此操作是否被视为安全,或者它是否使对象处于不稳定状态?
- 转换后我称之为
dog.voice()
.它正确地打印了CAT
的名字(它现在是一只猫),但仍然写WOOF I am a
,即使我认为它应该调用Cat
的voice
方法?(你可以看到我调用了相同的方法,但通过地址((&dog)->voice()
),一切都正常工作。
此操作是否被认为是安全的,还是使对象处于不稳定状态?
此操作不安全,并会导致未定义的行为。Cat
和Dog
具有非平凡的析构函数,因此在重用存储cat
之前,dog
必须调用它们的析构函数,以便正确清理以前的对象。
转换后,我称之为
dog.voice()
.我正确打印了CAT
的名字(它现在是一只猫),但仍然写WOOF I am a
,甚至很难,我会认为它应该叫Cat
的voice
方法?(您可以看到的是我调用相同的方法,但通过地址((&dog)->voice()
),一切都正常工作。
在dog.transform(&dog);
后使用dog.voice();
是未定义的行为。 由于您重用了其存储而不破坏它,因此您具有未定义的行为。 假设你确实在transform
中摧毁了dog
,以摆脱你仍然没有摆脱困境的那一点未定义的行为。 在销毁后使用dog
是未定义的行为。 您所要做的就是捕获指针放置新返回值,并从此使用该指针。 您还可以在dog
上使用std::launder
,并reinterpret_cast
转换为您将其转换为的类型,但这不值得,因为您丢失了所有封装。
在使用放置 new 时,您还需要确保所使用的对象对于您正在构建的对象来说足够大。 在这种情况下,应该是因为类是相同的,但是比较大小的static_assert
将保证这一点,如果不是真的,则停止编译。
解决此问题的一种方法是创建一个不同的动物类,作为动物类的持有者(我在下面的示例代码中将其重命名为Animal_Base
)。 这使您可以封装Animal
所代表的对象类型的更改。 将代码更改为
class Animal_Base {
public:
virtual void voice() = 0;
virtual ~Animal_Base() = default;
};
class Cat : public Animal_Base {
public:
std::string name = "CAT";
void voice() override {
std::cout << "MEOW I am a " << name << std::endl;
}
};
class Dog : public Animal_Base {
public:
std::string name = "DOG";
void voice() override {
std::cout << "WOOF I am a " << name << std::endl;
}
};
class Animal
{
std::unique_ptr<Animal_Base> animal;
public:
void voice() { animal->voice(); }
// ask for a T, make sure it is a derived class of Animal_Base, reset pointer to T's type
template<typename T, std::enable_if_t<std::is_base_of_v<Animal_Base, T>, bool> = true>
void transform() { animal = std::make_unique<T>(); }
// Use this to say what type of animal you want it to represent. Doing this instead of making
// Animal a temaplte so you can store Animals in an array
template<typename T, std::enable_if_t<std::is_base_of_v<Animal_Base, T>, bool> = true>
Animal(T&& a) : animal(std::make_unique<T>(std::forward<T>(a))) {}
};
然后将main
调整为
int main()
{
Animal cat{Cat{}};
Animal dog{Dog{}};
std::cout << "Cat says: ";
cat.voice() ;
std::cout << "Dog says: ";
dog.voice();
dog.transform<Cat>();
std::cout << "Dog says: ";
dog.voice();
std::cout << "Dog address says: ";
(&dog)->voice();
return 0;
}
产生输出
Cat says: MEOW I am a CAT
Dog says: WOOF I am a DOG
Dog says: MEOW I am a CAT
Dog address says: MEOW I am a CAT
这是安全和便携的。
此代码至少存在三个问题:
- 不能保证当放置 new 时称为 new 时,您在其中构建新对象的对象的大小足以容纳新对象
- 您没有调用用作占位符的对象的析构函数
- 在重复使用
Dog
对象的存储后,可以使用该对象。
1) 不,由于以下原因,这是不安全的:
- 该行为未定义,对于某些编译器可能会有所不同。
- 分配的内存需要足够大,以容纳新创建的结构。
- 某些编译器可能会调用原始对象的析构函数,即使它是虚拟的,这会导致泄漏和崩溃。
- 在代码中,不会调用原始对象的析构函数,因此可能会导致内存泄漏。
2)我在MSVC2015上观察到dog.voice()
会在不检查实际虚拟表的情况下调用Dog::voice
。在第二种情况下,它检查虚拟表,该表已被修改为Cat::voice
。但是,正如其他用户所经历的那样,其他一些编译器可能会执行一些优化,并在所有情况下直接调用与声明匹配的方法。
- 迭代时从向量和内存中删除对象
- 有没有一种方法可以使用placement new将堆叠对象分配给分配的内存
- Constexpr替代了新的放置方式,可以让内存中的对象保持未初始化状态
- 对具有动态分配的内存和析构函数的类对象的引用
- 当指向对象的指针作为参数传递给 std::thread 时,内存可见性
- 内存清理程序报告全局对象构造中未初始化值的使用
- 如何删除列出的"QGraphicsPathItem"对象以控制进程内存使用情况?
- 有没有办法列出所有共享内存对象的名称?
- 我的共享内存对象保存在哪里
- boost共享内存对象中的指针
- 创建 boost::interprocess 共享内存对象的非共享副本
- 提升中的异常:进程间,共享内存对象删除
- 自动C++内存/对象实例管理?智能指针
- 将矢量或任何其他容器存储在boost进程间共享内存对象中
- OpenCL:减少示例,并保留内存对象/将cuda代码转换为OpenCL
- boost::创建托管共享内存对象时出现进程间ubuntu异常
- 用户可能无法在 Linux 系统上打开共享内存对象的原因
- 如何测试boost共享内存对象是否被删除?
- 将指向结构体或内存对象的空指针强制转换为新结构体
- Apache模块共享内存对象