我目前正在调试崩溃日志。发生崩溃是因为(c++-)对象的 vtable 指针是 0x1,而据我从崩溃日志中可以看出,该对象的其余部分似乎没问题。
程序在尝试调用虚拟方法时崩溃。
我的问题:在什么情况下 vtable 指针可以变为空?operator delete 是否将 vtable 指针设置为 null?
这发生在使用 gcc 4.0.1(Apple Inc. build 5493)的 OS X 上。
可能是记忆的践踏——一些vtable
错误地写在上面的东西。在 C++ 中,几乎有无数种方法可以“实现”这一目标。例如,缓冲区溢出。
您拥有的任何类型的未定义行为都可能导致这种情况。例如:
另请参阅问题实际上可能的未定义行为的最糟糕示例是什么?以及C++ 程序员应该知道的所有常见的未定义行为是什么?.
您最好的选择是使用边界和内存检查器,以帮助进行大量调试。
一个非常常见的情况:试图从构造函数中调用纯虚方法......
构造函数
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
类的说明(粗略)
为 _vtable 分配足够的内存(当然)和足够的内存(每个虚函数 1 个函数指针,通常按照它们的声明顺序,从最左边的基数开始)
调用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
调用析构函数之前发生了什么?我不 ;)
结论
永远不要从构造函数或析构函数中调用虚方法。
如果不是这样,那么您将面临内存损坏,运气不好;)
我的第一个猜测是某些代码是memset()
一个类对象。
这完全取决于实现。然而,假设删除后一些其他操作可能会将内存空间设置为空,这是相当安全的。
其他可能性包括通过一些松散的指针覆盖内存 - 实际上在我的情况下它几乎总是这样......
也就是说,您永远不应该在删除后尝试使用对象。