決定程式呼叫時,將使用哪個可執行**塊,是由編譯器負責的。
將源**中的函式呼叫解釋為執行特定的函式**塊被稱為 函式名聯編。
在c++
語言中,這個過程比在
c語言中更麻煩一些(因為
c++存在函式過載,編譯器要給函式重新命名),編譯器要檢視函式引數及函式名才能確定使用哪個函式(根據匹配的優先順序,一共
4級)。
c/c++編譯器可以在編譯過程中完成這種聯編,在編譯過程中進行聯編被稱為
靜態聯編(static binding),又稱為早期聯編(early binding)。
然而虛函式使這項工作變得更困難(因為指標指向哪個類物件,這並不確定)。因此,編譯器必須生成能夠在程式執行時選擇正確的虛方法的**,這被稱為 動態聯編(dynamic binding),又稱為晚期聯編(late binding)。
指標和引用型別的相容性:
c++中,動態聯編與通過指標和引用呼叫方法相關(就是說,跟指標呼叫哪種方法有關係),從某種程度上來講,這是由繼承控制的。
公有繼承建立is-a
關係的一種方法是,如何處理指向物件的指標和引用。因為:
①一般情況下,乙個型別的指標或引用(如int*
或int&
)是不能指向另乙個型別的(如
double);
②但指向基類的指標和引用,可以指向派生類物件(不需要進行顯式型別轉換)。
將派生類指標、引用轉換為基類引用或指標,被稱為 向上強制轉換(upcasting)。
也就是說,將派生類指標、引用,賦給基類指標、引用之類的行為,就是向上強制轉換,是可以不顯式宣告的。
而將基類指標、引用轉換為派生類的指標、引用,被稱為 向下強制轉換(downcasting
)。向下強制轉換是需要顯示宣告的。
原因在於,基類的方法、資料成員,派生類都有,因此基類的指標、引用能做的事情,派生類也能做,不存在相容問題,因此向上強制轉換是安全的。
但相反,卻存在。
只有顯式的宣告向下強制轉換,才能告訴程式設計師,需要靠程式設計師來確保操作的安全。
隱式向上強制轉換使基類指標或引用,可以指向基類物件或派生類物件,因此需要動態聯編。c++
使用虛成員函式來滿足這種需求。
虛成員函式和動態聯編:
編譯器對非虛方法,使用靜態聯編;
編譯器對虛方法,使用動態聯編。
使用靜態聯編的好處:
①無需額外耗費開銷(如記憶體,處理器效能之類)來跟蹤究竟使用哪一種聯編;
②因此效率會更高。
一般來說,如果要在派生類中重新定義基類的方法,那麼最好把它設定為虛方法(這樣可以適用於指標),否則設定為非虛方法(因為虛方法的意義就在於多型)。
但事實上,設計的時候不一定知道會不會用。因此可能存在幾種情況:
①沒有設定虛方法,派生類也沒用(效率高);
②沒有設定虛方法,派生類用了(不支援多型,根據物件、指標、引用型別決定);
③設定虛方法,派生類沒用(應該是動態聯編,支援多型);
④設定虛方法,派生類用了(動態聯編,多型)。
虛方法的工作原理:
c++規定虛方法行為,而編譯器作者決定實現方法,程式設計師不需要知道虛方法的實現方法就能用。
編譯器處理虛函式的方法為:給每個物件新增乙個隱藏成員,而這個隱藏成員,包含乙個指向 函式位址 陣列的指標,這種陣列被稱為虛函式表(virtual funciton table, ftbl)。虛函式表中儲存了對類物件進行宣告的虛函式的位址。
大概意思就是(不保證完全準確,另外,不同編譯器上可能也有所區別):
①每個類物件,有乙個隱藏的成員,這個隱藏成員是乙個指標。
②這個指標幹嘛的呢,他指向乙個陣列(虛函式表)。
③這個陣列幹嘛的呢,他儲存了若干個函式的位址。
④這些位址哪來的呢,他是該類物件的虛方法的位址(注意,是涉及到虛方法才儲存,非虛方法是不儲存的,例如基類物件儲存了其所有虛方法的位址)。
⑤假如這個類物件的類是派生類,那麼首先他儲存了基類所有虛方法的位址(這是肯定的),但若基類的某個方法是虛方法,派生類重新定義了這個方法,那麼他就不儲存這個基類的方法的位址了,改儲存派生類方法的位址。
⑥如果基類指標指向這個派生類物件,那麼就在使用類方法的時候,就會呼叫這個物件的隱藏成員,然後找到虛函式表。然後根據虛函式表的位址,來找使用哪個虛方法。
例如,(6.1
)使用某個基類和派生類都有的虛方法,表中儲存的是派生類的,於是呼叫之;
(6.2
)如果基類有虛方法,派生類沒有重新定義,那麼表中儲存的是基類的,於是呼叫基類的;
(6.3
)如果基類沒有該方法,派生類有方法,由於基類指標只能使用基類的方法,因此無法使用。
(6.4
)如果基類有方法,但不是虛方法(此時表中沒有儲存該位址),那麼由於是基類指標並沒有儲存該方法的位址,因此自然也不會被派生類的方法替代。於是執行基類的方法。
⑦注意,虛方法面對的物件是指標和引用。因此,這個虛函式表其實也只對指標和引用起作用(物件是根據物件的型別決定呼叫哪個的)。
因此,使用虛函式,在記憶體和執行速度方面有一定的成本,包括:
①每個物件都增大,增大量為儲存位址的空間。
②對每個類,編譯器都建立乙個虛函式位址表(陣列);
③對每個函式呼叫,都將執行一項額外的操作,即到表中查詢位址。
雖然非虛函式的效率比虛函式稍高,但不具有動態聯編的功能。
有關虛函式的注意事項:
虛函式的一些特點:
①在基類方法的宣告中使用關鍵字virtual
,可使該方法在基類、以及所有的派生類、還有派生類的派生類中,是虛的;
②如果使用指向物件的引用或指標來呼叫虛方法,程式將根據其指向的物件(而不是指標、引用的型別)來決定呼叫的方法。
③如果定義的類被用作基類,那麼應將那些要被重新定義的類方法,定義為虛方法。
虛函式需要注意一些內容:
①建構函式不能是虛函式。
因為給基類建立物件時,宣告派生類的資料成員並沒有意義(比如基類沒有int a
,派生類有,呼叫派生類的物件則會生成
int a
,但是這對基類並沒有意義)
而給派生類建立物件時,自動會呼叫基類的建構函式,因此也不需要通過虛方法來呼叫(何況向下強制轉換並不好)。
②析構函式需要是虛函式。
例如基類指標new
分配乙個派生類的物件:
brass *q = new
brass_plus(one);
這個時候,指標指向的物件是派生類的。
如果delete
,假如是非虛析構函式,那麼則執行的是基類的析構函式(顯然是不對的)。我們如果想讓他執行派生類的析構函式,則需要是虛析構函式,才能呼叫派生類的析構函式。
③友元不能是虛函式。
因為只有成員函式才能是虛函式。友元不是類成員,所以不是虛函式。
基類的友元函式 不是 派生類的友元函式,只是使用友元函式時,將派生類轉換為了基類。
如果因為這個原因引起了設計問題,可以通過讓友元函式使用虛成員函式來解決。
——不懂,意思是讓友元函式呼叫虛成員函式麼?
④沒有重新定義
如果派生類沒有重新定義函式,那麼將使用該函式的基類版本。
如果派生類位於派生鏈中,則將使用最新的虛函式版本(基類a
——》派生
b——》派生
c,如果
c沒有定義,那麼使用
b定義的虛函式版本,不過前提應該是
a類指標指向
c類物件)。
例外的情況是基類版本是隱藏的(貌似指的是派生類同名函式隱藏了基類的同名函式)
⑤重新定義將隱藏方法
假如基類有虛方法void show();
派生類有虛方法void show();
那麼毫無疑問,派生類的虛方法將替代基類的虛方法。
但若派生類的虛方法變為void show(int a);
這並不會產生過載函式,而是隱藏掉基類同名的基類虛方法(無視特徵標),因此,也不會產生過載函式(無引數和有引數兩個版本)。
因此,產生兩個結論:
(1)如果重新定義繼承的方法,應確保與原來的原型完全一致(這是多型的意義),但如果返回值是基類引用或指標,則可以修改為返回指向派生類的引用或指標。
這種特性被稱為:返回型別協變,因為允許返回型別隨類型別的變化而變化。
但注意:只適合返回值,不適合引數(因為不同類作為引數對私有成員的訪問許可權不同)
(2)如果基類宣告被過載了,並且某乙個是虛函式,在派生類被定義了,那麼則應在派生類重新定義所有的基類版本(因為同名的過載都被隱藏了)。
不過如果沒特殊需求的話,可以定義需要定義的過載版本,其他版本可以使用基類的(假如需要顯示的和基類的一樣的話)。
使用方法有兩種:強制轉換為基類型別(對類物件使用),或者在派生類的函式定義中,呼叫基類的函式定義。
靜態聯編和動態聯編
聯編是指乙個電腦程式自身彼此關聯 使乙個 源程式經過編譯 連線,成為乙個可執行程式 的過程,在這個聯編過程中,需要確定程式中的操作呼叫 函式呼叫 與執行該操作 函式 的 段之間的對映關係,按照聯編所進行的階段不同,可分為靜態聯編和動態聯編。靜態聯編 呼叫函式和被調函式在程式編譯時,他們在記憶體中的位...
靜態聯編和動態聯編
聯編就是將模組或者函式合併在一起生成可執行 的處理過程,同時對每個模組或者函式呼叫分配記憶體位址,並且對外部訪問也分配正確的記憶體位址,它是電腦程式彼此關聯的過程。按照聯編所進行的階段不同,可分為兩種不同的聯編方法 靜態聯編和動態聯編。靜態聯編是指在編譯階段就將函式實現和函式呼叫關聯起來,因此靜態聯...
靜態聯編和動態聯編
聯編就是將模組或者函式合併在一起生成可執行 的處理過程,同時對每個模組或者函式呼叫分配記憶體位址,並且對外部訪問也分配正確的記憶體位址,它是電腦程式彼此關聯的過程。按照聯編所進行的階段不同,可分為兩種不同的聯編方法 靜態聯編和動態聯編。靜態聯編是指在編譯階段就將函式實現和函式呼叫關聯起來,因此靜態聯...