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