專欄簡介:本專欄主要面向C++初學者,解釋C++的一些基本概念和基礎語言特性,涉及C++標準庫的用法,面向對象特性,泛型特性高級用法。通過使用標準庫中定義的抽象設施,使你更加適應高級程序設計技術。希望對讀者有幫助!
目錄
- 13.3交換操作
- 編寫我們自己的swap函數
- swap函數應該調用swap,而不是std::swap
- 在賦值運算符中使用swap
13.3交換操作
除了定義拷貝控制成員,管理資源的類通常還定義一個名為swap的函數。對于那些與重排元素順序的算法一起使用的類,定義swap是非常重要的。這類算法在需要交換兩個元素時會調用swap。
如果一個類定義了自己的swap,那么算法將使用類自定義版本。否則,算法將使用標準庫定義的swap。雖然與往常一樣我們不知道swap是如何實現的,但理論上很容易理解,為了交換兩個對象我們需要進行一次拷貝和兩次賦值。例如,交換兩個類值HasPtr對象的代碼可能像下面這樣:
HasPtr temp=v1;//創建v1的值的一個臨時副本
v1 = v2}//將v2的值賦予v1
v2=temp;//將保存的v1的值賦子v2
這段代碼將原來v1中的string拷貝了兩次一一第一次是HasPtr的拷貝構造函數將v1拷貝給temp,第二次是賦值運算符將temp賦予v2。將v2賦予v1的語句還拷貝了原來v2中的string。如我們所見,拷貝一個類值的HasPtr會分配一個新string并將其拷貝到HasPtr指向的位置。
理論上,這些內存分配都是不必要的。我們更希望swap交換指針,而不是分配string的新副本。即,我們希望這樣交換兩個HasPtr:
string*temp=v1.ps;//為v1.ps中的指針創建一個副本
v1.ps=v2.ps;//將v2.ps中的指針賦孫v1.ps
v2.ps=temp;//將保孫的v1.ps中原來的指針賦子v2.ps
編寫我們自己的swap函數
可以在我們的類上定義一個自己版本的swap來重載swap的默認行為。swap的典型實現如下:
class HasPtr{
friend void swap(HasPtr&,HasPtr&);
//其他成員定義
};
inline
void swap(HasPtr&lhs,HasPtr&rhs)
{
using std::swap;
swap(lhs.ps,rhs.ps);//交換指針,而不是string數據
swap(lhs.i,rhs.i);//交換int成員
}
我們首先將swap定義為friend,以便能訪問HasPtr的(private的)敏據成員。由于swap的存在就是為了優化代碼,我們將其聲明為inline函數。swap的函數體對給定對象的每個數據成員調用swap。我們首先swap綁定到rhs和lhs的對象的指針成員,然后是int成員。
與拷貝控制成員不同,swap并不是必要的。但是,對于分配了資源的類,定義swap可能是一種很重要的優化手段。
swap函數應該調用swap,而不是std::swap
此代碼中有一個很重要的微妙之處:雖然這一點在這個特殊的例子中并不重要,但在一般情況下它非常重要一一swap函數中調用的swap不是std::swap。在本例中,數據成員是內置類型的,而內置類型是沒有特定版本的swap的,所以在本例中,對swap的調用會調用標準庫std::swap。
但是,如果一個類的成員有自己類型特定的swap函數,調用std::swap就是錯誤的了。例如,假定我們有另一個命名為Foo的類,它有一個類型為HasPtr的成員h。如果我們未定義Foo版本的swap,那么就會使用標準庫版本的swap。如我們所見,標準庫swap對HasPtr管理的string進行了不必要的拷貝。
我們可以為Foo編寫一個swap函數,來避免這些拷貝。但是,如果這樣編寫Foo版本的swap:
void swap(Foo& lhs,Foo &rhs)
{
//錯誤:這個函數使用了標準庫版本的swap,而不是HasPtr版本
std::swap(lhs.h,rhs.h);
//交換類型Foo的其他成員
}
此編碼會編譯通過,且正常運行。但是,使用此版本與簡單使用默認版本的swap并沒有任何性能差異。問題在于我們顯式地調用了標準庫版本的swap。但是,我們不希望使用std中的版本,我們希望調用為HasPtr對象定義的版本。
正確的swap函數如下所示:
void swap(Foo &lhs,Foo &rhs){
using std::swap;
swap(lhs.h,rhs.h);//使用HasPtr版本的swap
//交換類型Foo的其他成員
}
每個swap調用應該都是未加限定的。即,每個調用都應該是swap,而不是std::swap。如果存在類型特定的swap版本,其匹配程度會優于std中定義的版本。因此,如果存在類型特定的swap版本,swap調用會與之匹配。如果不存在類型特定的版本,則會使用std中的版本(假定作用域中有using聲明)。
非常代細的讀者可能會奇怪為什么swap函數中的using聲明沒有隱藏HRasPtr版本swap的聲明。
在賦值運算符中使用swap
定義swap的類通常用swap來定義它們的賦值運算符。這些運算符使用了一種名為拷貝并交換(copy and swap)的技術。這種技術將左側運算對象與右側運算對象的一個副本進行交換:
//注意rhs是按值傳遞的,意味著HasPtr的拷貝構造函數
//將右側運算對象中的string拷貝到rhs
HasPtr& HasPtr::operator=(HasPtr rhs)
{
//交換左側運算對象和局部變量rhs的內部
swap(*this,rhs);//rhs現在指向本對象曾經使用的內存
return*this;//rhs被銷毀,從而delete了rhs中的指針
}
在這個版本的賦值運算符中,參數并不是一個引用,我們將右側運算對象以傳值方式傳遞給了賦值運算符。因此,rhs是右側運算對象的一個副本。參數傳遞時拷貝HasPtr的操作會分配該對象的string的一個新副本。
在賦值運算符的函數體中,我們調用swap來交換hs和this中的數據成員。這個調用將左側運算對象中原來保存的指針存入rhs中,并將zhs中原來的指針存入this中。因此,在swap調用之后,*this中的指針成員將指向新分配的string一一右側運算對象中string的一個副本。
當賦值運算符結束時,rhs被銷毀,HasPtr的析構函數將執行。此析構函數delete rhs現在指向的內存,即,釋放掉左側運算對象中原來的內存。
這個技術的有趣之處是它自動處理了自賦值情況且天然就是異常安全的。它通過在改變左側運算對象之前拷貝右側運算對象保證了自賦值的正確,這與我們在原來的賦值運算符中使用的方法是一致的。它保證異常安全的方法也與原來的賦值運算符實現一樣。代碼中唯一可能拋出異常的是拷貝構造函數中的new表達式。如果真發生了異常,它也會在我們改變左側運算對象之前發生。