12.類和對象
12.1 類的封裝
1.成員變量和成員函數
(1)成員變量
C++中用于表示類屬性的變量。
類的成員變量和普通變量一樣,也有資料類型和名稱,占用固定長度的記憶體。但是,在定義類的時候不能對成員變量指派,因為類隻是一種資料類型或者說是一種模闆,本身不占用記憶體空間,而變量的值則需要記憶體來存儲。
(2)成員函數
C++中用于表示類行為的變量。 類的成員函數也和普通函數一樣,都有傳回值和參數清單,它與一般函數的差別是:成員函數是一個類的成員,出現在類體中,它的作用範圍由類來決定;而普通函數是獨立的,作用範圍是全局的,或位于某個命名空間内。
(3)C++中可以給成員變量和成員函數定義通路級别。
public:成員變量和成員函數可以在類的内部和外界通路和調用。
private:成員變量和成員函數隻能在類的内部被通路和調用。
(4)注意:
1)所有對象共享類的成員函數。
2)普通成員函數不能通過類名來直接調用,它需要通過具體的對象來調用。
3)通過類名可以直接通路public成員變量。 下面的語句中:
class Test
{
public:
int n;
int func()
{
return n;
}
};
Test :: n //合法
Test :: func() //不合法
2.類的作用域
- 類成員的作用域都隻在類的内部,外部無法直接通路。
- 成員函數可以直接通路成員變量和調用成員函數。
- 類的外部可以通過類變量通路public成員。
- 類成員的作用域與通路級别沒有關系。
- C++中用struct定義類時,所有成員的預設通路級别為public,用class定義類時,所有成員的預設通路級别為private。
12.2 對象的構造和析構
1.構造函數
C++提供了一種特殊的成員函數,它的名字和類名相同,沒有傳回值,不需要使用者顯式調用,而是在建立對象時自動執行。這種特殊的成員函數就是構造函數,構造函數主要進行初始化工作。構造函數的調用是強制性的,一旦在類中定義了構造函數,那麼建立對象時就一定要調用,不調用是錯誤的。如果有多個重載的構造函數,那麼建立對象時提供的實參必須和其中的一個構造函數比對;反過來說,建立對象時隻有一個構造函數會被調用。
和普通成員函數一樣,構造函數是允許重載的。一個類可以有多個重載的構造函數,建立對象時根據傳遞的實參來判斷調用哪一個構造函數。
2.初始化清單
初始化與指派不同:
初始化:對正在建立的對象進行初值設定。
指派:對已經存在的對象進行值設定。
C++提供了初始化清單用于在構造函數中對成員變量進行初始化。初始化清單可以用于全部成員變量,也可以隻用于部分成員變量。
注意:
(1)成員變量的初始化順序與初始化清單中列出的變量的順序無關,它隻與成員變量在類中聲明的順序有關。
(2)初始化 const 成員變量的唯一方法就是使用初始化清單,如下所示:
class A
{
public:
A(int len);
private:
const int m_len;
};
//必須使用初始化清單來初始化 m_len
A::A(int len): m_len(len)
{
...
}
3.析構函數
與構造函數相對應的是析構函數,主要進行對象銷毀工作,在銷毀對象時自動執行。
注意:析構函數沒有參數,不能被重載,是以一個類隻能有一個析構函數。如果使用者沒有定義,編譯器會自動生成一個預設的析構函數。
4.特殊的構造函數
(1)無參構造函數
如果類中沒有定義構造函數,那麼編譯器會自動生成一個預設的無參構造函數,隻是這個構造函數的函數體是空的,也沒有形參,也不執行任何操作。
(2)拷貝構造函數
如果類中沒有定義拷貝構造函數,那麼編譯器會自動生成一個預設的拷貝構造函數,該函數的功能是複制成員變量。以下情況會調用拷貝構造函數:
- 一個對象通過另外一個對象初始化時。
- 一個對象以值傳遞的方式傳入函數體,需要拷貝構造函數建立一個臨時對象壓入到棧空間中。
- 一個對象以值傳遞的方式從函數傳回,需要執行拷貝構造函數建立一個臨時對象作為傳回值。
拷貝構造函數必須使用引用傳遞。當一個對象需要以值傳遞的方式進行傳遞時,編譯器會調用它的拷貝構造函數生成一個副本,如果類A的拷貝構造函數的參數不是引用傳遞,而采用值傳遞,那麼就又需要為了建立傳遞給拷貝構造函數的參數的臨時對象,而又一次調用類A的拷貝構造函數,這是一個無限遞歸調用。
(3)淺拷貝和深拷貝的差別:
淺拷貝沒有開辟新的空間,拷貝的指針和原來的指針指向同一片記憶體區域,如果原來的指針所指向的資源釋放了,那麼再次釋放淺拷貝的指針就會出錯。
對于簡單的類,預設的拷貝構造函數一般就夠用了,我們也沒有必要再顯式地定義一個功能類似的拷貝構造函數。但是當類持有其它資源時,例如動态配置設定的記憶體、指向其他資料的指針等,預設的拷貝構造函數就不能拷貝這些資源了,我們必須顯式地定義拷貝構造函數,以完整地拷貝對象的所有資料,這就是深拷貝。深拷貝會開辟新的空間用來存放新的值,即使原來的對象被釋放掉,不會影響深拷貝得到的值。
如果一個類擁有指針類型的成員變量,那麼絕大部分情況下就需要深拷貝。因為隻有這樣,才能将指針指向的内容再複制出一份來,讓原有對象和新生對象互相獨立,彼此之間不受影響。如果類的成員變量沒有指針,一般淺拷貝足以。
示例如下:
class Student
{
private:
char *name;
public:
Student()
{
name = new char(10);
cout << "Student()" << endl;
}
Student(const Student &s)
{
//淺拷貝,對象的name和傳入對象的name指向相同的位址
//this->name = s.name;
//cout << "shallow copy" << endl;
//深拷貝
this->name = new char(10);
memcpy(this->name, s.name, strlen(s.name));
cout << "deep copy" << endl;
}
~Student()
{
cout << "~Student() " << &name << endl;
delete name;
name = nullptr;
};
};
int main()
{
Student stu1;
Student stu2(stu1);
return 0;
}
/*淺拷貝由于兩個指針指向相同的位址,是以釋放s2時會報錯,運作結果如下
*Student()
*shallow copy
*~Student() 001EFBC0
*~Student() 001EFBCC
*報錯
*/
/*深拷貝正常運作,運作結果如下:
*Student()
*deep copy
*~Student() 001EFBC0
*~Student() 001EFBCC
*/
(4)轉換構造函數
轉換構造函數用于将其他類型的變量,隐式轉換為本類對象,轉換構造函數的形參是其他類型變量,且隻有一個形參,如下所示:
class Student
{
private:
string name;
int score;
public:
Student{}:name(""), score(0){}
//轉換構造函數,形參是其他類型變量,且隻有一個形參
Student(int grade):name("Jack"), score(grade){ }
~Student(){}
}
(5)移動構造函數
移動構造函數是C++ 11新增的構造函數,該構造函數以移動而非深拷貝的方式初始化含有指針成員的類對象。對于程式執行過程中産生的臨時對象,往往隻用于傳遞資料(沒有其它的用處),并且會很快會被銷毀。是以在使用臨時對象初始化新對象時,我們可以将其包含的指針成員指向的記憶體資源直接移給新對象所有,無需再新拷貝一份,這大大提高了初始化的執行效率。
5.指派函數
(1)C++的一個空類,編譯器會預設加入的成員函數:
1)無參構造函數
2)拷貝構造函數
3)析構函數
4)指派函數
(2)構造函數和指派函數的差別
1)對象不存在,且沒用别的對象來初始化,就是調用了構造函數;
2)對象不存在,且用别的對象來初始化,就是拷貝構造函數;
3)對象存在,用别的對象來給它指派,就是指派函數。
//調用拷貝構造函數
A a;
A b(a);
A b=a;
//調用指派函數
A a;
A b;
b=a;
12.3 對象的記憶體模型
類是建立對象的模闆,不占用記憶體空間,不存在于編譯後的可執行檔案中;而對象是實實在在的資料,需要記憶體來存儲。對象被建立時會在棧區或者堆區配置設定記憶體。C++編譯器會将成員變量和成員函數分開存儲:分别為每個對象的成員變量配置設定記憶體,但是所有對象都共享同一段函數代碼(成員變量在堆區或棧區配置設定記憶體,成員函數在代碼區配置設定記憶體)。如下圖所示:
對象的大小隻受成員變量的影響,和成員函數沒有關系。示例代碼如下:
class Student
{
private:
char *m_name;
int m_age;
float m_score;
public:
void setname(char *name){}
void setage(int age){}
void setscore(float score){}
void show(){}
};
int main()
{
//在棧上建立對象
Student stu;
cout<<sizeof(stu)<<endl;
//在堆上建立對象
Student *pstu = new Student();
cout<<sizeof(*pstu)<<endl;
//類的大小
cout<<sizeof(Student)<<endl;
return 0;
}
/*運作結果如下
*12
*12
*12
*解釋:Student類包含三個成員變量,它們的類型分别是 char *、int、float,都占用 4 個位元組的記憶體,加起來共占用 12 個位元組的記憶體
*/
上述代碼中:假設 stu 的起始位址為 0X1000,那麼該對象的記憶體分布如下所示:
13.繼承
13.1 繼承的通路級别
繼承指類之間的父子關系,子類的對象可以獲得父類的對象的屬性和方法,并可以添加父類中沒有的屬性和方法。子類對象可以直接初始化父類對象,也可以直接指派給父類對象。
面向對象中的通路級别主要有三種,分别是:public、private和protected。其中private成員不能被外界通路,子類也不能通路父類的private成員;protected成員也不能被外界通路,但是子類可以通路父類的protected成員。
面向對象中還存在三種繼承關系:
- public繼承:父類成員在子類中保持原有通路級别
- private繼承:父類在子類中變為私有成員
- private繼承:父類中的public成員在子類中變為protected成員,其它成員不變
一般情況下,隻使用public繼承。
不同的繼承方式和通路級别總結如下表:
繼承方式\父類成員通路級别 | public | protected | private |
---|---|---|---|
使用 using 關鍵字可以改變父類成員在子類中的通路權限,例如将 public 改為 private、将 protected 改為 public。但是using 隻能改變父類中 public 和 protected 成員的通路權限,不能改變 private 成員的通路權限,因為父類中 private 成員在子類中是不可見的,根本不能使用,是以父類中的 private 成員在子類中無論如何都不能通路。 示例如下:
class Parent
{
public:
int a;
protected:
char *name;
private:
double num;
}
class Child : public Parent
{
public:
using Parent::name; //将protected改為public
private:
using Parent::a; //将public改為private
}
13.2 繼承的構造函數和析構函數
子類中的構造函數需要對從父類中繼承的成員進行初始化,對于父類中的private對象,需要子類調用父類的構造函數來完成初始化。
子類對象建立時,構造函數的調用順序為:1)調用父類的構造函數;2)調用自身的構造
注意:子類構造函數中隻能調用直接父類的構造函數,不能調用間接父類的。
例如:假設A——>B——>C。
以上面的 A、B、C 類為例,C 是最終的子類,B 就是 C 的直接父類,A 就是 C 的間接父類。
2.析構函數
析構函數調用的調用順序與構造函數相反。
class A
{
public:
A()
{
cout << "A()"<< endl;
}
~A()
{
cout << "~A()"<< endl;
}
};
class B : public A
{
public:
B()
{
cout << "B()"<< endl;
}
~B()
{
cout << "~B()"<< endl;
}
};
class C : public B
{
public:
C()
{
cout << "C()"<< endl;
}
~C()
{
cout << "~C()"<< endl;
}
};
int main()
{
C c;
return 0;
}
/*運作結果如下:
*A()
*B()
*C()
*~C()
*~B()
*~A()
*/
13.3 繼承時對象的記憶體模型
當發生類的繼承關系時,子類的記憶體模型可以看成是父類成員變量和子類新增成員變量的總和,其中記憶體分布時父類對象排在前面,子類對象排在後面。示例如下:
class A
{
public:
int m_a;
int m_b;
...
};
class B: public A
{
public:
int m_c;
...
};
class C: public B
{
public:
int m_d;
...
};
int main()
{
C obj_c;
}
上述示例中,假設 obj_c 的起始位址為 0X1200,那麼它的記憶體分布如下圖所示:
如果有成員變量遮蔽,仍然會留在子類對象的記憶體中,示例如下:
class A
{
public:
int m_a;
int m_b;
...
};
class B: public A
{
public:
int m_c;
...
};
class C: public B
{
public:
int m_b; //遮蔽A類的成員變量
int m_c; //遮蔽B類的成員變量
int m_d;
...
};
int main()
{
C obj_c;
}
上述示例中,假設 obj_c 的起始位址為 0X1300,那麼它的記憶體分布如下圖所示:
13.4 多重繼承和虛繼承
1.多重繼承
C++支援多重繼承,即一個子類可以有兩個或多個父類,子類繼承所有父類的成員函數,子類對象可以當作任意父類對象使用(多重繼承容易讓代碼邏輯複雜、思路混亂,一直備受争議,中小型項目中較少使用),多重繼承示例如下:
class D: public A, private B, protected C
{
...
}
//D 是多重繼承形式的子類,它以公有的方式繼承 A 類,以私有的方式繼承 B 類,以保護的方式繼承 C 類。
多重繼承形式下的構造函數和單繼承形式基本相同,隻是要在子類的構造函數中調用多個父類的構造函數。父類構造函數的調用順序和和它們在子類構造函數中出現的順序無關,而是和聲明子類時父類出現的順序相同。析構函數的執行順序則和構造函數的執行順序相反,例如:
class D: public A, private B, protected C
{
public:
D(形參清單): B(實參清單), C(實參清單), A(實參清單)
{
...
}
}
//上述代碼中根據聲明子類時父類出現的順序,先調用 A 類的構造函數,再調用 B 類構造函數,最後調用 C 類構造函數。
當兩個或多個基類中有同名的成員時,如果直接通路該成員,就會産生命名沖突,編譯器不知道使用哪個父類的成員。這個時候需要在成員名字前面加上類名和域解析符
::
,以顯式地指明到底使用哪個類的成員,消除二義性。
2.虛繼承和虛基類
多重繼承會引發各種各樣的問題,例如,發生菱形繼承時,可能産生備援的成員,如下圖所示:
類 A 派生出類 B 和類 C,類 D 繼承自類 B 和類 C,這個時候類 A 中的成員變量和成員函數繼承到類 D 中變成了兩份,通路時也會産生歧義。
為了解決多重繼承時的命名沖突和備援資料問題,C++提出了虛繼承,使得在子類中隻保留一份間接父類的成員。在繼承方式前面加上 virtual 關鍵字就是虛繼承,示例如下:
//間接父類A
class A
{
protected:
int m_a;
};
//直接父類B
class B: virtual public A //虛繼承
{
protected:
int m_b;
};
//直接父類C
class C: virtual public A //虛繼承
{
protected:
int m_c;
};
//子類D
class D: public B, public C
{
public:
void seta(int a){ m_a = a; } //正确
void setb(int b){ m_b = b; } //正确
void setc(int c){ m_c = c; } //正确
void setd(int d){ m_d = d; } //正确
private:
int m_d;
};
虛繼承的目的是讓某個類做出聲明,承諾願意共享它的基類。其中,這個被共享的基類就稱為虛基類(Virtual Base Class),本例中的 A 就是一個虛基類。在這種機制下,不論虛基類在繼承體系中出現了多少次,在派生類中都隻包含一份虛基類的成員。但是在實際應用,無法确定使用直接繼承還是虛繼承。
14.多态
14.1 同名函數覆寫
同名覆寫:如果子類中的成員(包括成員變量和成員函數)和父類中的成員同名,那麼就會覆寫從父類繼承過來的成員。在子類中使用該成員時,實際上使用的是子類新增的成員,而不是從父類繼承來的。需要通過域解析符
::
通路父類中同名成員。
指派相容:子類對象可以當作父類對象使用,父類指針和引用也可以作用于子類對象
函數重寫:
①子類可以重定義父類中已經存在的成員函數,這種重定義發生在繼承中,叫做函數重寫。子類可以重寫父類中的函數,父類中被重寫的函數依然會繼承給子類,子類中重寫的函數将覆寫父類中的函數。
②父類指針可以指向子類對象,但是通過父類指針隻能通路子類的成員變量,但是不能通路子類的成員函數。這是因為編譯期間,編譯器隻能根據指針的類型判斷所指向的對象,根據指派相容原理,編譯器認為父類指針指向的是父類對象,是以父類指針隻會調用父類中的同名函數。
③子類指針不能指向父類對象,子類引用也不能引用父類對象,除非進行強制類型轉換。
class A
{
public:
void func()
{
cout << "Parent Class" << endl;
}
};
class B : public A
{
public:
void func()
{
cout << "Child Class" << endl;
}
};
int main()
{
A a;
B b;
a.func();
A *pa = &b; //父類指針指向子類對象
pa->func();
return 0;
}
/*運作結果如下:
*Parent Class
*Parent Class
*/
重寫和重載的差別:
靜态聯編:在程式的編譯期間就能确定具體的函數調用(重載)
動态聯編:在程式實際運作後才能确定具體的函數調用(重寫)
14.2 多态和虛函數
1.多态的概念和意義
C++中,通過父類指針隻能通路子類的成員變量,但是不能通路子類的成員函數。我們期望父類指針指向父類對象則調用父類的成員(包括成員函數和成員變量),指向子類對象則調用子類中的成員。C++中新增了虛函數,虛函數的聲明需要在函數聲明前面增加 virtual 關鍵字。
有了虛函數,父類指針可以按照基類的方式來做事,也可以按照子類的方式來做事,它有多種形态,或者說有多種表現方式,我們将這種現象稱為多态(Polymorphism)。示例如下:
class A
{
public:
virtual void func() //虛函數
{
cout << "Parent Class" << endl;
}
};
class B : public A
{
public:
void func()
{
cout << "Child Class" << endl;
}
};
int main()
{
A a;
B b;
a.func();
A *pa = &b; //父類指針指向子類對象
pa->func();
return 0;
}
/*運作結果如下:
*Parent Class
*Child Class
*/
上述示例中,将func改為虛函數,使得父類指針指向子類對象時,通路的是子類成員。
構成多态的條件
1)必須存在繼承關系;
2)繼承關系中必須有同名的虛函數,并且它們是覆寫關系(函數原型相同)。
3)存在父類的指針,通過該指針調用虛函數。
2.虛函數使用注意事項
1)虛函數是根據指針的指向來調用的,指針指向哪個類的對象就調用哪個類的虛函數。
2)隻需要在虛函數的聲明處加上 virtual 關鍵字,函數定義處可以加也可以不加。
3)為了友善,可以隻将父類中的函數聲明為虛函數,這樣所有子類中具有覆寫關系的同名函數都将自動成為虛函數。
4)當在父類中定義了虛函數時,如果派生類沒有定義新的函數來覆寫此函數,那麼将使用父類的虛函數。
5)隻有子類的虛函數覆寫父類的虛函數(函數原型完全相同)才能構成多态(通過父類指針通路子類函數),例如:
父類虛函數的原型為:virtual void func(),
子類虛函數的原型為:virtual void func(int),
那麼當父類指針p指向子類對象時,語句p -> func(100);将會出錯,而語句p -> func();将調用父類的函數。
6)聲明虛函數的條件: 首先看成員函數所在的類是否會作為父類,然後看成員函數在類的繼承後有無可能被更改功能,如果希望更改其功能的,一般應該将它聲明為虛函數。如果成員函數在類被繼承後功能不需修改,或子類用不到該函數,則不要把它聲明為虛函數。
7)不能是虛函數的函數
構造函數。對于父類的構造函數,它僅僅是在子類構造函數中被調用,這種機制不同于繼承。也就是說,子類不繼承父類的構造函數,将構造函數聲明為虛函數沒有什麼意義。
内聯函數。 内聯函數表示在編譯階段進行函數體的替換操作,而虛函數在運作期間确定,是以不能是虛函數。
靜态函數。靜态函數不屬于對象屬于類,沒有this指針,是以将靜态函數設定為虛函數沒有什麼意義。
友元函數。友元函數不屬于類的成員函數,不能被繼承,是以不能是虛函數。
普通函數。普通函數不是類的成員函數,不存在繼承關系。
14.3 虛函數的實作原理
1.虛函數表和虛指針
當聲明一個虛函數時,編譯器會為該類生成一個虛函數表,虛函數表是存儲成員函數位址的資料結構,類中virtual成員函數會被放入虛函數表中。繼承該類的子類也會生成一個虛函數表,當使用該類定義對象時,會為該類的對象定義一個虛函數指針,指向該類型的虛函數表,這個虛函數指針的初始化是在構造函數中完成的。如果有一個父類指針指向子類對象,那麼當調用虛函數時,就會根據所指真正對象的虛函數表去尋找虛函數的位址,也就可以調用虛函數表中的虛函數,以此實作多态。
C++中一般情況下,空類的大小是1,這是為了讓對象的執行個體能夠互相差別。具體來說,空類同樣可以被執行個體化,并且每個執行個體在記憶體中都有獨一無二的位址,是以,編譯器會給空類隐含加上一個位元組,這樣空類執行個體化之後就會擁有獨一無二的記憶體位址。當該空類作為父類時,該類的大小就優化為0了,子類的大小就是子類本身的大小。
靜态成員存放在靜态存儲區,不占用類的大小, 普通函數也不占用類大小。
但如果類中包含一個虛函數,那麼此時類的大小就變為了4(64位機器是8),因為有虛函數的類對象中都有一個虛函數表指針。
class A {}; //類的大小為1
class B {int a; static int b}; //類的大小為4
class C {virtual void func(){}}; //類的大小為4(64位機器為8)
虛函數指針和虛函數表示例如下:
上圖中,調用成員函數時,首先會判斷該函數是不是虛函數,如果是,編譯器會到目前對象裡去查找虛指針,目的就是查找該虛指針所指向的虛函數表,再在虛函數表裡找到該成員函數的位址,最後通過這個位址調用具體的成員函數。這個過程中涉及到兩個位址,一個是虛指針指向的虛函數表的位址,一個是虛函數表中要查找的對應虛函數的位址。
當發生繼承關系時,子類會拷貝父類的虛函數表,如果子類中有重寫父類中的虛函數,就替換成已經重寫的虛函數位址,如果子類有自身的虛函數,就追加自身的虛函數到虛函數表中。
2.虛函數表和虛函數的存放區域
首先整理一下虛函數表的特征:
- 虛函數表是全局共享的元素,即全局僅有一個,在編譯時就構造完成。
- 虛函數表類似一個數組,類對象中存儲虛指針,指向虛函數表,即虛函數表不是函數,不是程式代碼。
- 虛函數表存儲虛函數的位址,即虛函數表的元素是指向類成員函數的指針,而類中虛函數的個數在編譯時期可以确定,即虛函數表的大小可以确定,即大小是在編譯時期确定的。
在C++中,記憶體模型一般分為五個區域:棧區、堆區、函數區(存放函數體等二進制代碼)、全局靜态區、常量區。
C++中虛函數表位于隻讀資料段(.rodata),也就是C++記憶體模型中的常量區;而虛函數則位于代碼段(.text),也就是C++記憶體模型中的代碼區。
3.析構函數聲明成虛函數
一般要将析構函數聲明成虛函數,這是為了降低記憶體洩露的可能性。例如:一個父類指針指向子類的對象,在使用完準備銷毀時,如果父類析構函數沒有聲明成虛函數,那麼編譯器根據指針類型就會認為目前對象是父類,調用父類的析構函數,而子類的析構函數則沒有被調用。而如果父類析構函數聲明為虛函數,那麼編譯器就會根據實際對象,執行子類的析構函數,再執行父類的析構函數。
4.構造函數調用順序總結
- 虛基類構造函數(被繼承的順序)
- 非虛基類構造函數(被繼承的順序)
- 成員對象構造函數(聲明順序)
- 自己的構造函數
15.抽象類和接口
可以将虛函數聲明為純虛函數,示例如下:
virtual void func(int a, int b) = 0;
純虛函數沒有函數體,隻有函數聲明,在虛函數聲明的結尾加上
=0
,表明此函數為純虛函數。
包含純虛函數的類稱為抽象類(Abstract Class)。抽象類無法執行個體化,也就是無法建立對象。原因很明顯,純虛函數沒有函數體,不是完整的函數,無法調用,也無法為其配置設定記憶體空間。抽象類隻能被繼承并重寫相關函數。一個純虛函數就可以使類成為抽象類,但是抽象類中除了包含純虛函數外,還可以包含其它的成員函數(虛函數或普通函數)和成員變量。
當設計一個類時,如果确認這個類是父類,那就需要考慮它有沒有可能成為一個抽象類。 判斷标準:父類有沒有必要産生對象?如果沒有,就可以設計成一個抽象類。
例如:圖形就是一個抽象的概念,因為圖形有很多種,圓形、矩形或者三角形等等。在現實中需要知道具體的圖形類型才能求面積。是以,如果存在圖形類Shape,那麼它隻是一個概念上的類型,沒有具體的對象。Shape這個類的的作用就是專門用來被繼承,由繼承它的類來實作不同的功能。
有一種特殊的抽象類,叫做接口,滿足如下條件的C++類稱為接口:
- 類中沒有定義任何的成員變量。
- 所有的成員函數都是公有的。
- 所有的成員函數都是純虛函數
16.模闆
16.1 函數模闆
函數模闆是一種特殊的函數,可用不同類型進行調用,看起來和普通函數很相似,差別是,類型可被參數化。
函數模闆文法規則如下:
template <typename T1, typename T2,...>
void func(T1& a, T2& b)
{
...
}
其中,template關鍵字表示聲明這是函數模闆,它後面緊跟尖括号,typename關鍵字表示聲明具體的類型參數。typename關鍵字也可以使用class關鍵字替代,它們沒有任何差別。
從整體上看,
template<typename T>
被稱為模闆頭。模闆頭中包含的類型參數可以用在函數定義的各個位置,包括傳回值、形參清單和函數體,定義了函數模闆後,就可以像調用普通函數一樣來調用它們了。編譯器會根據實參的類型自動推導T的類型(編譯器對函數模闆進行兩次編譯,對模闆進行編譯,對參數替換後的代碼進行編譯)。
函數模闆可以像普通函數一樣被重載,對于多參數函數模闆,可以從左向右顯式的指明部分實參,例如:
//重載函數模闆
template<class T> void func(T &a, T &b);
template<typename T> void func(T a[], T b[], int len);
//顯式指明實參
template<typename T1, typename T2> void func(T1 a)
{
T2 b;
}
func<int>(10); //省略 T2 的類型
func<int, int>(20); //指明 T1、T2 的類型
16.2 類模闆
1.類模闆的定義
C++除了支援函數模闆,還支援類模闆,函數模闆中定義的類型參數可以用在函數聲明和函數定義中,類模闆中定義的類型參數可以用在類聲明和類實作中。類模闆的目的同樣是将資料的類型參數化。
類模闆文法規則如下:
template<typename T1 , typename T2 , ...>
class Test
{
...
};
類模闆隻能顯式指定具體類型,不能自動推導類型,且類模闆必須在頭檔案中定義,聲明和實作必須在同一檔案中。
類模闆定義和使用示例如下所示:
template<typename T1, typename T2> //這裡不能有分号
class Test
{
private:
T1 m_a;
T2 m_b;
public:
T1 getA() const
{
return m_a;
}
T2 getB() const
{
return m_b;
}
};
//建立對象
Test<int, double> t1(1, 1.5);
Test<int, char*> t2(12.4, "Hello Template");
Test<int, double*> *p1 = new Test<int, double>(5, 7.2);
2.類模闆的特例化
類模闆可以被特例化,特例化的本質是執行個體化一個模闆,是模闆的分開實作,而非重載它,特例化不影響參數比對,參數比對都以最佳比對為原則。與函數模闆不同,函數模闆隻支援完全特例化,而在類中,可以對類模闆進行完全特例化,也可以對類模闆進行部分特例化。例如:
template<typename T1, typename T2>
class Test
{
public:
void print()
{
cout << "普通類模闆" << endl;
}
};
//完全特例化
template<>
class Test<int, int> // 當 Test 類模闆的兩個參數都是int時,使用這個實作
{
public:
void print()
{
cout << "完全特例化" << endl;
}
};
//部分特例化
template<typename T>
class Test< T, T > // 當 Test 類模闆的兩個參數類型完全相同時,使用這個實作
{
public:
void print()
{
cout << "部分特例化" << endl;
}
};
int main()
{
Test<int, double> t1;
Test<int, int> t2;
Test<double, double> t3;
t1.print();
t2.print();
t3.print();
return 0;
}
/*運作結果如下:
*普通類模闆
*完全特例化
*部分特例化
*/
17.異常
1.異常的概念
程式的錯誤大緻可以分為三種,分别是文法錯誤、邏輯錯誤和運作時錯誤:
1) 文法錯誤在編譯和連結階段就能發現,隻有 100% 符合文法規則的代碼才能生成可執行程式。文法錯誤是最容易發現、最容易定位、最容易排除的錯誤,程式員最不需要擔心的就是這種錯誤。
2) 邏輯錯誤是說我們編寫的代碼思路有問題,不能夠達到最終的目标,這種錯誤可以通過調試來解決。
3) 運作時錯誤是指程式在運作期間發生的錯誤,例如除數為 0、記憶體配置設定失敗、數組越界、檔案不存在等。異常(Exception)機制就是為解決運作時錯誤而引入的。
C++ 異常處理機制讓我們能夠捕獲運作時錯誤,主要涉及try、catch、throw 三個關鍵字,異常處理流程如下:
抛出(Throw)--> 檢測(Try) --> 捕獲(Catch)
異常的文法如下:
//捕獲異常
try
{
// 可能抛出異常的語句
}
catch(exceptionType variable)
{
// 處理異常的語句
}
//抛出異常
throw exceptionData; //exceptionData為異常資料
throw抛出的異常必須被catch處理,如果目前能夠處理,則程式繼續執行,否則将異常向上傳遞,如果所有函數都無法處理異常,程式将停止執行
2.異常類型及比對規則
catch 關鍵字後面可以定義異常類型,它指明了目前的 catch 可以處理什麼類型的異常,異常類型可以是 int、char、float、bool 等基本類型,也可以是指針、數組、字元串、結構體、類等聚合類型。可以将 catch 看做一個沒有傳回值的函數,當異常發生後 catch 會被調用,并且會接收實參(異常資料)。不同的是,異常類型和 catch 能處理的類型是在運作階段比對的。
一個 try 後面可以跟多個 catch,異常抛出後,自上而下嚴格比對每個catch的類型,可以使用catch(...)的方式捕獲任何異常,例如:
try
{
throw 0;
}
catch(exceptionType variable_1)
{
}
catch(exceptionType variable_2)
{
}
catch(exceptionType_n variable_n)
{
}
catch(...)
{
}
1)catch 在比對異常類型的過程中,也會進行類型轉換,但是這種轉換受到了更多的限制,僅能進行:
- 向上轉型:子類向父類的轉換
- const 轉換:将非 const 類型轉換為 const 類型,例如将 char * 轉換為 const char *
- 數組或函數指針轉換:如果函數形參不是引用類型,那麼數組名會轉換為數組指針,函數名也會轉換為函數指針。
其他的都不能應用于 catch。
2)當有繼承關系時,子類的異常對象可以被父類的catch語句塊抓住。
3)構造函數和析構函數最好不要抛出異常,否則萬一異常處理失敗則容易發生記憶體洩露。
3.exception類
C++語言本身或者标準庫抛出的異常都是 exception 的子類,稱為标準異常(Standard Exception)。可以通過下面的語句來捕獲所有的标準異常:
try
{
//可能抛出異常的語句
}
catch(exception &e)
{
//處理異常的語句
}
exception 類位于 <exception> 頭檔案中,它被聲明為:
class exception
{
public:
exception () throw(); //構造函數
exception (const exception&) throw(); //拷貝構造函數
exception& operator= (const exception&) throw(); //運算符重載
virtual ~exception() throw(); //虛析構函數
virtual const char* what() const throw(); //虛函數
}
what() 函數傳回一個能識别異常的字元串,正如它的名字“what”一樣,可以粗略地告訴你這是什麼異常。不過C++标準并沒有規定這個字元串的格式,各個編譯器的實作也不同,是以 what() 的傳回值僅供參考。
exception類的繼承層次如圖所示:
總結如表所示:
異常名稱 | 說明 | 子類異常名稱 | 子類異常說明 |
---|---|---|---|
logic_error | 邏輯錯誤。 | length_error | 試圖生成一個超出該類型最大長度的對象時抛出該異常,例如 vector 的 resize 操作。 |
domain_error | 參數的值域錯誤,主要用在數學函數中,例如使用一個負值調用隻能操作非負數的函數。 | ||
out_of_range | 超出有效範圍。 | ||
invalid_argument | 參數不合适。在标準庫中,當利用string對象構造 bitset 時,而 string 中的字元不是 0 或1 的時候,抛出該異常。 | ||
runtime_error | 運作時錯誤。 | range_error | 計算結果超出了有意義的值域範圍。 |
overflow_error | 算術計算上溢。 | ||
underflow_error | 算術計算下溢。 | ||
bad_alloc | 使用 new 或 new[ ] 配置設定記憶體失敗時抛出的異常。 | ||
bad_typeid | 使用 typeid 操作一個 NULL指針,而且該指針是帶有虛函數的類,這時抛出 bad_typeid 異常。 | ||
bad_cast | 使用 dynamic_cast 轉換失敗時抛出的異常。 | ||
ios_base::failure | io 過程中出現的異常。 | ||
bad_exception | 這是個特殊的異常,如果函數的異常清單裡聲明了 bad_exception 異常,當函數内部抛出了異常清單中沒有的異常時,如果調用的 unexpected() 函數中抛出了異常,不論什麼類型,都會被替換為 bad_exception 類型 |
18.智能指針
記憶體洩露發生之後,軟體可能會遇到卡死、無響應或者反應很慢等問題, C++中智能指針主要用于解決記憶體洩露問題。智能指針是一個類,用來存儲指向動态配置設定對象的指針,負責自動釋放動态配置設定的對象,防止堆記憶體洩漏。動态配置設定的資源,交給一個類對象去管理,當類對象聲明周期結束時,自動調用析構函數釋放資源。
實作智能指針的關鍵步驟:重載指針特征操作符(->和*);隻能通過類的成員函數重載(智能指針的本質是類的對象);
更多關于智能指針的知識點,将在C++11新特性中總結。
參考:
- 《C++ Primer 第5版》