派生類與基類 指針指向


https://juejin.im/post/6844904054930292749

派生類和基類的關系並不是兩個獨立的類型,在派生關系中, 派生類型“是一個”基類類型(Derived class is a base class)。在C++語法里規定:基類指針可以指向一個派生類對象, 但派生類指針不能指向基類對象。

用問題里的例子來說
DerivedClass is a BaseClass

派生類型之間的數據結構類似於這樣:

BaseClass : [Base Data]
DerivedClass : [Base Data][Derived Data]

派生類型的數據附加在其父類之后,這意味着當使用一個父類型指針指向其派生類的時候,父類訪問到的數據是派生類當中由父類繼承下來的這部分數據


對比起見,我們再定義一個派生類的派生類

class DerivedDerivedClass : public DerivedClass 
它的數據結構如下:
DerivedDerivedClass : [Base Data][Derived Data][DerivedDerived Data]

而通過基類指針
BaseClass *pbase
訪問每一個類型的數據部分為:
[Base Data]
[Base Data][Derived Data]
[Base Data][Derived Data][DerivedDerived Data]

通過派生類指針
DerivedClass *pderived
訪問每一個類型的數據部分為:
[Base Data] 不能訪問,派生類型指針不能指向基類對象(因為數據內容不夠大,通過派生類指針訪問基類對象會導致越界)
[Base Data][Derived Data]
[Base Data][Derived Data][DerivedDerived Data]
 
 

函數重載、函數隱藏、函數覆蓋

函數重載只會發生在同作用域中(或同一個類中),函數名稱相同,但參數類型或參數個數不同。 函數重載不能通過函數的返回類型來區分,因為在函數返回之前我們並不知道函數的返回類型。

函數隱藏和函數覆蓋只會發生在基類和派生類之間。

函數隱藏是指派生類中函數與基類中的函數同名,但是這個函數在基類中並沒有被定義為虛函數,這種情況就是函數的隱藏。
所謂隱藏是指使用常規的調用方法,派生類對象訪問這個函數時,會優先訪問派生類中的這個函數,基類中的這個函數對派生類對象來說是隱藏起來的。 但是隱藏並不意味這不存在或完全不可訪問。通過 b->Base::func()訪問基類中被隱藏的函數。

函數覆蓋特指由基類中定義的虛函數引發的一種多態現象。在某基類中聲明為 virtual 並在一個或多個派生類中被重新定義的成員函數,用法格式為:virtual 函數返回類型 函數名(參數表) {函數體};實現多態性,通過指向派生類的基類指針或引用,訪問派生類中同名覆蓋成員函數。

虛函數 是在基類中使用關鍵字 virtual 聲明的函數。在派生類中重新定義基類中定義的虛函數時,會告訴編譯器不要靜態鏈接到該函數。

函數覆蓋(多態)的條件:

  • 1: 基類中的成員函數被virtual關鍵字聲明為虛函數;
  • 2:派生類中該函數必須和基類中函數的名稱、參數類型和個數等完全一致;
  • 3:將派生類的對象賦給基類指針或者引用,實現多態。

函數覆蓋(多態)實現了一種基類訪問(不同)派生類的方法。我們把它稱為基類的逆襲。

基類指針和派生類指針之間的轉換

1. 基類指針指向基類對象、派生類指針指向派生類對象
這種情況是常用的,只需要通過對應類的指針直接調用對應類的功能就可以了。

#include<iostream>
using namespace std;
 
class Father{
public:    
    void print()
    {
        printf("Father's function!");
    }
};
 
class Son:public Father
{
public:
    void print()
    {
        printf("Son's function!");
    }
};
 
int main()
{
    Father f1;
    Son s1;
 
    Father* f = &f1;
    Son* s = &s1;
 
    f->print();
    cout<<endl<<endl;
    s->print();
}

 

2. 基類指針指向派生類對象

這種情況是允許的,通過定義一個基類指針和一個派生類對象,把基類指針指向派生類對象,但是需要注意,通常情況這時的指針調用的是基類的成員函數。分四種情況:

    一、 函數在基類和派生類中都存在

這時通過“指向派生類對象的基類指針”調用成員函數,調用的是基類的成員函數。

    Father f1;
    Son s1;

    Father* f = &s1;
    f->print();  //調用的是基類成員函數

    二、函數在基類中不存在,在派生類中存在

由於調用的還是基類中的成員函數,試圖通過基類指針調用派生類才有的成員函數,則編譯器會報錯。

error C2039: “xxx”: 不是“Father”的成員

      三、 將基類指針強制轉換為派生類指針

這種是向下的強制類型轉換,轉換之后“指向派生類的基類指針”就可以訪問派生類的成員函數:

    Son s1;
    Father* f = &s1;
    Son *s = (Son*)f;
    s->print1(); //調用派生類成員函數

但是這種強制轉換操作是一種潛在的危險操作。

      四、基類中存在虛函數的情況

如果基類中的成員函數被定義為虛函數,並且在派生類中也實現了該函數,則通過“指向派生類的基類指針” 訪問虛函數,訪問的是派生類中的實現。允許“基類指針指向派生類”這個操作,最大的意義也就在此,通過虛函數和函數覆蓋,實現了“多態”(指向不同的派生類,實現不同功能)。

    Father f1;
    Son s1;

    Father* f = &s1;
    f->print();   //調用派生類成員函數

 

3. 派生類指針指向基類對象

會產生編譯錯誤。基類對象無法被當作派生類對象,派生類中可能具有只有派生類才有的成員或成員函數。
即便是使用強制轉換,將派生類指針強制轉換成基類指針,通過這個“強制指向基類的派生類指針”訪問的函數依然是派生類的成員函數。

    Father f1;
    Son s1;

    Son* s=&s1;
    Father* f = (Father*) s;

    f->print();  //調用派生類成員函數


綜上,可以通過基類指針訪問派生類方法(強制轉換和虛函數),不存在通過派生類指針調用基類成員函數的方法(即便是強制轉換)。

 

參考 

https://www.jianshu.com/p/a75b267325c2

https://juejin.im/post/6844904054930292749

有下面的一個CPerson類:

class CPerson
{ public: void show() { cout<<"I am a people"<<endl; } }; 

在實際編程中,我們經常會看到一個類型有下面兩種不同的使用方式:

CPerson s1; CPerson *s2 = NULL; s2 = new CPerson(); s1.show(); s2->show(); delete s2; 

那么這兩者在實際的使用中到底有何差別呢,下面從不同的方面來剖析一下。

調用方式上的不同

首先二者最明顯的區別就是調用方式上的不同,對象使用" . "操作符調用,而指針使用" -> "操作符調用,且指針在調用時需要先用new來分配空間,且用完后必須手動delete掉。如不想手動delete也可使用智能指針。

內存空間上的不同

二者的類型決定了它們在內存上的分布不同,一個是對象類型,一個是指針類型。對象類型在創建時就已為對象分配好內存空間,用的是內存棧,是個局部的臨時變量,作用域在該函數體內,隨函數的結束被釋放。

指針變量在創建時也是在內存棧,里面的值是對象的地址,當用new操作符時會在內存堆上分配一個空間,即存儲實際的對象內容,此時的指針變量里的值即為剛剛分配的內存地址。所以為什么要用delete釋放呢?這是因為內存棧里的變量會隨着函數的結束而釋放,內存堆里的內容需要用戶手動釋放,所以當函數調用結束時,指針變量會被釋放,如果不先delete的話,內存堆里的內容就會找不到地址,也就"無人看管"了。所以在實際使用中一定要記得用完后delete,若是數組,則是delete[]。

作為函數參數的不同

類的對象和指針都可作為函數參數傳遞,這其中還可以有一個引用,代碼如下:

void func(CPerson object) void func(CPerson* object) void func(CPerson &object) 

那么這幾種方式有何區別呢?下面來一一分析一下。
1: void func(CPerson object)
這種函數是非常不建議的,因為函數參數壓棧時,對object進行了復制(還記得拷貝構造函數嗎),所以函數對object的操作實際上是對新的對象空間進行的操作,不會影響原對象空間。由於不必要的拷貝對象是十分浪費時間的,也沒有意義,我們完全可以用函數func(const Cobject& object);來代替,同樣能保護對象的只讀性質。
2:void func(CPerson object)
這種方式是將類指針作為參數,函數壓入參數時實際上復制了指針的值(其實指針可以看作一個存放地址值的整形),實際復制的指針仍指向原對象的空間,所以func函數對該指針的操作是對原對象空間的操作。
3: void func(CPerson &object)
這種方式和傳指針類似,但函數壓入參數時實際上復制了object對象的this指針,其實this指針不是一個真正存在的指針,可以看作是每個對象的數據空間起始地址。func函數中對this指針的操作,實際上也是對原對象空間的操作。

不管值傳遞,引用傳遞啊亂七八遭的什么東西,反正調用函數時都是會復制參數的,只是不同的是復制的是地址,還是整個對象空間的區別而已。
相同的,函數CPerson func(CPerson& object);在return CPerson對象時,同樣會進行構貝構造。這些隱藏的對象復制都是不需要的,我們可以改為CPerson& func(CPerson& object);或是CPerson* func(CPerson& object);這樣,在return時,就只是對指針(地址)的復制而已。在不是特殊的情況下,不要將整個對象作為參數,也不要返回整個對象。

類指針實現多態

還有一個顯著區別就是類指針可以實現多態,通過分類指針調用子類對象,下面詳細說明。
C++的多態性用一句話概括就是:在基類的函數前加上virtual關鍵字,在派生類中重寫該函數,運行時將會根據對象的實際類型來調用相應的函數。如果對象類型是派生類,就調用派生類的函數;如果對象類型是基類,就調用基類的函數。

1:用virtual關鍵字申明的函數叫做虛函數,虛函數肯定是類的成員函數。
2:存在虛函數的類都有一個一維的虛函數表叫做虛表,類的對象有一個指向虛表開始的虛指針。虛表是和類對應的,虛表指針是和對象對應的。
3:多態性是一個接口多種實現,是面向對象的核心,分為類的多態性和函數的多態性。
4:多態用虛函數來實現,結合動態綁定.
5:純虛函數是虛函數再加上 = 0;
6:抽象類是指包括至少一個純虛函數的類。純虛函數:virtual void fun()=0;即抽象類!必須在子類實現這個函數,即先有名稱,沒有內容,在派生類實現內容。

下面看一個具體例子:

class CPerson
{ public: virtual void show() { cout<<"I am a people"<<endl; } }; class CStudent:public CPerson { public: void show() { cout<<"I am a student"<<endl; } }; int main() { CStudent stu; CPerson *per = &stu; per->show(); system("pause"); return 0; } 

輸出的結果為:


 
運行jie

可以看到父類指針指向子類對象后,調用的是子類的show函數,注意多態的實現需要在函數前加virtual修飾,使其為虛函數,否則調用的還是父類的,總之要實現多態,類指針和虛函數缺一不可。關於多態的詳細分析具體可以參考http://www.cnblogs.com/cxq0017/p/6074247.html

通過以上對類對象和類指針不同方面的分析,我們在知道在什么時候應該使用類對象,什么時候使用類指針比較好。



作者:litexy
鏈接:https://www.jianshu.com/p/a75b267325c2
來源:簡書
著作權歸作者所有。商業轉載請聯系作者獲得授權,非商業轉載請注明出處。


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM