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時編譯器可以報錯;