通常在C的编程中,我们经常使用memset函数将一块连续的内存区域清零或设置为其它指定的值,最近在移植一段java代码到C++的时候,不当使用memset函数花费了我几个小时的调试时间。对于虚函数的底层机制很多
资料都有较详细
阐述,但对我个人而言,这次的调试让我感触颇深。
先来看一段代码,在继承的类Advance之中,有很多属性字段,我
希望将其清成0或NULL,于是在构造函数中我通过memset将当前类的所有属性置0。
class Base{
public:
virtual void kickoff() = 0;
};
class Advance:public Base{
public:
Advance(){
memset(this, 0, sizeof(Advance));
}
void kickoff(){
count++;
//... do something else;
}
private:
int attr1, attr2;
char* label;
int count;
//...
other attributes, they should be initiated to 0 or NULL at beginning.
};
int _tmain(int argc, _TCHAR* argv[])
{
Base* ptr = new Advance();
ptr->kickoff();
return 0;
}
这样看似能正常运行,但运行程序时,你会发现类似于下面的错误:
TestVirtual.exe 中的 0x00415390 处未处理的异常: 0xC0000005: 读取位置 0x00000000
时发生访问冲突
同时断点停留在ptr->kickoff()处,从错误提示我们可以得知无法调用kickoff方法,这个方法的指针没有被正确初始化,但为什么呢?
指出问题之前,先看看这段文献上的关于虚函数机制的说明:
函数赖以生存的底层机制:vptr +
vtable。虚函数的运行时实现采用了VPTR/VTBL的形式,这项
技术的基础:
①编译器在后台为每个包含虚函数的类产生一个静态函数指针数组(虚函数表),在这个类或者它的基类中定义的每一个虚函数都有一个相应的函数指针。
②每个包含虚函数的类的每一个实例包含一个不可见的数据成员vptr(虚函数指针),这个指针被构造函数自动初始化,指向类的vtbl(虚函数表)
③当客户调用虚函数的时候,编译器产生代码反指向到vptr,索引到vtbl中,然后在指定的位置上找到函数指针,并发出调用。
这里的问题,就出在
memset(this, 0, sizeof(Advance));
上面,虚函数指针应该在进入构造函数赋值体之前自动初始化的,而memset却又将已经初始化好的指针清0了,这就是为什么会产生上面的访问零址的错误。将上面的memset语句去除程序就可以正常运行了。
所以,从上面的问题中,我们可以看出在构造函数体内调用memset将整个对象清0是很有风险的,当没有虚函数的时候上面程序可以正常运行(可以试着将Base类的纯虚函数声明改成非虚函数再运行程序)。初始化类的属性对象时,比较稳妥的办法还是手动逐个进行初使化