C/C++: Pure virtual function called! 錯誤解析

       瞭解這個錯誤的原因,首先要知道 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);
}

 

注意:

那麼是不是如果在基類的構造函數和虛析構函數中調用的不是純虛函數,而是有具體實現的虛函數就可以呢,其實這種行爲也是不行的(但是有可能不會被發覺也不會崩潰)。在上面的情況中如果在基類的構造函數和虛析構函數中調用了基類中定義的另外一個虛函數(有具體實現),那就會執行基類虛函數表中的實現,那麼如果這樣,你覺得多態還有什麼意義了呢?

 

所以,建議不要在任何沒有完全構造或者析構完成的對象中調用虛函數。

 

警告

在這個過程中即使對象被析構了仍然有很大可能性能夠訪問他的成員變量和虛指針,是因爲對象析構只是系統回收了對象所佔用內存空間的使用權,但是那段內存很可能還沒有被賦值或清空,還是一個對象的完整結構,所對如果使用原來的對象或者指針進行操作仍然有可能會有效果,但回收之後指針已經是野指針,再使用它也會引發未知的結果。

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章