概述
C++的招牌能力之一,也是C++的核心特性沒有之一, 也是在 C 基礎擴充套件的最重要的能力,一切皆可封裝為物件,有三大主要特性,封裝、多型、繼承。
基礎
簡單理解,類就是使用者自定義的一種資料結構,封裝了資料和行為(函式)的組合。類中的資料稱為成員變數,函式稱為成員函式。類可以被看作是一種模板,可以用來建立具有相同屬性和行為的多個物件,類定義格式如下。
class ClassName {
Access Specifiers: // 訪問修飾符 public、private、
type VarName; // 成員變數
type function(){} // 成員函式
}
定義名稱為 Person
的類,包含一個成員變數、成員函式
class Person {
public:
string name;
void say() {
std::cout << "Im " << this->name << endl;
}
}
上面示例,在類的內部使用了this
指標訪問成員屬性name
,也可以使用作用域解析運算子::
,調整 say 函式程式碼如下,效果相同
void say() {
std::cout << "Im " << Person::name << endl;
}
然後,以該類為模板建立物件實體
Person p;
p.name = "tom";
p.say();
和宣告普通變數類似,建立了物件p
,同時修改了 name
屬性和呼叫 say
函式,訪問成員使用.
點運算子。
透過指標方式訪問成員屬性
Person *ptr = &p;
// 兩種方式都可以
(*ptr).say();
ptr->say();
上面示例中,兩種方能等價,(*ptr).say()
不能寫成*ptr.say();
,因為點運算子.
的優先順序高於*
,這種寫法會將t.age
看成一個指標,然後取它對應的值,會出現無法預料的結果,因為寫法很麻煩C 語言引入新的箭頭運算子->
,在 C++中得到沿用,並擴大到物件屬性
許可權修飾符可控制成員的可見範圍,C++支援三種修飾符,分別是:
public
,公開屬性,物件內部、外部都可以訪問和修改private
,私有屬性,預設屬性,僅物件內部可以訪問和修改protected
,受保護屬性,物件內部、子類可以訪問和修改
簡單示例
class Person {
public:
string name;
private:
int age;
protected:
string address;
}
看起來類和結構體定義比較相似,的確兩者部分特性相同,但有本質區別,簡單總結如下
- 兩者都是自定義複合資料結構,可組合資料和方法
- 結構體的屬性預設是
public
且不允許修改,類屬性預設是private
並支援修改 - 類支援繼承,結構體不支援繼承
- 類支援建構函式、解構函式,結構體不支援
- 類提供了封裝機制,可以隱藏內部實現,只暴露必要的介面;結構體通常用於儲存資料,不太注重封裝。
- 類支援運算子過載,結構體不支援
- 類支援模板類,結構體不支援
總結,類和結構體在C++中都用於定義自定義資料型別,但類更注重封裝、繼承和多型,而結構體更注重儲存資料。
建構函式
建構函式是一種特殊的成員函式,在建立物件時自動執行,主要用於初始化物件,從型別可以分為:無參建構函式、有參建構函式、複製建構函式。建構函式的定義也有所區別,首先函式名必須和類名稱相同,並且沒有返回值(注意void
都不需要),不能手動呼叫,支援初始化列表,支援過載。
建構函式示例,分別定義了三種型別的建構函式
class Person {
public:
string name;
Person() { // 無參建構函式
this->name = "tom";
}
Person(string name) { // 有參建構函式
this->name = name;
}
Person(Person const &p) { // 複製建構函式
this->name = p.name;
}
}
建立物件,根據引數自動匹配對應的建構函式,並呼叫執行
Person p; // 呼叫無參建構函式
Person p1("jerry"); // 呼叫有參建構函式
Person p2(p1); // 呼叫複製建構函式
特別注意,呼叫無慘建構函式時候沒有小括號 ()
,和函式申明語法衝突了,兩者格式一樣產生二義性,編譯器無法識別是函式申明、還是呼叫建構函式。
C++規範所有的類有兩個預設建構函式,由系統隱式提供,分別是無慘建構函式、複製建構函式。預設的複製建構函式,自動複製所有的屬性到新物件上,使用淺複製,可以過載實現深複製。
class Person {
public:
string name;
Addr *p_addr; // 增加一個指標型別成員變數
Person(Person const &p) { // 過載複製函式
this->name = p.name;
this->p_addr = new Addr(*p.p_addr); // 深度複製,在堆申請記憶體並建立新物件,記得在解構函式中釋放堆記憶體
}
}
如果過載了建構函式,系統有可能不再提供預設建構函式,遵循如下規則:
- 過載有參建構函式,系統就不再提供預設無慘建構函式
- 過載複製建構函式,系統就不再提供預設無慘建構函式、複製建構函式。
一般情況如果過載了建構函式,會顯示再新增一個無參構造,如下示例
class Person {
public:
Person(string name) { // 過載有參建構函式
...
}
Persion() = default; // 使用C++11預設建構函式特性
}
C++支援多種方式呼叫建構函式,也就是多種不同的方式建立物件,Java
程式設計師可能感到驚訝,分別有如下三種
括號法
和申明變數的格式一樣,申請記憶體,建立物件,呼叫對應建構函式初始化物件
Person p; // 呼叫無參建構函式
Person p1("jerry"); // 呼叫有參建構函式
Person p2(p1); // 呼叫複製建構函式
顯示法
有點類似函式呼叫,返回匿名物件
Person p = Person() // 呼叫無參建構函式
Person p1 = Person("jerry"); // 呼叫有參建構函式
Person p2 = Person(p1); // 呼叫複製建構函式
隱式法
最詭異的呼叫方式,看起來是給變數賦值。單引數的建構函式有個預設隱藏技能,型別轉換運算子,當等號右邊的型別恰好匹配構造的引數型別,就會呼叫對應建構函式。
Person p1 = "jerry"; // 呼叫有參建構函式
Person p2 = p1; // 呼叫複製建構函式
上面示例中兩個語句都觸發型別轉換運算子,會呼叫對應的建構函式。特別是p2 = p1
語句,看起來p2
等於p1
賦值語句,其實底層呼叫複製建構函式,兩者是完全獨立的物件。
這個技能看起來很酷,但在某些情況下容易產生困擾,C++提供了一個修飾符 explicit
,被修飾的構造器關閉型別轉換運算子的功能
class Person {
public:
string name;
explicit Person(Person const &p) { // 修飾, 關閉型別轉換
this->name = p.name;
}
}
隱式呼叫建立物件
// 建立物件
Person p;
Person p1 = p; // 編譯失敗
簡單總結,使用哪種方式都可以,要統一風格。
呼叫方法 | 無參構造 | 有參構造 | 複製構造 |
---|---|---|---|
括號法 | 有限支援 | 支援 | 支援 |
顯示法 | 支援 | 支援 | 支援 |
隱式法 | 不支援 | 有限支援 | 支援 |
使用new
建立物件,任何建立物件的方式前都可以加new
關鍵字,會改變物件儲存的位置,將儲存在堆記憶體中,建立過程:申請堆記憶體,建立物件,返回指標
Person *p = new Person("jerry");
if (p == NULL) {
exit(-1);
}
p->say();
delete p;
示例中,建立的物件就儲存在堆記憶體,如果分配失敗則退出程式,程式最後釋放堆記憶體。
初始化列表,建構函式的特有技能,可在函式定義中增加增加初始化資訊,在函式執行前就完成物件屬性的初始化
class Person {
public:
string name;
explicit Person(const string &new_name) : name(new_name) {}
}
上面示例,函式定義中的: name(name)
就是初始化列表,在函式執行前,使用實參new_name
初始化物件的name
屬性,函式邏輯為空,也完成了賦值初始化,當然可以在函式繼續修改。
初始化引數列表,還可以用於預設值初始化
class Person {
public:
string name;
Person() : name("tom") {} // 建構函式
}
上面示例,在無參建構函式上增加了初始化列表,引數是固定的 tom
,只要無參建構函式建立物件name
屬性總是tom
。
解構函式
與建構函式相對應的解構函式,物件銷燬時自動執行,主要用於釋放資源,如堆記憶體、檔案描述符等,函式定義也有特定格式,函式名稱是類名前加波浪號、沒有返回值(注意void
都不需要)、不能有引數、不支援過載
class Person {
public:
string name;
Person() : name(new_name) {} // 建構函式
~Person() { // 解構函式
...
}
}
物件銷燬收會自動執行解構函式,如果物件在棧儲存則退出棧時候執行,如果在堆儲存則釋放記憶體時候執行。
函式傳參
物件也可以做為函式引數傳遞,注意:和結構體特性一樣是值傳遞,形參和實參是不相等,也就是說在傳遞過程中會發生物件複製,Java
程式設計師可能又會感到驚訝。
定義了一個函式,形參是Person
型別
void match_name(Person person) {
if (person.name == "tom") {
// ...
}
}
呼叫該函式,注意:此時會觸發物件複製,建立的p
和傳入的p
是兩個獨立物件,底層是呼叫物件的複製建構函式實現。
Person p("tom");
metch_name(p);
函式的返回值如果是物件,也會觸發物件複製,如下示例
Addr match_name(Person person) { // 返回值是addr物件
if (person.name == "tom") {
Addr *addr = person.p_addr;
return *addr;
}
}
呼叫該函式,也會觸發函式複製,返回的addr
和接收addr
是兩個獨立的物件,底層也呼叫 addr
複製建構函式實現
Person p("tom");
Addr addr = match_name(p);
這種特性並不友好,大多是情況下都是希望傳遞物件自身,而不是複製後的新物件,可能是沿用了 C 語言的結構體特性,C++的物件是在結構體基礎擴充而來的。解決方案有兩種,分別是指標傳遞和引用傳遞,其實兩者本質一樣,都是地址傳遞,引用傳遞簡化了語法,更加推薦引用傳遞方式。下面是兩種傳遞方式示例。
使用指標傳遞
void match_name(Person const *person) {
if (person->name == "tom") {
...
}
}
呼叫
Person p("tom");
Addr addr = match_name(&p);
使用引用傳遞,使用更加簡潔
void match_name(Person const &person) {
if (person.name == "tom") {
...
}
}
呼叫
Person p("tom");
Addr addr = match_name(p);
特別注意的是返回值,如果使用指標或引用返回物件型別,一定要確保指標不能指向區域性變數,區域性變數儲存在棧中,函式呼叫結束就隨之銷燬了,返回的地址就是野指標
Addr* match_name(Person const &person) {
return person.p_addr; // ok
// ok, 指向堆記憶體
// return new Addr(*(person.p_addr));
// err, 指向區域性變數了,野指標
// Addr addr(*(person.p_addr));
// return &addr;
}
this 指標
這是個比較特殊的成員變數,由系統預設提供,它是一個指標,總是指向當前的物件例項。和 Java 的 this、python 的 self 等功能類似
簡單示例
class Person {
public:
string name;
explicit Person(string const &name) : name(name) {}
void say() {
cout << "Im " << this->name << endl;
}
};
建立兩個物件,並執行 say
函式
Person p1("tom");
p1.say(); // Im tom
Person p2("jerry");
p2.say(); // Im Jerry
相同的語句this->name
讀取內容不一樣,因為this
總是指向當前的物件
程式碼調整下,把類定義中的this->name
調整為 name
,然後看看效果
void say() {
cout << "Im " << name << endl;
}
和上面的示例一樣,建立兩個物件,然後執行 say
Person p1("tom");
p1.say(); // Im tom
Person p2("jerry");
p2.say(); // Im Jerry
可以發現兩者完完全一樣。這個讀取變數優先順序有關係:區域性變數->物件變數->全域性變數,逐層向上查詢,如果是物件變數,系統會自動補齊this
指標。
如果物件屬性和區域性變數同名時,又想訪問物件的成員變數,就可以使用this
指標,精確控制讀取位置
class Person {
private:
string name;
public:
void setName(string const &name) {
this->name = name; // this指標
}
};
如上示例,使用this
指標,把區域性變數name
賦值給物件變數name
。也可以總顯示的使用this
指標,程式碼指向更加清晰。
還有個重要作用,如果成員函式希望返回物件本身,就可以使用this
class Person {
private:
string name;
int age;
public:
Person* setName(string const &name) {
this->name = name;
return this;
}
Person* setAge(int const &age) {
this->age = age;
return this;
}
};
使用示例,有點類似 Java
常用的Build
技能
Person p;
p1.setName("tom").setAge(10);
另外特別注意, 指標this
是被 const
修飾過的指標常量,也就是說不允許修改的指向位置,但是可以修改指向的值。宣告this
的虛擬碼如下
Person * const this;
如下示例,如果修改this
指向將編譯失敗
void setName(string const &name) {
this = NULL: // err
}
const 修飾
被const
修飾的變數為只讀變數,也可以用於修飾類的多個位置,分別有不同的功能,逐一介紹
修飾成員屬性,可稱為常屬性,就算是 public 的成員屬性,只要被修飾都變成只讀,不可修改
class Person {
public:
int age;
string const name; // 修飾
void setName(string const &name) {
this->name = name; // err, 編譯失敗
}
}
修飾成員函式,可稱為常函式,被修飾的成員函式,不允許修改物件自身的任何屬性。
class Person {
public:
string name;
void setName(string const &name) const { // 修飾
this->name = name; // err, 編譯失敗
}
}
以上示例,修飾後的成員函式不允許修改成員屬性,另外注意const
的位置,是在函式小括號之後,這是個專用語法。
修飾成員函式,其本質是進一步修飾this
指標,指向的值也不能修改了,修飾後的this
宣告虛擬碼如下
const Person * const this;
所以被const
修飾的成員函式,可以讀取任意屬性,但是不能修改任何屬性。
但是 C++有開又增加mutable
修飾符,被修飾的成員屬性,在常函式中也允許修改,可以更精細的控制許可權。
class Person {
public:
int age;
mutable string name;
void setName(string const &name) const {
this->name = name; // ok
}
void setAge(int const &age) const {
this->age = age; // err, 編譯失敗
}
void say() {
cout << "Im " << this->name << endl;
}
}
如上示例,name
屬性被mutable
修飾了,所以name
屬性允許在常函式中修改;age
屬性則不允許被修改。
修飾物件,可稱為常物件,被修飾後不允許修改物件的任何屬性,被mutable
的除外
const Person p; // const 修飾
p.name = "tom"; // ok
p.age = 10; // err
另外注意,常物件只允許呼叫常函式,下面示例編譯失敗
p.setNmae("ok"); // ok, setNmae是常函式
p.say(); // err, 普通不允許呼叫
靜態成員
使用 static 關鍵字定義的是靜態屬性,靜態成員無論建立多少個類的物件,靜態成員都只有一個副本。
class Person {
private
static string category; // 靜態屬性
string getCategory() { // 靜態函式
category;
}
public:
string name;
}