目錄
- 1、多態概念
- 1.多態性有兩種表現的方式
- 2、聯編(實現多態)
- 1.靜態聯編
- 2.動態聯編
- 3、實現運行時多態
- 1.為何要使用運行時多態?
- 2.如何實現運行時多態
- 3.多態的例子
- 1.調用哪個同名虛函數?
- 2. 用途:可以用父類指針訪問子類對象成員
- 4. 虛函數的傳遞性
- 虛函數的缺點:
- 4、代碼示例
- 1、繼承與重載函數的例子
- 任務1、2
- 任務3
- 2、使用運行時多態
- 3、override覆寫
- 6、運行時多態的總結
- 1.Summary: static binding v.s. dynamic binding
- 2. Summary: 靜態聯編的簡單示例
- 3. Summary: 通過基類指針訪問同名函數的示例
- 4. Summary:動態聯編的簡單示例-指針形式
- 5. Summary:動態聯編的簡單示例-引用形式
- 7、C++11:使用override和final
- 1. override顯式聲明覆寫
- 2. final 顯式聲明禁止覆寫
1、多態概念
廣義的多態:不同類型的實體/對象對于同一消息(可以理解為同一個名字的函數)有不同的響應,就是OOP中的多態性。
1.多態性有兩種表現的方式
1. 重載多態:調用參數不同的同名函數,表現出不同的行為
class C {
public: int f(int x);int f( );
};
2.子類型多態:不同的對象調用同名重定義函數,表現出不同的行為
class A { virtual int f() {return 1;} };
class B: public A { virtual int f() {return 8;} };
A a; B b;
A* p = &b; //p雖然是a類型,但是它指向B類型的對象
a.f() // call A::f()
b.f() // call B::f()
p->f(); // call B::f()
//a和p是同類型,但是調用同名函數的響應是不一樣的。
2、聯編(實現多態)
確定具有多態性的語句調用哪個函數的過程稱為聯編。
1.靜態聯編
靜態聯編在程序編譯時(Compile-time)確定調用哪個函數。
如:函數重載
2.動態聯編
在程序運行時(Run-time),才能夠確定調用哪個函數
用動態聯編實現的多態,也稱為運行時多態(Run-time Polymorphism)。
3、實現運行時多態
1.為何要使用運行時多態?
我們有三個類ABC,繼承鏈如下:
A<-B<-C;
我們想使用print()調用toString()輸出信息,就需要寫三個重載函數:
void print(A obj);
void print(B obj);
void print(C obj);
這樣比較麻煩。
我們可以使用運行時多態解決這個問題:
2.如何實現運行時多態
實現運行時多態有兩個要素:
(1) virtual function (虛函數)
(2) Override (覆寫) : redefining a virtual function in a derived class. (在派生類中重定義一個虛函數)
在正常的函數名字前加上virtual關鍵字,這個函數就會變為虛函數:
struct A{virtual std::string toString(){return "A";}
};
覆寫是在派生類中定義一個與基類虛函數同名,參數一樣,返回值一樣的函數。
注意這里我們傳遞的是ABC類型的指針:
3.多態的例子
1.調用哪個同名虛函數?
(1) 不由指針類型決定;
(2) 而由指針所指的【實際對象】的類型決定
(3) 運行時,檢查指針所指對象類型
2. 用途:可以用父類指針訪問子類對象成員
注意print函數的參數是基類類型的指針。
然后將ABC類型的對象的地址作為函數參數傳入。
后面兩個print函數指針會做隱式類型轉換。將派生類的地址轉化為基類類型的指針,這個轉換是安全的。
返回 的結果是不一樣的,是和對象的實際情況相關的。
4. 虛函數的傳遞性
基類定義了虛同名函數,那么派生類中的同名函數自動變為虛函數。
注意:只需要在繼承連上最頂端的基類上加上關鍵字即可:
虛函數的缺點:
1、類中保存著一個Virtual function table (虛函數表)。
2、運行時聯編/動態聯編,會有額外的邏輯。
所以調用虛函數比非虛函數開銷大
4、代碼示例
1、繼承與重載函數的例子
任務1、2
1、創建A/B/C三個類,B繼承A,C繼承B,ABC均有toString函數
2、創建print函數,接受A類型的參數,調用A對象的toString()
不管傳進來什么類型,都是打印基類類型的數據
//本部分要展示的內容如下:
//1、創建A/B/C三個類,B繼承A,C繼承B,ABC均有toString函數
//2、創建print函數,接受A類型的參數,調用A對象的toString()
//3、重載print函數,接受B/C類型參數,調用toStirng()#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A a) {cout << a.toString() << endl;
}
int main()
{A a;B b;C c;print(a);print(b);print(c);
}
任務3
重載print函數,接受B/C類型參數,調用toStirng()
//本部分要展示的內容如下:
//1、創建A/B/C三個類,B繼承A,C繼承B,ABC均有toString函數
//2、創建print函數,接受A類型的參數,調用A對象的toString()
//3、重載print函數,接受B/C類型參數,調用toStirng()#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A a) {cout << a.toString() << endl;
}
void print(B b) {cout << b.toString() << endl;
}
void print(C c) {cout << c.toString() << endl;
}
int main()
{A a;B b;C c;print(a);print(b);print(c);
}
通過三個重載函數,將打印的數據與類型匹配。
2、使用運行時多態
本部分要展示的內容如下:
1、將基類A的toStirng函數改為虛函數
2、將print函數參數改為基類指針類型
main()中調用print(),實參為指向對象的基類指針
3、添加一個print函數,參數是基類引用類型
在main()中調用print(),參數為對象的基類引用
//展示運行時多態的實現
#include<iostream>
#include<string>
using std::cout;
using std::endl;
class A {
public:virtual std::string toString() { return "A"; }
};
class B : public A {
public:std::string toString() { return "B"; }
};
class C : public B {
public:std::string toString() { return "C"; }
};void print(A* a) {cout << a->toString() << endl;
}
void print(A& a) {cout << a.toString() << endl;
}
int main()
{A a;B b;C c;A* p1 = &a;A* p2 = &b;A* p3 = &c;print(p1);print(p2);print(p3);print(a);print(b);print(c);
}
可以發現參數是基類引用類型與參數是基類引用類型實現的效果是一樣的。這些就是多態的具體展示。
注意如果在print函數中沒有找到該對象的同名函數,那么就會順著繼承鏈,直到在基類中找到這個同名函數。
也就是說,在繼承鏈中如果某個類的虛函數名字寫錯了就會出現許多BUG。
3、override覆寫
為了解決上面的問題,c++提供了覆寫操作。這在第7大點將詳細講述:
表明了對基類函數的覆寫,如果函數名字或者參數或者返回值不一樣,編譯器就會報錯,提醒程序員。
6、運行時多態的總結
1.Summary: static binding v.s. dynamic binding
基類與派生類中有同名函數
(1) 通過派生類對象訪問同名函數,是靜態聯編
(2) 通過基類對象的指針訪問同名函數,是靜態聯編
(3) 通過基類對象的指針或引用訪問同名虛函數,是動態聯編
2. Summary: 靜態聯編的簡單示例
class P { public: f(){…} }; //父類
class C: public P { public: f(){…} }; //子類
main () {P p; C c;p.f(); //調用P::f()c.f(); //調用C::f()
}
說明: 對象是什么類型,就調什么類型
3. Summary: 通過基類指針訪問同名函數的示例
class P { public: f(){…} }; //父類
class C: public P { public: f(){…} }; //子類
main () {P* ptr; P p; C c;ptr = &p;ptr->f(); // 調用P::f()ptr=&c;ptr->f(); // 調用P::f()
}
說明: 指針是什么類型,就調什么類型
4. Summary:動態聯編的簡單示例-指針形式
class P { public: virtual f(){…} }; //父類
class C: public P { public: f(){…} }; //子類,f自動virtual
main () {P* ptr; P p; C c;ptr = &p;ptr->f(); //調用P::f()ptr=&c;ptr->f(); //調用C::f()
}
說明: 函數虛,不看指針看真對象
5. Summary:動態聯編的簡單示例-引用形式
class P { public: virtual f(){…} }; //父類
class C:public P { public: f(){…} }; //子類,f自動virtual
main () {P p; C c;P& pr1 = p;pr1.f(); //調用P::f()P& pr2 = c;pr2.f(); //調用C::f()
}
說明: 函數虛,不看引用看真對象
7、C++11:使用override和final
1. override顯式聲明覆寫
C++11引入override標識符,指定一個虛函數覆寫另一個虛函數。
class A {
public:virtual void foo() {}void bar() {}
};
class B : public A {
public://此處foo為常函數,與基類的foo函數不是同名覆寫函數void foo() const override { // 錯誤: B::foo 不覆寫 A::foo} // (簽名不匹配)void foo() override; // OK : B::foo 覆寫 A::foovoid bar() override {} // 錯誤: A::bar 非虛
};
void B::foo() override {// 錯誤: override只能放到類內使用
}
override的價值在于:避免程序員在覆寫時錯命名或無虛函數導致隱藏bug
summary:
1、override標識符應該寫到派生類同名虛函數的后面
2、非虛函數不能覆寫
3、覆寫只能在類的內部使用
2. final 顯式聲明禁止覆寫
C++11引入final特殊標識符,指定派生類不能覆寫虛函數。
struct Base {virtual void foo();
};struct A : Base
{ void foo() final; // A::foo 被覆寫且是最終覆寫void bar() final; // 錯誤:非虛函數不能被覆寫或是 final
};
struct B final : A // struct B 為 final,不能被繼承
{void foo() override; // 錯誤: foo 不能被覆寫,因為它在 A 中是 final
};
final標識符的作用: