1. 程式人生 > >為什麼要使用虛擬函式和 指標(或是引用)才能實現多型?

為什麼要使用虛擬函式和 指標(或是引用)才能實現多型?

補充:(2016.04.05)

上次使用了g++ -fdump-tree-original test3.cpp命令看到了編譯器補充後的函式是什麼樣子的,但是沒有記錄,今天補充下:

   下文中第一main函式的程式碼使用如上命名編譯後生成一個名為:test3.cpp.003t.original的檔案。開啟檔案檢視後會發下編譯器給補充的default建構函式和copy函式分別如下圖所示:



       從圖中可以看到,編譯器會自動給程式碼新增預設的建構函式和copy建構函式,同時在此兩個建構函式中加入,對 _vptr.shape(指向虛表的,虛指標)賦值,指向Shape類的虛擬函式表,之所以附帶貼上編譯器新增的預設建構函式,是為了說明copy建構函式中的虛指標確實是指向Shape類的虛擬函式表。

       在咱們的例子中,當以shape1,shape2作為OutputShape()函式的引數時,由於OutputShape函式定義時的預設引數為Shape型別,而傳進去的引數均為Shape型別的子類,此時會隱式呼叫Shape類的copy建構函式。而copy建構函式是隻對成員變數進行拷貝, 虛指標(_vpyr)指向Shape的虛擬函式表。所以,輸出結構就是……。

       對下文的第二個main函式的程式碼使用如上命名編譯後生成一個名為:test3.cpp.003t.original的檔案。開啟檔案檢視後會發下編譯器給補充的default建構函式和copy函式之外,還給預設補充了copy assignment operator【注意,上邊的程式碼中並沒有生成copy assignment operator函式,《深入理解C++物件模型》中說,編譯器預設會給程式碼生成4個函式,分別是:default建構函式(前提是類沒有宣告任何建構函式)、copy建構函式、copy assignment operator和解構函式;但同時也說,如果四種函式在函式中根本就沒有使用的時候,則不會生成之,有用的叫non-trivial的,沒用的叫trivial的;此次生成copy assignment operator函式,而上邊的情況編譯器並沒有生成此copy assignment operator函式也印證了此說法】,下圖所示為編譯器生成的copy assignment operator函式:


觀察發現此copy assignment operator函式的形式引數為Shape的const的引用,所以此問題其實是對上邊描述的情況的一個包裝!

        對下文的第三個main函式的程式碼使用如上命名編譯後生成一個名為:test3.cpp.003t.original的檔案。

    此部分需要自己對照第3個main函式仔細分析,其實我現在還是沒有完全明白!因為我手工更改了_vptr指標的值,所以以指標的方式的temp1->DrawSelf()呼叫DrawSelf函式時,編譯器將其翻譯為:

OBJ_TYPE_REF(*NON_LVALUE_EXPR <NON_LVALUE_EXPR <temp1>->_vptr.Shape>;NON_LVALUE_EXPR <temp1>->0) (NON_LVALUE_EXPR <temp1>) >>

所以執行後輸出結果為“連線各頂點”;但是以temp.DrawSelf()的方式呼叫DrawSelf函式時,編譯器將其翻譯為:DrawSelf (&temp) >>>

但是我查找了整個檔案,並沒有找到引數為Shape*的DrawSelf函式,我猜測此處是因為DrawSelf被宣告為虛擬函式,此處是直接以Shape::DrawSelf的方式呼叫了Shape的虛擬函式???此處仍是一個疑問,倘若有高手看到,還望指教,感謝!

至此,此文此文完結,剩餘的一個疑問後續學習時留心解決!(2016.04.05晚20:57)

補充:

此文中一樓的朋友的回覆:——"使用物件時,成員函式的呼叫派遣是由編譯器確定後硬編碼程序序的,沒有經過虛擬函式表。"符合了我上文的猜測,但是原理還是不太明瞭!(2017.2.10-16:43)

補充:

今天(2016.3.16)使用如下命令在檢視編譯器是如何忘建構函式里加程式碼的時候突然有了個疑問:虛擬函式表是屬於類的還是物件的,因為我看到貌似是直接賦一個固定的值給_vptr。網上搜索了下,確定了自己的猜測:虛擬函式表是屬於類的!

g++ -fdump-tree-original test3.cpp

------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------

正文:

        首先說說為什麼是這樣一個題目,其實我最開始思考的是子類例項化的物件賦值給父類例項化的物件,為什麼不能實現多型!搜尋後發現一個哥們兒跟我的疑問一樣,但是題目是這個,我也覺的這個題目更好些。

        最近我在學習《深度探索C++物件模型》這本書,明白了C++物件模型的記憶體佈局。但也恰巧是這個記憶體佈局讓我又一次陷入了深深的疑惑之中。先看看我的例子:

注:此例也是引用某位博主的,只是搜尋的內容太多了,找不到原連線的位置了……。
#include <iostream>
using namespace std;

class Polygon;
class Shape//形狀
{
public:
    virtual void DrawSelf()//繪製自己
    {
       cout << "我是一個什麼也繪不出的圖形" << endl;
    }
};

class Polygon:public Shape//多邊形
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "連線各頂點" << endl;
    }
};

class Circ:public Shape//圓
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "以圓心和半徑為依據畫弧" << endl;
    }
};

void OutputShape(Shape arg)//專門負責呼叫形狀的繪製自己的函式
{
    arg.DrawSelf();
}

int main()
{
    Polygon shape1;
    Circ shape2;
    //Shape temp;
    //temp=shape1;
    //temp.must(shape1);
    OutputShape(shape1);
    OutputShape(shape2);
}
上述程式碼執行後輸出的結果為:

        為什麼是這樣的結果?因為在呼叫OutputShape(shape1) 和 OutputShape(shape2)時,在引數傳遞時會呼叫arg(Shape)的copy建構函式,arg的copy建構函式中應該僅僅是對成員變數的拷貝!(已經確認僅僅只拷貝成員變數的值,不會涉及_vptr的賦值),所以_vptr還是指向原來虛擬函式表,所以呼叫的仍為基類的DrawSelf()函式。(注意此處將會產生slice問題,即將子類例項記憶體佈局中從父類繼承來的變數copy 到父類例項的對應成員變數中,子類自身的成員變數被捨棄。)

注:此處我需要搞清楚編譯器自動給加的copy建構函式究竟長什麼樣?但是我現在還沒找到好的辦法。(已解決,參見2016.04.05的補充

如果將上述的main函式改為:

int main()
{
    Polygo shape1;
    Circ shape2;
    Shape temp;
    temp=shape1;
    //temp.must(shape1);
    OutputShape(temp);
    OutputShape(shape2);
}

輸出的結果仍然為:


為什麼是這樣的結果?arg的copy assignment operator中應該僅僅是對成員變數的拷貝!所以_vptr還是指向原來虛擬函式表,所以呼叫的仍為基類的DrawSelf()函式。

注:此處我需要搞清楚編譯器自動給加的 copy assignment operator函式究竟長什麼樣?但是我現在還沒找到好的辦法。(已解決,參見2016.04.05的補充

基於以上兩個疑問,我在搜尋的過程中又發現了篇關於深拷貝和淺拷貝的文章,覺的寫的淺顯易懂,也轉載了過來。

對於徹底搞清楚copy建構函式和copy assignment operator也需要一篇文章(這篇文章中和effective C++ 中的條款5有不一致的地方需要驗證,究竟誰說的對)。

        起初我碰到這個問題時上網搜尋了下,發現了這篇文章中作者和我其實有相同的疑問。通過閱讀作者的文章,瞭解到了是因為_vptr沒有拷貝的原因,但是作者的疑問也同樣困擾了我,到現在也沒有想到合理的解釋。我的實驗待如下(基於上述程式碼):

#include <iostream>
using namespace std;

class Polygon;
class Shape//形狀
{
public:
    virtual void DrawSelf()//繪製自己
    {
       cout << "我是一個什麼也繪不出的圖形" << endl;
    }

    Shape& must (const Polygon& b)
    {
        cout<<"i am in"<<endl;
        *(long *)this=*(long *)&b;
        this->DrawSelf();
        return *this;
    }
};

class Polygon:public Shape//多邊形
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "連線各頂點" << endl;
    }
};

class Circ:public Shape//圓
{
public:
    void DrawSelf()   //繪製自己
    {
       cout << "以圓心和半徑為依據畫弧" << endl;
    }
};

void OutputShape(Shape arg)//專門負責呼叫形狀的繪製自己的函式
{
    arg.DrawSelf();
}

int main()
{
    Polygon shape1;
    Circ shape2;
    Shape temp;
    temp.must(shape1);//i am in   and  連線各個節點
    Shape *temp1=&temp;
    temp1->DrawSelf();//連線各個節點
    temp.DrawSelf();//我是一個什麼也繪不出的圖形
    OutputShape(shape1);//我是一個什麼也繪不出的圖形
    OutputShape(shape2);//我是一個什麼也繪不出的圖形
}
此段函式的輸出結果是:



       我在Shape類中加入一個must()函式來強行改變此Shape例項的_vptr的值為一個Polygo例項的_vptr的值,並直接在must()函式中呼叫DrawSelf()函式,輸出達到預期。在main()函式中使用指標的方式來呼叫DrawSelf()也符合預期。但是使用temp.DrawSelf()的方式呼叫時,結果卻出乎意料的輸出了“我是一個什麼也繪不出的圖形”,但是預期確實是 “連線各頂點”。這肯定是編譯器解釋  temp.DrawSelf()  的方式導致的,但是我現在還沒有辦法解釋到底怎麼回事兒!

注:此處我需要搞清楚編譯器究竟是怎麼解釋    temp.DrawSelf() 的?但是我現在還沒找到好的辦法。(已解決,參見2016.04.05的補充

記錄下自己的思考過程,請忽略:

為什麼是這樣的結果?按照之前此文章的實踐驗證和《深度理解C++物件模型》的理解,shape1和shape2的記憶體佈局以及Shape類的例項的記憶體佈局,如上圖所示:

那麼將shape1和shape2賦給arg,其記憶體佈局中父類部分的值就都拷貝過去了,由於只有一個_vptr指標,所以此指標就覆蓋了arg中原有的_vptr中的值了呀,那麼呼叫虛擬函式查詢虛表時,就應該和使用指標(引用)的效果一樣呀,都應該呼叫的是子類中的DrawSelf()函式呀,怎麼最後呼叫了基類的DrawSelf()函數了呢?too naive o(∩_∩)o 哈哈。


圖中“其他成員”代表可以宣告別的成員變數,程式碼中為了簡單並未定義成員變數。(這樣看對頸椎好,對不住了。也簡單,看一眼就好了)