C++中以显式析构+operator delete或派生类指针delete替代delete表达式是否合法?
对于通过new表达式获取的指针,在C++中能否用显式调用析构函数(或std::destroy_at)后调用operator delete,或通过派生类指针调用delete的方式,替换对应的delete表达式?
在项目中使用自定义RTTI机制,对于需要动态分派析构函数的类型,直接用delete表达式只会调用基类析构函数,无法正确调用派生类析构,可能引发内存泄漏。为解决该问题,通过自定义RTTI根据对象运行时类型,手动调用正确析构后调用operator delete释放内存,或改为通过派生类指针调用delete。该方案在clang和MSVC编译环境下运行正常,但想确认这种做法是否会触发未定义行为。
关于virtual
可以通过将析构函数声明为virtual实现动态分派,但这会为对象引入vtable指针,而对象本身已包含运行时类型信息,这会带来不必要的运行时开销,且属于重复解决已解决的问题,认为这是设计缺陷。
代码示例
原生动态分派(虚析构)
struct Base { virtual ~Base(); }; struct Derived: Base { // 需要正确析构的数据成员 }; // ... Base* p = new Derived(/* ... */); // ... delete p; // 会调用Derived::~Derived()
自定义RTTI实现
struct Base { protected: int typeID = ID_OF_TYPE_BASE; }; struct Derived1: Base { Derived1() { typeID = ID_OF_TYPE_DERIVED1; } }; struct Derived2: Base { Derived2() { typeID = ID_OF_TYPE_DERIVED2; } };
给定Base类型指针p,可基于*p的实际运行时类型分派函数,类似std::variant上的std::visit:
visit(p, [](auto* q) { // 此处*q的静态类型就是*p的运行时类型,即Base、Derived1或Derived2 });
通过这种方式分派到正确的析构函数:
visit(p, [](auto* q) { std::destroy_at(q); });
实际想问的是:如下操作是否符合C++标准?
Base* p = new Derived1(/* ... */); // ... // delete p; // 仅调用Base::~Base() - 可能因未销毁子对象导致内存泄漏 visit(p, [](auto* q) { // std::destroy_at(q); // 调用Derived1::~Derived1() // ::operator delete(q); // 通过派生类指针释放内存 // 解决方案:直接通过派生类指针调用delete! delete q; });
注:并非询问自定义RTTI替代方案是否合理,也不是表明使用裸new和delete是最佳实践,这些操作已封装在unique_ptr中。
你的两种方案均符合C++标准,不会触发未定义行为,具体依据如下:
方案1:显式调用析构函数后调用operator delete
C++标准允许显式调用析构函数(包括通过std::destroy_at)销毁对象,之后只要通过指向分配内存起始地址的指针调用operator delete即可完成内存释放。需要注意两点:
- 必须确保调用的是对应派生类的析构函数(你的自定义RTTI已经保证了这一点)
- 传递给
operator delete的指针必须是new表达式返回的原始指针,或是指向同一内存起始位置的派生类指针(单继承且无虚继承场景下,基类与派生类对象地址一致;若存在虚继承/多重继承,需确保visit返回的是对象原始起始地址)
方案2:通过派生类指针调用delete
当visit返回指向实际派生类对象的指针q时,调用delete q完全符合标准:
delete表达式会先调用q指向对象的析构函数(此时q的静态类型就是派生类,会直接调用派生类析构,进而链式调用基类析构)- 随后
operator delete会释放new Derived1分配的内存,q的类型与内存分配时的类型完全匹配,符合delete表达式的要求。
关键标准依据
C++标准明确规定:
- 若
delete表达式中指针的静态类型与对象动态类型不同,且静态类型不是动态类型的带虚析构的基类,行为未定义——你的方案中delete q的静态类型就是对象动态类型,完全规避了该问题。 - 显式调用析构函数后,通过指向内存起始处的正确指针调用
operator delete是合法操作,因为new/operator delete负责内存分配/释放,析构函数负责对象销毁,两者职责分离。
需要额外注意:如果类层次存在多重继承或虚继承,必须确保visit返回的是对象的原始起始地址,而非基类子对象的地址,否则operator delete会收到错误指针,引发未定义行为。从你的代码示例看,当前是单继承无虚继承场景,不存在该问题。
内容的提问来源于stack exchange,提问作者chrysante

