c++中的類(class)-----筆記(類多態)


1,多態是一種運行期綁定機制,通過這種機制,實現將函數名綁定到函數具體實現代碼的目的。一個函數的名稱與其入口地址是緊密相連的,入口地址是該函數在內存中的起始地址。如果對一個函數的綁定發生在運行時刻而非編譯時刻,我們就稱該函數是多態的。

 

2,C++多態的三個前提條件:(a)必須存在一個繼承體系結構;(b)繼承體系結構中的一些類必須具有同名的 virtual 成員函數(virtual 是關鍵字);(c)至少有一個基類類型的指針或者基類類型的引用可用來對 virtual 成員函數進行調用。

 1 #include<iostream>
 2 #include<string>
 3 using namespace std;
 4 
 5 class TradesPerson {
 6     public:
 7         virtual void sayHi() {
 8             cout<<"Just hi."<<endl;
 9         }
10 }; 
11 
12 class Tinker : public TradesPerson {
13     public:
14         virtual void sayHi() {
15             cout<<"Hi,I tinker."<<endl;
16         }
17 };
18 
19 class Tailor : public TradesPerson {
20     public:
21         virtual void sayHi() {
22             cout<<"Hi,I Tailor."<<endl;
23         }
24 };
25 
26 int main() {
27     TradesPerson *p;
28     int which;
29     
30     do {
31         cout<<"1 == TradesPerson, 2 == Tinker, 3 == Tailor"<<endl;
32         cin>>which;
33     }while(which<1 || which > 3);
34     
35     switch(which) {
36         case 1 : p = new TradesPerson; break;
37         case 2 : p = new Tinker; break;
38         case 3 : p = new Tailor; break;
39     }
40     p->sayHi();
41     delete p;
42     
43     return 0;
44 }
多態使用示例

 

3,因為基類類型的指針可以指向任何基類對象或派生類對象,所以上面程序我們並不需要強制類型轉換。在上面的代碼中,每個函數都使用了關鍵字 virtual ,但實際上並沒有必要,因為當聲明了基類的一個成員函數為虛函數后,那么即使該成員函數沒有在派生類中被顯式地聲明為虛函數,但它在所有派生類中也將自動成為虛函數。在上面代碼中,只是 TradesPerson 中聲明為虛函數也是可以的,因為 Tinker::sayHi 仍然是虛函數,因為它與 TradesPerson::sayHi 具有相同的函數簽名,而 Tradesperson::sayHi 是在基類中聲明的虛函數。

 

4,如果虛函數在類聲明之外定義,關鍵字 virtual 僅在函數聲明時需要,不需在函數定義中使用 virtual 關鍵字。C++ 僅允許將成員函數定義為虛函數,頂層函數不能為虛函數。

 

5.1,C++ 使用 vtable (虛成員函數表)來實現虛成員函數的運行期綁定。虛成員函數表存在的用途是支持運行時查詢,使得系統可以將某一函數名綁定到虛成員函數表中的特定入口地址。虛成員函數表的實現是與系統無關的。

 1 class B {
 2     public:
 3         virtual void m1() { /*    */}
 4         virtual void m2() { /*    */}
 5 };
 6 
 7 class D : public B {
 8     public:
 9         virtual void m1() { /*    */}   // override m1
10 };
vtable 示例
虛成員函數 入口地址示例 虛成員函數 入口地址示例
   B::m1     0x7723     D::m1     0x99a7
   B::m2     0x23b4     D::m2     0x23b4

 

 

 

 

 在虛成員函數表中,對應於程序中的每一個虛成員函數,都有一個單獨的入口地址,我們發現 B::m2 和 D::m2 有相同的入口地址(0x23b4),這是因為派生類 D 沒有覆蓋成員函數 m2,而是直接繼承了其基類的 B。如果執行以下代碼:

1 int main() {
2     B b;
3     D d;
4     B* p;
5     //...        // p is set to b's or d's address
6     p->m1();     // vtable lookup for run-time binding
7     //...
8 }
vtable 使用示例

上述代碼中,p->m1(); 綁定到虛成員函數表中的某一項,系統首先決定指針 p 指向哪個對象,如果 p 指向 B 的對象 b,系統將在虛成員函數表中查詢 B::m1 的表項;如果指向 d,系統將在虛成員函數表中查詢 D::m1 的入口地址,一旦查詢完成,就可以執行相應的函數體。

note: 使用動態綁定的程序會影響效率,因為虛成員函數需要額外的存儲空間,而且對虛成員函數表進行查詢也需要額外的時間。

5.2,對象中的vptr指針什么時候初始化?

 1 #include <iostream>
 2 
 3 using namespace std;
 4 
 5 class Parent{
 6 public:
 7     Parent(int a=0){
 8         this->a = a;
 9         print();
10     }
11     virtual void print(){ cout << "Parent" <<endl;}
12 private:
13     int a;
14 };
15 
16 class Son:public Parent{
17 public:
18     Son(int a=0,int b=0):Parent(a){
19         this->b = b;
20         print();
21     }
22     virtual void print(){ cout << "Son" <<endl;}
23 private:
24     int b;
25 };
26 
27 int main(){
28     Son s;
29     return 0;
30 }
31 
32 // 輸出結果
33 // Parent
34 // Son
虛表指針初始化過程

結論:Son s 的過程:

1)初始化 s.vptr 指針,初始化時分步;

2)  當執行父類的構造函數時,s.vptr 指向了父類的虛函數表,當父類構造函數運行完畢后,會把 s.vptr 指針指向子類的虛函數表

3)結論:子類的 s.vptr 指針初始化分步完成

 

6,構造函數和析構函數:構造函數不能是虛成員函數,但析構函數可以是虛成員函數。

7,虛析構函數:舉一個例子來說明虛析構函數的重要性:

 1 #include<iostream>
 2 #include<string>
 3 using namespace std;
 4 
 5 class A {
 6     public:
 7         A() {
 8             cout<<endl<<"A() firing"<<endl;
 9             p = new char[5];             // allocate 5 bytes
10         }
11         ~A() {
12             cout<<"~A() firing"<<endl;
13             delete[] p;
14         }
15     private:
16         char* p;
17 };
18 
19 class Z : public A {
20     public:
21         Z() {
22             cout<<"Z() firing"<<endl;
23             q = new char[500];        // allocate 500 bytes
24         }
25         ~Z() {
26             cout<<"Z() firing"<<endl;
27             delete[] q;
28         }
29     private:
30         char* q; 
31 };
32 
33 void f() {
34     A* ptr;
35     ptr = new Z();
36     delete ptr;
37 }
38 
39 int main() {
40     for(int i=0;i<3;++i)
41         f();
42     return 0;
43 }
44 
45 /* output
46 
47 A() firing
48 Z() firing
49 ~A() firing
50 
51 A() firing
52 Z() firing
53 ~A() firing
54 
55 A() firing
56 Z() firing
57 ~A() firing
58 */
內存泄漏示例

在上述代碼中,當我們通過 ptr 進行 delete 操作時,盡管 ptr 實際指向一個 Z 對象,但只有 ~A() 被調用,這是因為它們的析構函數不是虛成員函數,所以編譯器實施的是靜態綁定。編譯器根據ptr 的數據類型 A* 來決定調用哪一個析構函數,因此,僅調用了~A(),而沒有調用 ~Z(),這樣就會造成內存泄漏。通過定義基類的析構函數 ~A() 為虛成員函數可以確保其派生類的析構函數也為虛成員函數。當通過 ptr 來刪除其所指的對象時,編譯器進行的是運行期綁定。在這里,因為 ptr 指向一個 Z 類型的對象,所以 ~Z() 被調用,接着 ~A() 也被調用,這是因為析構函數的調用是沿着繼承樹自下向上延伸的。

note: 通常來說,如果基類有一個指向動態分配內存的數據成員,並定義了負責釋放這塊內存的析構函數,就應該將這個析構函數聲明為虛成員函數,這樣做可以保證在以后添加該類的派生類時發揮多態性的作用。

 

8,對象成員函數和類成員函數:只有非靜態成員函數才可以是虛成員函數,換句話說,只有對象成員函數才可以是虛成員函數。

1 class C {
2     public:
3         static virtual void f();  // ERROR: static and virtual
4         static void g();   // OK
5         virtual h();      // OK
6 };
錯誤示例

試圖使一個成員函數既定義為虛成員函數又為靜態函數,這是不允許的。

 

9,重載:在一個類中,成員函數可以有相同的函數名,只要它們的函數簽名不同即可,我們將這種情況成為重載。重載與編譯期綁定相對應,不管是成員函數還是頂層函數。編譯器依據函數簽名來進行綁定。在進行重載時,總是使用編譯器綁定,在這個方面重載函數(不管是成員函數還是頂層函數)和虛函數是截然不同的,虛函數總是在運行期綁定。

 1 class C {
 2     C() { /*  */ }
 3     C( int x ) { /*   */}
 4 };
 5 
 6 void f(double d) { /*  */}
 7 void f(char c) { /*   */}
 8 
 9 int main() {
10     C c1;
11     C c2(26);
12     f( 3.14 );
13     f('z');
14     //...
15 }
重載函數使用示例

 

10,覆蓋:假定基類 B 有一個成員函數 m,其派生類 D 也有一個具有相同函數簽名的成員函數 m,如果這個成員函數是虛函數,則任何通過指針或引用對 m 的調用都會激活運行期綁定。對這種情況,我們稱派生類的成員函數 D::m 覆蓋了其基類的成員函數 B::m。如果成員函數不是虛函數,都 m 的任何調用均為編譯器綁定。

 1 class B {
 2     public:
 3         void m() { cout<<"B::m"<<endl;}
 4 };
 5 
 6 class D : class B {
 7     public:
 8         void m() { cout<<"D::m"<<endl;}
 9 };
10 
11 int main() {
12     B* p;
13     p = new D;
14     p->m();   // invoke m
15     return 0;
16 }
覆蓋示例

上述代碼中,因為 m 不是虛函數,而在 C++ 中只有虛函數才會進行運行期綁定。編譯器會使用 p 的數據類型 B* 進行綁定,結果是綁定到 B::m 。上述調用相當於 p->B::m。

 

11,遮蔽:假定基類擁有一個非虛函數 m ,其派生類 D 也有一個成員函數m,我們就說函數 D:m 遮蔽了繼承而來的函數 B::m。如果派生類的同名成員函數與其基類的這個成員函數有不同的函數簽名,那么這種遮蔽情況會相當復雜。

 1 class A {
 2     public:
 3         void m( int x ) { cout<<x<<endl; }
 4 };
 5 
 6 class C : public A {
 7     public:
 8         void m() { cout<< "Hi"<<endl; }
 9 };
10 
11 int main() {
12     C c1;
13     c1.m();
14 //    c1.m(26);  // [Error] no matching function for call to 'C::m(int)'
15     
16     return 0;
17 }
遮蔽示例

上面的程序將產生一條嚴重編譯錯誤,因為 D 又定義了一個同名函數,因此 D 的本地函數 D::m 遮蔽了繼承而來的函數 B::m 。要調用這個繼承而來的帶單個參數的基類函數 B::m,必須修改成以下格式:c.A::m(26);  

note:虛函數和非虛函數都有可能產生名字遮蔽,實際上一旦派生類的虛函數不能覆蓋基類的虛函數,就會產生虛函數遮蔽。將函數定義為虛函數並不能消除遮蔽現象,改正的辦法就是顯式調用 B::m;這樣做雖然消除了編譯錯誤,但不是好的編譯風格,為了發揮多態的作用,B::m 和 D::m 應該具有相同的函數簽名,而不僅是具有相同的函數名。

 1 class B {
 2     public:
 3         virtual void m(int x) { cout<<x<<endl;}
 4 };
 5 
 6 class D : public B {
 7     public:
 8         virtual void m() { cout<<"Hi"<<endl;}
 9 };
10 
11 int main() {
12     D d1;
13     d1.m();
14 //    d1.m(26);    //  Error: D's m takes no arguments
15     
16     return 0;
17 }
虛函數遮蔽示例

 

12,名字共享:(a)頂層函數重載;(b)構造函數重載;(c)多態中的相同函數簽名的函數。但是在類層次中共享函數名但函數簽名不同時,將產生遮蔽,而遮蔽是非常危險的,建議要謹慎地運用這種遮蔽類型的名字共享機制。

 

13,抽象基類:抽象基類確保其派生類必須定義某些指定的函數,否則這個派生類就不能被實例化。要求 該類必須擁有一個純虛成員函數,在純虛成員函數聲明的結尾加上 =0 就可以將這個函數定義為純虛成員函數。

1 class ABC {
2     public:
3         virtual void open() = 0;
4 };
純虛成員函數

 

14,雖然不能創建一個抽象基類的對象,但抽象基類可以擁有派生類,從抽象基類派生來的類必須覆蓋基類的所有純虛成員函數,否則派生類也成為抽象基類,因而也不能用來創建對象。一個抽象基類可以有其他不是純虛成員函數或甚至不是虛函數的成員函數,還可以有數據成員。抽象基類的成員可以是 private、protected 或 public。

 

15,定義純虛成員函數的限制:只有虛函數才可以成為純虛成員函數,非虛函數或頂層函數都不能聲明為純虛成員函數。

1 void f() = 0; // ERROR : not a virtual method
2 
3 class {
4     public:
5         void open = 0; // ERROR: not a virtual method
6 }; 
純虛函數錯誤定義示例

 

16,抽象基類作用:通過這種機制,可以用來指明某些虛函數必須被派生類覆蓋,否則這些派生類就不能擁有對象。從這種意義上看,抽象基類實際上定義了一個公共接口,這個接口被所有從抽象基類派生的類共享。因為抽象基類通常只有 public 成員函數,所以經常使用關鍵字 struct(默認為 public ) 來聲明抽象基類。

 

17,運行期類型識別(RTTI):一個基類指針不經過明確的轉型操作,就能指向基類對象或派生類對象,反過來就不大一樣了,將一個派生類指針指向基類對象是一種不明智的做法。當然,通過明確的轉型操作可以做到這一點:

 1 class B {
 2     //...
 3 };
 4 
 5 class D : public B {
 6     //...
 7 };
 8 
 9 int main() {
10     D* p;
11     p = new B;  // ERROR: explicit cast needed
12     p = static_cast<D*>(new B);   // caution
13     //...
14     return 0;
15 }
static_cast 用法示例

上述這種用法是合法的,但這種轉型操作相當危險,可能會造成難以跟蹤的運行期錯誤。static_cast 不能保證類型安全(type safely)。如果 p 不小心指向了一個沒有定義 m 的 B 對象,將會導致錯誤。

 1 class B {
 2     f() { }  // Note: no method m
 3 };
 4 
 5 class D : public B {
 6     void m() { }  // not in base class
 7 };
 8 
 9 int main() {
10     D* p;
11     p = static_cast<D*>(new B);
12     p->m();     // ERROR: there is no B::m 
13     return 0;
14 }
類型不安全使用示例

 

18,C++ 提供的 dynamic_cast 操作符可以在運行期檢測某個轉型動作是否類型安全。dynamic_cast 和 static_cast 有同樣的語法,不過 dynamtic_cast 僅對多態類型(至少有一個虛函數的類)有效。

 1 class B {
 2     virtual f() { }  // Note: no method m
 3 };
 4 
 5 class D : public B {
 6     void m() { }  // not in base class
 7 };
 8 
 9 int main() {
10     D* p;
11     p = dynamic_cast< D* >(new B);
12     if(p)  // 如果轉型動作安全,返回指向的對象指針 ptr 
13         p->m();
14     else   // 如果轉型動作不安全,返回 false 
15         cout<<"Not safe for p to point to a B"<<endl;
16 
17     return 0;
18 }
轉換類型動作檢查是否安全

 

19,dynamic_cast 的規則:假定基類 B 具有多態性,而類 D 是直接或間接從類 B 派生而來的。通過繼承,類 D 也因此具有多態性,在這種情況下:

  (a)從派生類 D* 到基類 B* 的dynamic_cast 可以進行,這稱為 向上轉型(upcast)。

  (b)從基類 B* 到派生類 D* 的dynamic_cast 不能進行,這稱為 向下轉型(downcast)

  假定類 A 和類 Z 都具有多態性,但它們之間不存在繼承關系,在這種情況下,相互的轉型均不能進行。

 

20,typeid 用法:操作符 typeid 返回一個 type_info 類對象的引用,type_info 是一個系統類,用來描述類型,這個操作符可以施加於類型名(包括類名)或 C++ 表達式。

1 #include<typeinfo>
2 
3 int main() {
4     float x;
5     long y;
6     bool result = typeid(y) == typeid(x);
7     cout<< boolalpha << result <<endl;
8     return 0; 
9 }
typeid 使用示例

 


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM