今天遇到了個面試,其中有的問題我當(dāng)時還真不能確定,遂發(fā)出來,大家分享。
先大致講一下流程,一面還挺順利,游刃有余;二面就有些緊張了,是個額頭頭發(fā)不多但是顯得很精干的男士(下文簡稱為A)。
只摘錄其中的部分我很“為難”的地方:
A:string是值類型是引用類型?
ME:(我心想string是class,肯定是)引用類型
A:那我有個方法,參數(shù)為string,我在里面改變他的值,原來的會變嗎?
ME:(這個我當(dāng)時很猶豫,雖說string平時用,但是還真考慮過這個。我要是說會不會變吧,豈不是自打嘴巴?String是引用類型,怎么還值專遞呢?)
當(dāng)時我就記得園子里有句話:String是引用類型,但是用起來像值類型。我就說的是不變。
下面上一段代碼分析一下:
static void Foo(string s) { s = "bbb"; }
string s = "aaa"; Foo(s); Console.WriteLine(s); |
這個確實是不會變的,調(diào)用完之后還是“aaa”,這是為什么呢?
1 string s = "aaa"; 2 00000051 8B 05 88 20 C0 02 mov eax,dword ptr ds:[02C02088h] 3 00000057 89 45 B8 mov dword ptr [ebp-48h],eax 4 92: Foo(s); 5 0000005a 8B 4D B8 mov ecx,dword ptr [ebp-48h] 6 0000005d E8 A6 AF D4 FF call FFD4B008 7 00000062 90 nop 8 93: Console.WriteLine(s); 9 00000063 8B 4D B8 mov ecx,dword ptr [ebp-48h] 10 00000066 E8 95 24 3F 67 call 673F2500 11 12 13 14 15 16 static void Foo(string s) 17 82: { 18 00000000 55 push ebp 19 00000001 8B EC mov ebp,esp 20 00000003 57 push edi 21 00000004 56 push esi 22 00000005 53 push ebx 23 00000006 83 EC 30 sub esp,30h 24 00000009 33 C0 xor eax,eax 25 0000000b 89 45 F0 mov dword ptr [ebp-10h],eax 26 0000000e 33 C0 xor eax,eax 27 00000010 89 45 E4 mov dword ptr [ebp-1Ch],eax 28 00000013 89 4D C4 mov dword ptr [ebp-3Ch],ecx 29 00000016 83 3D E0 8C 7B 00 00 cmp dword ptr ds:[007B8CE0h],0 30 0000001d 74 05 je 00000024 31 0000001f E8 1D 91 57 68 call 68579141 32 00000024 90 nop 33 83: s = "bbb"; 34 00000025 8B 05 90 20 C0 02 mov eax,dword ptr ds:[02C02090h] 35 0000002b 89 45 C4 mov dword ptr [ebp-3Ch],eax 36 84: } 37 0000002e 90 nop 38 0000002f 8D 65 F4 lea esp,[ebp-0Ch] 39 00000032 5B pop ebx 40 00000033 5E pop esi 41 00000034 5F pop edi 42 00000035 5D pop ebp 43 00000036 C3 ret |
可以看到第2行將字符串的地址寫入到 eax,然后寫到堆棧的【ebp-48h】處;
調(diào)用Foo方法前,放到ecx中。
在方法Foo中,可以看到又經(jīng)ecx放到了【ebp-3Ch】處;
在執(zhí)行s=“bbb”的時候,同樣將新字符串的地址放到了【ebp-3Ch】處,但是原來的字符串并為更改,只是更改了臨時變量s的引用。
所以在調(diào)用完方法Foo之后,原來的字符串還是“aaa”,沒有改變。
所以這個時候我回答不變是對的,但是我不知道為什么string的傳遞是類似于值傳遞的,有點運氣了。
接下來,他又問
A:那如果我有個類,里面有string成員,我同樣改變他的值,外面的會變嗎?這個時候我回答的是可以改變。
是不是這樣呢?同樣,上代碼:
1 class C1 2 { 3 public string s1="aaa"; 4 } 5 6 static void Foo(C1 c1) 7 { 8 c1.s1 = "bbb"; 9 } 10 11 C1 c1 = new C1(); 12 Foo(c1); 13 Console.WriteLine(c1.s1 ); |
1 Foo(c1); 2 0000006c 8B 4D B8 mov ecx,dword ptr [ebp-48h] 3 0000006f E8 94 AF 7F FF call FF7FB008 4 00000074 90 nop 5 93: Console.WriteLine(c1.s1 ); 6 00000075 8B 45 B8 mov eax,dword ptr [ebp-48h] 7 00000078 8B 48 04 mov ecx,dword ptr [eax+4] 8 0000007b E8 80 24 52 67 call 67522500 9 10 11 12 static void Foo(C1 c1) 13 82: { 14 00000000 55 push ebp 15 00000001 8B EC mov ebp,esp 16 00000003 57 push edi 17 00000004 56 push esi 18 00000005 53 push ebx 19 00000006 83 EC 30 sub esp,30h 20 00000009 33 C0 xor eax,eax 21 0000000b 89 45 F0 mov dword ptr [ebp-10h],eax 22 0000000e 33 C0 xor eax,eax 23 00000010 89 45 E4 mov dword ptr [ebp-1Ch],eax 24 00000013 89 4D C4 mov dword ptr [ebp-3Ch],ecx 25 00000016 83 3D E0 8C 13 00 00 cmp dword ptr ds:[00138CE0h],0 26 0000001d 74 05 je 00000024 27 0000001f E8 AD 90 6A 68 call 686A90D1 28 00000024 90 nop 29 83: c1.s1 = "bbb"; 30 00000025 8B 05 90 20 D7 02 mov eax,dword ptr ds:[02D72090h] 31 0000002b 8B 4D C4 mov ecx,dword ptr [ebp-3Ch] 32 0000002e 8D 51 04 lea edx,[ecx+4] 33 00000031 E8 9A 16 45 68 call 684516D0 34 84: } 35 00000036 90 nop 36 00000037 8D 65 F4 lea esp,[ebp-0Ch] 37 0000003a 5B pop ebx 38 0000003b 5E pop esi 39 0000003c 5F pop edi 40 0000003d 5D pop ebp 41 0000003e C3 ret |
在執(zhí)行30行的時候eax是01DBC268,其內(nèi)存的內(nèi)容拷貝出來是:
54 0b a0 67 04 00 00 00 03 00 00 00 62 00 62 00 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
可以看出這是一個string的實例,前面的67a00b54是MT的地址,后面的00000004是字符串的實際長度,00000003是字符串有效內(nèi)容的長度,
后面的3個0062是連著三個字符‘b’,看來確實是字符串“bbb”。再后面00的就不管了。
接著依次執(zhí)行31和32行,則ecx是01D9EEC8,edx是01D9EECC;據(jù)猜測ecx應(yīng)該是c1的地址,把內(nèi)存考出來看一下:
d0 99 41 00 94 ee d9 01 00 00 00 00 24 43 9d 67 0a 00 00 00 70 07 a0 67 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00
|
而此時edx就應(yīng)該是s1的地址,可以看出edx就比ecx相差4,所以01d9ee94就應(yīng)該是字符串“aaa”的地址,同樣考出來看看:
54 0b a0 67 04 00 00 00 03 00 00 00 61 00 61 00 61 00 00 00 00 00 00
|
可以看出,“aaa”和“bbb”的頭幾個部分完全是一樣的,就是后面的一個是61,一個是62.
那么問題很簡單了,知道把c1里的字符串地址從01d9ee94換成01DBC268就算OK了。事實上33行就是做這個事情的。
看一下執(zhí)行完33行后的c1的內(nèi)容:
d0 99 41 00 68 c2 db 01 00 00 00 00 24 43 9d 67 0a 00 00 00 70 07 a0 67 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
可以看出,確實是換了。
所以到這里,問題解決了。
#p#
接著這老大又問
A:有沒有其他方法可以改變字符串?
ME:加ref或out關(guān)鍵字可以,或者用指針。
我們看一下加ref(或加out,其實是一樣的)的為什么可以改變,更詳細(xì)的看一下。
static void Foo(ref string s) { s= "bbb"; }
string s = "aaa"; Foo(ref s); Console.WriteLine(s ); |
繼續(xù)匯編:
1 string s = "aaa"; 2 0000004c 8B 05 88 20 ED 02 mov eax,dword ptr ds:[02ED2088h] 3 00000052 89 45 B8 mov dword ptr [ebp-48h],eax 4 92: Foo(ref s); 5 00000055 8D 4D B8 lea ecx,[ebp-48h] 6 00000058 E8 AB AF D0 FF call FFD0B008 7 0000005d 90 nop 8 93: Console.WriteLine(s ); 9 0000005e 8B 4D B8 mov ecx,dword ptr [ebp-48h] 10 00000061 E8 9A 24 49 67 call 67492500 11 12 13 14 static void Foo(ref string s) 15 82: { 16 00000000 55 push ebp 17 00000001 8B EC mov ebp,esp 18 00000003 57 push edi 19 00000004 56 push esi 20 00000005 53 push ebx 21 00000006 83 EC 30 sub esp,30h 22 00000009 33 C0 xor eax,eax 23 0000000b 89 45 F0 mov dword ptr [ebp-10h],eax 24 0000000e 33 C0 xor eax,eax 25 00000010 89 45 E4 mov dword ptr [ebp-1Ch],eax 26 00000013 89 4D C4 mov dword ptr [ebp-3Ch],ecx 27 00000016 83 3D E0 8C 6D 00 00 cmp dword ptr ds:[006D8CE0h],0 28 0000001d 74 05 je 00000024 29 0000001f E8 1D 91 61 68 call 68619141 30 00000024 90 nop 31 83: s= "bbb"; 32 00000025 8B 05 90 20 ED 02 mov eax,dword ptr ds:[02ED2090h] 33 0000002b 8B 4D C4 mov ecx,dword ptr [ebp-3Ch] 34 0000002e 8D 11 lea edx,[ecx] 35 00000030 E8 A3 0E 3C 68 call 683C0ED8 36 84: } 37 00000035 90 nop 38 00000036 8D 65 F4 lea esp,[ebp-0Ch] 39 00000039 5B pop ebx 40 0000003a 5E pop esi 41 0000003b 5F pop edi 42 0000003c 5D pop ebp 43 0000003d C3 ret |
同樣,關(guān)注代碼的32~34行:
eax:01DEC25C,內(nèi)容:
54 0b a0 67 04 00 00 00 03 00 00 00 62 00 62 00 62 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 00 |
確實是字符串“bbb”
ecx和edx都是:05C7E778,內(nèi)容:0x01dcee94,這個是字符串“aaa”的地址。
執(zhí)行完35行之后,地址05C7E778的內(nèi)容變成了01DEC25C,在之后第9行代碼確實地址變成了01DEC25C,則可以推斷05C7E778是上個堆棧
s引用的位置,則35行的代碼則是將新“bbb”的地址寫到原來的s引用處。
A繼續(xù)問:ref和out有什么區(qū)別?
ME:我說兩者沒什么區(qū)別,就是out不要求變量初始化。
A:那要是初始化了呢,改變了之后是什么值?
ME:(這個我還真被問住了。不知道可以,但是不能亂說啊。)基于對out這個關(guān)鍵字的理解,我認(rèn)為應(yīng)該返回改變后的值。
如果將原來的ref改為out,匯編代碼完全相似,區(qū)別就是變量是否初始化問題,如果不初始化,其實變量在棧中也是有位置的,只不過地址內(nèi)容為0.
如果初始化,則和ref完全一樣。代碼我就不貼了,大家可以自己調(diào)式看一看。
問題:為什么默認(rèn)的字符串作為參數(shù)傳遞是類似的值傳遞呢?請大家告訴我。
靠,弄了半天,才記得所有傳遞默認(rèn)都是值傳遞,這才是問題的根源。老了,腦袋記不清了,以前看C語言的時候還特別注意了這點,結(jié)果還是忘記了。
問題的答案請看我最下面的留言。
在這里有些誤導(dǎo)大家了,給大家致歉。
【編輯推薦】
- 求職者看面試官:和不懂技術(shù)的人談技術(shù)
- 思科認(rèn)證CCIE考試介紹:費用及實驗面試等
- 面試官:我如何招到聰明又能做事的人