C#值類型和引用類型用C語言理解
我剛用C#一個來月,可能理解得不對,還請大家指教。
讀懂文章你需要對C語言的指針有所理解。
需要注意區(qū)別:對CC++來說,任何類型都可以當(dāng)成C#的“引用類型”,因為有指針。
【在內(nèi)存上】
void?foo() { ????int?aaa?=?0;?//值類型,aaa在Stack上分配(SUB?ESP,XX) ????int*?paaa?=?new?int[123];?//引用類型,paaa在Heap上分配,HeapAlloc(GetProcessHeap()...) ????foo2(&aaa);?//【引用】值類型aaa }->函數(shù)退出,aaa從棧上銷毀,paaa依然存在在進(jìn)程堆中。 void?foo2(int*?paaa) { ????*paaa?=?123; }
【Box和UnBox】
打個很簡單的比方:
有函數(shù):
void?foo(int*?pa) { ????beginthread(&new_thread...,pa);?//啟動新線程操作指針pa } void?new_thread(int*?pa) { ????Sleep(1000);?//新線程睡眠1秒 ????if?(*pa?==?123)?//判斷pa指針的內(nèi)容是123 ????????OutputDebugStringA("OK.");???? }
還有函數(shù):
void?exec1() { ????int?pa?=?123; ????foo(&pa); }
大家可能看出問題了,當(dāng)我們執(zhí)行foo指向棧上的pa的時候,因為exec1函數(shù)已經(jīng)退出,pa的值被系統(tǒng)回收,不可預(yù)料,線程體在睡眠一秒后去訪問這個棧上的地址可能得到的值不是123,但是如果我們在堆上分配:
void?exec2() { ????int*?pa?=?(int*)malloc(sizeof(int)); ????*pa?=?123; ????foo(pa); }
執(zhí)行exec2,就可以確保pa不會被回收,因為沒有顯式free。
C#同理,Box就是把在exec1中的值類型pa進(jìn)行exec2的處理,然后執(zhí)行處理后pa的值在exec1退出后依然在棧上銷毀,但是執(zhí)行foo給進(jìn)去的pa是一個.NET通過類似malloc這種分配堆內(nèi)存的方法分配的一個新的指針,并且把原來棧上的值pa給memcpy進(jìn)去了,并且把這個指針交給了foo函數(shù),這個【指針】就是被裝箱(Box)后的【引用類型】,此時foo引用了pa,并且把它又交給new_thread,new_thread操作完了pa后,再也沒人使用這個引用類型pa,GC會把pa進(jìn)行free。
我們可以得出,棧類型在函數(shù)體退出后,會被系統(tǒng)自動釋放,而堆類型不會,還記得ATL的CComPtr么,而堆數(shù)據(jù)則要靠GC進(jìn)行垃圾回收。
如果你對Windows COM和C++有理解,可以這樣想象:
ATL::CComPtr = 值類型auto-free。為什么,因為CComPtr在函數(shù)體內(nèi)構(gòu)造(AddRef),在函數(shù)體退出析構(gòu)(Release)。
std::shared_ptr = 引用類型auto-free。類是new來的,shared_ptr進(jìn)行引用計數(shù)管理,在某個函數(shù)體退出時引用計數(shù)跑0,才進(jìn)行delete。
然后我們還可以得出,值類型指向?qū)嶋H的數(shù)據(jù),而引用類型指向數(shù)據(jù)指針。
這個你用后腦就可以想出來了,例子:
void?foo() { ????int?aa?=?123;?//aa直接指向123,編譯器通過ESP指針進(jìn)行間接訪問 ????int*?paa?=?(int*)malloc(sizeof(int));?//paa指向堆上的一塊內(nèi)存 ????*paa?=?123;?//編譯器使用直接指針訪問 }
所以說,C#中,值類型還是棧變量,引用類型則是shared_ptr。
反之,引用到值的拆箱:
void?foo() { ????int*?pa?=?new?int[123];?//引用類型 ????pa[0]?=?123; ????int?aa?=?*pa;?//aa是棧上的值類型,從堆上把數(shù)據(jù)進(jìn)行memcpy,UnBox }
所以這2個過程是即浪費時間又浪費內(nèi)存的。
從上面我們可以看出,值類型裝箱到引用類型,裝箱后,值類型改變了,被裝箱的引用類型不會改變,反之同理,比如:
void?foo() { ????int?aa?=?123;?//值類型 ????int*?pa?=?new?int[123];?//引用類型 ????pa[0]?=?aa;?//堆上的引用類型數(shù)據(jù)改了 ????aa?=?456;?//棧上的值類型變了,但堆上的數(shù)據(jù)可不會同步變,這可不是WPF的數(shù)據(jù)綁定,哈哈 }
用C#代碼:
private?void?Form1_Load(object?sender,?EventArgs?e) { ????????????int?aaa?=?123;?//Value?Type ????????????IntPtr?paaa?=?System.Runtime.InteropServices.Marshal.AllocHGlobal(sizeof(int));?//Ref?Type ????????????System.Runtime.InteropServices.Marshal.WriteInt32(paaa,aaa);?//Box ????????????aaa?=?System.Runtime.InteropServices.Marshal.ReadInt32(paaa);?//UnBox }
【繼承關(guān)系】
C#的玩意兒都繼承System.Object,棧上的值類型繼承System.ValueType,這是編譯器定義的隱性繼承。
也就是這樣:
void?foo() { ????int?aaa?=?123; ????System.Diagnostics.Debug.WriteLine(((aaa?as?ValueType)?as?object).ToString()); }
于是可以這樣:
private?bool?IsValueType(object?o) { ????????????return?o?is?ValueType???true?:?false; } private?void?Form1_Load(object?sender,?EventArgs?e) { ????????????int?aaa?=?123; ????????????System.Diagnostics.Debug.WriteLine(IsValueType(aaa).ToString());?//True ????????????var?bc?=?new?Form(); ????????????System.Diagnostics.Debug.WriteLine(IsValueType(bc).ToString());?//False }
【好玩的東西】
值類型的引用類型:
大家知道,值類型使用棧內(nèi)存,在C++中,我們有棧上類(就是RIIA,資源分配即初始化),但是在C#中,所有類都必需使用new進(jìn)行堆上申請,由GC進(jìn)行統(tǒng)一管理。
比如在DirectShow基本類里面的自動臨界區(qū)類CCritSec,就像C#里面的lock關(guān)鍵字一樣。
棧的特征就是分配->初始化:
int aa = 123;
退出自動銷毀。
這個RIIA范式在C++程序員手里那是用得爽。
C#下,因為一切引用類型都是在堆上跑,我們?nèi)绻鱿馬IIA這樣的東西怎么辦?
可能你已經(jīng)想到了,using關(guān)鍵字和IDisposable。
這個我就不多說了,自己搜索一下就知道了。