本篇摘要
交換兩個變量是非常古老的話題了,然而本文絕對保證給你新鮮的感覺!本文涉及到最簡單的“不用臨時變量交換兩個整數”還涉及到如果利用異或來實現兩個指針、兩個浮點數的交換,要知道指針的浮點數是不允許直接異或運算的哦;同時本文還闡述了如何交換用戶自定義類型及其指針。
本文完全是個人自由發揮之作,歡迎廣大磚家來拍磚,我個人感覺文中必然有很多不足,甚至錯誤之處,這并非我謙虛,事實上我寫本文的目的就是希望在挨磚板中成長!新人或許看不懂很多東西,如果你看不懂,那么就不要隨便膜拜,因為看不懂的或許原本就是錯的,高手看完后如果本文寫得還可以,那么請留下好評,以供新手參考本文是否有閱讀本文的必要,如果覺得本文完全是垃圾,那么請不要客氣,您可以赤裸地,露骨地指出本文的錯誤和不足,讓本人在批評中進步,但請不要進行人身攻擊,謝謝!
準備工作
由于本文涉及到交換兩個用戶自定義類型的變量,為了舉例方便,本文定義如下的Person類(其中省略了拷貝構造函數的重寫,因為本文不用到它):
class Person
{
public:
Person(int age ,const char* name ):m_Age(age)
{
int len = strlen(name);
this->m_Name = new char[len+1];
strcpy(this->m_Name,name);
}
Person()
{
this->m_Age = -1;
this->m_Name = 0;
}
void PrintSelf()
{
cout<<this->m_Name<<":"<<this->m_Age<<endl;
}
Person& operator= (const Person& other)
{
if (this == &other)
{
return *this;
}
else
{
this->m_Age = other.m_Age;
delete this->m_Name;
int len = strlen(other.m_Name);
this->m_Name = new char[len+1];
strcpy(this->m_Name,other.m_Name);
return *this;
}
}
~Person()
{
delete this->m_Name;
}
private:
int m_Age;
char* m_Name;
};
為了后文表述方便,這里再定義Person類的兩個對象和兩個指針,定義如下:
Person
youngMan(18,” young man”);
Person
oldMan(81,” old man”);
Person*
pYoungMan = &youngMan;
Person*
pOldMan = &oldMan;
最常見的交換兩個對象的方法:GeneralSwap
通常,我們為了交換兩個變量都采取下面的方法來實現,它需要一個臨時變量:
template<class T>
void GeneralSwap(T& a,T& b)
{
T temp;
temp = a;
a = b;
b = temp;
}
顯然人人都知道這個寫法,但是我仍然覺得有必要重點申明幾點:1、注意函數的參數是引用(也可指針),為什么我就不解釋了;2、這個交換函數基本上是最簡單、最通用的,簡單到人人都會寫,通用到它幾乎可適用于任何數據類型:char , int , long , float, double等各種系統自定義數學類型(無符號的,帶符號的),用戶自定義數據類型(需要有默認構造函數,否則語句T temp;會報錯),以及各種指針(系統自定義類型的指針,和用戶自定義類型的指針)。當然用戶自定義類型中如果包含了指針數據成員,那么需要重載賦值運算符,事實上這樣的用戶自定義類,你都應該自己重寫賦值運算符、拷貝構造函數,否則不但不能使用GeneralSwap,其他涉及到拷貝和賦值的操作都可能導致出錯!
利用GeneralSwap交換兩個用戶自定義對象
下面深入探討一下關于用戶自定義對象的交換問題:針對準備工作中的Person類的兩個對象youngMan和oldMan語句GeneralSwap(youngMan,oldMan);能實現他們的交換。短短一行代碼就能實現將一個18歲的花季少男跟一個81歲的老頭子掉包,這像不像是耍魔術啊,呵呵。要注意了,該交換代碼雖短,但涉及到默認構造函數的調用(GeneralSwap中的T temp;語句)和賦值運算符重載函數的調用(GeneralSwap中的三個賦值語句)。
或許您很少這么用吧,事實上在我寫本文之前,我都沒真正交換過兩個自定義的對象,通常我們都不愿意這么交換兩個自定義對象。原因是效率太低!或許你要問,萬一有的應用就是需要交換兩個自定義的對象怎么辦?好辦,用指針啊!對,指針的好處就是效率高,為什么C++比java效率高,原因之一就是java取消了指針。下面的第一行代碼就是交換兩個Person類的指針:
GeneralSwap(pYoungMan,pOldMan);
//GeneralSwap(*pYoungMan,*
pOldMan); //效率低
為什么使用指針就效率高了呢?原因是指針就是地址,地址就是整數,于是問題等價于交換兩個整數,因此它不調用賦值運算符重載函數!只要你在應用程序中始終通過指向對象的指針來訪問對象,那么交換兩個指針就能達到交換對象的目的。注意被注釋掉的第二行代碼,它是正確的,但是它又回到了交換兩個實際對象,其效率低,最好不要這么用!
對于這個最常見、最簡單的GeneralSwap我都廢話了一大堆,而且還扯出了一個沒多少用的關于用戶自定義對象的交換問題,這實屬個人思維散射,請磚家們狠狠地拍。
在進行下一個方法之前,再次強調一點,這個方法的特點是簡單、通用!后面的方法都將與之做比較。
利用加減法實現兩個數的交換
幾乎人人都知道還可以利用加減法來實現兩個數的交換,其代碼也異常簡單:
template<class T>
void Add_Sub_Swap_1(T& a, T& b)
{
a = a+b;
b = a-b;
a = a-b;
}
Add_Sub_Swap_1可以用于交換兩個整數,但由于涉及加減法,因此有數據溢出的危險;也可以用于交換浮點數,但是有可能由于舍入誤差導致結果不準確。
Add_Sub_Swap_1不能用于交換兩個用戶自定義的對象,下面的語句編譯就通過不,編譯器告訴你Person類沒有定義operator +等符號:
Add_Sub_Swap_1(youngMan,oldMan);//編譯通不過!
Add_Sub_Swap_1不能用于交換兩個指針,語句Add_Sub_Swap_1(pYoungMan,pOldMan);編譯時將報錯:error C2110:
cannot add two pointers,是的,兩個指針不能直接做加法運算(減法是可以的)。那么是不是就不能利用加減法實現兩個指針的交換呢?答案是:“可以!”,接下來我將闡述如何實現。
利用加減法交換兩個指針
Add_Sub_Swap_1不能用于交換兩個指針,前面我說可以用加減法來實現兩個指針的交換,這是有根據的:指針仍然是變量,只不過它是存儲普通變量的地址的變量。只要我們把指針“看作”變量,那么就能實現加法。那么如何把指針“看作”變量呢?答案是:“通過強制類型轉換”!指針表示變量的地址,在32位平臺上它是一個無符號的整數,因此可以將指針強制轉換為無符號類型的整數。我對上面的Add_Sub_Swap_1進行了改進:
template<class T>
void Add_Sub_Swap_2(T& a, T& b)
{
*(( unsigned*)(&a)) = *(( unsigned*)(&a)) + *(( unsigned*)(&b));
*(( unsigned*)(&b)) = *(( unsigned*)(&a)) - *(( unsigned*)(&b));
*(( unsigned*)(&a)) = *(( unsigned*)(&a)) - *(( unsigned*)(&b));
}
利用Add_Sub_Swap_2既可以交換兩個普通的整數、浮點數同時它可以交換兩個任意類型的指針(包含系統預定義類型和用戶自定義類型的指針,其實本質上所有指針都屬于同一種類型:32位無符號整數類型)。不信您試試Add_Sub_Swap_2(pYoungMan,pOldMan);它能得到正確答案。
雖然Add_Sub_Swap_2解決了Add_Sub_Swap_1無法交換兩個指針的問題,但是它仍然無法交換兩個用戶自定義類型的變量,原因是用戶自定義類型沒有加減法運算。看來要想用加減法實現兩個用戶定義類型的交換是不可能的了(除非用戶自定義的operator+和operator-能滿足交換兩個對象的目的,這很難,除非是非常簡單的用戶自定義類型,比如你不使用系統類型int非要定義一個MyInt類)。
利用異或實現兩個整數的交換
同樣地,幾乎人人都知道利用異或來交換兩個數,其實現也非常簡單:
template <class T>
void Xor_Swap_1(T& a,T& b)
{
a = a^b;
b = a^b;
a = a^b;
}
上面的函數的實用性非常有限,它只能交換兩個整數(包含char,int,long),要想交換兩個浮點數是不行的,因為浮點數不能參與位運算,要想交換兩個指針也是不行的,編譯器不允許你把兩個指針拿來做位運算,要想交換兩個用戶自定義對象也是不行的,因為它仍然不能參與位運算。那么是不是利用異或交換兩個變量就沒法用于浮點數、指針和用戶自定義的對象了呢?答案是“能”!后面幾節我將闡述這些問題。
利用異或實現兩個float和指針的交換
前面的Xor_Swap_1無法實現兩個浮點數和指針的交換,其原因是浮點數和指針均不直接支持位運算。那么如何才能利用異或來交換兩個浮點數和指針呢?方法仍然是“強制類型轉換”!因為浮點數在內存中仍然是用一串二進制bit來表示的嘛,只要把浮點數看作(強制類型轉換)二進制bit構成的整數,那么就能進行位運算了,至于指針嘛,處理方法完全相同。具體如何做呢,其實現大概是這樣的:
template <class T>
void Xor_Swap_2(T& a,T& b)
{
*((unsigned*)(&a)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
*((unsigned*)(&b)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
*((unsigned*)(&a)) = *((unsigned*)(&a)) ^ *((unsigned*)(&b));
}
利用這個函數可以交換兩個float類型的變量,也可以交換任意類型的指針!非常值得注意的是:用它交換兩個double類型數據或者兩個Person類的對象(youngMan,oldMan)均能編譯通過,但是其結果卻是錯的。至于為什么,以及如何解決,這將是我下一節要闡述的內容。
利用異或實現兩個double類型變量和用戶自定義變量的交換
Xor_Swap_2解決了利用異或不能交換兩個float數據和指針的問題,然而它卻不能正確地交換兩個double數據和兩個Person類對象。這是為什么呢?原因是函數內部是把參數強制類型轉換成unsigned類型的,而sizeof(float)和sizeof(pointor)的值都等于sizeof(unsigned),但是sizeof(double)卻不等于sizeof(unsigned),也就是說把double強制轉換成unsigned類型時,發生了“位截斷”(在概念是區別與數據截斷),那么得到的結果肯定就不對了。至于無法交換兩個Person類對象,其原因也相同。
這里我要深入分析一下強制類型轉換是如何發生位截斷的,首先看看以下測試的輸出結果,注意代碼中的注釋,為了節約篇幅,我把值得注意的地方都放在注釋中了:
Double a = 1.0,b=2.0;
Xor_Swap_2(a,b);//交換兩個double數據
Cout<<a<<b;//輸出仍然是1.0和2.0,a,b的值并未改變
Xor_Swap_2(youngMan,oldMan);//交換兩個用戶自定義對象
youngMan.PrintSelf();//輸出young man:81
oldMan.PrintSelf();//輸出old man:18
可以看出兩個double數據并沒被交換,而兩個Person對象在交換之后發生了怪異現象:產生了81歲的年輕人和18歲的老年人!這一點正好說明強制類型轉換時發生了位截斷,由于Person類的第一個數據成員m_Age正好是int型,在Xor_Swap_2內部做強制類型轉換時正好取得了兩個對象的m_Age成員,于是出現了兩個對象被部分交換的情況,那么又如何解釋兩個double數據沒有變法呢?事實上兩個double數據仍然發生了部分交換,因為這里的兩個double數(a,b)的前4個字節正好相同,因此看不出部分交換。
既然我們知道了Xor_Swap_2為什么不能用于交換兩個double類型的數據和兩個用戶自定義的數據,那么就有辦法對它進行改進。具體改進的思想就是把參數按照一個byte一個byte地分別異或,按照這個思路我實現了如下的函數:
template <class T>
void Xor_Swap_3(T& a,T& b)
{
int size = sizeof(T);
for (int i = 0;i<size;i++)
{
*((unsigned char*)(&a)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
*((unsigned char*)(&b)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
*((unsigned char*)(&a)+i) = (*((unsigned char*)(&a)+i)) ^ (*((unsigned char*)(&b)+i));
}
}
這個版本的函數不僅能交換兩個整數、任何指針、float數和double數,更牛逼的是它能交換兩個用戶定義類型的變量!事實上它基本上是在內存一級上操作數據,而任何類型的數據對象最終都表現為內存對象。這其實就是通過內存拷貝實現兩個對象的交換的一個版本吧,當然還有利用memcpy等手段進行內存拷貝來實現兩個變量的交換的,這里我就不贅述了。
結束語
本篇到此寫完了,有種不好的感覺,因為文中大量使用“強制類型轉換”而這個東西是C++中容易出錯的地方,而我再寫本文時,并沒有去復習關于強制類型轉換的相關知識,因此擔心很多地方有潛在的出錯可能,還請各位磚家指正!
@import url(http://www.shnenglu.com/CuteSoft_Client/CuteEditor/Load.ashx?type=style&file=SyntaxHighlighter.css);@import url(/css/cuteeditor.css);