1. 程式人生 > >為什麼不要在建構函式中呼叫虛擬函式

為什麼不要在建構函式中呼叫虛擬函式

先看一段在建構函式中直接呼叫虛擬函式的程式碼:

#include <iostream>

class Base
{
public:
    Base() { Foo(); }   ///< 列印 1

    virtual void Foo()
    {
        std::cout << 1 << std::endl;
    }
};

class Derive : public Base
{
public:
    Derive() : Base(), m_pData(new int(2)) {}
    ~Derive() { delete m_pData; }

    virtual void Foo()
    {
        std::cout << *m_pData << std::endl;
    }
private:
    int* m_pData;
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

 這裡的結果將列印:1。

  這表明第6行執行的的是Base::Foo()而不是Derive::Foo(),也就是說:虛擬函式在建構函式中“不起作用”。為什麼?

  當例項化一個派生類物件時,首先進行基類部分的構造,然後再進行派生類部分的構造。即建立Derive物件時,會先呼叫Base的建構函式,再呼叫Derive的建構函式。

  當在構造基類部分時,派生類還沒被完全建立,從某種意義上講此時它只是個基類物件。即當Base::Base()執行時Derive物件還沒被完全建立,此時它被當成一個Base物件,而不是Derive物件,因此Foo繫結的是Base的Foo。

  C++之所以這樣設計是為了減少錯誤和Bug的出現。假設在建構函式中虛擬函式仍然“生效”,即Base::Base()中的Foo();所呼叫 的是Derive::Foo()。當Base::Base()被呼叫時派生類中的資料m_pData還未被正確初始化,這時執行 Derive::Foo()將導致程式對一個未初始化的地址解引用,得到的結果是不可預料的,甚至是程式崩潰(訪問非法記憶體)。

  總結來說:基類部分在派生類部分之前被構造,當基類建構函式執行時派生類中的資料成員還沒被初始化。如果基類建構函式中的虛擬函式呼叫被解析成呼叫派生類的虛擬函式,而派生類的虛擬函式中又訪問到未初始化的派生類資料,將導致程式出現一些未定義行為和bug。

  對於這一點,一般編譯器會給予一定的支援。如果將基類中的Foo宣告成純虛擬函式時(看下面程式碼),編譯器可能會:在編譯時給出警告、連結時給出 符號未解析錯誤(unresolved external symbol)。如果能生成可執行檔案,執行時一定出錯。因為Base::Base()中的Foo總是呼叫Base::Foo,而此時Base::Foo 只宣告沒定義。大部分編譯器在連結時就能識別出來。

#include <iostream>

class Base
{
public:
    Base() { Foo(); }   ///< 可能的結果:編譯警告、連結出錯、執行時錯誤

    virtual void Foo() = 0;
};

class Derive : public Base
{
public:
    Derive() : Base(), m_pData(new int(2)) {}
    ~Derive() { delete m_pData; }

    virtual void Foo()
    {
        std::cout << *m_pData << std::endl;
    }
private:
    int* m_pData;
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

 如果編譯器都能夠在編譯或連結時識別出這種錯誤呼叫,那麼我們犯錯的機會將大大減少。只是有一些比較不直觀的情況(看下面程式碼),編譯器是無法判斷出來的。這種情況下它可以生成可執行檔案,但是當程式執行時會出錯。

#include <iostream>

class Base
{
public:
    Base() { Subtle(); }   ///< 執行時錯誤(pure virtual function call)

    virtual void Foo() = 0;
    void Subtle() { Foo(); }
};

class Derive : public Base
{
public:
    Derive() : Base(), m_pData(new int(2)) {}
    ~Derive() { delete m_pData; }

    virtual void Foo()
    {
        std::cout << *m_pData << std::endl;
    }
private:
    int* m_pData;
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

 從編譯器開發人員的角度上看,如何實現上述的“特性”呢?

  我的猜測是在虛擬函式表地址的繫結上做文章:在“當前類”(正在被構造的類)的建構函式被呼叫時,將“當前類”的虛擬函式表地址繫結到物件上。當基 類部分被構造時,“當前類”是基類,這裡是Base,即當Base::Base()的函式體被呼叫時,Base的虛擬函式表地址會被繫結到物件上。而當 Derive::Derive()的函式體被呼叫時,Derive的虛擬函式表地址被繫結到物件上,因此最終物件上繫結的是Derive的虛擬函式表。

  這樣編譯器在處理的時候就會變得很自然。因為每個類在被構造時不用去關心是否有其他類從自己派生,而不需要關心自己是否從其他類派生,而只要按 照一個統一的流程,在自身的建構函式執行之前把自身的虛擬函式表地址繫結到當前物件上(一般是儲存在物件記憶體空間中的前4個位元組)。因為物件的構造是從最基 類部分(比如A<-B<-C,A是最基類,C是最派生類)開始構造,一層一層往外構造中間類(B),最後構造的是最派生類(C),所以最終對 象上繫結的就自然而然就是最派生類的虛擬函式表。

  也就是說物件的虛擬函式表在物件被構造的過程中是在不斷變化的,構造基類部分(Base)時被繫結一次,構造派生類部分(Derive)時,又重 新繫結一次。基類建構函式中的虛擬函式呼叫,按正常的虛擬函式呼叫規則去呼叫函式,自然而然地就呼叫到了基類版本的虛擬函式,因為此時物件繫結的是基類的虛擬函式 表。

  下面要給出在WIN7下的Visual Studio2010寫的一段程式,用以驗證“物件的虛擬函式表在物件被構造的過程中是在不斷變化的”這個觀點。

  這個程式在類的建構函式裡做了三件事:1.打印出this指標的地址;2.列印虛擬函式表的地址;3.直接通過虛擬函式表來呼叫虛擬函式。

  列印this指標,是為了表明建立Derive物件是,不管是執行Base::Base()還是執行Derive::Derive(),它們構造的是同一個物件,因此兩次打印出來的this指標必定相等。

#include <iostream>

class Base
{
public:
    Base() { PrintBase(); }
    virtual ~Base() { PrintBase(); }

    void PrintBase()
    {
        std::cout << "Address of Base: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Base Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];   ///< 注意這裡索引變成 1 了,因為解構函式定義在Foo之前
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void  Foo() { std::cout << "Base" << std::endl; }
};

class Derive : public Base
{
public:
    Derive() : Base() { PrintDerive(); }
    virtual ~Derive() { PrintDerive(); }

    void PrintDerive()
    {
        std::cout << "Address of Derive: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Derive Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];   ///< 注意這裡索引變成 1 了,因為解構函式定義在Foo之前
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void Foo() { std::cout << "Derive" << std::endl; }
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

   列印虛擬函式表的地址,是為了表明在建立Derive物件的過程中,虛擬函式表的地址是有變化的,因此兩次打印出來的虛擬函式表地址必定不相等。

  直接通過函式表來呼叫虛擬函式,只是為了表明前面所列印的確實是正確的虛擬函式表地址,因此Base::Base()的第19行將列印Base,而Derive::Derive()的第43行將列印Derive。

  注意:這段程式碼是編譯器相關的,因為虛擬函式表的地址在物件中儲存的位置不一定是前4個位元組,這是由編譯器的實現細節來決定的,因此這段程式碼在不同的編譯器未必能正常工作,這裡所使用的是Visual Studio2010。

#include <iostream>

class Base
{
public:
    Base() { PrintBase(); }

    void PrintBase()
    {
        std::cout << "Address of Base: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Base Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[0];
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void  Foo() { std::cout << "Base" << std::endl; }
};

class Derive : public Base
{
public:
    Derive() : Base() { PrintDerive(); }

    void PrintDerive()
    {
        std::cout << "Address of Derive: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Derive Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[0];
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void Foo() { std::cout << "Derive" << std::endl; }
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

 輸出的結果跟預料的一樣:

Address of Base: 002E7F98
Address of Base Vtable: 01387840
Call Foo by vt -> Base

Address of Derive: 002E7F98
Address of Derive Vtable: 01387834
Call Foo by vt -> Derive

 在解構函式中呼叫虛擬函式,和在建構函式中呼叫虛擬函式一樣。

  解構函式的呼叫跟建構函式的呼叫順序是相反的,它從最派生類的解構函式開始的。也就是說當基類的解構函式執行時,派生類的解構函式已經執行過, 派生類中的成員資料被認為已經無效。假設基類中虛擬函式呼叫能呼叫得到派生類的虛擬函式,那麼派生類的虛擬函式將訪問一些已經“無效”的資料,所帶來的問題和訪 問一些未初始化的資料一樣。而同樣,我們可以認為在析構的過程中,虛擬函式表也是在不斷變化的。

  將上面的程式碼增加解構函式的呼叫,並稍微修改一下,就能驗證這一點:

#include <iostream>

class Base
{
public:
    Base() { PrintBase(); }
    virtual ~Base() { PrintBase(); }

    void PrintBase()
    {
        std::cout << "Address of Base: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Base Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];   ///< 注意這裡索引變成 1 了,因為解構函式定義在Foo之前
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void  Foo() { std::cout << "Base" << std::endl; }
};

class Derive : public Base
{
public:
    Derive() : Base() { PrintDerive(); }
    virtual ~Derive() { PrintDerive(); }

    void PrintDerive()
    {
        std::cout << "Address of Derive: " << this << std::endl;

        // 虛表的地址存在物件記憶體空間裡的頭4個位元組
        int* vt = (int*)*((int*)this);
        std::cout << "Address of Derive Vtable: " << vt << std::endl;

        // 通過vt來呼叫Foo函式,以證明vt指向的確實是虛擬函式表
        std::cout << "Call Foo by vt -> ";
        void (*pFoo)(Base* const) = (void (*)(Base* const))vt[1];   ///< 注意這裡索引變成 1 了,因為解構函式定義在Foo之前
        (*pFoo)(this);

        std::cout << std::endl;
    }

    virtual void Foo() { std::cout << "Derive" << std::endl; }
};

int main()
{
    Base* p = new Derive();
    delete p;
    return 0;
}

下面是列印結果,可以看到構造和析構是順序相反的兩個過程:

Address of Base: 001E7F98
Address of Base Vtable: 01297844
Call Foo by vt -> Base

Address of Derive: 001E7F98
Address of Derive Vtable: 01297834
Call Foo by vt -> Derive

Address of Derive: 001E7F98
Address of Derive Vtable: 01297834
Call Foo by vt -> Derive

Address of Base: 001E7F98
Address of Base Vtable: 01297844
Call Foo by vt -> Base

 最終結論:

    2. 物件的虛擬函式表地址在物件的構造和析構過程中會隨著部分類的構造和析構而發生變化,這一點應該是編譯器實現相關的。

注:以上的討論是基於簡單的單繼承,對於多重繼承或虛繼承會有一些細節上的差別。

相關推薦

為什麼不要建構函式呼叫虛擬函式

先看一段在建構函式中直接呼叫虛擬函式的程式碼: #include <iostream> class Base { public: Base() { Foo(); } ///< 列印 1 virtual void Foo() { std:

不要建構函式和解構函式呼叫虛擬函式

提到建構函式和解構函式,想必大家肯定是非常瞭解,但是能否在建構函式或是解構函式中呼叫虛擬函式呢? 答案是千萬不要這麼做,這麼做不會得到大家想要的結果。 首先提一下建構函式,建構函式的順序是從基類開始構造->子類,如果在基類中呼叫虛擬函式,由於建構函式基類中僅存在自身

C++建構函式呼叫虛擬函式是否有多型的效果

C++多型的一個重要應用就是虛擬函式。但是當我們再基類的建構函式中呼叫一個子類過載的虛擬函式會出現多型的效果嗎?我們具體看一下下面的例項: #include <iostream> #define P(x) std::cout<<x<<std::endl;

C++建構函式呼叫虛擬函式

談談關於建構函式中呼叫虛擬函式的情況,僅討論單繼承,不考慮虛擬繼承和多重繼承。 測試平臺:VS2013 + Win7X64 一個例子: #include <stdlib.h> #i

建構函式/解構函式呼叫虛擬函式

先看一段在建構函式中直接呼叫虛擬函式的程式碼: 1 #include <iostream> 2 3 class Base 4 { 5 public: 6 Base() { Foo(); } ///< 列印 1 7 8

C/C++—— 在建構函式呼叫虛擬函式能實現多型嗎(Vptr指標初始化的過程分析)

問題引入: 比如:如果我們想在父類的建構函式中呼叫虛擬函式,當定義子類物件的時候,父類的建構函式中的虛擬函式執行的是子類中的函式。 在下面的例子中,定義子類物件的時候,在父類建構函式中的print虛擬函式執行的不是子類中的print函式,而是父類中的prin

多型性---建構函式和解構函式呼叫虛擬函式

參考 C++ primer 15.4.5 /* 建構函式和解構函式中的虛擬函式 */ #include<iostream> using namespace std; class Base { public: //在建構函式和解構函式中呼叫虛擬函式,則執行自身型別定義的版本。原因是初始

C++ 建構函式呼叫虛擬函式

我們知道:C++中的多型使得可以根據物件的真實型別(動態型別)呼叫不同的虛擬函式。這種呼叫都是物件已經構建完成的情況。那如果在建構函式中呼叫虛擬函式,會怎麼樣呢? 有這麼一段程式碼: class A { public: A ():m_iVal(0){test();}

C++建構函式呼叫虛擬函式嗎?

      環境:XPSP3 VS2005         今天黑總給應聘者出了一個在C++的建構函式中呼叫虛擬函式的問題,具體的題目要比標題複雜,大體情況可以看如下的程式碼: class Base { public: Base() { Fuction(); }

C++進階--建構函式和解構函式虛擬函式

//############################################################################ /* 任何時候都不要在建構函式或解構函式中呼叫虛擬函式 */ class dog { public: string m_name

建構函式可以呼叫虛擬函式嗎?語法上通過嗎?語義上可以通過嗎?

牛客網 ------------------- ------------------- ------------------- 設計模式 ------------------- ------------------- (adsbygoogle =

C++在多層繼承呼叫虛擬函式

在一個類中的虛擬函式說明,只對派生類中重定義的函式有影響,對它的基類中的函式不起作用。 例: #include <iostream> usingnamespacestd; class A { public:     void show()  

為什麼不能在建構函式使用虛擬函式

先上程式碼: // c_datastructure.cpp : 定義控制檯應用程式的入口點。 // #include "stdafx.h" #include<iostream> #in

C++通過虛擬函式呼叫虛擬函式

    C++的類如果有虛擬函式,則該類的第一個成員的數值,是一個地址,指向其虛擬函式表。例如     class CTest { public: virtual void Test1(void) { cout<&l

【lua】C 函式呼叫Lua函式時,對於lua_pcall使用的困惑

最近在學習使用Lua,也通過基本的語法知識完成了公司的一個關於配置檔案引數合法性檢查的小任務。雖然任務完成了,但對於一些函式的呼叫目的還是搞不明白,這兩天再次重看了Manual Reference,稍微梳理出了一點眉目,記錄在此。 首先看一段小小小程式 fun

C 函式呼叫Lua函式時,對於lua_pcall使用的困惑

最近在學習使用Lua,也通過基本的語法知識完成了公司的一個關於配置檔案引數合法性檢查的小任務。雖然任務完成了,但對於一些函式的呼叫目的還是搞不明白,這兩天再次重看了Manual Reference,稍微梳理出了一點眉目,記錄在此。 首先看一段小小小程式 //test.lua

虛解構函式建構函式最好不要呼叫虛擬函式

參考Effective c++ 條款7 和調款9 條款7: 多型性質的基類虛解構函式的重要性! 1、帶多型性質的 base classes應該宣告一個virtual 解構函式, 如果class帶有任何virtual函式,它就應該擁有一個virtual解構函

建構函式是否可以呼叫虛擬函式

1. 從語法上講,呼叫完全沒有問題。 2. 但是從效果上看,往往不能達到需要的目的。 Effective 的解釋是: 派生類物件構造期間進入基類的建構函式時,物件型別變成了基類型別,而不是派生類型別。 同樣,進入基類解構函式時,物件也是基類型別。 所以,虛擬函式始終僅僅呼叫

C++ 建構函式,解構函式能否呼叫虛擬函式

牛客網 ------------------- ------------------- ------------------- 設計模式 ------------------- -------------------

建構函式與解構函式呼叫虛擬函式

本文參考《effective C++》第九條款 在C++中,提倡不能在建構函式和解構函式中呼叫虛擬函式。 這是為什麼呢? 首先,我們先回顧一下C++虛擬函式的作用。 虛擬函式的引入是c++執行時多型的體現,通過呼叫虛擬函式可以在執行程式時實現動態繫結,體現