介紹
類是面向對象程式設計中的概念,是面向對象編程的基礎。
類的實質是 一種數據類型,類似於int、char等基本類型,不同的是它是一種複雜的數據類型。因為它的本質是類型,而不是數據,所以不存在於記憶體中,不能被直接操作,只有被實例化為對象時,才會變得可操作。
類是 對現實生活中一類具有共同特徵的事物的抽象。如果一個程式里提供的類型與套用中的概念有直接的對應,這個程式就會更容易理解,也更容易修改。一組經過很好選擇的用戶定義的類會使程式更簡潔。此外,它還能使各種形式的代碼分析更容易進行。特別地,它還會使編譯器有可能檢查對象的非法使用。
類的內部封裝了方法,用於操作自身的成員。類是對某種對象的定義,具有行為(be-havior),它描述一個對象能夠做什麼以及做的方法(method),它們是可以對這個對象進行操作的程式和過程。它包含有關對象行為方式的信息,包括它的名稱、方法、屬性和事件。
類的構成包括數據成員和成員函式。數據成員對應類的屬性,類的數據成員也是一種數據類型,並不需要分配記憶體。成員函式則用於操作類的各項屬性,是一個類具有的特有的操作,比如“學生”可以“上課”,而“水果”則不能。類和外界發生互動的操作稱為接口。
用法
定義一個類
公有成員、私有成員、保護成員均包含數據成員和成員函式兩部分,彼此沒有順序之分。一個public/private/protected關鍵字下可以跟多個成員,直到下一個public/private/protected關鍵字。如果成員前面沒有public/private/protected關鍵字,默認為私有成員。
結尾部分的分號必不可少,否則會發生語法錯誤。
無論公有成員、私有成員還是保護成員,彼此之間都可以訪問。比如公有的成員函式可以操作保護的數據成員,也可以調用私有的成員函式。
類的數據成員是類型,所以不能被賦值,聲明數據成員和聲明普通變數的格式相同,比如“int n;”。
1.公有成員、私有成員、保護成員均包含數據成員和成員函式兩部分,彼此沒有順序之分。一個public/private/protected關鍵字下可以跟多個成員,直到下一個public/private/protected關鍵字。如果成員前面沒有public/private/protected關鍵字,默認為私有成員。
2.結尾部分的分號必不可少,否則會發生語法錯誤。
3.無論公有成員、私有成員還是保護成員,彼此之間都可以訪問。比如公有的成員函式可以操作保護的數據成員,也可以調用私有的成員函式。
4.類的數據成員是類型,所以不能被賦值,聲明數據成員和聲明普通變數的格式相同,比如“int n;”。
成員函式的實現
成員函式可以在類內實現,也可以在類外實現。內部實現的成員函式被默認為加上了inline;外部實現的成員函式必須加上域操作符,即“類名::成員函式”。
構造函式與析構函式
構造函式和析構函式是特殊的成員函式,和普通成員函式不同的地方在於:
函式名固定
構造函式和析構函式的函式名必須是類名。
聲明格式不同
構造函式和析構函式沒有返回值,連空返回值——void也沒有。
構造函式的聲明形式:類名(參數列表);
析構函式的聲明形式:~類名();
重載的特殊性
構造函式和普通成員函式一樣可以被重載,析構函式不可以重載,只能是空參數。
調用過程不同
構造函式和析構函式不能被顯式地調用,只能由編譯器自動調用。
1.函式名固定
構造函式和析構函式的函式名必須是類名。
2.聲明格式不同
構造函式和析構函式沒有返回值,連空返回值——void也沒有。
構造函式的聲明形式:類名(參數列表);
析構函式的聲明形式:~類名();
3.重載的特殊性
構造函式和普通成員函式一樣可以被重載,析構函式不可以重載,只能是空參數。
4.調用過程不同
構造函式和析構函式不能被顯式地調用,只能由編譯器自動調用。
構造函式用於創建類的對象,任何創建對象的行為,都會導致構造函式被調用。析構函式和構造函式的功能相反,析構函式用於銷毀對象,當類的對象超出作用域被銷毀時,析構函式被調用。
即使顯式地定義構造函式和析構函式,也還是會有默認的構造函式和析構函式,函式內部無任何語句,不執行任何操作。默認構造函式是無參數的。需要注意的是,一旦顯式定義任意形參的構造函式,默認構造函式都不會生成,即只有沒有定義構造函式的類才存在默認構造函式。
一般情況下,默認的構造函式和析構函式可以滿足功能需要,然而當需要重載構造函式,或是需要動態分配資源的時候,就不得不定義自己的構造函式甚至析構函式了。
拷貝構造函式是特殊的構造函式,在複製對象時被調用,定義的格式為“類名(類名& 參數名)”。拷貝構造函式也存在默認的,但很多情況下都需要重載。
類的實例化
就像聲明某種類型的變數一樣,聲明一個類類型的對象,就是類的實例化,會涉及到必要的記憶體分配。
不同語言中類的實例化形式是不同的。
C++
類名 對象名(參數列表);
如果沒有參數,括弧必須省略,即“類名 對象名;”,自動調用構造函式。特殊地,參數可以是類的對象,此時會自動調用拷貝構造函式。
Java/C#
類名 對象名 = new 類名(參數列表);
括弧不能省略。
1.C++
類名 對象名(參數列表);
如果沒有參數,括弧必須省略,即“類名 對象名;”,自動調用構造函式。特殊地,參數可以是類的對象,此時會自動調用拷貝構造函式。
2.Java/C#
類名 對象名 = new 類名(參數列表);
括弧不能省略。
對象可以訪問類的成員,但並不是所有成員都可以被訪問,能否訪問取決於聲明該成員時所用的關鍵字(public/protected/private)。具體規則如下:
類的公有成員可以被該類,其派生類和類實例化的對象訪問。
類的保護成員可以被該類及其派生類訪問,不可以被該類的對象訪問。
類的私有成員可以被該類訪問,不可以被派生類及其該類的對象訪問。
1.類的公有成員可以被該類,其派生類和類實例化的對象訪問。
2.類的保護成員可以被該類及其派生類訪問,不可以被該類的對象訪問。
3.類的私有成員可以被該類訪問,不可以被派生類及其該類的對象訪問。
派生與繼承
子類即是繼承而來的類,父類即是被繼承的類,或者稱之為基類。
public修飾的為公有繼承,private修飾的為私有繼承,protected修飾的為保護繼承。
父類可以只有一個,也可以有多個。只有一個父類稱為單繼承,多個父類稱為多繼承。C++支持多繼承的機制,Java則只具有單繼承功能,並增加了“接口”的概念,一個類可以實現多個接口。
如果不標明繼承方式,默認為私有繼承。
1.子類即是繼承而來的類,父類即是被繼承的類,或者稱之為基類。
2.public修飾的為公有繼承,private修飾的為私有繼承,protected修飾的為保護繼承。
3.父類可以只有一個,也可以有多個。只有一個父類稱為單繼承,多個父類稱為多繼承。C++支持多繼承的機制,Java則只具有單繼承功能,並增加了“接口”的概念,一個類可以實現多個接口。
4.如果不標明繼承方式,默認為私有繼承。
派生和繼承是類的重要特性,繼承是由抽象到具體的體現。通過繼承,子類可以使用父類的成員。
但要注意的是,派生和繼承在帶來方便的同時,也會使類與類之間的關係變得複雜,尤其是涉及到私有繼承和保護繼承時,類中成員的關係可能會變得難以理解。所以在涉及類時,儘量避免過多層次的繼承,私有繼承和保護繼承的使用也要慎重。
繼承來的成員和自身聲明的成員並無本質區別,也有公有成員、私有成員、保護成員之分。繼承時,父類中成員類型(公有成員/私有成員/保護成員)和繼承方式(公有繼承/私有繼承/保護繼承)不同,情況不同。可以歸納為:
三種類型的繼承,父類的成員均被子類繼承(之前的百科關於這點的描述是錯誤的),只是由類實例化的對象對其繼承的成員的訪問許可權會有所變化。三種不同方式的繼承,描述的是子類實例化對象對其成員的訪問許可權,並非是描述子類時,子類對繼承自父類的成員的訪問許可權。
公有繼承
繼承自父類的成員保持不變。
私有繼承
繼承自父類的成員全部變為私有成員。
保護繼承
繼承自父類的公有成員變為保護成員,其餘不變。
1.公有繼承
繼承自父類的成員保持不變。
2.私有繼承
繼承自父類的成員全部變為私有成員。
3.保護繼承
繼承自父類的公有成員變為保護成員,其餘不變。
操作符重載
操作符重載必須在類中進行,重載操作符可以使操作符對在類中的語義發生變化。除了. ,.* ,:: ,? : 、sizeof、typeid這幾個運算符不能重載之外,大部分運算符都能被重載。但要注意,重載操作符並不能改變操作符的優先權和結合律,而且從認知規律上講,重載的操作符功能必須與原意相近,否則很難被人理解。
操作符重載是函式,在使用該操作符時被調用。操作符重載函式的聲明形式:返回值 operator操作符(參數列表);
友元
友元可以是函式,被稱為友元函式;也可以是類,被稱為友元類。
通常,類中的私有成員只能被自身使用,無法被它的對象訪問。因此,另一個類即便可以使用該類的對象,也無法訪問該類的私有成員,通過定義友元的方法可以做到這一點。
友元就是在一個類中“再次聲明”另一個類的成員函式或是另一個類,被“再次聲明”的成員函式或類可以訪問該類的私有成員。這種“再次聲明”並不是普通的聲明,格式為:friend 函式/類名;
顯然,友元會破壞類的封裝性,使本該隱藏的成員暴露出來,因此應當謹慎使用。
組合
繼承可以描述“交通工具”和“公車”的關係,卻無法描述“公車”和“車輪”的關係。
大多數“車輪”具有的特性是“公車”所不具有的。比如說“車輪”具有“重量”,而“公車”的“重量”則是另一個含義。而通過私有成員、保護成員機制控制這些成員的繼承性,會使繼承變得複雜而難以理解。而且
繼承來的數據成員只有一個,而一輛“公車”卻有四個“車輪”,四個“車輪”的“重量”。
1.大多數“車輪”具有的特性是“公車”所不具有的。比如說“車輪”具有“重量”,而“公車”的“重量”則是另一個含義。而通過私有成員、保護成員機制控制這些成員的繼承性,會使繼承變得複雜而難以理解。而且
2.繼承來的數據成員只有一個,而一輛“公車”卻有四個“車輪”,四個“車輪”的“重量”。
引入組合的概念,“公車”完全可以由“車輪”、“方向盤”、“車身”等類組合而來。方法就是將類當成其他的數據類型一樣,在另一個類中定義該類類型的數據成員。
抽象類
並不是所有種類的事物都可以被實例化,換而言之,有的種類只是一種抽象概念,現實中並沒有實際存在的對應物。比如:假設所有的動物都會叫,我們可以定義一個類——“動物”,定義類中的一個成員函式——“叫”,我們知道貓的叫聲,也知道狗的叫聲,然而“動物”是如何“叫”的?——我們根本無法實現它。所以,我們引入了抽象類的概念,抽象類是無法被實例化的,無法聲明抽象類的對象。
通常,用 abstract 修飾的類是抽象類;C++中包含純虛函式的類是抽象類;Java中含有抽象方法的類是抽象類;繼承了純虛函式而沒有實現它的類也是抽象類。
抽象類只能被用作基類,作用體現在:
約束派生類必須實現的成員函式或方法。
不同派生類中同名的成員函式實現不同,體現了多態性。
1.約束派生類必須實現的成員函式或方法。
2.不同派生類中同名的成員函式實現不同,體現了多態性。
靜態成員
用static修飾的成員是靜態成員,可以是成員函式或數據成員。靜態成員是所有對象共有的,只分配一次記憶體,產生新的對象時不會產生副本。
靜態數據成員的初始化必須在類外進行,使用靜態成員時必須使用類名和域操作符。
特性
類的三大特性
封裝性
將數據和操作封裝為一個有機的整體,由於類中私有成員都是隱藏的,只向外部提供有限的接口,所以能夠保證內部的高內聚性和與外部的低耦合性。用者不必了解具體的實現細節,而只是要通過外部接口,以特定的訪問許可權來使用類的成員,能夠增強安全性和簡化編程。
繼承性
繼承性更符合認知規律,使程式更易於理解,同時節省不必要的重複代碼。
多態性
同一操作作用於不同對象,可以有不同的解釋,產生不同的執行結果。在運行時,可以通過指向基類的指針,來調用實現派生類中的方法。
1.封裝性
將數據和操作封裝為一個有機的整體,由於類中私有成員都是隱藏的,只向外部提供有限的接口,所以能夠保證內部的高內聚性和與外部的低耦合性。用者不必了解具體的實現細節,而只是要通過外部接口,以特定的訪問許可權來使用類的成員,能夠增強安全性和簡化編程。
2.繼承性
繼承性更符合認知規律,使程式更易於理解,同時節省不必要的重複代碼。
3.多態性
同一操作作用於不同對象,可以有不同的解釋,產生不同的執行結果。在運行時,可以通過指向基類的指針,來調用實現派生類中的方法。
與結構體的區別
在C++、C#語言中,class和struct都可以定義一個類,它們的區別如下:
C#中,class是引用類型,繼承自System.Object類;struct是值類型,繼承自System.ValueType類,不具多態性。但是注意,System.ValueType是個引用類型。
從職能觀點來看,class表現為行為;而struct常用於存儲數據。
class支持繼承,可以繼承自類和接口;而struct沒有繼承性,struct不能從class繼承,也不能作為class的基類,但struct支持接口繼承。
class可以聲明無參構造函式,可以聲明析構函式;而struct只能聲明帶參數構造函式,且不能聲明析構函式。因此,struct沒有自定義的默認無參構造函式,默認無參構造器只是簡單地把所有值初始化為它們的0等價值。
Java/C#中,實例化時,class要使用new關鍵字;而struct可以不使用new關鍵字,如果不以new來實例化struct,則其所有的欄位將處於未分配狀態,直到所有欄位完成初始化,否則引用未賦值的欄位會導致編譯錯誤。
class可以實現抽象類(abstract),可以聲明抽象函式;而struct為抽象,也不能聲明抽象函式。
class可以聲明protected成員、virtual成員、sealed成員和override成員;而struct不可以,但是值得注意的是,struct可以重載System.Object的3個虛方法,Equals()、ToString()和 GetHashTable()。
class的對象複製分為淺拷貝和深拷貝,必須經過特別的方法來完成複製;而struct創建的對象複製簡單,可以直接以等號連線即可。
class實例由垃圾回收機制來保證記憶體的回收處理;而struct變數使用完後立即自動解除記憶體分配。
作為參數傳遞時,class變數是以按址方式傳遞;而struct變數是以按值方式傳遞的。
1.C#中,class是引用類型,繼承自System.Object類;struct是值類型,繼承自System.ValueType類,不具多態性。但是注意,System.ValueType是個引用類型。
2.從職能觀點來看,class表現為行為;而struct常用於存儲數據。
3.class支持繼承,可以繼承自類和接口;而struct沒有繼承性,struct不能從class繼承,也不能作為class的基類,但struct支持接口繼承。
4.class可以聲明無參構造函式,可以聲明析構函式;而struct只能聲明帶參數構造函式,且不能聲明析構函式。因此,struct沒有自定義的默認無參構造函式,默認無參構造器只是簡單地把所有值初始化為它們的0等價值。
5.Java/C#中,實例化時,class要使用new關鍵字;而struct可以不使用new關鍵字,如果不以new來實例化struct,則其所有的欄位將處於未分配狀態,直到所有欄位完成初始化,否則引用未賦值的欄位會導致編譯錯誤。
6.class可以實現抽象類(abstract),可以聲明抽象函式;而struct為抽象,也不能聲明抽象函式。
7.class可以聲明protected成員、virtual成員、sealed成員和override成員;而struct不可以,但是值得注意的是,struct可以重載System.Object的3個虛方法,Equals()、ToString()和 GetHashTable()。
8.class的對象複製分為淺拷貝和深拷貝,必須經過特別的方法來完成複製;而struct創建的對象複製簡單,可以直接以等號連線即可。
9.class實例由垃圾回收機制來保證記憶體的回收處理;而struct變數使用完後立即自動解除記憶體分配。
10.作為參數傳遞時,class變數是以按址方式傳遞;而struct變數是以按值方式傳遞的。
我們可以簡單的理解,class是一個可以動的機器,有行為,有多態,有繼承;而struct就是個零件箱,組合了不同結構的零件。其實,class和struct最本質的區別就在於class是引用類型,記憶體分配於託管堆;而struct是值類型,記憶體分配於執行緒的堆疊上。由此差異,導致了上述所有的不同點。所以只有深刻的理解記憶體分配的相關內容,才能更好的駕馭。
當然,使用class基本可以替代struct的任何場合,class後來居上。雖然在某些方面struct有性能方面的優勢,但是在面向對象編程里,基本是class橫行的天下。
那么,有人不免會提出,既然class幾乎可以完全替代struct來實現所有的功能,那么struct還有存在的必要嗎?答案是,至少在以下情況下,鑒於性能上的考慮,我們應該考慮使用struct來代替class:
實現一個主要用於存儲數據的結構時,可以考慮struct。
struct變數占有堆疊的空間,因此只適用於數據量相對小的場合。
struct數組具有更高的效率。
1.實現一個主要用於存儲數據的結構時,可以考慮struct。
2.struct變數占有堆疊的空間,因此只適用於數據量相對小的場合。
3.struct數組具有更高的效率。
示例
類的復用