为何C++中使用virtual函数与std::string后delete指针类类型变化?
我想了解在使用std::string和virtual函数时类类型发生变化的原因。
测试代码1(含虚函数和std::string)
#include <iostream> #include <typeinfo> class Base { public: std::string str; virtual void vvfunc() {}; }; class Derived : public Base { public: void vvfunc() {} }; using namespace std; int main() { Derived* pd = new Derived; Derived* pd2 = pd; cout<<&pd<<endl; cout<<&pd2<<endl; cout << typeid( pd ).name() << endl; cout << typeid( *pd ).name() << endl; cout << typeid( pd2 ).name() << endl; cout << typeid( *pd2 ).name() << endl; delete pd; cout << typeid( pd2 ).name() << endl; cout << typeid( *pd2 ).name() << endl; }
输出1:
0x7ffeeaadd8f0 0x7ffeeaadd8d8 P7Derived 7Derived P7Derived 7Derived P7Derived 4Base
测试代码2(无虚函数,或无std::string)
#include <iostream> #include <typeinfo> class Base { public: std::string str; void vvfunc() {}; }; class Derived : public Base { public: void vvfunc() {} }; using namespace std; int main() { Derived* pd = new Derived; Derived* pd2 = pd; cout<<&pd<<endl; cout<<&pd2<<endl; cout << typeid( pd ).name() << endl; cout << typeid( *pd ).name() << endl; cout << typeid( pd2 ).name() << endl; cout << typeid( *pd2 ).name() << endl; delete pd; cout << typeid( pd2 ).name() << endl; cout << typeid( *pd2 ).name() << endl; }
输出2:
0x7ffeec60e790 0x7ffeec60e778 P7Derived 7Derived P7Derived 7Derived P7Derived 7Derived
请问有人能解释这种现象的原因吗?
首先必须敲黑板:在delete pd之后,pd2是一个悬垂指针(dangling pointer),访问*pd2属于C++标准定义的未定义行为(Undefined Behavior, UB)——标准没有规定这种情况下的结果,任何输出都是合法的。不过我们可以从常见编译器的实现细节角度,解释你看到的差异:
1. 多态类型的typeid判断逻辑
当类包含虚函数时,它属于多态类型。对于多态类型的表达式(比如*pd2),typeid的结果不是由变量的静态类型决定的,而是通过对象内部的**虚函数指针(vptr)指向的虚函数表(vtable)**来动态判断的。
2. 为什么有std::string和虚函数时会出现类型变化?
非平凡析构函数触发vptr修改
std::string的析构函数是非平凡的(即不是编译器生成的空析构,而是有实际内存清理逻辑)。当你执行delete pd时:
- 程序先调用
Derived的析构函数(这里是空实现,但仍会触发后续逻辑) - 接着调用
Base的析构函数,其中会销毁std::string成员str,涉及内存释放等操作。
像GCC这样的编译器,在执行基类的析构函数时,会主动将对象的vptr修改为基类的vtable地址。这么做的目的是:如果析构函数中调用了虚函数,此时应该调用基类版本的虚函数,而不是派生类的(毕竟派生类的部分已经被销毁了)。
当对象完全销毁后,vptr已经被修改为Base的vtable指针,此时通过悬垂指针pd2访问*pd2,typeid会读取这个已经被修改的vptr,从而得到Base的类型信息。
3. 为什么另外两种情况不会出现这个现象?
情况A:没有虚函数
此时类不是多态类型,typeid(*pd2)的结果由静态类型直接决定——pd2的静态类型是Derived*,所以*pd2的静态类型是Derived,不管对象是否销毁,typeid都会返回Derived的类型信息。
情况B:没有std::string
此时Base的析构函数是平凡的(编译器生成默认空析构),编译器不会在析构过程中修改vptr。即使对象被销毁,vptr仍然指向Derived的vtable,所以typeid(*pd2)读取到的还是Derived的类型信息。
重要提醒
再次强调:访问悬垂指针是严格禁止的未定义行为,你看到的结果只是特定编译器在特定环境下的表现,绝对不能依赖这种行为。实际开发中,一定要确保指针在使用前是有效的,避免访问已经被销毁的对象。
内容的提问来源于stack exchange,提问作者yhshin

