常見面試題目:
1. 值型別和參考型別的區別?
2. 結構和類的區別?
3. delegate是參考型別還是值型別?enum、int[]和string呢?
4. 堆和堆疊的區別?
5. 什么情況下會在堆(堆疊)上分配資料?它們有性能上的區別嗎?
6.“結構”物件可能分配在堆上嗎?什么情況下會發生,有什么需要注意的嗎?
7. 理解引數按值傳遞?以及按參考傳遞?
8. out 和 ref 的區別與相同點?
9. C#支持哪幾個預定義的值型別?C#支持哪些預定義的參考型別?
10. 有幾種方法可以判定值型別和參考型別?
11. 說說值型別和參考型別的生命周期?
12. 如果結構體中定義參考型別,物件在記憶體中是如何存盤的?例如下面結構體中的class類 User物件是存盤在堆疊上,還是堆上?
public struct MyStruct
{
public int Index;
public User User;
認識值型別與參考型別
萬變不離其宗,只要搞清楚值型別和參考型別的原理,上面所有題目就都迎刃而解了,
基本概念
CLR支持兩只型別:參考型別和值型別,這是.NET語言的基礎和關鍵,他們從型別定義、實體創建、引數傳遞,到記憶體分配都有所不同,雖然看上去簡單,但真正理解其內涵的人卻好像并不多,
圖片參考
下圖清晰了展示了.NET中型別分類,值型別主要是一些簡單的、基礎的資料型別,參考型別主要用于更豐富的、復雜的、復合的資料型別,
記憶體結構
值型別和參考型別最根源的區別就是其記憶體分配的差異,在這之前首先要理解CLR的記憶體中兩個重要的概念:
Stack 堆疊:執行緒堆疊,由作業系統管理,存放值型別、參考型別變數(就是參考物件在托管堆上的地址),堆疊是基于執行緒的,也就是說一個執行緒會包含一個執行緒堆疊,執行緒堆疊中的值型別在物件作用域結束后會被清理,效率很高,
GC Heap托管堆:行程初始化后在行程地址空間上劃分的記憶體空間,存盤.NET運行程序中的物件,所有的參考型別都分配在托管堆上,托管堆上分配的物件是由GC來管理和釋放的,托管堆是基于行程的,當然托管堆內部還有其他更為復雜的結構,有興趣的可以深入了解,
結合下圖理解,變數a及其值3都是存盤在堆疊上面,變數b在堆疊上存盤,其值指向字串“123”的托管堆物件地址(字串是參考型別,字串物件是存盤在托管堆上面,字串是一個特殊的參考型別,后面文章會專門探討)”
值型別一直都存盤在堆疊上面嗎?所有的參考型別都存盤在托管堆上面嗎?
1.單獨的值型別變數,如區域值型別變數都是存盤在堆疊上面的;
2.當值型別是自定義class的一個欄位、屬性時,它隨參考型別存盤在托管堆上,此時她是參考型別的一部分;
4.所有的參考型別肯定都是存放在托管堆上的,
5.還有一種情況,同上面題目12,結構體(值型別)中定義參考型別欄位,結構體是存盤在堆疊上,其參考變數欄位只存盤記憶體地址,指向堆中的參考實體,
物件的傳遞
將值型別的變數賦值給另一個變數(或者作為引數傳遞),會執行一次值復制,將參考型別的變數賦值給另一個參考型別的變數,它復制的值是參考物件的記憶體地址,因此賦值后就會多個變數指向同一個參考物件實體,理解這一點非常重要,下面代碼測驗驗證一下:
int v1 = 0;
int v2 = v1;
v2 = 100;
Console.WriteLine("v1=" + v1); //輸出:v1=0
Console.WriteLine("v2=" + v2); //輸出:v2=100
User u1=new User();
u1.Age = 0;
User u2 = u1;
u2.Age = 100;
Console.WriteLine("u1.Age=" + u1.Age); //輸出:u1.Age=100
Console.WriteLine("u2.Age=" + u2.Age); //輸出:u2.Age=100,因為u1/u2指向同一個物件
當把物件作為引數傳遞的時候,效果同上面一樣,他們都稱為按值傳遞,但因為值型別和參考型別的區別,導致其產生的效果也不同,
引數-按值傳遞:
private void DoTest(int a)
{
a *= 2;
}
private void DoUserTest(User user)
{
user.Age *= 2;
}
[NUnit.Framework.Test]
public void DoParaTest()
{
int a = 10;
DoTest(a);
Console.WriteLine("a=" + a); //輸出:a=10
User user = new User();
user.Age = 10;
DoUserTest(user);
Console.WriteLine("user.Age=" + user.Age); //輸出:user.Age=20
}
上面的代碼示例,兩個方法的引數,都是按值傳遞
- 對于值型別(int a) :傳遞的是變數a的值拷貝副本,因此原本的a值并沒有改變,
- 對于參考型別(User user) :傳遞的是變數user的參考地址(User物件實體的記憶體地址)拷貝副本,因此他們操作都是同一個User物件實體,
引數-按參考傳遞:
按參考傳遞的兩個主要關鍵字:out 和 ref不管值型別還是參考型別,按參考傳遞的效果是一樣的,都不傳遞值副本,而是參考的參考(類似c++的指標的指標),out 和 ref告訴編譯器方法傳遞額是引數地址,而不是引數本身,理解這一點很重要,
代碼簡單測驗一下,如果換成out效果是相同的
private void DoTest( ref int a)
{
a *= 2;
}
private void DoUserTest(ref User user)
{
user.Age *= 2;
}
[NUnit.Framework.Test]
public void DoParaTest()
{
int a = 10;
DoTest(ref a);
Console.WriteLine("a=" + a); //輸出:a=20 ,a的值改變了
User user = new User();
user.Age = 10;
DoUserTest(ref user);
Console.WriteLine("user.Age=" + user.Age); //輸出:user.Age=20
}
out 和 ref的主要異同:
out和ref都指示編譯器傳遞引數地址,在行為上是相同的;- 他們的使用機制稍有不同,ref要求引數在使用之前要顯式初始化,out要在方法內部初始化;
out和ref不可以多載,就是不能定義Method(ref int a)和Method(out int a)這樣的多載,從編譯角度看,二者的實質是相同的,只是使用時有區別;
常見問題
題目答案決議:
1. 值型別和參考型別的區別?
值型別包括簡單型別、結構體型別和列舉型別,參考型別包括自定義類、陣列、介面、委托等,
- 1、賦值方式:將一個值型別變數賦給另一個值型別變數時,將復制包含的值,這與參考型別變數的賦值不同,參考型別變數的賦值只復制物件的參考(即記憶體地址,類似C++中的指標),而不復制物件本身,
- 2、繼承:值型別不可能派生出新的型別,所有的值型別均隱式派生自 System.ValueType,但與參考型別相同的是,結構也可以實作介面,
- 3、null:與參考型別不同,值型別不可能包含 null 值,然而,可空型別允許將 null 賦給值型別(他其實只是一種語法形式,在clr底層做了特殊處理),
- 4、每種值型別均有一個隱式的默認建構式來初始化該型別的默認值,值型別初始會默認為0,參考型別默認為null,
- 5、值型別存盤在堆疊中,參考型別存盤在托管堆中,
2. 結構和類的區別?
結構體是值型別,類是參考型別,主要區別如題1,其他的區別:
- 結構不支持無慘建構式,不支持解構式,并且不能有protected修飾;
- 結構常用于資料存盤,類class多用于行為;
- class需要用new關鍵字實體化物件,struct可以不適用new關鍵字;
- class可以為抽象類,struct不支持抽象;
3. delegate是參考型別還是值型別?enum、int[]和string呢?
enum列舉是值型別,其他都是參考型別,
4. 堆和堆疊的區別?
執行緒堆疊:簡稱堆疊 Stack 托管堆: 簡稱堆 Heap
- 值型別大多分配在堆疊上,參考型別都分配在堆上;
- 堆疊由作業系統管理,堆疊上的變數在其作用域完成后就被釋放,效率較高,但空間有限,堆受CLR的GC控制;
- 堆疊是基于執行緒的,每個執行緒都有自己的執行緒堆疊,初始大小為1M,堆是基于行程的,一個行程分配一個堆,堆的大小由GC根據運行情況動態控制;
6.“結構”物件可能分配在堆上嗎?什么情況下會發生,有什么需要注意的嗎?
結構是值型別,有兩種情況會分配在對上面:
- 結構作為class的一個欄位或屬性,會隨class一起分配在堆上面;
- 裝箱后會在堆中存盤,盡量避免值型別的裝箱,值型別的拆箱和裝箱都有性能損失,下一篇會重點關注;
7. 理解引數按值傳遞?以及按參考傳遞?
- 按值傳遞:對于值型別傳遞的它的值拷貝副本,而參考型別傳遞的是參考變數的記憶體地址,他們還是指向的同一個物件,
- 按參考傳遞:通過關鍵字out和ref傳遞引數的記憶體地址,值型別和參考型別的效果是相同的,
8. out 和 ref的區別與相同點?
out和ref都指示編譯器傳遞引數地址,在行為上是相同的;- 他們的使用機制稍有不同,ref要求引數在使用之前要顯式初始化,out要在方法內部初始化;
out和ref不可以多載,就是不能定義Method(ref int a)和Method(out int a)這樣的多載,從編譯角度看,二者的實質是相同的,只是使用時有區別;
9. C#支持哪幾個預定義的值型別?C#支持哪些預定義的參考型別?
值型別:整數、浮點數、字符、bool和decimal
參考型別:Object,String
10. 有幾種方法可以判定值型別和參考型別?
簡單來說,繼承自System.ValueType的是值型別,反之是參考型別,
11. 說說值型別和參考型別的生命周期?
值型別在作用域結束后釋放,
參考型別由GC垃圾回收期回收,這個答案可能太簡單了,更詳細的答案在后面的文章會說到,
12. 如果結構體中定義參考型別,物件在記憶體中是如何存盤的?例如下面結構體中的class類 User物件是存盤在堆疊上,還是堆上?
public struct MyStruct
{
public int Index;
public User User;
}
MyStruct存盤在堆疊中,其欄位User的實體存盤在堆中,MyStruct.User欄位存盤指向User物件的記憶體地址,
轉載請註明出處,本文鏈接:https://www.uj5u.com/net/93258.html
標籤:C#
