代理類-摘自《C++沉思錄》Andrew Koenig

       我們怎樣才能設計一個C++容器,使它有能力包含類型不同而彼此相關的對象呢?容器通常只能包含一種類型的對象,所以很難在容器中存儲對象本身。存儲指向對象的指針,雖然允許通過繼承來處理類型不同的問題,但是也增加了內存分配的額外負擔。

       這裏,我們將討論一種方法,通過定義名爲代理(surrogate)的對象來解決該問題。代理運行起來和它所代表的對象基本相同,但是允許將整個派生類層次壓縮在一個對象類型中。正如對本部分的介紹中所說的那樣,surrogate是handle類中最簡單的一種。

1.    問題   ​  

       假如有一個表示不同種類的交通工具的類派生層次:

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都有一些類Vehicle中成員聲明的共有屬性。但是,有的Vehicle具有一些其他Vehicle所沒有的屬性。例如,只有Aircraft能飛,也只有Helicopter能盤旋。

       下面假設我們要跟蹤處理一系列不同種類的Vehicle。在實際中,我們可能會使用某種容器類;然而,爲了使表述更簡潔,我們使用數組來實現。這樣的話,就試一試

       Vehicle parking_lot[1000];

沒有產生預期的效果,爲什麼?

       表面上看是出於Vehicle是一個抽象基類,因爲成員函數weight和start都是純虛函數。通過在聲明中寫=0,明確聲明瞭這些函數可以空着不定義。因此,只有從類Vehicle派生出來的類才能夠實例化,類Vehicle本身不會有對象。既然Vehicle對象不存在,當然也就不可能有其對象數組了。

       我們的失敗還有更深層次的原因。如果我們思考一下,假設存在類Vehicle的對象會出現什麼樣的情況,原因就會更加明顯。譬如,假設我們剔除了類Vehicle中的所有純虛數。如果我們寫類似於下面的語句,會有什麼結果呢?

Automobile x = /*...*/
Partking_log[num_vehicles++] = x;

       答案是:把x賦給parking_lot的元素,會把x轉換成一個Vehicle對象,同時會丟失所有在Vehicle類中沒有的成員。該賦值語句還會把這個被剪裁了的對象複製到parking_lot數組中去。

      這樣,我們就只能說parking_lot是Vehicles的集合,而不是所有繼承自Vehicle的對象的集合。

2.    經典解決方案

       這種情況,實現靈活性的常見做法是提供一個間接層(indirection)最早的合適的間接層形式就是存儲指針,而不是對象本身:

Vehicle* parking_lot[1000];			//指針數組

       然後,輸入類似

Automobile x = /*...*/;
Partking_log[num_vehicles++] = &x;

的語句。這種方法解決了迫切的問題,但也帶來了兩個新問題。

       首先,我們存儲在parking_lot中的是指向x的指針,在本例中是一個局部變量。這樣,一旦變量x沒有了,parking_lot就不知道指向什麼東西了。  

       我們可以這麼變通一下,放入parking_lot中的值,不是指向原對象的指針,而是指向它們的副本的指針。然後,可以採用一個約定,就是當我們釋放parking_lot時,也釋放其中所指向的全部對象。因此,可以把前面的例子改爲:

Automobile x = /*...*/;
Partking_log[num_vehicles++] = new Automobile(x);

       儘管這樣修改可以不用存儲指向本地對象的指針,它也帶來了動態內存管理的負擔。另外,只有當我們知道要放到parking_lot中的對象是靜態類型後,這種方法才能起作用。如果不知道又會怎樣?例如,假設我們想讓parking_lot[p]指向一個新建的Vehicle,這個Vehicle的類型和值與由parking_lot[q]指向的對象相同,情況會是怎樣?我們不能使用這樣的語句

if(p != q){
	delete parking_lot[p];
	parking_lot[p] = parking_lot[q];
}

       因爲接下來parking_lot[p]和parking_lot[q]將指向相同的對象。我們也不能使用這樣的語句

if(p != q){
	delete parking_lot[p];
	parking_lot[p] = new Vehicle(parking_lot[q]);
}

       因爲這樣我們又會回到前面的問題:沒有Vehicle類型的對象,即使有,也不是我們想要的! 

    
2.    虛複製函數

       讓我們想一個辦法來複制編譯時類型未知的對象。我們知道,C++中處理未知類型的對象的方法就是使用虛函數。這種函數的一個顯而易見的名字就是copy,clone也可以,不過稍微有點似是而非。

       由於我們是想能夠複製任何類型的Vehicle,所以應該在Vehicle類中增加一個合適的純虛函數:

class Vehicle{
public:
	virtual double weight() const = 0;
	virtual void start() = 0;
	virtual Vehicle* copy() const = 0;
	//...
};

       接下來,在每個派生自Vehicle的類中添加一個新的成員函數copy。指導思想就是,如果vp指向某個繼承自Vehicle的不確定類的對象,則vp->copy()會獲得一個指針,該指針指向該對象的一個新建副本。例如,如果Truck繼承自(直接或者間接地)類Vehicle,則它的copy函數就類似於:

Vehicle* Truck::copy() const
{
	return new Truck(*this);
}

       當然,處理完一個對象後,需要清除該對象。要做到這一點,就必須確保類Vehicle有一個虛析構函數:

class Vehicle{
public:
	virtual double weight() const = 0;
	virtual void start() = 0;
	virtual Vehicle* copy() const = 0;
	virtual ~Vehicle() {}
	//...
};

4.    定義代理類

       我們已經理解了根據需要複製對象的方法。現在,來看看內存分配。有沒有一種方法既能使我們避免顯式地處理內存分配,又能保持類Vehicle在運行時綁定的屬性呢?

       解決這個問題的關鍵是要用類來表示概念,這在C++中是很常見的。我總把這一點當作最基本的C++設計原則。在複製對象的過程中運用這個設計原則,就是定義一個行爲和Vehicle對象相似、而又潛在地表示例所有繼承自Vehicle類的對象的東西。我們把這種類的對象叫做代理(surrogate)。

       每個Vehicle代理都代表某個繼承自Vehicle類的對象。只要該代理關聯着這個對象,該對象就肯定存在。因此,複製代理就會複製相應的對象,而給代理賦新值也會先刪除舊對象、再複製新對象(Dag Bruck指出,這種處理方式與我們所預期的賦值行爲稍微有所不同,因爲這種方式改變了左側的代理類實際關聯的那個對象的類型)。所幸的是,我們在類Vehicle中已經有了虛函數copy來完成這些複製工作。所以,我們可以開始定義自己的代理了:

class VehicleSurrogate{
public:
	VehicleSurrogate();
	VehicleSurrogate(const Vehicle&);
	~VehicleSurrogate();
	VehicleSurrogate(const VehicleSurrogate&);
	VehicleSurrogate& operator=(const VehicleSurrogate&);

private:
	Vehicle* vp;
};

       上述代理類有一個以const Vehicle&爲參數的構造函數,這樣就能爲任意繼承自Vehicle的類的對象創建代理類。同時,代理類還有一個缺省構造函數,所以我們能夠創建VehicleSurrogate對象的數組。

       然而,缺省構造函數也會給我們帶來了問題:如果Vehicle是個抽象類,我們應該如何規定VehicleSurrogate的缺省操作?它所指向的對象的類型是什麼?不可能是Vehicle,因爲根本就沒有Vehicle對象。

       爲了得到一個更好的方法,我們要引入行爲類似於零指針的空代理(empty surrogate)的概念。能夠創建、銷燬、和複製這樣的代理,但是進行其他操作就視爲出錯。

       到目前爲止,不再需要任何其他的操作了,這就使得我們能很容易地寫出成員函數的定義:

VehicleSurrogate::VehicleSurrogate(): vp(0) {}
VehicleSurrogate::VehicleSurrogate(const Vehicle& v): vp(v.copy()) {}
VehicleSurrogate::~VehicleSurrogate()
{
	delete vp;
}

VehicleSurrogate::VehicleSurrogate(const VehicleSurrogate& v): vp(v.vp? v.vp->copy(): 0) {}
VehicleSurrogate& VehicleSurrogate::operate=(const VehicleSurrogate& v)
{
	if(this != &v)
	{
		delete vp;
		vp = (v.vp ? v.vp->copy() : 0);
	}
	return *this;
}

       這裏有3個技巧值得我們注意。

       首先,注意每次對copy的調用都是一個虛擬調用。這些調用只能是虛擬的,別無選擇,因爲類Vehicle的對象不存在。即使是在那個只接收一個const Vehicle&參數的複製構造函數中,它所進行的v.copy調用也是一次虛擬調用,因爲v是一個引用而不是一個普通對象。

       其次,注意關於複製構造函數和賦值操作符中的v.vp非零的檢測。這個檢測是必需的,否則調用v.vp->copy時就會出錯。

       再次,注意對賦值操作符進行檢測,確保沒有將代理賦值給它自身。

       下面剩下的工作只是另該代理類支持Vehicle所能支持的其他操作了。在前面的例子中,有weight和start,所以要把它們加入到類VehicleSurrogate中:

class VehicleSurrogate{
public:
	VehicleSurrogate();
	VehicleSurrogate(const Vehicle&);
	~VehicleSurrogate();
	VehicleSurrogate(const VehicleSurrogate&);
	VehicleSurrogate& operate=(const VehicleSurrogate&);
	// 來自類Vehicle的操作
	double weight() const;
	void start();
	//...
private:
	Vehicle* vp;
};

       注意這些函數都不是虛擬的:我們這裏所使用的對象都是類VehicleSurrogate的對象;沒有繼承自該類的對象。當然,函數本身可以調用相應Vehicle對象中的虛函數。它們也應該檢查確保vp不爲零:

double VehicleSurrogate::weight() const
{
	if(vp == 0)
		throw "empty VehicleSurrogate.weight()";
	vp->weight();
}
double VehicleSurrogate::start() const
{
	if(vp == 0)
		throw "empty VehicleSurrogate.start()";
	vp->start();
}

       一旦我們完成了所有這些工作,就很容易定義我們的停車場(parking_lot)了:

VehicleSurrogate parking_lot[1000];
Automobile x;
Parking_lot[num_vehicles++] = x;

       最後一條語句等價於

parking_lot[num_vehicles++] = VehicleSurrogate(x);

       這個語句創建了一個關於對象x的副本,並將VehicleSurrogate對象綁定到該副本,然後將這個對象賦值給parking_lot一個元素。當最後銷燬parking_lot數組時,所有這些副本也將被銷燬。

 

總結:

       代理類通過最頂層基類(抽象類)指針獲得處理未知類型對象的能力,然後所有的操作函數都在代理類定義,用戶直接使用代理類而不是抽象類。此功能最大好處是,能在同一個容器中,通過統一接口(即代理類)操作來自於同一抽象類的派生類,從而使得數據構建簡便而無數據損失。

       另一種理解:希望把繼承於同一基類的不同派生類放到同一容器中時,可以使用基類指針的辦法,但是會出現不同指針指向同一對象的問題,導致指針懸掛風險。爲了避免此問題,可以把基類指針嵌套在一個代理類裏面,代理類的複製和賦值操作都是通過對被操作對象的拷貝實現,而不是對指針的拷貝,這也就避免了出現多指針指向同一對象的情況。用戶使用的是代理類而不是基類指針,操作代理類與操作其代理的對象效果一樣。

       這樣做的弊端是:類的複製次數無法控制,提高內存空間成本。解決這個問題的一種方法是使用智能指針,通過對指向對象的指針進行計數,來決定創建和銷燬對象的時機。當然,如果C++有類似於JAVA的垃圾回收機制,就不用考慮這些問題了,值得注意的是,JAVA垃圾回收機制的早期方法用的正是智能指針(即對對象被引用情況進行計數,以決定是否可以將其回收)。但是此方法在存在類循環引用時無效,因此後期JAVA使用另一種方法,即對非活躍對象通過二叉樹進行遍歷,遍歷不到的即可回收。
 




 

發佈了12 篇原創文章 · 獲贊 1 · 訪問量 1萬+
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章