淺談C與C++的設計與編程風格(二)

上次總結了C++(面向對象)設計的核心思想,並且例舉了使用類模型來替代if和switch的一種較爲典型的情況。下面想來談談C++在編碼方面的特點。
在很多經典的C++教程中都有一個建議:應儘量使用戶代碼(庫的使用者)看起來短小而簡單。按照常識,簡單的代碼通常要比大段的代碼好理解,而用戶代碼通常實現的是最上層的功能或者界面,它的不確定性更大,經驗告訴我們,最容易出錯的代碼正是那些被頻繁修改的代碼!因此簡單的用戶代碼是有好處的,在開發一個庫時我們應儘量遵守這個建議。
C++有兩個機制可以幫助我們實現這一點,一個是運算符重載,另一個就是模板。
C++的運算符重載機制非常強大,這裏就舉一個簡單的例子。C語言中有一種對數組的特殊初始化方式,比如:int a[4] = {1,4,2,3}; 這樣的語法非常清晰且容易讓人理解,然而很可惜的是,這樣的語法不能用於對數組的賦值,對數組的賦值必須使用一個循環操作,當這個數組中的內容不是按順序排列時,對數組的賦值很可能會演變成一個非常複雜的過程。
但是在C++中,我們卻可以利用重載運算符來實現一種相似的賦值語法,方法就是我們設計一個迭代器,並重載該迭代器的 , 運算符,然後再設計一個可以使用這個迭代器的數組類,並重載這個數組的=運算符,它們的代碼大致會是這樣:
// 一個重載了,運算符的迭代器類
class CCopyIterator
{
    int * m_piItem;
public:
    CCopyIterator(int in_aiItems[])
    {
        m_piItem = in_aiItems;
    };
    CCopyIterator & operator ,(int in_iVal)
    {
        *(m_piItem++) = in_iVal;
        return *this;
    }
};
//可以使用CCopyIterator賦值的數組
class CSmartArray
{
    int m_aiItems[100]; // 這裏使用一個定長的數組是爲了迴避回收內存空間的問題,畢竟這不是我們要討論的主要問題
public:
    CCopyIterator operator =(int in_iFirstVal)
    {
        CopyIterator rtn(m_aiItems);
        return (rtn, in_iFirstVal);
    }
};
 
//用戶代碼:
CSmartArray sa;
sa = 2, 0, 1, 0;
sa = 110, 119, 120;
//怎麼樣,很酷吧?
在這個例子中我們不難發現,這個CCopyIterator只能給int的數組賦值,而且這個CSmartArray也只能存放int型的數據,如果我們想給任意類型的數組都使用這樣的語法賦值該怎麼辦呢?很簡單,使用模板!
// 被改進的CCopyIterator
template<typename val_type> class CCopyIterator
{
    val_type * m_pItem;
public:
    CCopyIterator(val_type in_aFirstItem[])
    {
        m_pItem = in_aFirstItem;
    };
    CCopyIterator & operator ,(const val_type & in_rVal) // 我們稍稍調整一下參數的類型,不妨思考一下爲什麼要做這樣的調整
    {
        *(m_pItem++) = in_rVal;
        return *this;
    }
};
//被改進的CSmartArray類
template<typename val_type, const size_t ARRAY_LENGTH> class CSmartArray
{
    val_type m_aItems[ARRAY_LENGTH]; // 這裏我們通過一個模板參數來定義數組的大小
public:
    CCopyIterator<val_type> operator =(const val_type & in_rFirstVal) // 我們稍稍調整一下參數的類型
    {
        CopyIterator<val_type> rtn(m_aItems);
        return (rtn, in_rFirstVal);
    }
};
 
//用戶代碼:
CSmartArray<char, 20> sa1;
sa1 = '2', '0', '1', '0';
CSmartArray<shot, 100> sa2;
sa2 = '中', '華', '人', '民', '共', '和', '國', '萬', '歲';
//這裏只是爲了演示不同類型的CSmartArray的使用,實際上對於字符串的賦值,可以有更好的方式
從以上的例子可以看出,使用運算符重載和模板,可以大大的簡化用戶代碼,使得用戶代碼變得更容易理解!另外,模板除了能簡化代碼外,還有更強大的功能,以後會繼續討論。
下面我們再來看一下如何使用模板函數來及簡化用戶代碼。在C語言中,很多時候我們都需要用到for循環語句,而且循環中可能會需要做很多的判斷,這樣的代碼往往非常難以理解,修改的時候也很容易出錯,這時候一個ForEach模板函數就會非常有用:
//一個最常用的ForEach模板函數
template<typename val_type, typename func_type>
void ForEach(val_type & io_rBegin, const val_type & in_rEnd, func_type in_routine)
{
    while(io_rBegin != in_rEnd)
        in_routine(io_rBegin++); // in_routine可以是一個函數指針,也可以是一個函數對象
}
這裏ForEach函數所做的就是從io_rBegin到in_rEnd,對每一個元素都調用in_routine,如果用一個相似的C函數來做個比較:
typedef void (*for_each_routine_t)(int & rVal);
void ForEachInt_CStyle(int & io_riBegin, const int & in_riEnd, for_each_routine_t in_pfnRoutine)
{
    while(io_riBegin != in_riEnd)
        in_pfnRoutine(io_riBegin++);
}
不難發現,這個ForEach模板函數能夠比ForEachInt_CStyle函數適用於更多類型的數據,然而最重要的是,ForEachInt_CStyle調用的in_routine只能是隻有一個參數的函數指針(這使得這個函數的實用性幾乎爲0),而ForEach模板函數卻不同,它的in_routine參數還可以是一個函數對象。什麼是函數對象?就是一個重載了()運算符的類的對象,比如:
//一個做累加的函數對象類
class FSum
{
public
    int m_iRtn;
    FSum()
    {
        m_iRtn = 0;
    }
    void operator ()(int in_iVal)
    {
        m_iRtn += in_iVal;
    }
};
不難發現,使用函數對象的好處是,它的參數是可以擴展的(例子中的m_iRtn成員變量就是這個函數對象類的返回參數),正是函數對象的這種靈活性,使得將它和ForEach模板函數配合使用,可以實現很多功能!
//使用ForEach函數做累加
//首先我們將FSum改寫成模板
template<typename val_type> class FSum
{
public
    val_type m_Rtn
    FSum()
        : m_Rtn(0) // 使用顯式構造語法是爲了適應val_type參數是一個類時的情形
    {
    }
    void operator ()(const val_type & in_rVal)
    {
        m_Rtn += in_rVal;
    }
};

// 用戶代碼:
FSum<int> foSum;
int i = 1;
ForEach(i, 100, &foSum);

//再來看一段C實現:
int sum = 0;
for (int i = 1; i <= 100; ++i)
    sum += i;
從表面上看,上面這個例子中,使用ForEach模板函數似乎並沒有帶來多少好處(同時也說明不是所有地方都應該套用ForEach模板函數),但是在一些更復雜的算法中,比如遍歷搜索,使用ForEach模板函數的好處就能體現出來!另外C++標準模板庫的sort模板函數,就是一個典型的ForEach型模板函數。
(未完待續)

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章