今天遇到了个面试,其中有的问题我当时还真不能确定,遂发出来,大家分享。
先大致讲一下流程,一面还挺顺利,游刃有余;二面就有些紧张了,是个额头头发不多但是显得很精干的男士(下文简称为A)。
只摘录其中的部分我很“为难”的地方:
A:string是值类型是引用类型?
ME:(我心想string是class,肯定是)引用类型
A:那我有个方法,参数为string,我在里面改变他的值,原来的会变吗?
ME:(这个我当时很犹豫,虽说string平时用,但是还真考虑过这个。我要是说会不会变吧,岂不是自打嘴巴?String是引用类型,怎么还值专递呢?)
当时我就记得园子里有句话:String是引用类型,但是用起来像值类型。我就说的是不变。
下面上一段代码分析一下:
static void Foo(string s) { s = "bbb"; }
string s = "aaa"; Foo(s); Console.WriteLine(s); |
这个确实是不会变的,调用完之后还是“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】处;
调用Foo方法前,放到ecx中。
在方法Foo中,可以看到又经ecx放到了【ebp-3Ch】处;
在执行s=“bbb”的时候,同样将新字符串的地址放到了【ebp-3Ch】处,但是原来的字符串并为更改,只是更改了临时变量s的引用。
所以在调用完方法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 |
在执行30行的时候eax是01DBC268,其内存的内容拷贝出来是:
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是字符串有效内容的长度,
后面的3个0062是连着三个字符‘b’,看来确实是字符串“bbb”。再后面00的就不管了。
接着依次执行31和32行,则ecx是01D9EEC8,edx是01D9EECC;据猜测ecx应该是c1的地址,把内存考出来看一下:
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就应该是s1的地址,可以看出edx就比ecx相差4,所以01d9ee94就应该是字符串“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行就是做这个事情的。
看一下执行完33行后的c1的内容:
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关键字可以,或者用指针。
我们看一下加ref(或加out,其实是一样的)的为什么可以改变,更详细的看一下。
static void Foo(ref string s) { s= "bbb"; }
string s = "aaa"; Foo(ref s); Console.WriteLine(s ); |
继续汇编:
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 |
同样,关注代码的32~34行:
eax:01DEC25C,内容:
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,内容:0x01dcee94,这个是字符串“aaa”的地址。
执行完35行之后,地址05C7E778的内容变成了01DEC25C,在之后第9行代码确实地址变成了01DEC25C,则可以推断05C7E778是上个堆栈
s引用的位置,则35行的代码则是将新“bbb”的地址写到原来的s引用处。
A继续问:ref和out有什么区别?
ME:我说两者没什么区别,就是out不要求变量初始化。
A:那要是初始化了呢,改变了之后是什么值?
ME:(这个我还真被问住了。不知道可以,但是不能乱说啊。)基于对out这个关键字的理解,我认为应该返回改变后的值。
如果将原来的ref改为out,汇编代码完全相似,区别就是变量是否初始化问题,如果不初始化,其实变量在栈中也是有位置的,只不过地址内容为0.
如果初始化,则和ref完全一样。代码我就不贴了,大家可以自己调式看一看。
问题:为什么默认的字符串作为参数传递是类似的值传递呢?请大家告诉我。
靠,弄了半天,才记得所有传递默认都是值传递,这才是问题的根源。老了,脑袋记不清了,以前看C语言的时候还特别注意了这点,结果还是忘记了。
问题的答案请看我最下面的留言。
在这里有些误导大家了,给大家致歉。
【编辑推荐】
- 求职者看面试官:和不懂技术的人谈技术
- 思科认证CCIE考试介绍:费用及实验面试等
- 面试官:我如何招到聪明又能做事的人