本篇文章僅僅從很表層來介紹一個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. 待后續遇到補充!