多型按字面的意思就是多種形態。當類之間存在層次結構,並且類之間是通過繼承關聯時,就會用到多型。
C++ 多型意味着呼叫成員函數時,會根據呼叫函數的物件的型別來執行不同的函數。爲了更爲詳細的說明多型,此處我們劃分爲**靜態多型 **和 **動態多型 ** 兩種狀態來講解
靜態多型是編譯器在編譯期間完成的,編譯器會根據實參型別來選擇呼叫合適的函數,如果有合適的函數可以呼叫就調,沒有的話就會發出警告或者報錯 。 該種方式的出現有兩處地方: **函數過載 ** 和 泛型程式設計| 函數模板
int Add(int a, int b){
return a + b;
};
double Add(double a, double b)
{
return a + b;
};
int main()
{
Add(10, 20);
Add(10.0,20.0); //正常程式碼
return 0;
}
它是在程式執行時根據父類別的參照(指針)指向的物件來確定自己具體該呼叫哪一個類的虛擬函式。
A *a = new A();
Father * f = new Fahter();
Father * f2 = new Child();
動態多型的必須滿足兩個條件:
- 父類別中必須包含虛擬函式,並且子類中一定要對父類別中的虛擬函式進行重寫。
- 通過父類別物件的指針或者參照呼叫虛擬函式。
通常情況下,如果要把一個參照或者指針系結到一個物件身上,那麼要求參照或者指針必須和物件的型別一致。 不過在繼承關係下,父類別的參照或指針可以系結到子類的物件,這種現象具有欺騙性,因爲在使用這個參照或者指針的時候,並不清楚它所系結的具體型別,無法明確是父類別的物件還是子類的物件。
int *p = new int(3);
繼承關係:
father *p = new father();
child *c = new child();
//繼承的時候,有一種特殊情況:
father *p2 = new child():
只有在繼承關係下,才需要考慮靜態和動態型別,這裏僅僅是強調型別而已。所謂的
靜態型別
指的是,在編譯時就已經知道它的變數宣告時對應的型別是什麼。而動態型別
則是執行的時候,數據的型別才得以確定。只有在
參照
或者指針
場景下,才需要考慮 靜態或者動態型別。因爲非參照或者非指針狀態下,實際上發生了一次拷貝動作。father *f2 = new son();
//靜態型別:不需要執行,編譯狀態下,即可知曉 a,b的型別。
int a = 3;
string b = "abc";
//動態型別:
//f在編譯時,型別是Father ,但在執行時,真正的型別由getObj來決定。目前不能明確getObj返回的是Father的物件還是Child的物件。
Child getObj(){
Child c ;
return c;
};
Father &f = getObj();
父類別的參照或指針可以系結到子類的物件 , 那麼在存取同名函數時,常常出現意想不到的效果。
class father{
public:
void show(){
cout << "father show" << endl;
}
};
class children : public father{
public:
void show(){
cout << "children show" << endl;
}
};
int main(){
father f = children();
f.show(); // 列印father show
}
程式呼叫函數時,到底執行哪一個程式碼塊,由編譯器來負責回答這個問題。將原始碼中的函數呼叫解釋爲執行特定的函數程式碼,稱之爲
函數名聯編
。 在C語言裏面,每個函數名都對應一個不同的函數。但是由於C++裏面存在過載的緣故,編譯器必須檢視函數參數以及函數名才能 纔能確定使用哪個函數,編譯器可以在編譯階段完成這種聯編,在編譯階段即可完成聯編也被稱爲:靜態聯編 | 早期聯編
。 程式在執行期間才決定執行哪個函數,這種稱之爲動態聯編 | 晚期聯編
class WashMachine{
public:
void wash(){
cout << "洗衣機在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash(){
cout << "智慧洗衣機在洗衣服" << endl;
}
};
int main(){
WashMachine *w1= new WashMachine(); //父類別指針指向父類別物件 列印:洗衣機在洗衣服
w1->wash();
SmartWashMachine *s = new SmartWashMachine(); //子類指針指向子類物件 列印: 智慧洗衣機...
s->wash();
WashMachine *w2 = new SmartWashMachine(); //父類別指針指向子類物件 列印:洗衣機在洗衣服
w2->wash();
return 0 ;
}
動態聯編在處理子類重新定義父類別函數的場景下,確實比靜態聯編好,靜態聯編只會無腦的執行父類別函數。但是不能因此就否定靜態聯編的作用。動態聯編狀態下,爲了能夠讓指針順利存取到子類函數,需要對指針進行跟蹤、這需要額外的開銷。但是並不是所有的函數都處於繼承狀態下,那麼此時靜態聯編更優秀些。
編寫c++程式碼時,不能保證全部是繼承體系的父類別和子類,也不能保證沒有繼承關係的存在,所以爲了囊括兩種情況,c++ 才提供了兩種方式。
正所謂兩害相權取其輕,考慮到大部分的函數都不是處在繼承結構中,所以效率更高的靜態聯編也就成了預設的的選擇。
做這樣的設計是出於什麼目的?
C++中的虛擬函式的作用主要是實現了多型的機制 機製 , 有了虛擬函式就可以在父類別的指針或者參照指向子類的範例的前提下,然後通過父類別的指針或者參照呼叫實際子類的成員函數。這種技術讓父類別的指針或參照具備了多種形態。定義虛擬函式非常簡單,只需要在函數宣告前,加上
virtual
關鍵字即可。 在父類別的函數上新增 virtual 關鍵字,可使子類的函數也變成虛擬函式。如果基礎類別指針指向的是一個基礎類別物件,則基礎類別的虛擬函式被呼叫
如果基礎類別指針指向的是一個派生類物件,則派生類的虛擬函式被呼叫。
class WashMachine{
public:
virtual void wash(){
cout << "洗衣機在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
virtual void wash(){
cout << "智慧洗衣機在洗衣服" << endl;
}
};
int main(){
WashMachine *w2 = new SmartWashMachine(); //父類別指針指向子類物件 列印..洗衣機在洗衣服
w2->wash();
return 0 ;
}
瞭解虛擬函式的工作原理,有助於理解虛擬函式。
通常情況下,編譯器處理虛擬函式的方法是: 給每一個物件新增一個隱藏指針成員,它指向一個數組,數組裏面存放着物件中所有函數的地址。這個陣列稱之爲虛擬函式表(virtual function table ) 。表中儲存着類物件的虛擬函式地址。
父類別物件包含的指針,指向父類別的虛擬函式表地址,子類物件包含的指針,指向子類的虛擬函式表地址。
如果子類重新定義了父類別的函數,那麼函數表中存放的是新的地址,如果子類沒有重新定義,那麼表中存放的是父類別的函數地址。
若子類有自己虛擬函式,則只需要新增到表中即可。
建構函式不能是虛擬函式。因爲虛擬函式的作用是標註在父子類同名方法上,父類別和子類的構造並不是同名方法。並且虛擬函式是用於控制在父類別參照 | 指針指向子類物件時,決定呼叫父類別還是子類的同名函數。
class father{
public:
virtual father(){ //報錯!
cout <<"父親建構函式~!~" << endl;
}
}
在繼承體系下, 如果父類別的指針可以指向子類物件,這就導致在使用
delete
釋放記憶體時,卻是通過父類別指針來釋放,這會導致父類別的解構函式會被執行,而子類的解構函式並不會執行,此舉有可能導致程式結果並不是我們想要的。究其原因,是因爲靜態聯編的緣故,在編譯時,就知道要執行誰的解構函式。爲了解決這個問題,需要把父類別的解構函式變成虛擬解構函式,也就是加上
virtual
的定義。一旦父類別的解構函式是虛擬函式,那麼子類的解構函式也將自動變成虛擬函式。概括: 繼承關係下,所有人的構造都不能是虛擬函式,並且所有人的解構函式都必須是虛擬函式。
只要在父親的解構函式加上 virtual ,那麼所有的解構函式都變成 虛擬函式
class WashMachine{
public:
virtual ~WashMachine(){
cout << "執行父類別解構函式" << endl;
}
};
class SmartWashMachine : public WashMachine{
~SmartWashMachine(){
cout << "執行子類解構函式" << endl;
}
};
int main(){
WashMachine *w = new SmartWashMachine(); //父類別指針指向子類物件
delete w; //會執行父類別的解構函式
return 0 ;
}
在父類別的函數上新增 virtual 關鍵字,可使子類的函數也變成虛擬函式。
一旦形成父類別參照 或者 指針指向子類物件時,呼叫同名的虛擬函式,執行的是子類的虛擬函式,這是因爲存在動態聯編。 預設情況下,如果沒有virtual關鍵設定,那麼執行的是父類別的函數。
如果某個類被宣告成父類別,那麼那些要在子類重新定義的函數,應該宣告爲虛擬函式。 基本處理是:只要是同名的函數,都應該加上virtual關鍵字。
建構函式不能是虛擬函式,應該把父類別的解構函式變成虛擬函式。
在繼承關係下,子類可以重寫父類別的函數,但是有時候擔心程式設計師在編寫時,有可能因爲粗心寫錯程式碼。所以在C++ 11中,推出了
override
關鍵字,用於表示,子類的函數就是重寫了父類別的同名函數 。 不過值得注意的是,override
標記的函數,必須是虛擬函式。
override
的用意並不會影響程式的執行結果,僅僅是作用於編譯階段,用於檢查子類是否真的重寫父類別函數
class WashMachine{
public:
virtual void wash(){
cout << "洗衣機在洗衣服" << endl;
}
};
class SmartWashMachine : public WashMachine{
public:
void wash() override{ //表示重寫父類別的函數
cout << "智慧洗衣機在洗衣服" << endl;
}
};
在c++11 推出了final關鍵字,其作用有兩個: (1)、禁止虛擬函式被重寫;(2)、禁止類被繼承。
注意: 只有虛擬函式才能 纔能被標記爲final ,其他的普通函數無法標記final。
class Person final{ //表示該類是最終類,無法被繼承
};
class Student : public Person{}; //編譯錯誤
//===============================
class Person {
virtual void run() final{ //表示該方法時最終方法,無法被重寫
}
};
class Student : public Person{
void run(){ //錯誤
}
};
純虛擬函式是一種特殊的虛擬函式,C++中包含純虛擬函式的類,被稱爲是「抽象類」。抽象類不能使用new出物件,只有實現了這個純虛擬函式的子類才能 纔能new出物件。C++中的純虛擬函式更像是「只提供宣告,沒有實現」,是對子類的約束。
純虛擬函式就是沒有函數體,同時在定義的時候,其函數名後面要加上「= 0」。
例如洗衣機具有特徵是洗衣服,但洗衣機不會去洗,而是特定洗衣機,例如海爾滾筒式洗衣機纔會去洗,而且也必須存在洗衣服的功能(怎麼洗,如何洗),所以此時要繼承洗衣機這個抽象類,
class WashMachine{
public:
//沒有函數體,表示洗衣機能洗衣服,但是具體怎麼洗,每個品牌不一樣
virtual void wash() = 0;
};
class HaierMachine:public WashMachine{
public :
virtual void wash(){
cout << "海爾牌洗衣機在洗衣服" << endl;
}
};
class LittleSwanMachine:public WashMachine{
public :
virtual void wash(){
cout << "小天鵝洗衣機在洗衣服" << endl;
}
};
int main(){
//WashMachine w; 錯誤,抽象類無法建立物件
WashMachine *w1 = new HaierMachine() ;
WashMachine *w2 = new LittleSwanMachine() ;
return 0 ;
}
- 如果有一個類當中有純虛擬函式,那麼這個類就是抽象類
- 抽象類是無法建立物件的,因爲一旦能夠建立物件,裏面的純虛擬函式沒有函數體,也就不知道要執行什麼邏輯了,所以禁止抽象類建立物件。
- 抽象類當中也可以有普通的成員函數,雖然父類別不能建立物件,但是子類可以建立,所以這些函數可以由子類存取。
- 如果一個子類繼承了一個父類別(父類別是抽象類),那麼子類就必須重寫所有的純虛擬函式,否則視子類爲抽象類,因爲繼承體系下,等同於子類擁有了和父類別一樣的程式碼。
所謂介面,其實就是用於描述行爲和功能,並不會給出具體的實現。C++中沒有提供類似
interface
這樣的關鍵字來定義介面 , 純虛擬函式往往承擔起了這部分功能。抽象類可以用來定義一種事物的行爲特徵,例如:洗衣機: 洗衣服。
class Person{
Person() {}; // 可以用於初始化成員函數
virtual ~IPerson(){} //防止子類解構函式無法被呼叫問題
//每個人吃什麼,做什麼都不一樣,,即可宣告爲純虛擬函式
virtual void eat() = 0 ;
virtual void work() = 0 ;
...
};
c++ 把記憶體的控制權對程式設計師開放,讓程式顯式的控制記憶體,這樣能夠快速的定位到佔用的記憶體,完成釋放的工作。但是此舉經常會引發一些問題,比如忘記釋放記憶體。由於記憶體沒有得到及時的回收、重複利用,所以在一些c++程式中,常會遇到程式突然退出、佔用記憶體越來越多,最後不得不選擇重新啓動來恢復。造成這些現象的原因可以歸納爲下面 下麪幾種情況:
- 野指針: 記憶體已經被釋放、但是指針仍然指向它。這時記憶體有可能被系統重新分配給程式使用,從而會導致無法估計的錯誤
- 重複釋放:程式試圖釋放已經釋放過的記憶體,或者釋放已經被重新分配過的記憶體,就會導致重複釋放錯誤.
- 記憶體漏失: 不再使用的記憶體,並沒有釋放,或者忘記釋放,導致記憶體沒有得到回收利用。 忘記呼叫delete
隨着多執行緒程式的廣泛使用,爲了避免出現上述問題,c++提供了智慧指針,並且c++11對c++98版本的智慧指針進行了修改,以應對實際的應用需求。
在98版本提供的
auto_ptr
在 c++11得到刪除,原因是拷貝是返回左值、不能呼叫delete[] 等。 c++11標準改用unique_ptr
|shared_ptr
|weak_ptr
等指針來自動回收堆中分配的記憶體。智慧指針的用法和原始指針用法一樣,只是它多了些釋放回收的機制 機製罷了。智慧指針位於 標頭檔案中,所以要想使用智慧指針,還需要匯入這個標頭檔案
#include<memory>
unique_ptr
是一個獨享所有權的智慧指針,它提供了嚴格意義上的所有權。也就是隻有這個指針能夠存取這片空間,不允許拷貝,但是允許移動(轉讓所有權)。
unique_ptr<int> p(new int(10)); //指針p無法被複制
unique_ptr<int> p2 = p ; //錯誤
cout << *p << endl; //依然可以使用 解除參照獲取數據。
unique_ptr<int> p3 = move(p) ; // 正確,至此,p將不再擁有控制權。
cout << *p3 << endl; // p3 現在是唯一指針
cout << *p << endl; // p 現在已經無法取值了。
p3.reset(); // 可以使用reset 顯式釋放記憶體。
p3.reset(new int(6));
p3.get() ; // 可以獲取到指針存放的地址值。
shared_ptr
: 允許多個智慧指針共用同一塊記憶體,由於並不是唯一指針,所以爲了保證最後的釋放回收,採用了計數處理,每一次的指向計數 + 1 , 每一次的reset會導致計數 -1 ,直到最終爲0 ,記憶體纔會最終被釋放掉。 可以使用use_cout
來檢視目前的指針個數
shared_ptr<int> s1(new int(3));
shared_ptr<int> s2 = s1;
s1.reset();
s2.reset(); // 至此全部解除指向 計數爲0 。
cout << *s1 << endl; //無法取到值
對於參照計數法實現的計數,總是避免不了回圈參照(或環形參照)的問題,即我中有你,你中有我,
shared_ptr
也不例外。 下面 下麪的例子就是,這是因爲f和s內部的智慧指針互相指向了對方,導致自己的參照計數一直爲1,所以沒有進行解構,這就造成了記憶體漏失。
class son;
class father {
public:
father(){cout <<"father 構造" << endl;}
~father(){cout <<"father 解構" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 構造" << endl;}
~son(){cout <<"son 解構" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
shared_ptr<father> father;
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
爲了避免
shared_ptr
的環形參照問題,需要引入一個弱指針weak_ptr,它指向一個由
shared_ptr管理的物件而不影響所指物件的生命週期,也就是將一個
weak_ptr系結到一個
shared_ptr不會改變
shared_ptr的參照計數。不論是否有
weak_ptr指向,一旦最後一個指向物件的
shared_ptr被銷燬,物件就會被釋放。從這個角度看,
weak_ptr更像是
shared_ptr`的一個助手而不是智慧指針。
class father {
public:
father(){cout <<"father 構造" << endl;}
~father(){cout <<"father 解構" << endl;}
void setSon(shared_ptr<son> s) {
son = s;
}
private:
shared_ptr<son> son;
};
class son {
public:
son(){cout <<"son 構造" << endl;}
~son(){cout <<"son 解構" << endl;}
void setFather(shared_ptr<father> f) {
father = f;
}
private:
//shared_ptr<father> father;
weak_ptr<father> father; //替換成weak_ptr 即可。
};
int main(){
shared_ptr<father> f(new father());
shared_ptr<son> s(new son());
f->setSon(s);
s->setFather(f);
}
在C++中記憶體分爲5個區,分別是
堆
、棧
、全域性/靜態儲存區
和程式碼|常數儲存區
|共用記憶體區
。棧區:又叫堆疊,儲存非靜態區域性變數、函數參數、 返回值等,棧是可以向下生長的
共用記憶體區:是高效的I/O對映方式,用於裝載一個共用的動態記憶體庫。使用者可使用系統介面建立共用共用內 存,做進程間通訊
堆區:用於程式執行時動態記憶體分配,堆是可以向上增長的
靜態區:儲存全域性數據和靜態數據
程式碼區:儲存可執行的程式碼、只讀常數
- 從棧上分配:在執行函數時,函數內區域性變數的儲存單元都可以在棧上建立,函數執行結束時這些儲存單元自動被釋放。棧記憶體分配運算內建於處理器的指令集中,效率很高,但是棧本身的容量有限
- 從堆上分配:動態成員儲存位置,程式在執行的時候用
malloc
或new
申請任意多少的記憶體空間,程式設計師自己負責在何時用free
或delete
釋放記憶體.動態記憶體的生存期由使用者決定,使用非常靈活,但問題也最多。- 從靜態區上分配:記憶體在程式編譯的時候就已經分配好,這塊記憶體在程式的整個執行期間都存在.例如全域性變數、static變數
在 c++ 中 , 如果要在堆記憶體中申請空間,那麼需要藉助
new
操作符,釋放申請的空間,使用delete
操作 。而c語言使用的是malloc
和free
,實際上new
和delete
的底層實際上就是malloc
和free
。
在c++中, new是一個關鍵字,同時也是一個操作符,用於在堆區申請開闢記憶體 , new的操作還具備以下幾個特徵:
- 記憶體申請成功後,會返回一個指向該記憶體的地址。
- 若記憶體申請失敗,則拋出異常,
- 申請成功後,如果是程式設計師定義的型別,會執行相應的建構函式
int *a = new int();
stu *s = new stu();
//new的背後先建立
在c++中,
delete
和new
是成對出現的,所以就有了no new no delete
的說法。delete
用於釋放new
申請的記憶體空間。delete
的操作具備以下幾個特徵:
- 如果指針的值是0 ,delete不會執行任何操作,有檢測機制 機製
- delete只是釋放記憶體,不會修改指針,指針仍然會指向原來的地址
- 重複delete,有可能出現異常
- 如果是自定義型別,會執行解構函式
int *p = new int(6);
delete p ; // 回收數據
*p = 18 ; //依然可以往裏面存值,但是不建議這麼做。
malloc
和 free 實際上是C語言 申請記憶體的語法,在C++ 也得到了儲存。只是與 new 和 delete 不同的是, 它們 是函數,而 new 和 delete是作爲關鍵字使用。 若想使用,需要匯入#include<stdlib.h>
- malloc 申請成功之後,返回的是void型別的指針。需要將void*指針轉換成我們需要的型別。
- malloc 要求制定申請的記憶體大小 , 而new由編譯器自行計算。
- 申請失敗,返回的是NULL , 比如: 記憶體不足。
- 不會執行自定義型別的建構函式
int *p=(int *)malloc(int); //如果申請失敗,返回的是NULL
free 和 malloc是成堆出現的,所以也有了 no malloc no free的說法。 free 用於釋放 mallo申請的記憶體空間。
- 如果是空指針,多次釋放沒有問題,非空指針,重複釋放有問題
- 不會執行對應的解構
- delete的底層執行的是free
free (p);
在申請動態記憶體時,new有一些靈活上的缺陷,其中一方面是它將記憶體分配和物件構造系結在一起,delete將物件的解構和記憶體釋放系結到一起。如果是單個物件,那麼這無可厚非。 但是如果分配一大片記憶體時,通常都想按需去構造物件,而不是把整個空間都構造了。避免了記憶體的浪費。
如下:開闢了10個空間,但是隻使用了3個位置。
//動態陣列:
stu *s = new stu[5]; //分配5個位置,打算儲存5個學生物件
s[0] = son()
allocator 是定義在 標頭檔案 中的一個類, 它可以做到將記憶體分配和物件構造分離出來。它分配的記憶體是原始的,未構造的。與vector相似,它也是一個模板類,所以在使用的時候,需要制定分配記憶體的型別是什麼型別。它會根據給定物件的型別來確定恰當的記憶體大小。分配成功後,返回一個指向記憶體區域的一個指針。
allocator | 定義一個名爲a的allocator物件,它可以爲型別爲T的物件分配記憶體 |
---|---|
a.allocate(n) | 分配一段連續的爲構造的記憶體,能容納n個型別爲T的物件 |
a.deallocate(p, n) | 釋放從指針p中地址開始的記憶體,這塊記憶體儲存了n個型別爲T的物件。p必須是以個先前有allocate返回的指針,而且n必須是建立p時所要求的大小。在呼叫deallocate以前,使用者必須對每個在這塊記憶體中建立的物件呼叫destroy |
a.construct(p, args) | p即分配記憶體返回的指針,可以通過指針運算進行偏移,args 即構造物件使用的參數,用來在p指向的記憶體塊中構造一個物件。 |
a.destroy§ | p爲型別爲T的指針,對p指向的物件執行解構函式。 |
//建立allocator物件
allocator<son> sa;
//申請分配一塊5個連續的記憶體地址
son *p = sa.allocate(5);
//往第一個位置構造一個son
sa.construct(p , "張三",18); //第一個位置
sa.construct(p+1 , "李四",19); //第二個位置
sa.construct(p+4 , "王五",19); //第5個位置
//銷燬物件,執行解構函式
sa.destroy(p);
sa.destroy(p+1);
sa.destroy(p+4);
//釋放記憶體,標記這些記憶體可用
sa.deallocate(p , 5);