標籤:

為什麼 C++ 中,基類指針可以指向派生類對象?

BaseClass *pbase = NULL;
DerivedClass dclass;
pbase = dclass;

這樣設計豈不是類型不一致了?


這有什麼奇怪的?哪個面向對象語言不允許upcast?Java/C#也都允許。


題主對類型系統認識不夠,先不要糾結於基類指針能不能指派生類,以及原理如何的問題了……類的繼承就是表示一種「繼承類是基類中的更具體的東西」的關係。比如水果是基類,蘋果是繼承類,顯然蘋果也是一種水果呀,你說「水果能吃」的時候,水果也可以指的是蘋果呀。

類型一致並不是死板地說類型一定要完全一樣,類型是一種約束,幫助你驗證程序的正確性。比如說你女朋友說:「我要吃水果!」,這時候你送上去一個「蘋果」,也應該是滿足條件的,而送上去一個饅頭可能就孤獨一生了,這就是類型系統的作用……


可以指向,但是無法使用不存在於基類只存在於派生類的元素。(所以我們需要虛函數和純虛函數)

原因是這樣的:

在內存中,一個基類類型的指針是覆蓋N個單位長度的內存空間。

當其指向派生類的時候,由於派生類元素在內存中堆放是:前N個是基類的元素,N之後的是派生類的元素。

於是基類的指針就可以訪問到基類也有的元素了,但是此時無法訪問到派生類(就是N之後)的元素。


把男女老少當人看;把雞鴨貓狗當動物看。有那麼難理解嗎?


派生類和基類的關係並不是兩個獨立的類型,在派生關係中,派生類型「是一個」基類類型(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]


如果你考慮到萬能的void*作為中轉的話,在C/C++里你可以用任何一個類型的指針指向任何一個類型的數據。在語言層面,C/C++都不禁止你這麼干,只是選擇權交給你了,怎麼用,你自己看著辦,用錯了,後果自負

所以,本質上,C/C++是完全信賴程序員的---即使某些語法禁止你怎麼干,其實你還是有辦法繞過的。

舉個例子:C++的引用,很多「誤人子弟的」資料都說它和指針的區別在於指針可以為NULL,而引用不可以為NULL,但是真的如此嗎?

int* p = NULL;
int r = *p;

這時候的r的到底引用了什麼,大家可以自己分析一下。

所以,在C/C++的世界裡,問「為什麼可以」的人,都是不合格沒入門的程序員。只有那些問:「(在XXX情況下)為什麼不可以」的,才是入了門的合格的程序員。

最後再吐一句槽:現在各種腳本語言,把程序員這個職業門檻拉到簡直就是無下限了。只要不是弱智,認得26個字母的,會寫個hello world的,都敢說自己是程序員了。所以,為什麼很多人說C/C++難學難懂?就是因為他們就不是C/C++標準委員會那幫大牛眼中的「合格的程序員」。自己寫出來的代碼,自己都不知道為什麼要這麼寫,自己寫的代碼自己不負責,居然還要別人(編譯器、解釋器)去禁止自己不能這麼寫,真把程序員的工作當作copy paste了啊?活該這些人一輩子做代碼民工,拿兩三千的工資。


剛好最近在複習C++,記的筆記里有相關的地方,希望能解決您的部分困惑。

class Base { ... };
class Derived: public Base { ... };
Derived d;
Base *pb = d; // implicitly convert Derived* =&> Base*

This example demonstrates that a single object (e.g., an object of type Derived) might have more than one address (e.g., its address when pointed to by aBase* pointer and its address when pointed to by a Derived* pointer). That can"t happen in C. It can"t happen in Java. It can"t happen in C#. It does happen in C++. In fact, when multiple inheritance is in use, it happens virtually all the time, but it can happen under single inheritance, too.

Reference: &<&&>


是為了實現面向對象中 里氏替換,多態,介面隔離等採用的語言層面的一種機制


猜想提問者可能不太明白C++中靜態類型和動態類型的區別。

代碼中pb的靜態類型是Base*,這個是不可改變的,在其定義時就已經決定了。

但pb的動態類型是DerivedClass*,這個可以在運行時改變(這樣才實現了多態)。

DerivedClass2 dv2; //指向另一個子類,
pb = dv2; //此時pb的靜態類型仍是Base*,但其動態類型已改為是DerivedClass2*

請參考:C++中的靜態綁定和動態綁定


派生類對象本來就能當基類用,

用來打人的棒子,為什麼能用來打男人?


這是多態的基礎。另外我能說c++是弱類型的嗎。


因為每一個派生類對象都「是」一個基類對象。


c++是一門很現實(貼近生活)的語言。

有些特性,需要從使用層面關注,

有些特性,需要從實現層面關注,

有些特性,需要從編譯層面關注,

有些特性,需要從運行層面關注。

你這個問題,表面上是使用層面的問題,是的,為什麼一個類型的指針可以指向另外一種類型?c++本身也是不支持這種用法的,但人們發現在實際應用中發現:類A的某些方法,類B也需要,但是人們又不想在類B重新實現一遍類A的方法,為了避免代碼重複以及重複所帶來的其他問題,於是繼承這個特性就被發明了。讓類B繼承A,就使其在使用層面擁有了類A的方法。類A就變成類B的一個子集,或者類B就可以當成類A來使用。但是,在指針和引用的問題上,屬於更進一步的發明,這個是和虛函數(virtual)關聯在一起的。人們發明了繼承之後,又希望能集中管理這些父子對象,比如報出他們各自的名字,年齡等。在c的年代裡,實現這些功能需要一堆代碼。而c++通過虛函數和指針引用等特性,可以讓程序員避免每次寫一堆相對重複的代碼。

但是,繼承和虛函數的出現也容易導致被濫用。因為這個特性,是為特定的問題環境所準備的。但是如果問題環境不太適合,使用起來反而會帶來一些不必要的問題和隱憂。

題主,現在不要問為什麼,而是在遇到一個合適的需求的時候,你就會發現這種特性的用武之地。

c++就是這樣,給了你很多選擇,但是你要清楚,什麼時候適合用什麼。


這和c++對象的內存模型有一定關係的,派生類在內存中包括兩個部分:一部分是父類的部分,另一部分才是對象本身擴展的。

首先把派生類地址付給基類指針是符合邏輯的,因為派生類包括基類的部分,派生類指針轉換為基類指針後,基類指針指向派生類中基類的部分;另一方面設計為派生類對象地址可以付給基類指針這一特性是實現多態的基礎。

其實在一定的條件下(比如說基類指針本來就指向派生類對象,否則可能會導致指針越界),基類對象指針也可以轉換為派生類對象指針,詳細可以參考下dynamic_cast 、static_cast


我來回答一個簡單的,上面都說的挺對的。。派生類可以繼承基類的成員函數和變數,當然指針也可以繼承。基類指針可以指向派生類也可以指向基類。但是,反過來卻不行。當然,反過來可以由dynamic_cast方法。


指針指哪裡都行,變數類型終身不可改變(也就是說偏移量是固定的),所指的地址空間可以用不同的類型去解釋(也就是所謂的類型轉換)


來個例子。

struct Base

{

Base(int a):m_a(a){}

int m_a;

}

struct Derived: public Base

{

Derived():Base(1), m_b(2),m_c(3){}

int m_b;

double m_c;

}

Base *pBase = NULL;

Derived d;

Derived *pDerived = d;

d的數據成員有哪些呢? m_a和m_b,m_c;

這三個成員怎麼布局呢? m_a在前,m_b在中間,m_c在最後(不是很嚴謹,你懂我意思就好)。

可以試驗一下:

int *p = reinterpret_cast&(d);

則 m_a == *p; //同為1

即m_a位於d的起始位置,即基類數據成員位於派生類對象的開頭位置(有虛指針則情況略有不同)。

這是pBase和pDerived互轉的基礎。

為什麼要這樣?為了多態。

多態有什麼好處?再舉個例子。

有一個遊戲,裡面有100種怪物,他們繼承自同一個怪物基類Mob;

class Mob

{

virtual attack(Mob*);

}

現在每種怪物都需要實現一個攻擊任何怪物的方法。

沒有多態的時候可以這麼寫:

class Mob1 : public Mob

{

bool attack(Mob1 mob1);

bool attack(Mob2 mob2);

bool attack(Mob3 mob3);

bool attack(Mob4 mob4);

.....

bool attack(Mob100 mob100);

}

有了多態以後:

class Mob1 : public Mob

{

bool attack(Mob* pMob);

//以前有99個成員函數,現在變成了1個,因為pMob可以有多種形態。

}

如有疏漏,請傾噴。


多太裡面就是這樣用的呀,定義一個父類指針指向子類對象


這樣才能體現多太啊……


計算機的世界是二進位的,一個對象就是一塊內存,而指針是一個地址,用於指向一個內存區。

C++中,子類對象可這樣認為,包含一個父類對象以及自己定義的對象,所以父類指針指向一個包含自己對象類型的內存不就顯得不那麼不可理喻了。這鐘設計才更能體現面向對象中的集成關係,才能更好地發揮面向對象的優勢。

當然。如果在C的世界裡面,你一個指針可以指向任何一塊內存的,指向任何一個對象,關鍵合不合法在於你如何讓去使用該指針。


推薦閱讀:

在取消同步的情況下,為什麼cin的速度比scanf快?
GCC編譯的程序為什麼沒有正確調用拷貝構造函數?
C++ 程序員有必要熟練使用除標準庫以外的第三方庫嗎?
為什麼c++的整數會溢出,而Python的整數不會溢出呢?

TAG:編程 | C | CC | 指針 |