polymorphism 靜態聯編和動態聯編 多態性(polymorphism)提供介面與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程式具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。 c++ ...
polymorphism
靜態聯編和動態聯編
多態性(polymorphism)提供介面與具體實現之間的另一層隔離,從而將”what”和”how”分離開來。多態性改善了代碼的可讀性和組織性,同時也使創建的程式具有可擴展性,項目不僅在最初創建時期可以擴展,而且當項目在需要有新的功能時也能擴展。
c++支持編譯時多態(靜態多態)和運行時多態(動態多態),運算符重載和函數重載就是編譯時多態,而派生類和虛函數實現運行時多態。
靜態多態和動態多態的區別就是函數地址是早綁定(靜態聯編)還是晚綁定(動態聯編)。如果函數的調用,在編譯階段就可以確定函數的調用地址,並產生代碼,就是靜態多態(編譯時多態),就是說地址是早綁定的。而如果函數的調用地址不能編譯不能在編譯期間確定,而需要在運行時才能決定,這這就屬於晚綁定(動態多態,運行時多態)。
將源代碼中的函數調用解釋為執行特定的函數代碼塊被稱為函數名聯編。編譯器必須查看函數參數以及函數名才能確定使用哪個函數。
指針和引用類型的相容性以及向上類型轉換
在C++裡面動態聯編與通過指針和引用的調用方法有關。通常c++不允許將一種一類的地址賦給另一種類型的指針,也不允許一種類型的引用指向另一種類型。
double x = 2.5;
int * pi = &x; //類型不對不能這樣定義
long & r1 = x; //問題同上
對象可以作為自己的類或者作為它的基類的對象來使用。還能通過基類的地址來操作它。取一個對象的地址(指針或引用),並將其作為基類的地址來處理,這種稱為向上類型轉換。
父類引用或指針可以指向子類對象,通過父類指針或引用來操作子類對象。
就是指將子類對象的引用賦給父類類型的引用變數的過程。在面向對象編程中,這種類型轉換是安全的,因為子類對象可以被當做父類對象來對待。通過向上類型轉換,可以實現多態性,即一個父類引用變數可以引用不同子類對象,並根據實際對象類型調用相應的方法。
class Base {
public :
virtual void func() {
cout << "class Base" << endl;
}
};
class Derive_1 : public Base {
public:
void func() {
cout << "class Derive_1" << endl;
}
};
class Derive_2 : public Base {
public:
void func() {
cout << "class Derive_2" << endl;
}
};
void test() {
Derive_1 d1;
Derive_2 d2;
//向上類型轉換
Base* b1 = &d1;
Base* b2 = &d2;
//通過父類引用變數調用子類方法
b1->func();
b2->func();
}
雖然父類調用func函數,但是父類的指針全部指向了子類的引用,並且可以完成隱式類型轉換。再如下麵這個代碼
class Base {
public :
void func() {
cout << "class Base" << endl;
}
};
class Derive_1 : public Base {
public:
void func() {
cout << "class Derive_1" << endl;
}
};
void GetQuestion(Base& b) {
b.func();
}
void test() {
Derive_1 d1;
GetQuestion(d1);
}
參數定的是基類的引用,但是傳參傳的是子類,最後調用的依然是基類的方法。這個地方就引出了一個叫做捆綁
的概念。把函數體與函數調用相聯繫稱為綁定(捆綁,binding)
當綁定在程式運行之前(由編譯器和連接器)完成時,稱為早綁定(early binding).C語言中只有一種函數調用方式,就是早綁定。上面的問題就是由於早綁定引起的,因為編譯器在只有Base地址時並不知道要調用的正確函數。編譯是根據指向對象的指針或引用的類型來選擇函數調用。
在代碼里,GetQuestion函數的參數類型是Base&,編譯器確定了應該調用的func函數是Base::func(),並不是傳入的d1.解決方法就是遲綁定(遲捆綁,動態綁定,運行時綁定,late binding),意味著綁定要根據對象的實際類型,發生在運行。C++語言要實現這種動態綁定,必須有某種機制來確定運行時對象的類型並調用合適的成員函數。對於一種編譯語言,編譯器並不知道實際的對象類型(編譯器並不知道Animal類型的指針或引用指向的實際的對象類型)。
虛函數
其實在上面的代碼里也能發現區別就是基類的函數是不是虛函數決定了這個綁定發生在什麼時候。如果沒有定義虛的,b.func();將根據引用類型調用函數,編譯時已知類型之後,對於非虛方法就是用的是靜態聯編。
-
為創建一個需要動態綁定的虛成員函數,可以簡單在這個函數聲明前面加上virtual關鍵字,定義時候不需要.
-
如果一個函數在基類中被聲明為virtual,那麼在所有派生類中它都是virtual的.
-
在派生類中virtual函數的重定義稱為重寫(override).
-
Virtual關鍵字只能修飾成員函數.
-
構造函數不能為虛函數
僅需要在基類中聲明一個函數為virtual.調用所有匹配基類聲明行為的派生類函數都將使用虛機制。雖然可以在派生類聲明前使用關鍵字virtual(這也是無害的),但這個樣會使得程式顯得冗餘和雜亂。
用《C++Primers》裡面的一個圖解釋一下虛函數的工作原理。通常編譯器處理虛函數的方法是:給每個對象添加一個隱藏成員
,這個隱藏成員中保存了一個指向函數地址數組的指針(圖裡的vptr),而這種地址數組就叫做虛函數表(vtbl)。虛函數表裡存儲了為類對象進行聲明的虛函數的地址。比如基類對象Base包含一個指針,該指針指向基類中所有虛函數的地址表,派生類對象將包含一個指向獨立地址表的指針。如果派生類提供了虛函數的新定義,該虛函數表將保存新的地址;相反,該虛函數表將保存原始版本的地址。
調用虛函數時,程式將查看存儲在對象里的vtbl地址,然後轉向相應的函數地址表,如果使用類聲明中的第一個虛函數,則程式將使用數組中的第一個函數地址,並執行具有該地址的函數。總之使用虛函數時,在記憶體和執行速度方面有一定成本:
- 每個對象都會被增大其存儲空間(和虛基類一樣)
- 每個類編譯器都會有一個虛函數地址表
- 每個函數的調用都需要執行一項額外的操作就是到表裡查找。
實現動態綁定細節過程
當子類無重寫基類虛函數時:
子類完全繼承基類的函數,他們擁有各自的虛函數表
當程式執行到這裡,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,此時由於子類並沒有重寫也就是覆蓋基類的func1函數,所以調用func1時,仍然調用的是基類的func1.
當子類重寫基類虛函數時
子類重寫了基類的func1,但是沒有寫func2,所以對應的地址表應該是
當程式執行到這裡,會去animal指向的空間中尋找vptr指針,通過vptr指針找到func1函數,由於子類重寫基類的func1函數,所以調用func1時,調用的是子類的func1.
抽象基類和純虛函數
在設計時,常常希望基類僅僅作為其派生類的一個介面。這就是說,僅想對基類進行向上類型轉換,使用它的介面,而不希望用戶實際的創建一個基類的對象。同時創建一個純虛函數允許介面中放置成員原函數,而不一定要提供一段可能對這個函數毫無意義的代碼。
做到這點,可以在基類中加入至少一個純虛函數(pure virtual function),使得基類稱為抽象類(abstract class).
-
純虛函數使用關鍵字virtual,併在其後面加上=0。如果試圖去實例化一個抽象類,編譯器則會阻止這種操作。
-
當繼承一個抽象類的時候,必須實現所有的純虛函數,否則由抽象類派生的類也是一個抽象類。
-
Virtual void fun() = 0;告訴編譯器在vtable中為函數保留一個位置,但在這個特定位置不放地址。
建立公共介面目的是為了將子類公共的操作抽象出來,可以通過一個公共介面來操縱一組類,且這個公共介面不需要事先(或者不需要完全實現)。可以創建一個公共類.
class AbstractClass {
public:
virtual void sleep() = 0;
virtual void dolove() = 0;
virtual void cook() = 0;
void func() {
cook();
dolove();
sleep();
}
};
class Regina : public AbstractClass {
public:
virtual void sleep() {
cout << "Regina::sleep()" << endl;
}
virtual void dolove() {
cout << "Regina::dolove()" << endl;
}
virtual void cook() {
cout << "Regina::cook()" << endl;
}
};
class Ivanlee : public AbstractClass {
public:
virtual void sleep() {
cout << "Ivanlee::sleep()" << endl;
}
virtual void dolove() {
cout << "Ivanlee::dolove()" << endl;
}
virtual void cook() {
cout << "Ivanlee::cook()" << endl;
}
};
void home(AbstractClass* a) {
a->func();
delete a;
}
void home(AbstractClass& a) {
a.func();
}
void test() {
home(new Regina);
Ivanlee ivan;
home(ivan);
}
純虛函數和虛函數是 C++ 中的重要概念,它們都與多態性(polymorphism)和繼承相關。它們之間的主要區別在於以下幾點:
- 虛函數:
- 虛函數是在基類中聲明為虛函數的
成員函數
,它可以在派生類中被重寫(覆蓋)。 - 虛函數可以有預設的實現,如果派生類沒有重寫虛函數,則會調用基類的實現。
- 虛函數通過基類指針或引用調用時,可以根據指針或引用所指向的對象的實際類型來動態地決定調用哪個版本的函數(動態聯編)。
- 虛函數是在基類中聲明為虛函數的
- 純虛函數:
- 純虛函數是在基類中聲明並且沒有給出實現的虛函數,
它只是一個介面
,要求任何派生類都必須提供實現。 - 在 C++ 中,通過在虛函數聲明後面加上
= 0
來將其聲明為純虛函數,例如:virtual void myFunction() = 0;
。 - 含有純虛函數的類稱為抽象類,無法實例化對象,只能作為基類來派生出其他類。派生類必須提供純虛函數的實現,否則它們也會變成抽象類。
- 純虛函數是在基類中聲明並且沒有給出實現的虛函數,
虛析構函數
虛析構函數是為瞭解決基類的指針指向派生類對象,並用基類的指針刪除派生類對象。當通過基類指針刪除指向派生類對象的實例時,如果析構函數不是虛函數,那麼只會調用基類的析構函數,而不會調用派生類的析構函數,這可能導致派生類資源得不到正確釋放,從而產生記憶體泄漏或未定義的行為。
class Base {
public:
virtual ~Base() {
// 虛析構函數
}
};
class Derived : public Base {
public:
~Derived() {
// 派生類的析構函數
}
};
int main() {
Base* ptr = new Derived();
delete ptr; // 通過基類指針刪除派生類對象
return 0;
}
重寫 重載 重定義
class Shape {
public:
virtual double calculateArea() {
return 0.0;
}
};
-
重寫(Override):
-
重寫是指派生類重新定義(覆蓋)基類中已經存在的虛函數的行為。
-
當派生類定義一個與基類中的虛函數具有相同名稱和簽名的函數時,它就會覆蓋(重寫)基類中的虛函數。
-
通過使用重寫,可以在派生類中改變虛函數的行為,實現多態性,即在運行時根據對象的實際類型來確定調用哪個版本的函數。
class Rectangle : public Shape { public: double calculateArea() override { // 重寫基類的虛函數 return width * height; } private: double width, height; };
-
-
重載(Overload):
-
重載是指在同一個作用域內允許存在多個同名函數,但它們的參數列表不同(參數類型、參數個數或參數順序不同)。
-
重載函數可以具有相同的名稱,但是由於參數列表不同,編譯器可以根據調用時提供的參數類型來確定應該調用哪個版本的函數。
class Shape { public: virtual double calculateArea() { return 0.0; } double calculateArea(int a, int b) { // 重載的函數 return a * b; } };
-
-
重新定義(Redefine):
-
重新定義通常用於描述對於非虛函數的重新定義。在基類和派生類中,如果存在同名但參數列表不同的函數,這種情況稱為函數的重新定義。
-
在重新定義中,基類和派生類中的函數並不構成多態性,調用哪個版本的函數取決於編譯器能夠靜態確定的最匹配的函數。
class Circle : public Shape { public: void draw(int radius) { // 派生類中重新定義的函數 cout << "Drawing a circle with radius " << radius << endl; } };
-
本文來自博客園,作者:ivanlee717,轉載請註明原文鏈接:https://www.cnblogs.com/ivanlee717/p/18052060