在尝试使用类型转换指针调用析构函数时,我的代码中出现了分段错误。但是如果我将析构函数更改为非虚拟析构函数,它就能正常工作。
#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错误。还有其他任何类型转换指针以使其正确的方法。
答案 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的答案