在C++中,以類、虛函數等為代表的數據抽象功能一直是C++的核心和難點。這裡我想結合自己的使用經驗,談談對C++中抽象的一點淺薄看法!
我認為C++的抽象應該是指:從我們需要解決的問題出發,在與該問題相關的一組關聯對象中提取出主要的或共有的部分――說簡單一點,就是用相同的行為來操作不同的對象。
從提出問題到找出與該問題相關的對象,這是一個互動的、反復的過程。在對相關對象的抽象中,隨著熟悉的深入,我們可能會修改最初的目標,而最初目標的修改又可能使一組新的相關對象被加入進來。如:假設現在要設計一個基於廣域網的郵件服務器,首先可能需要通過socket對底層協議進行封裝,為高層的pop3、smtp協議提供一組標准的接口。開始為了使問題簡化我們可能計劃只封裝TCP/IP協議,不過基於以下兩點我們有理由修改最初的需求:
1、 pop3、smtp需要的底層接口很簡單。除了連接,僅需要發送、接收一塊數據的功能
2、 用socket進行網絡編程,大多數常見協議間的差別很小,有許多都僅僅只是初始化和連接不同而已我們只需要做很小的努力就可以兼容大多數常用協議(如:ATM、Ipx、紅外線協議等)。
現在決定修改需求,除了TCP/IP協議,還要支持一些其他的的常用協議。通過對最初目標的修改,除了TCP/IP協議對象,又會有一組相關的協議對象被加入進來。我們可以很輕易從這組相關對象中提出共有的部分,將他抽象到另一個公共對象中。當然,根據具體應用環境不同,這可能並不是最佳方案。
C++中常規的抽象是在一組相互間有“血緣”關系的類中展開的。如:
Class Parent
{
virtual ~Parent(){};
virtual void GetValue(){ .... };
virtual void Test(){ ... };
};
class child1 : public parent
{
virtual ~child1(){};
virtual void GetValue(){...};
virtual void Test(){ ... } const;
};
class child2 : public parent
{
virtual ~child2(){};
virtual void GetValue(){...};
virtual void Test(){ ... } ;
};
(順便說一句,child1::Test() const 不是基類 parent::Test() 的重載。)
由上可總結出C++中抽象的一些基本特點:
1、被抽象對象必須直接或間接派生至某個類對象
2、假如你不用沒有類型安全的操作,如:向下轉型操作或強制類型轉化操作(像COM那樣)。那麼派生類中需要抽象的動作必須在某個基類中出現。
3、 基類的析構函數必須是一個虛函數(嗯.... 有點無賴!)
................
上述特點一般而言不會影響我們的抽象,但在一些非凡情況下就很難說了。比如:
假設為某個項目進行二次開發,到手的資料可能就是一堆dll、一堆頭文件和一堆文檔。這些dll裡輸出了很多的類,其中有一大堆都是離散的、毫無關系的類對象。經過一段時間的開發,你可能發現為了分別操作這些對象,程序中布滿了switch...case.../if....else....語句。更擾人的是其實這些對象完全可以從某個基類派生,有些操作完全可以定義成virtual function。但在不能修改source code 的情況下(其實就算有源代碼這樣的修改也不可行)如何對這組對象進行抽象呢?
還有一些例子,比如:在MFC中,假設我們從Cdialog派生一組對話框類,假如我們在某個派生類中定義了一個自己的virtual function。那麼除了重新在Cdialog和派生類之間再派生一個類層次,我們無法從外部以抽象的方式直接調用這個虛函數。但為了一個派生類和一個virtual function就添加一個類層次,這也太.....
將以上特例總結一下:C++中進行抽象的一組類之間必須有“血緣”關系。但在實際應用中我們有
時候有必要對一組離散的、沒有關系的類對象(如來自不同的類庫或者根本就沒有virtual function)進行一些抽象操作――可能因為工作關系,我接觸這種情況的機會比較多。傳統的C++沒有直接提供這方面的支持。在實際應用中我經常使用如下方法:
#include <list>
class parent
{
public:
virtual ~parent(){};
virtual void DoSomething( void ) const = 0;
};
template< typename T >
class child : public parent
{
public:
virtual ~child()
{
delete tu;
}
child( ):
{
tu = new T;
}
void DoSomething( void ) const
{
tu->InitObj();
}
private:
T *tu;
};
class test
{
public:
void InitObj( void )
{
::MessageBox( NULL, "Test", "test...ok!", MB_OK );
}
};
int main()
{
using namespace std;
list< parent* > plist;
parent *par = new child<test>();
plist.push_back( par );
..................
}
以上方法用模板的方式來產生對象的代理。優點是完全未損失C++類型安全檢查的特性,class object的一般普通成員函數就可以進行抽象調用了。缺點是調用的函數名被事先確定了――但這往往是不能接受的。為了改進這一點我在後來的使用中引入了member function pointer。代碼如下:
#include<list>
class parent
{
public:
virtual ~parent(){};
virtual void do1( void ) const = 0;
virtual int do2( char* ) const = 0;
};
template< typename T >
class child : public parent
{
typedef void (T::*PFUN1)( void );
typedef int (T::*PFUN2)( char* );
public:
virtual ~child()
{
delete tu;
}
//////////////////////////////////////
child( PFUN1 p1 ):
fun1(p1), fun2(NULL)
{
tu = new T;
}
//------------------------------------
child( PFUN2 p2 ):
fun1(NULL), fun2(p2)
{
tu = new T;
}
//-------------------------------------
child( PFUN1 p1, PFUN2 p2 ):
fun1(p1), fun2(p2)
{
tu = new T;
}
////////////////////////////////////////
int do2( char *pch ) const
{
return fun2?(tu->*fun2)( pch ) : -1;
}
void do1( void ) const
{
fun1?(tu->*fun1)() : -1;
}
private:
T *tu;
PFUN1 fun1;
PFUN2 fun2;
};
class test
{
public:
void test1( void )
{
::MessageBox( NULL, "Test", "test...ok!", MB_OK );
}
};
int main()
{
using namespace std;
list< parent* > plist;
parent *par = new child<test>( test::test1 );
plist.push_back( par );
.........................
}
在這個例子中我只引用了兩種類型的member function pointer:
typedef void (T::*PFUN1)( void );
typedef int (T::*PFUN2)( char* );
按上面的方法很輕易擴展到其他函數類型。ConstrUCt child( PFUN1 p1, PFUN2 p2 )只是為了說明一個class object可以注冊多種方法。更好的做法可能是將函數注冊功能獨立成一個函數。
總體來說以上方法只能作為一個特例來看。我們總是應該以常規的C++的方式進行抽象。C++中關於抽象的一些限制並不是我們進行抽象的阻礙。我們應該把它們看作“橋上的欄桿”,他們可以保證我們的邏輯思維沿著正確地方向前進!
歡迎交流:
[email protected]