數組和指針
首先我們來看一個經典的C/C++求數組元素個數的解決方案:
#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof((x)[0]))
利用C/C++編譯器將表達式 ar[n] 在編譯器解釋為 *(ar+n) 的特性,我們可以提供一個更“先進”的版本:
#define NUM_ELEMENTS(x) (sizeof((x)) / sizeof(0[(x)]))
這種下標索引的可交換性,只對內建的下標索引操作符有效,這一限制可以被用于約束NUM_ELEMENTS宏只對數組/指針有效,而拒絕重載了下標索引操作符的類類型。
接下來,我們來看NUM_ELEMENTS的實際運用:
int ar[10];
cout << NUM_ELEMENTS(ar) << endl; // 毋庸置疑,輸出為10。
...
void fn(int ar[10])
{
cout << NUM_ELEMENTS(ar) << endl; // 本行結果呢?如果你說10,那么將會使你大失所望,
} // 事實上,程序輸出為1。
看起來一切都井井有條,問題究竟出在哪里呢?呃,是這樣的,在C/C++中你無法將數組傳給函數!在C里面,數組被傳遞函數時總是被轉換成指針,非常干脆地阻止了你獲取數組大小的企圖。而C++基于兼容性的考慮,亦是如此。
所以先前給出的NUM_ELEMENTS宏定義依賴于預處理器進行的文本替換,因而存在一個嚴重的缺陷:如果我們(不小心)將它應用到一個指針身上,文本替換出來的結果將是錯誤的。
幸運的是,我們可以利用大多數現代編譯器都支持的一個特性來將數組和指針區別對待,那就是:從數組到指針的轉換(退化)在引用類型的模板實參決議中并不會發生。因而我們可以重新定義NUM_ELEMENTS宏為:
template<int N>
struct array_size_struct
{
byte_t c[N];
};
template<typename T, int N>
array_size_struct<N> static_array_size_fn(T(&)[N]);
#define NUM_ELEMENTS(x) sizeof(static_array_size_fn(x).c)
其基本原理是:聲明(但不定義)一個模板函數,它接受一個元素類型為T、大小為N的數組的引用。這樣一來,指針類型以及用戶自定義類型就被拒之門外了(編譯報錯)。并且由于C++標準中,sizeof()的操作數不會被求值,所以我們無需定義static_array_size_fn()函數體,從而上述設施完全是零代價的。沒有任何運行時開銷,也不會導致代碼膨脹。
讓我們回到“C/C++數組在被傳遞給函數時會退化成指針”的問題上來,如果我們在現實中需要將一個將任意長度的數組傳遞給一個期望接受數組的函數,那么該怎么辦呢?困惑的實質在于數組的大小在傳遞過程中丟失了,因此,如果我們可以找到一種將數組大小隨之傳遞給函數的機制,問題就會迎刃而解。有了上面宏定義的經驗,通過模板我們找到一個解決方案:
template<typename T>
class array_proxy
{
public:
typedef T value_type;
typedef array_proxy<T> class_type;
typedef value_type * pointer;
typedef value_type * const_pointer; // Non-const!
typedef value_type & reference;
typedef value_type & const_reference; // Non-const!
typedef size_t size_type;
// 構造函數
public:
template<size_t N>
explicit array_proxy(T(&t)[N]) // 元素類型為T的數組
: m_begin(&t[0])
, m_end(&t[N])
{}
template<typename D, size_t N>
explicit array_proxy(D(&d)[N]) // 元素類型為T兼容類型的數組
: m_begin(&d[0])
, m_end(&d[N])
{
constraint_must_be_same_size(T, D); // 確保D和T大小相同
}
template<typename D>
array_proxy(array_proxy<D> &d)
: m_begin(d.begin())
, m_end(d.end())
{
constraint_must_be_same_size(T, D); // 確保D和T大小相同
}
// 狀態
public:
pointer base();
const_pointer base() const;
size_type size() const;
bool empty() const;
static size_type max_size();
// 下標索引操作符
public:
reference operator [](size_t index);
const_reference operator [](size_t index) const;
// 迭代操作
public:
pointer begin();
const_pointer begin() const;
pointer end();
const_pointer end() const;
// 數據成員
private:
pointer const m_begin;
pointer const m_end;
// 聲明但不予實現
private:
array_proxy & operator =(array_proxy const &);
};
// 轉發函數
template<typename T, size_t N>
inline array_proxy<T> make_array_proxy(T(&t)[N])
{
return array_proxy<T>(t);
}
template<typename T>
inline array_proxy<T> make_array_proxy(T * base, size_t size)
{
return array_proxy<T>(base, size);
}
客戶代碼修改為:
void process_array(const array_proxy<int> & ar)
{
std::copy(ar.begin(), ar.end(), ostream_iterator<int>(cout, " "));
}
int _tmain(int argc, _TCHAR* argv[])
{
int ar[5] = {0, 1, 2, 3, 4};
process_array(make_array_proxy(ar));
return 0;
}
我們的問題終于有了一個徹底的解決方案。該解決方案是高效的(在任何一個說得過去的編譯器上它都沒有任何額外的開銷),是類型安全的,并且完全使得函數的設計者能夠防止潛在的誤用(更確切的說,讓代碼能夠更強的抵御派生類數組的誤用)。此外,它還足夠智能,允許派生類跟父類具有相同大小的情況下,它們的數組被“代理”。
最后一個優點是現在再也不可能將錯誤的數組長度傳給被調函數了,以前我們慣用的使用兩個參數(一個傳遞數組指針,一個傳遞數組長度)的函數版本中誤傳長度的危險是時時存在的。這個優勢使我們得以遵從DRY(Don't Repeat Yourself!)原則。
NULL宏
在C語言中,void*類型可以被隱式地轉換為其他任何指針類型,所以我們可以將NULL定義為((void*)0),從而跟其他任何指針類型間實現互相轉換。然而,C++不允許從void*到任何指針的隱式轉換,又因為C++中0可以被轉換為任何指針類型,因此,C++標準規定:NULL宏是一個由實現定義的C++空指針常量....其可能的定義方式包括0和0L,但絕對不是(void*)0。
由于0不可置疑的可以轉換成任何整型,甚至wchar_t和bool,以及浮點類型,這就意味著,使用NULL的時候,類型檢查將不再發生,我們很容易毫無察覺的走向厄運的深淵,連個警告都沒有。考慮如下情況:
// 自定義的字符串類
//
class String
{
explicit String(char const *s); // 接受外界傳入的空指針
explicit String(int cch, char chInit = '\0'); // 根據可能被使用的字符數估計,來初始化底層存儲
};
現在當我們用NULL做參數構造String時,第二個構造函數會被調用!也許和你的初衷不同,編譯器卻會一聲不吭的編譯通過。這可不妙。如果你將int改為size_t(或short、或long、或任何不是int的內建類型),編譯器將會在兩個轉換之間左右為難,結果是得到一個二義性錯誤。
我們想要個完善的空指針關鍵字!很快作者想出了辦法,你不應該感到驚訝,解決方案離不開模板:
struct NULL_v
{
// 構造函數
public:
NULL_v()
{}
// 轉換操作符
public:
template<typename T>
operator T* () const
{
return 0;
}
template<typename T2, typename C>
operator T2 C::*() const
{
return 0;
}
template<typename T>
bool equals(T const & rhs) const
{
return rhs == 0;
}
// 聲明但不予實現
private:
void operator &() const; // Scott: 純粹是值的東西的地址是沒有任何意義的。
NULL_v(NULL_v const &);
NULL_v& operator =(NULL_v const &);
};
template<typename T>
inline bool operator ==(NULL_v const & lhs, T const & rhs)
{
return lhs.equals(rhs);
}
template<typename T>
inline bool operator ==(T const & lhs, NULL_v const & rhs)
{
return rhs.equals(lhs);
}
template<typename T>
inline bool operator !=(NULL_v const & lhs, T const & rhs)
{
return !lhs.equals(rhs);
}
template<typename T>
inline bool operator !=(T const & lhs, NULL_v const & rhs)
{
return !rhs.equals(lhs);
}