有兩種函數允許編譯器進行這些的轉換:單參數構造函數(single-argument constructors)和隱式類型轉換運算符。單參數構造函數是指只用一個參數即可以調用的構 造函數。該函數可以是只定義了一個參數,也可以是雖定義了多個參數但第一個參數以後的 所有參數都有缺省值。
第一個例子:
class Name {
public:
Name(const string& s);
... };
class Rational {
public:
Rational(int numerator = 0,
int denominator = 1);
...
// for names of things
// 轉換 string 到
// Name
// 有理數類
// 轉換 int 到
// 有理數類
};
第二個例子:隱式類型轉換運算符只是一個樣子奇怪的成員函數:operator 關鍵字,其後跟一個類
型符號。
class Rational {
public: ...
operator double() const;
};
// 在下面這種情況下,這個函數會被自動調用: Rational r(1, 2);
double d = 0.5 * r;
// 轉換 Rational 類成
// double 類型
// r 的值是 1/2
// 轉換 r 到double,
// 然後做乘法
隱式類型轉換可能出現的問題:
#include
using namespace std;
class rational {
public:
rational(double a, double b) {
val = a / b;
}
operator double() {
return val;
}
private:
double val;
};
int main() {
rational test(3, 4);
cout << test << endl;
return 0;
}
我們本以為沒有定義operator <<,所以編譯器會報錯,但實際上編譯器會把test隱式類型轉換為double類型。這看起來很不錯,實際上回出現很多不可預計的問題。它表明了隱式類型轉換的缺點: ==它們的存在將導致錯誤的發生==。
解決方法是用不使用語法關鍵字的等同的函數來替代轉換運算符。例如為了把 Rational 對象轉換為 double,用 asDouble 函數代替 operator double 函數:?
class Rational {?public:
...
double asDouble() const;
};
// 這個成員函數能被顯式調用: Rational r(1, 2);
cout << r;
cout << r.asDouble();
//轉變 Rational // 成 double
// 錯誤! Rationa 對象沒有 // operator<<
// 正確, 用 double 類型 //打印 r
在多數情況下,這種顯式轉換函數的使用雖然不方便,但是函數被悄悄調用的情況不再 會發生,這點損失是值得的。就好像在編寫庫時,string沒有給出隱式類型轉換為char*的操作符,而是給出了c_str()來轉換就是這個道理。
以下討論單參數構造函數進行隱式類型轉換的問題。
template?class Array {?public:
Array(int lowBound, int highBound);
Array(int size);
T& operator[](int index);
...
};
第一個構造函數允許調用者確定數組索引的范圍,例如從 10 到 20。它是一個兩參數構造函數,所以不能做為類型轉換函數。第二個構造函數讓調用者僅僅定義數組元素的個數(使 用方法與內置數組的使用相似),不過不同的是它能做為類型轉換函數使用,能導致無窮的痛苦。
例如比較 Array對象,部分代碼如下:
bool operator==( const Array& lhs,
Array a(10);
const Array& rhs);
Array b(10);
...
for (int i = 0; i < 10; ++i)
if (a == b[i]) {
do something for when
a[i] and b[i] are equal;
} else {
// 哎呦! "a" 應該是 "a[i]"
do something for when they're not;
}
我們想用 a 的每個元素與 b 的每個元素相比較,但是當錄入 a 時,我們偶然忘記了數組 下標。當然我們希望編譯器能報出各種各樣的警告信息,但是它根本沒有。因為它把這個調 用看成用 Array參數(對於 a)和 int(對於 b[i])參數調用 operator==函數,然而沒有 operator==函數是這樣的參數類型,我們的編譯器注意到它能通過調用 Array構造函 數能轉換 int 類型到 Array類型,這個構造函數只有一個 int 類型的參數。然後編譯器如此去編譯,生成的代碼就像這樣:
for (int i = 0; i < 10; ++i)
if (a == static_cast< Array >(b[i])) ...
每一次循環都把 a 的內容與一個大小為 b[i]的臨時數組(內容是未定義的)比較。這 不僅不可能以正確的方法運行,而且還是效率低下的。因為每一次循環我們都必須建立和釋 放 Array對象。
解決的方法是利用一個最新編譯器的特性,explicit 關鍵字。為了解決隱式類型轉換 而特別引入的這個特性,它的使用方法很好理解。構造函數用 explicit 聲明,如果這樣做, 編譯器會拒絕為了隱式類型轉換而調用構造函數。顯式類型轉換依然合法:
template
class Array {
public:
...?explicit Array(int size); // 注意使用"explicit"
... };
Array a(10);
Array b(10);
if (a == b[i]) ...
if (a == Array(b[i])) ...
// 正確, explicit 構造函數 // 在建立對象時能正常使用
// 也正確?// 錯誤! 沒有辦法
// 隱式轉換?// int 到 Array
// 正確,顯式從 int 到 // Array轉換
// (但是代碼的邏輯
// 不合理)
if (a == static_cast< Array >(b[i])) ...
// 同樣正確,同樣
// 不合理?
if (a == (Array)b[i]) ... //C 風格的轉換也正確,
// 但是邏輯
// 依舊不合理
關於explicit:(不允許參數隱式類型轉換!)
classTest1
{
public:
Test1(intn)
{
num=n;
}//普通構造函數
private:
intnum;
};
classTest2
{
public:
explicitTest2(intn)
{
num=n;
}//explicit(顯式)構造函數
private:
intnum;
};
intmain()
{
Test1t1=12;//隱式調用其構造函數,成功
Test2t2=12;//編譯錯誤,不能隱式調用其構造函數
Test2t2(12);//顯式調用成功
return0;
}
重載函數間的區別決定於它們的參數類型上的差異,但是不 論是 increment 或 decrement 的前綴還是後綴都只有一個參數。為了解決這個語言問題,C++ 規定後綴形式有一個int類型參數,當函數被調用時,編譯器傳遞一個0做為int參數的值 給該函數:?
class UPInt {?public:
UPInt& operator++();
const UPInt operator++(int);
UPInt& operator--();
const UPInt operator--(int);
UPInt& operator+=(int);
... };
UPInt i;
++i;
i++;
--i;
i--;
值得注意的是,==前綴返回的是引用,後綴返回的是const對象==。(很容易通過前綴自增和後綴自增的區別來判讀合理性。)
UPInt& UPInt::operator++() {
*this += 1;
return *this;
}
const UPInt UPInt::operator++(int) {
UPInt oldValue = *this;
++(*this); // 增加 return oldValue;?}
如果後綴的increment不是const對象,那麼以下代碼就是正確的:
UPInt i;?i++++; // 兩次 increment 後綴 這組代碼與下面的代碼相同:
i.operator++(0).operator++(0);
C++使用==布爾表達式短路求值法==(short-circuit evaluation)。這表示一旦 確定了布爾表達式的真假值,即使還有部分表達式沒有被測試,布爾表達式也停止運算。
char *p;?...?if ((p != 0) && (strlen(p) > 10)) ...?// 這裡不用擔心當 p 為空時 strlen 無法正確運行,因為如果 p 不等於 0 的測試失敗,strlen 不會被調用。同樣:?
int rangeCheck(int index)?{?if ((index < lowerBound) || (index > upperBound)) ...?...?}
C++允許根據用戶定義的類型,來定制&&和||操作符。方法是重載函數 operator&& 和 operator||,你能在全局重載或每個類裡重載。但是你就失去了短路求值的特性。
if (expression1 && expression2) ...
// 對於編譯器來說,等同於下面代碼之一:
if (expression1.operator&&(expression2)) ...
// when operator&& is a
// member function
if (operator&&(expression1, expression2)) ...
// when operator&& is a
// global function
這好像沒有什麼不同,但是函數調用法與短路求值法是絕對不同的。首先當函數被調用時,需要運算其所有參數,所以調用函數 functions operator&& 和 operator||時,兩個 參數都需要計算,換言之,沒有采用短路計算法。第二是 C++語言規范沒有定義函數參數的 計算順序,所以沒有辦法知道表達式1與表達式2哪一個先計算。完全可能與具有從左參數 到右參數計算順序的短路計算法相反。
不能重載的部分:
. .* :: ?:?new delete sizeof typeid
static_cast dynamic_cast const_cast reinterpret_cast
能重載的部分:
operator new operator delete?operator new[] operator delete[] +-*/%^&|~
! =<>+=-=*=/=%= ^=&=|=<<>> >>=<<=== != <=>=&&||++ -- , ->*-> () []
操作符重載的目的是使程序更容易閱讀,書 寫和理解,而不是用你的知識去迷惑其他人。如果你沒有一個好理由重載操作符,就不要重 載。在遇到&&, ||, 和 ,時,找到一個好理由是困難的,因為無論你怎麼努力,也不能讓它 們的行為特性與所期望的一樣。
string *ps = new string(“Memory Management”);?
你使用的 new 是 new 操作符。這個操作符就象 sizeof 一樣是語言內置的,你不能改變它的 含義,它的功能總是一樣的。它要完成的功能分成兩部分。第一部分是分配足夠的內存以便 容納所需類型的對象。第二部分是它調用構造函數初始化內存中的對象。new 操作符總是做 這兩件事情,你不能以任何方式改變它的行為。
你所能改變的是如何為對象分配內存。new 操作符調用一個函數來完成必需的內存分 配,你能夠重寫或重載這個函數來改變它的行為。new 操作符為分配內存所調用函數的名字 是 operator new。
函數 operator new 通常這樣聲明:
void * operator new(size_t size);
void *rawMemory = operator new(sizeof(string));
操作符 operator new 將返回一個指針,指向一塊足夠容納一個 string 類型對象的內存。
就象 malloc 一樣,operator new 的職責只是分配內存。它對構造函數一無所知。 operator new 所了解的是內存分配。把 operator new 返回的未經處理的指針傳遞給一個對 象是 new 操作符的工作。?
但是有時你有一些已經被分配但是尚未處理的(raw)內存,你需要在這些內存中構造一個對象。你可以 使用一個特殊的 operator new ,它被稱為 placement new。
void * operator new(size_t, void *location)?{
return location;
}
operator new 的目的是為對象分配內存然後返回指向該內存的指針。在使用 placement new 的情況下,調 用者已經獲得了指向內存的指針,因為調用者知道對象應該放在哪裡。placement new 必須 做的就是返回轉遞給它的指針。(沒有用的(但是強制的)參數 size_t 沒有名字,以防止編 譯器發出警告說它沒有被使用。)
Operator delete 用來釋放內存,它被這樣聲明:
void operator delete(void *memoryToBeDeallocated);
因此,
delete ps;
導致編譯器生成類似於這樣的代碼:?
ps->~string(); // call the object's dtor operator
delete(ps); // deallocate the memory
// the object occupied
這有一個隱含的意思是如果你只想處理未被初始化的內存,你應該繞過 new 和 delete
操作符,而調用 operator new 獲得內存和 operator delete 釋放內存給系統:
void *buffer =
operator new(50*sizeof(char));
// 分配足夠的?// 內存以容納 50 個 char
...
operator delete(buffer);
//沒有調用構造函數
// 釋放內存 // 沒有調用析構函數
如果你用 placement new 在內存中建立對象,你應該避免在該內存中用 delete 操作符。
因為 delete 操作符調用 operator delete 來釋放內存,但是包含對象的內存最初不是被 operator new 分配的,placement new 只是返回轉遞給它的指針。誰知道這個指針來自何方? 而你應該顯式調用對象的析構函數來解除構造函數的影響:?
// 在共享內存中分配和釋放內存的函數
void * mallocShared(size_t size);?void freeShared(void *memory);?void *sharedMemory = mallocShared(sizeof(Widget));?Widget *pw = // 如上所示,
constructWidgetInBuffer(sharedMemory, 10); // 使用
...
delete pw;
pw->~Widget();
freeShared(pw);
// 結果不確定! 共享內存來自?
// mallocShared, 而不是 operator new
// 正確。 析構 pw 指向的 Widget,?// 但是沒有釋放?
//包含 Widget 的內存?
// 正確。 釋放 pw 指向的共享內存?
// 但是沒有調用析構函數
new 和 delete 操作符是內置的,其行為不受你的控制,凡是它們調用的內存分配和釋放函數則可以控制。當你想定制 new 和 delete 操作符的行為時,請記住你不能真的做到這 一點。你只能改變它們為完成它們的功能所采取的方法,而它們所完成的功能則被語言固定 下來,不能改變。