C++Primer(類繼承)

一、派生類和基類之間的三種關係

1、派生類對象可以使用基類中不是私有限制的方法

2、 基類的指針可以在不用轉換的情況下指向派生類對象。

3、基類的引用變量可以在不用轉換的情況下指向派生類的對象


只能夠把派生類的對象賦值給基類的引用或者地址賦值給基類的指針,但是反過來的話不行,是單向的。

引用的兼容性可以允許你用派生類的對象去初始化基類的對象,其中的操作就是把派生類對象中數據基類對象的那部分成員用來初始化基類對象的成員。在進行初始化的時候會調用相應的複製構造函數來完成操作。因爲在創建派生類對象的時候也會創建一個相應的基類的對象,這種初始化方式,正是用生成派生類對象時的基類對象來初始化的。


二、多態性

多態性一直以來很抽象,但是仔細理解起來感覺只有一句話概括的很準確“一個方法根據調用他對象的不同而展現出不同的行爲和功能”。

多態性主要體現在類的繼承上面。實現多態性有兩點:

1、在派生類中重新定義一個基類中的方法

2、在基類中定義一個虛函數,在派生類中根據自己的需求自己實現。


之前在MFC的C++中的知識碰到過這樣一種情況,父類和子類同有一個同名的函數,但是實現不同。把一個派生類的對象的地址賦值給基類的指針,結果調用這個同名的方法的時候,調用的是基類的方法。也就是說,如果父類和子類中都有一個同名的函數的話,如果這個函數沒有被定義爲虛函數,那麼在調用的時候將根據指針類型來選擇的方法,但是如果定義爲一個虛函數的時候,將根據指針指向的對象來確定相應的方法來調用。


有的時候會將基類的析構函數也聲明爲虛函數,這是爲了在刪除子類對象的時候,如果遇到上面那種情況,可以調用子類的析構函數。



三、靜態連編和動態連編

在源代碼中的函數調用解釋爲執行特定的函數代碼塊被稱爲函數名連編。靜態連編髮生在程序的編譯階段,動態連編髮生在程序的運行階段。


對於一些基本的數據類型來說,C++禁止不同類型的指針和地址進行賦值,比如我要把一個double的數據的地址賦值給一個int類型的指針,這是錯誤的,但是在類裏面是可以的。

將派生類的引用和指針轉換爲基類的引用和指針叫做向上強制類型轉換,是不用顯式調用的,在賦值的時候就會自動調用。反過來就叫做向下的強制類型的轉換,向下的強制類型轉換是需要顯式調用的。隱式的向上強制類型轉換需要動態連編的支持,因爲他是在程序運行的時候才能夠確定指向的到底是基類的對象還是派生類的對象。

編譯器對非虛函數使用靜態連編,對虛函數使用動態連編。


因爲動態連編需要很多準備工作,加大了系統的開銷,但是如果沒有虛函數的情況下,或者說沒有類繼承的情況下我們可能根本用不到動態連編,相對來說,靜態連編雖然靈活性欠缺但是速度很快。

每個對象其實都有一個隱藏成員,它是一個指針指向一個數組,這個數組裏面保存了很多的函數地址。這種數組被稱爲是虛函數表。虛函數表中存儲了爲派生類對象聲明的所有的虛函數的地址。如果派生類重新定義了虛函數,那麼虛函數裏面存儲的就是我們新定義的函數 地址,如果沒有定義,那麼虛函數表裏面存儲的就是基類中原始版本的函數地址。指向虛函數表的指針是每個對象都有一個,虛函數表 是整個類只有一個。


派生類不繼承基類的構造函數,也就是說,派生類執行自己的構造函數和繼承沒什麼關係。但是虛構函數還是有必要聲明爲虛函數的。因爲子類對象在創建的時候如果有new的參與,那麼把返回的指針賦值給父類類型的指針的話,如果析構函數不是虛的,那麼只能夠執行父類的析構函數,也就是說,釋放掉子類對象中屬於基類對象的那部分內存,聲明爲虛函數之後纔會先執行子類的析構函數,再執行父類的析構函數。


最重要的一點就是:只有成員函數才設計到虛函數的問題,友元函數是不涉及到這些的。

基類中的虛函數,在派生類中重新定義的時候,無論參數列表是否和基類的虛函數相同,派生類都會自動屏蔽掉基類的版本,也就是說,派生類對象再調用這個名字的函數的時候,只能調用派生類中新定義的版本,編譯器不會生成該函數的兩個重載版本。

要區別的一個概念就是重載和重寫是不同的。重載是發生在一個類裏面,同名函數的兩個不同的版本,而重寫應該指的就是虛函數在派生類中的重新實現。無論參數列表是否相同,只要是子類中的虛函數名和基類中的相同,那麼重新定義子類中的虛函數就會自動屏蔽掉基類中的同名虛函數。


如果基類中的虛函數被重載,那麼如果在子類中要需要用到這幾種重載的形式應該都進行重寫。如果只是重寫了一個,在子類中會自動屏蔽掉基類裏面同名的虛函數,無論他有幾個版本。


四、保護的訪問權限

一般來說,基類的數據都會被定義在隱藏的部分中,保護的訪問權限下的東西只能夠被子類訪問,所以將一些方法聲明爲保護權限,能夠防止讓子類意外的其他類或對象訪問。


五、抽象類

包含純虛函數的類叫做抽象類,一般來說,抽象類都是用作基類的,並且抽象類不能夠創建對象。當我們要想把一個類定義成抽象類的時候,就在某一個虛函數的聲明後面加上=0,至於你是否要在這個抽象類的實現文件當中實現這個函數是隨意的。


六、類繼承和動態內存分配

如果基類當中用到了動態內存分配,比如複製構造函數,賦值運算符的重載,顯式定義析構函數等等。如果子類不需要使用動態內存分配,那麼在繼承下來之後是否還要對子類顯式定義這些函數呢。在類對象進行復制的時候,類中的成員複製也是根據他們的數據類型相應的選擇合適的複製方式,比如正常的基本數據類型使用規定好了的複製方式,但是一些類的成員就會用到複製函數來完成。在進行子類對象類型之間的複製的時候,因爲子類對象的創建也同時創建了基類對象,所以在複製基類對象的部分會自動調用基類的複製函數,如果子類中沒有特殊的類成員需要複製,比如動態申請的,那麼就可以在子類中不定義複製函數。


上面說的是基類使用了動態內存分配的方式,那麼如果子類也使用了動態內存的方式來創建成員的話,就需要爲子類也提供複製構造函數還有析構函數,以及重載的賦值運算符。

在子類執行復制構造函數的時候,會自動調用基類的複製構造函數,基類的 複製構造函數可能需要一個基類對象類型的引用變量,所以在子類複製構造函數裏面需要用成員初始化列表來將子類對象的引用作爲參數傳遞進去,因爲基類的引用是可以指向子類的對象的,然後在基類的複製構造函數中會使用子類對象中屬於基類對象的部分,然後給他們賦值。


子類的賦值運算符函數,不單單要負責子類部分數據的賦值,還要負責基類部分數據的賦值,所以在實現子類的賦值運算符函數的時候必須要顯式調用基類的賦值運算符函數。


在類的繼承中,有關於子類和基類都使用動態內存分配的方法實現有以下三種情況:

子類的析構函數會自動調用基類的析構函數

子類的複製構造函數需要通過初始化成員列表來傳遞子類對象的引用從而調用相應的基類複製構造函數。

子類的賦值運算符函數需要在函數內部利用作用域標識符的形式來調用基類的賦值運算符函數


以上的三種情況,都是因爲子類對象的創建會自動創建基類的對象,在子類對象執行上面的操作的時候,基類對象的部分必須調用它們專屬的方法進行處理。


七、類設計回顧

編譯器總會爲我們自動的生成一些比較重要的函數,比如默認的構造函數。

默認的構造函數包括兩個版本,一個是不加任何參數的,第二個是即使有參數,但是參數列表裏面的參數都是有默認值的。

基類默認的構造函數在子類的構造函數調用之前會先調用基類的構造函數,如果在子類構造函數的初始化列表中沒有給基類傳遞任何參數,那麼將會調用基類的默認的構造函數。


複製構造函數屬於一種特殊的構造函數,他以所屬類的對象作爲參數,在用舊對象初始化新對象的時候,或者在傳遞參數的時候是按值傳遞對象,或者在函數的返回值是對象,或者在編譯器生成臨時的匿名對象的時候,都會調用複製構造函數。


複製構造函數一般用處比較多的就是參數的傳遞函數對象的初始化,編譯器在我們使用複製構造函數的時候會爲我們定義一個默認的複製構造函數,這個構造函數的功能就是按成員順序從一個對象複製到另一個對象,如果不使用複製構造函數,編譯器只提供聲明,但是不提供定義。但是,如果類的成員裏面有指針以及靜態成員的時候,最好自己顯示的定義一個複製構造函數,防止淺拷貝帶來的內存問題。


默認的賦值運算符函數其實和複製構造函數的作用類似,只不過,在執行默認的賦值運算符函數的過程中,如果有的成員是特殊的類對象的話,那麼他會自動去調用該類的賦值運算符。並且如果成員裏面有指針這一類的類型,也需要自己本身來定義一個顯式的賦值運算符函數。

編譯器不會爲我們定義不同類型成員的賦值運算符函數,如果想讓不同類型的對象進行賦值的話,就需要定義特殊的符合要求的賦值函數。或者先使用轉換函數,然後再使用賦值函數。


構造函數不能被繼承的一個原因就是,每一個構造函數都擔當着創建和初始化類類對象的工作,類繼承的方法都是能夠 讓子類的對象用父類的方法,然而基類的構造函數似乎在子類中沒什麼重要的作用。


帶有一個特殊參數的構造函數是用來把非類類型的對象轉化爲類類型的。我們可以定義參數不同的構造函數來實現這一功能。那麼要把類類型轉化爲double這樣的類型呢,就需要一種轉換函數。轉換函數必須是類成員函數,轉換函數可以是沒有參數的,也可以是由返回值類型沒有參數的。即使轉換函數不指定目標類型,也同樣能夠正常轉換。

類名 要轉換的類型名(){}這就是轉換函數的基本格式                                                                                                                                                                      

在參數傳遞過程中,如果要傳遞的參數是對象,那麼我們儘可能要用引用來作爲參數,而不是進行普通的值傳遞,一個是普通的值傳遞可能會涉及到複製構造函數的使用還有就是效率問題,如果怕傳遞的引用肯能會在函數裏面更改,可以使用一個const的引用來接受傳遞進來的參數。


並且在有虛函數的時候,被定義爲接受基類引用參數的函數也可以接受派生類的對象參數。


在調用函數的時候,時常會提到到底是返回對象好一點還是返回引用好一點。單指效率來說,還是返回引用好一點。但是有些時候是隻能返回對象不能返回引用的。比如,我們要返回一個在函數裏面臨時創建的一個對象,因爲臨時創建的對象在函數調用完成之後就會被銷燬,所以返回引用是不行的,就只能夠返回對象。返回對象就是返回一個臨時的對象副本,並且在過程之中還會調用相應的複製構造函數創建出一個臨時對象。


八、類繼承的注意事項

派生類不能夠繼承基類的構造函數,所以創建子類對象的時候必須要執行子類的構造函數,並且通過成員初始化列表的形式來嗲用基類的構造函數,讓他爲我們創建子類對象中的基類部分。


析構函數也是同樣不能夠繼承的,爲了防止一些其他的問題發生,基類的析構 函數一般來說都要聲明爲虛函數。

賦值運算符函數主要用於同類對象的賦值,賦值運算符函數必須是成員函數,換句話說,每一個類都有自己的賦值運算符函數,在有子類和基類的時候,子類對象的賦值運算符函數將會自動調用基類的賦值運算符函數來對子類對象中屬於基類的部分進行賦值,對於類中的成員是其他類的對象的時候,在賦值期間也要調用該類對應的賦值運算符函數。如果我們沒有顯式定義賦值運算符函數,那麼編譯器只會提供聲明但是不會提供定義,一旦我們在程序中用到了類對象的賦值操作的話,編譯器將會爲我們自動定義一個賦值運算符函數。如果派生類中沒有顯式定義賦值運算符函數的需求的話,那麼在賦值的時候會自動調用基類的賦值運算符函數來進行基類部分的賦值,但是如果子類中存在這種需要,在顯式實現賦值運算符函數的過程中,不但要對子類的新成員進行賦值,同時也要顯式的調用基類的賦值運算符函數才行。


在子類重新實現了基類當中的虛函數的時候,在調用的時候一定要傳遞對象的引用而不是值,因爲傳遞值的話,如果重新實現的函數的形參是基類對象的話,那麼可能會出現錯誤。


因爲友元函數不是類成員函數,所以不能夠繼承,但是如果想在子類中訪問基類中的友元函數,可以把子類的指針或者引用轉爲基類的類型,再調用基類的友元函數。


發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章