摘要
這個系列是本人在工作或工作之余開發和學習C\C++的一些筆記。本文涉及C++/CLI的一些內容。
本文為原創,首發於我的個人博客:.NET程序員的C\C++情結(3)。歡迎交流指正。轉載請注明出處。
雖然現在主要從事.NET平台的開發,但是一直以來對C\C++有着那份難以割舍的情結。本文會涉及到托管C++的一些隨筆記錄。
當然,如果寫純.NET應用的話,C#無疑是最合適的語言的。但是托管C++在同時處理Native調用和托管調用上無疑是十分吸引人的,往往用來作為托管世界和Native世界的橋梁。當然。你可以說用.NET的“平台調用”特性同樣能夠勝任,蘿卜青菜各有所愛吧。
托管C++基礎語言特性
在托管C++中需要像下面這樣定義一個托管類型
{
public :
property UInt32 FieldId;
}
默認情況下這樣的類是默認實現IDisposable的,原因很簡單,既然用到C++來封裝托管類型,那么八成類型需要涉及到非托管對象,實現IDisposable減少了出錯的可能。可以同時實現兩種“析構函數”:
~ ARSession( void)
前者是好比Dispose(),后者是C++原生的析構函數。
可以同時引用托管的命名空間和C++命名空間
using namespace System :: Collections :: Generic;
using namespace std;
也可以向普通C++一樣#include頭文件,編譯的過程可以理解成跟本地C++的編譯過程一樣,只是在編譯的時候會有/clr開關,並至少引用相應的托管dll:mscorlib.dll
對於托管類型,在類型的標識右使用”^”標注,比如:
array < String ^>^
List < AREntry ^>^
但注意,對於Nullable的值類型,使用
而不是
前者在C#中會看到是uint?,而后者在C#中會看到是ValueType
托管C++支持類似C#中的ref
out的話需要加一個Attribute
void foo ([ Out ] Bar ^% x);
在本地堆中申請內存是使用new關鍵字,而在托管堆中申請內存,使用gcnew關鍵字:
托管C++的內存管理
上面簡單介紹的一些語言特性是我實際碰到的,可能不全。與語言特性相比,更為重要的是內存管理帶來的復雜性。原生的C++只有一個由C運行庫管理的“本地堆”,而C++/CLI允許同時操作本地堆和托管堆。眾所周知,托管堆由CLR管理,在托管堆中的內存會隨時被CLR回收和壓縮,這意味着,如果使用C#的引用或者C++/CLI中的“Handle”(即由String^等“戴帽子的類型“聲明的變量)來操作托管堆的內存,不會有任何問題,因為CLR會自動更改引用或Handle指向的地址。然而,如果在本地堆或者棧上的本地指針來指向托管堆上的內存的話,CLR不會對壓縮內存帶來的地址修改負任何責任。如果發生這種情況的話,再次使用該指針將導致內存違規。下面這張圖可以解釋這個現象(圖片來源http://www.codeproject.com/Articles/17817/C-CLI-in-Action-Using-interior-and-pinning-pointer):
在上圖中,本地指針指向的地址本來是Data,但是當CLR的GC工作后,Data可能被壓縮至托管堆的其他地方,而取而代之的是另外一塊內存。很典型的情況就是,我們要在托管的byte[]和非托管的usigned char*對象之間傳遞內存,下面這段代碼將String對象轉化成以UTF8編碼的字節數組:
{
if( String :: IsNullOrEmpty( Source))
return NULL;
array < Byte >^ vText = System :: Text :: Encoding :: UTF8 -> GetBytes( Source);
pin_ptr < unsigned char > pText = & vText [ 0 ];
char * Des = ( char *) calloc( vText -> Length + 1 , sizeof( char));
memcpy( Des , pText , vText -> Length);
Des [ vText -> Length ] = '\0';
return Des;
}
上述代碼實際上是將托管堆中的一部分內存數據copy到非托管堆,使其奏效的關鍵就是pin_ptr<unsigned char>這個指針了。
在托管C++中也可以使用如下方法代替上面的實現:
但是,似乎在轉換過程中是以ANSI編碼來轉換的,具體沒有詳細研究。不過marshal_as是可以擴展的,詳見:http://msdn.microsoft.com/zh-cn/library/bb384865.aspx
C++運行庫的問題
在開發過程中碰到一個很怪異的_CrtIsValidHeapPointer錯誤,關於這個問題,需要了解Microsoft C運行庫以及其管理堆內存的一些原則:
首先,到目前為止,Microsoft C運行庫實際上已經有很多版本了,在應用程序執行期間,很可能在內存中存在多個版本的C運行庫,而且每個C運行庫版本維護自己的堆,這樣,如果在不同的運行庫之間引用堆內存,那么在Debug模式下會有一個_CrtIsValidHeapPointer宏來防止這個操作(Release模式沒有驗證過是不是就沒有這個限制了)。那么典型的場景就是,當我們在引用某個第三方動態鏈接庫時,如果這個第三方的動態鏈接庫所引用的C運行庫跟我們的主程序不一致,那么將會在內存中同時存在兩個版本的運行庫,所以,如果主程序申請的堆內存,由其他dll來釋放,那么就會報錯。所以,所謂的“誰申請誰釋放”的原則在這里實際上也是適用的。上面這個錯誤就是在Debug模式下,幫助開發人員發現這種跨運行庫的heap的指針引用的問題,尚不知道這種引用是否完全不合法,還是僅僅只有風險。
另外,如果以靜態鏈接的方式鏈接到C運行庫的話,即使是同一個版本的運行庫,在內存中也存在兩份copy,並有兩塊由不同運行庫維護的堆內存。
從上述這點看來,如果要自己開發一個dll的話,記得要提供堆內存釋放的函數,以避免出現不同運行庫的沖突。
C++模板
老是說C++的模板真心比C#的泛型在語言層面要復雜的多,使用模板並不難,但是要自己設計模板類,就出問題了。這里簡單總結一些模板的基礎。
模板類的聲明如下:
public class IntelligentARStructAR
{
private :
T _Struct;
public :
~ IntelligentARStructAR();
}
模板類的實現(定義):
模板類的具化:
編譯器在編譯過程中,需要等模板在源代碼中使用的時候,才會生成一個對應的類型,這個過程叫模板類的具化。
編譯器要生成一個模板的定義,必須同時能看到模板的聲明、模板的定義以及模板的具化要素,如果編譯器在編譯階段不能具化,那么只能寄希望於鏈接器。
來看個典型的錯誤:
- template.h:里面有模板的聲明
- template.cpp:include template.h,里面有模板的實現(定義)
- main.cpp:include template.h,里面有使用模板(即模板具化的要素)
編譯器在編譯template.cpp時,同時看到了模板聲明和模板定義,但是因為沒有模板的具化要素,編譯器無法生成模板類型(因為,在沒有要素的情況下,不可能知道T這個類型的結構大小,也就無法生成二進制代碼);在編譯main.cpp,能夠看到的是模板的聲明和模板的具化要素,但沒有模板的定義,於是無法編譯通過。
這個典型的使用就是:C++編譯器不能支持對模板的分離式編譯的原因。
解決這個問題的方法有如下幾種:
- 在具化要素時,讓編譯器看到模板定義。典型的方式是將模板的聲明和定義同時寫在頭文件中。
- 用另外的編譯單元中顯示的具化。在另一個cpp文件中顯示的使用模板,這樣鏈接器能夠在鏈接階段找到模板類型。
- export關鍵字。據說還沒有編譯器實現。