pin_ptr —— 定身法
千萬不要小看了pin_ptr的能力,它是Native世界和Managed世界之間的橋樑。在通常情況下,任何時候,GC都會啓動,一旦進行GC,託管堆就會被壓縮,對象的位置就會被移動,這時候所有指向對象的Handle都會被更新。但是,往往有時候程序員會希望能夠把託管堆上的數據(的地址)傳給Native接口,比如,爲了複用一個Native的高效算法,或者爲了高效的做某些其它事情,這種情況下普通的Native指針顯然不能勝任,因爲如果允許Native指針指向託管堆上的對象,那麼一旦發生了GC,這些得不到更新的Native指針將指向錯誤的位置,造成嚴重的後果。辦法是先把對象“定”在Managed堆上,然後再把地址傳給Native接口,這個“定身法”就是pin_ptr——它告訴GC:在壓縮堆的時候請不要移動該對象!
arr[0] = 'C';
arr[1] = '+';
arr[2] = '+';
pin_ptr<char> p = &arr[0]; // 整個arr都被定在堆上
char* pbegin=p;
std::sort(pbegin,pbegin+3); //複用Native的算法!
std::cout<輸出 “++C”
在上面的代碼中,我們複用了STL裏的sort算法。事實上,既然有了pin_ptr,我們可以複用絕大部分的Native算法。這就爲我們構建一個緊湊高效的程序內核提供了途徑。
值得注意的是,一旦對象中的成員被定在了堆上,那麼該對象整個就被定在了堆上——這很好理解,因爲對象移動必然意味着其成員的移動。
還有另一個值得注意的地方就是:pin_ptr只能指向某些特定的類型如基本類型,值類型等。因爲這些類型的內存佈局都是特定的,所以對於Native代碼來說,通過Native指針訪問它們不會引起意外的後果。但是,ref class的內存佈局是動態的,CLR可以對它的佈局進行重整以做某些優化(如調整數據成員排布以更好的利用空間),從而不再是Native世界所能理解的靜態結構。然而,這裏最主要的問題還是:ref class底層的對象模型和Native世界的對象模型根本就不一致(比如vtbl的結構和vptr的位置),所以用Native指針來接受一個ref class實例的地址並調用它的方法簡直肯定是一種災難。由於這個原因,編譯器嚴格禁止pin_ptr指向ref class的實例。
interior_ptr —— 託管環境下的Native指針
Handle的缺憾是不能進行指針運算(由於其固有的語義要求,畢竟Handle面對的是一個要求“安全”的託管環境),所以Handle的能力較爲有限,不如標準C++程序員所熟悉的Native指針那麼強大。在STL中,iterator是一種極爲強大也極具效率的工具,其底層實現往往用到Native指針。而到了託管堆上,我們還有Native指針嗎?當然,原來的形如T*的指針是不能再用了,因爲它不能跟蹤託管堆上對象的移動。所以C++/CLI中引入了一種新的指針形式——interior_ptr。interior_ptr和Native指針的語義幾乎完全一樣,只不過interior_ptr指向託管堆,在GC時interior_ptr能夠得到更新,除此之外,interior_ptr允許你進行指針運算,允許你解引用,一切和Native指針並無二致。interior_ptr爲你操縱託管堆上的數據序列(如array)提供了強大而高效的工具,iterator模式因此可以原版照搬到託管環境中,例如:
template<typename T>
void sort2(interior_ptr begin,interior_ptr end)
{
... //排序算法
for(interior_ptr pn=begin;pn!=end;++pn)
{
System::Console::WriteLine(*pn);
}
}
int main()
{
array<char>^ arr = gcnew array<char>(3);
... //賦值
interior_ptr<char> begin = &arr[0]; //指向頭部的指針
interior_ptr<char> end = begin + 3; //注意,不能寫&arr[3],會下標越界
sort2(begin,end); //類似STL的排序方式!
}
T*,pin_ptr,interior_ptr——把它們放到一起
T*,pin_ptr,interior_ptr是C++/CLI中三種最爲重要的指針形式。它們之間的關係像這樣:
強大的Override機制
在標準C++中,虛函數重寫機制是隱式的,只要兩個函數的簽名(Signature)一樣,並且基類的同名函數爲虛函數,那麼不管派生類的函數是否爲virtual,都會發生虛函數重寫。某種程度上,這就限制了用戶對它的派生類的控制能力——虛函數的版本問題就是其一。而在C++/CLI中,你擁有最爲強大的override機制,你可以更爲明顯的來表示你的意圖,例如下面的代碼:
class B
{
public:
virtual void f() ;
virtual void g() abstract; //純虛函數,需要派生類重寫,否則派生類就是純虛類
virtual void h() sealed; //阻止派生類重寫該函數
virtual void i() ;
}
class D:public B
{
virtual void f() new ; //新版本的f,雖然名字和B::f相同,但是並沒有重寫B::f。
virtual void h() override ; //錯誤!sealed函數不能被重寫
virtual void k() = B::i ; //“命名式”重寫!
}
通過正確的使用這些強大的override機制,你可以獲得對類成員函數更強大的描述能力,避免出乎意料的隱式重寫和版本錯誤。不過需要提醒的是,“命名式”重寫是一種強大的能力,但是需要謹慎使用,如果使用不當或濫用很可能導致名字錯亂。