1. 程式人生 > >c++的多型(過載、覆蓋、隱藏)

c++的多型(過載、覆蓋、隱藏)

    描述這類的文章有很多,這裡用最簡潔的方式用於記牢:

    1、什麼是過載(overload):

        在同一個作用域下的兩個同名函式,並且它們的引數不同(返回值是否相同可選),這樣的兩個函式叫過載。

        注意理解"在同一個作用域下",至少包括以下:

        a. 在同一個名字空間下面,比如都在名字空間abcde下定義的兩個普通函式,或者兩個全域性函式;

        b. 在同一個型別下的同一個作用域下,比如都在class A下的public/protected/private;

        過載是"靜態聯編(靜態繫結)"的一種。

        插:所謂的"聯編(或者叫繫結)",是指"將一個識別符號號,和一個儲存地址關聯起來"。"聯編"分為兩種,一種是"靜態聯編",指的是編譯時即確定關聯關係,包括:普通函式過載、類成員函式過載、運算子過載、模板過載、隱藏(沒有修飾以virtual的父子類同名成員函式);一種是"動態過載",指的是執行時確定呼叫哪個,包括:覆蓋。"靜態聯編"和"動態聯編"共同組成了c++的"多型"功能。

        如下面的兩個全域性函式:

double overload_sample_0 () {
    std::cout << "overload sample 0_1 in namespace what_is_overload" << std::endl;
    return 1.1;
}
int overload_sample_0 (int a) {
    std::cout << "overload sample 0_2 in namespace what_is_overload" << std::endl;
    return 1;
}

        兩個函式的名稱都是overload_sample_0,作用域都是全域性作用域,引數不一樣,過載;

        再如下面的兩個函式:

namespace what_is_overload {
    void overload_sample_1 () {
        std::cout << "overload sample 1_1 in namespace what_is_overload" << std::endl;
    }
    int overload_sample_1 (int a) {
        std::cout << "overload sample 1_2 in namespace what_is_overload" << std::endl;
        return 0;
    }
};

        兩個函式的名稱都是overload_sample_1,作用域都是名字空間what_is_overload之下,引數不一樣,過載;

        再如下面的函式:

class cls {
    public:
        cls() = default;
        ~cls() = default;
        cls(const cls &other) = default;
        cls(cls &&other) = default;

        void overload_1 (){ std::cout << "overload 1" << std::endl; }
        void overload_1 (int a){ std::cout << "overload 1" << std::endl; }
        //int overload_1 () { std::cout << "overload 1" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_1 (char c) { std::cout << "overload 1" << std::endl; return 0; }

    protected:
        void overload_2 (){ std::cout << "overload 2" << std::endl; } 
        void overload_2 (int a){ std::cout << "overload 2" << std::endl; }
        //int overload_2 () { std::cout << "overload 2" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_2 (char c) { std::cout << "overload 2" << std::endl; return 0; }

    private:
        void overload_3 (){ std::cout << "overload 3" << std::endl; }
        void overload_3 (int a){ std::cout << "overload 3" << std::endl; }
        //int overload_3 () { std::cout << "overload 3" << std::endl; return 0; } // could not be distinguished with the first one
        int overload_3 (char c) { std::cout << "overload 3" << std::endl; return 0; }
    };

        public、protected、private下,都有各自內部的類成員函式的,函式名相同,作用域相同,引數不一樣,過載;

        插:描述過載時,很多文章注重說,函式的名稱一樣,引數、返回值不同,事實上精確的是 ,函式的引數必須不同。試想下面兩個函式:

        int overload(){.....}

        double overload() {.....}

        然後呼叫時:overload();    //which one?

        函式的引數一樣,編譯器怎麼可能區分呼叫時呼叫的是哪一個。

    2、什麼是覆蓋(override):

        父子類中的同名、同參數、同返回值的多個成員函式,從子到父形成的關係稱為覆蓋關係。

        覆蓋關係屬於"動態聯編",即執行時通過確定指標所指向的是哪個物件,決定執行哪個實現,支撐"動態聯編"的是虛擬函式表,關於虛擬函式表:

        a. 什麼是虛擬函式表:對含有"virtual"修飾符的類,編譯器在編譯時,會給該型別製造一個該型別所屬的虛擬函式表;

        b. 虛擬函式表本質上是:一個函式指標表,類的函式名-----地址,這個函式必須用virtual修飾過;

        c. 虛擬函式表在哪:在應用程式的常量區;

        d. 虛擬函式表怎麼作用:執行時,根據指標所對應的物件是哪個型別的物件,從虛擬函式表中找到對應函式名的地址進而執行;

        對解構函式用virtual修飾是典型的覆蓋的運用,當(父類指標指向的)子類物件發生析構時:

        a. 如果父類的解構函式用virtual修飾,那麼析構時,通過"動態聯編"會呼叫子類的解構函式;然後自然析構父類;

        b. 如果父類的解構函式沒有用virtual修飾,那麼析構時,僅僅自然析構父類;這個過程實際就是所謂的隱藏;

        參考下面的程式碼:

//what is inherit? inherit is that father and son has the same function name and the same arg and same return-value
//in c++11, if do want to override, you'd better explicit notice the son function with "override", so compiler will do know that it is inherit, but not another base-virtual-function.
    class father {
    public:
        father() = default;
        //~father() { std::cout << "father destruct" << std::endl; };
        ~father() { std::cout << "father destruct" << std::endl; };
        father(const father &other) = default;
        father(father &&other) = default;

        virtual void inherit () { std::cout << "inherit, father" << std::endl; }
    };
    class son : public father {
    public:
        son() = default;
        //~son() { std::cout << "son destruct" << std::endl; };
        virtual~son() { std::cout << "son destruct" << std::endl; };
        son(const son &other) = default;
        son(son &&other) = default;

//explicit notice compiler that, inherit_c11 here is override of function inherit
//if qualify with "final", class grandson could not override.
//"override" and "final" is used in c++11
        virtual void inherit () override { std::cout << "inherit, son" << std::endl; }
        //virtual void inherit () override final { std::cout << "inherit, son" << std::endl; }
    };

    class grandson : public son {
    public:
        grandson() = default;
        virtual ~grandson() { std::cout << "grandson destruct" << std::endl; };
        grandson(const grandson &other) = default;
        grandson(grandson &&other) = default;

        virtual void inherit () override final {std::cout << "inherit, grandson" << std::endl;  }
    };

        父類、子類、孫類,相繼是public的繼承關係,解構函式均用virtual修飾,下面依次是實驗的方式的知識點的鞏固:

        a. 如果解構函式去除了virtual修飾,那麼嘗試下面的測試,看看差別在哪:

son s

        僅僅在棧中定義了son型別的變數s,析構時,可以發現父類也會被析構;

std::shared_ptr<father> p((father *)new son());
        用father指標,指向在堆中建立的son物件,析構時,可以發現僅僅析構了父類物件,即發生記憶體洩漏;隱藏;
std::shared_ptr<son> pfs((son *)new father());

        用son指標,指向在堆中建立的father物件,這時程式會崩潰;如果加virtual修飾,會正常析構父類物件;

        結論:

        1、父子類的解構函式,要加入virtual修飾;

        2、不要用子類指標,指向父物件;

              用父類指標指向子類物件,實際開發中是具備實際意義的,是動態實現多型的使用呈現。

              而反過來是令人奇怪的,因為子類已經繼承了父類的方法,為什麼要用子類指標指向父類物件。

        對於普通函式的覆蓋,和解構函式是一樣道理。注意:

        1、建構函式不可以進行覆蓋;

        2、運算子過載屬於過載,不可以進行覆蓋;

        3、要保證覆蓋的函式,函式引數、返回值均相同;

        一定要加入virtual修飾符,但有時可能會忘記,如果忘記則可能會出現隱藏:

        1、父類有成員函式function;沒有用virtual修飾;

        2、子類有成員函式function,函式名、引數、 返回值均相同;

        那麼如下面的例子,會出現問題:

std::shared_ptr<father> psf((father *)new son());

        這時如果執行psf->function(),不會在執行時"動態聯編",而是出現如下情況:那麼指標是什麼型別,就執行哪個型別的function;或者說,override退化成hide;隱藏(hide)一般屬於程式開發bug;

        為了防止忘寫virtual導致的隱藏,c++11規定可以在程式中,對希望是覆蓋的成員函式,顯式的修飾以override,這樣如果沒寫virtual會在編譯時報錯,避免忘修飾virtual;

        c++11另外還規定了修飾符final,對於不想再被子類覆蓋的虛擬函式,加入final修飾符,它的子類即便想覆蓋也無妨再覆蓋了,或者說,覆蓋行為到它這裡截止;

    3、什麼是隱藏(hide):

        知道了什麼是覆蓋,那麼忘記修飾virtual導致的"覆蓋失誤"就是隱藏。隱藏的特點是:不管指標實際指向的物件是什麼樣的型別,完全根據指標型別,執行對應的成員函式。

        隱藏導致編譯器完全沒有建立虛擬函式表(因為沒有virtual修飾符),所以完全走的是"靜態聯編",即按指標型別來。隱藏的危害發生在,原本希望是覆蓋,結果發現和預期完全不符。

        下面是隱藏的一些例子:

TEST (test1, what_is_hide) {
//hide: father class and son class, has the function with same name, but without qualify with 'virtual', type of pointer would determine which would be call
    class father {
    public:
        father() = default;
        ~father() = default;
        father(const father &other) = default;
        father(father &&other) = default;

//for hide, whether return-value is also same, is not necessary.so hide_0 equals hide_1
        void hide_0 () { std::cout << "hide_0, father" << std::endl; }
        void hide_1 () { std::cout << "hide_1, father" << std::endl; }
        void hide_2 (int a) { std::cout << "hide_2, father" << std::endl; }
    };

    class son : public father {
    public:
        son() = default;
        ~son() = default;
        son(const son &other) = default;
        son(son &&other) = default;

        void hide_0 () { std::cout << "hide_0, son" << std::endl; }
        int hide_1 () { std::cout << "hide_1, son" << std::endl; }
        void hide_2 () { std::cout << "hide_2, son" << std::endl; }
    };

//when hide, type of pointer determine which would be call
//hide explains that why destruct-function without virtual, would memory leak. destruct-function without virtual, would determined by type of pointer, but not type of object
    father f;
    son s;
    f.hide_0(); //father
    f.hide_1(); //father
    f.hide_2(1);//father
    s.hide_0(); //son
    s.hide_1(); //son
    s.hide_2(); //son

    std::shared_ptr<son> ps(new son());
    ps->hide_0();   //son
    ps->hide_1();   //son
    ps->hide_2();   //son

    std::shared_ptr<son> psf((son *)new father());
    psf->hide_0();  //son
    psf->hide_1();  //son
    psf->hide_2();  //son

    std::shared_ptr<father> pf(new son());
    pf->hide_0();   //father
    pf->hide_1();   //father
    pf->hide_2(1);   //father

    std::shared_ptr<father> pfs((father *)new son());
    pfs->hide_0();  //father
    pfs->hide_1();  //father
    pfs->hide_2(2);  //father
}

        c++多型的總結:

        1、"靜態聯編":

            1、同一作用域(相同名字空間,型別下相同作用域(public/protected/private))的普通/成員函式,函式名相同,引數不相同;

            2、運算子過載;

            3、編譯時確定模板實現類(模板過載);

            如:template<class T> class A{ T data}; A<int> a;

            4、隱藏;隱藏是程式開發的bug應該加以避免;

        2、"動態聯編":

            指的就是覆蓋,執行時由虛擬函式表根據所處物件的型別,確定執行哪一個函式;

            良好的程式應該使用覆蓋;

        3、如何避免覆蓋變成隱藏:

            a. 不要忘了用virtual修飾;

            b. 同時使用override修飾,忘了用virtual時編譯器可以報錯;