理解C++物件導向程式設計[多型性部分] (轉)

worldblog發表於2007-12-12
理解C++物件導向程式設計[多型性部分] (轉)[@more@]

[面向]

初次寫文章,錯誤一定百出不止。我只是希望我學習物件導向程式設計的一些理解看法
能對有關有共同愛好的人有一些小小用處,還談不上幫助。

[文字涉及稱謂說明]
1.變數:這個稱謂包括兩個部分。
內建型別例項(比如int,float,char等)
自定義型別例項(比如類(class),聯合(union),結構(struct)等)

同時本文有時候也泛泛的稱變數和為成員.
2.父類/子類: 相對於繼承層次而言,被繼承的為父類;繼承的為子類.
3.物件: 本文指類型別的例項(an instance of a class)

************************************基礎部分**************************
[物件模型概要]
因為本文主要是討論多型性在語言層面上的表現,所以我這裡不打算做太深入的
討論,只是為了本文的闡釋目的,做一些針對性的說明,概念性解釋一下記憶體物件模型。

C++類型別的成員函式,我們可以把他們看成C語言中函式。它只是被進行
一定的相關處理防止命名衝突而已。同時除了類中靜態成員函式以外,這些函式
比普通的C語言函式增加了一個引數(this指標),因為可以處理物件中成員變數。
所以我們並沒有在class的物件記憶體佈局中看到任何與成員函式有關的東西。

當然要是類宣告中或者類的繼承層次中出現virtual函式的話,你會在記憶體佈局中看到
有一個叫“虛擬函式指標表”的東東的存在。當我們不是使用單一繼承的話,情況變得
更加複雜了,正如前面所講的那樣,本文主要在解釋如何在語言使用層面上使用語言,
因此我不會在這裡談論更多關於記憶體佈局的內容,需要了解的同行,請自行參閱有關
物件導向程式設計的書籍。

[程式成員生命週期概要]
空間:定義程式成員在程式中(或者翻譯單元中)的儲存形式.正如我們經常遇到的那樣,
  有很多程式全域性變數,程式區域性變數等等概念.同時這個概念決定了變數的生命
  週期.這個概念主要指出了變數放在程式執行的哪個記憶體部分,比如說程式資料段,
  程式棧等等.
生命週期:程式成員在那個時間段是可以利用的,簡單一點講就是程式執行中有一段時間某個成員
  是可以引用的,我們就把這段時間稱為這個成員的生命週期.
可見性:哪些介面(類,函式)可以引用程式成員
連結方式:概念是指一個翻譯單元中宣告定義成員是否可以被程式的其他的翻譯單元使用.

************************************理論部分**************************
[重要的三個結論]
為了說明結論引入類:
class CBase
{
public:
  BaseFunc(){}
};
class CDerived :public CBase
{
public: 
  DeriFunc(){}
};

☆結論一☆.如果我們以一個[父類指標]pointer指向一個[子類物件],那麼透過pointer我們
只能呼叫引用父類型別(指類定義)中所定義的函式.
我們這樣寫:
CBase* pBase;
雖然我們可以用指標pBase指向CDerived物件,但是由於
pBase是一個CBase型別指標,所以我們只能呼叫(或引用)
BaseFunc(),不能DeriFunc()。

☆結論二☆. 如果我們以一個[子類指標]指向一個[父類物件],我們引用函式前,必須透過RTTI來
確定指標指向物件的具體型別.也就是說要做顯式的造型轉換.這種做法應該是我們做
程式設計師深惡痛絕的做法.

CDerived* pDeri;
CDerived *pDeri;
CBase aBase("Jason");
pDeri = &aBase;

☆結論三☆. 如果父類和子類都定義了[相同名稱的成員函式],我們這個時候透過指標呼叫
引用成員函式的決議,就必須根據指標型別(注意不是指標實際指向物件的型別)
來確定我們引用的是哪個函式.實際上這個結論就是函式遮蔽(mask)功能,這個結論很重要的,
因為這裡不管是不是虛擬函式,統統管用的,所以這個結論殺傷力很大的,後面我重點
解釋這個結論。


************************************解釋部分**************************

☆結論一解釋☆:

為什麼是這樣的,我們下面看一下一個例子來了解為什麼語言設計者採用這樣的策略呢?

#include
 #include

class ClassA
{
 public:
 int m_data1;
 void func1() { }
 void func2() { }
 virtual void vfunc1() { }
 virtual void vfunc2() { }
 };

 class ClassB : public ClassA
 {
 public:
 int m_data2;
 void func2() { }
 void func3(){}
 virtual void vfunc1() { }
 };

當我們寫這樣的一下函式;
看看這個函式實現方面有那些問題呢?

void change(ClassA& ca)
{
 ca.func1();
 ca.func2();
 //ca.func3();
 ca.vfunc1();
 ca.vfunc2();
}
當我們這樣函式:
ClassA ca;
ClassB cb;
/*
呼叫說明,當我們這樣呼叫的時候,大家應該都很明白的發現
那個func3()函式是不能呼叫的,因為這個函式在ClassA中根本
就是不存在的。很簡單吧,但是複雜的問題本身就是很簡單的,
我們繼續下面一個例子
*/
change(ca);
/*
你也許說這樣應該可以呼叫func3()函式了吧,不錯ClassB中的確是
存在一個名字叫func3()的函式,但是我們仍然不能透過上面那個函式
呼叫他的,為什麼呢?請接著看我下面的解釋
*/
change(cb);

/*
因為我們是用一個父類的引用引用一個子類的型別的。
從函式的實現細節,我們的目的很明確的,就是要實現
多型性,因為我們在函式使用的時候才能知道函式引數
引用指向物件的真正型別,可能是ClassA也可能是ClassB的,
當然我們就不能貿然的呼叫func3()那個函式了。我們我們
那樣實現函式在編譯器的時候就會被擋住了。

☆結論二☆
這個結論屬於程式設計師程式設計的能力問題,因為語言提供了靈活性,但是
如何使用是程式設計師本身的事情,如果你沒有很好的理解語言本身,你
寫的程式碼就是經常會出現問題,這個只能怪我們自己,要進修,絕對
不要動輒就說這個語言不好。

*********************本人廢話集***************************
很多語言都講什麼簡單簡潔的,我在這裡講一個不是很生動的例子,
看過這個例子以後,我不知道你對於那些所謂簡潔的語言的看法會
有什麼改變。呵呵~~~~

經常同行人講某某企業公司太沒有自由了,嚴重影響自己創造性之類的話。
你想過這些話沒有?我來說我的理解,如果這位同行換一家比較自由的公司,
按理講,他的創造性應該可以得到一定程式的釋放(就是發揮了)。
這個就是自由度從小到大給我帶來的好處。

同時由於我們這個同行還是以為嚴格要求自己的人,所以他即使在自由度很大的
空間中也可以很好的把握自己的方向。

好了,中常休息的時間差不多結束了。現在回到正題,一個給程式設計師自由度很大的
語言,我們稱為靈活性比較高的語言。在這樣的語言環境中,我們並不是每一個都
可以很好利用這個靈活性的,有時候造成錯誤發生,但是我們不應該責怪語言本身,
我們需要做的只有一件事情,就是努力提高自己的水平。

而自由度很低的語言,我們就喪失了這種靈活性,我們再想擴大自由度的時候已經
有些不太可能的。但是我們為了獲得在自由度大的環境下自由毒比較小的時候,我們
只要嚴格要求自己就行了。

上面的廢話我是建立在程式設計師就是程式設計師本身,程式設計師不是程式設計機器的基礎之上的。
無論如何生產不會(至少說幾使年之內),象傳統生產行業那樣的,何況現在
已經進入“人管理“(知識管理)時代。
**********************************************************


☆結論三☆
說到這個結論,實際上就是C++中與overr ,overload齊名的mask問題了。
藉助上面的分析我們一口氣把這個問題也弄明白。
我這個例子和解釋是借用<>,特此說明。
不過這裡也加入我的一些說明,這樣我覺得很容易理解。

下面看一個可能使你會發瘋的例子。

class Base {
public:
  void f(const Base& base){}
  virtual void f(int x){}
};

class Derived: public Base {
public:
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);  // 錯誤!

這裡就發生了函式遮蔽作用,你要是不清楚的話,請你再次看一下上面的結論三。

這不很合理,但ARM對這種行為提供瞭解釋。假設呼叫f時,你真的是想呼叫Derived中的版本,但不小心用錯了引數型別。進一步假設Derived是在繼承層次結構的下層,你不知道Derived間接繼承了某個基類BaseClass,而且BaseClass中宣告瞭一個帶int引數的虛擬函式f。這種情況下,你就會無意中呼叫了BaseClass::f,一個你甚至不知道它存在的函式!在使用大型類層次結構的情況下,這種錯誤會時常發生;所以,為了防患於未然,Stroustrup決定讓派生類成員按名字隱藏掉基類成員。

順便指出,如果想讓Derived的使用者可以訪問Base::f,可以很容易地透過一個using宣告來完成:

class Derived: public Base {
public:
  using Base::f;  // 將Base::f引入到
  // Derived的空間範圍
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);  // 正確,呼叫Base::f

對於尚不支援using宣告的編譯器,另一個選擇是採用行內函數:

class Derived: public Base {
public:
  virtual void f(int x) { Base::f(x); }
  virtual void f(double *pd);
};

Derived *pd = new Derived;
pd->f(10);  // 正確,呼叫Derived::f(int),
  // 間接呼叫了Base::f(int)

 

 


來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/10752043/viewspace-992203/,如需轉載,請註明出處,否則將追究法律責任。

相關文章