首先假設我們要設計一系列交通工具的類,一般來說我們會定義一個交通工具的基類,裡面存放所有交通工具都有的成員和屬性,比如這樣:
class Vehicle { public: virtual double weight() const = 0; virtual void start() = 0; // ...... };
然後會有一些交通工具繼承關係,比如這樣:
class RoadVehicle : public Vehicle { /* ...... */ }; class AutoVehicle : public RoadVehicle { /* ...... */ }; class Aircraft : public Vehicle { /* ...... */ }; class Helicopter : public Aircraft { /* ...... */ };
現在我們要定義一個容器,來儲存不同型別的交通工具。
這個要求看起來簡單,但沒有想象中那麼容易。化繁為簡,比如我們用一個陣列來儲存不同的交通工具,首先我可能會這麼寫:
Vehicle parking_lot[1000];
仔細一想,這麼寫好像不對,為什麼呢?因為 Vehicle 裡面有純虛擬函式,所以 Vehicle 是個抽象類,抽象類是不會有物件的,所以這麼定義是肯定不行的。一般分析也就到這裡為止了,但繼續想一下,如果我把 Vehicle 中的所有純虛擬函式去掉,那麼這種定義好像就是OK的,語法上不會有問題,但是有另一個問題,比如下面這樣的賦值:
Helicopter x = /* ...... */ parking_lot[num_vehicles++] = x;
這樣的賦值會導致 Helicopter 物件被轉換成一個Vehicle物件,它將丟失自己的 Helicopter 屬性,這可不是我們想要的,這就好像把一個 double 數轉換成整型放進整形陣列裡,丟失了自己的小數部分。
看到這裡,馬上有人會提出,那麼在 parking_lot 中儲存 Vehicle 的指標不就可以了嗎?我們一起來看看:
Vehicle *parking_lot[1000]; // 指標陣列
然後我們重複上面的賦值操作:
Helicopter x = /* ...... */ parking_lot[num_vehicles++] = &x;
看起來一切OK,但是有經驗的程式設計師(比如說我,:))一眼就看出這裡很危險,為什麼危險呢?因為儲存指標本身就是一件危險的事情,具體說來,這裡的 x 看起來是一個區域性變數,如果 x 被釋放掉了,那麼 parking_lot 陣列裡的指標立馬成了懸垂指標,指向什麼內容就不知道了。一個富有責任心的程式設計師是鐵定不會這麼幹的。
那我們是不是就沒折了呢?也不是,既然放指標不行,那麼我複製一下這個物件算了,如下:
Helicopter x = /* ...... */ parking_lot[num_vehicles++] = new Helicopter(x);
雖然浪費了些時間和記憶體,但是這麼做看起來確實可以,自己分配了記憶體當然要由自己來釋放,所以我們繼續規定在 delete 這個 parking_lot 的時候,我們也釋放其中所指向的物件。如果這麼幹只有自己管理記憶體這麼一個負擔的話,我想我還能接受,但是這裡有一個不那麼明顯的問題。就是我們放入 parking_lot 中的物件,必須要是已知型別的物件,一說到這裡有的看官就立馬明白了我的意思了,也就是說對於那些編譯時型別未知的物件,這裡就沒辦法儲存了,舉個例子,比如我需要在 parking_lot[p] 中放 parking_lot[q] 的物件,該怎麼辦呢?我們並不知道 parking_lot[q] 的物件型別,所以我們沒辦法複製這個物件,同時,我們不能讓 parking_lot 中有兩個指標指向同一個物件,因為我們在刪除這個容器時會把裡面的物件也刪掉,如果有兩個指標指向同一個物件那麼就會刪除兩次。當然,你可以用別的方法來避免,但這還是讓我無法忍受了。
對於編譯時的未知物件,聰明的程式設計師已經想到辦法解決了。為什麼我們要知道它們是什麼?只要它們自己知道自己是什麼,然後告訴我們就OK了唄!good boy!說明白些,就是我們可以讓繼承自 Vehicle 的類來告訴別人他們到底是什麼,一個簡單的辦法就是在 Vehicle 中定義的 copy 的純虛擬函式,然後繼承自 Vehicle 的類都設計自己的 copy 函式,用來把自己複製一份返回給呼叫者,這樣呼叫者就不用知道這些亂七八糟的交通工具是什麼了。我們來繼續修改程式碼:
class Vehicle { public: virtual double weight() const = 0; virtual void start() = 0; virtual Vehicle *copy() const = 0; // ...... };
然後我們修改 Helicopter 類,增加一個 copy 函式:
Vehicle *Helicopter::copy() const { return new Helicopter(*this); }
這樣我們就再也不需要知道x的型別或者是 parking_lot[q] 的型別了,直接呼叫 x.copy() 函式或者 parking_lot[q]->copy() 函式就OK了。
parking_lot[num_vehicles++] = x.copy(); parking_lot[p] = parking_lot[q]->copy();
我們完美的解決了上面提到的第二個問題,但程式設計師從來都是追求完美的,那麼我們有辦法解決這個顯示處理記憶體分配的問題嗎?這也是程式設計師幸福的地方,別的領域追求完美是極其困難的,但程式碼總能讓我們欣喜。《C++ 沉思錄》裡提到了一個非常深刻的概念——“用類來表示概念”,到底是個什麼意思呢?就是說我們設計類,不光可以是一個具體的事物,同樣,也可以是一個概念,比如,你可以用類來表示人,男人,女人等等,同樣你可以用類來表示家庭,人是具體的,而家庭只是一個概念,家庭裡肯定有有人,所以把控了家庭這個概念,也就把控了人(不要跟我抬槓說有些人沒有家庭,舉個例子而已,親!)。
具體表現在程式碼上就是我們通過定義一個代理類,來表達這些不同的交通工具,這個代理類應該可以代表不同的交通工具,同時它需要幫助我管理記憶體,而且需要能夠例項化,因為這樣我就不用再糾結上面那個 Vehicle 是抽象類沒辦法定義容器的問題,所以,這個代理類的作用是讓我能夠定義代理類的容器,同時不需要我來考慮記憶體的管理問題,而且要支援編譯時型別未知的情況。
代理類只是一個管理交通工具的管理者,它不是一個具體的東西,就跟大明星的經紀人一樣。那看來它必須儲存一個明星,也就是得有一個指向交通工具的指標,同時它需要上臺面,那麼它需要真實的建構函式,同時它需要能夠放進容器,所以它需要一個預設建構函式:
class VechicleProxy { public: VechicleProxy(); VechicleProxy(const Vehicle &); ~VechicleProxy(); VechicleProxy(const VechicleProxy &); VechicleProxy &operator=(const VechicleProxy &); private: Vehicle *p; };
上面多加了幾個建構函式和賦值操作符,也不難理解,畢竟是一個真實的類嘛。其中以 const Vehicle& 為引數的複製建構函式就提供了為任意交通工具做代理的能力。一切看起來OK,但是在預設建構函式裡我們能夠為 p 指標賦值什麼呢?好像只能賦為0了。這個零指標也就是說通常說的空代理。那麼讓我們來完成這個代理類的成員函式吧:
VechicleProxy::VechicleProxy(): p(0) { } VechicleProxy::VechicleProxy(const Vehicle &BigStar): p(BigStar.copy()) {} VechicleProxy::~VechicleProxy() { delete p; } VechicleProxy::VechicleProxy(const VechicleProxy &v): p(v.p ? v.p->copy() : 0) {} VechicleProxy::operator=(const VechicleProxy &v) { if (this != &v) { delete p; p = (v.p ? v.p->copy() : 0); } return *this; }
這裡沒有什麼多餘的祕密了,仔細點都OK。寫到這裡我們終於可以定義一個完美的 parking_lot 了。
VehicleProxy parking_lot[1000]; Helicopter x; parking_lot[num_vehicles++] = x;
總結一下:
當我們使用繼承和容器的時候,通常需要處理兩個問題:記憶體的分配和編譯時型別未知物件的繫結。使用一個被成為代理類的東西,我們把複雜的繼承層次壓縮到了一起,讓這個類能夠代表所有的子型別,用類來表示概念的武器果然犀利。