1. 程式人生 > 其它 >c學習47-虛擬函式(多型)

c學習47-虛擬函式(多型)

1. 多型

1. C++中的虛擬函式的作用主要是實現了多型的機制。關於多型,簡而言之就是用父類型別的指標指向其子類的例項,然後通過父類的指標呼叫實際子類的成員函式。這種技術可以讓父類的指標有“多種形態”,這是一種泛型技術。

2. 所謂泛型技術,說白了就是試圖使用不變的程式碼來實現可變的演算法。比如:模板技術,RTTI技術,虛擬函式技術,要麼是試圖做到在編譯時決議,要麼試圖做到執行時決議。

2. 多型特點

1.多型是在不同繼承關係的類物件,去調同一函式,產生了不同的行為。

2. 就是說,有一對繼承關係的兩個類,這兩個類裡面都有一個函式且名字、引數、返回值均相同,然後我們通過呼叫函式來實現不同類物件完成不同的事件。

3. 構成條件

  1. 呼叫函式的物件必須是指標或者引用。
  2. 被呼叫的函式必須是虛擬函式,且完成了虛擬函式的重寫。

4. 虛擬函式表

1.虛擬函式(Virtual Function)是通過一張虛擬函式表(Virtual Table)來實現的。簡稱為V-Table。在這個表中,主是要一個類的虛擬函式的地址表,這張表解決了繼承、覆蓋的問題,保證其容真實反應實際的函式。有虛擬函式的類的例項中這個表被分配在了這個例項的記憶體中。當我們用父類的指標來操作一個子類的時候,這張虛擬函式表就顯得由為重要了,它就像一個地圖一樣,指明瞭實際所應該呼叫的函式。

2.C++的編譯器應該是保證虛擬函式表的指標存在於物件例項中最前面的位置(這是為了保證取到虛擬函式表的有最高的效能——如果有多層繼承或是多重繼承的情況下)。所以假如物件A的類有虛擬函式,那麼物件A首地址“&A”就是虛擬函式表的地址。

5. 虛擬函式“地址呼叫”的分析

解析“(Fun)*(int*)*(int*)(&base27_b_01)”
(&base27_b_01)  是虛擬函式表地址
(int*)(&base27_b_01) 強轉為(int*) 型別
*(int*)(&base27_b_01) 解引用,表示虛擬函式表(非虛擬函式表地址),也是虛擬函式表的第一個函式指向入口地址
(int*)*(int*)(&base27_b_01)  強轉為(int*) 型別
*(int*)*(int*)(&base27_b_01) 解引用,表示第一個虛擬函式(入口地址,函式首地址,真實的函式程式碼)
(Fun)*(int*)*(int*)(&base27_b_01) 再強轉為自定義的函式指標,就可以呼叫了
總結是:兩次解引用,三次強制轉換。第一次解引用到真實虛擬函式表也是函式指向入口地址,
第二次解引用表示真正的函式程式碼段,函式入口地址,函式首地址
重要區分:表與表地址,函式與函式地址,指向函式入口地址與入口地址

6.索引

1. 不用虛擬函式,派生類對基類成員函式重定義,基類指標賦值為子類的物件引用,呼叫的成員函式是基類成員函式。
2. 採用虛擬函式,派生類對基類成員函式重定義,基類指標賦值為子類的物件引用,呼叫的成員函式是子類成員函式。
3. 虛擬函式表
4. (Fun)*(int*)*(int*)(&base27_b_01) 因為我的系統指標佔8位,轉成(int*)型別會告警,那麼轉成(long*)怎麼樣?
5. gdb檢視虛擬函式入口地址

————————————————————————————————————————-

1.

class base27_A{//基類
public:
    void fun27_01(){//非虛擬函式
        printf("我是基類\n");  
    }  
};
class class27_A:public base27_A{//子類1
public:
    void fun27_01(){
        printf("我是子類1\n");  
    }  
};
class class27_B:public base27_A{//子類2
public:
    void fun27_01(){
        printf("我是子類2\n");  
    }  
};
void test_27_01(){//測試函式
    base27_A *base;
    class27_A class27_a_01;
    class27_B class27_b_01;
    base = &class27_a_01;//賦值子類1的物件,基類指標呼叫的還是基類成員函式。
    base->fun27_01();
    base = &class27_b_01;//賦值子類2的物件,基類指標呼叫的還是基類成員函式。
    base->fun27_01();
}

輸出結果:
    我是基類
    我是基類

2. 基類改為 虛擬函式,子類如上一樣,結果就變了。

class base27_A{//基類
public:
    virtual void fun27_01(){//新增關鍵字virtual,變成虛擬函式,結果就變了
        printf("我是基類\n");  
    }  
};

輸出結果:
    我是子類1
    我是子類2

3. 虛擬函式表

/* 虛擬函式表 */
class base27_B{//基類
public:
    virtual void fun27_01(){
        printf("我是基類函式1\n");
    }
    virtual void fun27_02(){
        printf("我是基類函式2\n");
    }
    virtual void fun27_03(){
        printf("我是基類函式3\n");
    }
};
class class27_c:public base27_B{//子類
public:
    void fun27_01(){
        printf("我是子類函式1\n");
    }
    void fun27_02(){
        printf("我是子類函式2\n");
    }
    void fun27_03(){
        printf("我是子類函式3\n");
    }
};
void test_27_02(){//測試函式
    typedef void(*Fun)(void); //定義一個函式指標別名
    base27_B base27_b_01;//基類例項化一個物件
    Fun pFun1 = NULL;//定義一個函式指標
    printf("base27_b_01物件的虛擬函式表地址:%p\n",(int*)(&base27_b_01));
    /*物件的首地址,“&base27_b_01”,把地址強制轉為(int*)型別。(int*)型別好操作,
    如要執行下一個虛擬函式表的下一個,加2就行了((int*)+2);我的linux系統要加2,因為指標佔8位元組*/
    printf("物件base27_b_01第一個虛擬函式地址:%p\n",(int*)*(int*)(&base27_b_01));
    //虛擬函式表地址,解引用就是第一個虛擬函式指向入口地址。
    printf("物件base27_b_01第二個虛擬函式地址:%p\n",(int*)*(int*)(&base27_b_01)+2);
    //第一個虛擬函式地址加1就是第二個函式指向入口地址。
    pFun1 = (Fun)*(int*)*(int*)(&base27_b_01);//第一個函式入口地址,函式指向入口地址解引用就是入口地址,再強轉為函式指標,就可以呼叫
    pFun1();//呼叫函式
    pFun1 = (Fun)*((int*)*(int*)(&base27_b_01)+2);//第二個虛擬函式函式入口地址
    pFun1();//呼叫函式
    pFun1 = (Fun)*((int*)*(int*)(&base27_b_01)+4);//第三個虛擬函式函式入口地址
    pFun1();//呼叫函式,因為我的系統指標佔用8位元組是兩個int大小,所以要加2。
    /*
    解析“(Fun)*(int*)*(int*)(&base27_b_01)”
    (&base27_b_01)  是虛擬函式表地址
    (int*)(&base27_b_01) 強轉為(int*) 型別
    *(int*)(&base27_b_01) 解引用,表示虛擬函式表(非虛擬函式表地址了),也是虛擬函式表的第一個函式入口地址
    (int*)*(int*)(&base27_b_01)  強轉為(int*) 型別
    *(int*)*(int*)(&base27_b_01) 解引用,表示第一個虛擬函式(非入口地址了)
    (Fun)*(int*)*(int*)(&base27_b_01) 再強轉為自定義的函式指標,就可以呼叫了
    總結是:兩次解引用,三次強制轉換。第一次解引用到真實虛擬函式表也是函式入口地址,
    第二次解引用表示真正的函式程式碼段
    重要區分:表與表地址,函式與函式地址,函式指向入口地址與入口地址
    */


輸出結果:
base27_b_01物件的虛擬函式表地址:0x7ffcd0764470
物件base27_b_01第一個虛擬函式地址:0x400cb0
物件base27_b_01第二個虛擬函式地址:0x400cb8
我是基類函式1
我是基類函式2
我是基類函式3

測試函式地址實現子類成員函式(父類虛擬函式),在上面的測試函式下面新增如下程式碼

    class27_c class27_c_01;
    pFun1 = (Fun)*(int*)*(int*)(&class27_c_01);
    pFun1();//執行第一個子類物件的成員函式(父類的虛擬函式)
    pFun1 = (Fun)*((int*)*(int*)(&class27_c_01)+2);
    pFun1();//執行第二個子類物件的成員函式(父類的虛擬函式)
    pFun1 = (Fun)*((int*)*(int*)(&class27_c_01)+4);
    pFun1();//執行第二個子類物件的成員函式(父類的虛擬函式)

輸出結果:
我是基類函式1
我是基類函式2
我是基類函式3
我是子類函式1
我是子類函式2
我是子類函式3

4. (Fun)*(int*)*(int*)(&base27_b_01) 因為我的系統指標佔8位,轉成(int*)型別會告警,那麼轉成(long*)怎麼樣? 測試發現不再告警。


void test_27_03(){//測試函式
    typedef void(*Fun)(void); //定義一個函式指標別名
    base27_B base27_b_01;//基類例項化一個物件
    Fun pFun1 = NULL;//定義一個函式指標
    printf("base27_b_01物件的虛擬函式表地址:%p\n",(long*)(&base27_b_01));
    printf("物件base27_b_01第一個虛擬函式地址:%p\n",(long*)*(long*)(&base27_b_01));
    printf("物件base27_b_01第二個虛擬函式地址:%p\n",(long*)*(long*)(&base27_b_01)+2);
    pFun1 = (Fun)*(long*)*(long*)(&base27_b_01);
    pFun1();
    pFun1 = (Fun)*((long*)*(long*)(&base27_b_01)+1);
    pFun1();
    pFun1 = (Fun)*((long*)*(long*)(&base27_b_01)+2);
    pFun1();
    printf("---------物件class27_c_01-------\n");
    class27_c class27_c_01;
    pFun1 = (Fun)*(long*)*(long*)(&class27_c_01);
    pFun1();//執行第一個子類物件的成員函式(父類的虛擬函式)
    pFun1 = (Fun)*((long*)*(long*)(&class27_c_01)+1);
    pFun1();//執行第二個子類物件的成員函式(父類的虛擬函式)
    pFun1 = (Fun)*((long*)*(long*)(&class27_c_01)+2);
    pFun1();//執行第二個子類物件的成員函式(父類的虛擬函式)
}

此時編譯沒有告警
輸出結果:
base27_b_01物件的虛擬函式表地址:0x7ffd44f29dc0
物件base27_b_01第一個虛擬函式地址:0x400e10
物件base27_b_01第二個虛擬函式地址:0x400e20
我是基類函式1
我是基類函式2
我是基類函式3
---------物件class27_c_01-------
我是子類函式1
我是子類函式2
我是子類函式3

5. gdb檢視虛擬函式入口地址。以上程式碼新增如下程式碼

printf("---------虛擬函式入口地址-------\n");
printf("物件base27_b_01第一個虛擬函式入口地址:%p\n",(long*)*(long*)*(long*)(&base27_b_01));
printf("物件base27_b_01第二個虛擬函式入口地址:%p\n",(long*)*((long*)*(long*(&base27_b_01)+1));
printf("物件base27_b_01第三個虛擬函式入口地址:%p\n",(long*)*((long*)*(long*(&base27_b_01)+2));

輸出結果:
---------虛擬函式入口地址-------
物件base27_b_01第一個虛擬函式入口地址:0x400b26
物件base27_b_01第二個虛擬函式入口地址:0x400b3e
物件base27_b_01第三個虛擬函式入口地址:0x400b56

gda檢視結果。分析得出,此命令檢視的是虛擬函式入口地址,而不是,虛擬函式表裡的指向入口地址,是指向入口地址再解引用得到函式入口地址。從“地址差”也可以發現不是恆定的指標位元組大小(8byte)。

參考:c學習-48 gdb學習3