c++的類的封裝/繼承/多態的簡單介紹


 本篇文章僅僅從很表層來介紹一個C++語言中的類,包括什么是類,類的封裝性/繼承性和多態性。高手直接跳過吧,看了浪費時間,新手或者想溫習一下的可以瀏覽看看。

 

1. 什么是類?

到底什么是類(class)??類就是一種類型,是用戶自己定義的一個類型,和內置類型如int/float/double類似,  用一個類可以去定義一個變量,即課本中所謂的類的實例化,會得到一個object。類這個類型比較特別,它即包括了數據(數據成員),又包含了若干個操作這些數據的方法(即成員函數)。為什么需要類呢?類提供了一種對事物的抽象,增強了事物的聚合性,類讓我們可以把一個事物當作一個整體去看,方便描述,方便建模。 例如:我們可以定義一個學生的類:包括了姓名/性別/年齡/學號ID等信息

  1 class Student
  2 {
  3 public:
  4         int GetID();
  5         string GetName();
  6         string GetSex();
  7         int GetAge();
  8         
  9         void SetID(int ID_); 
 10         void SetName(string Name_); 
 11         void SetSex(string Sex_); 
 12         void SetAge(int Age_); 
 13  
 14 private: 
 15         string m_Name; 
 16         string m_Sex; 
 17         int m_ID; 
 18         int m_Age 
 19 };           

如上所示,我們定義了一個Student的類, 定義它之后,編譯器就不光知道了Student是一個類型,而且知道了類型的一些細節,例如當使用該類型去定義一個變量(object)時,需要分配多少內存等,例如:

1     // 輸入Student類型在內存中占多少字節                                                     
2     cout << "Student類型的大小為:" << sizeof(Student) << endl;
3 
4     //實例化一個叫小明的學生,並命名為SB(有點矛盾)
5     Student xiaoming;
6     xiaoming.SetName("SB");
7     cout << xiaoming.GetName() << endl;

 

接下來,詳細說說如何定義一個類:

定義一個類,需要做的是:1. 聲明類擁有的數據成員, 2. 聲明類擁有的成員函數:

1 class Dog
2 {
3 public: 
4     int Age;
5     int GetAge();
6 };

成員函數在哪里定義呢?即可以在類的內部直接定義,也可以在類的外部進行定義(此時,需要指明所屬的類)。當定義在類的內部時,默認聲明為inline函數。 當類外部定義成員函數時,可以有類內聲明為inline函數,也可以在定義時候聲明了inline函數,但是個人更喜歡在類外定義的時候聲明為inline函數,這樣可以根據自己定義一個函數的實際情況,決定是否聲明為inline函數,而不需要提前考慮好。

 1 // 類的內部定義
 2 class Dog 
 3 {
 4 public: 
 5     int Age;
 6     int GetAge()    //當定義在類的內部時,默認聲明為inline函數
 7     {
 8         return Age;
 9     }
10 };
11 
12 // 類外部定義
13 class Dog
14 {                                                                                                                                                                                               
15 public: 
16     int Age;
17     int GetAge();
18 };
19 
20 Dog::GetAge()
21 {
22     return Age;
23 }
24 
25 // 類外部定義,顯示聲明為inline函數
26 class Dog
27 {
28 public: 
29     int Age;
30     inline int GetAge();
31 };

另外:

1. 類成員函數通過隱式this指針訪問類內部的數據成員的; 如果把this指針修飾為const,在成員函數后面加const ,這就是const 成員函數:  GetAge() const 
2.  編譯器在解析一個類,總是先把類內部的數據成員解析完畢,再去處理成員函數,因此,定義一個類時,不必關心數據成員與成員函數的先后位置,數據成員聖成員函數問題可見的。
3. 一個類域也是一個作用域(用{ }括起來的部分),正因為如此, 1. 在類的外部定義成員函數時,指定類名就進入了類的作用域中了,就可以找到數據成員了; 2. 對類內的靜態數據成員與靜態成員函數,可以通過類名作用域訪問(對非靜態的不可以,因為非靜態的數據成員屬於具體的一個對象而不屬於類,雖然非靜態的成員函數實際上在多個對象之間是共享的,但是也只能通過對象名對象的指針訪問,因為它們有隱式的this指針)

 

2. 類的封裝性

 封裝性就是說可以把一部分東西封裝起來,不讓別人看到。在C++中,類這種類型通過它的訪問控制符來體現了它的封裝性,包括:

public: 公有的,在類的外部放着,誰都可以看到;
protected: 保護性的,只讓它的子類可以看到;
private: 私有的,即把它們封裝在了類的內部,在類的外面是看不到的,只有類內部的人可以看到;

例如:定義了一個House的類,House外面的只能看到pulic下的內容,而在House里面,可以看到所有內容;

 1 class House
 2 {
 3 public:                                                                                      
 4     int Windows;
 5     void OpenWindow();
 6     int doors;
 7     void OpenDoors();
 8 
 9 protected:
10     int *** // 這個不好舉例子
11 
12 private:
13     int Desk;
14     int light;
15     void OpenLight();
16 }

    使類具有封裝性的目的是:我們可以定義一些類,只對類的使用者留一下公有的接口(即pulic下的內容),而類內部的相關操作對類的使用者來說是透明的,用戶不操心。我可以隨便改類內部的代碼,只有公有的接口不變,類的用戶的代碼是不需要調整的。保證數據安全,方便用戶使用,大家都省心啊。

 

3. 類的繼承

    如果我們想在一個類的基礎上繼續創建一個新類,這就用到了類的繼承性。繼承可以使用三個訪問標志符控制:public、protectd和private。  無論哪個繼承,對直接子類沒有任何影響,只對子類的用戶有影響。基類中的private成員無論使用哪個訪問標志符,子類的用戶是看不到的,基類的public與protected成員是否能讓子類的用戶看到由三種訪問標志符控制 。

public繼承: 基類內的數據成員與成員函數封裝特性不變在子類中不變
protected繼承: 基類內的數據成員與成員函數的public部分在子類中變為protected
private繼承: 基類內的數據成員與成員函數的public和protected部分變為private

例如:

 1 class Base
 2 {
 3 public:
 4     *****
 5 protected:
 6     *****
 7 };
 8                                                                                 
 9 // 公有繼承
10 class Derived1 : public Base
11 {
12 public:
13     .....
14 private:
15     ....
16 };
17 
18 // 私有繼承
19 class Derived2 : private Base
20 {
21 public:
22     ;;;;;;
23 };

a. 基類中虛函數和純虛函數

    思考這么一個問題: 當基類定義了某個函數,而在子類中又定義了一個同名的函數(返回類型與參數列表可以不同),這時會發生什么?
答: 子類內的該函數會隱藏基類中同名的函數。即使他們的參數列表與返回類型不同,也不會發生重載,因為重載必須在相同的作用域內發生,如果作用域不同,編譯器查找時,總會先找到最近作用域內的同名函數。

    很多時候,我們想在基類與子類中定義相同的接口(即同名函數),它們實現各自的功能,這就可以把這樣的函數聲明為虛函數,即virtual.   當通過類的指針與引用調用虛函數時,會發生動態綁定,即多態。如下面例子所示:

 1 // 程序
 2 #include<iostream>                                                              
 3 using namespace std;
 4 
 5 class Base
 6 {
 7 public:
 8     virtual void Say() {cout << "I am Base!" << endl;}
 9 };
10 
11 class Derived : public Base
12 {
13 public:
14     virtual void Say() {cout << "I am Derived!" << endl;}
15 };
16 
17 int main()
18 {
19     Base* pA = new Base;
20     Derived* pB = new Derived;
21     pA->Say();
22     pB->Say();
23 }
24 
25 // 輸出
26 yin@debian-yinheyi:~/c$ ./a.out 
27 I am Base!
28 I am Derived!

    當我們不想實例化一個類時,我們可以定義一個抽象基類,它只負責提供接口。包含純虛函數的類為抽象類。純虛函數必須在子類中進行聲明與定義。而虛函數可以不在子類中聲明與定義,這時候它會像普通成員函數一樣繼承基類中的虛函數的實現。

1 class Base
2 {
3 public:                                                                         
4     virtual void Say() = 0;     //純虛函數
5 }; 

總結來說:

1. 當我們要繼承一個類的接口與實現時,我們在基類中定義普普通通的成員即可。
2. 當我們想要繼承一個類的接口與默認的實現時,我們在基類中定義為虛函數。
3. 當我們只要繼承一個類的接口時,我們在基類中定義為純虛函數。

其它說明:

1. 在子類中,當我們重新聲明和定義虛函數時,可以加上virtual關鍵字(virtual只能用在類內,不可以把virtual 用在類外),也可以不加, 在c++11標准中,引入了override關鍵字來顯示表示覆蓋基類中虛函數的定義,override關鍵字有利於給編譯器更多信息,用於查錯。例如當我們在子類中定義了一個與基類中虛函數名字相同,但是參數列表不同的函數,我們本意是定義子類特有的虛函數版本,來覆蓋基類中的版本。然而這時候,基類與子類中的函數是獨立的,只是基類中的版本隱藏了而已。如果使用了override,編譯器發現沒有覆蓋,就會報錯。 如果我們不想讓基類中的某個虛函數被覆蓋掉,可以使用final關鍵字。(另外覆蓋,即override只會發生在虛函數身上

2. 如果我們定義 了一個類,並且不想該類被繼承,可以在定義這個類時,在類名后面加上final關鍵字。

 1 class Base
 2 {
 3 public:
 4     virtual void Say() {cout << "I am Base!" << endl;}
 5     virtual void Dad() final { cout << " I am your dad!" << endl;}   //對該虛函數使用final關鍵字
 6 };
 7 
 8 class Derived final : public Base       // 對類Derived 使用了final 關鍵字                                                                                                                       
 9 {
10 public:
11     void Say() override { cout << " I am Derived!" << endl;}        //使用了override關鍵字
12 };

3. 雖然一個純虛函數不需要定義,但是其實我們是可以定義一個純虛函數的,不過調用它的唯一途徑是”調用時明確指出它的class的名稱“。

 1 // 程序
 2 class Base
 3 {
 4 public:
 5     virtual void Hello() = 0;
 6 };
 7 void Base::Hello() {cout << "hello " << endl;}                                                                                                                                                  
 8 
 9 class Derived final : public Base
10 {
11 public:
12     void Hello() override {cout << "a,很疼的" << endl;}
13 };
14 
15 int main()
16 {
17     Derived* pB = new Derived;
18     pB->Hello();
19     pB->Base::Hello();
20 }
21 
22 // 輸出
23 yin@debian-yinheyi:~/c$ ./a.out 
24 a,很疼的
25 hello 

4. 基類與子類中的虛函數的返回類型和參數列表必須完全一致,如果不一致的話,編譯器認為他們是完全不相關的函數。他們之間不會發生覆蓋(override),子類中的同名函數只會隱藏子類中的同名函數。

b. 繼承中的作用域

關於類的作用域,我們要明白以下幾點:

1. 類本身是一個作用域,使用{ }括起來的。
2. 在類的繼承過程中,子類的作用域是嵌套在基類的作用域之內的(這就明白了為什么有時候子類中的成員函數會隱藏掉基類中函數,就時候如果我們想要使用被隱藏的基類函數,可以通過顯示指明類名(這時可以理解為作用域名)來訪問。
3. 在一個類的作用域中,編譯器在解析類時,它總會先解析類中聲明的所以數據成員與成員函數,再去解析成員函數的定義。正因為這樣的原因,無論數據成員定義在成員函數的后面還是前面,還是成員函數的順序前后之類的, 一個成員函數總是可以找到該類的數據成員或調用其它成員函數。

基於作用域的一個例子:

 1 // 程序
 2 class Base
 3 {
 4  public:
 5     virtual void Hello() { cout << "Hello, I am Base!" << endl; }
 6     void Hi() { cout << "Hi, Hi, Base!" << endl;}
 7 };
 8 
 9 class Derived : public Base
10 {
11 public:
12     void Hello() override { cout << "Hello, I am Derived!" << endl;}
13     void Hi() { cout << "Hi, Hi, Derived!" << endl;}
14 };
15 
16 int main()
17 {
18     Derived Derive;
19     Derive.Hello();
20     Derive.Hi();
21     Derive.Base::Hello();        //顯示調用基類的虛函數
22     Derive.Base::Hi();            // 顯示調用基類普通函數
23 
24     Base *pBase = &Derive;
25     pBase->Hello();        //指針調用,進行動態綁定,即多態
26     pBase->Base::Hello();     //顯示調用基類的虛函數
27     pBase->Base::Hi();          // 顯示調用基類普通函數
28 
29     return 0;
30 }
31 
32 //程序輸出:
33 Hello, I am Derived!
34 Hi, Hi, Derived!
35 Hello, I am Base!
36 Hi, Hi, Base!
37 Hello, I am Derived!
38 Hello, I am Base!
39 Hi, Hi, Base!

 

 

額外小知識點:

1. 待后續遇到補充!

 


免責聲明!

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



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