標籤:

C++向上轉型,為什麼不需要強制轉換?

代碼如下:

class Base
{
public:
Base() {}
virtual ~Base() {}
};

class Derived : public Base
{
public:
Derived() {}
virtual ~Derived() {}
};

int main()
{
Base *b;
Derived *d = new Derived();

/* 向上轉型 */
b = d; // 為什麼此處不需要寫成 b = static_cast&(b)???

/* 向下轉型 */
d = static_cast&(b);

return 0;
}

問題在代碼中已注釋的形式寫出來了,就是向上轉型,為什麼不需要強制轉換,望大家指教。諸如,Derived是Base之類的我是理解的,最好能從內存的角度解釋一下,謝謝大家了!

---------------------------------------------------------------------------------------------------------------------------------------

謝謝各位的回答。從邏輯上講,子類和基類的關係是is-a,從這個層面上講我是理解的。但是,從內存的角度怎麼理解呢?Milo Yip老師說合法的Dervied指針指向的對象會包含其Base的數據,這點我也理解了。

可是在網路編程中,bind函數通常這樣寫:bind(servSock, (struct sockaddr *)servAddr, sizeof(servAddr)); 但是,實際上struct sockaddr * 和struct sockaddr_in * 指向的數據大小是完全相同的,既然如此,為什麼不可以直接寫成 bind(servSock, servAddr, sizeof(servAddr));呢?是因為不滿足Liskov替代原則嗎?

如果一個指針A所指向的類型的大小 大於等於 指針B所指向的類型的大小, 那麼將指針A轉化為指針B,是不是就可以不用強制轉換?

謝謝大家!


如果要從內存去解釋,就是合法的Dervied指針指向的對象會包含其Base的數據,語法上容許自動cast是為了方便。但語意上最好要符合Liskov替代原則。


玫瑰花是花,花一定是玫瑰花嗎?


sockaddr is-not-a sockaddr_in,這兩個類型根本就不是父子類型關係,只是碰巧尺寸一樣而已(其實還不一定是一樣的尺寸,因為它們都可以包含實現的細節),碰巧尺寸一樣的東西總不能隨便互換吧,哪個編程語言也不敢這麼設計啊。


《深度探索C++對象模型》

補充的例子跟C++的多態沒有關係,我先說下為什麼沒有用多態,再說一下為什麼多態機制用在這不合適吧。

為什麼沒用多態。

我猜其中一個原因是socket出現在70年代,C++出現在80年代,因此從歷史上來說,socket這套API也不可能提供C++ OOP風格的介面。

為什麼不適合多態。

C++多態中使用了虛函數表指針。也就是對於每個實例,都有一個額外的4B的overhead,而IP地址加埠才6B。例子中對象的大小忘記了,可以查閱一下,但4B在這裡肯定是一個不可忽視的overhead。

虛函數表指針的引入使得每次函數調用都要經過額外的定址,因此效率略有損失。

日常的應用或許可以容忍這樣的overhead和損失,但是網路通信這麼低層和頻繁的操作因為一點點編程上的便利就做出這樣的犧牲實在得不償失。

那函數中這個用法既然不是多態那該怎麼理解?

從內存上去理解。網路編程直接涉及內存的大小端,本地序網路序等問題,如果看一眼網路協議話,基本就是告訴你哪幾個bit存什麼東西,位運算大有發揮之地。因此理解內存是寫socket程序的必備姿勢。各種結構本來就是對內存的解釋,對同一段內存不同的解釋有各種原因,比如方便,統一介面。但是代碼無法從內存的10猜出你想表示什麼,所以只能通過類型轉換來告訴程序怎麼理解這段內存。

可不可以用OOP來理解呢?

可以。OOP是把同樣的東西用不同的方式去理解一下,使其更符合人類的習慣而非電腦的習慣。當然要付出額外的時空代價。從問題看題主已經有這樣的理解了,我就不重複了


struct sockaddr 和 struct sockaddr_in 是 C 的數據結構,不是 C++ 類,當然不能用里氏替換原則。

至於說這兩個結構內容、大小一樣,是因為目前99.999%的網路應用都是基於Internet的 TCP/IP 協議,還沒有發展出其他網路被普遍使用,所以socket的地址也只有internet格式的sockaddr_in。sockaddr是當時保留以後擴展其他形態的網路用的。


設計模式的角度,根據里氏代換原則,任何基類可以出現的地方,子類一定可以出現。

向上轉型 可以理解成子類包含父類的所有信息,所以父類替換成子類是沒有任何問題。

向下轉型 子類替換成父類,因為父類是有可能不含有子類的相關信息的。需要人為確認這樣

轉化是沒有問題的。所以語法上需要強制轉換。


因為 frac{Gammavdash a:alphaquadGammavdashalpha<:eta}{Gammavdash a:eta}({
m Subsumption})


因為向上轉型一定是合法的,而向下轉型不一定合法


赤兔就是馬,不用特別聲明;但是一匹馬是不是赤兔,需要要特別地聲明、鑒別等。我覺得這是現實生活中很通用的一種邏輯。


從邏輯上講Drived "IS A" Base,所以Drived可以作為Base來訪問。

C++對於這個邏輯的一種常見的實現是在內存中子類首先包含其繼承的所有基類,然後再是子類自己

比如Derived的在內存中的表達是:

Derived Object Memory Layout:

d ------&> +-----------+

...............|...Base......|

...............+-----------+

................|..Derived..|

...............+-----------+

d指針指向的是derived object的開始位置。

b = d,編譯器可以毫無顧慮的把d的值賦給b,因為它知道賦值完了之後,b指向的是derived object的Base部分,所以可以按Base正常訪問。

但是d = b 就不能無憂無慮了,因為b指向的是一個base類,編譯器不能確認它是不是一個derived object, 這時候需要你來告訴編譯器(也就是強制類型轉換),b指向的實際上是一個derived object。

補充的例子跟多態沒什麼關係,還有「根據數據大小來決定是否需要類型轉換」這個依據是invalid的。正如 王勐 所說,類型轉換實際上是告訴程序如何去理解這塊內存。


邏輯上來說,就像@藍色說的一樣,很好理解。

實現上來說,因為C++構造對象的方式保證了派生類啟始地址到 sizeof超類 的內存區域和超類結構相同。強制類型轉換後訪問成員變數和函數的時候並不會產生錯誤。

理解這個問題的關鍵我認為在於需要從更底層的角度思考問題,指針本質是整數,它只表明了內存的起始地址,卻沒有指明對於這塊區域存放的數據需要怎麼解釋(數據在最底層沒有不同,都可以解釋成int和float或任何數據類型)。這樣,為了賦予一個內存塊唯一的解讀方法,讓數據有意義,高級語言給指針加上了類型。回到題目來說,這個指針在類型轉化後值沒有改變,改變的是解釋方式。為了使新的解讀同樣合理有效,只要保證派生類具有第二段的性質即可。

如果想深入,這也可以引申出虛函數和實函數等等一系列的問題。


在底層的內存里,C++的類是父類的成員後面跟著子類的成員變數,所以父類對象能用的地方放個子類對象一樣能用。

當然說虛函數肯定還是要還原回去,就是虛表指針換一下。----這裡剛才說錯了,其實虛表指針不變,形成多態。

回到問題,子類向上轉型,其實就相當於你不用子類多出來的成員變數而已,當然是安全的了。不過考慮到虛函數的問題,如果子類重寫了父類的虛函數,那向上轉父類以後再調那個虛函數,其實調的可是子類的虛函數,而不是父類的了。

向上轉以後再向下轉的話可能就沒這麼幸運了,除非之前的內存都沒被破壞掉。


把Base改成People,Derived改成Student,你會發現,學生是人,人不一定是學生,所以P(People)=S(Student)很正常


推薦閱讀:

有沒有一本書,專門講各種UI效果怎麼實現的,而不是講各種庫的使用辦法的?
打字速度對編程的影響大嗎?
為什麼同為系統級編程語言,Rust 能擁有現代構建/包管理工具,C++ 卻不能?
為什麼C++調用空指針對象的成員函數可以運行通過?
在函數內new一個對象,如果作為引用返回,是不是就可以不用delete了?

TAG:C |