尝试调用虚拟析构函数时获取分段错误

时间:2011-10-20 20:57:03

标签: c++

在尝试使用类型转换指针调用析构函数时,我的代码中出现了分段错误。但是如果我将析构函数更改为非虚拟析构函数,它就能正常工作。

#include <iostream>
using namespace std;
class Test
{
public:
   Test() { cout << "Cons" << endl;}
   ~Test() {cout << "Des"<<endl;}
   void *var_ptr;
};
class Test3
{
public:
   Test3() { cout << "Cons3" << endl;}
   //virtual ~Test3(){cout << "Des3" << endl;};
   ~Test3(){cout << "Des3" << endl;};
};
class Test2:public Test3
{
public:
  Test2() { cout << "Cons2" << endl;}
 ~Test2() {cout << "Des2"<<endl;}
};
int  main ()
{
  Test *testPtr = new Test();
 int *ivalue ;
 ivalue = new int;
 testPtr->var_ptr = (void*)ivalue;
 ((Test2*)(testPtr->var_ptr))->~Test2();
}

OutPut :
Cons
Segmentation fault

Without virtual dtor :
Output :
Cons
Des2
Des3

我不能避免使用类型转换指针(Test2 *)到我的代码。我想了解为什么我在使用虚拟析构函数时遇到了一个seg错误。还有其他任何类型转换指针以使其正确的方法。

3 个答案:

答案 0 :(得分:4)

问题是您的代码会触发未定义的行为,这意味着几乎任何事情都可能发生。该程序无效,崩溃是一种选择,因为崩溃不仅仅是兼容编译器可以对您的代码执行的另一种选择。你应该做的是纠正你的计划。

只是为了理解行为的原因,而不是试图暗示你可以将它用于任何事情:未定义的行为 undefined ,这意味着你不能依赖它永远。对于正在发生的事情的实际解释,您可以使用以下代码复制类似的情况:

struct test1 {
   void f() { printf( "Hi\n" ); }
};
struct test2 { 
   int x; 
   void f() { printf( "%d\n", x); }
};
int main() {
   static_cast<test1*>(0)->f();    // probably won't crash
   static_cast<test2*>(0)->f();    // probably will crash
}

这个无效程序在概念上与你的程序类似,不同之处在于,在这种情况下,指针不引用任何有效内存,而是空指针。行为的实际解释与(大多数)编译器如何处理代码有关。定义成员函数时,编译器会生成一个普通函数的等价函数,其中第一个参数是指向对象的指针,其余参数随后出现。每当访问成员属性时,都会取消引用指针并读取另一端的值,但如果没有使用成员,则编译器可能根本不取消引用指针。上面的代码将在内部编译为类似于以下C代码的内容:

struct test1 {};
void test1_f( struct test1* this ) {
   printf( "Hi\n" );
}
struct test2 { int x; }
void test2_f( struct test2* this ) {
   printf( "%d\n", p->x );
}

在第一种情况下,指针根本就​​不被使用,所以即使它是一个空指针,因为它没有被解除引用,代码似乎工作(它仍然无效,应该被纠正,但在这个特定的实现它不会崩溃)。在第二种情况下,指针使用,以访问成员x,这将尝试读取虚拟地址0周围的内存,这将触发分段错误,程序将崩溃

回到原来的问题,当你声明一个虚函数时,编译器(它不是强制的,但是所有的编译器都会这样做)将为层次结构中的每个类型创建一个虚拟表,它将添加一个隐藏的指针字段到引用特定类型的虚拟表的对象。 C ++代码:

struct test {
   virtual void f() { printf( "Hi\n" ); }
};
int main() {
   static_cast<test*>(0)->f();   
}

被翻译成等价的(为简单起见,假设编译器在这里使用动态调度,考虑到0,实际上是对函数的调用,返回NULL test*以便编译器不知道该类型必须使用动态调度):

struct test {
   void (**__vptr)();                        // hidden vptr
}
test_test(struct test* this) {               // constructor
   this->__vptr = &__test_vtable;
}
void test_f( struct test* this ) {
   printf( "Hi\n" );
}
void (*__test_vtable)()[] = { &test_f };     // type is slightly off here
int main() {
   ((struct test*)(0)->__vptr)[ 0 ]( (struct test*)0 );
}

从上到下,布局了该类型的实际结构,包括指向虚拟表的隐藏指针。隐式创建构造函数,并将__vptr的值设置为适当的表。在main中,首先取消引用指针以获取最终覆盖的地址,然后调用该地址处的函数作为0指针传递this。请注意,即使test_f未取消引用指针,调用者也已尝试取消引用以访问vtable,这会触发分段错误和崩溃。

最后在你的情况下,虚方法不仅仅是任何方法,而是析构函数,它增加了一些复杂性来编写等效的C代码,事实上,取决于ABI编译器将生成多个析构函数,但前面使用虚拟f函数显示的相同问题将触发,程序将崩溃。

答案 1 :(得分:3)

当您将虚拟成员添加到其大小实际增加的类型时 - 在x86上,前四个字节包含vtable的地址。您可以使用内存监视器自己见证它。无论如何,程序期望找到vtable的地址(以获取顶级析构函数的地址),而是找到* ivalue的值。

顺便说一句,我知道这只是一个例子,但你应该让类型名称更有意义。

编辑:嗯,这唤醒了我的黑暗面,如果你想玩额外的脏,这里可能完全是VS2010特定代码似乎有效(记得在Test3中取消注释虚析构函数):< / p>
int main()
{
    // this is the actual signature of function that is called when you call destructor
    // first parameter is the pointer to the object, second to the destructor itself
    // (maybe simply first entry of the vtable?), and the last one is 0 if you
    // call explicitly and 1 if called via delete
    typedef void (__fastcall *destructor_func_t)(void*, void*, unsigned int);

    // first acquire vtable address; you have to create dummy instance of your class
    // to get it
    Test2* dummy_instance = new Test2;
    int* vtable = *reinterpret_cast<int**>(dummy_instance);
    // destructor will be called here
    delete dummy_instance;

    int* someRubbish = new int;

    // assume that destructor is the first entry in the table
    destructor_func_t destructor = reinterpret_cast<destructor_func_t>(vtable[0]);
    // destructor will be called here
    destructor(someRubbish, destructor, 0);

    delete someRubbish;
    return 0;
}

在我的机器上运行,没有抛出异常,输出:

Cons3
Cons2
Des2
Des3
Des2
Des3

答案 2 :(得分:1)

首先,您没有声明正确的内在性,Test2继承自Test3,但您创建了一个Test对象。所以,我认为Test3也继承了Test:

class Test3: public Test
{
    ...
};

其次,如果要销毁Test2,则必须创建它:

Test *testPtr = new Test2();

第三,如果要通过Test基类销毁Test2对象,则必须在基类中声明虚析构函数。所以编译器会自动调用两个析构函数:

class Test
{
    virtual ~Test() { ... }
};

你破坏Test2分配的对象只是

delete testPtr;

第四,检查@gwiazdorrr的答案