了解这个错误的原因,首先要知道 C++对象 在构造和析构时,都做了什么事情?首先要说明这种情况是在 C++基类包含纯虚函数以及虚析构函数的时候出现的。
那么,C++构造函数都做了什么事情呢?
第一步:构造最顶层的基类部分
a、让实例指向基类的虚函数表 b、构造基类实例成员变量 c、执行基类构造函数
第二步:构造派生类独有部分
a、让实例指向派生类的虚函数表 b、构造派生类实例成员变量 c、执行派生类构造函数
析构函数则是相反的顺序,就像这样:
第一步:析构派生类部分
a、(实例已经指向派生类的虚函数表) b、执行派生类析构函数 c、析构派生类实例成员变量
第二步:析构基类部分
a、让实例指向基类的虚函数表 b、执行基类析构函数 c、析构基类实例成员变量
如上所述,构造函数的执行分为两个阶段 ,先构造一个基类(虚指针被赋值为基类虚函数表的地址 ),当开始构造派生类的部分时,先将虚指针重置为指向派生类的虚函数表,然后为派生类独有的成员分配空间并执行构造函数赋值。
析构函数也分为两个阶段,首先执行派生类的析构函数并将派生类的成员变量释放,然后将这个析构了一半的对象的虚指针重新指向基类的虚函数表,最后再去析构基类的部分。用一段代码展示析构的过程。
#include <iostream>
#include <unistd.h>
using namespace std;
class Base
{
public:
Base()
{
printf("base constructor virtual pointer %p\n", (void*)*(int64_t*)((void*)this));
}
virtual ~Base()
{
printf("base destructor virtual pointer %p\n", (void*)*(int64_t*)((void*)this));
}
virtual void doIt() const = 0;
};
class Derived : public Base
{
public:
Derived()
{
printf("derived constructor virtual pointer %p\n", (void*)*(int64_t*)((void*)this));
}
~Derived() override
{
printf("derived destructor virtual pointer %p\n", (void*)*(int64_t*)((void*)this));
}
};
int main()
{
Derived* const base = new Derived();
delete base;
}
输出显示构造和析构两个过程分别拥有两个阶段,两个阶段之间会将虚指针重置。

产生 Pure virtual function called! 主要有两种场景
一是在基类的构造函数中直接或间接的调用了基类的纯虚函数(直接调用一般编译器会直接报错),主要是因为在基类的构造函数执行过程中,此时这个没有完全构造成功的对象的虚指针指向的是基类的虚函数表,此时调用这个纯虚函数是通过基类的虚指针调用,但基类并不实现纯虚函数,因此会造成错误。
第二种场景,在基类的虚析构函数中调用纯虚函数,因为如上所述,如果析构已经执行到了基类的部分,说明此事这个并没有被完全析构的对象的虚指针指向的还是基类的虚函数表,跟上面的情况相同。用一段代码来展示析构中出现的问题。
#include <iostream>
#include <unistd.h>
using namespace std;
class Base
{
public:
virtual ~Base()
{
printf("base destructor\n");
}
virtual void doVirtual() const = 0;
};
class Derived : public Base
{
public:
~Derived() override
{
printf("derived destructor ");
}
void doVirtual() const override
{
std::cout<<"Derived::doVirtual()"<<std::endl;
}
};
void* task(void* const p)
{
const Base * const base = reinterpret_cast<const Base*>(p);
while (true)
{
base->doVirtual();
usleep(50000);
}
}
int main()
{
Base* const base = new Derived();
pthread_t t;
pthread_create(&t, nullptr, task, (void*)base);
delete base;
base->doVirtual();
pthread_join(t, nullptr);
}
注意:
那么是不是如果在基类的构造函数和虚析构函数中调用的不是纯虚函数,而是有具体实现的虚函数就可以呢,其实这种行为也是不行的(但是有可能不会被发觉也不会崩溃)。在上面的情况中如果在基类的构造函数和虚析构函数中调用了基类中定义的另外一个虚函数(有具体实现),那就会执行基类虚函数表中的实现,那么如果这样,你觉得多态还有什么意义了呢?
所以,建议不要在任何没有完全构造或者析构完成的对象中调用虚函数。
警告
在这个过程中即使对象被析构了仍然有很大可能性能够访问他的成员变量和虚指针,是因为对象析构只是系统回收了对象所占用内存空间的使用权,但是那段内存很可能还没有被赋值或清空,还是一个对象的完整结构,所对如果使用原来的对象或者指针进行操作仍然有可能会有效果,但回收之后指针已经是野指针,再使用它也会引发未知的结果。
|