8

我目前正在调试崩溃日志。发生崩溃是因为(c++-)对象的 vtable 指针是 0x1,而据我从崩溃日志中可以看出,该对象的其余部分似乎没问题。

程序在尝试调用虚拟方法时崩溃。

我的问题:在什么情况下 vtable 指针可以变为空?operator delete 是否将 vtable 指针设置为 null?

这发生在使用 gcc 4.0.1(Apple Inc. build 5493)的 OS X 上。

4

5 回答 5

9

可能是记忆的践踏——一些vtable错误地写在上面的东西。在 C++ 中,几乎有无数种方法可以“实现”这一目标。例如,缓冲区溢出。

于 2010-01-15T14:38:41.480 回答
8

您拥有的任何类型的未定义行为都可能导致这种情况。例如:

  • 指针算术或其他导致程序写入无效内存的错误。
  • 未初始化的变量,无效的强制转换......
  • 以多态方式处理数组可能会导致这种情况成为次要影响。
  • 尝试在删除后使用对象。

另请参阅问题实际上可能的未定义行为的最糟糕示例是什么?以及C++ 程序员应该知道的所有常见的未定义行为是什么?.

您最好的选择是使用边界和内存检查器,以帮助进行大量调试。

于 2010-01-15T14:38:52.200 回答
7

一个非常常见的情况:试图从构造函数中调用纯虚方法......

构造函数

struct Interface
{
  Interface();
  virtual void logInit() const = 0;
};

struct Concrete: Interface()
{
  virtual void logInit() const { std::cout << "Concrete" << std::endl; }
};

现在,假设以下实现Interface()

Interface::Interface() {}

然后一切都很好:

Concrete myConcrete;
myConcrete.pure();    // outputs "Concrete"

在构造函数之后调用 pure 真是太痛苦了,分解代码会更好吗?

Interface::Interface() { this->logInit(); } // DON'T DO THAT, REALLY ;)

然后我们可以一行完成!

Concrete myConcrete;  // CRASHES VIOLENTLY

为什么 ?

因为对象是自下而上构建的。让我们看看它。

构建Concrete类的说明(粗略)

  1. 为 _vtable 分配足够的内存(当然)和足够的内存(每个虚函数 1 个函数指针,通常按照它们的声明顺序,从最左边的基数开始)

  2. 调用Concrete构造函数(你看不到的代码)

    a> 调用Interface构造函数,用它的指针初始化_vtable

    b> 调用Interface构造函数的主体(你写的)

    c> 为那些方法覆盖 _vtable 中的指针具体覆盖

    d> 调用Concrete构造函数的主体(你写的)

所以有什么问题 ?好吧,看看b>c>订购;)

当您从构造函数中调用virtual方法时,它并没有达到您的预期。它确实去 _vtable 查找指针,但_vtable尚未完全初始化。因此,对于所有重要的事情,效果:

D() { this->call(); }

实际上是:

D() { this->D::call(); }

从构造函数中调用虚拟方法时,您不会使用正在构建的对象的完整动态类型,而是调用当前构造函数的静态类型。

在我的Interface/Concrete示例中,它表示Interface类型,并且该方法是纯虚拟的,因此 _vtable 不包含真正的指针(例如,0x0 或 0x01,如果您的编译器足够友好,可以设置调试值来帮助您)。

析构函数

巧合的是,让我们检查一下 Destructor 案例;)

struct Interface { ~Interface(); virtual void logClose() const = 0; }
Interface::~Interface() { this->logClose(); }

struct Concrete { ~Concrete(); virtual void logClose() const; char* m_data; }

Concrete::~Concrete() { delete[] m_data; } // It's all about being clean
void Concrete::logClose()
{
  std::cout << "Concrete refering to " << m_data << std::endl;
}

那么破坏时会发生什么?那么 _vtable 工作得很好,并且调用了真正的运行时类型......然而,这里的意思是未定义的行为,因为谁知道m_data在它被删除之后和Interface调用析构函数之前发生了什么?我不 ;)

结论

永远不要从构造函数或析构函数中调用虚方法。

如果不是这样,那么您将面临内存损坏,运气不好;)

于 2010-01-15T20:21:58.837 回答
4

我的第一个猜测是某些代码是memset()一个类对象。

于 2010-01-15T15:16:40.940 回答
1

这完全取决于实现。然而,假设删除后一些其他操作可能会将内存空间设置为空,这是相当安全的。

其他可能性包括通过一些松散的指针覆盖内存 - 实际上在我的情况下它几乎总是这样......

也就是说,您永远不应该在删除后尝试使用对象。

于 2010-01-15T14:40:06.970 回答