You need to enable JavaScript to run this app.
优惠活动
大模型
产品
解决方案
定价
更多

为何C++中使用virtual函数与std::string后delete指针类类型变化?

问题:std::string与虚函数共存时销毁对象后类型判断异常的原因

我想了解在使用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

相关产品推荐
方舟 Agent Plan

超全模态模型 × Harness 升级,最新支持 Deepseek-V4.1-Flash、GLM-5.3 系列、Doubao-Seedream-5.0-pro、Kimi-K3 (部分), 限时 9.9 元起

最近更新时间:2026.05.11 08:33:41